mirror of
https://github.com/zed-industries/zed.git
synced 2024-12-27 10:34:53 +03:00
Merge branch 'main' into elevations
This commit is contained in:
commit
b0ddbeb0ad
1
.github/workflows/ci.yml
vendored
1
.github/workflows/ci.yml
vendored
@ -56,6 +56,7 @@ jobs:
|
|||||||
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
|
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
|
||||||
APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }}
|
APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }}
|
||||||
APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }}
|
APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }}
|
||||||
|
ZED_AMPLITUDE_API_KEY: ${{ secrets.ZED_AMPLITUDE_API_KEY }}
|
||||||
steps:
|
steps:
|
||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
run: |
|
run: |
|
||||||
|
33
.github/workflows/release_actions.yml
vendored
Normal file
33
.github/workflows/release_actions.yml
vendored
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
discord_release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Discord Webhook Action
|
||||||
|
uses: tsickert/discord-webhook@v5.3.0
|
||||||
|
with:
|
||||||
|
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
|
||||||
|
|
||||||
|
${{ github.event.release.body }}
|
||||||
|
```
|
||||||
|
amplitude_release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: "3.10.5"
|
||||||
|
architecture: "x64"
|
||||||
|
cache: "pip"
|
||||||
|
- run: pip install -r script/amplitude_release/requirements.txt
|
||||||
|
- run: python script/amplitude_release/main.py ${{ github.event.release.tag_name }} ${{ secrets.ZED_AMPLITUDE_API_KEY }} ${{ secrets.ZED_AMPLITUDE_SECRET_KEY }}
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -9,3 +9,4 @@
|
|||||||
/assets/themes/*.json
|
/assets/themes/*.json
|
||||||
/assets/themes/internal/*.json
|
/assets/themes/internal/*.json
|
||||||
/assets/themes/experiments/*.json
|
/assets/themes/experiments/*.json
|
||||||
|
**/venv
|
1546
Cargo.lock
generated
1546
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -3,6 +3,11 @@ members = ["crates/*"]
|
|||||||
default-members = ["crates/zed"]
|
default-members = ["crates/zed"]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
|
[workspace.dependencies]
|
||||||
|
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||||
|
serde_json = { version = "1.0", features = ["preserve_order", "raw_value"] }
|
||||||
|
rand = { version = "0.8" }
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "366210ae925d7ea0891bc7a0c738f60c77c04d7b" }
|
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "366210ae925d7ea0891bc7a0c738f60c77c04d7b" }
|
||||||
async-task = { git = "https://github.com/zed-industries/async-task", rev = "341b57d6de98cdfd7b418567b8de2022ca993a6e" }
|
async-task = { git = "https://github.com/zed-industries/async-task", rev = "341b57d6de98cdfd7b418567b8de2022ca993a6e" }
|
||||||
@ -21,3 +26,4 @@ split-debuginfo = "unpacked"
|
|||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
debug = true
|
debug = true
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# syntax = docker/dockerfile:1.2
|
# syntax = docker/dockerfile:1.2
|
||||||
|
|
||||||
FROM rust:1.62-bullseye as builder
|
FROM rust:1.64-bullseye as builder
|
||||||
WORKDIR app
|
WORKDIR app
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# syntax = docker/dockerfile:1.2
|
# syntax = docker/dockerfile:1.2
|
||||||
|
|
||||||
FROM rust:1.62-bullseye as builder
|
FROM rust:1.64-bullseye as builder
|
||||||
WORKDIR app
|
WORKDIR app
|
||||||
RUN --mount=type=cache,target=/usr/local/cargo/registry \
|
RUN --mount=type=cache,target=/usr/local/cargo/registry \
|
||||||
--mount=type=cache,target=./target \
|
--mount=type=cache,target=./target \
|
||||||
|
@ -1,4 +0,0 @@
|
|||||||
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5 11C5 14.3137 7.68629 17 11 17C14.3137 17 17 14.3137 17 11C17 7.68629 14.3137 5 11 5C7.68629 5 5 7.68629 5 11ZM11 3C6.58172 3 3 6.58172 3 11C3 15.4183 6.58172 19 11 19C15.4183 19 19 15.4183 19 11C19 6.58172 15.4183 3 11 3Z" fill="white"/>
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.09092 8.09088H14.6364L10.5511 12.4545H12.4546L13.9091 13.9091H7.36365L11.7273 9.54543H9.54547L8.09092 8.09088Z" fill="white"/>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 571 B |
@ -3,8 +3,12 @@
|
|||||||
{
|
{
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"up": "menu::SelectPrev",
|
"up": "menu::SelectPrev",
|
||||||
|
"pageup": "menu::SelectFirst",
|
||||||
|
"shift-pageup": "menu::SelectFirst",
|
||||||
"ctrl-p": "menu::SelectPrev",
|
"ctrl-p": "menu::SelectPrev",
|
||||||
"down": "menu::SelectNext",
|
"down": "menu::SelectNext",
|
||||||
|
"pagedown": "menu::SelectLast",
|
||||||
|
"shift-pagedown": "menu::SelectFirst",
|
||||||
"ctrl-n": "menu::SelectNext",
|
"ctrl-n": "menu::SelectNext",
|
||||||
"cmd-up": "menu::SelectFirst",
|
"cmd-up": "menu::SelectFirst",
|
||||||
"cmd-down": "menu::SelectLast",
|
"cmd-down": "menu::SelectLast",
|
||||||
@ -60,13 +64,18 @@
|
|||||||
"cmd-z": "editor::Undo",
|
"cmd-z": "editor::Undo",
|
||||||
"cmd-shift-z": "editor::Redo",
|
"cmd-shift-z": "editor::Redo",
|
||||||
"up": "editor::MoveUp",
|
"up": "editor::MoveUp",
|
||||||
|
"pageup": "editor::PageUp",
|
||||||
|
"shift-pageup": "editor::MovePageUp",
|
||||||
"down": "editor::MoveDown",
|
"down": "editor::MoveDown",
|
||||||
|
"pagedown": "editor::PageDown",
|
||||||
|
"shift-pagedown": "editor::MovePageDown",
|
||||||
"left": "editor::MoveLeft",
|
"left": "editor::MoveLeft",
|
||||||
"right": "editor::MoveRight",
|
"right": "editor::MoveRight",
|
||||||
"ctrl-p": "editor::MoveUp",
|
"ctrl-p": "editor::MoveUp",
|
||||||
"ctrl-n": "editor::MoveDown",
|
"ctrl-n": "editor::MoveDown",
|
||||||
"ctrl-b": "editor::MoveLeft",
|
"ctrl-b": "editor::MoveLeft",
|
||||||
"ctrl-f": "editor::MoveRight",
|
"ctrl-f": "editor::MoveRight",
|
||||||
|
"ctrl-l": "editor::CenterScreen",
|
||||||
"alt-left": "editor::MoveToPreviousWordStart",
|
"alt-left": "editor::MoveToPreviousWordStart",
|
||||||
"alt-b": "editor::MoveToPreviousWordStart",
|
"alt-b": "editor::MoveToPreviousWordStart",
|
||||||
"alt-right": "editor::MoveToNextWordEnd",
|
"alt-right": "editor::MoveToNextWordEnd",
|
||||||
@ -93,6 +102,7 @@
|
|||||||
"cmd-shift-down": "editor::SelectToEnd",
|
"cmd-shift-down": "editor::SelectToEnd",
|
||||||
"cmd-a": "editor::SelectAll",
|
"cmd-a": "editor::SelectAll",
|
||||||
"cmd-l": "editor::SelectLine",
|
"cmd-l": "editor::SelectLine",
|
||||||
|
"cmd-shift-i": "editor::Format",
|
||||||
"cmd-shift-left": [
|
"cmd-shift-left": [
|
||||||
"editor::SelectToBeginningOfLine",
|
"editor::SelectToBeginningOfLine",
|
||||||
{
|
{
|
||||||
@ -117,8 +127,18 @@
|
|||||||
"stop_at_soft_wraps": true
|
"stop_at_soft_wraps": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"pageup": "editor::PageUp",
|
"ctrl-v": [
|
||||||
"pagedown": "editor::PageDown",
|
"editor::MovePageDown",
|
||||||
|
{
|
||||||
|
"center_cursor": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"alt-v": [
|
||||||
|
"editor::MovePageUp",
|
||||||
|
{
|
||||||
|
"center_cursor": true
|
||||||
|
}
|
||||||
|
],
|
||||||
"ctrl-cmd-space": "editor::ShowCharacterPalette"
|
"ctrl-cmd-space": "editor::ShowCharacterPalette"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -375,6 +395,7 @@
|
|||||||
{
|
{
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"ctrl-alt-cmd-f": "workspace::FollowNextCollaborator",
|
"ctrl-alt-cmd-f": "workspace::FollowNextCollaborator",
|
||||||
|
"cmd-shift-c": "collab::ToggleCollaborationMenu",
|
||||||
"cmd-alt-i": "zed::DebugElements"
|
"cmd-alt-i": "zed::DebugElements"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -394,7 +415,6 @@
|
|||||||
"context": "Workspace",
|
"context": "Workspace",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"shift-escape": "dock::FocusDock",
|
"shift-escape": "dock::FocusDock",
|
||||||
"cmd-shift-c": "contacts_panel::ToggleFocus",
|
|
||||||
"cmd-shift-b": "workspace::ToggleRightSidebar"
|
"cmd-shift-b": "workspace::ToggleRightSidebar"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -427,17 +447,53 @@
|
|||||||
{
|
{
|
||||||
"context": "Terminal",
|
"context": "Terminal",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
// Overrides for global bindings, remove at your own risk:
|
|
||||||
"up": "terminal::Up",
|
|
||||||
"down": "terminal::Down",
|
|
||||||
"escape": "terminal::Escape",
|
|
||||||
"enter": "terminal::Enter",
|
|
||||||
"ctrl-c": "terminal::CtrlC",
|
|
||||||
// Useful terminal actions:
|
|
||||||
"ctrl-cmd-space": "terminal::ShowCharacterPalette",
|
"ctrl-cmd-space": "terminal::ShowCharacterPalette",
|
||||||
"cmd-c": "terminal::Copy",
|
"cmd-c": "terminal::Copy",
|
||||||
"cmd-v": "terminal::Paste",
|
"cmd-v": "terminal::Paste",
|
||||||
"cmd-k": "terminal::Clear"
|
"cmd-k": "terminal::Clear",
|
||||||
|
// Some nice conveniences
|
||||||
|
"cmd-backspace": [
|
||||||
|
"terminal::SendText",
|
||||||
|
"\u0015"
|
||||||
|
],
|
||||||
|
"cmd-right": [
|
||||||
|
"terminal::SendText",
|
||||||
|
"\u0005"
|
||||||
|
],
|
||||||
|
"cmd-left": [
|
||||||
|
"terminal::SendText",
|
||||||
|
"\u0001"
|
||||||
|
],
|
||||||
|
// There are conflicting bindings for these keys in the global context.
|
||||||
|
// these bindings override them, remove at your own risk:
|
||||||
|
"up": [
|
||||||
|
"terminal::SendKeystroke",
|
||||||
|
"up"
|
||||||
|
],
|
||||||
|
"pageup": [
|
||||||
|
"terminal::SendKeystroke",
|
||||||
|
"pageup"
|
||||||
|
],
|
||||||
|
"down": [
|
||||||
|
"terminal::SendKeystroke",
|
||||||
|
"down"
|
||||||
|
],
|
||||||
|
"pagedown": [
|
||||||
|
"terminal::SendKeystroke",
|
||||||
|
"pagedown"
|
||||||
|
],
|
||||||
|
"escape": [
|
||||||
|
"terminal::SendKeystroke",
|
||||||
|
"escape"
|
||||||
|
],
|
||||||
|
"enter": [
|
||||||
|
"terminal::SendKeystroke",
|
||||||
|
"enter"
|
||||||
|
],
|
||||||
|
"ctrl-c": [
|
||||||
|
"terminal::SendKeystroke",
|
||||||
|
"ctrl-c"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
@ -9,11 +9,10 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"h": "vim::Left",
|
"h": "vim::Left",
|
||||||
"backspace": "vim::Left",
|
"backspace": "vim::Backspace",
|
||||||
"j": "vim::Down",
|
"j": "vim::Down",
|
||||||
"k": "vim::Up",
|
"k": "vim::Up",
|
||||||
"l": "vim::Right",
|
"l": "vim::Right",
|
||||||
"0": "vim::StartOfLine",
|
|
||||||
"$": "vim::EndOfLine",
|
"$": "vim::EndOfLine",
|
||||||
"shift-g": "vim::EndOfDocument",
|
"shift-g": "vim::EndOfDocument",
|
||||||
"w": "vim::NextWordStart",
|
"w": "vim::NextWordStart",
|
||||||
@ -38,7 +37,60 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"%": "vim::Matching",
|
"%": "vim::Matching",
|
||||||
"escape": "editor::Cancel"
|
"escape": "editor::Cancel",
|
||||||
|
"i": [
|
||||||
|
"vim::PushOperator",
|
||||||
|
{
|
||||||
|
"Object": {
|
||||||
|
"around": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"a": [
|
||||||
|
"vim::PushOperator",
|
||||||
|
{
|
||||||
|
"Object": {
|
||||||
|
"around": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"0": "vim::StartOfLine", // When no number operator present, use start of line motion
|
||||||
|
"1": [
|
||||||
|
"vim::Number",
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"2": [
|
||||||
|
"vim::Number",
|
||||||
|
2
|
||||||
|
],
|
||||||
|
"3": [
|
||||||
|
"vim::Number",
|
||||||
|
3
|
||||||
|
],
|
||||||
|
"4": [
|
||||||
|
"vim::Number",
|
||||||
|
4
|
||||||
|
],
|
||||||
|
"5": [
|
||||||
|
"vim::Number",
|
||||||
|
5
|
||||||
|
],
|
||||||
|
"6": [
|
||||||
|
"vim::Number",
|
||||||
|
6
|
||||||
|
],
|
||||||
|
"7": [
|
||||||
|
"vim::Number",
|
||||||
|
7
|
||||||
|
],
|
||||||
|
"8": [
|
||||||
|
"vim::Number",
|
||||||
|
8
|
||||||
|
],
|
||||||
|
"9": [
|
||||||
|
"vim::Number",
|
||||||
|
9
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -98,6 +150,15 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"context": "Editor && vim_operator == n",
|
||||||
|
"bindings": {
|
||||||
|
"0": [
|
||||||
|
"vim::Number",
|
||||||
|
0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"context": "Editor && vim_operator == g",
|
"context": "Editor && vim_operator == g",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
@ -112,13 +173,6 @@
|
|||||||
{
|
{
|
||||||
"context": "Editor && vim_operator == c",
|
"context": "Editor && vim_operator == c",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"w": "vim::ChangeWord",
|
|
||||||
"shift-w": [
|
|
||||||
"vim::ChangeWord",
|
|
||||||
{
|
|
||||||
"ignorePunctuation": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"c": "vim::CurrentLine"
|
"c": "vim::CurrentLine"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -134,9 +188,34 @@
|
|||||||
"y": "vim::CurrentLine"
|
"y": "vim::CurrentLine"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"context": "Editor && VimObject",
|
||||||
|
"bindings": {
|
||||||
|
"w": "vim::Word",
|
||||||
|
"shift-w": [
|
||||||
|
"vim::Word",
|
||||||
|
{
|
||||||
|
"ignorePunctuation": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"s": "vim::Sentence",
|
||||||
|
"'": "vim::Quotes",
|
||||||
|
"`": "vim::BackQuotes",
|
||||||
|
"\"": "vim::DoubleQuotes",
|
||||||
|
"(": "vim::Parentheses",
|
||||||
|
")": "vim::Parentheses",
|
||||||
|
"[": "vim::SquareBrackets",
|
||||||
|
"]": "vim::SquareBrackets",
|
||||||
|
"{": "vim::CurlyBrackets",
|
||||||
|
"}": "vim::CurlyBrackets",
|
||||||
|
"<": "vim::AngleBrackets",
|
||||||
|
">": "vim::AngleBrackets"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"context": "Editor && vim_mode == visual",
|
"context": "Editor && vim_mode == visual",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
|
"u": "editor::Undo",
|
||||||
"c": "vim::VisualChange",
|
"c": "vim::VisualChange",
|
||||||
"d": "vim::VisualDelete",
|
"d": "vim::VisualDelete",
|
||||||
"x": "vim::VisualDelete",
|
"x": "vim::VisualDelete",
|
||||||
|
@ -42,21 +42,20 @@
|
|||||||
// 3. Position the dock full screen over the entire workspace"
|
// 3. Position the dock full screen over the entire workspace"
|
||||||
// "default_dock_anchor": "expanded"
|
// "default_dock_anchor": "expanded"
|
||||||
"default_dock_anchor": "right",
|
"default_dock_anchor": "right",
|
||||||
// How to auto-format modified buffers when saving them. This
|
// Whether or not to perform a buffer format before saving
|
||||||
// setting can take three values:
|
"format_on_save": "on",
|
||||||
|
// How to perform a buffer format. This setting can take two values:
|
||||||
//
|
//
|
||||||
// 1. Don't format code
|
// 1. Format code using the current language server:
|
||||||
// "format_on_save": "off"
|
|
||||||
// 2. Format code using the current language server:
|
|
||||||
// "format_on_save": "language_server"
|
// "format_on_save": "language_server"
|
||||||
// 3. Format code using an external command:
|
// 2. Format code using an external command:
|
||||||
// "format_on_save": {
|
// "format_on_save": {
|
||||||
// "external": {
|
// "external": {
|
||||||
// "command": "prettier",
|
// "command": "prettier",
|
||||||
// "arguments": ["--stdin-filepath", "{buffer_path}"]
|
// "arguments": ["--stdin-filepath", "{buffer_path}"]
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
"format_on_save": "language_server",
|
"formatter": "language_server",
|
||||||
// How to soft-wrap long lines of text. This setting can take
|
// How to soft-wrap long lines of text. This setting can take
|
||||||
// three values:
|
// three values:
|
||||||
//
|
//
|
||||||
@ -75,6 +74,25 @@
|
|||||||
"hard_tabs": false,
|
"hard_tabs": false,
|
||||||
// How many columns a tab should occupy.
|
// How many columns a tab should occupy.
|
||||||
"tab_size": 4,
|
"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
|
// Settings specific to the terminal
|
||||||
"terminal": {
|
"terminal": {
|
||||||
// What shell to use when opening a terminal. May take 3 values:
|
// What shell to use when opening a terminal. May take 3 values:
|
||||||
@ -140,6 +158,9 @@
|
|||||||
// 2. Make the option keys behave as a 'meta' key, e.g. for emacs
|
// 2. Make the option keys behave as a 'meta' key, e.g. for emacs
|
||||||
// "option_to_meta": true,
|
// "option_to_meta": true,
|
||||||
"option_as_meta": false,
|
"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
|
// Any key-value pairs added to this list will be added to the terminal's
|
||||||
// enviroment. Use `:` to seperate multiple values.
|
// enviroment. Use `:` to seperate multiple values.
|
||||||
"env": {
|
"env": {
|
||||||
|
@ -46,6 +46,7 @@ impl ActivityIndicator {
|
|||||||
cx: &mut ViewContext<Workspace>,
|
cx: &mut ViewContext<Workspace>,
|
||||||
) -> ViewHandle<ActivityIndicator> {
|
) -> ViewHandle<ActivityIndicator> {
|
||||||
let project = workspace.project().clone();
|
let project = workspace.project().clone();
|
||||||
|
let auto_updater = AutoUpdater::get(cx);
|
||||||
let this = cx.add_view(|cx: &mut ViewContext<Self>| {
|
let this = cx.add_view(|cx: &mut ViewContext<Self>| {
|
||||||
let mut status_events = languages.language_server_binary_statuses();
|
let mut status_events = languages.language_server_binary_statuses();
|
||||||
cx.spawn_weak(|this, mut cx| async move {
|
cx.spawn_weak(|this, mut cx| async move {
|
||||||
@ -66,11 +67,14 @@ impl ActivityIndicator {
|
|||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
cx.observe(&project, |_, _, cx| cx.notify()).detach();
|
cx.observe(&project, |_, _, cx| cx.notify()).detach();
|
||||||
|
if let Some(auto_updater) = auto_updater.as_ref() {
|
||||||
|
cx.observe(auto_updater, |_, _, cx| cx.notify()).detach();
|
||||||
|
}
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
statuses: Default::default(),
|
statuses: Default::default(),
|
||||||
project: project.clone(),
|
project: project.clone(),
|
||||||
auto_updater: AutoUpdater::get(cx),
|
auto_updater,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
cx.subscribe(&this, move |workspace, _, event, cx| match event {
|
cx.subscribe(&this, move |workspace, _, event, cx| match event {
|
||||||
@ -285,7 +289,7 @@ impl View for ActivityIndicator {
|
|||||||
.workspace
|
.workspace
|
||||||
.status_bar
|
.status_bar
|
||||||
.lsp_status;
|
.lsp_status;
|
||||||
let style = if state.hovered && action.is_some() {
|
let style = if state.hovered() && action.is_some() {
|
||||||
theme.hover.as_ref().unwrap_or(&theme.default)
|
theme.hover.as_ref().unwrap_or(&theme.default)
|
||||||
} else {
|
} else {
|
||||||
&theme.default
|
&theme.default
|
||||||
|
35
crates/call/Cargo.toml
Normal file
35
crates/call/Cargo.toml
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
[package]
|
||||||
|
name = "call"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "src/call.rs"
|
||||||
|
doctest = false
|
||||||
|
|
||||||
|
[features]
|
||||||
|
test-support = [
|
||||||
|
"client/test-support",
|
||||||
|
"collections/test-support",
|
||||||
|
"gpui/test-support",
|
||||||
|
"project/test-support",
|
||||||
|
"util/test-support"
|
||||||
|
]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
client = { path = "../client" }
|
||||||
|
collections = { path = "../collections" }
|
||||||
|
gpui = { path = "../gpui" }
|
||||||
|
project = { path = "../project" }
|
||||||
|
util = { path = "../util" }
|
||||||
|
|
||||||
|
anyhow = "1.0.38"
|
||||||
|
futures = "0.3"
|
||||||
|
postage = { version = "0.4.1", features = ["futures-traits"] }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
client = { path = "../client", features = ["test-support"] }
|
||||||
|
collections = { path = "../collections", features = ["test-support"] }
|
||||||
|
gpui = { path = "../gpui", features = ["test-support"] }
|
||||||
|
project = { path = "../project", features = ["test-support"] }
|
||||||
|
util = { path = "../util", features = ["test-support"] }
|
261
crates/call/src/call.rs
Normal file
261
crates/call/src/call.rs
Normal file
@ -0,0 +1,261 @@
|
|||||||
|
mod participant;
|
||||||
|
pub mod room;
|
||||||
|
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use client::{proto, Client, TypedEnvelope, User, UserStore};
|
||||||
|
use gpui::{
|
||||||
|
AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext,
|
||||||
|
Subscription, Task,
|
||||||
|
};
|
||||||
|
pub use participant::ParticipantLocation;
|
||||||
|
use postage::watch;
|
||||||
|
use project::Project;
|
||||||
|
pub use room::Room;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
pub fn init(client: Arc<Client>, user_store: ModelHandle<UserStore>, cx: &mut MutableAppContext) {
|
||||||
|
let active_call = cx.add_model(|cx| ActiveCall::new(client, user_store, cx));
|
||||||
|
cx.set_global(active_call);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct IncomingCall {
|
||||||
|
pub room_id: u64,
|
||||||
|
pub caller: Arc<User>,
|
||||||
|
pub participants: Vec<Arc<User>>,
|
||||||
|
pub initial_project: Option<proto::ParticipantProject>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ActiveCall {
|
||||||
|
room: Option<(ModelHandle<Room>, Vec<Subscription>)>,
|
||||||
|
incoming_call: (
|
||||||
|
watch::Sender<Option<IncomingCall>>,
|
||||||
|
watch::Receiver<Option<IncomingCall>>,
|
||||||
|
),
|
||||||
|
client: Arc<Client>,
|
||||||
|
user_store: ModelHandle<UserStore>,
|
||||||
|
_subscriptions: Vec<client::Subscription>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Entity for ActiveCall {
|
||||||
|
type Event = room::Event;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActiveCall {
|
||||||
|
fn new(
|
||||||
|
client: Arc<Client>,
|
||||||
|
user_store: ModelHandle<UserStore>,
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
room: None,
|
||||||
|
incoming_call: watch::channel(),
|
||||||
|
_subscriptions: vec![
|
||||||
|
client.add_request_handler(cx.handle(), Self::handle_incoming_call),
|
||||||
|
client.add_message_handler(cx.handle(), Self::handle_call_canceled),
|
||||||
|
],
|
||||||
|
client,
|
||||||
|
user_store,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_incoming_call(
|
||||||
|
this: ModelHandle<Self>,
|
||||||
|
envelope: TypedEnvelope<proto::IncomingCall>,
|
||||||
|
_: Arc<Client>,
|
||||||
|
mut cx: AsyncAppContext,
|
||||||
|
) -> Result<proto::Ack> {
|
||||||
|
let user_store = this.read_with(&cx, |this, _| this.user_store.clone());
|
||||||
|
let call = IncomingCall {
|
||||||
|
room_id: envelope.payload.room_id,
|
||||||
|
participants: user_store
|
||||||
|
.update(&mut cx, |user_store, cx| {
|
||||||
|
user_store.get_users(envelope.payload.participant_user_ids, cx)
|
||||||
|
})
|
||||||
|
.await?,
|
||||||
|
caller: user_store
|
||||||
|
.update(&mut cx, |user_store, cx| {
|
||||||
|
user_store.get_user(envelope.payload.caller_user_id, cx)
|
||||||
|
})
|
||||||
|
.await?,
|
||||||
|
initial_project: envelope.payload.initial_project,
|
||||||
|
};
|
||||||
|
this.update(&mut cx, |this, _| {
|
||||||
|
*this.incoming_call.0.borrow_mut() = Some(call);
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(proto::Ack {})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_call_canceled(
|
||||||
|
this: ModelHandle<Self>,
|
||||||
|
_: TypedEnvelope<proto::CallCanceled>,
|
||||||
|
_: Arc<Client>,
|
||||||
|
mut cx: AsyncAppContext,
|
||||||
|
) -> Result<()> {
|
||||||
|
this.update(&mut cx, |this, _| {
|
||||||
|
*this.incoming_call.0.borrow_mut() = None;
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn global(cx: &AppContext) -> ModelHandle<Self> {
|
||||||
|
cx.global::<ModelHandle<Self>>().clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn invite(
|
||||||
|
&mut self,
|
||||||
|
recipient_user_id: u64,
|
||||||
|
initial_project: Option<ModelHandle<Project>>,
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
) -> Task<Result<()>> {
|
||||||
|
let client = self.client.clone();
|
||||||
|
let user_store = self.user_store.clone();
|
||||||
|
cx.spawn(|this, mut cx| async move {
|
||||||
|
if let Some(room) = this.read_with(&cx, |this, _| this.room().cloned()) {
|
||||||
|
let initial_project_id = if let Some(initial_project) = initial_project {
|
||||||
|
Some(
|
||||||
|
room.update(&mut cx, |room, cx| room.share_project(initial_project, cx))
|
||||||
|
.await?,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
room.update(&mut cx, |room, cx| {
|
||||||
|
room.call(recipient_user_id, initial_project_id, cx)
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
} else {
|
||||||
|
let room = cx
|
||||||
|
.update(|cx| {
|
||||||
|
Room::create(recipient_user_id, initial_project, client, user_store, cx)
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
this.update(&mut cx, |this, cx| this.set_room(Some(room), cx));
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cancel_invite(
|
||||||
|
&mut self,
|
||||||
|
recipient_user_id: u64,
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
) -> Task<Result<()>> {
|
||||||
|
let room_id = if let Some(room) = self.room() {
|
||||||
|
room.read(cx).id()
|
||||||
|
} else {
|
||||||
|
return Task::ready(Err(anyhow!("no active call")));
|
||||||
|
};
|
||||||
|
|
||||||
|
let client = self.client.clone();
|
||||||
|
cx.foreground().spawn(async move {
|
||||||
|
client
|
||||||
|
.request(proto::CancelCall {
|
||||||
|
room_id,
|
||||||
|
recipient_user_id,
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
anyhow::Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn incoming(&self) -> watch::Receiver<Option<IncomingCall>> {
|
||||||
|
self.incoming_call.1.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn accept_incoming(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||||
|
if self.room.is_some() {
|
||||||
|
return Task::ready(Err(anyhow!("cannot join while on another call")));
|
||||||
|
}
|
||||||
|
|
||||||
|
let call = if let Some(call) = self.incoming_call.1.borrow().clone() {
|
||||||
|
call
|
||||||
|
} else {
|
||||||
|
return Task::ready(Err(anyhow!("no incoming call")));
|
||||||
|
};
|
||||||
|
|
||||||
|
let join = Room::join(&call, self.client.clone(), self.user_store.clone(), cx);
|
||||||
|
cx.spawn(|this, mut cx| async move {
|
||||||
|
let room = join.await?;
|
||||||
|
this.update(&mut cx, |this, cx| this.set_room(Some(room.clone()), cx));
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decline_incoming(&mut self) -> Result<()> {
|
||||||
|
let call = self
|
||||||
|
.incoming_call
|
||||||
|
.0
|
||||||
|
.borrow_mut()
|
||||||
|
.take()
|
||||||
|
.ok_or_else(|| anyhow!("no incoming call"))?;
|
||||||
|
self.client.send(proto::DeclineCall {
|
||||||
|
room_id: call.room_id,
|
||||||
|
})?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hang_up(&mut self, cx: &mut ModelContext<Self>) -> Result<()> {
|
||||||
|
if let Some((room, _)) = self.room.take() {
|
||||||
|
room.update(cx, |room, cx| room.leave(cx))?;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn share_project(
|
||||||
|
&mut self,
|
||||||
|
project: ModelHandle<Project>,
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
) -> Task<Result<u64>> {
|
||||||
|
if let Some((room, _)) = self.room.as_ref() {
|
||||||
|
room.update(cx, |room, cx| room.share_project(project, cx))
|
||||||
|
} else {
|
||||||
|
Task::ready(Err(anyhow!("no active call")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_location(
|
||||||
|
&mut self,
|
||||||
|
project: Option<&ModelHandle<Project>>,
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
) -> Task<Result<()>> {
|
||||||
|
if let Some((room, _)) = self.room.as_ref() {
|
||||||
|
room.update(cx, |room, cx| room.set_location(project, cx))
|
||||||
|
} else {
|
||||||
|
Task::ready(Err(anyhow!("no active call")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_room(&mut self, room: Option<ModelHandle<Room>>, cx: &mut ModelContext<Self>) {
|
||||||
|
if room.as_ref() != self.room.as_ref().map(|room| &room.0) {
|
||||||
|
if let Some(room) = room {
|
||||||
|
if room.read(cx).status().is_offline() {
|
||||||
|
self.room = None;
|
||||||
|
} else {
|
||||||
|
let subscriptions = vec![
|
||||||
|
cx.observe(&room, |this, room, cx| {
|
||||||
|
if room.read(cx).status().is_offline() {
|
||||||
|
this.set_room(None, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
cx.notify();
|
||||||
|
}),
|
||||||
|
cx.subscribe(&room, |_, _, event, cx| cx.emit(event.clone())),
|
||||||
|
];
|
||||||
|
self.room = Some((room, subscriptions));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.room = None;
|
||||||
|
}
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn room(&self) -> Option<&ModelHandle<Room>> {
|
||||||
|
self.room.as_ref().map(|(room, _)| room)
|
||||||
|
}
|
||||||
|
}
|
42
crates/call/src/participant.rs
Normal file
42
crates/call/src/participant.rs
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use client::{proto, User};
|
||||||
|
use gpui::WeakModelHandle;
|
||||||
|
use project::Project;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub enum ParticipantLocation {
|
||||||
|
SharedProject { project_id: u64 },
|
||||||
|
UnsharedProject,
|
||||||
|
External,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ParticipantLocation {
|
||||||
|
pub fn from_proto(location: Option<proto::ParticipantLocation>) -> Result<Self> {
|
||||||
|
match location.and_then(|l| l.variant) {
|
||||||
|
Some(proto::participant_location::Variant::SharedProject(project)) => {
|
||||||
|
Ok(Self::SharedProject {
|
||||||
|
project_id: project.id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Some(proto::participant_location::Variant::UnsharedProject(_)) => {
|
||||||
|
Ok(Self::UnsharedProject)
|
||||||
|
}
|
||||||
|
Some(proto::participant_location::Variant::External(_)) => Ok(Self::External),
|
||||||
|
None => Err(anyhow!("participant location was not provided")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Default)]
|
||||||
|
pub struct LocalParticipant {
|
||||||
|
pub projects: Vec<proto::ParticipantProject>,
|
||||||
|
pub active_project: Option<WeakModelHandle<Project>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct RemoteParticipant {
|
||||||
|
pub user: Arc<User>,
|
||||||
|
pub projects: Vec<proto::ParticipantProject>,
|
||||||
|
pub location: ParticipantLocation,
|
||||||
|
}
|
472
crates/call/src/room.rs
Normal file
472
crates/call/src/room.rs
Normal file
@ -0,0 +1,472 @@
|
|||||||
|
use crate::{
|
||||||
|
participant::{LocalParticipant, ParticipantLocation, RemoteParticipant},
|
||||||
|
IncomingCall,
|
||||||
|
};
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use client::{proto, Client, PeerId, TypedEnvelope, User, UserStore};
|
||||||
|
use collections::{BTreeMap, HashSet};
|
||||||
|
use futures::StreamExt;
|
||||||
|
use gpui::{AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task};
|
||||||
|
use project::Project;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use util::ResultExt;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub enum Event {
|
||||||
|
RemoteProjectShared {
|
||||||
|
owner: Arc<User>,
|
||||||
|
project_id: u64,
|
||||||
|
worktree_root_names: Vec<String>,
|
||||||
|
},
|
||||||
|
RemoteProjectUnshared {
|
||||||
|
project_id: u64,
|
||||||
|
},
|
||||||
|
Left,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Room {
|
||||||
|
id: u64,
|
||||||
|
status: RoomStatus,
|
||||||
|
local_participant: LocalParticipant,
|
||||||
|
remote_participants: BTreeMap<PeerId, RemoteParticipant>,
|
||||||
|
pending_participants: Vec<Arc<User>>,
|
||||||
|
participant_user_ids: HashSet<u64>,
|
||||||
|
pending_call_count: usize,
|
||||||
|
leave_when_empty: bool,
|
||||||
|
client: Arc<Client>,
|
||||||
|
user_store: ModelHandle<UserStore>,
|
||||||
|
subscriptions: Vec<client::Subscription>,
|
||||||
|
pending_room_update: Option<Task<()>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Entity for Room {
|
||||||
|
type Event = Event;
|
||||||
|
|
||||||
|
fn release(&mut self, _: &mut MutableAppContext) {
|
||||||
|
self.client.send(proto::LeaveRoom { id: self.id }).log_err();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Room {
|
||||||
|
fn new(
|
||||||
|
id: u64,
|
||||||
|
client: Arc<Client>,
|
||||||
|
user_store: ModelHandle<UserStore>,
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
) -> Self {
|
||||||
|
let mut client_status = client.status();
|
||||||
|
cx.spawn_weak(|this, mut cx| async move {
|
||||||
|
let is_connected = client_status
|
||||||
|
.next()
|
||||||
|
.await
|
||||||
|
.map_or(false, |s| s.is_connected());
|
||||||
|
// Even if we're initially connected, any future change of the status means we momentarily disconnected.
|
||||||
|
if !is_connected || client_status.next().await.is_some() {
|
||||||
|
if let Some(this) = this.upgrade(&cx) {
|
||||||
|
let _ = this.update(&mut cx, |this, cx| this.leave(cx));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
status: RoomStatus::Online,
|
||||||
|
participant_user_ids: Default::default(),
|
||||||
|
local_participant: Default::default(),
|
||||||
|
remote_participants: Default::default(),
|
||||||
|
pending_participants: Default::default(),
|
||||||
|
pending_call_count: 0,
|
||||||
|
subscriptions: vec![client.add_message_handler(cx.handle(), Self::handle_room_updated)],
|
||||||
|
leave_when_empty: false,
|
||||||
|
pending_room_update: None,
|
||||||
|
client,
|
||||||
|
user_store,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn create(
|
||||||
|
recipient_user_id: u64,
|
||||||
|
initial_project: Option<ModelHandle<Project>>,
|
||||||
|
client: Arc<Client>,
|
||||||
|
user_store: ModelHandle<UserStore>,
|
||||||
|
cx: &mut MutableAppContext,
|
||||||
|
) -> Task<Result<ModelHandle<Self>>> {
|
||||||
|
cx.spawn(|mut cx| async move {
|
||||||
|
let response = client.request(proto::CreateRoom {}).await?;
|
||||||
|
let room = cx.add_model(|cx| Self::new(response.id, client, user_store, cx));
|
||||||
|
|
||||||
|
let initial_project_id = if let Some(initial_project) = initial_project {
|
||||||
|
let initial_project_id = room
|
||||||
|
.update(&mut cx, |room, cx| {
|
||||||
|
room.share_project(initial_project.clone(), cx)
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
Some(initial_project_id)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
match room
|
||||||
|
.update(&mut cx, |room, cx| {
|
||||||
|
room.leave_when_empty = true;
|
||||||
|
room.call(recipient_user_id, initial_project_id, cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(()) => Ok(room),
|
||||||
|
Err(error) => Err(anyhow!("room creation failed: {:?}", error)),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn join(
|
||||||
|
call: &IncomingCall,
|
||||||
|
client: Arc<Client>,
|
||||||
|
user_store: ModelHandle<UserStore>,
|
||||||
|
cx: &mut MutableAppContext,
|
||||||
|
) -> Task<Result<ModelHandle<Self>>> {
|
||||||
|
let room_id = call.room_id;
|
||||||
|
cx.spawn(|mut cx| async move {
|
||||||
|
let response = client.request(proto::JoinRoom { id: room_id }).await?;
|
||||||
|
let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?;
|
||||||
|
let room = cx.add_model(|cx| Self::new(room_id, client, user_store, cx));
|
||||||
|
room.update(&mut cx, |room, cx| {
|
||||||
|
room.leave_when_empty = true;
|
||||||
|
room.apply_room_update(room_proto, cx)?;
|
||||||
|
anyhow::Ok(())
|
||||||
|
})?;
|
||||||
|
Ok(room)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_leave(&self) -> bool {
|
||||||
|
self.leave_when_empty
|
||||||
|
&& self.pending_room_update.is_none()
|
||||||
|
&& self.pending_participants.is_empty()
|
||||||
|
&& self.remote_participants.is_empty()
|
||||||
|
&& self.pending_call_count == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn leave(&mut self, cx: &mut ModelContext<Self>) -> Result<()> {
|
||||||
|
if self.status.is_offline() {
|
||||||
|
return Err(anyhow!("room is offline"));
|
||||||
|
}
|
||||||
|
|
||||||
|
cx.notify();
|
||||||
|
cx.emit(Event::Left);
|
||||||
|
self.status = RoomStatus::Offline;
|
||||||
|
self.remote_participants.clear();
|
||||||
|
self.pending_participants.clear();
|
||||||
|
self.participant_user_ids.clear();
|
||||||
|
self.subscriptions.clear();
|
||||||
|
self.client.send(proto::LeaveRoom { id: self.id })?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn id(&self) -> u64 {
|
||||||
|
self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn status(&self) -> RoomStatus {
|
||||||
|
self.status
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn local_participant(&self) -> &LocalParticipant {
|
||||||
|
&self.local_participant
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remote_participants(&self) -> &BTreeMap<PeerId, RemoteParticipant> {
|
||||||
|
&self.remote_participants
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pending_participants(&self) -> &[Arc<User>] {
|
||||||
|
&self.pending_participants
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn contains_participant(&self, user_id: u64) -> bool {
|
||||||
|
self.participant_user_ids.contains(&user_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_room_updated(
|
||||||
|
this: ModelHandle<Self>,
|
||||||
|
envelope: TypedEnvelope<proto::RoomUpdated>,
|
||||||
|
_: Arc<Client>,
|
||||||
|
mut cx: AsyncAppContext,
|
||||||
|
) -> Result<()> {
|
||||||
|
let room = envelope
|
||||||
|
.payload
|
||||||
|
.room
|
||||||
|
.ok_or_else(|| anyhow!("invalid room"))?;
|
||||||
|
this.update(&mut cx, |this, cx| this.apply_room_update(room, cx))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_room_update(
|
||||||
|
&mut self,
|
||||||
|
mut room: proto::Room,
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
) -> Result<()> {
|
||||||
|
// Filter ourselves out from the room's participants.
|
||||||
|
let local_participant_ix = room
|
||||||
|
.participants
|
||||||
|
.iter()
|
||||||
|
.position(|participant| Some(participant.user_id) == self.client.user_id());
|
||||||
|
let local_participant = local_participant_ix.map(|ix| room.participants.swap_remove(ix));
|
||||||
|
|
||||||
|
let remote_participant_user_ids = room
|
||||||
|
.participants
|
||||||
|
.iter()
|
||||||
|
.map(|p| p.user_id)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let (remote_participants, pending_participants) =
|
||||||
|
self.user_store.update(cx, move |user_store, cx| {
|
||||||
|
(
|
||||||
|
user_store.get_users(remote_participant_user_ids, cx),
|
||||||
|
user_store.get_users(room.pending_participant_user_ids, cx),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
self.pending_room_update = Some(cx.spawn(|this, mut cx| async move {
|
||||||
|
let (remote_participants, pending_participants) =
|
||||||
|
futures::join!(remote_participants, pending_participants);
|
||||||
|
|
||||||
|
this.update(&mut cx, |this, cx| {
|
||||||
|
this.participant_user_ids.clear();
|
||||||
|
|
||||||
|
if let Some(participant) = local_participant {
|
||||||
|
this.local_participant.projects = participant.projects;
|
||||||
|
} else {
|
||||||
|
this.local_participant.projects.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(participants) = remote_participants.log_err() {
|
||||||
|
for (participant, user) in room.participants.into_iter().zip(participants) {
|
||||||
|
let peer_id = PeerId(participant.peer_id);
|
||||||
|
this.participant_user_ids.insert(participant.user_id);
|
||||||
|
|
||||||
|
let old_projects = this
|
||||||
|
.remote_participants
|
||||||
|
.get(&peer_id)
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|existing| &existing.projects)
|
||||||
|
.map(|project| project.id)
|
||||||
|
.collect::<HashSet<_>>();
|
||||||
|
let new_projects = participant
|
||||||
|
.projects
|
||||||
|
.iter()
|
||||||
|
.map(|project| project.id)
|
||||||
|
.collect::<HashSet<_>>();
|
||||||
|
|
||||||
|
for project in &participant.projects {
|
||||||
|
if !old_projects.contains(&project.id) {
|
||||||
|
cx.emit(Event::RemoteProjectShared {
|
||||||
|
owner: user.clone(),
|
||||||
|
project_id: project.id,
|
||||||
|
worktree_root_names: project.worktree_root_names.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for unshared_project_id in old_projects.difference(&new_projects) {
|
||||||
|
cx.emit(Event::RemoteProjectUnshared {
|
||||||
|
project_id: *unshared_project_id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.remote_participants.insert(
|
||||||
|
peer_id,
|
||||||
|
RemoteParticipant {
|
||||||
|
user: user.clone(),
|
||||||
|
projects: participant.projects,
|
||||||
|
location: ParticipantLocation::from_proto(participant.location)
|
||||||
|
.unwrap_or(ParticipantLocation::External),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.remote_participants.retain(|_, participant| {
|
||||||
|
if this.participant_user_ids.contains(&participant.user.id) {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
for project in &participant.projects {
|
||||||
|
cx.emit(Event::RemoteProjectUnshared {
|
||||||
|
project_id: project.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(pending_participants) = pending_participants.log_err() {
|
||||||
|
this.pending_participants = pending_participants;
|
||||||
|
for participant in &this.pending_participants {
|
||||||
|
this.participant_user_ids.insert(participant.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pending_room_update.take();
|
||||||
|
if this.should_leave() {
|
||||||
|
let _ = this.leave(cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.check_invariants();
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
cx.notify();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_invariants(&self) {
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
{
|
||||||
|
for participant in self.remote_participants.values() {
|
||||||
|
assert!(self.participant_user_ids.contains(&participant.user.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
for participant in &self.pending_participants {
|
||||||
|
assert!(self.participant_user_ids.contains(&participant.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
self.participant_user_ids.len(),
|
||||||
|
self.remote_participants.len() + self.pending_participants.len()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn call(
|
||||||
|
&mut self,
|
||||||
|
recipient_user_id: u64,
|
||||||
|
initial_project_id: Option<u64>,
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
) -> Task<Result<()>> {
|
||||||
|
if self.status.is_offline() {
|
||||||
|
return Task::ready(Err(anyhow!("room is offline")));
|
||||||
|
}
|
||||||
|
|
||||||
|
cx.notify();
|
||||||
|
let client = self.client.clone();
|
||||||
|
let room_id = self.id;
|
||||||
|
self.pending_call_count += 1;
|
||||||
|
cx.spawn(|this, mut cx| async move {
|
||||||
|
let result = client
|
||||||
|
.request(proto::Call {
|
||||||
|
room_id,
|
||||||
|
recipient_user_id,
|
||||||
|
initial_project_id,
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
this.update(&mut cx, |this, cx| {
|
||||||
|
this.pending_call_count -= 1;
|
||||||
|
if this.should_leave() {
|
||||||
|
this.leave(cx)?;
|
||||||
|
}
|
||||||
|
result
|
||||||
|
})?;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn share_project(
|
||||||
|
&mut self,
|
||||||
|
project: ModelHandle<Project>,
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
) -> Task<Result<u64>> {
|
||||||
|
if let Some(project_id) = project.read(cx).remote_id() {
|
||||||
|
return Task::ready(Ok(project_id));
|
||||||
|
}
|
||||||
|
|
||||||
|
let request = self.client.request(proto::ShareProject {
|
||||||
|
room_id: self.id(),
|
||||||
|
worktrees: project
|
||||||
|
.read(cx)
|
||||||
|
.worktrees(cx)
|
||||||
|
.map(|worktree| {
|
||||||
|
let worktree = worktree.read(cx);
|
||||||
|
proto::WorktreeMetadata {
|
||||||
|
id: worktree.id().to_proto(),
|
||||||
|
root_name: worktree.root_name().into(),
|
||||||
|
visible: worktree.is_visible(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
});
|
||||||
|
cx.spawn(|this, mut cx| async move {
|
||||||
|
let response = request.await?;
|
||||||
|
|
||||||
|
project.update(&mut cx, |project, cx| {
|
||||||
|
project
|
||||||
|
.shared(response.project_id, cx)
|
||||||
|
.detach_and_log_err(cx)
|
||||||
|
});
|
||||||
|
|
||||||
|
// If the user's location is in this project, it changes from UnsharedProject to SharedProject.
|
||||||
|
this.update(&mut cx, |this, cx| {
|
||||||
|
let active_project = this.local_participant.active_project.as_ref();
|
||||||
|
if active_project.map_or(false, |location| *location == project) {
|
||||||
|
this.set_location(Some(&project), cx)
|
||||||
|
} else {
|
||||||
|
Task::ready(Ok(()))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(response.project_id)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_location(
|
||||||
|
&mut self,
|
||||||
|
project: Option<&ModelHandle<Project>>,
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
) -> Task<Result<()>> {
|
||||||
|
if self.status.is_offline() {
|
||||||
|
return Task::ready(Err(anyhow!("room is offline")));
|
||||||
|
}
|
||||||
|
|
||||||
|
let client = self.client.clone();
|
||||||
|
let room_id = self.id;
|
||||||
|
let location = if let Some(project) = project {
|
||||||
|
self.local_participant.active_project = Some(project.downgrade());
|
||||||
|
if let Some(project_id) = project.read(cx).remote_id() {
|
||||||
|
proto::participant_location::Variant::SharedProject(
|
||||||
|
proto::participant_location::SharedProject { id: project_id },
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
proto::participant_location::Variant::UnsharedProject(
|
||||||
|
proto::participant_location::UnsharedProject {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.local_participant.active_project = None;
|
||||||
|
proto::participant_location::Variant::External(proto::participant_location::External {})
|
||||||
|
};
|
||||||
|
|
||||||
|
cx.notify();
|
||||||
|
cx.foreground().spawn(async move {
|
||||||
|
client
|
||||||
|
.request(proto::UpdateParticipantLocation {
|
||||||
|
room_id,
|
||||||
|
location: Some(proto::ParticipantLocation {
|
||||||
|
variant: Some(location),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||||
|
pub enum RoomStatus {
|
||||||
|
Online,
|
||||||
|
Offline,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RoomStatus {
|
||||||
|
pub fn is_offline(&self) -> bool {
|
||||||
|
matches!(self, RoomStatus::Offline)
|
||||||
|
}
|
||||||
|
}
|
@ -12,6 +12,7 @@ test-support = ["collections/test-support", "gpui/test-support", "rpc/test-suppo
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
collections = { path = "../collections" }
|
collections = { path = "../collections" }
|
||||||
|
db = { path = "../db" }
|
||||||
gpui = { path = "../gpui" }
|
gpui = { path = "../gpui" }
|
||||||
util = { path = "../util" }
|
util = { path = "../util" }
|
||||||
rpc = { path = "../rpc" }
|
rpc = { path = "../rpc" }
|
||||||
@ -31,7 +32,10 @@ smol = "1.2.5"
|
|||||||
thiserror = "1.0.29"
|
thiserror = "1.0.29"
|
||||||
time = { version = "0.3", features = ["serde", "serde-well-known"] }
|
time = { version = "0.3", features = ["serde", "serde-well-known"] }
|
||||||
tiny_http = "0.8"
|
tiny_http = "0.8"
|
||||||
|
uuid = { version = "1.1.2", features = ["v4"] }
|
||||||
url = "2.2"
|
url = "2.2"
|
||||||
|
serde = { version = "*", features = ["derive"] }
|
||||||
|
tempfile = "3"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
collections = { path = "../collections", features = ["test-support"] }
|
collections = { path = "../collections", features = ["test-support"] }
|
||||||
|
@ -530,7 +530,7 @@ impl ChannelMessage {
|
|||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
let sender = user_store
|
let sender = user_store
|
||||||
.update(cx, |user_store, cx| {
|
.update(cx, |user_store, cx| {
|
||||||
user_store.fetch_user(message.sender_id, cx)
|
user_store.get_user(message.sender_id, cx)
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
Ok(ChannelMessage {
|
Ok(ChannelMessage {
|
||||||
@ -601,7 +601,7 @@ mod tests {
|
|||||||
|
|
||||||
let user_id = 5;
|
let user_id = 5;
|
||||||
let http_client = FakeHttpClient::with_404_response();
|
let http_client = FakeHttpClient::with_404_response();
|
||||||
let client = Client::new(http_client.clone());
|
let client = cx.update(|cx| Client::new(http_client.clone(), cx));
|
||||||
let server = FakeServer::for_client(user_id, &client, cx).await;
|
let server = FakeServer::for_client(user_id, &client, cx).await;
|
||||||
|
|
||||||
Channel::init(&client);
|
Channel::init(&client);
|
||||||
|
@ -3,6 +3,7 @@ pub mod test;
|
|||||||
|
|
||||||
pub mod channel;
|
pub mod channel;
|
||||||
pub mod http;
|
pub mod http;
|
||||||
|
pub mod telemetry;
|
||||||
pub mod user;
|
pub mod user;
|
||||||
|
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
@ -11,10 +12,12 @@ use async_tungstenite::tungstenite::{
|
|||||||
error::Error as WebsocketError,
|
error::Error as WebsocketError,
|
||||||
http::{Request, StatusCode},
|
http::{Request, StatusCode},
|
||||||
};
|
};
|
||||||
|
use db::Db;
|
||||||
use futures::{future::LocalBoxFuture, FutureExt, SinkExt, StreamExt, TryStreamExt};
|
use futures::{future::LocalBoxFuture, FutureExt, SinkExt, StreamExt, TryStreamExt};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AnyWeakViewHandle, AsyncAppContext,
|
actions, serde_json::Value, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle,
|
||||||
Entity, ModelContext, ModelHandle, MutableAppContext, Task, View, ViewContext, ViewHandle,
|
AnyWeakViewHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
|
||||||
|
MutableAppContext, Task, View, ViewContext, ViewHandle,
|
||||||
};
|
};
|
||||||
use http::HttpClient;
|
use http::HttpClient;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
@ -28,9 +31,11 @@ use std::{
|
|||||||
convert::TryFrom,
|
convert::TryFrom,
|
||||||
fmt::Write as _,
|
fmt::Write as _,
|
||||||
future::Future,
|
future::Future,
|
||||||
|
path::PathBuf,
|
||||||
sync::{Arc, Weak},
|
sync::{Arc, Weak},
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
|
use telemetry::Telemetry;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
use util::{ResultExt, TryFutureExt};
|
use util::{ResultExt, TryFutureExt};
|
||||||
@ -48,14 +53,21 @@ lazy_static! {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub const ZED_SECRET_CLIENT_TOKEN: &str = "618033988749894";
|
pub const ZED_SECRET_CLIENT_TOKEN: &str = "618033988749894";
|
||||||
|
pub const INITIAL_RECONNECTION_DELAY: Duration = Duration::from_millis(100);
|
||||||
|
pub const CONNECTION_TIMEOUT: Duration = Duration::from_secs(5);
|
||||||
|
|
||||||
actions!(client, [Authenticate]);
|
actions!(client, [Authenticate]);
|
||||||
|
|
||||||
pub fn init(rpc: Arc<Client>, cx: &mut MutableAppContext) {
|
pub fn init(client: Arc<Client>, cx: &mut MutableAppContext) {
|
||||||
cx.add_global_action(move |_: &Authenticate, cx| {
|
cx.add_global_action({
|
||||||
let rpc = rpc.clone();
|
let client = client.clone();
|
||||||
cx.spawn(|cx| async move { rpc.authenticate_and_connect(true, &cx).log_err().await })
|
move |_: &Authenticate, cx| {
|
||||||
|
let client = client.clone();
|
||||||
|
cx.spawn(
|
||||||
|
|cx| async move { client.authenticate_and_connect(true, &cx).log_err().await },
|
||||||
|
)
|
||||||
.detach();
|
.detach();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,6 +75,7 @@ pub struct Client {
|
|||||||
id: usize,
|
id: usize,
|
||||||
peer: Arc<Peer>,
|
peer: Arc<Peer>,
|
||||||
http: Arc<dyn HttpClient>,
|
http: Arc<dyn HttpClient>,
|
||||||
|
telemetry: Arc<Telemetry>,
|
||||||
state: RwLock<ClientState>,
|
state: RwLock<ClientState>,
|
||||||
|
|
||||||
#[allow(clippy::type_complexity)]
|
#[allow(clippy::type_complexity)]
|
||||||
@ -232,10 +245,11 @@ impl Drop for Subscription {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Client {
|
impl Client {
|
||||||
pub fn new(http: Arc<dyn HttpClient>) -> Arc<Self> {
|
pub fn new(http: Arc<dyn HttpClient>, cx: &AppContext) -> Arc<Self> {
|
||||||
Arc::new(Self {
|
Arc::new(Self {
|
||||||
id: 0,
|
id: 0,
|
||||||
peer: Peer::new(),
|
peer: Peer::new(),
|
||||||
|
telemetry: Telemetry::new(http.clone(), cx),
|
||||||
http,
|
http,
|
||||||
state: Default::default(),
|
state: Default::default(),
|
||||||
|
|
||||||
@ -318,7 +332,7 @@ impl Client {
|
|||||||
let reconnect_interval = state.reconnect_interval;
|
let reconnect_interval = state.reconnect_interval;
|
||||||
state._reconnect_task = Some(cx.spawn(|cx| async move {
|
state._reconnect_task = Some(cx.spawn(|cx| async move {
|
||||||
let mut rng = StdRng::from_entropy();
|
let mut rng = StdRng::from_entropy();
|
||||||
let mut delay = Duration::from_millis(100);
|
let mut delay = INITIAL_RECONNECTION_DELAY;
|
||||||
while let Err(error) = this.authenticate_and_connect(true, &cx).await {
|
while let Err(error) = this.authenticate_and_connect(true, &cx).await {
|
||||||
log::error!("failed to connect {}", error);
|
log::error!("failed to connect {}", error);
|
||||||
if matches!(*this.status().borrow(), Status::ConnectionError) {
|
if matches!(*this.status().borrow(), Status::ConnectionError) {
|
||||||
@ -339,6 +353,7 @@ impl Client {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
Status::SignedOut | Status::UpgradeRequired => {
|
Status::SignedOut | Status::UpgradeRequired => {
|
||||||
|
self.telemetry.set_authenticated_user_info(None, false);
|
||||||
state._reconnect_task.take();
|
state._reconnect_task.take();
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
@ -421,6 +436,29 @@ impl Client {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn add_request_handler<M, E, H, F>(
|
||||||
|
self: &Arc<Self>,
|
||||||
|
model: ModelHandle<E>,
|
||||||
|
handler: H,
|
||||||
|
) -> Subscription
|
||||||
|
where
|
||||||
|
M: RequestMessage,
|
||||||
|
E: Entity,
|
||||||
|
H: 'static
|
||||||
|
+ Send
|
||||||
|
+ Sync
|
||||||
|
+ Fn(ModelHandle<E>, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
|
||||||
|
F: 'static + Future<Output = Result<M::Response>>,
|
||||||
|
{
|
||||||
|
self.add_message_handler(model, move |handle, envelope, this, cx| {
|
||||||
|
Self::respond_to_request(
|
||||||
|
envelope.receipt(),
|
||||||
|
handler(handle, envelope, this.clone(), cx),
|
||||||
|
this,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn add_view_message_handler<M, E, H, F>(self: &Arc<Self>, handler: H)
|
pub fn add_view_message_handler<M, E, H, F>(self: &Arc<Self>, handler: H)
|
||||||
where
|
where
|
||||||
M: EntityMessage,
|
M: EntityMessage,
|
||||||
@ -595,6 +633,9 @@ impl Client {
|
|||||||
if credentials.is_none() && try_keychain {
|
if credentials.is_none() && try_keychain {
|
||||||
credentials = read_credentials_from_keychain(cx);
|
credentials = read_credentials_from_keychain(cx);
|
||||||
read_from_keychain = credentials.is_some();
|
read_from_keychain = credentials.is_some();
|
||||||
|
if read_from_keychain {
|
||||||
|
self.report_event("read credentials from keychain", Default::default());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if credentials.is_none() {
|
if credentials.is_none() {
|
||||||
let mut status_rx = self.status();
|
let mut status_rx = self.status();
|
||||||
@ -622,13 +663,15 @@ impl Client {
|
|||||||
self.set_status(Status::Reconnecting, cx);
|
self.set_status(Status::Reconnecting, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
match self.establish_connection(&credentials, cx).await {
|
futures::select_biased! {
|
||||||
|
connection = self.establish_connection(&credentials, cx).fuse() => {
|
||||||
|
match connection {
|
||||||
Ok(conn) => {
|
Ok(conn) => {
|
||||||
self.state.write().credentials = Some(credentials.clone());
|
self.state.write().credentials = Some(credentials.clone());
|
||||||
if !read_from_keychain && IMPERSONATE_LOGIN.is_none() {
|
if !read_from_keychain && IMPERSONATE_LOGIN.is_none() {
|
||||||
write_credentials_to_keychain(&credentials, cx).log_err();
|
write_credentials_to_keychain(&credentials, cx).log_err();
|
||||||
}
|
}
|
||||||
self.set_connection(conn, cx).await;
|
self.set_connection(conn, cx);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Err(EstablishConnectionError::Unauthorized) => {
|
Err(EstablishConnectionError::Unauthorized) => {
|
||||||
@ -652,14 +695,19 @@ impl Client {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
_ = cx.background().timer(CONNECTION_TIMEOUT).fuse() => {
|
||||||
|
self.set_status(Status::ConnectionError, cx);
|
||||||
|
Err(anyhow!("timed out trying to establish connection"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn set_connection(self: &Arc<Self>, conn: Connection, cx: &AsyncAppContext) {
|
fn set_connection(self: &Arc<Self>, conn: Connection, cx: &AsyncAppContext) {
|
||||||
let executor = cx.background();
|
let executor = cx.background();
|
||||||
log::info!("add connection to peer");
|
log::info!("add connection to peer");
|
||||||
let (connection_id, handle_io, mut incoming) = self
|
let (connection_id, handle_io, mut incoming) = self
|
||||||
.peer
|
.peer
|
||||||
.add_connection(conn, move |duration| executor.timer(duration))
|
.add_connection(conn, move |duration| executor.timer(duration));
|
||||||
.await;
|
|
||||||
log::info!("set status to connected {}", connection_id);
|
log::info!("set status to connected {}", connection_id);
|
||||||
self.set_status(Status::Connected { connection_id }, cx);
|
self.set_status(Status::Connected { connection_id }, cx);
|
||||||
cx.foreground()
|
cx.foreground()
|
||||||
@ -878,6 +926,7 @@ impl Client {
|
|||||||
) -> Task<Result<Credentials>> {
|
) -> Task<Result<Credentials>> {
|
||||||
let platform = cx.platform();
|
let platform = cx.platform();
|
||||||
let executor = cx.background();
|
let executor = cx.background();
|
||||||
|
let telemetry = self.telemetry.clone();
|
||||||
executor.clone().spawn(async move {
|
executor.clone().spawn(async move {
|
||||||
// Generate a pair of asymmetric encryption keys. The public key will be used by the
|
// Generate a pair of asymmetric encryption keys. The public key will be used by the
|
||||||
// zed server to encrypt the user's access token, so that it can'be intercepted by
|
// zed server to encrypt the user's access token, so that it can'be intercepted by
|
||||||
@ -956,6 +1005,8 @@ impl Client {
|
|||||||
.context("failed to decrypt access token")?;
|
.context("failed to decrypt access token")?;
|
||||||
platform.activate(true);
|
platform.activate(true);
|
||||||
|
|
||||||
|
telemetry.report_event("authenticate with browser", Default::default());
|
||||||
|
|
||||||
Ok(Credentials {
|
Ok(Credentials {
|
||||||
user_id: user_id.parse()?,
|
user_id: user_id.parse()?,
|
||||||
access_token,
|
access_token,
|
||||||
@ -1020,6 +1071,18 @@ impl Client {
|
|||||||
log::debug!("rpc respond. client_id:{}. name:{}", self.id, T::NAME);
|
log::debug!("rpc respond. client_id:{}. name:{}", self.id, T::NAME);
|
||||||
self.peer.respond_with_error(receipt, error)
|
self.peer.respond_with_error(receipt, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn start_telemetry(&self, db: Arc<Db>) {
|
||||||
|
self.telemetry.start(db);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn report_event(&self, kind: &str, properties: Value) {
|
||||||
|
self.telemetry.report_event(kind, properties)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn telemetry_log_file_path(&self) -> Option<PathBuf> {
|
||||||
|
self.telemetry.log_file_path()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AnyWeakEntityHandle {
|
impl AnyWeakEntityHandle {
|
||||||
@ -1085,7 +1148,7 @@ mod tests {
|
|||||||
cx.foreground().forbid_parking();
|
cx.foreground().forbid_parking();
|
||||||
|
|
||||||
let user_id = 5;
|
let user_id = 5;
|
||||||
let client = Client::new(FakeHttpClient::with_404_response());
|
let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
|
||||||
let server = FakeServer::for_client(user_id, &client, cx).await;
|
let server = FakeServer::for_client(user_id, &client, cx).await;
|
||||||
let mut status = client.status();
|
let mut status = client.status();
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
@ -1115,6 +1178,76 @@ mod tests {
|
|||||||
assert_eq!(server.auth_count(), 2); // Client re-authenticated due to an invalid token
|
assert_eq!(server.auth_count(), 2); // Client re-authenticated due to an invalid token
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test(iterations = 10)]
|
||||||
|
async fn test_connection_timeout(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
|
||||||
|
deterministic.forbid_parking();
|
||||||
|
|
||||||
|
let user_id = 5;
|
||||||
|
let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
|
||||||
|
let mut status = client.status();
|
||||||
|
|
||||||
|
// Time out when client tries to connect.
|
||||||
|
client.override_authenticate(move |cx| {
|
||||||
|
cx.foreground().spawn(async move {
|
||||||
|
Ok(Credentials {
|
||||||
|
user_id,
|
||||||
|
access_token: "token".into(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
client.override_establish_connection(|_, cx| {
|
||||||
|
cx.foreground().spawn(async move {
|
||||||
|
future::pending::<()>().await;
|
||||||
|
unreachable!()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
let auth_and_connect = cx.spawn({
|
||||||
|
let client = client.clone();
|
||||||
|
|cx| async move { client.authenticate_and_connect(false, &cx).await }
|
||||||
|
});
|
||||||
|
deterministic.run_until_parked();
|
||||||
|
assert!(matches!(status.next().await, Some(Status::Connecting)));
|
||||||
|
|
||||||
|
deterministic.advance_clock(CONNECTION_TIMEOUT);
|
||||||
|
assert!(matches!(
|
||||||
|
status.next().await,
|
||||||
|
Some(Status::ConnectionError { .. })
|
||||||
|
));
|
||||||
|
auth_and_connect.await.unwrap_err();
|
||||||
|
|
||||||
|
// Allow the connection to be established.
|
||||||
|
let server = FakeServer::for_client(user_id, &client, cx).await;
|
||||||
|
assert!(matches!(
|
||||||
|
status.next().await,
|
||||||
|
Some(Status::Connected { .. })
|
||||||
|
));
|
||||||
|
|
||||||
|
// Disconnect client.
|
||||||
|
server.forbid_connections();
|
||||||
|
server.disconnect();
|
||||||
|
while !matches!(status.next().await, Some(Status::ReconnectionError { .. })) {}
|
||||||
|
|
||||||
|
// Time out when re-establishing the connection.
|
||||||
|
server.allow_connections();
|
||||||
|
client.override_establish_connection(|_, cx| {
|
||||||
|
cx.foreground().spawn(async move {
|
||||||
|
future::pending::<()>().await;
|
||||||
|
unreachable!()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
deterministic.advance_clock(2 * INITIAL_RECONNECTION_DELAY);
|
||||||
|
assert!(matches!(
|
||||||
|
status.next().await,
|
||||||
|
Some(Status::Reconnecting { .. })
|
||||||
|
));
|
||||||
|
|
||||||
|
deterministic.advance_clock(CONNECTION_TIMEOUT);
|
||||||
|
assert!(matches!(
|
||||||
|
status.next().await,
|
||||||
|
Some(Status::ReconnectionError { .. })
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test(iterations = 10)]
|
#[gpui::test(iterations = 10)]
|
||||||
async fn test_authenticating_more_than_once(
|
async fn test_authenticating_more_than_once(
|
||||||
cx: &mut TestAppContext,
|
cx: &mut TestAppContext,
|
||||||
@ -1124,7 +1257,7 @@ mod tests {
|
|||||||
|
|
||||||
let auth_count = Arc::new(Mutex::new(0));
|
let auth_count = Arc::new(Mutex::new(0));
|
||||||
let dropped_auth_count = Arc::new(Mutex::new(0));
|
let dropped_auth_count = Arc::new(Mutex::new(0));
|
||||||
let client = Client::new(FakeHttpClient::with_404_response());
|
let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
|
||||||
client.override_authenticate({
|
client.override_authenticate({
|
||||||
let auth_count = auth_count.clone();
|
let auth_count = auth_count.clone();
|
||||||
let dropped_auth_count = dropped_auth_count.clone();
|
let dropped_auth_count = dropped_auth_count.clone();
|
||||||
@ -1173,7 +1306,7 @@ mod tests {
|
|||||||
cx.foreground().forbid_parking();
|
cx.foreground().forbid_parking();
|
||||||
|
|
||||||
let user_id = 5;
|
let user_id = 5;
|
||||||
let client = Client::new(FakeHttpClient::with_404_response());
|
let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
|
||||||
let server = FakeServer::for_client(user_id, &client, cx).await;
|
let server = FakeServer::for_client(user_id, &client, cx).await;
|
||||||
|
|
||||||
let (done_tx1, mut done_rx1) = smol::channel::unbounded();
|
let (done_tx1, mut done_rx1) = smol::channel::unbounded();
|
||||||
@ -1219,7 +1352,7 @@ mod tests {
|
|||||||
cx.foreground().forbid_parking();
|
cx.foreground().forbid_parking();
|
||||||
|
|
||||||
let user_id = 5;
|
let user_id = 5;
|
||||||
let client = Client::new(FakeHttpClient::with_404_response());
|
let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
|
||||||
let server = FakeServer::for_client(user_id, &client, cx).await;
|
let server = FakeServer::for_client(user_id, &client, cx).await;
|
||||||
|
|
||||||
let model = cx.add_model(|_| Model::default());
|
let model = cx.add_model(|_| Model::default());
|
||||||
@ -1247,7 +1380,7 @@ mod tests {
|
|||||||
cx.foreground().forbid_parking();
|
cx.foreground().forbid_parking();
|
||||||
|
|
||||||
let user_id = 5;
|
let user_id = 5;
|
||||||
let client = Client::new(FakeHttpClient::with_404_response());
|
let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
|
||||||
let server = FakeServer::for_client(user_id, &client, cx).await;
|
let server = FakeServer::for_client(user_id, &client, cx).await;
|
||||||
|
|
||||||
let model = cx.add_model(|_| Model::default());
|
let model = cx.add_model(|_| Model::default());
|
||||||
|
283
crates/client/src/telemetry.rs
Normal file
283
crates/client/src/telemetry.rs
Normal file
@ -0,0 +1,283 @@
|
|||||||
|
use crate::http::HttpClient;
|
||||||
|
use db::Db;
|
||||||
|
use gpui::{
|
||||||
|
executor::Background,
|
||||||
|
serde_json::{self, value::Map, Value},
|
||||||
|
AppContext, Task,
|
||||||
|
};
|
||||||
|
use isahc::Request;
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use parking_lot::Mutex;
|
||||||
|
use serde::Serialize;
|
||||||
|
use serde_json::json;
|
||||||
|
use std::{
|
||||||
|
io::Write,
|
||||||
|
mem,
|
||||||
|
path::PathBuf,
|
||||||
|
sync::Arc,
|
||||||
|
time::{Duration, SystemTime, UNIX_EPOCH},
|
||||||
|
};
|
||||||
|
use tempfile::NamedTempFile;
|
||||||
|
use util::{post_inc, ResultExt, TryFutureExt};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
pub struct Telemetry {
|
||||||
|
http_client: Arc<dyn HttpClient>,
|
||||||
|
executor: Arc<Background>,
|
||||||
|
session_id: u128,
|
||||||
|
state: Mutex<TelemetryState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct TelemetryState {
|
||||||
|
metrics_id: Option<Arc<str>>,
|
||||||
|
device_id: Option<Arc<str>>,
|
||||||
|
app_version: Option<Arc<str>>,
|
||||||
|
os_version: Option<Arc<str>>,
|
||||||
|
os_name: &'static str,
|
||||||
|
queue: Vec<AmplitudeEvent>,
|
||||||
|
next_event_id: usize,
|
||||||
|
flush_task: Option<Task<()>>,
|
||||||
|
log_file: Option<NamedTempFile>,
|
||||||
|
}
|
||||||
|
|
||||||
|
const AMPLITUDE_EVENTS_URL: &'static str = "https://api2.amplitude.com/batch";
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref AMPLITUDE_API_KEY: Option<String> = std::env::var("ZED_AMPLITUDE_API_KEY")
|
||||||
|
.ok()
|
||||||
|
.or_else(|| option_env!("ZED_AMPLITUDE_API_KEY").map(|key| key.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct AmplitudeEventBatch {
|
||||||
|
api_key: &'static str,
|
||||||
|
events: Vec<AmplitudeEvent>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct AmplitudeEvent {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
user_id: Option<Arc<str>>,
|
||||||
|
device_id: Option<Arc<str>>,
|
||||||
|
event_type: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
event_properties: Option<Map<String, Value>>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
user_properties: Option<Map<String, Value>>,
|
||||||
|
os_name: &'static str,
|
||||||
|
os_version: Option<Arc<str>>,
|
||||||
|
app_version: Option<Arc<str>>,
|
||||||
|
platform: &'static str,
|
||||||
|
event_id: usize,
|
||||||
|
session_id: u128,
|
||||||
|
time: u128,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
const MAX_QUEUE_LEN: usize = 1;
|
||||||
|
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
const MAX_QUEUE_LEN: usize = 10;
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
const DEBOUNCE_INTERVAL: Duration = Duration::from_secs(1);
|
||||||
|
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
const DEBOUNCE_INTERVAL: Duration = Duration::from_secs(30);
|
||||||
|
|
||||||
|
impl Telemetry {
|
||||||
|
pub fn new(client: Arc<dyn HttpClient>, cx: &AppContext) -> Arc<Self> {
|
||||||
|
let platform = cx.platform();
|
||||||
|
let this = Arc::new(Self {
|
||||||
|
http_client: client,
|
||||||
|
executor: cx.background().clone(),
|
||||||
|
session_id: SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_millis(),
|
||||||
|
state: Mutex::new(TelemetryState {
|
||||||
|
os_version: platform
|
||||||
|
.os_version()
|
||||||
|
.log_err()
|
||||||
|
.map(|v| v.to_string().into()),
|
||||||
|
os_name: platform.os_name().into(),
|
||||||
|
app_version: platform
|
||||||
|
.app_version()
|
||||||
|
.log_err()
|
||||||
|
.map(|v| v.to_string().into()),
|
||||||
|
device_id: None,
|
||||||
|
queue: Default::default(),
|
||||||
|
flush_task: Default::default(),
|
||||||
|
next_event_id: 0,
|
||||||
|
log_file: None,
|
||||||
|
metrics_id: None,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if AMPLITUDE_API_KEY.is_some() {
|
||||||
|
this.executor
|
||||||
|
.spawn({
|
||||||
|
let this = this.clone();
|
||||||
|
async move {
|
||||||
|
if let Some(tempfile) = NamedTempFile::new().log_err() {
|
||||||
|
this.state.lock().log_file = Some(tempfile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
this
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn log_file_path(&self) -> Option<PathBuf> {
|
||||||
|
Some(self.state.lock().log_file.as_ref()?.path().to_path_buf())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start(self: &Arc<Self>, db: Arc<Db>) {
|
||||||
|
let this = self.clone();
|
||||||
|
self.executor
|
||||||
|
.spawn(
|
||||||
|
async move {
|
||||||
|
let device_id = if let Some(device_id) = db
|
||||||
|
.read(["device_id"])?
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.next()
|
||||||
|
.and_then(|bytes| String::from_utf8(bytes).ok())
|
||||||
|
{
|
||||||
|
device_id
|
||||||
|
} else {
|
||||||
|
let device_id = Uuid::new_v4().to_string();
|
||||||
|
db.write([("device_id", device_id.as_bytes())])?;
|
||||||
|
device_id
|
||||||
|
};
|
||||||
|
|
||||||
|
let device_id = Some(Arc::from(device_id));
|
||||||
|
let mut state = this.state.lock();
|
||||||
|
state.device_id = device_id.clone();
|
||||||
|
for event in &mut state.queue {
|
||||||
|
event.device_id = device_id.clone();
|
||||||
|
}
|
||||||
|
if !state.queue.is_empty() {
|
||||||
|
drop(state);
|
||||||
|
this.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
anyhow::Ok(())
|
||||||
|
}
|
||||||
|
.log_err(),
|
||||||
|
)
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_authenticated_user_info(
|
||||||
|
self: &Arc<Self>,
|
||||||
|
metrics_id: Option<String>,
|
||||||
|
is_staff: bool,
|
||||||
|
) {
|
||||||
|
let is_signed_in = metrics_id.is_some();
|
||||||
|
self.state.lock().metrics_id = metrics_id.map(|s| s.into());
|
||||||
|
if is_signed_in {
|
||||||
|
self.report_event_with_user_properties(
|
||||||
|
"$identify",
|
||||||
|
Default::default(),
|
||||||
|
json!({ "$set": { "staff": is_staff } }),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn report_event(self: &Arc<Self>, kind: &str, properties: Value) {
|
||||||
|
self.report_event_with_user_properties(kind, properties, Default::default());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn report_event_with_user_properties(
|
||||||
|
self: &Arc<Self>,
|
||||||
|
kind: &str,
|
||||||
|
properties: Value,
|
||||||
|
user_properties: Value,
|
||||||
|
) {
|
||||||
|
if AMPLITUDE_API_KEY.is_none() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut state = self.state.lock();
|
||||||
|
let event = AmplitudeEvent {
|
||||||
|
event_type: kind.to_string(),
|
||||||
|
time: SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_millis(),
|
||||||
|
session_id: self.session_id,
|
||||||
|
event_properties: if let Value::Object(properties) = properties {
|
||||||
|
Some(properties)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
user_properties: if let Value::Object(user_properties) = user_properties {
|
||||||
|
Some(user_properties)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
user_id: state.metrics_id.clone(),
|
||||||
|
device_id: state.device_id.clone(),
|
||||||
|
os_name: state.os_name,
|
||||||
|
platform: "Zed",
|
||||||
|
os_version: state.os_version.clone(),
|
||||||
|
app_version: state.app_version.clone(),
|
||||||
|
event_id: post_inc(&mut state.next_event_id),
|
||||||
|
};
|
||||||
|
state.queue.push(event);
|
||||||
|
if state.device_id.is_some() {
|
||||||
|
if state.queue.len() >= MAX_QUEUE_LEN {
|
||||||
|
drop(state);
|
||||||
|
self.flush();
|
||||||
|
} else {
|
||||||
|
let this = self.clone();
|
||||||
|
let executor = self.executor.clone();
|
||||||
|
state.flush_task = Some(self.executor.spawn(async move {
|
||||||
|
executor.timer(DEBOUNCE_INTERVAL).await;
|
||||||
|
this.flush();
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(self: &Arc<Self>) {
|
||||||
|
let mut state = self.state.lock();
|
||||||
|
let events = mem::take(&mut state.queue);
|
||||||
|
state.flush_task.take();
|
||||||
|
drop(state);
|
||||||
|
|
||||||
|
if let Some(api_key) = AMPLITUDE_API_KEY.as_ref() {
|
||||||
|
let this = self.clone();
|
||||||
|
self.executor
|
||||||
|
.spawn(
|
||||||
|
async move {
|
||||||
|
let mut json_bytes = Vec::new();
|
||||||
|
|
||||||
|
if let Some(file) = &mut this.state.lock().log_file {
|
||||||
|
let file = file.as_file_mut();
|
||||||
|
for event in &events {
|
||||||
|
json_bytes.clear();
|
||||||
|
serde_json::to_writer(&mut json_bytes, event)?;
|
||||||
|
file.write_all(&json_bytes)?;
|
||||||
|
file.write(b"\n")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let batch = AmplitudeEventBatch { api_key, events };
|
||||||
|
json_bytes.clear();
|
||||||
|
serde_json::to_writer(&mut json_bytes, &batch)?;
|
||||||
|
let request =
|
||||||
|
Request::post(AMPLITUDE_EVENTS_URL).body(json_bytes.into())?;
|
||||||
|
this.http_client.send(request).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
.log_err(),
|
||||||
|
)
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -6,7 +6,10 @@ use anyhow::{anyhow, Result};
|
|||||||
use futures::{future::BoxFuture, stream::BoxStream, Future, StreamExt};
|
use futures::{future::BoxFuture, stream::BoxStream, Future, StreamExt};
|
||||||
use gpui::{executor, ModelHandle, TestAppContext};
|
use gpui::{executor, ModelHandle, TestAppContext};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use rpc::{proto, ConnectionId, Peer, Receipt, TypedEnvelope};
|
use rpc::{
|
||||||
|
proto::{self, GetPrivateUserInfo, GetPrivateUserInfoResponse},
|
||||||
|
ConnectionId, Peer, Receipt, TypedEnvelope,
|
||||||
|
};
|
||||||
use std::{fmt, rc::Rc, sync::Arc};
|
use std::{fmt, rc::Rc, sync::Arc};
|
||||||
|
|
||||||
pub struct FakeServer {
|
pub struct FakeServer {
|
||||||
@ -79,7 +82,7 @@ impl FakeServer {
|
|||||||
|
|
||||||
let (client_conn, server_conn, _) = Connection::in_memory(cx.background());
|
let (client_conn, server_conn, _) = Connection::in_memory(cx.background());
|
||||||
let (connection_id, io, incoming) =
|
let (connection_id, io, incoming) =
|
||||||
peer.add_test_connection(server_conn, cx.background()).await;
|
peer.add_test_connection(server_conn, cx.background());
|
||||||
cx.background().spawn(io).detach();
|
cx.background().spawn(io).detach();
|
||||||
let mut state = state.lock();
|
let mut state = state.lock();
|
||||||
state.connection_id = Some(connection_id);
|
state.connection_id = Some(connection_id);
|
||||||
@ -93,15 +96,18 @@ impl FakeServer {
|
|||||||
.authenticate_and_connect(false, &cx.to_async())
|
.authenticate_and_connect(false, &cx.to_async())
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
server
|
server
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn disconnect(&self) {
|
pub fn disconnect(&self) {
|
||||||
|
if self.state.lock().connection_id.is_some() {
|
||||||
self.peer.disconnect(self.connection_id());
|
self.peer.disconnect(self.connection_id());
|
||||||
let mut state = self.state.lock();
|
let mut state = self.state.lock();
|
||||||
state.connection_id.take();
|
state.connection_id.take();
|
||||||
state.incoming.take();
|
state.incoming.take();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn auth_count(&self) -> usize {
|
pub fn auth_count(&self) -> usize {
|
||||||
self.state.lock().auth_count
|
self.state.lock().auth_count
|
||||||
@ -126,6 +132,8 @@ impl FakeServer {
|
|||||||
#[allow(clippy::await_holding_lock)]
|
#[allow(clippy::await_holding_lock)]
|
||||||
pub async fn receive<M: proto::EnvelopedMessage>(&self) -> Result<TypedEnvelope<M>> {
|
pub async fn receive<M: proto::EnvelopedMessage>(&self) -> Result<TypedEnvelope<M>> {
|
||||||
self.executor.start_waiting();
|
self.executor.start_waiting();
|
||||||
|
|
||||||
|
loop {
|
||||||
let message = self
|
let message = self
|
||||||
.state
|
.state
|
||||||
.lock()
|
.lock()
|
||||||
@ -137,15 +145,32 @@ impl FakeServer {
|
|||||||
.ok_or_else(|| anyhow!("other half hung up"))?;
|
.ok_or_else(|| anyhow!("other half hung up"))?;
|
||||||
self.executor.finish_waiting();
|
self.executor.finish_waiting();
|
||||||
let type_name = message.payload_type_name();
|
let type_name = message.payload_type_name();
|
||||||
Ok(*message
|
let message = message.into_any();
|
||||||
.into_any()
|
|
||||||
.downcast::<TypedEnvelope<M>>()
|
if message.is::<TypedEnvelope<M>>() {
|
||||||
.unwrap_or_else(|_| {
|
return Ok(*message.downcast().unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
if message.is::<TypedEnvelope<GetPrivateUserInfo>>() {
|
||||||
|
self.respond(
|
||||||
|
message
|
||||||
|
.downcast::<TypedEnvelope<GetPrivateUserInfo>>()
|
||||||
|
.unwrap()
|
||||||
|
.receipt(),
|
||||||
|
GetPrivateUserInfoResponse {
|
||||||
|
metrics_id: "the-metrics-id".into(),
|
||||||
|
staff: false,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
panic!(
|
panic!(
|
||||||
"fake server received unexpected message type: {:?}",
|
"fake server received unexpected message type: {:?}",
|
||||||
type_name
|
type_name
|
||||||
);
|
);
|
||||||
}))
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn respond<T: proto::RequestMessage>(
|
pub async fn respond<T: proto::RequestMessage>(
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
use super::{http::HttpClient, proto, Client, Status, TypedEnvelope};
|
use super::{http::HttpClient, proto, Client, Status, TypedEnvelope};
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
use collections::{hash_map::Entry, BTreeSet, HashMap, HashSet};
|
use collections::{hash_map::Entry, HashMap, HashSet};
|
||||||
use futures::{channel::mpsc, future, AsyncReadExt, Future, StreamExt};
|
use futures::{channel::mpsc, future, AsyncReadExt, Future, StreamExt};
|
||||||
use gpui::{AsyncAppContext, Entity, ImageData, ModelContext, ModelHandle, Task};
|
use gpui::{AsyncAppContext, Entity, ImageData, ModelContext, ModelHandle, Task};
|
||||||
use postage::{prelude::Stream, sink::Sink, watch};
|
use postage::{sink::Sink, watch};
|
||||||
use rpc::proto::{RequestMessage, UsersResponse};
|
use rpc::proto::{RequestMessage, UsersResponse};
|
||||||
use std::sync::{Arc, Weak};
|
use std::sync::{Arc, Weak};
|
||||||
use util::TryFutureExt as _;
|
use util::TryFutureExt as _;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Default, Debug)]
|
||||||
pub struct User {
|
pub struct User {
|
||||||
pub id: u64,
|
pub id: u64,
|
||||||
pub github_login: String,
|
pub github_login: String,
|
||||||
@ -39,14 +39,7 @@ impl Eq for User {}
|
|||||||
pub struct Contact {
|
pub struct Contact {
|
||||||
pub user: Arc<User>,
|
pub user: Arc<User>,
|
||||||
pub online: bool,
|
pub online: bool,
|
||||||
pub projects: Vec<ProjectMetadata>,
|
pub busy: bool,
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
|
||||||
pub struct ProjectMetadata {
|
|
||||||
pub id: u64,
|
|
||||||
pub visible_worktree_root_names: Vec<String>,
|
|
||||||
pub guests: BTreeSet<Arc<User>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
@ -138,14 +131,25 @@ impl UserStore {
|
|||||||
}),
|
}),
|
||||||
_maintain_current_user: cx.spawn_weak(|this, mut cx| async move {
|
_maintain_current_user: cx.spawn_weak(|this, mut cx| async move {
|
||||||
let mut status = client.status();
|
let mut status = client.status();
|
||||||
while let Some(status) = status.recv().await {
|
while let Some(status) = status.next().await {
|
||||||
match status {
|
match status {
|
||||||
Status::Connected { .. } => {
|
Status::Connected { .. } => {
|
||||||
if let Some((this, user_id)) = this.upgrade(&cx).zip(client.user_id()) {
|
if let Some((this, user_id)) = this.upgrade(&cx).zip(client.user_id()) {
|
||||||
let user = this
|
let fetch_user = this
|
||||||
.update(&mut cx, |this, cx| this.fetch_user(user_id, cx))
|
.update(&mut cx, |this, cx| this.get_user(user_id, cx))
|
||||||
.log_err()
|
.log_err();
|
||||||
.await;
|
let fetch_metrics_id =
|
||||||
|
client.request(proto::GetPrivateUserInfo {}).log_err();
|
||||||
|
let (user, info) = futures::join!(fetch_user, fetch_metrics_id);
|
||||||
|
if let Some(info) = info {
|
||||||
|
client.telemetry.set_authenticated_user_info(
|
||||||
|
Some(info.metrics_id),
|
||||||
|
info.staff,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
client.telemetry.set_authenticated_user_info(None, false);
|
||||||
|
}
|
||||||
|
client.telemetry.report_event("sign in", Default::default());
|
||||||
current_user_tx.send(user).await.ok();
|
current_user_tx.send(user).await.ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -233,7 +237,6 @@ impl UserStore {
|
|||||||
let mut user_ids = HashSet::default();
|
let mut user_ids = HashSet::default();
|
||||||
for contact in &message.contacts {
|
for contact in &message.contacts {
|
||||||
user_ids.insert(contact.user_id);
|
user_ids.insert(contact.user_id);
|
||||||
user_ids.extend(contact.projects.iter().flat_map(|w| &w.guests).copied());
|
|
||||||
}
|
}
|
||||||
user_ids.extend(message.incoming_requests.iter().map(|req| req.requester_id));
|
user_ids.extend(message.incoming_requests.iter().map(|req| req.requester_id));
|
||||||
user_ids.extend(message.outgoing_requests.iter());
|
user_ids.extend(message.outgoing_requests.iter());
|
||||||
@ -257,9 +260,7 @@ impl UserStore {
|
|||||||
for request in message.incoming_requests {
|
for request in message.incoming_requests {
|
||||||
incoming_requests.push({
|
incoming_requests.push({
|
||||||
let user = this
|
let user = this
|
||||||
.update(&mut cx, |this, cx| {
|
.update(&mut cx, |this, cx| this.get_user(request.requester_id, cx))
|
||||||
this.fetch_user(request.requester_id, cx)
|
|
||||||
})
|
|
||||||
.await?;
|
.await?;
|
||||||
(user, request.should_notify)
|
(user, request.should_notify)
|
||||||
});
|
});
|
||||||
@ -268,7 +269,7 @@ impl UserStore {
|
|||||||
let mut outgoing_requests = Vec::new();
|
let mut outgoing_requests = Vec::new();
|
||||||
for requested_user_id in message.outgoing_requests {
|
for requested_user_id in message.outgoing_requests {
|
||||||
outgoing_requests.push(
|
outgoing_requests.push(
|
||||||
this.update(&mut cx, |this, cx| this.fetch_user(requested_user_id, cx))
|
this.update(&mut cx, |this, cx| this.get_user(requested_user_id, cx))
|
||||||
.await?,
|
.await?,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -493,7 +494,7 @@ impl UserStore {
|
|||||||
.unbounded_send(UpdateContacts::Clear(tx))
|
.unbounded_send(UpdateContacts::Clear(tx))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
async move {
|
async move {
|
||||||
rx.recv().await;
|
rx.next().await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -503,25 +504,43 @@ impl UserStore {
|
|||||||
.unbounded_send(UpdateContacts::Wait(tx))
|
.unbounded_send(UpdateContacts::Wait(tx))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
async move {
|
async move {
|
||||||
rx.recv().await;
|
rx.next().await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_users(
|
pub fn get_users(
|
||||||
&mut self,
|
&mut self,
|
||||||
mut user_ids: Vec<u64>,
|
user_ids: Vec<u64>,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) -> Task<Result<()>> {
|
) -> Task<Result<Vec<Arc<User>>>> {
|
||||||
user_ids.retain(|id| !self.users.contains_key(id));
|
let mut user_ids_to_fetch = user_ids.clone();
|
||||||
if user_ids.is_empty() {
|
user_ids_to_fetch.retain(|id| !self.users.contains_key(id));
|
||||||
Task::ready(Ok(()))
|
|
||||||
} else {
|
cx.spawn(|this, mut cx| async move {
|
||||||
let load = self.load_users(proto::GetUsers { user_ids }, cx);
|
if !user_ids_to_fetch.is_empty() {
|
||||||
cx.foreground().spawn(async move {
|
this.update(&mut cx, |this, cx| {
|
||||||
load.await?;
|
this.load_users(
|
||||||
Ok(())
|
proto::GetUsers {
|
||||||
|
user_ids: user_ids_to_fetch,
|
||||||
|
},
|
||||||
|
cx,
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.read_with(&cx, |this, _| {
|
||||||
|
user_ids
|
||||||
|
.iter()
|
||||||
|
.map(|user_id| {
|
||||||
|
this.users
|
||||||
|
.get(user_id)
|
||||||
|
.cloned()
|
||||||
|
.ok_or_else(|| anyhow!("user {} not found", user_id))
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn fuzzy_search_users(
|
pub fn fuzzy_search_users(
|
||||||
@ -532,7 +551,7 @@ impl UserStore {
|
|||||||
self.load_users(proto::FuzzySearchUsers { query }, cx)
|
self.load_users(proto::FuzzySearchUsers { query }, cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn fetch_user(
|
pub fn get_user(
|
||||||
&mut self,
|
&mut self,
|
||||||
user_id: u64,
|
user_id: u64,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
@ -612,39 +631,15 @@ impl Contact {
|
|||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
let user = user_store
|
let user = user_store
|
||||||
.update(cx, |user_store, cx| {
|
.update(cx, |user_store, cx| {
|
||||||
user_store.fetch_user(contact.user_id, cx)
|
user_store.get_user(contact.user_id, cx)
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
let mut projects = Vec::new();
|
|
||||||
for project in contact.projects {
|
|
||||||
let mut guests = BTreeSet::new();
|
|
||||||
for participant_id in project.guests {
|
|
||||||
guests.insert(
|
|
||||||
user_store
|
|
||||||
.update(cx, |user_store, cx| {
|
|
||||||
user_store.fetch_user(participant_id, cx)
|
|
||||||
})
|
|
||||||
.await?,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
projects.push(ProjectMetadata {
|
|
||||||
id: project.id,
|
|
||||||
visible_worktree_root_names: project.visible_worktree_root_names.clone(),
|
|
||||||
guests,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
user,
|
user,
|
||||||
online: contact.online,
|
online: contact.online,
|
||||||
projects,
|
busy: contact.busy,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn non_empty_projects(&self) -> impl Iterator<Item = &ProjectMetadata> {
|
|
||||||
self.projects
|
|
||||||
.iter()
|
|
||||||
.filter(|project| !project.visible_worktree_root_names.is_empty())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn fetch_avatar(http: &dyn HttpClient, url: &str) -> Result<Arc<ImageData>> {
|
async fn fetch_avatar(http: &dyn HttpClient, url: &str) -> Result<Arc<ImageData>> {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
[package]
|
[package]
|
||||||
authors = ["Nathan Sobo <nathan@warp.dev>"]
|
authors = ["Nathan Sobo <nathan@zed.dev>"]
|
||||||
default-run = "collab"
|
default-run = "collab"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
name = "collab"
|
name = "collab"
|
||||||
@ -16,7 +16,6 @@ required-features = ["seed-support"]
|
|||||||
collections = { path = "../collections" }
|
collections = { path = "../collections" }
|
||||||
rpc = { path = "../rpc" }
|
rpc = { path = "../rpc" }
|
||||||
util = { path = "../util" }
|
util = { path = "../util" }
|
||||||
|
|
||||||
anyhow = "1.0.40"
|
anyhow = "1.0.40"
|
||||||
async-trait = "0.1.50"
|
async-trait = "0.1.50"
|
||||||
async-tungstenite = "0.16"
|
async-tungstenite = "0.16"
|
||||||
@ -55,13 +54,16 @@ features = ["runtime-tokio-rustls", "postgres", "time", "uuid"]
|
|||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
collections = { path = "../collections", features = ["test-support"] }
|
collections = { path = "../collections", features = ["test-support"] }
|
||||||
gpui = { path = "../gpui", features = ["test-support"] }
|
gpui = { path = "../gpui", features = ["test-support"] }
|
||||||
rpc = { path = "../rpc", features = ["test-support"] }
|
call = { path = "../call", features = ["test-support"] }
|
||||||
client = { path = "../client", features = ["test-support"] }
|
client = { path = "../client", features = ["test-support"] }
|
||||||
editor = { path = "../editor", features = ["test-support"] }
|
editor = { path = "../editor", features = ["test-support"] }
|
||||||
language = { path = "../language", features = ["test-support"] }
|
language = { path = "../language", features = ["test-support"] }
|
||||||
|
fs = { path = "../fs", features = ["test-support"] }
|
||||||
|
git = { path = "../git", features = ["test-support"] }
|
||||||
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
|
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
|
||||||
lsp = { path = "../lsp", features = ["test-support"] }
|
lsp = { path = "../lsp", features = ["test-support"] }
|
||||||
project = { path = "../project", features = ["test-support"] }
|
project = { path = "../project", features = ["test-support"] }
|
||||||
|
rpc = { path = "../rpc", features = ["test-support"] }
|
||||||
settings = { path = "../settings", features = ["test-support"] }
|
settings = { path = "../settings", features = ["test-support"] }
|
||||||
theme = { path = "../theme" }
|
theme = { path = "../theme" }
|
||||||
workspace = { path = "../workspace", features = ["test-support"] }
|
workspace = { path = "../workspace", features = ["test-support"] }
|
||||||
@ -70,6 +72,7 @@ env_logger = "0.9"
|
|||||||
util = { path = "../util" }
|
util = { path = "../util" }
|
||||||
lazy_static = "1.4"
|
lazy_static = "1.4"
|
||||||
serde_json = { version = "1.0", features = ["preserve_order"] }
|
serde_json = { version = "1.0", features = ["preserve_order"] }
|
||||||
|
unindent = "0.1"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
seed-support = ["clap", "lipsum", "reqwest"]
|
seed-support = ["clap", "lipsum", "reqwest"]
|
||||||
|
27
crates/collab/migrations/20220913211150_create_signups.sql
Normal file
27
crates/collab/migrations/20220913211150_create_signups.sql
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS "signups" (
|
||||||
|
"id" SERIAL PRIMARY KEY,
|
||||||
|
"email_address" VARCHAR NOT NULL,
|
||||||
|
"email_confirmation_code" VARCHAR(64) NOT NULL,
|
||||||
|
"email_confirmation_sent" BOOLEAN NOT NULL,
|
||||||
|
"created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"device_id" VARCHAR,
|
||||||
|
"user_id" INTEGER REFERENCES users (id) ON DELETE CASCADE,
|
||||||
|
"inviting_user_id" INTEGER REFERENCES users (id) ON DELETE SET NULL,
|
||||||
|
|
||||||
|
"platform_mac" BOOLEAN NOT NULL,
|
||||||
|
"platform_linux" BOOLEAN NOT NULL,
|
||||||
|
"platform_windows" BOOLEAN NOT NULL,
|
||||||
|
"platform_unknown" BOOLEAN NOT NULL,
|
||||||
|
|
||||||
|
"editor_features" VARCHAR[],
|
||||||
|
"programming_languages" VARCHAR[]
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX "index_signups_on_email_address" ON "signups" ("email_address");
|
||||||
|
CREATE INDEX "index_signups_on_email_confirmation_sent" ON "signups" ("email_confirmation_sent");
|
||||||
|
|
||||||
|
ALTER TABLE "users"
|
||||||
|
ADD "github_user_id" INTEGER;
|
||||||
|
|
||||||
|
CREATE INDEX "index_users_on_email_address" ON "users" ("email_address");
|
||||||
|
CREATE INDEX "index_users_on_github_user_id" ON "users" ("github_user_id");
|
@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE "users"
|
||||||
|
ADD "metrics_id" uuid NOT NULL DEFAULT gen_random_uuid();
|
@ -1,6 +1,6 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
auth,
|
auth,
|
||||||
db::{ProjectId, User, UserId},
|
db::{Invite, NewUserParams, ProjectId, Signup, User, UserId, WaitlistSummary},
|
||||||
rpc::{self, ResultExt},
|
rpc::{self, ResultExt},
|
||||||
AppState, Error, Result,
|
AppState, Error, Result,
|
||||||
};
|
};
|
||||||
@ -24,13 +24,10 @@ use tracing::instrument;
|
|||||||
|
|
||||||
pub fn routes(rpc_server: &Arc<rpc::Server>, state: Arc<AppState>) -> Router<Body> {
|
pub fn routes(rpc_server: &Arc<rpc::Server>, state: Arc<AppState>) -> Router<Body> {
|
||||||
Router::new()
|
Router::new()
|
||||||
|
.route("/user", get(get_authenticated_user))
|
||||||
.route("/users", get(get_users).post(create_user))
|
.route("/users", get(get_users).post(create_user))
|
||||||
.route(
|
.route("/users/:id", put(update_user).delete(destroy_user))
|
||||||
"/users/:id",
|
|
||||||
put(update_user).delete(destroy_user).get(get_user),
|
|
||||||
)
|
|
||||||
.route("/users/:id/access_tokens", post(create_access_token))
|
.route("/users/:id/access_tokens", post(create_access_token))
|
||||||
.route("/bulk_users", post(create_users))
|
|
||||||
.route("/users_with_no_invites", get(get_users_with_no_invites))
|
.route("/users_with_no_invites", get(get_users_with_no_invites))
|
||||||
.route("/invite_codes/:code", get(get_user_for_invite_code))
|
.route("/invite_codes/:code", get(get_user_for_invite_code))
|
||||||
.route("/panic", post(trace_panic))
|
.route("/panic", post(trace_panic))
|
||||||
@ -45,6 +42,11 @@ pub fn routes(rpc_server: &Arc<rpc::Server>, state: Arc<AppState>) -> Router<Bod
|
|||||||
)
|
)
|
||||||
.route("/user_activity/counts", get(get_active_user_counts))
|
.route("/user_activity/counts", get(get_active_user_counts))
|
||||||
.route("/project_metadata", get(get_project_metadata))
|
.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))
|
||||||
|
.route("/unsent_invites", get(get_unsent_invites))
|
||||||
|
.route("/sent_invites", post(record_sent_invites))
|
||||||
.layer(
|
.layer(
|
||||||
ServiceBuilder::new()
|
ServiceBuilder::new()
|
||||||
.layer(Extension(state))
|
.layer(Extension(state))
|
||||||
@ -84,6 +86,31 @@ pub async fn validate_api_token<B>(req: Request<B>, next: Next<B>) -> impl IntoR
|
|||||||
Ok::<_, Error>(next.run(req).await)
|
Ok::<_, Error>(next.run(req).await)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct AuthenticatedUserParams {
|
||||||
|
github_user_id: i32,
|
||||||
|
github_login: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct AuthenticatedUserResponse {
|
||||||
|
user: User,
|
||||||
|
metrics_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_authenticated_user(
|
||||||
|
Query(params): Query<AuthenticatedUserParams>,
|
||||||
|
Extension(app): Extension<Arc<AppState>>,
|
||||||
|
) -> Result<Json<AuthenticatedUserResponse>> {
|
||||||
|
let user = app
|
||||||
|
.db
|
||||||
|
.get_user_by_github_account(¶ms.github_login, Some(params.github_user_id))
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| Error::Http(StatusCode::NOT_FOUND, "user not found".into()))?;
|
||||||
|
let metrics_id = app.db.get_user_metrics_id(user.id).await?;
|
||||||
|
return Ok(Json(AuthenticatedUserResponse { user, metrics_id }));
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct GetUsersQueryParams {
|
struct GetUsersQueryParams {
|
||||||
query: Option<String>,
|
query: Option<String>,
|
||||||
@ -108,48 +135,76 @@ async fn get_users(
|
|||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
struct CreateUserParams {
|
struct CreateUserParams {
|
||||||
|
github_user_id: i32,
|
||||||
github_login: String,
|
github_login: String,
|
||||||
invite_code: Option<String>,
|
email_address: String,
|
||||||
email_address: Option<String>,
|
email_confirmation_code: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
admin: bool,
|
admin: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
invite_count: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Debug)]
|
||||||
|
struct CreateUserResponse {
|
||||||
|
user: User,
|
||||||
|
signup_device_id: Option<String>,
|
||||||
|
metrics_id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn create_user(
|
async fn create_user(
|
||||||
Json(params): Json<CreateUserParams>,
|
Json(params): Json<CreateUserParams>,
|
||||||
Extension(app): Extension<Arc<AppState>>,
|
Extension(app): Extension<Arc<AppState>>,
|
||||||
Extension(rpc_server): Extension<Arc<rpc::Server>>,
|
Extension(rpc_server): Extension<Arc<rpc::Server>>,
|
||||||
) -> Result<Json<User>> {
|
) -> Result<Json<CreateUserResponse>> {
|
||||||
let user_id = if let Some(invite_code) = params.invite_code {
|
let user = NewUserParams {
|
||||||
let invitee_id = app
|
github_login: params.github_login,
|
||||||
.db
|
github_user_id: params.github_user_id,
|
||||||
.redeem_invite_code(
|
invite_count: params.invite_count,
|
||||||
&invite_code,
|
};
|
||||||
¶ms.github_login,
|
|
||||||
params.email_address.as_deref(),
|
// Creating a user via the normal signup process
|
||||||
)
|
let result = if let Some(email_confirmation_code) = params.email_confirmation_code {
|
||||||
.await?;
|
|
||||||
rpc_server
|
|
||||||
.invite_code_redeemed(&invite_code, invitee_id)
|
|
||||||
.await
|
|
||||||
.trace_err();
|
|
||||||
invitee_id
|
|
||||||
} else {
|
|
||||||
app.db
|
app.db
|
||||||
.create_user(
|
.create_user_from_invite(
|
||||||
¶ms.github_login,
|
&Invite {
|
||||||
params.email_address.as_deref(),
|
email_address: params.email_address,
|
||||||
params.admin,
|
email_confirmation_code,
|
||||||
|
},
|
||||||
|
user,
|
||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
|
}
|
||||||
|
// Creating a user as an admin
|
||||||
|
else if params.admin {
|
||||||
|
app.db
|
||||||
|
.create_user(¶ms.email_address, false, user)
|
||||||
|
.await?
|
||||||
|
} else {
|
||||||
|
Err(Error::Http(
|
||||||
|
StatusCode::UNPROCESSABLE_ENTITY,
|
||||||
|
"email confirmation code is required".into(),
|
||||||
|
))?
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if let Some(inviter_id) = result.inviting_user_id {
|
||||||
|
rpc_server
|
||||||
|
.invite_code_redeemed(inviter_id, result.user_id)
|
||||||
|
.await
|
||||||
|
.trace_err();
|
||||||
|
}
|
||||||
|
|
||||||
let user = app
|
let user = app
|
||||||
.db
|
.db
|
||||||
.get_user_by_id(user_id)
|
.get_user_by_id(result.user_id)
|
||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| anyhow!("couldn't find the user we just created"))?;
|
.ok_or_else(|| anyhow!("couldn't find the user we just created"))?;
|
||||||
|
|
||||||
Ok(Json(user))
|
Ok(Json(CreateUserResponse {
|
||||||
|
user,
|
||||||
|
metrics_id: result.metrics_id,
|
||||||
|
signup_device_id: result.signup_device_id,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
@ -171,7 +226,9 @@ async fn update_user(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(invite_count) = params.invite_count {
|
if let Some(invite_count) = params.invite_count {
|
||||||
app.db.set_invite_count(user_id, invite_count).await?;
|
app.db
|
||||||
|
.set_invite_count_for_user(user_id, invite_count)
|
||||||
|
.await?;
|
||||||
rpc_server.invite_count_updated(user_id).await.trace_err();
|
rpc_server.invite_count_updated(user_id).await.trace_err();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -186,54 +243,6 @@ async fn destroy_user(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_user(
|
|
||||||
Path(login): Path<String>,
|
|
||||||
Extension(app): Extension<Arc<AppState>>,
|
|
||||||
) -> Result<Json<User>> {
|
|
||||||
let user = app
|
|
||||||
.db
|
|
||||||
.get_user_by_github_login(&login)
|
|
||||||
.await?
|
|
||||||
.ok_or_else(|| Error::Http(StatusCode::NOT_FOUND, "User not found".to_string()))?;
|
|
||||||
Ok(Json(user))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct CreateUsersParams {
|
|
||||||
users: Vec<CreateUsersEntry>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct CreateUsersEntry {
|
|
||||||
github_login: String,
|
|
||||||
email_address: String,
|
|
||||||
invite_count: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn create_users(
|
|
||||||
Json(params): Json<CreateUsersParams>,
|
|
||||||
Extension(app): Extension<Arc<AppState>>,
|
|
||||||
) -> Result<Json<Vec<User>>> {
|
|
||||||
let user_ids = app
|
|
||||||
.db
|
|
||||||
.create_users(
|
|
||||||
params
|
|
||||||
.users
|
|
||||||
.into_iter()
|
|
||||||
.map(|params| {
|
|
||||||
(
|
|
||||||
params.github_login,
|
|
||||||
params.email_address,
|
|
||||||
params.invite_count,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
let users = app.db.get_users_by_ids(user_ids).await?;
|
|
||||||
Ok(Json(users))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct GetUsersWithNoInvites {
|
struct GetUsersWithNoInvites {
|
||||||
invited_by_another_user: bool,
|
invited_by_another_user: bool,
|
||||||
@ -368,22 +377,24 @@ struct CreateAccessTokenResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn create_access_token(
|
async fn create_access_token(
|
||||||
Path(login): Path<String>,
|
Path(user_id): Path<UserId>,
|
||||||
Query(params): Query<CreateAccessTokenQueryParams>,
|
Query(params): Query<CreateAccessTokenQueryParams>,
|
||||||
Extension(app): Extension<Arc<AppState>>,
|
Extension(app): Extension<Arc<AppState>>,
|
||||||
) -> Result<Json<CreateAccessTokenResponse>> {
|
) -> Result<Json<CreateAccessTokenResponse>> {
|
||||||
// request.require_token().await?;
|
|
||||||
|
|
||||||
let user = app
|
let user = app
|
||||||
.db
|
.db
|
||||||
.get_user_by_github_login(&login)
|
.get_user_by_id(user_id)
|
||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| anyhow!("user not found"))?;
|
.ok_or_else(|| anyhow!("user not found"))?;
|
||||||
|
|
||||||
let mut user_id = user.id;
|
let mut user_id = user.id;
|
||||||
if let Some(impersonate) = params.impersonate {
|
if let Some(impersonate) = params.impersonate {
|
||||||
if user.admin {
|
if user.admin {
|
||||||
if let Some(impersonated_user) = app.db.get_user_by_github_login(&impersonate).await? {
|
if let Some(impersonated_user) = app
|
||||||
|
.db
|
||||||
|
.get_user_by_github_account(&impersonate, None)
|
||||||
|
.await?
|
||||||
|
{
|
||||||
user_id = impersonated_user.id;
|
user_id = impersonated_user.id;
|
||||||
} else {
|
} else {
|
||||||
return Err(Error::Http(
|
return Err(Error::Http(
|
||||||
@ -415,3 +426,59 @@ async fn get_user_for_invite_code(
|
|||||||
) -> Result<Json<User>> {
|
) -> Result<Json<User>> {
|
||||||
Ok(Json(app.db.get_user_for_invite_code(&code).await?))
|
Ok(Json(app.db.get_user_for_invite_code(&code).await?))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn create_signup(
|
||||||
|
Json(params): Json<Signup>,
|
||||||
|
Extension(app): Extension<Arc<AppState>>,
|
||||||
|
) -> Result<()> {
|
||||||
|
app.db.create_signup(params).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_waitlist_summary(
|
||||||
|
Extension(app): Extension<Arc<AppState>>,
|
||||||
|
) -> Result<Json<WaitlistSummary>> {
|
||||||
|
Ok(Json(app.db.get_waitlist_summary().await?))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct CreateInviteFromCodeParams {
|
||||||
|
invite_code: String,
|
||||||
|
email_address: String,
|
||||||
|
device_id: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_invite_from_code(
|
||||||
|
Json(params): Json<CreateInviteFromCodeParams>,
|
||||||
|
Extension(app): Extension<Arc<AppState>>,
|
||||||
|
) -> Result<Json<Invite>> {
|
||||||
|
Ok(Json(
|
||||||
|
app.db
|
||||||
|
.create_invite_from_code(
|
||||||
|
¶ms.invite_code,
|
||||||
|
¶ms.email_address,
|
||||||
|
params.device_id.as_deref(),
|
||||||
|
)
|
||||||
|
.await?,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct GetUnsentInvitesParams {
|
||||||
|
pub count: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_unsent_invites(
|
||||||
|
Query(params): Query<GetUnsentInvitesParams>,
|
||||||
|
Extension(app): Extension<Arc<AppState>>,
|
||||||
|
) -> Result<Json<Vec<Invite>>> {
|
||||||
|
Ok(Json(app.db.get_unsent_invites(params.count).await?))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn record_sent_invites(
|
||||||
|
Json(params): Json<Vec<Invite>>,
|
||||||
|
Extension(app): Extension<Arc<AppState>>,
|
||||||
|
) -> Result<()> {
|
||||||
|
app.db.record_sent_invites(¶ms).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
@ -11,7 +11,7 @@ mod db;
|
|||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct GitHubUser {
|
struct GitHubUser {
|
||||||
id: usize,
|
id: i32,
|
||||||
login: String,
|
login: String,
|
||||||
email: Option<String>,
|
email: Option<String>,
|
||||||
}
|
}
|
||||||
@ -26,8 +26,11 @@ async fn main() {
|
|||||||
let github_token = std::env::var("GITHUB_TOKEN").expect("missing GITHUB_TOKEN env var");
|
let github_token = std::env::var("GITHUB_TOKEN").expect("missing GITHUB_TOKEN env var");
|
||||||
let client = reqwest::Client::new();
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
let current_user =
|
let mut current_user =
|
||||||
fetch_github::<GitHubUser>(&client, &github_token, "https://api.github.com/user").await;
|
fetch_github::<GitHubUser>(&client, &github_token, "https://api.github.com/user").await;
|
||||||
|
current_user
|
||||||
|
.email
|
||||||
|
.get_or_insert_with(|| "placeholder@example.com".to_string());
|
||||||
let staff_users = fetch_github::<Vec<GitHubUser>>(
|
let staff_users = fetch_github::<Vec<GitHubUser>>(
|
||||||
&client,
|
&client,
|
||||||
&github_token,
|
&github_token,
|
||||||
@ -64,16 +67,40 @@ async fn main() {
|
|||||||
let mut zed_user_ids = Vec::<UserId>::new();
|
let mut zed_user_ids = Vec::<UserId>::new();
|
||||||
for (github_user, admin) in zed_users {
|
for (github_user, admin) in zed_users {
|
||||||
if let Some(user) = db
|
if let Some(user) = db
|
||||||
.get_user_by_github_login(&github_user.login)
|
.get_user_by_github_account(&github_user.login, Some(github_user.id))
|
||||||
.await
|
.await
|
||||||
.expect("failed to fetch user")
|
.expect("failed to fetch user")
|
||||||
{
|
{
|
||||||
zed_user_ids.push(user.id);
|
zed_user_ids.push(user.id);
|
||||||
} else {
|
} else if let Some(email) = &github_user.email {
|
||||||
zed_user_ids.push(
|
zed_user_ids.push(
|
||||||
db.create_user(&github_user.login, github_user.email.as_deref(), admin)
|
db.create_user(
|
||||||
|
email,
|
||||||
|
admin,
|
||||||
|
db::NewUserParams {
|
||||||
|
github_login: github_user.login,
|
||||||
|
github_user_id: github_user.id,
|
||||||
|
invite_count: 5,
|
||||||
|
},
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
.expect("failed to insert user"),
|
.expect("failed to insert user")
|
||||||
|
.user_id,
|
||||||
|
);
|
||||||
|
} else if admin {
|
||||||
|
zed_user_ids.push(
|
||||||
|
db.create_user(
|
||||||
|
&format!("{}@zed.dev", github_user.login),
|
||||||
|
admin,
|
||||||
|
db::NewUserParams {
|
||||||
|
github_login: github_user.login,
|
||||||
|
github_user_id: github_user.id,
|
||||||
|
invite_count: 5,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("failed to insert user")
|
||||||
|
.user_id,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
1188
crates/collab/src/db_tests.rs
Normal file
1188
crates/collab/src/db_tests.rs
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -4,6 +4,8 @@ mod db;
|
|||||||
mod env;
|
mod env;
|
||||||
mod rpc;
|
mod rpc;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod db_tests;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod integration_tests;
|
mod integration_tests;
|
||||||
|
|
||||||
|
@ -22,7 +22,7 @@ use axum::{
|
|||||||
routing::get,
|
routing::get,
|
||||||
Extension, Router, TypedHeader,
|
Extension, Router, TypedHeader,
|
||||||
};
|
};
|
||||||
use collections::HashMap;
|
use collections::{HashMap, HashSet};
|
||||||
use futures::{
|
use futures::{
|
||||||
channel::mpsc,
|
channel::mpsc,
|
||||||
future::{self, BoxFuture},
|
future::{self, BoxFuture},
|
||||||
@ -88,11 +88,6 @@ impl<R: RequestMessage> Response<R> {
|
|||||||
self.server.peer.respond(self.receipt, payload)?;
|
self.server.peer.respond(self.receipt, payload)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn into_receipt(self) -> Receipt<R> {
|
|
||||||
self.responded.store(true, SeqCst);
|
|
||||||
self.receipt
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Server {
|
pub struct Server {
|
||||||
@ -151,11 +146,17 @@ impl Server {
|
|||||||
|
|
||||||
server
|
server
|
||||||
.add_request_handler(Server::ping)
|
.add_request_handler(Server::ping)
|
||||||
.add_request_handler(Server::register_project)
|
.add_request_handler(Server::create_room)
|
||||||
.add_request_handler(Server::unregister_project)
|
.add_request_handler(Server::join_room)
|
||||||
|
.add_message_handler(Server::leave_room)
|
||||||
|
.add_request_handler(Server::call)
|
||||||
|
.add_request_handler(Server::cancel_call)
|
||||||
|
.add_message_handler(Server::decline_call)
|
||||||
|
.add_request_handler(Server::update_participant_location)
|
||||||
|
.add_request_handler(Server::share_project)
|
||||||
|
.add_message_handler(Server::unshare_project)
|
||||||
.add_request_handler(Server::join_project)
|
.add_request_handler(Server::join_project)
|
||||||
.add_message_handler(Server::leave_project)
|
.add_message_handler(Server::leave_project)
|
||||||
.add_message_handler(Server::respond_to_join_project_request)
|
|
||||||
.add_message_handler(Server::update_project)
|
.add_message_handler(Server::update_project)
|
||||||
.add_message_handler(Server::register_project_activity)
|
.add_message_handler(Server::register_project_activity)
|
||||||
.add_request_handler(Server::update_worktree)
|
.add_request_handler(Server::update_worktree)
|
||||||
@ -205,7 +206,9 @@ impl Server {
|
|||||||
.add_request_handler(Server::follow)
|
.add_request_handler(Server::follow)
|
||||||
.add_message_handler(Server::unfollow)
|
.add_message_handler(Server::unfollow)
|
||||||
.add_message_handler(Server::update_followers)
|
.add_message_handler(Server::update_followers)
|
||||||
.add_request_handler(Server::get_channel_messages);
|
.add_request_handler(Server::get_channel_messages)
|
||||||
|
.add_message_handler(Server::update_diff_base)
|
||||||
|
.add_request_handler(Server::get_private_user_info);
|
||||||
|
|
||||||
Arc::new(server)
|
Arc::new(server)
|
||||||
}
|
}
|
||||||
@ -362,8 +365,7 @@ impl Server {
|
|||||||
timer.await;
|
timer.await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
.await;
|
|
||||||
|
|
||||||
tracing::info!(%user_id, %login, %connection_id, %address, "connection opened");
|
tracing::info!(%user_id, %login, %connection_id, %address, "connection opened");
|
||||||
|
|
||||||
@ -383,7 +385,11 @@ impl Server {
|
|||||||
|
|
||||||
{
|
{
|
||||||
let mut store = this.store().await;
|
let mut store = this.store().await;
|
||||||
store.add_connection(connection_id, user_id, user.admin);
|
let incoming_call = store.add_connection(connection_id, user_id, user.admin);
|
||||||
|
if let Some(incoming_call) = incoming_call {
|
||||||
|
this.peer.send(connection_id, incoming_call)?;
|
||||||
|
}
|
||||||
|
|
||||||
this.peer.send(connection_id, store.build_initial_contacts_update(contacts))?;
|
this.peer.send(connection_id, store.build_initial_contacts_update(contacts))?;
|
||||||
|
|
||||||
if let Some((code, count)) = invite_code {
|
if let Some((code, count)) = invite_code {
|
||||||
@ -466,69 +472,58 @@ impl Server {
|
|||||||
async fn sign_out(self: &mut Arc<Self>, connection_id: ConnectionId) -> Result<()> {
|
async fn sign_out(self: &mut Arc<Self>, connection_id: ConnectionId) -> Result<()> {
|
||||||
self.peer.disconnect(connection_id);
|
self.peer.disconnect(connection_id);
|
||||||
|
|
||||||
let mut projects_to_unregister = Vec::new();
|
let mut projects_to_unshare = Vec::new();
|
||||||
let removed_user_id;
|
let mut contacts_to_update = HashSet::default();
|
||||||
{
|
{
|
||||||
let mut store = self.store().await;
|
let mut store = self.store().await;
|
||||||
let removed_connection = store.remove_connection(connection_id)?;
|
let removed_connection = store.remove_connection(connection_id)?;
|
||||||
|
|
||||||
for (project_id, project) in removed_connection.hosted_projects {
|
for project in removed_connection.hosted_projects {
|
||||||
projects_to_unregister.push(project_id);
|
projects_to_unshare.push(project.id);
|
||||||
broadcast(connection_id, project.guests.keys().copied(), |conn_id| {
|
broadcast(connection_id, project.guests.keys().copied(), |conn_id| {
|
||||||
self.peer.send(
|
self.peer.send(
|
||||||
conn_id,
|
conn_id,
|
||||||
proto::UnregisterProject {
|
proto::UnshareProject {
|
||||||
project_id: project_id.to_proto(),
|
project_id: project.id.to_proto(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
for (_, receipts) in project.join_requests {
|
|
||||||
for receipt in receipts {
|
|
||||||
self.peer.respond(
|
|
||||||
receipt,
|
|
||||||
proto::JoinProjectResponse {
|
|
||||||
variant: Some(proto::join_project_response::Variant::Decline(
|
|
||||||
proto::join_project_response::Decline {
|
|
||||||
reason: proto::join_project_response::decline::Reason::WentOffline as i32
|
|
||||||
},
|
|
||||||
)),
|
|
||||||
},
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for project_id in removed_connection.guest_project_ids {
|
for project in removed_connection.guest_projects {
|
||||||
if let Some(project) = store.project(project_id).trace_err() {
|
broadcast(connection_id, project.connection_ids, |conn_id| {
|
||||||
broadcast(connection_id, project.connection_ids(), |conn_id| {
|
|
||||||
self.peer.send(
|
self.peer.send(
|
||||||
conn_id,
|
conn_id,
|
||||||
proto::RemoveProjectCollaborator {
|
proto::RemoveProjectCollaborator {
|
||||||
project_id: project_id.to_proto(),
|
project_id: project.id.to_proto(),
|
||||||
peer_id: connection_id.0,
|
peer_id: connection_id.0,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
if project.guests.is_empty() {
|
|
||||||
self.peer
|
|
||||||
.send(
|
|
||||||
project.host_connection_id,
|
|
||||||
proto::ProjectUnshared {
|
|
||||||
project_id: project_id.to_proto(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.trace_err();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
removed_user_id = removed_connection.user_id;
|
for connection_id in removed_connection.canceled_call_connection_ids {
|
||||||
|
self.peer
|
||||||
|
.send(connection_id, proto::CallCanceled {})
|
||||||
|
.trace_err();
|
||||||
|
contacts_to_update.extend(store.user_id_for_connection(connection_id).ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(room) = removed_connection
|
||||||
|
.room_id
|
||||||
|
.and_then(|room_id| store.room(room_id))
|
||||||
|
{
|
||||||
|
self.room_updated(room);
|
||||||
|
}
|
||||||
|
|
||||||
|
contacts_to_update.insert(removed_connection.user_id);
|
||||||
};
|
};
|
||||||
|
|
||||||
self.update_user_contacts(removed_user_id).await.trace_err();
|
for user_id in contacts_to_update {
|
||||||
|
self.update_user_contacts(user_id).await.trace_err();
|
||||||
|
}
|
||||||
|
|
||||||
for project_id in projects_to_unregister {
|
for project_id in projects_to_unshare {
|
||||||
self.app_state
|
self.app_state
|
||||||
.db
|
.db
|
||||||
.unregister_project(project_id)
|
.unregister_project(project_id)
|
||||||
@ -541,13 +536,14 @@ impl Server {
|
|||||||
|
|
||||||
pub async fn invite_code_redeemed(
|
pub async fn invite_code_redeemed(
|
||||||
self: &Arc<Self>,
|
self: &Arc<Self>,
|
||||||
code: &str,
|
inviter_id: UserId,
|
||||||
invitee_id: UserId,
|
invitee_id: UserId,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let user = self.app_state.db.get_user_for_invite_code(code).await?;
|
if let Some(user) = self.app_state.db.get_user_by_id(inviter_id).await? {
|
||||||
|
if let Some(code) = &user.invite_code {
|
||||||
let store = self.store().await;
|
let store = self.store().await;
|
||||||
let invitee_contact = store.contact_for_user(invitee_id, true);
|
let invitee_contact = store.contact_for_user(invitee_id, true);
|
||||||
for connection_id in store.connection_ids_for_user(user.id) {
|
for connection_id in store.connection_ids_for_user(inviter_id) {
|
||||||
self.peer.send(
|
self.peer.send(
|
||||||
connection_id,
|
connection_id,
|
||||||
proto::UpdateContacts {
|
proto::UpdateContacts {
|
||||||
@ -558,11 +554,13 @@ impl Server {
|
|||||||
self.peer.send(
|
self.peer.send(
|
||||||
connection_id,
|
connection_id,
|
||||||
proto::UpdateInviteInfo {
|
proto::UpdateInviteInfo {
|
||||||
url: format!("{}{}", self.app_state.invite_link_prefix, code),
|
url: format!("{}{}", self.app_state.invite_link_prefix, &code),
|
||||||
count: user.invite_count as u32,
|
count: user.invite_count as u32,
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -593,76 +591,286 @@ impl Server {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn register_project(
|
async fn create_room(
|
||||||
self: Arc<Server>,
|
self: Arc<Server>,
|
||||||
request: TypedEnvelope<proto::RegisterProject>,
|
request: TypedEnvelope<proto::CreateRoom>,
|
||||||
response: Response<proto::RegisterProject>,
|
response: Response<proto::CreateRoom>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let user_id;
|
||||||
|
let room_id;
|
||||||
|
{
|
||||||
|
let mut store = self.store().await;
|
||||||
|
user_id = store.user_id_for_connection(request.sender_id)?;
|
||||||
|
room_id = store.create_room(request.sender_id)?;
|
||||||
|
}
|
||||||
|
response.send(proto::CreateRoomResponse { id: room_id })?;
|
||||||
|
self.update_user_contacts(user_id).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn join_room(
|
||||||
|
self: Arc<Server>,
|
||||||
|
request: TypedEnvelope<proto::JoinRoom>,
|
||||||
|
response: Response<proto::JoinRoom>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let user_id;
|
||||||
|
{
|
||||||
|
let mut store = self.store().await;
|
||||||
|
user_id = store.user_id_for_connection(request.sender_id)?;
|
||||||
|
let (room, recipient_connection_ids) =
|
||||||
|
store.join_room(request.payload.id, request.sender_id)?;
|
||||||
|
for recipient_id in recipient_connection_ids {
|
||||||
|
self.peer
|
||||||
|
.send(recipient_id, proto::CallCanceled {})
|
||||||
|
.trace_err();
|
||||||
|
}
|
||||||
|
response.send(proto::JoinRoomResponse {
|
||||||
|
room: Some(room.clone()),
|
||||||
|
})?;
|
||||||
|
self.room_updated(room);
|
||||||
|
}
|
||||||
|
self.update_user_contacts(user_id).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn leave_room(self: Arc<Server>, message: TypedEnvelope<proto::LeaveRoom>) -> Result<()> {
|
||||||
|
let mut contacts_to_update = HashSet::default();
|
||||||
|
{
|
||||||
|
let mut store = self.store().await;
|
||||||
|
let user_id = store.user_id_for_connection(message.sender_id)?;
|
||||||
|
let left_room = store.leave_room(message.payload.id, message.sender_id)?;
|
||||||
|
contacts_to_update.insert(user_id);
|
||||||
|
|
||||||
|
for project in left_room.unshared_projects {
|
||||||
|
for connection_id in project.connection_ids() {
|
||||||
|
self.peer.send(
|
||||||
|
connection_id,
|
||||||
|
proto::UnshareProject {
|
||||||
|
project_id: project.id.to_proto(),
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for project in left_room.left_projects {
|
||||||
|
if project.remove_collaborator {
|
||||||
|
for connection_id in project.connection_ids {
|
||||||
|
self.peer.send(
|
||||||
|
connection_id,
|
||||||
|
proto::RemoveProjectCollaborator {
|
||||||
|
project_id: project.id.to_proto(),
|
||||||
|
peer_id: message.sender_id.0,
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.peer.send(
|
||||||
|
message.sender_id,
|
||||||
|
proto::UnshareProject {
|
||||||
|
project_id: project.id.to_proto(),
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(room) = left_room.room {
|
||||||
|
self.room_updated(room);
|
||||||
|
}
|
||||||
|
|
||||||
|
for connection_id in left_room.canceled_call_connection_ids {
|
||||||
|
self.peer
|
||||||
|
.send(connection_id, proto::CallCanceled {})
|
||||||
|
.trace_err();
|
||||||
|
contacts_to_update.extend(store.user_id_for_connection(connection_id).ok());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for user_id in contacts_to_update {
|
||||||
|
self.update_user_contacts(user_id).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn call(
|
||||||
|
self: Arc<Server>,
|
||||||
|
request: TypedEnvelope<proto::Call>,
|
||||||
|
response: Response<proto::Call>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let caller_user_id = self
|
||||||
|
.store()
|
||||||
|
.await
|
||||||
|
.user_id_for_connection(request.sender_id)?;
|
||||||
|
let recipient_user_id = UserId::from_proto(request.payload.recipient_user_id);
|
||||||
|
let initial_project_id = request
|
||||||
|
.payload
|
||||||
|
.initial_project_id
|
||||||
|
.map(ProjectId::from_proto);
|
||||||
|
if !self
|
||||||
|
.app_state
|
||||||
|
.db
|
||||||
|
.has_contact(caller_user_id, recipient_user_id)
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
return Err(anyhow!("cannot call a user who isn't a contact"))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let room_id = request.payload.room_id;
|
||||||
|
let mut calls = {
|
||||||
|
let mut store = self.store().await;
|
||||||
|
let (room, recipient_connection_ids, incoming_call) = store.call(
|
||||||
|
room_id,
|
||||||
|
recipient_user_id,
|
||||||
|
initial_project_id,
|
||||||
|
request.sender_id,
|
||||||
|
)?;
|
||||||
|
self.room_updated(room);
|
||||||
|
recipient_connection_ids
|
||||||
|
.into_iter()
|
||||||
|
.map(|recipient_connection_id| {
|
||||||
|
self.peer
|
||||||
|
.request(recipient_connection_id, incoming_call.clone())
|
||||||
|
})
|
||||||
|
.collect::<FuturesUnordered<_>>()
|
||||||
|
};
|
||||||
|
self.update_user_contacts(recipient_user_id).await?;
|
||||||
|
|
||||||
|
while let Some(call_response) = calls.next().await {
|
||||||
|
match call_response.as_ref() {
|
||||||
|
Ok(_) => {
|
||||||
|
response.send(proto::Ack {})?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
call_response.trace_err();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut store = self.store().await;
|
||||||
|
let room = store.call_failed(room_id, recipient_user_id)?;
|
||||||
|
self.room_updated(&room);
|
||||||
|
}
|
||||||
|
self.update_user_contacts(recipient_user_id).await?;
|
||||||
|
|
||||||
|
Err(anyhow!("failed to ring call recipient"))?
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn cancel_call(
|
||||||
|
self: Arc<Server>,
|
||||||
|
request: TypedEnvelope<proto::CancelCall>,
|
||||||
|
response: Response<proto::CancelCall>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let recipient_user_id = UserId::from_proto(request.payload.recipient_user_id);
|
||||||
|
{
|
||||||
|
let mut store = self.store().await;
|
||||||
|
let (room, recipient_connection_ids) = store.cancel_call(
|
||||||
|
request.payload.room_id,
|
||||||
|
recipient_user_id,
|
||||||
|
request.sender_id,
|
||||||
|
)?;
|
||||||
|
for recipient_id in recipient_connection_ids {
|
||||||
|
self.peer
|
||||||
|
.send(recipient_id, proto::CallCanceled {})
|
||||||
|
.trace_err();
|
||||||
|
}
|
||||||
|
self.room_updated(room);
|
||||||
|
response.send(proto::Ack {})?;
|
||||||
|
}
|
||||||
|
self.update_user_contacts(recipient_user_id).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn decline_call(
|
||||||
|
self: Arc<Server>,
|
||||||
|
message: TypedEnvelope<proto::DeclineCall>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let recipient_user_id;
|
||||||
|
{
|
||||||
|
let mut store = self.store().await;
|
||||||
|
recipient_user_id = store.user_id_for_connection(message.sender_id)?;
|
||||||
|
let (room, recipient_connection_ids) =
|
||||||
|
store.decline_call(message.payload.room_id, message.sender_id)?;
|
||||||
|
for recipient_id in recipient_connection_ids {
|
||||||
|
self.peer
|
||||||
|
.send(recipient_id, proto::CallCanceled {})
|
||||||
|
.trace_err();
|
||||||
|
}
|
||||||
|
self.room_updated(room);
|
||||||
|
}
|
||||||
|
self.update_user_contacts(recipient_user_id).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_participant_location(
|
||||||
|
self: Arc<Server>,
|
||||||
|
request: TypedEnvelope<proto::UpdateParticipantLocation>,
|
||||||
|
response: Response<proto::UpdateParticipantLocation>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let room_id = request.payload.room_id;
|
||||||
|
let location = request
|
||||||
|
.payload
|
||||||
|
.location
|
||||||
|
.ok_or_else(|| anyhow!("invalid location"))?;
|
||||||
|
let mut store = self.store().await;
|
||||||
|
let room = store.update_participant_location(room_id, location, request.sender_id)?;
|
||||||
|
self.room_updated(room);
|
||||||
|
response.send(proto::Ack {})?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn room_updated(&self, room: &proto::Room) {
|
||||||
|
for participant in &room.participants {
|
||||||
|
self.peer
|
||||||
|
.send(
|
||||||
|
ConnectionId(participant.peer_id),
|
||||||
|
proto::RoomUpdated {
|
||||||
|
room: Some(room.clone()),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.trace_err();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn share_project(
|
||||||
|
self: Arc<Server>,
|
||||||
|
request: TypedEnvelope<proto::ShareProject>,
|
||||||
|
response: Response<proto::ShareProject>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let user_id = self
|
let user_id = self
|
||||||
.store()
|
.store()
|
||||||
.await
|
.await
|
||||||
.user_id_for_connection(request.sender_id)?;
|
.user_id_for_connection(request.sender_id)?;
|
||||||
let project_id = self.app_state.db.register_project(user_id).await?;
|
let project_id = self.app_state.db.register_project(user_id).await?;
|
||||||
self.store().await.register_project(
|
let mut store = self.store().await;
|
||||||
request.sender_id,
|
let room = store.share_project(
|
||||||
|
request.payload.room_id,
|
||||||
project_id,
|
project_id,
|
||||||
request.payload.online,
|
request.payload.worktrees,
|
||||||
|
request.sender_id,
|
||||||
)?;
|
)?;
|
||||||
|
response.send(proto::ShareProjectResponse {
|
||||||
response.send(proto::RegisterProjectResponse {
|
|
||||||
project_id: project_id.to_proto(),
|
project_id: project_id.to_proto(),
|
||||||
})?;
|
})?;
|
||||||
|
self.room_updated(room);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn unregister_project(
|
async fn unshare_project(
|
||||||
self: Arc<Server>,
|
self: Arc<Server>,
|
||||||
request: TypedEnvelope<proto::UnregisterProject>,
|
message: TypedEnvelope<proto::UnshareProject>,
|
||||||
response: Response<proto::UnregisterProject>,
|
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let project_id = ProjectId::from_proto(request.payload.project_id);
|
let project_id = ProjectId::from_proto(message.payload.project_id);
|
||||||
let (user_id, project) = {
|
let mut store = self.store().await;
|
||||||
let mut state = self.store().await;
|
let (room, project) = store.unshare_project(project_id, message.sender_id)?;
|
||||||
let project = state.unregister_project(project_id, request.sender_id)?;
|
|
||||||
(state.user_id_for_connection(request.sender_id)?, project)
|
|
||||||
};
|
|
||||||
self.app_state.db.unregister_project(project_id).await?;
|
|
||||||
|
|
||||||
broadcast(
|
broadcast(
|
||||||
request.sender_id,
|
message.sender_id,
|
||||||
project.guests.keys().copied(),
|
project.guest_connection_ids(),
|
||||||
|conn_id| {
|
|conn_id| self.peer.send(conn_id, message.payload.clone()),
|
||||||
self.peer.send(
|
|
||||||
conn_id,
|
|
||||||
proto::UnregisterProject {
|
|
||||||
project_id: project_id.to_proto(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
for (_, receipts) in project.join_requests {
|
self.room_updated(room);
|
||||||
for receipt in receipts {
|
|
||||||
self.peer.respond(
|
|
||||||
receipt,
|
|
||||||
proto::JoinProjectResponse {
|
|
||||||
variant: Some(proto::join_project_response::Variant::Decline(
|
|
||||||
proto::join_project_response::Decline {
|
|
||||||
reason: proto::join_project_response::decline::Reason::Closed
|
|
||||||
as i32,
|
|
||||||
},
|
|
||||||
)),
|
|
||||||
},
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send out the `UpdateContacts` message before responding to the unregister
|
|
||||||
// request. This way, when the project's host can keep track of the project's
|
|
||||||
// remote id until after they've received the `UpdateContacts` message for
|
|
||||||
// themself.
|
|
||||||
self.update_user_contacts(user_id).await?;
|
|
||||||
response.send(proto::Ack {})?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -716,71 +924,9 @@ impl Server {
|
|||||||
};
|
};
|
||||||
|
|
||||||
tracing::info!(%project_id, %host_user_id, %host_connection_id, "join project");
|
tracing::info!(%project_id, %host_user_id, %host_connection_id, "join project");
|
||||||
let has_contact = self
|
|
||||||
.app_state
|
|
||||||
.db
|
|
||||||
.has_contact(guest_user_id, host_user_id)
|
|
||||||
.await?;
|
|
||||||
if !has_contact {
|
|
||||||
return Err(anyhow!("no such project"))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.store().await.request_join_project(
|
|
||||||
guest_user_id,
|
|
||||||
project_id,
|
|
||||||
response.into_receipt(),
|
|
||||||
)?;
|
|
||||||
self.peer.send(
|
|
||||||
host_connection_id,
|
|
||||||
proto::RequestJoinProject {
|
|
||||||
project_id: project_id.to_proto(),
|
|
||||||
requester_id: guest_user_id.to_proto(),
|
|
||||||
},
|
|
||||||
)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn respond_to_join_project_request(
|
|
||||||
self: Arc<Server>,
|
|
||||||
request: TypedEnvelope<proto::RespondToJoinProjectRequest>,
|
|
||||||
) -> Result<()> {
|
|
||||||
let host_user_id;
|
|
||||||
|
|
||||||
{
|
|
||||||
let mut state = self.store().await;
|
|
||||||
let project_id = ProjectId::from_proto(request.payload.project_id);
|
|
||||||
let project = state.project(project_id)?;
|
|
||||||
if project.host_connection_id != request.sender_id {
|
|
||||||
Err(anyhow!("no such connection"))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
host_user_id = project.host.user_id;
|
|
||||||
let guest_user_id = UserId::from_proto(request.payload.requester_id);
|
|
||||||
|
|
||||||
if !request.payload.allow {
|
|
||||||
let receipts = state
|
|
||||||
.deny_join_project_request(request.sender_id, guest_user_id, project_id)
|
|
||||||
.ok_or_else(|| anyhow!("no such request"))?;
|
|
||||||
for receipt in receipts {
|
|
||||||
self.peer.respond(
|
|
||||||
receipt,
|
|
||||||
proto::JoinProjectResponse {
|
|
||||||
variant: Some(proto::join_project_response::Variant::Decline(
|
|
||||||
proto::join_project_response::Decline {
|
|
||||||
reason: proto::join_project_response::decline::Reason::Declined
|
|
||||||
as i32,
|
|
||||||
},
|
|
||||||
)),
|
|
||||||
},
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let (receipts_with_replica_ids, project) = state
|
|
||||||
.accept_join_project_request(request.sender_id, guest_user_id, project_id)
|
|
||||||
.ok_or_else(|| anyhow!("no such request"))?;
|
|
||||||
|
|
||||||
|
let mut store = self.store().await;
|
||||||
|
let (project, replica_id) = store.join_project(request.sender_id, project_id)?;
|
||||||
let peer_count = project.guests.len();
|
let peer_count = project.guests.len();
|
||||||
let mut collaborators = Vec::with_capacity(peer_count);
|
let mut collaborators = Vec::with_capacity(peer_count);
|
||||||
collaborators.push(proto::Collaborator {
|
collaborators.push(proto::Collaborator {
|
||||||
@ -800,10 +946,7 @@ impl Server {
|
|||||||
|
|
||||||
// Add all guests other than the requesting user's own connections as collaborators
|
// Add all guests other than the requesting user's own connections as collaborators
|
||||||
for (guest_conn_id, guest) in &project.guests {
|
for (guest_conn_id, guest) in &project.guests {
|
||||||
if receipts_with_replica_ids
|
if request.sender_id != *guest_conn_id {
|
||||||
.iter()
|
|
||||||
.all(|(receipt, _)| receipt.sender_id != *guest_conn_id)
|
|
||||||
{
|
|
||||||
collaborators.push(proto::Collaborator {
|
collaborators.push(proto::Collaborator {
|
||||||
peer_id: guest_conn_id.0,
|
peer_id: guest_conn_id.0,
|
||||||
replica_id: guest.replica_id as u32,
|
replica_id: guest.replica_id as u32,
|
||||||
@ -813,39 +956,28 @@ impl Server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for conn_id in project.connection_ids() {
|
for conn_id in project.connection_ids() {
|
||||||
for (receipt, replica_id) in &receipts_with_replica_ids {
|
if conn_id != request.sender_id {
|
||||||
if conn_id != receipt.sender_id {
|
|
||||||
self.peer.send(
|
self.peer.send(
|
||||||
conn_id,
|
conn_id,
|
||||||
proto::AddProjectCollaborator {
|
proto::AddProjectCollaborator {
|
||||||
project_id: project_id.to_proto(),
|
project_id: project_id.to_proto(),
|
||||||
collaborator: Some(proto::Collaborator {
|
collaborator: Some(proto::Collaborator {
|
||||||
peer_id: receipt.sender_id.0,
|
peer_id: request.sender_id.0,
|
||||||
replica_id: *replica_id as u32,
|
replica_id: replica_id as u32,
|
||||||
user_id: guest_user_id.to_proto(),
|
user_id: guest_user_id.to_proto(),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// First, we send the metadata associated with each worktree.
|
// First, we send the metadata associated with each worktree.
|
||||||
for (receipt, replica_id) in &receipts_with_replica_ids {
|
response.send(proto::JoinProjectResponse {
|
||||||
self.peer.respond(
|
|
||||||
*receipt,
|
|
||||||
proto::JoinProjectResponse {
|
|
||||||
variant: Some(proto::join_project_response::Variant::Accept(
|
|
||||||
proto::join_project_response::Accept {
|
|
||||||
worktrees: worktrees.clone(),
|
worktrees: worktrees.clone(),
|
||||||
replica_id: *replica_id as u32,
|
replica_id: replica_id as u32,
|
||||||
collaborators: collaborators.clone(),
|
collaborators: collaborators.clone(),
|
||||||
language_servers: project.language_servers.clone(),
|
language_servers: project.language_servers.clone(),
|
||||||
},
|
})?;
|
||||||
)),
|
|
||||||
},
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (worktree_id, worktree) in &project.worktrees {
|
for (worktree_id, worktree) in &project.worktrees {
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
@ -864,16 +996,13 @@ impl Server {
|
|||||||
is_last_update: worktree.is_complete,
|
is_last_update: worktree.is_complete,
|
||||||
};
|
};
|
||||||
for update in proto::split_worktree_update(message, MAX_CHUNK_SIZE) {
|
for update in proto::split_worktree_update(message, MAX_CHUNK_SIZE) {
|
||||||
for (receipt, _) in &receipts_with_replica_ids {
|
self.peer.send(request.sender_id, update.clone())?;
|
||||||
self.peer.send(receipt.sender_id, update.clone())?;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stream this worktree's diagnostics.
|
// Stream this worktree's diagnostics.
|
||||||
for summary in worktree.diagnostic_summaries.values() {
|
for summary in worktree.diagnostic_summaries.values() {
|
||||||
for (receipt, _) in &receipts_with_replica_ids {
|
|
||||||
self.peer.send(
|
self.peer.send(
|
||||||
receipt.sender_id,
|
request.sender_id,
|
||||||
proto::UpdateDiagnosticSummary {
|
proto::UpdateDiagnosticSummary {
|
||||||
project_id: project_id.to_proto(),
|
project_id: project_id.to_proto(),
|
||||||
worktree_id: *worktree_id,
|
worktree_id: *worktree_id,
|
||||||
@ -882,10 +1011,22 @@ impl Server {
|
|||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
for language_server in &project.language_servers {
|
||||||
|
self.peer.send(
|
||||||
|
request.sender_id,
|
||||||
|
proto::UpdateLanguageServer {
|
||||||
|
project_id: project_id.to_proto(),
|
||||||
|
language_server_id: language_server.id,
|
||||||
|
variant: Some(
|
||||||
|
proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated(
|
||||||
|
proto::LspDiskBasedDiagnosticsUpdated {},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.update_user_contacts(host_user_id).await?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -898,7 +1039,7 @@ impl Server {
|
|||||||
let project;
|
let project;
|
||||||
{
|
{
|
||||||
let mut store = self.store().await;
|
let mut store = self.store().await;
|
||||||
project = store.leave_project(sender_id, project_id)?;
|
project = store.leave_project(project_id, sender_id)?;
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
%project_id,
|
%project_id,
|
||||||
host_user_id = %project.host_user_id,
|
host_user_id = %project.host_user_id,
|
||||||
@ -917,27 +1058,8 @@ impl Server {
|
|||||||
)
|
)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(requester_id) = project.cancel_request {
|
|
||||||
self.peer.send(
|
|
||||||
project.host_connection_id,
|
|
||||||
proto::JoinProjectRequestCancelled {
|
|
||||||
project_id: project_id.to_proto(),
|
|
||||||
requester_id: requester_id.to_proto(),
|
|
||||||
},
|
|
||||||
)?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if project.unshare {
|
|
||||||
self.peer.send(
|
|
||||||
project.host_connection_id,
|
|
||||||
proto::ProjectUnshared {
|
|
||||||
project_id: project_id.to_proto(),
|
|
||||||
},
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.update_user_contacts(project.host_user_id).await?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -946,61 +1068,20 @@ impl Server {
|
|||||||
request: TypedEnvelope<proto::UpdateProject>,
|
request: TypedEnvelope<proto::UpdateProject>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let project_id = ProjectId::from_proto(request.payload.project_id);
|
let project_id = ProjectId::from_proto(request.payload.project_id);
|
||||||
let user_id;
|
|
||||||
{
|
{
|
||||||
let mut state = self.store().await;
|
let mut state = self.store().await;
|
||||||
user_id = state.user_id_for_connection(request.sender_id)?;
|
|
||||||
let guest_connection_ids = state
|
let guest_connection_ids = state
|
||||||
.read_project(project_id, request.sender_id)?
|
.read_project(project_id, request.sender_id)?
|
||||||
.guest_connection_ids();
|
.guest_connection_ids();
|
||||||
let unshared_project = state.update_project(
|
let room =
|
||||||
project_id,
|
state.update_project(project_id, &request.payload.worktrees, request.sender_id)?;
|
||||||
&request.payload.worktrees,
|
|
||||||
request.payload.online,
|
|
||||||
request.sender_id,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
if let Some(unshared_project) = unshared_project {
|
|
||||||
broadcast(
|
|
||||||
request.sender_id,
|
|
||||||
unshared_project.guests.keys().copied(),
|
|
||||||
|conn_id| {
|
|
||||||
self.peer.send(
|
|
||||||
conn_id,
|
|
||||||
proto::UnregisterProject {
|
|
||||||
project_id: project_id.to_proto(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
|
||||||
);
|
|
||||||
for (_, receipts) in unshared_project.pending_join_requests {
|
|
||||||
for receipt in receipts {
|
|
||||||
self.peer.respond(
|
|
||||||
receipt,
|
|
||||||
proto::JoinProjectResponse {
|
|
||||||
variant: Some(proto::join_project_response::Variant::Decline(
|
|
||||||
proto::join_project_response::Decline {
|
|
||||||
reason:
|
|
||||||
proto::join_project_response::decline::Reason::Closed
|
|
||||||
as i32,
|
|
||||||
},
|
|
||||||
)),
|
|
||||||
},
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
broadcast(request.sender_id, guest_connection_ids, |connection_id| {
|
broadcast(request.sender_id, guest_connection_ids, |connection_id| {
|
||||||
self.peer.forward_send(
|
self.peer
|
||||||
request.sender_id,
|
.forward_send(request.sender_id, connection_id, request.payload.clone())
|
||||||
connection_id,
|
|
||||||
request.payload.clone(),
|
|
||||||
)
|
|
||||||
});
|
});
|
||||||
}
|
self.room_updated(room);
|
||||||
};
|
};
|
||||||
|
|
||||||
self.update_user_contacts(user_id).await?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1022,9 +1103,7 @@ impl Server {
|
|||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let project_id = ProjectId::from_proto(request.payload.project_id);
|
let project_id = ProjectId::from_proto(request.payload.project_id);
|
||||||
let worktree_id = request.payload.worktree_id;
|
let worktree_id = request.payload.worktree_id;
|
||||||
let (connection_ids, metadata_changed) = {
|
let connection_ids = self.store().await.update_worktree(
|
||||||
let mut store = self.store().await;
|
|
||||||
let (connection_ids, metadata_changed) = store.update_worktree(
|
|
||||||
request.sender_id,
|
request.sender_id,
|
||||||
project_id,
|
project_id,
|
||||||
worktree_id,
|
worktree_id,
|
||||||
@ -1034,20 +1113,11 @@ impl Server {
|
|||||||
request.payload.scan_id,
|
request.payload.scan_id,
|
||||||
request.payload.is_last_update,
|
request.payload.is_last_update,
|
||||||
)?;
|
)?;
|
||||||
(connection_ids, metadata_changed)
|
|
||||||
};
|
|
||||||
|
|
||||||
broadcast(request.sender_id, connection_ids, |connection_id| {
|
broadcast(request.sender_id, connection_ids, |connection_id| {
|
||||||
self.peer
|
self.peer
|
||||||
.forward_send(request.sender_id, connection_id, request.payload.clone())
|
.forward_send(request.sender_id, connection_id, request.payload.clone())
|
||||||
});
|
});
|
||||||
if metadata_changed {
|
|
||||||
let user_id = self
|
|
||||||
.store()
|
|
||||||
.await
|
|
||||||
.user_id_for_connection(request.sender_id)?;
|
|
||||||
self.update_user_contacts(user_id).await?;
|
|
||||||
}
|
|
||||||
response.send(proto::Ack {})?;
|
response.send(proto::Ack {})?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -1401,7 +1471,7 @@ impl Server {
|
|||||||
let users = match query.len() {
|
let users = match query.len() {
|
||||||
0 => vec![],
|
0 => vec![],
|
||||||
1 | 2 => db
|
1 | 2 => db
|
||||||
.get_user_by_github_login(&query)
|
.get_user_by_github_account(&query, None)
|
||||||
.await?
|
.await?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.collect(),
|
.collect(),
|
||||||
@ -1724,6 +1794,44 @@ impl Server {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn update_diff_base(
|
||||||
|
self: Arc<Server>,
|
||||||
|
request: TypedEnvelope<proto::UpdateDiffBase>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let receiver_ids = self.store().await.project_connection_ids(
|
||||||
|
ProjectId::from_proto(request.payload.project_id),
|
||||||
|
request.sender_id,
|
||||||
|
)?;
|
||||||
|
broadcast(request.sender_id, receiver_ids, |connection_id| {
|
||||||
|
self.peer
|
||||||
|
.forward_send(request.sender_id, connection_id, request.payload.clone())
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_private_user_info(
|
||||||
|
self: Arc<Self>,
|
||||||
|
request: TypedEnvelope<proto::GetPrivateUserInfo>,
|
||||||
|
response: Response<proto::GetPrivateUserInfo>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let user_id = self
|
||||||
|
.store()
|
||||||
|
.await
|
||||||
|
.user_id_for_connection(request.sender_id)?;
|
||||||
|
let metrics_id = self.app_state.db.get_user_metrics_id(user_id).await?;
|
||||||
|
let user = self
|
||||||
|
.app_state
|
||||||
|
.db
|
||||||
|
.get_user_by_id(user_id)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| anyhow!("user not found"))?;
|
||||||
|
response.send(proto::GetPrivateUserInfoResponse {
|
||||||
|
metrics_id,
|
||||||
|
staff: user.admin,
|
||||||
|
})?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) async fn store(&self) -> StoreGuard<'_> {
|
pub(crate) async fn store(&self) -> StoreGuard<'_> {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
tokio::task::yield_now().await;
|
tokio::task::yield_now().await;
|
||||||
|
File diff suppressed because it is too large
Load Diff
53
crates/collab_ui/Cargo.toml
Normal file
53
crates/collab_ui/Cargo.toml
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
[package]
|
||||||
|
name = "collab_ui"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "src/collab_ui.rs"
|
||||||
|
doctest = false
|
||||||
|
|
||||||
|
[features]
|
||||||
|
test-support = [
|
||||||
|
"call/test-support",
|
||||||
|
"client/test-support",
|
||||||
|
"collections/test-support",
|
||||||
|
"editor/test-support",
|
||||||
|
"gpui/test-support",
|
||||||
|
"project/test-support",
|
||||||
|
"settings/test-support",
|
||||||
|
"util/test-support",
|
||||||
|
"workspace/test-support",
|
||||||
|
]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
call = { path = "../call" }
|
||||||
|
client = { path = "../client" }
|
||||||
|
clock = { path = "../clock" }
|
||||||
|
collections = { path = "../collections" }
|
||||||
|
editor = { path = "../editor" }
|
||||||
|
fuzzy = { path = "../fuzzy" }
|
||||||
|
gpui = { path = "../gpui" }
|
||||||
|
menu = { path = "../menu" }
|
||||||
|
picker = { path = "../picker" }
|
||||||
|
project = { path = "../project" }
|
||||||
|
settings = { path = "../settings" }
|
||||||
|
theme = { path = "../theme" }
|
||||||
|
util = { path = "../util" }
|
||||||
|
workspace = { path = "../workspace" }
|
||||||
|
anyhow = "1.0"
|
||||||
|
futures = "0.3"
|
||||||
|
log = "0.4"
|
||||||
|
postage = { version = "0.4.1", features = ["futures-traits"] }
|
||||||
|
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
call = { path = "../call", features = ["test-support"] }
|
||||||
|
client = { path = "../client", features = ["test-support"] }
|
||||||
|
collections = { path = "../collections", features = ["test-support"] }
|
||||||
|
editor = { path = "../editor", features = ["test-support"] }
|
||||||
|
gpui = { path = "../gpui", features = ["test-support"] }
|
||||||
|
project = { path = "../project", features = ["test-support"] }
|
||||||
|
settings = { path = "../settings", features = ["test-support"] }
|
||||||
|
util = { path = "../util", features = ["test-support"] }
|
||||||
|
workspace = { path = "../workspace", features = ["test-support"] }
|
566
crates/collab_ui/src/collab_titlebar_item.rs
Normal file
566
crates/collab_ui/src/collab_titlebar_item.rs
Normal file
@ -0,0 +1,566 @@
|
|||||||
|
use crate::{contact_notification::ContactNotification, contacts_popover};
|
||||||
|
use call::{ActiveCall, ParticipantLocation};
|
||||||
|
use client::{Authenticate, ContactEventKind, PeerId, User, UserStore};
|
||||||
|
use clock::ReplicaId;
|
||||||
|
use contacts_popover::ContactsPopover;
|
||||||
|
use gpui::{
|
||||||
|
actions,
|
||||||
|
color::Color,
|
||||||
|
elements::*,
|
||||||
|
geometry::{rect::RectF, vector::vec2f, PathBuilder},
|
||||||
|
json::{self, ToJson},
|
||||||
|
Border, CursorStyle, Entity, ModelHandle, MouseButton, MutableAppContext, RenderContext,
|
||||||
|
Subscription, View, ViewContext, ViewHandle, WeakViewHandle,
|
||||||
|
};
|
||||||
|
use settings::Settings;
|
||||||
|
use std::ops::Range;
|
||||||
|
use theme::Theme;
|
||||||
|
use workspace::{FollowNextCollaborator, JoinProject, ToggleFollow, Workspace};
|
||||||
|
|
||||||
|
actions!(collab, [ToggleCollaborationMenu, ShareProject]);
|
||||||
|
|
||||||
|
pub fn init(cx: &mut MutableAppContext) {
|
||||||
|
cx.add_action(CollabTitlebarItem::toggle_contacts_popover);
|
||||||
|
cx.add_action(CollabTitlebarItem::share_project);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CollabTitlebarItem {
|
||||||
|
workspace: WeakViewHandle<Workspace>,
|
||||||
|
user_store: ModelHandle<UserStore>,
|
||||||
|
contacts_popover: Option<ViewHandle<ContactsPopover>>,
|
||||||
|
_subscriptions: Vec<Subscription>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Entity for CollabTitlebarItem {
|
||||||
|
type Event = ();
|
||||||
|
}
|
||||||
|
|
||||||
|
impl View for CollabTitlebarItem {
|
||||||
|
fn ui_name() -> &'static str {
|
||||||
|
"CollabTitlebarItem"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||||
|
let workspace = if let Some(workspace) = self.workspace.upgrade(cx) {
|
||||||
|
workspace
|
||||||
|
} else {
|
||||||
|
return Empty::new().boxed();
|
||||||
|
};
|
||||||
|
|
||||||
|
let theme = cx.global::<Settings>().theme.clone();
|
||||||
|
let project = workspace.read(cx).project().read(cx);
|
||||||
|
|
||||||
|
let mut container = Flex::row();
|
||||||
|
if workspace.read(cx).client().status().borrow().is_connected() {
|
||||||
|
if project.is_shared()
|
||||||
|
|| project.is_remote()
|
||||||
|
|| ActiveCall::global(cx).read(cx).room().is_none()
|
||||||
|
{
|
||||||
|
container.add_child(self.render_toggle_contacts_button(&theme, cx));
|
||||||
|
} else {
|
||||||
|
container.add_child(self.render_share_button(&theme, cx));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
container.add_children(self.render_collaborators(&workspace, &theme, cx));
|
||||||
|
container.add_children(self.render_current_user(&workspace, &theme, cx));
|
||||||
|
container.add_children(self.render_connection_status(&workspace, cx));
|
||||||
|
container.boxed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CollabTitlebarItem {
|
||||||
|
pub fn new(
|
||||||
|
workspace: &ViewHandle<Workspace>,
|
||||||
|
user_store: &ModelHandle<UserStore>,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) -> Self {
|
||||||
|
let active_call = ActiveCall::global(cx);
|
||||||
|
let mut subscriptions = Vec::new();
|
||||||
|
subscriptions.push(cx.observe(workspace, |_, _, cx| cx.notify()));
|
||||||
|
subscriptions.push(cx.observe(&active_call, |_, _, cx| cx.notify()));
|
||||||
|
subscriptions.push(cx.observe_window_activation(|this, active, cx| {
|
||||||
|
this.window_activation_changed(active, cx)
|
||||||
|
}));
|
||||||
|
subscriptions.push(cx.observe(user_store, |_, _, cx| cx.notify()));
|
||||||
|
subscriptions.push(
|
||||||
|
cx.subscribe(user_store, move |this, user_store, event, cx| {
|
||||||
|
if let Some(workspace) = this.workspace.upgrade(cx) {
|
||||||
|
workspace.update(cx, |workspace, cx| {
|
||||||
|
if let client::Event::Contact { user, kind } = event {
|
||||||
|
if let ContactEventKind::Requested | ContactEventKind::Accepted = kind {
|
||||||
|
workspace.show_notification(user.id as usize, cx, |cx| {
|
||||||
|
cx.add_view(|cx| {
|
||||||
|
ContactNotification::new(
|
||||||
|
user.clone(),
|
||||||
|
*kind,
|
||||||
|
user_store,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
workspace: workspace.downgrade(),
|
||||||
|
user_store: user_store.clone(),
|
||||||
|
contacts_popover: None,
|
||||||
|
_subscriptions: subscriptions,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
|
||||||
|
let workspace = self.workspace.upgrade(cx);
|
||||||
|
let room = ActiveCall::global(cx).read(cx).room().cloned();
|
||||||
|
if let Some((workspace, room)) = workspace.zip(room) {
|
||||||
|
let workspace = workspace.read(cx);
|
||||||
|
let project = if active {
|
||||||
|
Some(workspace.project().clone())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
room.update(cx, |room, cx| {
|
||||||
|
room.set_location(project.as_ref(), cx)
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn share_project(&mut self, _: &ShareProject, cx: &mut ViewContext<Self>) {
|
||||||
|
if let Some(workspace) = self.workspace.upgrade(cx) {
|
||||||
|
let active_call = ActiveCall::global(cx);
|
||||||
|
let project = workspace.read(cx).project().clone();
|
||||||
|
active_call
|
||||||
|
.update(cx, |call, cx| call.share_project(project, cx))
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toggle_contacts_popover(
|
||||||
|
&mut self,
|
||||||
|
_: &ToggleCollaborationMenu,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) {
|
||||||
|
match self.contacts_popover.take() {
|
||||||
|
Some(_) => {}
|
||||||
|
None => {
|
||||||
|
if let Some(workspace) = self.workspace.upgrade(cx) {
|
||||||
|
let project = workspace.read(cx).project().clone();
|
||||||
|
let user_store = workspace.read(cx).user_store().clone();
|
||||||
|
let view = cx.add_view(|cx| ContactsPopover::new(project, user_store, cx));
|
||||||
|
cx.subscribe(&view, |this, _, event, cx| {
|
||||||
|
match event {
|
||||||
|
contacts_popover::Event::Dismissed => {
|
||||||
|
this.contacts_popover = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
self.contacts_popover = Some(view);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_toggle_contacts_button(
|
||||||
|
&self,
|
||||||
|
theme: &Theme,
|
||||||
|
cx: &mut RenderContext<Self>,
|
||||||
|
) -> ElementBox {
|
||||||
|
let titlebar = &theme.workspace.titlebar;
|
||||||
|
let badge = if self
|
||||||
|
.user_store
|
||||||
|
.read(cx)
|
||||||
|
.incoming_contact_requests()
|
||||||
|
.is_empty()
|
||||||
|
{
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(
|
||||||
|
Empty::new()
|
||||||
|
.collapsed()
|
||||||
|
.contained()
|
||||||
|
.with_style(titlebar.toggle_contacts_badge)
|
||||||
|
.contained()
|
||||||
|
.with_margin_left(titlebar.toggle_contacts_button.default.icon_width)
|
||||||
|
.with_margin_top(titlebar.toggle_contacts_button.default.icon_width)
|
||||||
|
.aligned()
|
||||||
|
.boxed(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
Stack::new()
|
||||||
|
.with_child(
|
||||||
|
MouseEventHandler::<ToggleCollaborationMenu>::new(0, cx, |state, _| {
|
||||||
|
let style = titlebar
|
||||||
|
.toggle_contacts_button
|
||||||
|
.style_for(state, self.contacts_popover.is_some());
|
||||||
|
Svg::new("icons/plus_8.svg")
|
||||||
|
.with_color(style.color)
|
||||||
|
.constrained()
|
||||||
|
.with_width(style.icon_width)
|
||||||
|
.aligned()
|
||||||
|
.constrained()
|
||||||
|
.with_width(style.button_width)
|
||||||
|
.with_height(style.button_width)
|
||||||
|
.contained()
|
||||||
|
.with_style(style.container)
|
||||||
|
.boxed()
|
||||||
|
})
|
||||||
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
|
.on_click(MouseButton::Left, move |_, cx| {
|
||||||
|
cx.dispatch_action(ToggleCollaborationMenu);
|
||||||
|
})
|
||||||
|
.aligned()
|
||||||
|
.boxed(),
|
||||||
|
)
|
||||||
|
.with_children(badge)
|
||||||
|
.with_children(self.contacts_popover.as_ref().map(|popover| {
|
||||||
|
Overlay::new(
|
||||||
|
ChildView::new(popover, cx)
|
||||||
|
.contained()
|
||||||
|
.with_margin_top(titlebar.height)
|
||||||
|
.with_margin_left(titlebar.toggle_contacts_button.default.button_width)
|
||||||
|
.with_margin_right(-titlebar.toggle_contacts_button.default.button_width)
|
||||||
|
.boxed(),
|
||||||
|
)
|
||||||
|
.with_fit_mode(OverlayFitMode::SwitchAnchor)
|
||||||
|
.with_anchor_corner(AnchorCorner::BottomLeft)
|
||||||
|
.boxed()
|
||||||
|
}))
|
||||||
|
.boxed()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_share_button(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||||
|
enum Share {}
|
||||||
|
|
||||||
|
let titlebar = &theme.workspace.titlebar;
|
||||||
|
MouseEventHandler::<Share>::new(0, cx, |state, _| {
|
||||||
|
let style = titlebar.share_button.style_for(state, false);
|
||||||
|
Label::new("Share".into(), style.text.clone())
|
||||||
|
.contained()
|
||||||
|
.with_style(style.container)
|
||||||
|
.boxed()
|
||||||
|
})
|
||||||
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
|
.on_click(MouseButton::Left, |_, cx| cx.dispatch_action(ShareProject))
|
||||||
|
.with_tooltip::<Share, _>(
|
||||||
|
0,
|
||||||
|
"Share project with call participants".into(),
|
||||||
|
None,
|
||||||
|
theme.tooltip.clone(),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.aligned()
|
||||||
|
.boxed()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_collaborators(
|
||||||
|
&self,
|
||||||
|
workspace: &ViewHandle<Workspace>,
|
||||||
|
theme: &Theme,
|
||||||
|
cx: &mut RenderContext<Self>,
|
||||||
|
) -> Vec<ElementBox> {
|
||||||
|
let active_call = ActiveCall::global(cx);
|
||||||
|
if let Some(room) = active_call.read(cx).room().cloned() {
|
||||||
|
let project = workspace.read(cx).project().read(cx);
|
||||||
|
let mut participants = room
|
||||||
|
.read(cx)
|
||||||
|
.remote_participants()
|
||||||
|
.iter()
|
||||||
|
.map(|(peer_id, collaborator)| (*peer_id, collaborator.clone()))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
participants
|
||||||
|
.sort_by_key(|(peer_id, _)| Some(project.collaborators().get(peer_id)?.replica_id));
|
||||||
|
participants
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|(peer_id, participant)| {
|
||||||
|
let project = workspace.read(cx).project().read(cx);
|
||||||
|
let replica_id = project
|
||||||
|
.collaborators()
|
||||||
|
.get(&peer_id)
|
||||||
|
.map(|collaborator| collaborator.replica_id);
|
||||||
|
let user = participant.user.clone();
|
||||||
|
Some(self.render_avatar(
|
||||||
|
&user,
|
||||||
|
replica_id,
|
||||||
|
Some((peer_id, &user.github_login, participant.location)),
|
||||||
|
workspace,
|
||||||
|
theme,
|
||||||
|
cx,
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
} else {
|
||||||
|
Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_current_user(
|
||||||
|
&self,
|
||||||
|
workspace: &ViewHandle<Workspace>,
|
||||||
|
theme: &Theme,
|
||||||
|
cx: &mut RenderContext<Self>,
|
||||||
|
) -> Option<ElementBox> {
|
||||||
|
let user = workspace.read(cx).user_store().read(cx).current_user();
|
||||||
|
let replica_id = workspace.read(cx).project().read(cx).replica_id();
|
||||||
|
let status = *workspace.read(cx).client().status().borrow();
|
||||||
|
if let Some(user) = user {
|
||||||
|
Some(self.render_avatar(&user, Some(replica_id), None, workspace, theme, cx))
|
||||||
|
} else if matches!(status, client::Status::UpgradeRequired) {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(
|
||||||
|
MouseEventHandler::<Authenticate>::new(0, cx, |state, _| {
|
||||||
|
let style = theme
|
||||||
|
.workspace
|
||||||
|
.titlebar
|
||||||
|
.sign_in_prompt
|
||||||
|
.style_for(state, false);
|
||||||
|
Label::new("Sign in".to_string(), style.text.clone())
|
||||||
|
.contained()
|
||||||
|
.with_style(style.container)
|
||||||
|
.boxed()
|
||||||
|
})
|
||||||
|
.on_click(MouseButton::Left, |_, cx| cx.dispatch_action(Authenticate))
|
||||||
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
|
.aligned()
|
||||||
|
.boxed(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_avatar(
|
||||||
|
&self,
|
||||||
|
user: &User,
|
||||||
|
replica_id: Option<ReplicaId>,
|
||||||
|
peer: Option<(PeerId, &str, ParticipantLocation)>,
|
||||||
|
workspace: &ViewHandle<Workspace>,
|
||||||
|
theme: &Theme,
|
||||||
|
cx: &mut RenderContext<Self>,
|
||||||
|
) -> ElementBox {
|
||||||
|
let is_followed = peer.map_or(false, |(peer_id, _, _)| {
|
||||||
|
workspace.read(cx).is_following(peer_id)
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut avatar_style;
|
||||||
|
if let Some((_, _, location)) = peer.as_ref() {
|
||||||
|
if let ParticipantLocation::SharedProject { project_id } = *location {
|
||||||
|
if Some(project_id) == workspace.read(cx).project().read(cx).remote_id() {
|
||||||
|
avatar_style = theme.workspace.titlebar.avatar;
|
||||||
|
} else {
|
||||||
|
avatar_style = theme.workspace.titlebar.inactive_avatar;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
avatar_style = theme.workspace.titlebar.inactive_avatar;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
avatar_style = theme.workspace.titlebar.avatar;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut replica_color = None;
|
||||||
|
if let Some(replica_id) = replica_id {
|
||||||
|
let color = theme.editor.replica_selection_style(replica_id).cursor;
|
||||||
|
replica_color = Some(color);
|
||||||
|
if is_followed {
|
||||||
|
avatar_style.border = Border::all(1.0, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let content = Stack::new()
|
||||||
|
.with_children(user.avatar.as_ref().map(|avatar| {
|
||||||
|
Image::new(avatar.clone())
|
||||||
|
.with_style(avatar_style)
|
||||||
|
.constrained()
|
||||||
|
.with_width(theme.workspace.titlebar.avatar_width)
|
||||||
|
.aligned()
|
||||||
|
.boxed()
|
||||||
|
}))
|
||||||
|
.with_children(replica_color.map(|replica_color| {
|
||||||
|
AvatarRibbon::new(replica_color)
|
||||||
|
.constrained()
|
||||||
|
.with_width(theme.workspace.titlebar.avatar_ribbon.width)
|
||||||
|
.with_height(theme.workspace.titlebar.avatar_ribbon.height)
|
||||||
|
.aligned()
|
||||||
|
.bottom()
|
||||||
|
.boxed()
|
||||||
|
}))
|
||||||
|
.constrained()
|
||||||
|
.with_width(theme.workspace.titlebar.avatar_width)
|
||||||
|
.contained()
|
||||||
|
.with_margin_left(theme.workspace.titlebar.avatar_margin)
|
||||||
|
.boxed();
|
||||||
|
|
||||||
|
if let Some((peer_id, peer_github_login, location)) = peer {
|
||||||
|
if let Some(replica_id) = replica_id {
|
||||||
|
MouseEventHandler::<ToggleFollow>::new(replica_id.into(), cx, move |_, _| content)
|
||||||
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
|
.on_click(MouseButton::Left, move |_, cx| {
|
||||||
|
cx.dispatch_action(ToggleFollow(peer_id))
|
||||||
|
})
|
||||||
|
.with_tooltip::<ToggleFollow, _>(
|
||||||
|
peer_id.0 as usize,
|
||||||
|
if is_followed {
|
||||||
|
format!("Unfollow {}", peer_github_login)
|
||||||
|
} else {
|
||||||
|
format!("Follow {}", peer_github_login)
|
||||||
|
},
|
||||||
|
Some(Box::new(FollowNextCollaborator)),
|
||||||
|
theme.tooltip.clone(),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.boxed()
|
||||||
|
} else if let ParticipantLocation::SharedProject { project_id } = location {
|
||||||
|
let user_id = user.id;
|
||||||
|
MouseEventHandler::<JoinProject>::new(peer_id.0 as usize, cx, move |_, _| content)
|
||||||
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
|
.on_click(MouseButton::Left, move |_, cx| {
|
||||||
|
cx.dispatch_action(JoinProject {
|
||||||
|
project_id,
|
||||||
|
follow_user_id: user_id,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.with_tooltip::<JoinProject, _>(
|
||||||
|
peer_id.0 as usize,
|
||||||
|
format!("Follow {} into external project", peer_github_login),
|
||||||
|
Some(Box::new(FollowNextCollaborator)),
|
||||||
|
theme.tooltip.clone(),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.boxed()
|
||||||
|
} else {
|
||||||
|
content
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_connection_status(
|
||||||
|
&self,
|
||||||
|
workspace: &ViewHandle<Workspace>,
|
||||||
|
cx: &mut RenderContext<Self>,
|
||||||
|
) -> Option<ElementBox> {
|
||||||
|
let theme = &cx.global::<Settings>().theme;
|
||||||
|
match &*workspace.read(cx).client().status().borrow() {
|
||||||
|
client::Status::ConnectionError
|
||||||
|
| client::Status::ConnectionLost
|
||||||
|
| client::Status::Reauthenticating { .. }
|
||||||
|
| client::Status::Reconnecting { .. }
|
||||||
|
| client::Status::ReconnectionError { .. } => Some(
|
||||||
|
Container::new(
|
||||||
|
Align::new(
|
||||||
|
ConstrainedBox::new(
|
||||||
|
Svg::new("icons/cloud_slash_12.svg")
|
||||||
|
.with_color(theme.workspace.titlebar.offline_icon.color)
|
||||||
|
.boxed(),
|
||||||
|
)
|
||||||
|
.with_width(theme.workspace.titlebar.offline_icon.width)
|
||||||
|
.boxed(),
|
||||||
|
)
|
||||||
|
.boxed(),
|
||||||
|
)
|
||||||
|
.with_style(theme.workspace.titlebar.offline_icon.container)
|
||||||
|
.boxed(),
|
||||||
|
),
|
||||||
|
client::Status::UpgradeRequired => Some(
|
||||||
|
Label::new(
|
||||||
|
"Please update Zed to collaborate".to_string(),
|
||||||
|
theme.workspace.titlebar.outdated_warning.text.clone(),
|
||||||
|
)
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.workspace.titlebar.outdated_warning.container)
|
||||||
|
.aligned()
|
||||||
|
.boxed(),
|
||||||
|
),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AvatarRibbon {
|
||||||
|
color: Color,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AvatarRibbon {
|
||||||
|
pub fn new(color: Color) -> AvatarRibbon {
|
||||||
|
AvatarRibbon { color }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Element for AvatarRibbon {
|
||||||
|
type LayoutState = ();
|
||||||
|
|
||||||
|
type PaintState = ();
|
||||||
|
|
||||||
|
fn layout(
|
||||||
|
&mut self,
|
||||||
|
constraint: gpui::SizeConstraint,
|
||||||
|
_: &mut gpui::LayoutContext,
|
||||||
|
) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) {
|
||||||
|
(constraint.max, ())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paint(
|
||||||
|
&mut self,
|
||||||
|
bounds: gpui::geometry::rect::RectF,
|
||||||
|
_: gpui::geometry::rect::RectF,
|
||||||
|
_: &mut Self::LayoutState,
|
||||||
|
cx: &mut gpui::PaintContext,
|
||||||
|
) -> Self::PaintState {
|
||||||
|
let mut path = PathBuilder::new();
|
||||||
|
path.reset(bounds.lower_left());
|
||||||
|
path.curve_to(
|
||||||
|
bounds.origin() + vec2f(bounds.height(), 0.),
|
||||||
|
bounds.origin(),
|
||||||
|
);
|
||||||
|
path.line_to(bounds.upper_right() - vec2f(bounds.height(), 0.));
|
||||||
|
path.curve_to(bounds.lower_right(), bounds.upper_right());
|
||||||
|
path.line_to(bounds.lower_left());
|
||||||
|
cx.scene.push_path(path.build(self.color, None));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dispatch_event(
|
||||||
|
&mut self,
|
||||||
|
_: &gpui::Event,
|
||||||
|
_: RectF,
|
||||||
|
_: RectF,
|
||||||
|
_: &mut Self::LayoutState,
|
||||||
|
_: &mut Self::PaintState,
|
||||||
|
_: &mut gpui::EventContext,
|
||||||
|
) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rect_for_text_range(
|
||||||
|
&self,
|
||||||
|
_: Range<usize>,
|
||||||
|
_: RectF,
|
||||||
|
_: RectF,
|
||||||
|
_: &Self::LayoutState,
|
||||||
|
_: &Self::PaintState,
|
||||||
|
_: &gpui::MeasurementContext,
|
||||||
|
) -> Option<RectF> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn debug(
|
||||||
|
&self,
|
||||||
|
bounds: gpui::geometry::rect::RectF,
|
||||||
|
_: &Self::LayoutState,
|
||||||
|
_: &Self::PaintState,
|
||||||
|
_: &gpui::DebugContext,
|
||||||
|
) -> gpui::json::Value {
|
||||||
|
json::json!({
|
||||||
|
"type": "AvatarRibbon",
|
||||||
|
"bounds": bounds.to_json(),
|
||||||
|
"color": self.color.to_json(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
97
crates/collab_ui/src/collab_ui.rs
Normal file
97
crates/collab_ui/src/collab_ui.rs
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
mod collab_titlebar_item;
|
||||||
|
mod contact_finder;
|
||||||
|
mod contact_list;
|
||||||
|
mod contact_notification;
|
||||||
|
mod contacts_popover;
|
||||||
|
mod incoming_call_notification;
|
||||||
|
mod notifications;
|
||||||
|
mod project_shared_notification;
|
||||||
|
|
||||||
|
use call::ActiveCall;
|
||||||
|
pub use collab_titlebar_item::{CollabTitlebarItem, ToggleCollaborationMenu};
|
||||||
|
use gpui::MutableAppContext;
|
||||||
|
use project::Project;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use workspace::{AppState, JoinProject, ToggleFollow, Workspace};
|
||||||
|
|
||||||
|
pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
|
||||||
|
collab_titlebar_item::init(cx);
|
||||||
|
contact_notification::init(cx);
|
||||||
|
contact_list::init(cx);
|
||||||
|
contact_finder::init(cx);
|
||||||
|
contacts_popover::init(cx);
|
||||||
|
incoming_call_notification::init(cx);
|
||||||
|
project_shared_notification::init(cx);
|
||||||
|
|
||||||
|
cx.add_global_action(move |action: &JoinProject, cx| {
|
||||||
|
let project_id = action.project_id;
|
||||||
|
let follow_user_id = action.follow_user_id;
|
||||||
|
let app_state = app_state.clone();
|
||||||
|
cx.spawn(|mut cx| async move {
|
||||||
|
let existing_workspace = cx.update(|cx| {
|
||||||
|
cx.window_ids()
|
||||||
|
.filter_map(|window_id| cx.root_view::<Workspace>(window_id))
|
||||||
|
.find(|workspace| {
|
||||||
|
workspace.read(cx).project().read(cx).remote_id() == Some(project_id)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
let workspace = if let Some(existing_workspace) = existing_workspace {
|
||||||
|
existing_workspace
|
||||||
|
} else {
|
||||||
|
let project = Project::remote(
|
||||||
|
project_id,
|
||||||
|
app_state.client.clone(),
|
||||||
|
app_state.user_store.clone(),
|
||||||
|
app_state.project_store.clone(),
|
||||||
|
app_state.languages.clone(),
|
||||||
|
app_state.fs.clone(),
|
||||||
|
cx.clone(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let (_, workspace) = cx.add_window((app_state.build_window_options)(), |cx| {
|
||||||
|
let mut workspace = Workspace::new(project, app_state.default_item_factory, cx);
|
||||||
|
(app_state.initialize_workspace)(&mut workspace, &app_state, cx);
|
||||||
|
workspace
|
||||||
|
});
|
||||||
|
workspace
|
||||||
|
};
|
||||||
|
|
||||||
|
cx.activate_window(workspace.window_id());
|
||||||
|
cx.platform().activate(true);
|
||||||
|
|
||||||
|
workspace.update(&mut cx, |workspace, cx| {
|
||||||
|
if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
|
||||||
|
let follow_peer_id = room
|
||||||
|
.read(cx)
|
||||||
|
.remote_participants()
|
||||||
|
.iter()
|
||||||
|
.find(|(_, participant)| participant.user.id == follow_user_id)
|
||||||
|
.map(|(peer_id, _)| *peer_id)
|
||||||
|
.or_else(|| {
|
||||||
|
// If we couldn't follow the given user, follow the host instead.
|
||||||
|
let collaborator = workspace
|
||||||
|
.project()
|
||||||
|
.read(cx)
|
||||||
|
.collaborators()
|
||||||
|
.values()
|
||||||
|
.find(|collaborator| collaborator.replica_id == 0)?;
|
||||||
|
Some(collaborator.peer_id)
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(follow_peer_id) = follow_peer_id {
|
||||||
|
if !workspace.is_following(follow_peer_id) {
|
||||||
|
workspace
|
||||||
|
.toggle_follow(&ToggleFollow(follow_peer_id), cx)
|
||||||
|
.map(|follow| follow.detach_and_log_err(cx));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
anyhow::Ok(())
|
||||||
|
})
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
});
|
||||||
|
}
|
@ -1,21 +1,15 @@
|
|||||||
use client::{ContactRequestStatus, User, UserStore};
|
use client::{ContactRequestStatus, User, UserStore};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, elements::*, AnyViewHandle, Entity, ModelHandle, MouseState, MutableAppContext,
|
elements::*, AnyViewHandle, Entity, ModelHandle, MouseState, MutableAppContext, RenderContext,
|
||||||
RenderContext, Task, View, ViewContext, ViewHandle,
|
Task, View, ViewContext, ViewHandle,
|
||||||
};
|
};
|
||||||
use picker::{Picker, PickerDelegate};
|
use picker::{Picker, PickerDelegate};
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use util::TryFutureExt;
|
use util::TryFutureExt;
|
||||||
use workspace::Workspace;
|
|
||||||
|
|
||||||
use crate::render_icon_button;
|
|
||||||
|
|
||||||
actions!(contact_finder, [Toggle]);
|
|
||||||
|
|
||||||
pub fn init(cx: &mut MutableAppContext) {
|
pub fn init(cx: &mut MutableAppContext) {
|
||||||
Picker::<ContactFinder>::init(cx);
|
Picker::<ContactFinder>::init(cx);
|
||||||
cx.add_action(ContactFinder::toggle);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ContactFinder {
|
pub struct ContactFinder {
|
||||||
@ -38,8 +32,8 @@ impl View for ContactFinder {
|
|||||||
"ContactFinder"
|
"ContactFinder"
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
|
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||||
ChildView::new(self.picker.clone()).boxed()
|
ChildView::new(self.picker.clone(), cx).boxed()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
fn on_focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||||
@ -107,7 +101,7 @@ impl PickerDelegate for ContactFinder {
|
|||||||
fn render_match(
|
fn render_match(
|
||||||
&self,
|
&self,
|
||||||
ix: usize,
|
ix: usize,
|
||||||
mouse_state: MouseState,
|
mouse_state: &mut MouseState,
|
||||||
selected: bool,
|
selected: bool,
|
||||||
cx: &gpui::AppContext,
|
cx: &gpui::AppContext,
|
||||||
) -> ElementBox {
|
) -> ElementBox {
|
||||||
@ -117,18 +111,21 @@ impl PickerDelegate for ContactFinder {
|
|||||||
|
|
||||||
let icon_path = match request_status {
|
let icon_path = match request_status {
|
||||||
ContactRequestStatus::None | ContactRequestStatus::RequestReceived => {
|
ContactRequestStatus::None | ContactRequestStatus::RequestReceived => {
|
||||||
"icons/check_8.svg"
|
Some("icons/check_8.svg")
|
||||||
}
|
|
||||||
ContactRequestStatus::RequestSent | ContactRequestStatus::RequestAccepted => {
|
|
||||||
"icons/x_mark_8.svg"
|
|
||||||
}
|
}
|
||||||
|
ContactRequestStatus::RequestSent => Some("icons/x_mark_8.svg"),
|
||||||
|
ContactRequestStatus::RequestAccepted => None,
|
||||||
};
|
};
|
||||||
let button_style = if self.user_store.read(cx).is_contact_request_pending(user) {
|
let button_style = if self.user_store.read(cx).is_contact_request_pending(user) {
|
||||||
&theme.contact_finder.disabled_contact_button
|
&theme.contact_finder.disabled_contact_button
|
||||||
} else {
|
} else {
|
||||||
&theme.contact_finder.contact_button
|
&theme.contact_finder.contact_button
|
||||||
};
|
};
|
||||||
let style = theme.picker.item.style_for(mouse_state, selected);
|
let style = theme
|
||||||
|
.contact_finder
|
||||||
|
.picker
|
||||||
|
.item
|
||||||
|
.style_for(mouse_state, selected);
|
||||||
Flex::row()
|
Flex::row()
|
||||||
.with_children(user.avatar.clone().map(|avatar| {
|
.with_children(user.avatar.clone().map(|avatar| {
|
||||||
Image::new(avatar)
|
Image::new(avatar)
|
||||||
@ -145,12 +142,21 @@ impl PickerDelegate for ContactFinder {
|
|||||||
.left()
|
.left()
|
||||||
.boxed(),
|
.boxed(),
|
||||||
)
|
)
|
||||||
.with_child(
|
.with_children(icon_path.map(|icon_path| {
|
||||||
render_icon_button(button_style, icon_path)
|
Svg::new(icon_path)
|
||||||
|
.with_color(button_style.color)
|
||||||
|
.constrained()
|
||||||
|
.with_width(button_style.icon_width)
|
||||||
|
.aligned()
|
||||||
|
.contained()
|
||||||
|
.with_style(button_style.container)
|
||||||
|
.constrained()
|
||||||
|
.with_width(button_style.button_width)
|
||||||
|
.with_height(button_style.button_width)
|
||||||
.aligned()
|
.aligned()
|
||||||
.flex_float()
|
.flex_float()
|
||||||
.boxed(),
|
.boxed()
|
||||||
)
|
}))
|
||||||
.contained()
|
.contained()
|
||||||
.with_style(style.container)
|
.with_style(style.container)
|
||||||
.constrained()
|
.constrained()
|
||||||
@ -160,34 +166,16 @@ impl PickerDelegate for ContactFinder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ContactFinder {
|
impl ContactFinder {
|
||||||
fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
|
|
||||||
workspace.toggle_modal(cx, |workspace, cx| {
|
|
||||||
let finder = cx.add_view(|cx| Self::new(workspace.user_store().clone(), cx));
|
|
||||||
cx.subscribe(&finder, Self::on_event).detach();
|
|
||||||
finder
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new(user_store: ModelHandle<UserStore>, cx: &mut ViewContext<Self>) -> Self {
|
pub fn new(user_store: ModelHandle<UserStore>, cx: &mut ViewContext<Self>) -> Self {
|
||||||
let this = cx.weak_handle();
|
let this = cx.weak_handle();
|
||||||
Self {
|
Self {
|
||||||
picker: cx.add_view(|cx| Picker::new(this, cx)),
|
picker: cx.add_view(|cx| {
|
||||||
|
Picker::new(this, cx)
|
||||||
|
.with_theme(|cx| &cx.global::<Settings>().theme.contact_finder.picker)
|
||||||
|
}),
|
||||||
potential_contacts: Arc::from([]),
|
potential_contacts: Arc::from([]),
|
||||||
user_store,
|
user_store,
|
||||||
selected_index: 0,
|
selected_index: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_event(
|
|
||||||
workspace: &mut Workspace,
|
|
||||||
_: ViewHandle<Self>,
|
|
||||||
event: &Event,
|
|
||||||
cx: &mut ViewContext<Workspace>,
|
|
||||||
) {
|
|
||||||
match event {
|
|
||||||
Event::Dismissed => {
|
|
||||||
workspace.dismiss_modal(cx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
1148
crates/collab_ui/src/contact_list.rs
Normal file
1148
crates/collab_ui/src/contact_list.rs
Normal file
File diff suppressed because it is too large
Load Diff
@ -49,10 +49,7 @@ impl View for ContactNotification {
|
|||||||
self.user.clone(),
|
self.user.clone(),
|
||||||
"wants to add you as a contact",
|
"wants to add you as a contact",
|
||||||
Some("They won't know if you decline."),
|
Some("They won't know if you decline."),
|
||||||
RespondToContactRequest {
|
Dismiss(self.user.id),
|
||||||
user_id: self.user.id,
|
|
||||||
accept: false,
|
|
||||||
},
|
|
||||||
vec![
|
vec![
|
||||||
(
|
(
|
||||||
"Decline",
|
"Decline",
|
171
crates/collab_ui/src/contacts_popover.rs
Normal file
171
crates/collab_ui/src/contacts_popover.rs
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
use crate::{contact_finder::ContactFinder, contact_list::ContactList, ToggleCollaborationMenu};
|
||||||
|
use client::UserStore;
|
||||||
|
use gpui::{
|
||||||
|
actions, elements::*, ClipboardItem, CursorStyle, Entity, ModelHandle, MouseButton,
|
||||||
|
MutableAppContext, RenderContext, View, ViewContext, ViewHandle,
|
||||||
|
};
|
||||||
|
use project::Project;
|
||||||
|
use settings::Settings;
|
||||||
|
|
||||||
|
actions!(contacts_popover, [ToggleContactFinder]);
|
||||||
|
|
||||||
|
pub fn init(cx: &mut MutableAppContext) {
|
||||||
|
cx.add_action(ContactsPopover::toggle_contact_finder);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Event {
|
||||||
|
Dismissed,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Child {
|
||||||
|
ContactList(ViewHandle<ContactList>),
|
||||||
|
ContactFinder(ViewHandle<ContactFinder>),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ContactsPopover {
|
||||||
|
child: Child,
|
||||||
|
project: ModelHandle<Project>,
|
||||||
|
user_store: ModelHandle<UserStore>,
|
||||||
|
_subscription: Option<gpui::Subscription>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ContactsPopover {
|
||||||
|
pub fn new(
|
||||||
|
project: ModelHandle<Project>,
|
||||||
|
user_store: ModelHandle<UserStore>,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) -> Self {
|
||||||
|
let mut this = Self {
|
||||||
|
child: Child::ContactList(
|
||||||
|
cx.add_view(|cx| ContactList::new(project.clone(), user_store.clone(), cx)),
|
||||||
|
),
|
||||||
|
project,
|
||||||
|
user_store,
|
||||||
|
_subscription: None,
|
||||||
|
};
|
||||||
|
this.show_contact_list(cx);
|
||||||
|
this
|
||||||
|
}
|
||||||
|
|
||||||
|
fn toggle_contact_finder(&mut self, _: &ToggleContactFinder, cx: &mut ViewContext<Self>) {
|
||||||
|
match &self.child {
|
||||||
|
Child::ContactList(_) => self.show_contact_finder(cx),
|
||||||
|
Child::ContactFinder(_) => self.show_contact_list(cx),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_contact_finder(&mut self, cx: &mut ViewContext<ContactsPopover>) {
|
||||||
|
let child = cx.add_view(|cx| ContactFinder::new(self.user_store.clone(), cx));
|
||||||
|
cx.focus(&child);
|
||||||
|
self._subscription = Some(cx.subscribe(&child, |_, _, event, cx| match event {
|
||||||
|
crate::contact_finder::Event::Dismissed => cx.emit(Event::Dismissed),
|
||||||
|
}));
|
||||||
|
self.child = Child::ContactFinder(child);
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_contact_list(&mut self, cx: &mut ViewContext<ContactsPopover>) {
|
||||||
|
let child =
|
||||||
|
cx.add_view(|cx| ContactList::new(self.project.clone(), self.user_store.clone(), cx));
|
||||||
|
cx.focus(&child);
|
||||||
|
self._subscription = Some(cx.subscribe(&child, |_, _, event, cx| match event {
|
||||||
|
crate::contact_list::Event::Dismissed => cx.emit(Event::Dismissed),
|
||||||
|
}));
|
||||||
|
self.child = Child::ContactList(child);
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Entity for ContactsPopover {
|
||||||
|
type Event = Event;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl View for ContactsPopover {
|
||||||
|
fn ui_name() -> &'static str {
|
||||||
|
"ContactsPopover"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||||
|
let theme = cx.global::<Settings>().theme.clone();
|
||||||
|
let child = match &self.child {
|
||||||
|
Child::ContactList(child) => ChildView::new(child, cx),
|
||||||
|
Child::ContactFinder(child) => ChildView::new(child, cx),
|
||||||
|
};
|
||||||
|
|
||||||
|
MouseEventHandler::<ContactsPopover>::new(0, cx, |_, cx| {
|
||||||
|
Flex::column()
|
||||||
|
.with_child(child.flex(1., true).boxed())
|
||||||
|
.with_children(
|
||||||
|
self.user_store
|
||||||
|
.read(cx)
|
||||||
|
.invite_info()
|
||||||
|
.cloned()
|
||||||
|
.and_then(|info| {
|
||||||
|
enum InviteLink {}
|
||||||
|
|
||||||
|
if info.count > 0 {
|
||||||
|
Some(
|
||||||
|
MouseEventHandler::<InviteLink>::new(0, cx, |state, cx| {
|
||||||
|
let style = theme
|
||||||
|
.contacts_popover
|
||||||
|
.invite_row
|
||||||
|
.style_for(state, false)
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
let copied =
|
||||||
|
cx.read_from_clipboard().map_or(false, |item| {
|
||||||
|
item.text().as_str() == info.url.as_ref()
|
||||||
|
});
|
||||||
|
|
||||||
|
Label::new(
|
||||||
|
format!(
|
||||||
|
"{} invite link ({} left)",
|
||||||
|
if copied { "Copied" } else { "Copy" },
|
||||||
|
info.count
|
||||||
|
),
|
||||||
|
style.label.clone(),
|
||||||
|
)
|
||||||
|
.aligned()
|
||||||
|
.left()
|
||||||
|
.constrained()
|
||||||
|
.with_height(theme.contacts_popover.invite_row_height)
|
||||||
|
.contained()
|
||||||
|
.with_style(style.container)
|
||||||
|
.boxed()
|
||||||
|
})
|
||||||
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
|
.on_click(MouseButton::Left, move |_, cx| {
|
||||||
|
cx.write_to_clipboard(ClipboardItem::new(
|
||||||
|
info.url.to_string(),
|
||||||
|
));
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
.boxed(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.contacts_popover.container)
|
||||||
|
.constrained()
|
||||||
|
.with_width(theme.contacts_popover.width)
|
||||||
|
.with_height(theme.contacts_popover.height)
|
||||||
|
.boxed()
|
||||||
|
})
|
||||||
|
.on_down_out(MouseButton::Left, move |_, cx| {
|
||||||
|
cx.dispatch_action(ToggleCollaborationMenu);
|
||||||
|
})
|
||||||
|
.boxed()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||||
|
if cx.is_self_focused() {
|
||||||
|
match &self.child {
|
||||||
|
Child::ContactList(child) => cx.focus(child),
|
||||||
|
Child::ContactFinder(child) => cx.focus(child),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
232
crates/collab_ui/src/incoming_call_notification.rs
Normal file
232
crates/collab_ui/src/incoming_call_notification.rs
Normal file
@ -0,0 +1,232 @@
|
|||||||
|
use call::{ActiveCall, IncomingCall};
|
||||||
|
use client::proto;
|
||||||
|
use futures::StreamExt;
|
||||||
|
use gpui::{
|
||||||
|
elements::*,
|
||||||
|
geometry::{rect::RectF, vector::vec2f},
|
||||||
|
impl_internal_actions, CursorStyle, Entity, MouseButton, MutableAppContext, RenderContext,
|
||||||
|
View, ViewContext, WindowBounds, WindowKind, WindowOptions,
|
||||||
|
};
|
||||||
|
use settings::Settings;
|
||||||
|
use util::ResultExt;
|
||||||
|
use workspace::JoinProject;
|
||||||
|
|
||||||
|
impl_internal_actions!(incoming_call_notification, [RespondToCall]);
|
||||||
|
|
||||||
|
pub fn init(cx: &mut MutableAppContext) {
|
||||||
|
cx.add_action(IncomingCallNotification::respond_to_call);
|
||||||
|
|
||||||
|
let mut incoming_call = ActiveCall::global(cx).read(cx).incoming();
|
||||||
|
cx.spawn(|mut cx| async move {
|
||||||
|
let mut notification_window = None;
|
||||||
|
while let Some(incoming_call) = incoming_call.next().await {
|
||||||
|
if let Some(window_id) = notification_window.take() {
|
||||||
|
cx.remove_window(window_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(incoming_call) = incoming_call {
|
||||||
|
const PADDING: f32 = 16.;
|
||||||
|
let screen_size = cx.platform().screen_size();
|
||||||
|
|
||||||
|
let window_size = cx.read(|cx| {
|
||||||
|
let theme = &cx.global::<Settings>().theme.incoming_call_notification;
|
||||||
|
vec2f(theme.window_width, theme.window_height)
|
||||||
|
});
|
||||||
|
let (window_id, _) = cx.add_window(
|
||||||
|
WindowOptions {
|
||||||
|
bounds: WindowBounds::Fixed(RectF::new(
|
||||||
|
vec2f(screen_size.x() - window_size.x() - PADDING, PADDING),
|
||||||
|
window_size,
|
||||||
|
)),
|
||||||
|
titlebar: None,
|
||||||
|
center: false,
|
||||||
|
kind: WindowKind::PopUp,
|
||||||
|
is_movable: false,
|
||||||
|
},
|
||||||
|
|_| IncomingCallNotification::new(incoming_call),
|
||||||
|
);
|
||||||
|
notification_window = Some(window_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq)]
|
||||||
|
struct RespondToCall {
|
||||||
|
accept: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct IncomingCallNotification {
|
||||||
|
call: IncomingCall,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IncomingCallNotification {
|
||||||
|
pub fn new(call: IncomingCall) -> Self {
|
||||||
|
Self { call }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn respond_to_call(&mut self, action: &RespondToCall, cx: &mut ViewContext<Self>) {
|
||||||
|
let active_call = ActiveCall::global(cx);
|
||||||
|
if action.accept {
|
||||||
|
let join = active_call.update(cx, |active_call, cx| active_call.accept_incoming(cx));
|
||||||
|
let caller_user_id = self.call.caller.id;
|
||||||
|
let initial_project_id = self.call.initial_project.as_ref().map(|project| project.id);
|
||||||
|
cx.spawn_weak(|_, mut cx| async move {
|
||||||
|
join.await?;
|
||||||
|
if let Some(project_id) = initial_project_id {
|
||||||
|
cx.update(|cx| {
|
||||||
|
cx.dispatch_global_action(JoinProject {
|
||||||
|
project_id,
|
||||||
|
follow_user_id: caller_user_id,
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
anyhow::Ok(())
|
||||||
|
})
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
} else {
|
||||||
|
active_call.update(cx, |active_call, _| {
|
||||||
|
active_call.decline_incoming().log_err();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_caller(&self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||||
|
let theme = &cx.global::<Settings>().theme.incoming_call_notification;
|
||||||
|
let default_project = proto::ParticipantProject::default();
|
||||||
|
let initial_project = self
|
||||||
|
.call
|
||||||
|
.initial_project
|
||||||
|
.as_ref()
|
||||||
|
.unwrap_or(&default_project);
|
||||||
|
Flex::row()
|
||||||
|
.with_children(self.call.caller.avatar.clone().map(|avatar| {
|
||||||
|
Image::new(avatar)
|
||||||
|
.with_style(theme.caller_avatar)
|
||||||
|
.aligned()
|
||||||
|
.boxed()
|
||||||
|
}))
|
||||||
|
.with_child(
|
||||||
|
Flex::column()
|
||||||
|
.with_child(
|
||||||
|
Label::new(
|
||||||
|
self.call.caller.github_login.clone(),
|
||||||
|
theme.caller_username.text.clone(),
|
||||||
|
)
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.caller_username.container)
|
||||||
|
.boxed(),
|
||||||
|
)
|
||||||
|
.with_child(
|
||||||
|
Label::new(
|
||||||
|
format!(
|
||||||
|
"is sharing a project in Zed{}",
|
||||||
|
if initial_project.worktree_root_names.is_empty() {
|
||||||
|
""
|
||||||
|
} else {
|
||||||
|
":"
|
||||||
|
}
|
||||||
|
),
|
||||||
|
theme.caller_message.text.clone(),
|
||||||
|
)
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.caller_message.container)
|
||||||
|
.boxed(),
|
||||||
|
)
|
||||||
|
.with_children(if initial_project.worktree_root_names.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(
|
||||||
|
Label::new(
|
||||||
|
initial_project.worktree_root_names.join(", "),
|
||||||
|
theme.worktree_roots.text.clone(),
|
||||||
|
)
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.worktree_roots.container)
|
||||||
|
.boxed(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.caller_metadata)
|
||||||
|
.aligned()
|
||||||
|
.boxed(),
|
||||||
|
)
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.caller_container)
|
||||||
|
.flex(1., true)
|
||||||
|
.boxed()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_buttons(&self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||||
|
enum Accept {}
|
||||||
|
enum Decline {}
|
||||||
|
|
||||||
|
Flex::column()
|
||||||
|
.with_child(
|
||||||
|
MouseEventHandler::<Accept>::new(0, cx, |_, cx| {
|
||||||
|
let theme = &cx.global::<Settings>().theme.incoming_call_notification;
|
||||||
|
Label::new("Accept".to_string(), theme.accept_button.text.clone())
|
||||||
|
.aligned()
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.accept_button.container)
|
||||||
|
.boxed()
|
||||||
|
})
|
||||||
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
|
.on_click(MouseButton::Left, |_, cx| {
|
||||||
|
cx.dispatch_action(RespondToCall { accept: true });
|
||||||
|
})
|
||||||
|
.flex(1., true)
|
||||||
|
.boxed(),
|
||||||
|
)
|
||||||
|
.with_child(
|
||||||
|
MouseEventHandler::<Decline>::new(0, cx, |_, cx| {
|
||||||
|
let theme = &cx.global::<Settings>().theme.incoming_call_notification;
|
||||||
|
Label::new("Decline".to_string(), theme.decline_button.text.clone())
|
||||||
|
.aligned()
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.decline_button.container)
|
||||||
|
.boxed()
|
||||||
|
})
|
||||||
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
|
.on_click(MouseButton::Left, |_, cx| {
|
||||||
|
cx.dispatch_action(RespondToCall { accept: false });
|
||||||
|
})
|
||||||
|
.flex(1., true)
|
||||||
|
.boxed(),
|
||||||
|
)
|
||||||
|
.constrained()
|
||||||
|
.with_width(
|
||||||
|
cx.global::<Settings>()
|
||||||
|
.theme
|
||||||
|
.incoming_call_notification
|
||||||
|
.button_width,
|
||||||
|
)
|
||||||
|
.boxed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Entity for IncomingCallNotification {
|
||||||
|
type Event = ();
|
||||||
|
}
|
||||||
|
|
||||||
|
impl View for IncomingCallNotification {
|
||||||
|
fn ui_name() -> &'static str {
|
||||||
|
"IncomingCallNotification"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(&mut self, cx: &mut RenderContext<Self>) -> gpui::ElementBox {
|
||||||
|
let background = cx
|
||||||
|
.global::<Settings>()
|
||||||
|
.theme
|
||||||
|
.incoming_call_notification
|
||||||
|
.background;
|
||||||
|
Flex::row()
|
||||||
|
.with_child(self.render_caller(cx))
|
||||||
|
.with_child(self.render_buttons(cx))
|
||||||
|
.contained()
|
||||||
|
.with_background_color(background)
|
||||||
|
.expanded()
|
||||||
|
.boxed()
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,7 @@
|
|||||||
use crate::render_icon_button;
|
|
||||||
use client::User;
|
use client::User;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
elements::{Flex, Image, Label, MouseEventHandler, Padding, ParentElement, Text},
|
elements::*, platform::CursorStyle, Action, Element, ElementBox, MouseButton, RenderContext,
|
||||||
platform::CursorStyle,
|
View,
|
||||||
Action, Element, ElementBox, MouseButton, RenderContext, View,
|
|
||||||
};
|
};
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@ -53,10 +51,17 @@ pub fn render_user_notification<V: View, A: Action + Clone>(
|
|||||||
)
|
)
|
||||||
.with_child(
|
.with_child(
|
||||||
MouseEventHandler::<Dismiss>::new(user.id as usize, cx, |state, _| {
|
MouseEventHandler::<Dismiss>::new(user.id as usize, cx, |state, _| {
|
||||||
render_icon_button(
|
let style = theme.dismiss_button.style_for(state, false);
|
||||||
theme.dismiss_button.style_for(state, false),
|
Svg::new("icons/x_mark_thin_8.svg")
|
||||||
"icons/x_mark_thin_8.svg",
|
.with_color(style.color)
|
||||||
)
|
.constrained()
|
||||||
|
.with_width(style.icon_width)
|
||||||
|
.aligned()
|
||||||
|
.contained()
|
||||||
|
.with_style(style.container)
|
||||||
|
.constrained()
|
||||||
|
.with_width(style.button_width)
|
||||||
|
.with_height(style.button_width)
|
||||||
.boxed()
|
.boxed()
|
||||||
})
|
})
|
||||||
.with_cursor_style(CursorStyle::PointingHand)
|
.with_cursor_style(CursorStyle::PointingHand)
|
232
crates/collab_ui/src/project_shared_notification.rs
Normal file
232
crates/collab_ui/src/project_shared_notification.rs
Normal file
@ -0,0 +1,232 @@
|
|||||||
|
use call::{room, ActiveCall};
|
||||||
|
use client::User;
|
||||||
|
use collections::HashMap;
|
||||||
|
use gpui::{
|
||||||
|
actions,
|
||||||
|
elements::*,
|
||||||
|
geometry::{rect::RectF, vector::vec2f},
|
||||||
|
CursorStyle, Entity, MouseButton, MutableAppContext, RenderContext, View, ViewContext,
|
||||||
|
WindowBounds, WindowKind, WindowOptions,
|
||||||
|
};
|
||||||
|
use settings::Settings;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use workspace::JoinProject;
|
||||||
|
|
||||||
|
actions!(project_shared_notification, [DismissProject]);
|
||||||
|
|
||||||
|
pub fn init(cx: &mut MutableAppContext) {
|
||||||
|
cx.add_action(ProjectSharedNotification::join);
|
||||||
|
cx.add_action(ProjectSharedNotification::dismiss);
|
||||||
|
|
||||||
|
let active_call = ActiveCall::global(cx);
|
||||||
|
let mut notification_windows = HashMap::default();
|
||||||
|
cx.subscribe(&active_call, move |_, event, cx| match event {
|
||||||
|
room::Event::RemoteProjectShared {
|
||||||
|
owner,
|
||||||
|
project_id,
|
||||||
|
worktree_root_names,
|
||||||
|
} => {
|
||||||
|
const PADDING: f32 = 16.;
|
||||||
|
let screen_size = cx.platform().screen_size();
|
||||||
|
|
||||||
|
let theme = &cx.global::<Settings>().theme.project_shared_notification;
|
||||||
|
let window_size = vec2f(theme.window_width, theme.window_height);
|
||||||
|
let (window_id, _) = cx.add_window(
|
||||||
|
WindowOptions {
|
||||||
|
bounds: WindowBounds::Fixed(RectF::new(
|
||||||
|
vec2f(screen_size.x() - window_size.x() - PADDING, PADDING),
|
||||||
|
window_size,
|
||||||
|
)),
|
||||||
|
titlebar: None,
|
||||||
|
center: false,
|
||||||
|
kind: WindowKind::PopUp,
|
||||||
|
is_movable: false,
|
||||||
|
},
|
||||||
|
|_| {
|
||||||
|
ProjectSharedNotification::new(
|
||||||
|
owner.clone(),
|
||||||
|
*project_id,
|
||||||
|
worktree_root_names.clone(),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
);
|
||||||
|
notification_windows.insert(*project_id, window_id);
|
||||||
|
}
|
||||||
|
room::Event::RemoteProjectUnshared { project_id } => {
|
||||||
|
if let Some(window_id) = notification_windows.remove(&project_id) {
|
||||||
|
cx.remove_window(window_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
room::Event::Left => {
|
||||||
|
for (_, window_id) in notification_windows.drain() {
|
||||||
|
cx.remove_window(window_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ProjectSharedNotification {
|
||||||
|
project_id: u64,
|
||||||
|
worktree_root_names: Vec<String>,
|
||||||
|
owner: Arc<User>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProjectSharedNotification {
|
||||||
|
fn new(owner: Arc<User>, project_id: u64, worktree_root_names: Vec<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
project_id,
|
||||||
|
worktree_root_names,
|
||||||
|
owner,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn join(&mut self, _: &JoinProject, cx: &mut ViewContext<Self>) {
|
||||||
|
let window_id = cx.window_id();
|
||||||
|
cx.remove_window(window_id);
|
||||||
|
cx.propagate_action();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dismiss(&mut self, _: &DismissProject, cx: &mut ViewContext<Self>) {
|
||||||
|
let window_id = cx.window_id();
|
||||||
|
cx.remove_window(window_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_owner(&self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||||
|
let theme = &cx.global::<Settings>().theme.project_shared_notification;
|
||||||
|
Flex::row()
|
||||||
|
.with_children(self.owner.avatar.clone().map(|avatar| {
|
||||||
|
Image::new(avatar)
|
||||||
|
.with_style(theme.owner_avatar)
|
||||||
|
.aligned()
|
||||||
|
.boxed()
|
||||||
|
}))
|
||||||
|
.with_child(
|
||||||
|
Flex::column()
|
||||||
|
.with_child(
|
||||||
|
Label::new(
|
||||||
|
self.owner.github_login.clone(),
|
||||||
|
theme.owner_username.text.clone(),
|
||||||
|
)
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.owner_username.container)
|
||||||
|
.boxed(),
|
||||||
|
)
|
||||||
|
.with_child(
|
||||||
|
Label::new(
|
||||||
|
format!(
|
||||||
|
"is sharing a project in Zed{}",
|
||||||
|
if self.worktree_root_names.is_empty() {
|
||||||
|
""
|
||||||
|
} else {
|
||||||
|
":"
|
||||||
|
}
|
||||||
|
),
|
||||||
|
theme.message.text.clone(),
|
||||||
|
)
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.message.container)
|
||||||
|
.boxed(),
|
||||||
|
)
|
||||||
|
.with_children(if self.worktree_root_names.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(
|
||||||
|
Label::new(
|
||||||
|
self.worktree_root_names.join(", "),
|
||||||
|
theme.worktree_roots.text.clone(),
|
||||||
|
)
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.worktree_roots.container)
|
||||||
|
.boxed(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.owner_metadata)
|
||||||
|
.aligned()
|
||||||
|
.boxed(),
|
||||||
|
)
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.owner_container)
|
||||||
|
.flex(1., true)
|
||||||
|
.boxed()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_buttons(&self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||||
|
enum Open {}
|
||||||
|
enum Dismiss {}
|
||||||
|
|
||||||
|
let project_id = self.project_id;
|
||||||
|
let owner_user_id = self.owner.id;
|
||||||
|
|
||||||
|
Flex::column()
|
||||||
|
.with_child(
|
||||||
|
MouseEventHandler::<Open>::new(0, cx, |_, cx| {
|
||||||
|
let theme = &cx.global::<Settings>().theme.project_shared_notification;
|
||||||
|
Label::new("Open".to_string(), theme.open_button.text.clone())
|
||||||
|
.aligned()
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.open_button.container)
|
||||||
|
.boxed()
|
||||||
|
})
|
||||||
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
|
.on_click(MouseButton::Left, move |_, cx| {
|
||||||
|
cx.dispatch_action(JoinProject {
|
||||||
|
project_id,
|
||||||
|
follow_user_id: owner_user_id,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.flex(1., true)
|
||||||
|
.boxed(),
|
||||||
|
)
|
||||||
|
.with_child(
|
||||||
|
MouseEventHandler::<Dismiss>::new(0, cx, |_, cx| {
|
||||||
|
let theme = &cx.global::<Settings>().theme.project_shared_notification;
|
||||||
|
Label::new("Dismiss".to_string(), theme.dismiss_button.text.clone())
|
||||||
|
.aligned()
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.dismiss_button.container)
|
||||||
|
.boxed()
|
||||||
|
})
|
||||||
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
|
.on_click(MouseButton::Left, |_, cx| {
|
||||||
|
cx.dispatch_action(DismissProject);
|
||||||
|
})
|
||||||
|
.flex(1., true)
|
||||||
|
.boxed(),
|
||||||
|
)
|
||||||
|
.constrained()
|
||||||
|
.with_width(
|
||||||
|
cx.global::<Settings>()
|
||||||
|
.theme
|
||||||
|
.project_shared_notification
|
||||||
|
.button_width,
|
||||||
|
)
|
||||||
|
.boxed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Entity for ProjectSharedNotification {
|
||||||
|
type Event = ();
|
||||||
|
}
|
||||||
|
|
||||||
|
impl View for ProjectSharedNotification {
|
||||||
|
fn ui_name() -> &'static str {
|
||||||
|
"ProjectSharedNotification"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(&mut self, cx: &mut RenderContext<Self>) -> gpui::ElementBox {
|
||||||
|
let background = cx
|
||||||
|
.global::<Settings>()
|
||||||
|
.theme
|
||||||
|
.project_shared_notification
|
||||||
|
.background;
|
||||||
|
Flex::row()
|
||||||
|
.with_child(self.render_owner(cx))
|
||||||
|
.with_child(self.render_buttons(cx))
|
||||||
|
.contained()
|
||||||
|
.with_background_color(background)
|
||||||
|
.expanded()
|
||||||
|
.boxed()
|
||||||
|
}
|
||||||
|
}
|
@ -4,8 +4,8 @@ use gpui::{
|
|||||||
actions,
|
actions,
|
||||||
elements::{ChildView, Flex, Label, ParentElement},
|
elements::{ChildView, Flex, Label, ParentElement},
|
||||||
keymap::Keystroke,
|
keymap::Keystroke,
|
||||||
Action, AnyViewHandle, Element, Entity, MouseState, MutableAppContext, View, ViewContext,
|
Action, AnyViewHandle, Element, Entity, MouseState, MutableAppContext, RenderContext, View,
|
||||||
ViewHandle,
|
ViewContext, ViewHandle,
|
||||||
};
|
};
|
||||||
use picker::{Picker, PickerDelegate};
|
use picker::{Picker, PickerDelegate};
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
@ -131,8 +131,8 @@ impl View for CommandPalette {
|
|||||||
"CommandPalette"
|
"CommandPalette"
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render(&mut self, _: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox {
|
fn render(&mut self, cx: &mut RenderContext<Self>) -> gpui::ElementBox {
|
||||||
ChildView::new(self.picker.clone()).boxed()
|
ChildView::new(self.picker.clone(), cx).boxed()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
fn on_focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||||
@ -224,7 +224,7 @@ impl PickerDelegate for CommandPalette {
|
|||||||
fn render_match(
|
fn render_match(
|
||||||
&self,
|
&self,
|
||||||
ix: usize,
|
ix: usize,
|
||||||
mouse_state: MouseState,
|
mouse_state: &mut MouseState,
|
||||||
selected: bool,
|
selected: bool,
|
||||||
cx: &gpui::AppContext,
|
cx: &gpui::AppContext,
|
||||||
) -> gpui::ElementBox {
|
) -> gpui::ElementBox {
|
||||||
|
@ -1,32 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "contacts_panel"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
[lib]
|
|
||||||
path = "src/contacts_panel.rs"
|
|
||||||
doctest = false
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
client = { path = "../client" }
|
|
||||||
collections = { path = "../collections" }
|
|
||||||
editor = { path = "../editor" }
|
|
||||||
fuzzy = { path = "../fuzzy" }
|
|
||||||
gpui = { path = "../gpui" }
|
|
||||||
menu = { path = "../menu" }
|
|
||||||
picker = { path = "../picker" }
|
|
||||||
project = { path = "../project" }
|
|
||||||
settings = { path = "../settings" }
|
|
||||||
theme = { path = "../theme" }
|
|
||||||
util = { path = "../util" }
|
|
||||||
workspace = { path = "../workspace" }
|
|
||||||
anyhow = "1.0"
|
|
||||||
futures = "0.3"
|
|
||||||
log = "0.4"
|
|
||||||
postage = { version = "0.4.1", features = ["futures-traits"] }
|
|
||||||
serde = { version = "1.0", features = ["derive", "rc"] }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
language = { path = "../language", features = ["test-support"] }
|
|
||||||
project = { path = "../project", features = ["test-support"] }
|
|
||||||
workspace = { path = "../workspace", features = ["test-support"] }
|
|
File diff suppressed because it is too large
Load Diff
@ -1,80 +0,0 @@
|
|||||||
use client::User;
|
|
||||||
use gpui::{
|
|
||||||
actions, ElementBox, Entity, ModelHandle, MutableAppContext, RenderContext, View, ViewContext,
|
|
||||||
};
|
|
||||||
use project::Project;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use workspace::Notification;
|
|
||||||
|
|
||||||
use crate::notifications::render_user_notification;
|
|
||||||
|
|
||||||
pub fn init(cx: &mut MutableAppContext) {
|
|
||||||
cx.add_action(JoinProjectNotification::decline);
|
|
||||||
cx.add_action(JoinProjectNotification::accept);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum Event {
|
|
||||||
Dismiss,
|
|
||||||
}
|
|
||||||
|
|
||||||
actions!(contacts_panel, [Accept, Decline]);
|
|
||||||
|
|
||||||
pub struct JoinProjectNotification {
|
|
||||||
project: ModelHandle<Project>,
|
|
||||||
user: Arc<User>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl JoinProjectNotification {
|
|
||||||
pub fn new(project: ModelHandle<Project>, user: Arc<User>, cx: &mut ViewContext<Self>) -> Self {
|
|
||||||
cx.subscribe(&project, |this, _, event, cx| {
|
|
||||||
if let project::Event::ContactCancelledJoinRequest(user) = event {
|
|
||||||
if *user == this.user {
|
|
||||||
cx.emit(Event::Dismiss);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
Self { project, user }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn decline(&mut self, _: &Decline, cx: &mut ViewContext<Self>) {
|
|
||||||
self.project.update(cx, |project, cx| {
|
|
||||||
project.respond_to_join_request(self.user.id, false, cx)
|
|
||||||
});
|
|
||||||
cx.emit(Event::Dismiss)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn accept(&mut self, _: &Accept, cx: &mut ViewContext<Self>) {
|
|
||||||
self.project.update(cx, |project, cx| {
|
|
||||||
project.respond_to_join_request(self.user.id, true, cx)
|
|
||||||
});
|
|
||||||
cx.emit(Event::Dismiss)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Entity for JoinProjectNotification {
|
|
||||||
type Event = Event;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl View for JoinProjectNotification {
|
|
||||||
fn ui_name() -> &'static str {
|
|
||||||
"JoinProjectNotification"
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
|
||||||
render_user_notification(
|
|
||||||
self.user.clone(),
|
|
||||||
"wants to join your project",
|
|
||||||
None,
|
|
||||||
Decline,
|
|
||||||
vec![("Decline", Box::new(Decline)), ("Accept", Box::new(Accept))],
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Notification for JoinProjectNotification {
|
|
||||||
fn should_dismiss_notification_on_event(&self, event: &<Self as Entity>::Event) -> bool {
|
|
||||||
matches!(event, Event::Dismiss)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,32 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "contacts_status_item"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
[lib]
|
|
||||||
path = "src/contacts_status_item.rs"
|
|
||||||
doctest = false
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
client = { path = "../client" }
|
|
||||||
collections = { path = "../collections" }
|
|
||||||
editor = { path = "../editor" }
|
|
||||||
fuzzy = { path = "../fuzzy" }
|
|
||||||
gpui = { path = "../gpui" }
|
|
||||||
menu = { path = "../menu" }
|
|
||||||
picker = { path = "../picker" }
|
|
||||||
project = { path = "../project" }
|
|
||||||
settings = { path = "../settings" }
|
|
||||||
theme = { path = "../theme" }
|
|
||||||
util = { path = "../util" }
|
|
||||||
workspace = { path = "../workspace" }
|
|
||||||
anyhow = "1.0"
|
|
||||||
futures = "0.3"
|
|
||||||
log = "0.4"
|
|
||||||
postage = { version = "0.4.1", features = ["futures-traits"] }
|
|
||||||
serde = { version = "1.0", features = ["derive", "rc"] }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
language = { path = "../language", features = ["test-support"] }
|
|
||||||
project = { path = "../project", features = ["test-support"] }
|
|
||||||
workspace = { path = "../workspace", features = ["test-support"] }
|
|
@ -1,94 +0,0 @@
|
|||||||
use editor::Editor;
|
|
||||||
use gpui::{elements::*, Entity, RenderContext, View, ViewContext, ViewHandle};
|
|
||||||
use settings::Settings;
|
|
||||||
|
|
||||||
pub enum Event {
|
|
||||||
Deactivated,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct ContactsPopover {
|
|
||||||
filter_editor: ViewHandle<Editor>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Entity for ContactsPopover {
|
|
||||||
type Event = Event;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl View for ContactsPopover {
|
|
||||||
fn ui_name() -> &'static str {
|
|
||||||
"ContactsPopover"
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
|
||||||
let theme = &cx.global::<Settings>().theme.contacts_popover;
|
|
||||||
|
|
||||||
Flex::row()
|
|
||||||
.with_child(
|
|
||||||
ChildView::new(self.filter_editor.clone())
|
|
||||||
.contained()
|
|
||||||
.with_style(
|
|
||||||
cx.global::<Settings>()
|
|
||||||
.theme
|
|
||||||
.contacts_panel
|
|
||||||
.user_query_editor
|
|
||||||
.container,
|
|
||||||
)
|
|
||||||
.flex(1., true)
|
|
||||||
.boxed(),
|
|
||||||
)
|
|
||||||
// .with_child(
|
|
||||||
// MouseEventHandler::<AddContact>::new(0, cx, |_, _| {
|
|
||||||
// Svg::new("icons/user_plus_16.svg")
|
|
||||||
// .with_color(theme.add_contact_button.color)
|
|
||||||
// .constrained()
|
|
||||||
// .with_height(16.)
|
|
||||||
// .contained()
|
|
||||||
// .with_style(theme.add_contact_button.container)
|
|
||||||
// .aligned()
|
|
||||||
// .boxed()
|
|
||||||
// })
|
|
||||||
// .with_cursor_style(CursorStyle::PointingHand)
|
|
||||||
// .on_click(MouseButton::Left, |_, cx| {
|
|
||||||
// cx.dispatch_action(contact_finder::Toggle)
|
|
||||||
// })
|
|
||||||
// .boxed(),
|
|
||||||
// )
|
|
||||||
.constrained()
|
|
||||||
.with_height(
|
|
||||||
cx.global::<Settings>()
|
|
||||||
.theme
|
|
||||||
.contacts_panel
|
|
||||||
.user_query_editor_height,
|
|
||||||
)
|
|
||||||
.aligned()
|
|
||||||
.top()
|
|
||||||
.contained()
|
|
||||||
.with_background_color(theme.background)
|
|
||||||
.with_uniform_padding(4.)
|
|
||||||
.boxed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ContactsPopover {
|
|
||||||
pub fn new(cx: &mut ViewContext<Self>) -> Self {
|
|
||||||
cx.observe_window_activation(Self::window_activation_changed)
|
|
||||||
.detach();
|
|
||||||
|
|
||||||
let filter_editor = cx.add_view(|cx| {
|
|
||||||
let mut editor = Editor::single_line(
|
|
||||||
Some(|theme| theme.contacts_panel.user_query_editor.clone()),
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
editor.set_placeholder_text("Filter contacts", cx);
|
|
||||||
editor
|
|
||||||
});
|
|
||||||
|
|
||||||
Self { filter_editor }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn window_activation_changed(&mut self, is_active: bool, cx: &mut ViewContext<Self>) {
|
|
||||||
if !is_active {
|
|
||||||
cx.emit(Event::Deactivated);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,94 +0,0 @@
|
|||||||
mod contacts_popover;
|
|
||||||
|
|
||||||
use contacts_popover::ContactsPopover;
|
|
||||||
use gpui::{
|
|
||||||
actions,
|
|
||||||
color::Color,
|
|
||||||
elements::*,
|
|
||||||
geometry::{rect::RectF, vector::vec2f},
|
|
||||||
Appearance, Entity, MouseButton, MutableAppContext, RenderContext, View, ViewContext,
|
|
||||||
ViewHandle, WindowKind,
|
|
||||||
};
|
|
||||||
|
|
||||||
actions!(contacts_status_item, [ToggleContactsPopover]);
|
|
||||||
|
|
||||||
pub fn init(cx: &mut MutableAppContext) {
|
|
||||||
cx.add_action(ContactsStatusItem::toggle_contacts_popover);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct ContactsStatusItem {
|
|
||||||
popover: Option<ViewHandle<ContactsPopover>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Entity for ContactsStatusItem {
|
|
||||||
type Event = ();
|
|
||||||
}
|
|
||||||
|
|
||||||
impl View for ContactsStatusItem {
|
|
||||||
fn ui_name() -> &'static str {
|
|
||||||
"ContactsStatusItem"
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
|
||||||
let color = match cx.appearance {
|
|
||||||
Appearance::Light | Appearance::VibrantLight => Color::black(),
|
|
||||||
Appearance::Dark | Appearance::VibrantDark => Color::white(),
|
|
||||||
};
|
|
||||||
MouseEventHandler::<Self>::new(0, cx, |_, _| {
|
|
||||||
Svg::new("icons/zed_22.svg")
|
|
||||||
.with_color(color)
|
|
||||||
.aligned()
|
|
||||||
.boxed()
|
|
||||||
})
|
|
||||||
.on_click(MouseButton::Left, |_, cx| {
|
|
||||||
cx.dispatch_action(ToggleContactsPopover);
|
|
||||||
})
|
|
||||||
.boxed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ContactsStatusItem {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self { popover: None }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn toggle_contacts_popover(&mut self, _: &ToggleContactsPopover, cx: &mut ViewContext<Self>) {
|
|
||||||
match self.popover.take() {
|
|
||||||
Some(popover) => {
|
|
||||||
cx.remove_window(popover.window_id());
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
let window_bounds = cx.window_bounds();
|
|
||||||
let size = vec2f(360., 460.);
|
|
||||||
let origin = window_bounds.lower_left()
|
|
||||||
+ vec2f(window_bounds.width() / 2. - size.x() / 2., 0.);
|
|
||||||
let (_, popover) = cx.add_window(
|
|
||||||
gpui::WindowOptions {
|
|
||||||
bounds: gpui::WindowBounds::Fixed(RectF::new(origin, size)),
|
|
||||||
titlebar: None,
|
|
||||||
center: false,
|
|
||||||
kind: WindowKind::PopUp,
|
|
||||||
is_movable: false,
|
|
||||||
},
|
|
||||||
|cx| ContactsPopover::new(cx),
|
|
||||||
);
|
|
||||||
cx.subscribe(&popover, Self::on_popover_event).detach();
|
|
||||||
self.popover = Some(popover);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_popover_event(
|
|
||||||
&mut self,
|
|
||||||
popover: ViewHandle<ContactsPopover>,
|
|
||||||
event: &contacts_popover::Event,
|
|
||||||
cx: &mut ViewContext<Self>,
|
|
||||||
) {
|
|
||||||
match event {
|
|
||||||
contacts_popover::Event::Deactivated => {
|
|
||||||
self.popover.take();
|
|
||||||
cx.remove_window(popover.window_id());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -258,9 +258,10 @@ impl ContextMenu {
|
|||||||
.with_children(self.items.iter().enumerate().map(|(ix, item)| {
|
.with_children(self.items.iter().enumerate().map(|(ix, item)| {
|
||||||
match item {
|
match item {
|
||||||
ContextMenuItem::Item { label, .. } => {
|
ContextMenuItem::Item { label, .. } => {
|
||||||
let style = style
|
let style = style.item.style_for(
|
||||||
.item
|
&mut Default::default(),
|
||||||
.style_for(Default::default(), Some(ix) == self.selected_index);
|
Some(ix) == self.selected_index,
|
||||||
|
);
|
||||||
|
|
||||||
Label::new(label.to_string(), style.label.clone())
|
Label::new(label.to_string(), style.label.clone())
|
||||||
.contained()
|
.contained()
|
||||||
@ -283,9 +284,10 @@ impl ContextMenu {
|
|||||||
.with_children(self.items.iter().enumerate().map(|(ix, item)| {
|
.with_children(self.items.iter().enumerate().map(|(ix, item)| {
|
||||||
match item {
|
match item {
|
||||||
ContextMenuItem::Item { action, .. } => {
|
ContextMenuItem::Item { action, .. } => {
|
||||||
let style = style
|
let style = style.item.style_for(
|
||||||
.item
|
&mut Default::default(),
|
||||||
.style_for(Default::default(), Some(ix) == self.selected_index);
|
Some(ix) == self.selected_index,
|
||||||
|
);
|
||||||
KeystrokeLabel::new(
|
KeystrokeLabel::new(
|
||||||
action.boxed_clone(),
|
action.boxed_clone(),
|
||||||
style.keystroke.container,
|
style.keystroke.container,
|
||||||
|
22
crates/db/Cargo.toml
Normal file
22
crates/db/Cargo.toml
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
[package]
|
||||||
|
name = "db"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "src/db.rs"
|
||||||
|
doctest = false
|
||||||
|
|
||||||
|
[features]
|
||||||
|
test-support = []
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
collections = { path = "../collections" }
|
||||||
|
anyhow = "1.0.57"
|
||||||
|
async-trait = "0.1"
|
||||||
|
parking_lot = "0.11.1"
|
||||||
|
rocksdb = "0.18"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
gpui = { path = "../gpui", features = ["test-support"] }
|
||||||
|
tempdir = { version = "0.3.7" }
|
@ -95,7 +95,7 @@ impl View for ProjectDiagnosticsEditor {
|
|||||||
.with_style(theme.container)
|
.with_style(theme.container)
|
||||||
.boxed()
|
.boxed()
|
||||||
} else {
|
} else {
|
||||||
ChildView::new(&self.editor).boxed()
|
ChildView::new(&self.editor, cx).boxed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,6 +25,7 @@ clock = { path = "../clock" }
|
|||||||
collections = { path = "../collections" }
|
collections = { path = "../collections" }
|
||||||
context_menu = { path = "../context_menu" }
|
context_menu = { path = "../context_menu" }
|
||||||
fuzzy = { path = "../fuzzy" }
|
fuzzy = { path = "../fuzzy" }
|
||||||
|
git = { path = "../git" }
|
||||||
gpui = { path = "../gpui" }
|
gpui = { path = "../gpui" }
|
||||||
language = { path = "../language" }
|
language = { path = "../language" }
|
||||||
lsp = { path = "../lsp" }
|
lsp = { path = "../lsp" }
|
||||||
@ -47,10 +48,12 @@ ordered-float = "2.1.1"
|
|||||||
parking_lot = "0.11"
|
parking_lot = "0.11"
|
||||||
postage = { version = "0.4", features = ["futures-traits"] }
|
postage = { version = "0.4", features = ["futures-traits"] }
|
||||||
rand = { version = "0.8.3", optional = true }
|
rand = { version = "0.8.3", optional = true }
|
||||||
serde = { version = "1.0", features = ["derive", "rc"] }
|
serde = { workspace = true }
|
||||||
smallvec = { version = "1.6", features = ["union"] }
|
smallvec = { version = "1.6", features = ["union"] }
|
||||||
smol = "1.2"
|
smol = "1.2"
|
||||||
tree-sitter-rust = { version = "*", optional = true }
|
tree-sitter-rust = { version = "*", optional = true }
|
||||||
|
tree-sitter-html = { version = "*", optional = true }
|
||||||
|
tree-sitter-javascript = { version = "*", optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
text = { path = "../text", features = ["test-support"] }
|
text = { path = "../text", features = ["test-support"] }
|
||||||
@ -67,3 +70,5 @@ rand = "0.8"
|
|||||||
unindent = "0.1.7"
|
unindent = "0.1.7"
|
||||||
tree-sitter = "0.20"
|
tree-sitter = "0.20"
|
||||||
tree-sitter-rust = "0.20"
|
tree-sitter-rust = "0.20"
|
||||||
|
tree-sitter-html = "0.19"
|
||||||
|
tree-sitter-javascript = "0.20"
|
||||||
|
@ -330,34 +330,91 @@ impl DisplaySnapshot {
|
|||||||
DisplayPoint(self.blocks_snapshot.max_point())
|
DisplayPoint(self.blocks_snapshot.max_point())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns text chunks starting at the given display row until the end of the file
|
||||||
pub fn text_chunks(&self, display_row: u32) -> impl Iterator<Item = &str> {
|
pub fn text_chunks(&self, display_row: u32) -> impl Iterator<Item = &str> {
|
||||||
self.blocks_snapshot
|
self.blocks_snapshot
|
||||||
.chunks(display_row..self.max_point().row() + 1, false, None)
|
.chunks(display_row..self.max_point().row() + 1, false, None)
|
||||||
.map(|h| h.text)
|
.map(|h| h.text)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns text chunks starting at the end of the given display row in reverse until the start of the file
|
||||||
|
pub fn reverse_text_chunks(&self, display_row: u32) -> impl Iterator<Item = &str> {
|
||||||
|
(0..=display_row).into_iter().rev().flat_map(|row| {
|
||||||
|
self.blocks_snapshot
|
||||||
|
.chunks(row..row + 1, false, None)
|
||||||
|
.map(|h| h.text)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.into_iter()
|
||||||
|
.rev()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn chunks(&self, display_rows: Range<u32>, language_aware: bool) -> DisplayChunks<'_> {
|
pub fn chunks(&self, display_rows: Range<u32>, language_aware: bool) -> DisplayChunks<'_> {
|
||||||
self.blocks_snapshot
|
self.blocks_snapshot
|
||||||
.chunks(display_rows, language_aware, Some(&self.text_highlights))
|
.chunks(display_rows, language_aware, Some(&self.text_highlights))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn chars_at(&self, point: DisplayPoint) -> impl Iterator<Item = char> + '_ {
|
pub fn chars_at(
|
||||||
|
&self,
|
||||||
|
mut point: DisplayPoint,
|
||||||
|
) -> impl Iterator<Item = (char, DisplayPoint)> + '_ {
|
||||||
|
point = DisplayPoint(self.blocks_snapshot.clip_point(point.0, Bias::Left));
|
||||||
|
self.text_chunks(point.row())
|
||||||
|
.flat_map(str::chars)
|
||||||
|
.skip_while({
|
||||||
let mut column = 0;
|
let mut column = 0;
|
||||||
let mut chars = self.text_chunks(point.row()).flat_map(str::chars);
|
move |char| {
|
||||||
while column < point.column() {
|
let at_point = column >= point.column();
|
||||||
if let Some(c) = chars.next() {
|
column += char.len_utf8() as u32;
|
||||||
column += c.len_utf8() as u32;
|
!at_point
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map(move |ch| {
|
||||||
|
let result = (ch, point);
|
||||||
|
if ch == '\n' {
|
||||||
|
*point.row_mut() += 1;
|
||||||
|
*point.column_mut() = 0;
|
||||||
} else {
|
} else {
|
||||||
break;
|
*point.column_mut() += ch.len_utf8() as u32;
|
||||||
}
|
}
|
||||||
|
result
|
||||||
|
})
|
||||||
}
|
}
|
||||||
chars
|
|
||||||
|
pub fn reverse_chars_at(
|
||||||
|
&self,
|
||||||
|
mut point: DisplayPoint,
|
||||||
|
) -> impl Iterator<Item = (char, DisplayPoint)> + '_ {
|
||||||
|
point = DisplayPoint(self.blocks_snapshot.clip_point(point.0, Bias::Left));
|
||||||
|
self.reverse_text_chunks(point.row())
|
||||||
|
.flat_map(|chunk| chunk.chars().rev())
|
||||||
|
.skip_while({
|
||||||
|
let mut column = self.line_len(point.row());
|
||||||
|
if self.max_point().row() > point.row() {
|
||||||
|
column += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
move |char| {
|
||||||
|
let at_point = column <= point.column();
|
||||||
|
column = column.saturating_sub(char.len_utf8() as u32);
|
||||||
|
!at_point
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map(move |ch| {
|
||||||
|
if ch == '\n' {
|
||||||
|
*point.row_mut() -= 1;
|
||||||
|
*point.column_mut() = self.line_len(point.row());
|
||||||
|
} else {
|
||||||
|
*point.column_mut() = point.column().saturating_sub(ch.len_utf8() as u32);
|
||||||
|
}
|
||||||
|
(ch, point)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn column_to_chars(&self, display_row: u32, target: u32) -> u32 {
|
pub fn column_to_chars(&self, display_row: u32, target: u32) -> u32 {
|
||||||
let mut count = 0;
|
let mut count = 0;
|
||||||
let mut column = 0;
|
let mut column = 0;
|
||||||
for c in self.chars_at(DisplayPoint::new(display_row, 0)) {
|
for (c, _) in self.chars_at(DisplayPoint::new(display_row, 0)) {
|
||||||
if column >= target {
|
if column >= target {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -370,7 +427,7 @@ impl DisplaySnapshot {
|
|||||||
pub fn column_from_chars(&self, display_row: u32, char_count: u32) -> u32 {
|
pub fn column_from_chars(&self, display_row: u32, char_count: u32) -> u32 {
|
||||||
let mut column = 0;
|
let mut column = 0;
|
||||||
|
|
||||||
for (count, c) in self.chars_at(DisplayPoint::new(display_row, 0)).enumerate() {
|
for (count, (c, _)) in self.chars_at(DisplayPoint::new(display_row, 0)).enumerate() {
|
||||||
if c == '\n' || count >= char_count as usize {
|
if c == '\n' || count >= char_count as usize {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -454,7 +511,7 @@ impl DisplaySnapshot {
|
|||||||
pub fn line_indent(&self, display_row: u32) -> (u32, bool) {
|
pub fn line_indent(&self, display_row: u32) -> (u32, bool) {
|
||||||
let mut indent = 0;
|
let mut indent = 0;
|
||||||
let mut is_blank = true;
|
let mut is_blank = true;
|
||||||
for c in self.chars_at(DisplayPoint::new(display_row, 0)) {
|
for (c, _) in self.chars_at(DisplayPoint::new(display_row, 0)) {
|
||||||
if c == ' ' {
|
if c == ' ' {
|
||||||
indent += 1;
|
indent += 1;
|
||||||
} else {
|
} else {
|
||||||
@ -565,7 +622,7 @@ pub mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::{movement, test::marked_display_snapshot};
|
use crate::{movement, test::marked_display_snapshot};
|
||||||
use gpui::{color::Color, elements::*, test::observe, MutableAppContext};
|
use gpui::{color::Color, elements::*, test::observe, MutableAppContext};
|
||||||
use language::{Buffer, Language, LanguageConfig, RandomCharIter, SelectionGoal};
|
use language::{Buffer, Language, LanguageConfig, SelectionGoal};
|
||||||
use rand::{prelude::*, Rng};
|
use rand::{prelude::*, Rng};
|
||||||
use smol::stream::StreamExt;
|
use smol::stream::StreamExt;
|
||||||
use std::{env, sync::Arc};
|
use std::{env, sync::Arc};
|
||||||
@ -609,7 +666,9 @@ pub mod tests {
|
|||||||
let buffer = cx.update(|cx| {
|
let buffer = cx.update(|cx| {
|
||||||
if rng.gen() {
|
if rng.gen() {
|
||||||
let len = rng.gen_range(0..10);
|
let len = rng.gen_range(0..10);
|
||||||
let text = RandomCharIter::new(&mut rng).take(len).collect::<String>();
|
let text = util::RandomCharIter::new(&mut rng)
|
||||||
|
.take(len)
|
||||||
|
.collect::<String>();
|
||||||
MultiBuffer::build_simple(&text, cx)
|
MultiBuffer::build_simple(&text, cx)
|
||||||
} else {
|
} else {
|
||||||
MultiBuffer::build_random(&mut rng, cx)
|
MultiBuffer::build_random(&mut rng, cx)
|
||||||
|
@ -5,7 +5,7 @@ use super::{
|
|||||||
use crate::{Anchor, ExcerptRange, ToPoint as _};
|
use crate::{Anchor, ExcerptRange, ToPoint as _};
|
||||||
use collections::{Bound, HashMap, HashSet};
|
use collections::{Bound, HashMap, HashSet};
|
||||||
use gpui::{ElementBox, RenderContext};
|
use gpui::{ElementBox, RenderContext};
|
||||||
use language::{BufferSnapshot, Chunk, Patch};
|
use language::{BufferSnapshot, Chunk, Patch, Point};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use std::{
|
use std::{
|
||||||
cell::RefCell,
|
cell::RefCell,
|
||||||
@ -18,7 +18,7 @@ use std::{
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
use sum_tree::{Bias, SumTree};
|
use sum_tree::{Bias, SumTree};
|
||||||
use text::{Edit, Point};
|
use text::Edit;
|
||||||
|
|
||||||
const NEWLINES: &[u8] = &[b'\n'; u8::MAX as usize];
|
const NEWLINES: &[u8] = &[b'\n'; u8::MAX as usize];
|
||||||
|
|
||||||
@ -42,7 +42,7 @@ pub struct BlockSnapshot {
|
|||||||
pub struct BlockId(usize);
|
pub struct BlockId(usize);
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
|
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
|
||||||
pub struct BlockPoint(pub super::Point);
|
pub struct BlockPoint(pub Point);
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
|
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
|
||||||
struct BlockRow(u32);
|
struct BlockRow(u32);
|
||||||
@ -157,6 +157,7 @@ pub struct BlockChunks<'a> {
|
|||||||
max_output_row: u32,
|
max_output_row: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct BlockBufferRows<'a> {
|
pub struct BlockBufferRows<'a> {
|
||||||
transforms: sum_tree::Cursor<'a, Transform, (BlockRow, WrapRow)>,
|
transforms: sum_tree::Cursor<'a, Transform, (BlockRow, WrapRow)>,
|
||||||
input_buffer_rows: wrap_map::WrapBufferRows<'a>,
|
input_buffer_rows: wrap_map::WrapBufferRows<'a>,
|
||||||
@ -994,7 +995,7 @@ mod tests {
|
|||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use std::env;
|
use std::env;
|
||||||
use text::RandomCharIter;
|
use util::RandomCharIter;
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
fn test_offset_for_row() {
|
fn test_offset_for_row() {
|
||||||
|
@ -18,11 +18,11 @@ use std::{
|
|||||||
use sum_tree::{Bias, Cursor, FilterCursor, SumTree};
|
use sum_tree::{Bias, Cursor, FilterCursor, SumTree};
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
|
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
|
||||||
pub struct FoldPoint(pub super::Point);
|
pub struct FoldPoint(pub Point);
|
||||||
|
|
||||||
impl FoldPoint {
|
impl FoldPoint {
|
||||||
pub fn new(row: u32, column: u32) -> Self {
|
pub fn new(row: u32, column: u32) -> Self {
|
||||||
Self(super::Point::new(row, column))
|
Self(Point::new(row, column))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn row(self) -> u32 {
|
pub fn row(self) -> u32 {
|
||||||
@ -274,6 +274,7 @@ impl FoldMap {
|
|||||||
if buffer.edit_count() != new_buffer.edit_count()
|
if buffer.edit_count() != new_buffer.edit_count()
|
||||||
|| buffer.parse_count() != new_buffer.parse_count()
|
|| buffer.parse_count() != new_buffer.parse_count()
|
||||||
|| buffer.diagnostics_update_count() != new_buffer.diagnostics_update_count()
|
|| buffer.diagnostics_update_count() != new_buffer.diagnostics_update_count()
|
||||||
|
|| buffer.git_diff_update_count() != new_buffer.git_diff_update_count()
|
||||||
|| buffer.trailing_excerpt_update_count()
|
|| buffer.trailing_excerpt_update_count()
|
||||||
!= new_buffer.trailing_excerpt_update_count()
|
!= new_buffer.trailing_excerpt_update_count()
|
||||||
{
|
{
|
||||||
@ -986,6 +987,7 @@ impl<'a> sum_tree::Dimension<'a, FoldSummary> for usize {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct FoldBufferRows<'a> {
|
pub struct FoldBufferRows<'a> {
|
||||||
cursor: Cursor<'a, Transform, (FoldPoint, Point)>,
|
cursor: Cursor<'a, Transform, (FoldPoint, Point)>,
|
||||||
input_buffer_rows: MultiBufferRows<'a>,
|
input_buffer_rows: MultiBufferRows<'a>,
|
||||||
@ -1195,8 +1197,8 @@ mod tests {
|
|||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use std::{cmp::Reverse, env, mem, sync::Arc};
|
use std::{cmp::Reverse, env, mem, sync::Arc};
|
||||||
use sum_tree::TreeMap;
|
use sum_tree::TreeMap;
|
||||||
use text::RandomCharIter;
|
|
||||||
use util::test::sample_text;
|
use util::test::sample_text;
|
||||||
|
use util::RandomCharIter;
|
||||||
use Bias::{Left, Right};
|
use Bias::{Left, Right};
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
|
@ -3,11 +3,10 @@ use super::{
|
|||||||
TextHighlights,
|
TextHighlights,
|
||||||
};
|
};
|
||||||
use crate::MultiBufferSnapshot;
|
use crate::MultiBufferSnapshot;
|
||||||
use language::{rope, Chunk};
|
use language::{Chunk, Point};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use std::{cmp, mem, num::NonZeroU32, ops::Range};
|
use std::{cmp, mem, num::NonZeroU32, ops::Range};
|
||||||
use sum_tree::Bias;
|
use sum_tree::Bias;
|
||||||
use text::Point;
|
|
||||||
|
|
||||||
pub struct TabMap(Mutex<TabSnapshot>);
|
pub struct TabMap(Mutex<TabSnapshot>);
|
||||||
|
|
||||||
@ -332,11 +331,11 @@ impl TabSnapshot {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
|
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
|
||||||
pub struct TabPoint(pub super::Point);
|
pub struct TabPoint(pub Point);
|
||||||
|
|
||||||
impl TabPoint {
|
impl TabPoint {
|
||||||
pub fn new(row: u32, column: u32) -> Self {
|
pub fn new(row: u32, column: u32) -> Self {
|
||||||
Self(super::Point::new(row, column))
|
Self(Point::new(row, column))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn zero() -> Self {
|
pub fn zero() -> Self {
|
||||||
@ -352,8 +351,8 @@ impl TabPoint {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<super::Point> for TabPoint {
|
impl From<Point> for TabPoint {
|
||||||
fn from(point: super::Point) -> Self {
|
fn from(point: Point) -> Self {
|
||||||
Self(point)
|
Self(point)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -362,7 +361,7 @@ pub type TabEdit = text::Edit<TabPoint>;
|
|||||||
|
|
||||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||||
pub struct TextSummary {
|
pub struct TextSummary {
|
||||||
pub lines: super::Point,
|
pub lines: Point,
|
||||||
pub first_line_chars: u32,
|
pub first_line_chars: u32,
|
||||||
pub last_line_chars: u32,
|
pub last_line_chars: u32,
|
||||||
pub longest_row: u32,
|
pub longest_row: u32,
|
||||||
@ -371,7 +370,7 @@ pub struct TextSummary {
|
|||||||
|
|
||||||
impl<'a> From<&'a str> for TextSummary {
|
impl<'a> From<&'a str> for TextSummary {
|
||||||
fn from(text: &'a str) -> Self {
|
fn from(text: &'a str) -> Self {
|
||||||
let sum = rope::TextSummary::from(text);
|
let sum = text::TextSummary::from(text);
|
||||||
|
|
||||||
TextSummary {
|
TextSummary {
|
||||||
lines: sum.lines,
|
lines: sum.lines,
|
||||||
@ -485,7 +484,6 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::{display_map::fold_map::FoldMap, MultiBuffer};
|
use crate::{display_map::fold_map::FoldMap, MultiBuffer};
|
||||||
use rand::{prelude::StdRng, Rng};
|
use rand::{prelude::StdRng, Rng};
|
||||||
use text::{RandomCharIter, Rope};
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_expand_tabs() {
|
fn test_expand_tabs() {
|
||||||
@ -508,7 +506,9 @@ mod tests {
|
|||||||
let tab_size = NonZeroU32::new(rng.gen_range(1..=4)).unwrap();
|
let tab_size = NonZeroU32::new(rng.gen_range(1..=4)).unwrap();
|
||||||
let len = rng.gen_range(0..30);
|
let len = rng.gen_range(0..30);
|
||||||
let buffer = if rng.gen() {
|
let buffer = if rng.gen() {
|
||||||
let text = RandomCharIter::new(&mut rng).take(len).collect::<String>();
|
let text = util::RandomCharIter::new(&mut rng)
|
||||||
|
.take(len)
|
||||||
|
.collect::<String>();
|
||||||
MultiBuffer::build_simple(&text, cx)
|
MultiBuffer::build_simple(&text, cx)
|
||||||
} else {
|
} else {
|
||||||
MultiBuffer::build_random(&mut rng, cx)
|
MultiBuffer::build_random(&mut rng, cx)
|
||||||
@ -522,7 +522,7 @@ mod tests {
|
|||||||
log::info!("FoldMap text: {:?}", folds_snapshot.text());
|
log::info!("FoldMap text: {:?}", folds_snapshot.text());
|
||||||
|
|
||||||
let (_, tabs_snapshot) = TabMap::new(folds_snapshot.clone(), tab_size);
|
let (_, tabs_snapshot) = TabMap::new(folds_snapshot.clone(), tab_size);
|
||||||
let text = Rope::from(tabs_snapshot.text().as_str());
|
let text = text::Rope::from(tabs_snapshot.text().as_str());
|
||||||
log::info!(
|
log::info!(
|
||||||
"TabMap text (tab size: {}): {:?}",
|
"TabMap text (tab size: {}): {:?}",
|
||||||
tab_size,
|
tab_size,
|
||||||
|
@ -3,12 +3,12 @@ use super::{
|
|||||||
tab_map::{self, TabEdit, TabPoint, TabSnapshot},
|
tab_map::{self, TabEdit, TabPoint, TabSnapshot},
|
||||||
TextHighlights,
|
TextHighlights,
|
||||||
};
|
};
|
||||||
use crate::{MultiBufferSnapshot, Point};
|
use crate::MultiBufferSnapshot;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
fonts::FontId, text_layout::LineWrapper, Entity, ModelContext, ModelHandle, MutableAppContext,
|
fonts::FontId, text_layout::LineWrapper, Entity, ModelContext, ModelHandle, MutableAppContext,
|
||||||
Task,
|
Task,
|
||||||
};
|
};
|
||||||
use language::Chunk;
|
use language::{Chunk, Point};
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use smol::future::yield_now;
|
use smol::future::yield_now;
|
||||||
use std::{cmp, collections::VecDeque, mem, ops::Range, time::Duration};
|
use std::{cmp, collections::VecDeque, mem, ops::Range, time::Duration};
|
||||||
@ -52,7 +52,7 @@ struct TransformSummary {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
|
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
|
||||||
pub struct WrapPoint(pub super::Point);
|
pub struct WrapPoint(pub Point);
|
||||||
|
|
||||||
pub struct WrapChunks<'a> {
|
pub struct WrapChunks<'a> {
|
||||||
input_chunks: tab_map::TabChunks<'a>,
|
input_chunks: tab_map::TabChunks<'a>,
|
||||||
@ -62,6 +62,7 @@ pub struct WrapChunks<'a> {
|
|||||||
transforms: Cursor<'a, Transform, (WrapPoint, TabPoint)>,
|
transforms: Cursor<'a, Transform, (WrapPoint, TabPoint)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct WrapBufferRows<'a> {
|
pub struct WrapBufferRows<'a> {
|
||||||
input_buffer_rows: fold_map::FoldBufferRows<'a>,
|
input_buffer_rows: fold_map::FoldBufferRows<'a>,
|
||||||
input_buffer_row: Option<u32>,
|
input_buffer_row: Option<u32>,
|
||||||
@ -959,7 +960,7 @@ impl SumTreeExt for SumTree<Transform> {
|
|||||||
|
|
||||||
impl WrapPoint {
|
impl WrapPoint {
|
||||||
pub fn new(row: u32, column: u32) -> Self {
|
pub fn new(row: u32, column: u32) -> Self {
|
||||||
Self(super::Point::new(row, column))
|
Self(Point::new(row, column))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn row(self) -> u32 {
|
pub fn row(self) -> u32 {
|
||||||
@ -1029,7 +1030,6 @@ mod tests {
|
|||||||
MultiBuffer,
|
MultiBuffer,
|
||||||
};
|
};
|
||||||
use gpui::test::observe;
|
use gpui::test::observe;
|
||||||
use language::RandomCharIter;
|
|
||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use smol::stream::StreamExt;
|
use smol::stream::StreamExt;
|
||||||
@ -1067,7 +1067,9 @@ mod tests {
|
|||||||
MultiBuffer::build_random(&mut rng, cx)
|
MultiBuffer::build_random(&mut rng, cx)
|
||||||
} else {
|
} else {
|
||||||
let len = rng.gen_range(0..10);
|
let len = rng.gen_range(0..10);
|
||||||
let text = RandomCharIter::new(&mut rng).take(len).collect::<String>();
|
let text = util::RandomCharIter::new(&mut rng)
|
||||||
|
.take(len)
|
||||||
|
.collect::<String>();
|
||||||
MultiBuffer::build_simple(&text, cx)
|
MultiBuffer::build_simple(&text, cx)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
File diff suppressed because it is too large
Load Diff
5081
crates/editor/src/editor_tests.rs
Normal file
5081
crates/editor/src/editor_tests.rs
Normal file
File diff suppressed because it is too large
Load Diff
@ -12,10 +12,11 @@ use crate::{
|
|||||||
CmdShiftChanged, GoToFetchedDefinition, GoToFetchedTypeDefinition, UpdateGoToDefinitionLink,
|
CmdShiftChanged, GoToFetchedDefinition, GoToFetchedTypeDefinition, UpdateGoToDefinitionLink,
|
||||||
},
|
},
|
||||||
mouse_context_menu::DeployMouseContextMenu,
|
mouse_context_menu::DeployMouseContextMenu,
|
||||||
EditorStyle,
|
AnchorRangeExt, EditorStyle,
|
||||||
};
|
};
|
||||||
use clock::ReplicaId;
|
use clock::ReplicaId;
|
||||||
use collections::{BTreeMap, HashMap};
|
use collections::{BTreeMap, HashMap};
|
||||||
|
use git::diff::DiffHunkStatus;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
color::Color,
|
color::Color,
|
||||||
elements::*,
|
elements::*,
|
||||||
@ -34,18 +35,25 @@ use gpui::{
|
|||||||
WeakViewHandle,
|
WeakViewHandle,
|
||||||
};
|
};
|
||||||
use json::json;
|
use json::json;
|
||||||
use language::{Bias, DiagnosticSeverity, OffsetUtf16, Selection};
|
use language::{Bias, DiagnosticSeverity, OffsetUtf16, Point, Selection};
|
||||||
use project::ProjectPath;
|
use project::ProjectPath;
|
||||||
use settings::Settings;
|
use settings::{GitGutter, Settings};
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
use std::{
|
use std::{
|
||||||
cmp::{self, Ordering},
|
cmp::{self, Ordering},
|
||||||
fmt::Write,
|
fmt::Write,
|
||||||
iter,
|
iter,
|
||||||
ops::Range,
|
ops::{DerefMut, Range},
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct DiffHunkLayout {
|
||||||
|
visual_range: Range<u32>,
|
||||||
|
status: DiffHunkStatus,
|
||||||
|
is_folded: bool,
|
||||||
|
}
|
||||||
|
|
||||||
struct SelectionLayout {
|
struct SelectionLayout {
|
||||||
head: DisplayPoint,
|
head: DisplayPoint,
|
||||||
range: Range<DisplayPoint>,
|
range: Range<DisplayPoint>,
|
||||||
@ -452,7 +460,6 @@ impl EditorElement {
|
|||||||
let bounds = gutter_bounds.union_rect(text_bounds);
|
let bounds = gutter_bounds.union_rect(text_bounds);
|
||||||
let scroll_top =
|
let scroll_top =
|
||||||
layout.position_map.snapshot.scroll_position().y() * layout.position_map.line_height;
|
layout.position_map.snapshot.scroll_position().y() * layout.position_map.line_height;
|
||||||
let editor = self.view(cx.app);
|
|
||||||
cx.scene.push_quad(Quad {
|
cx.scene.push_quad(Quad {
|
||||||
bounds: gutter_bounds,
|
bounds: gutter_bounds,
|
||||||
background: Some(self.style.gutter_background),
|
background: Some(self.style.gutter_background),
|
||||||
@ -466,7 +473,7 @@ impl EditorElement {
|
|||||||
corner_radius: 0.,
|
corner_radius: 0.,
|
||||||
});
|
});
|
||||||
|
|
||||||
if let EditorMode::Full = editor.mode {
|
if let EditorMode::Full = layout.mode {
|
||||||
let mut active_rows = layout.active_rows.iter().peekable();
|
let mut active_rows = layout.active_rows.iter().peekable();
|
||||||
while let Some((start_row, contains_non_empty_selection)) = active_rows.next() {
|
while let Some((start_row, contains_non_empty_selection)) = active_rows.next() {
|
||||||
let mut end_row = *start_row;
|
let mut end_row = *start_row;
|
||||||
@ -524,34 +531,120 @@ impl EditorElement {
|
|||||||
layout: &mut LayoutState,
|
layout: &mut LayoutState,
|
||||||
cx: &mut PaintContext,
|
cx: &mut PaintContext,
|
||||||
) {
|
) {
|
||||||
let scroll_top =
|
let line_height = layout.position_map.line_height;
|
||||||
layout.position_map.snapshot.scroll_position().y() * layout.position_map.line_height;
|
|
||||||
|
let scroll_position = layout.position_map.snapshot.scroll_position();
|
||||||
|
let scroll_top = scroll_position.y() * line_height;
|
||||||
|
|
||||||
|
let show_gutter = matches!(
|
||||||
|
&cx.global::<Settings>()
|
||||||
|
.git_overrides
|
||||||
|
.git_gutter
|
||||||
|
.unwrap_or_default(),
|
||||||
|
GitGutter::TrackedFiles
|
||||||
|
);
|
||||||
|
|
||||||
|
if show_gutter {
|
||||||
|
Self::paint_diff_hunks(bounds, layout, cx);
|
||||||
|
}
|
||||||
|
|
||||||
for (ix, line) in layout.line_number_layouts.iter().enumerate() {
|
for (ix, line) in layout.line_number_layouts.iter().enumerate() {
|
||||||
if let Some(line) = line {
|
if let Some(line) = line {
|
||||||
let line_origin = bounds.origin()
|
let line_origin = bounds.origin()
|
||||||
+ vec2f(
|
+ vec2f(
|
||||||
bounds.width() - line.width() - layout.gutter_padding,
|
bounds.width() - line.width() - layout.gutter_padding,
|
||||||
ix as f32 * layout.position_map.line_height
|
ix as f32 * line_height - (scroll_top % line_height),
|
||||||
- (scroll_top % layout.position_map.line_height),
|
|
||||||
);
|
|
||||||
line.paint(
|
|
||||||
line_origin,
|
|
||||||
visible_bounds,
|
|
||||||
layout.position_map.line_height,
|
|
||||||
cx,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
line.paint(line_origin, visible_bounds, line_height, cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some((row, indicator)) = layout.code_actions_indicator.as_mut() {
|
if let Some((row, indicator)) = layout.code_actions_indicator.as_mut() {
|
||||||
let mut x = bounds.width() - layout.gutter_padding;
|
let mut x = bounds.width() - layout.gutter_padding;
|
||||||
let mut y = *row as f32 * layout.position_map.line_height - scroll_top;
|
let mut y = *row as f32 * line_height - scroll_top;
|
||||||
x += ((layout.gutter_padding + layout.gutter_margin) - indicator.size().x()) / 2.;
|
x += ((layout.gutter_padding + layout.gutter_margin) - indicator.size().x()) / 2.;
|
||||||
y += (layout.position_map.line_height - indicator.size().y()) / 2.;
|
y += (line_height - indicator.size().y()) / 2.;
|
||||||
indicator.paint(bounds.origin() + vec2f(x, y), visible_bounds, cx);
|
indicator.paint(bounds.origin() + vec2f(x, y), visible_bounds, cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn paint_diff_hunks(bounds: RectF, layout: &mut LayoutState, cx: &mut PaintContext) {
|
||||||
|
let diff_style = &cx.global::<Settings>().theme.editor.diff.clone();
|
||||||
|
let line_height = layout.position_map.line_height;
|
||||||
|
|
||||||
|
let scroll_position = layout.position_map.snapshot.scroll_position();
|
||||||
|
let scroll_top = scroll_position.y() * line_height;
|
||||||
|
|
||||||
|
for hunk in &layout.hunk_layouts {
|
||||||
|
let color = match (hunk.status, hunk.is_folded) {
|
||||||
|
(DiffHunkStatus::Added, false) => diff_style.inserted,
|
||||||
|
(DiffHunkStatus::Modified, false) => diff_style.modified,
|
||||||
|
|
||||||
|
//TODO: This rendering is entirely a horrible hack
|
||||||
|
(DiffHunkStatus::Removed, false) => {
|
||||||
|
let row = hunk.visual_range.start;
|
||||||
|
|
||||||
|
let offset = line_height / 2.;
|
||||||
|
let start_y = row as f32 * line_height - offset - scroll_top;
|
||||||
|
let end_y = start_y + line_height;
|
||||||
|
|
||||||
|
let width = diff_style.removed_width_em * line_height;
|
||||||
|
let highlight_origin = bounds.origin() + vec2f(-width, start_y);
|
||||||
|
let highlight_size = vec2f(width * 2., end_y - start_y);
|
||||||
|
let highlight_bounds = RectF::new(highlight_origin, highlight_size);
|
||||||
|
|
||||||
|
cx.scene.push_quad(Quad {
|
||||||
|
bounds: highlight_bounds,
|
||||||
|
background: Some(diff_style.deleted),
|
||||||
|
border: Border::new(0., Color::transparent_black()),
|
||||||
|
corner_radius: 1. * line_height,
|
||||||
|
});
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
(_, true) => {
|
||||||
|
let row = hunk.visual_range.start;
|
||||||
|
let start_y = row as f32 * line_height - scroll_top;
|
||||||
|
let end_y = start_y + line_height;
|
||||||
|
|
||||||
|
let width = diff_style.removed_width_em * line_height;
|
||||||
|
let highlight_origin = bounds.origin() + vec2f(-width, start_y);
|
||||||
|
let highlight_size = vec2f(width * 2., end_y - start_y);
|
||||||
|
let highlight_bounds = RectF::new(highlight_origin, highlight_size);
|
||||||
|
|
||||||
|
cx.scene.push_quad(Quad {
|
||||||
|
bounds: highlight_bounds,
|
||||||
|
background: Some(diff_style.modified),
|
||||||
|
border: Border::new(0., Color::transparent_black()),
|
||||||
|
corner_radius: 1. * line_height,
|
||||||
|
});
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let start_row = hunk.visual_range.start;
|
||||||
|
let end_row = hunk.visual_range.end;
|
||||||
|
|
||||||
|
let start_y = start_row as f32 * line_height - scroll_top;
|
||||||
|
let end_y = end_row as f32 * line_height - scroll_top;
|
||||||
|
|
||||||
|
let width = diff_style.width_em * line_height;
|
||||||
|
let highlight_origin = bounds.origin() + vec2f(-width, start_y);
|
||||||
|
let highlight_size = vec2f(width * 2., end_y - start_y);
|
||||||
|
let highlight_bounds = RectF::new(highlight_origin, highlight_size);
|
||||||
|
|
||||||
|
cx.scene.push_quad(Quad {
|
||||||
|
bounds: highlight_bounds,
|
||||||
|
background: Some(color),
|
||||||
|
border: Border::new(0., Color::transparent_black()),
|
||||||
|
corner_radius: diff_style.corner_radius * line_height,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn paint_text(
|
fn paint_text(
|
||||||
&mut self,
|
&mut self,
|
||||||
bounds: RectF,
|
bounds: RectF,
|
||||||
@ -563,10 +656,8 @@ impl EditorElement {
|
|||||||
let style = &self.style;
|
let style = &self.style;
|
||||||
let local_replica_id = view.replica_id(cx);
|
let local_replica_id = view.replica_id(cx);
|
||||||
let scroll_position = layout.position_map.snapshot.scroll_position();
|
let scroll_position = layout.position_map.snapshot.scroll_position();
|
||||||
let start_row = scroll_position.y() as u32;
|
let start_row = layout.visible_display_row_range.start;
|
||||||
let scroll_top = scroll_position.y() * layout.position_map.line_height;
|
let scroll_top = scroll_position.y() * layout.position_map.line_height;
|
||||||
let end_row =
|
|
||||||
((scroll_top + bounds.height()) / layout.position_map.line_height).ceil() as u32 + 1; // Add 1 to ensure selections bleed off screen
|
|
||||||
let max_glyph_width = layout.position_map.em_width;
|
let max_glyph_width = layout.position_map.em_width;
|
||||||
let scroll_left = scroll_position.x() * max_glyph_width;
|
let scroll_left = scroll_position.x() * max_glyph_width;
|
||||||
let content_origin = bounds.origin() + vec2f(layout.gutter_margin, 0.);
|
let content_origin = bounds.origin() + vec2f(layout.gutter_margin, 0.);
|
||||||
@ -585,8 +676,6 @@ impl EditorElement {
|
|||||||
for (range, color) in &layout.highlighted_ranges {
|
for (range, color) in &layout.highlighted_ranges {
|
||||||
self.paint_highlighted_range(
|
self.paint_highlighted_range(
|
||||||
range.clone(),
|
range.clone(),
|
||||||
start_row,
|
|
||||||
end_row,
|
|
||||||
*color,
|
*color,
|
||||||
0.,
|
0.,
|
||||||
0.15 * layout.position_map.line_height,
|
0.15 * layout.position_map.line_height,
|
||||||
@ -607,8 +696,6 @@ impl EditorElement {
|
|||||||
for selection in selections {
|
for selection in selections {
|
||||||
self.paint_highlighted_range(
|
self.paint_highlighted_range(
|
||||||
selection.range.clone(),
|
selection.range.clone(),
|
||||||
start_row,
|
|
||||||
end_row,
|
|
||||||
selection_style.selection,
|
selection_style.selection,
|
||||||
corner_radius,
|
corner_radius,
|
||||||
corner_radius * 2.,
|
corner_radius * 2.,
|
||||||
@ -622,7 +709,10 @@ impl EditorElement {
|
|||||||
|
|
||||||
if view.show_local_cursors() || *replica_id != local_replica_id {
|
if view.show_local_cursors() || *replica_id != local_replica_id {
|
||||||
let cursor_position = selection.head;
|
let cursor_position = selection.head;
|
||||||
if (start_row..end_row).contains(&cursor_position.row()) {
|
if layout
|
||||||
|
.visible_display_row_range
|
||||||
|
.contains(&cursor_position.row())
|
||||||
|
{
|
||||||
let cursor_row_layout = &layout.position_map.line_layouts
|
let cursor_row_layout = &layout.position_map.line_layouts
|
||||||
[(cursor_position.row() - start_row) as usize];
|
[(cursor_position.row() - start_row) as usize];
|
||||||
let cursor_column = cursor_position.column() as usize;
|
let cursor_column = cursor_position.column() as usize;
|
||||||
@ -639,7 +729,7 @@ impl EditorElement {
|
|||||||
.snapshot
|
.snapshot
|
||||||
.chars_at(cursor_position)
|
.chars_at(cursor_position)
|
||||||
.next()
|
.next()
|
||||||
.and_then(|character| {
|
.and_then(|(character, _)| {
|
||||||
let font_id =
|
let font_id =
|
||||||
cursor_row_layout.font_for_index(cursor_column)?;
|
cursor_row_layout.font_for_index(cursor_column)?;
|
||||||
let text = character.to_string();
|
let text = character.to_string();
|
||||||
@ -796,12 +886,123 @@ impl EditorElement {
|
|||||||
cx.scene.pop_layer();
|
cx.scene.pop_layer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn paint_scrollbar(&mut self, bounds: RectF, layout: &mut LayoutState, cx: &mut PaintContext) {
|
||||||
|
enum ScrollbarMouseHandlers {}
|
||||||
|
if layout.mode != EditorMode::Full {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let view = self.view.clone();
|
||||||
|
let style = &self.style.theme.scrollbar;
|
||||||
|
|
||||||
|
let top = bounds.min_y();
|
||||||
|
let bottom = bounds.max_y();
|
||||||
|
let right = bounds.max_x();
|
||||||
|
let left = right - style.width;
|
||||||
|
let row_range = &layout.scrollbar_row_range;
|
||||||
|
let max_row = layout.max_row as f32 + (row_range.end - row_range.start);
|
||||||
|
|
||||||
|
let mut height = bounds.height();
|
||||||
|
let mut first_row_y_offset = 0.0;
|
||||||
|
|
||||||
|
// Impose a minimum height on the scrollbar thumb
|
||||||
|
let min_thumb_height =
|
||||||
|
style.min_height_factor * cx.font_cache.line_height(self.style.text.font_size);
|
||||||
|
let thumb_height = (row_range.end - row_range.start) * height / max_row;
|
||||||
|
if thumb_height < min_thumb_height {
|
||||||
|
first_row_y_offset = (min_thumb_height - thumb_height) / 2.0;
|
||||||
|
height -= min_thumb_height - thumb_height;
|
||||||
|
}
|
||||||
|
|
||||||
|
let y_for_row = |row: f32| -> f32 { top + first_row_y_offset + row * height / max_row };
|
||||||
|
|
||||||
|
let thumb_top = y_for_row(row_range.start) - first_row_y_offset;
|
||||||
|
let thumb_bottom = y_for_row(row_range.end) + first_row_y_offset;
|
||||||
|
let track_bounds = RectF::from_points(vec2f(left, top), vec2f(right, bottom));
|
||||||
|
let thumb_bounds = RectF::from_points(vec2f(left, thumb_top), vec2f(right, thumb_bottom));
|
||||||
|
|
||||||
|
if layout.show_scrollbars {
|
||||||
|
cx.scene.push_quad(Quad {
|
||||||
|
bounds: track_bounds,
|
||||||
|
border: style.track.border,
|
||||||
|
background: style.track.background_color,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
cx.scene.push_quad(Quad {
|
||||||
|
bounds: thumb_bounds,
|
||||||
|
border: style.thumb.border,
|
||||||
|
background: style.thumb.background_color,
|
||||||
|
corner_radius: style.thumb.corner_radius,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
cx.scene.push_cursor_region(CursorRegion {
|
||||||
|
bounds: track_bounds,
|
||||||
|
style: CursorStyle::Arrow,
|
||||||
|
});
|
||||||
|
cx.scene.push_mouse_region(
|
||||||
|
MouseRegion::new::<ScrollbarMouseHandlers>(view.id(), view.id(), track_bounds)
|
||||||
|
.on_move({
|
||||||
|
let view = view.clone();
|
||||||
|
move |_, cx| {
|
||||||
|
if let Some(view) = view.upgrade(cx.deref_mut()) {
|
||||||
|
view.update(cx.deref_mut(), |view, cx| {
|
||||||
|
view.make_scrollbar_visible(cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on_down(MouseButton::Left, {
|
||||||
|
let view = view.clone();
|
||||||
|
let row_range = row_range.clone();
|
||||||
|
move |e, cx| {
|
||||||
|
let y = e.position.y();
|
||||||
|
if let Some(view) = view.upgrade(cx.deref_mut()) {
|
||||||
|
view.update(cx.deref_mut(), |view, cx| {
|
||||||
|
if y < thumb_top || thumb_bottom < y {
|
||||||
|
let center_row =
|
||||||
|
((y - top) * max_row as f32 / height).round() as u32;
|
||||||
|
let top_row = center_row.saturating_sub(
|
||||||
|
(row_range.end - row_range.start) as u32 / 2,
|
||||||
|
);
|
||||||
|
let mut position = view.scroll_position(cx);
|
||||||
|
position.set_y(top_row as f32);
|
||||||
|
view.set_scroll_position(position, cx);
|
||||||
|
} else {
|
||||||
|
view.make_scrollbar_visible(cx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on_drag(MouseButton::Left, {
|
||||||
|
let view = view.clone();
|
||||||
|
move |e, cx| {
|
||||||
|
let y = e.prev_mouse_position.y();
|
||||||
|
let new_y = e.position.y();
|
||||||
|
if thumb_top < y && y < thumb_bottom {
|
||||||
|
if let Some(view) = view.upgrade(cx.deref_mut()) {
|
||||||
|
view.update(cx.deref_mut(), |view, cx| {
|
||||||
|
let mut position = view.scroll_position(cx);
|
||||||
|
position.set_y(
|
||||||
|
position.y() + (new_y - y) * (max_row as f32) / height,
|
||||||
|
);
|
||||||
|
if position.y() < 0.0 {
|
||||||
|
position.set_y(0.);
|
||||||
|
}
|
||||||
|
view.set_scroll_position(position, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn paint_highlighted_range(
|
fn paint_highlighted_range(
|
||||||
&self,
|
&self,
|
||||||
range: Range<DisplayPoint>,
|
range: Range<DisplayPoint>,
|
||||||
start_row: u32,
|
|
||||||
end_row: u32,
|
|
||||||
color: Color,
|
color: Color,
|
||||||
corner_radius: f32,
|
corner_radius: f32,
|
||||||
line_end_overshoot: f32,
|
line_end_overshoot: f32,
|
||||||
@ -812,6 +1013,8 @@ impl EditorElement {
|
|||||||
bounds: RectF,
|
bounds: RectF,
|
||||||
cx: &mut PaintContext,
|
cx: &mut PaintContext,
|
||||||
) {
|
) {
|
||||||
|
let start_row = layout.visible_display_row_range.start;
|
||||||
|
let end_row = layout.visible_display_row_range.end;
|
||||||
if range.start != range.end {
|
if range.start != range.end {
|
||||||
let row_range = if range.end.column() == 0 {
|
let row_range = if range.end.column() == 0 {
|
||||||
cmp::max(range.start.row(), start_row)..cmp::min(range.end.row(), end_row)
|
cmp::max(range.start.row(), start_row)..cmp::min(range.end.row(), end_row)
|
||||||
@ -900,6 +1103,75 @@ impl EditorElement {
|
|||||||
.width()
|
.width()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Folds contained in a hunk are ignored apart from shrinking visual size
|
||||||
|
//If a fold contains any hunks then that fold line is marked as modified
|
||||||
|
fn layout_git_gutters(
|
||||||
|
&self,
|
||||||
|
rows: Range<u32>,
|
||||||
|
snapshot: &EditorSnapshot,
|
||||||
|
) -> Vec<DiffHunkLayout> {
|
||||||
|
let buffer_snapshot = &snapshot.buffer_snapshot;
|
||||||
|
let visual_start = DisplayPoint::new(rows.start, 0).to_point(snapshot).row;
|
||||||
|
let visual_end = DisplayPoint::new(rows.end, 0).to_point(snapshot).row;
|
||||||
|
let hunks = buffer_snapshot.git_diff_hunks_in_range(visual_start..visual_end);
|
||||||
|
|
||||||
|
let mut layouts = Vec::<DiffHunkLayout>::new();
|
||||||
|
|
||||||
|
for hunk in hunks {
|
||||||
|
let hunk_start_point = Point::new(hunk.buffer_range.start, 0);
|
||||||
|
let hunk_end_point = Point::new(hunk.buffer_range.end, 0);
|
||||||
|
let hunk_start_point_sub = Point::new(hunk.buffer_range.start.saturating_sub(1), 0);
|
||||||
|
let hunk_end_point_sub = Point::new(
|
||||||
|
hunk.buffer_range
|
||||||
|
.end
|
||||||
|
.saturating_sub(1)
|
||||||
|
.max(hunk.buffer_range.start),
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
|
||||||
|
let is_removal = hunk.status() == DiffHunkStatus::Removed;
|
||||||
|
|
||||||
|
let folds_start = Point::new(hunk.buffer_range.start.saturating_sub(1), 0);
|
||||||
|
let folds_end = Point::new(hunk.buffer_range.end + 1, 0);
|
||||||
|
let folds_range = folds_start..folds_end;
|
||||||
|
|
||||||
|
let containing_fold = snapshot.folds_in_range(folds_range).find(|fold_range| {
|
||||||
|
let fold_point_range = fold_range.to_point(buffer_snapshot);
|
||||||
|
let fold_point_range = fold_point_range.start..=fold_point_range.end;
|
||||||
|
|
||||||
|
let folded_start = fold_point_range.contains(&hunk_start_point);
|
||||||
|
let folded_end = fold_point_range.contains(&hunk_end_point_sub);
|
||||||
|
let folded_start_sub = fold_point_range.contains(&hunk_start_point_sub);
|
||||||
|
|
||||||
|
(folded_start && folded_end) || (is_removal && folded_start_sub)
|
||||||
|
});
|
||||||
|
|
||||||
|
let visual_range = if let Some(fold) = containing_fold {
|
||||||
|
let row = fold.start.to_display_point(snapshot).row();
|
||||||
|
row..row
|
||||||
|
} else {
|
||||||
|
let start = hunk_start_point.to_display_point(snapshot).row();
|
||||||
|
let end = hunk_end_point.to_display_point(snapshot).row();
|
||||||
|
start..end
|
||||||
|
};
|
||||||
|
|
||||||
|
let has_existing_layout = match layouts.last() {
|
||||||
|
Some(e) => visual_range == e.visual_range && e.status == hunk.status(),
|
||||||
|
None => false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if !has_existing_layout {
|
||||||
|
layouts.push(DiffHunkLayout {
|
||||||
|
visual_range,
|
||||||
|
status: hunk.status(),
|
||||||
|
is_folded: containing_fold.is_some(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
layouts
|
||||||
|
}
|
||||||
|
|
||||||
fn layout_line_numbers(
|
fn layout_line_numbers(
|
||||||
&self,
|
&self,
|
||||||
rows: Range<u32>,
|
rows: Range<u32>,
|
||||||
@ -1288,6 +1560,8 @@ impl Element for EditorElement {
|
|||||||
let em_advance = style.text.em_advance(cx.font_cache);
|
let em_advance = style.text.em_advance(cx.font_cache);
|
||||||
let overscroll = vec2f(em_width, 0.);
|
let overscroll = vec2f(em_width, 0.);
|
||||||
let snapshot = self.update_view(cx.app, |view, cx| {
|
let snapshot = self.update_view(cx.app, |view, cx| {
|
||||||
|
view.set_visible_line_count(size.y() / line_height);
|
||||||
|
|
||||||
let wrap_width = match view.soft_wrap_mode(cx) {
|
let wrap_width = match view.soft_wrap_mode(cx) {
|
||||||
SoftWrap::None => Some((MAX_LINE_LEN / 2) as f32 * em_advance),
|
SoftWrap::None => Some((MAX_LINE_LEN / 2) as f32 * em_advance),
|
||||||
SoftWrap::EditorWidth => {
|
SoftWrap::EditorWidth => {
|
||||||
@ -1333,12 +1607,13 @@ impl Element for EditorElement {
|
|||||||
// The scroll position is a fractional point, the whole number of which represents
|
// The scroll position is a fractional point, the whole number of which represents
|
||||||
// the top of the window in terms of display rows.
|
// the top of the window in terms of display rows.
|
||||||
let start_row = scroll_position.y() as u32;
|
let start_row = scroll_position.y() as u32;
|
||||||
let scroll_top = scroll_position.y() * line_height;
|
let height_in_lines = size.y() / line_height;
|
||||||
|
let max_row = snapshot.max_point().row();
|
||||||
|
|
||||||
// Add 1 to ensure selections bleed off screen
|
// Add 1 to ensure selections bleed off screen
|
||||||
let end_row = 1 + cmp::min(
|
let end_row = 1 + cmp::min(
|
||||||
((scroll_top + size.y()) / line_height).ceil() as u32,
|
(scroll_position.y() + height_in_lines).ceil() as u32,
|
||||||
snapshot.max_point().row(),
|
max_row,
|
||||||
);
|
);
|
||||||
|
|
||||||
let start_anchor = if start_row == 0 {
|
let start_anchor = if start_row == 0 {
|
||||||
@ -1348,7 +1623,7 @@ impl Element for EditorElement {
|
|||||||
.buffer_snapshot
|
.buffer_snapshot
|
||||||
.anchor_before(DisplayPoint::new(start_row, 0).to_offset(&snapshot, Bias::Left))
|
.anchor_before(DisplayPoint::new(start_row, 0).to_offset(&snapshot, Bias::Left))
|
||||||
};
|
};
|
||||||
let end_anchor = if end_row > snapshot.max_point().row() {
|
let end_anchor = if end_row > max_row {
|
||||||
Anchor::max()
|
Anchor::max()
|
||||||
} else {
|
} else {
|
||||||
snapshot
|
snapshot
|
||||||
@ -1360,6 +1635,7 @@ impl Element for EditorElement {
|
|||||||
let mut active_rows = BTreeMap::new();
|
let mut active_rows = BTreeMap::new();
|
||||||
let mut highlighted_rows = None;
|
let mut highlighted_rows = None;
|
||||||
let mut highlighted_ranges = Vec::new();
|
let mut highlighted_ranges = Vec::new();
|
||||||
|
let mut show_scrollbars = false;
|
||||||
self.update_view(cx.app, |view, cx| {
|
self.update_view(cx.app, |view, cx| {
|
||||||
let display_map = view.display_map.update(cx, |map, cx| map.snapshot(cx));
|
let display_map = view.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||||
|
|
||||||
@ -1420,11 +1696,17 @@ impl Element for EditorElement {
|
|||||||
.collect(),
|
.collect(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
show_scrollbars = view.show_scrollbars();
|
||||||
});
|
});
|
||||||
|
|
||||||
let line_number_layouts =
|
let line_number_layouts =
|
||||||
self.layout_line_numbers(start_row..end_row, &active_rows, &snapshot, cx);
|
self.layout_line_numbers(start_row..end_row, &active_rows, &snapshot, cx);
|
||||||
|
|
||||||
|
let hunk_layouts = self.layout_git_gutters(start_row..end_row, &snapshot);
|
||||||
|
|
||||||
|
let scrollbar_row_range = scroll_position.y()..(scroll_position.y() + height_in_lines);
|
||||||
|
|
||||||
let mut max_visible_line_width = 0.0;
|
let mut max_visible_line_width = 0.0;
|
||||||
let line_layouts = self.layout_lines(start_row..end_row, &snapshot, cx);
|
let line_layouts = self.layout_lines(start_row..end_row, &snapshot, cx);
|
||||||
for line in &line_layouts {
|
for line in &line_layouts {
|
||||||
@ -1458,10 +1740,9 @@ impl Element for EditorElement {
|
|||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
|
|
||||||
let max_row = snapshot.max_point().row();
|
|
||||||
let scroll_max = vec2f(
|
let scroll_max = vec2f(
|
||||||
((scroll_width - text_size.x()) / em_width).max(0.0),
|
((scroll_width - text_size.x()) / em_width).max(0.0),
|
||||||
max_row.saturating_sub(1) as f32,
|
max_row as f32,
|
||||||
);
|
);
|
||||||
|
|
||||||
self.update_view(cx.app, |view, cx| {
|
self.update_view(cx.app, |view, cx| {
|
||||||
@ -1488,6 +1769,7 @@ impl Element for EditorElement {
|
|||||||
let mut context_menu = None;
|
let mut context_menu = None;
|
||||||
let mut code_actions_indicator = None;
|
let mut code_actions_indicator = None;
|
||||||
let mut hover = None;
|
let mut hover = None;
|
||||||
|
let mut mode = EditorMode::Full;
|
||||||
cx.render(&self.view.upgrade(cx).unwrap(), |view, cx| {
|
cx.render(&self.view.upgrade(cx).unwrap(), |view, cx| {
|
||||||
let newest_selection_head = view
|
let newest_selection_head = view
|
||||||
.selections
|
.selections
|
||||||
@ -1509,6 +1791,7 @@ impl Element for EditorElement {
|
|||||||
|
|
||||||
let visible_rows = start_row..start_row + line_layouts.len() as u32;
|
let visible_rows = start_row..start_row + line_layouts.len() as u32;
|
||||||
hover = view.hover_state.render(&snapshot, &style, visible_rows, cx);
|
hover = view.hover_state.render(&snapshot, &style, visible_rows, cx);
|
||||||
|
mode = view.mode;
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some((_, context_menu)) = context_menu.as_mut() {
|
if let Some((_, context_menu)) = context_menu.as_mut() {
|
||||||
@ -1556,6 +1839,7 @@ impl Element for EditorElement {
|
|||||||
(
|
(
|
||||||
size,
|
size,
|
||||||
LayoutState {
|
LayoutState {
|
||||||
|
mode,
|
||||||
position_map: Arc::new(PositionMap {
|
position_map: Arc::new(PositionMap {
|
||||||
size,
|
size,
|
||||||
scroll_max,
|
scroll_max,
|
||||||
@ -1565,14 +1849,19 @@ impl Element for EditorElement {
|
|||||||
em_advance,
|
em_advance,
|
||||||
snapshot,
|
snapshot,
|
||||||
}),
|
}),
|
||||||
|
visible_display_row_range: start_row..end_row,
|
||||||
gutter_size,
|
gutter_size,
|
||||||
gutter_padding,
|
gutter_padding,
|
||||||
text_size,
|
text_size,
|
||||||
|
scrollbar_row_range,
|
||||||
|
show_scrollbars,
|
||||||
|
max_row,
|
||||||
gutter_margin,
|
gutter_margin,
|
||||||
active_rows,
|
active_rows,
|
||||||
highlighted_rows,
|
highlighted_rows,
|
||||||
highlighted_ranges,
|
highlighted_ranges,
|
||||||
line_number_layouts,
|
line_number_layouts,
|
||||||
|
hunk_layouts,
|
||||||
blocks,
|
blocks,
|
||||||
selections,
|
selections,
|
||||||
context_menu,
|
context_menu,
|
||||||
@ -1589,7 +1878,8 @@ impl Element for EditorElement {
|
|||||||
layout: &mut Self::LayoutState,
|
layout: &mut Self::LayoutState,
|
||||||
cx: &mut PaintContext,
|
cx: &mut PaintContext,
|
||||||
) -> Self::PaintState {
|
) -> Self::PaintState {
|
||||||
cx.scene.push_layer(Some(bounds));
|
let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
|
||||||
|
cx.scene.push_layer(Some(visible_bounds));
|
||||||
|
|
||||||
let gutter_bounds = RectF::new(bounds.origin(), layout.gutter_size);
|
let gutter_bounds = RectF::new(bounds.origin(), layout.gutter_size);
|
||||||
let text_bounds = RectF::new(
|
let text_bounds = RectF::new(
|
||||||
@ -1613,11 +1903,12 @@ impl Element for EditorElement {
|
|||||||
}
|
}
|
||||||
self.paint_text(text_bounds, visible_bounds, layout, cx);
|
self.paint_text(text_bounds, visible_bounds, layout, cx);
|
||||||
|
|
||||||
if !layout.blocks.is_empty() {
|
|
||||||
cx.scene.push_layer(Some(bounds));
|
cx.scene.push_layer(Some(bounds));
|
||||||
|
if !layout.blocks.is_empty() {
|
||||||
self.paint_blocks(bounds, visible_bounds, layout, cx);
|
self.paint_blocks(bounds, visible_bounds, layout, cx);
|
||||||
cx.scene.pop_layer();
|
|
||||||
}
|
}
|
||||||
|
self.paint_scrollbar(bounds, layout, cx);
|
||||||
|
cx.scene.pop_layer();
|
||||||
|
|
||||||
cx.scene.pop_layer();
|
cx.scene.pop_layer();
|
||||||
}
|
}
|
||||||
@ -1703,12 +1994,18 @@ pub struct LayoutState {
|
|||||||
gutter_padding: f32,
|
gutter_padding: f32,
|
||||||
gutter_margin: f32,
|
gutter_margin: f32,
|
||||||
text_size: Vector2F,
|
text_size: Vector2F,
|
||||||
|
mode: EditorMode,
|
||||||
|
visible_display_row_range: Range<u32>,
|
||||||
active_rows: BTreeMap<u32, bool>,
|
active_rows: BTreeMap<u32, bool>,
|
||||||
highlighted_rows: Option<Range<u32>>,
|
highlighted_rows: Option<Range<u32>>,
|
||||||
line_number_layouts: Vec<Option<text_layout::Line>>,
|
line_number_layouts: Vec<Option<text_layout::Line>>,
|
||||||
|
hunk_layouts: Vec<DiffHunkLayout>,
|
||||||
blocks: Vec<BlockLayout>,
|
blocks: Vec<BlockLayout>,
|
||||||
highlighted_ranges: Vec<(Range<DisplayPoint>, Color)>,
|
highlighted_ranges: Vec<(Range<DisplayPoint>, Color)>,
|
||||||
selections: Vec<(ReplicaId, Vec<SelectionLayout>)>,
|
selections: Vec<(ReplicaId, Vec<SelectionLayout>)>,
|
||||||
|
scrollbar_row_range: Range<f32>,
|
||||||
|
show_scrollbars: bool,
|
||||||
|
max_row: u32,
|
||||||
context_menu: Option<(DisplayPoint, ElementBox)>,
|
context_menu: Option<(DisplayPoint, ElementBox)>,
|
||||||
code_actions_indicator: Option<(u32, ElementBox)>,
|
code_actions_indicator: Option<(u32, ElementBox)>,
|
||||||
hover_popovers: Option<(DisplayPoint, Vec<ElementBox>)>,
|
hover_popovers: Option<(DisplayPoint, Vec<ElementBox>)>,
|
||||||
|
@ -32,8 +32,9 @@ pub fn refresh_matching_bracket_highlights(editor: &mut Editor, cx: &mut ViewCon
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use crate::test::editor_lsp_test_context::EditorLspTestContext;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::test::EditorLspTestContext;
|
|
||||||
use indoc::indoc;
|
use indoc::indoc;
|
||||||
use language::{BracketPair, Language, LanguageConfig};
|
use language::{BracketPair, Language, LanguageConfig};
|
||||||
|
|
||||||
|
@ -354,7 +354,7 @@ impl InfoPopover {
|
|||||||
.with_style(style.hover_popover.container)
|
.with_style(style.hover_popover.container)
|
||||||
.boxed()
|
.boxed()
|
||||||
})
|
})
|
||||||
.on_move(|_, _| {})
|
.on_move(|_, _| {}) // Consume move events so they don't reach regions underneath.
|
||||||
.with_cursor_style(CursorStyle::Arrow)
|
.with_cursor_style(CursorStyle::Arrow)
|
||||||
.with_padding(Padding {
|
.with_padding(Padding {
|
||||||
bottom: HOVER_POPOVER_GAP,
|
bottom: HOVER_POPOVER_GAP,
|
||||||
@ -400,7 +400,7 @@ impl DiagnosticPopover {
|
|||||||
bottom: HOVER_POPOVER_GAP,
|
bottom: HOVER_POPOVER_GAP,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
})
|
||||||
.on_move(|_, _| {})
|
.on_move(|_, _| {}) // Consume move events so they don't reach regions underneath.
|
||||||
.on_click(MouseButton::Left, |_, cx| {
|
.on_click(MouseButton::Left, |_, cx| {
|
||||||
cx.dispatch_action(GoToDiagnostic)
|
cx.dispatch_action(GoToDiagnostic)
|
||||||
})
|
})
|
||||||
@ -427,13 +427,13 @@ impl DiagnosticPopover {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use futures::StreamExt;
|
|
||||||
use indoc::indoc;
|
use indoc::indoc;
|
||||||
|
|
||||||
use language::{Diagnostic, DiagnosticSet};
|
use language::{Diagnostic, DiagnosticSet};
|
||||||
use project::HoverBlock;
|
use project::HoverBlock;
|
||||||
|
use smol::stream::StreamExt;
|
||||||
|
|
||||||
use crate::test::EditorLspTestContext;
|
use crate::test::editor_lsp_test_context::EditorLspTestContext;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
display_map::ToDisplayPoint, link_go_to_definition::hide_link_definition,
|
display_map::ToDisplayPoint, link_go_to_definition::hide_link_definition,
|
||||||
movement::surrounding_word, Anchor, Autoscroll, Editor, Event, ExcerptId, MultiBuffer,
|
movement::surrounding_word, Anchor, Autoscroll, Editor, Event, ExcerptId, MultiBuffer,
|
||||||
MultiBufferSnapshot, NavigationData, ToPoint as _,
|
MultiBufferSnapshot, NavigationData, ToPoint as _, FORMAT_TIMEOUT,
|
||||||
};
|
};
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use futures::FutureExt;
|
use futures::FutureExt;
|
||||||
@ -9,8 +9,8 @@ use gpui::{
|
|||||||
elements::*, geometry::vector::vec2f, AppContext, Entity, ModelHandle, MutableAppContext,
|
elements::*, geometry::vector::vec2f, AppContext, Entity, ModelHandle, MutableAppContext,
|
||||||
RenderContext, Subscription, Task, View, ViewContext, ViewHandle,
|
RenderContext, Subscription, Task, View, ViewContext, ViewHandle,
|
||||||
};
|
};
|
||||||
use language::{Bias, Buffer, File as _, OffsetRangeExt, SelectionGoal};
|
use language::{Bias, Buffer, File as _, OffsetRangeExt, Point, SelectionGoal};
|
||||||
use project::{File, Project, ProjectEntryId, ProjectPath};
|
use project::{File, FormatTrigger, Project, ProjectEntryId, ProjectPath};
|
||||||
use rpc::proto::{self, update_view};
|
use rpc::proto::{self, update_view};
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
@ -20,9 +20,8 @@ use std::{
|
|||||||
fmt::Write,
|
fmt::Write,
|
||||||
ops::Range,
|
ops::Range,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
time::Duration,
|
|
||||||
};
|
};
|
||||||
use text::{Point, Selection};
|
use text::Selection;
|
||||||
use util::TryFutureExt;
|
use util::TryFutureExt;
|
||||||
use workspace::{
|
use workspace::{
|
||||||
searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle},
|
searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle},
|
||||||
@ -30,7 +29,6 @@ use workspace::{
|
|||||||
ToolbarItemLocation,
|
ToolbarItemLocation,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2);
|
|
||||||
pub const MAX_TAB_TITLE_LEN: usize = 24;
|
pub const MAX_TAB_TITLE_LEN: usize = 24;
|
||||||
|
|
||||||
impl FollowableItem for Editor {
|
impl FollowableItem for Editor {
|
||||||
@ -406,10 +404,14 @@ impl Item for Editor {
|
|||||||
project: ModelHandle<Project>,
|
project: ModelHandle<Project>,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) -> Task<Result<()>> {
|
) -> Task<Result<()>> {
|
||||||
|
self.report_event("save editor", cx);
|
||||||
|
|
||||||
let buffer = self.buffer().clone();
|
let buffer = self.buffer().clone();
|
||||||
let buffers = buffer.read(cx).all_buffers();
|
let buffers = buffer.read(cx).all_buffers();
|
||||||
let mut timeout = cx.background().timer(FORMAT_TIMEOUT).fuse();
|
let mut timeout = cx.background().timer(FORMAT_TIMEOUT).fuse();
|
||||||
let format = project.update(cx, |project, cx| project.format(buffers, true, cx));
|
let format = project.update(cx, |project, cx| {
|
||||||
|
project.format(buffers, true, FormatTrigger::Save, cx)
|
||||||
|
});
|
||||||
cx.spawn(|_, mut cx| async move {
|
cx.spawn(|_, mut cx| async move {
|
||||||
let transaction = futures::select_biased! {
|
let transaction = futures::select_biased! {
|
||||||
_ = timeout => {
|
_ = timeout => {
|
||||||
@ -476,6 +478,17 @@ impl Item for Editor {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn git_diff_recalc(
|
||||||
|
&mut self,
|
||||||
|
_project: ModelHandle<Project>,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) -> Task<Result<()>> {
|
||||||
|
self.buffer().update(cx, |multibuffer, cx| {
|
||||||
|
multibuffer.git_diff_recalc(cx);
|
||||||
|
});
|
||||||
|
Task::ready(Ok(()))
|
||||||
|
}
|
||||||
|
|
||||||
fn to_item_events(event: &Self::Event) -> Vec<workspace::ItemEvent> {
|
fn to_item_events(event: &Self::Event) -> Vec<workspace::ItemEvent> {
|
||||||
let mut result = Vec::new();
|
let mut result = Vec::new();
|
||||||
match event {
|
match event {
|
||||||
|
@ -400,7 +400,7 @@ mod tests {
|
|||||||
use indoc::indoc;
|
use indoc::indoc;
|
||||||
use lsp::request::{GotoDefinition, GotoTypeDefinition};
|
use lsp::request::{GotoDefinition, GotoTypeDefinition};
|
||||||
|
|
||||||
use crate::test::EditorLspTestContext;
|
use crate::test::editor_lsp_test_context::EditorLspTestContext;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
@ -70,8 +70,9 @@ pub fn deploy_context_menu(
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use crate::test::editor_lsp_test_context::EditorLspTestContext;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::test::EditorLspTestContext;
|
|
||||||
use indoc::indoc;
|
use indoc::indoc;
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
|
@ -29,6 +29,25 @@ pub fn up(
|
|||||||
start: DisplayPoint,
|
start: DisplayPoint,
|
||||||
goal: SelectionGoal,
|
goal: SelectionGoal,
|
||||||
preserve_column_at_start: bool,
|
preserve_column_at_start: bool,
|
||||||
|
) -> (DisplayPoint, SelectionGoal) {
|
||||||
|
up_by_rows(map, start, 1, goal, preserve_column_at_start)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn down(
|
||||||
|
map: &DisplaySnapshot,
|
||||||
|
start: DisplayPoint,
|
||||||
|
goal: SelectionGoal,
|
||||||
|
preserve_column_at_end: bool,
|
||||||
|
) -> (DisplayPoint, SelectionGoal) {
|
||||||
|
down_by_rows(map, start, 1, goal, preserve_column_at_end)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn up_by_rows(
|
||||||
|
map: &DisplaySnapshot,
|
||||||
|
start: DisplayPoint,
|
||||||
|
row_count: u32,
|
||||||
|
goal: SelectionGoal,
|
||||||
|
preserve_column_at_start: bool,
|
||||||
) -> (DisplayPoint, SelectionGoal) {
|
) -> (DisplayPoint, SelectionGoal) {
|
||||||
let mut goal_column = if let SelectionGoal::Column(column) = goal {
|
let mut goal_column = if let SelectionGoal::Column(column) = goal {
|
||||||
column
|
column
|
||||||
@ -36,7 +55,7 @@ pub fn up(
|
|||||||
map.column_to_chars(start.row(), start.column())
|
map.column_to_chars(start.row(), start.column())
|
||||||
};
|
};
|
||||||
|
|
||||||
let prev_row = start.row().saturating_sub(1);
|
let prev_row = start.row().saturating_sub(row_count);
|
||||||
let mut point = map.clip_point(
|
let mut point = map.clip_point(
|
||||||
DisplayPoint::new(prev_row, map.line_len(prev_row)),
|
DisplayPoint::new(prev_row, map.line_len(prev_row)),
|
||||||
Bias::Left,
|
Bias::Left,
|
||||||
@ -62,9 +81,10 @@ pub fn up(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn down(
|
pub fn down_by_rows(
|
||||||
map: &DisplaySnapshot,
|
map: &DisplaySnapshot,
|
||||||
start: DisplayPoint,
|
start: DisplayPoint,
|
||||||
|
row_count: u32,
|
||||||
goal: SelectionGoal,
|
goal: SelectionGoal,
|
||||||
preserve_column_at_end: bool,
|
preserve_column_at_end: bool,
|
||||||
) -> (DisplayPoint, SelectionGoal) {
|
) -> (DisplayPoint, SelectionGoal) {
|
||||||
@ -74,8 +94,8 @@ pub fn down(
|
|||||||
map.column_to_chars(start.row(), start.column())
|
map.column_to_chars(start.row(), start.column())
|
||||||
};
|
};
|
||||||
|
|
||||||
let next_row = start.row() + 1;
|
let new_row = start.row() + row_count;
|
||||||
let mut point = map.clip_point(DisplayPoint::new(next_row, 0), Bias::Right);
|
let mut point = map.clip_point(DisplayPoint::new(new_row, 0), Bias::Right);
|
||||||
if point.row() > start.row() {
|
if point.row() > start.row() {
|
||||||
*point.column_mut() = map.column_from_chars(point.row(), goal_column);
|
*point.column_mut() = map.column_from_chars(point.row(), goal_column);
|
||||||
} else if preserve_column_at_end {
|
} else if preserve_column_at_end {
|
||||||
@ -101,6 +121,22 @@ pub fn line_beginning(
|
|||||||
map: &DisplaySnapshot,
|
map: &DisplaySnapshot,
|
||||||
display_point: DisplayPoint,
|
display_point: DisplayPoint,
|
||||||
stop_at_soft_boundaries: bool,
|
stop_at_soft_boundaries: bool,
|
||||||
|
) -> DisplayPoint {
|
||||||
|
let point = display_point.to_point(map);
|
||||||
|
let soft_line_start = map.clip_point(DisplayPoint::new(display_point.row(), 0), Bias::Right);
|
||||||
|
let line_start = map.prev_line_boundary(point).1;
|
||||||
|
|
||||||
|
if stop_at_soft_boundaries && display_point != soft_line_start {
|
||||||
|
soft_line_start
|
||||||
|
} else {
|
||||||
|
line_start
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn indented_line_beginning(
|
||||||
|
map: &DisplaySnapshot,
|
||||||
|
display_point: DisplayPoint,
|
||||||
|
stop_at_soft_boundaries: bool,
|
||||||
) -> DisplayPoint {
|
) -> DisplayPoint {
|
||||||
let point = display_point.to_point(map);
|
let point = display_point.to_point(map);
|
||||||
let soft_line_start = map.clip_point(DisplayPoint::new(display_point.row(), 0), Bias::Right);
|
let soft_line_start = map.clip_point(DisplayPoint::new(display_point.row(), 0), Bias::Right);
|
||||||
@ -167,54 +203,79 @@ pub fn next_subword_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPo
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Scans for a boundary from the start of each line preceding the given end point until a boundary
|
/// Scans for a boundary preceding the given start point `from` until a boundary is found, indicated by the
|
||||||
/// is found, indicated by the given predicate returning true. The predicate is called with the
|
/// given predicate returning true. The predicate is called with the character to the left and right
|
||||||
/// character to the left and right of the candidate boundary location, and will be called with `\n`
|
/// of the candidate boundary location, and will be called with `\n` characters indicating the start
|
||||||
/// characters indicating the start or end of a line. If the predicate returns true multiple times
|
/// or end of a line.
|
||||||
/// on a line, the *rightmost* boundary is returned.
|
|
||||||
pub fn find_preceding_boundary(
|
pub fn find_preceding_boundary(
|
||||||
map: &DisplaySnapshot,
|
map: &DisplaySnapshot,
|
||||||
end: DisplayPoint,
|
from: DisplayPoint,
|
||||||
mut is_boundary: impl FnMut(char, char) -> bool,
|
mut is_boundary: impl FnMut(char, char) -> bool,
|
||||||
) -> DisplayPoint {
|
) -> DisplayPoint {
|
||||||
let mut point = end;
|
let mut start_column = 0;
|
||||||
loop {
|
let mut soft_wrap_row = from.row() + 1;
|
||||||
*point.column_mut() = 0;
|
|
||||||
if point.row() > 0 {
|
let mut prev = None;
|
||||||
if let Some(indent) = map.soft_wrap_indent(point.row() - 1) {
|
for (ch, point) in map.reverse_chars_at(from) {
|
||||||
*point.column_mut() = indent;
|
// Recompute soft_wrap_indent if the row has changed
|
||||||
|
if point.row() != soft_wrap_row {
|
||||||
|
soft_wrap_row = point.row();
|
||||||
|
|
||||||
|
if point.row() == 0 {
|
||||||
|
start_column = 0;
|
||||||
|
} else if let Some(indent) = map.soft_wrap_indent(point.row() - 1) {
|
||||||
|
start_column = indent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut boundary = None;
|
// If the current point is in the soft_wrap, skip comparing it
|
||||||
let mut prev_ch = if point.is_zero() { None } else { Some('\n') };
|
if point.column() < start_column {
|
||||||
for ch in map.chars_at(point) {
|
continue;
|
||||||
if point >= end {
|
}
|
||||||
|
|
||||||
|
if let Some((prev_ch, prev_point)) = prev {
|
||||||
|
if is_boundary(ch, prev_ch) {
|
||||||
|
return prev_point;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
prev = Some((ch, point));
|
||||||
|
}
|
||||||
|
DisplayPoint::zero()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scans for a boundary preceding the given start point `from` until a boundary is found, indicated by the
|
||||||
|
/// given predicate returning true. The predicate is called with the character to the left and right
|
||||||
|
/// of the candidate boundary location, and will be called with `\n` characters indicating the start
|
||||||
|
/// or end of a line. If no boundary is found, the start of the line is returned.
|
||||||
|
pub fn find_preceding_boundary_in_line(
|
||||||
|
map: &DisplaySnapshot,
|
||||||
|
from: DisplayPoint,
|
||||||
|
mut is_boundary: impl FnMut(char, char) -> bool,
|
||||||
|
) -> DisplayPoint {
|
||||||
|
let mut start_column = 0;
|
||||||
|
if from.row() > 0 {
|
||||||
|
if let Some(indent) = map.soft_wrap_indent(from.row() - 1) {
|
||||||
|
start_column = indent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut prev = None;
|
||||||
|
for (ch, point) in map.reverse_chars_at(from) {
|
||||||
|
if let Some((prev_ch, prev_point)) = prev {
|
||||||
|
if is_boundary(ch, prev_ch) {
|
||||||
|
return prev_point;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ch == '\n' || point.column() < start_column {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(prev_ch) = prev_ch {
|
prev = Some((ch, point));
|
||||||
if is_boundary(prev_ch, ch) {
|
|
||||||
boundary = Some(point);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ch == '\n' {
|
prev.map(|(_, point)| point).unwrap_or(from)
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
prev_ch = Some(ch);
|
|
||||||
*point.column_mut() += ch.len_utf8() as u32;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(boundary) = boundary {
|
|
||||||
return boundary;
|
|
||||||
} else if point.row() == 0 {
|
|
||||||
return DisplayPoint::zero();
|
|
||||||
} else {
|
|
||||||
*point.row_mut() -= 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Scans for a boundary following the given start point until a boundary is found, indicated by the
|
/// Scans for a boundary following the given start point until a boundary is found, indicated by the
|
||||||
@ -223,26 +284,48 @@ pub fn find_preceding_boundary(
|
|||||||
/// or end of a line.
|
/// or end of a line.
|
||||||
pub fn find_boundary(
|
pub fn find_boundary(
|
||||||
map: &DisplaySnapshot,
|
map: &DisplaySnapshot,
|
||||||
mut point: DisplayPoint,
|
from: DisplayPoint,
|
||||||
mut is_boundary: impl FnMut(char, char) -> bool,
|
mut is_boundary: impl FnMut(char, char) -> bool,
|
||||||
) -> DisplayPoint {
|
) -> DisplayPoint {
|
||||||
let mut prev_ch = None;
|
let mut prev_ch = None;
|
||||||
for ch in map.chars_at(point) {
|
for (ch, point) in map.chars_at(from) {
|
||||||
if let Some(prev_ch) = prev_ch {
|
if let Some(prev_ch) = prev_ch {
|
||||||
if is_boundary(prev_ch, ch) {
|
if is_boundary(prev_ch, ch) {
|
||||||
|
return map.clip_point(point, Bias::Right);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
prev_ch = Some(ch);
|
||||||
|
}
|
||||||
|
map.clip_point(map.max_point(), Bias::Right)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scans for a boundary following the given start point until a boundary is found, indicated by the
|
||||||
|
/// given predicate returning true. The predicate is called with the character to the left and right
|
||||||
|
/// of the candidate boundary location, and will be called with `\n` characters indicating the start
|
||||||
|
/// or end of a line. If no boundary is found, the end of the line is returned
|
||||||
|
pub fn find_boundary_in_line(
|
||||||
|
map: &DisplaySnapshot,
|
||||||
|
from: DisplayPoint,
|
||||||
|
mut is_boundary: impl FnMut(char, char) -> bool,
|
||||||
|
) -> DisplayPoint {
|
||||||
|
let mut prev = None;
|
||||||
|
for (ch, point) in map.chars_at(from) {
|
||||||
|
if let Some((prev_ch, _)) = prev {
|
||||||
|
if is_boundary(prev_ch, ch) {
|
||||||
|
return map.clip_point(point, Bias::Right);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
prev = Some((ch, point));
|
||||||
|
|
||||||
|
if ch == '\n' {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ch == '\n' {
|
// Return the last position checked so that we give a point right before the newline or eof.
|
||||||
*point.row_mut() += 1;
|
map.clip_point(prev.map(|(_, point)| point).unwrap_or(from), Bias::Right)
|
||||||
*point.column_mut() = 0;
|
|
||||||
} else {
|
|
||||||
*point.column_mut() += ch.len_utf8() as u32;
|
|
||||||
}
|
|
||||||
prev_ch = Some(ch);
|
|
||||||
}
|
|
||||||
map.clip_point(point, Bias::Right)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool {
|
pub fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool {
|
||||||
@ -273,7 +356,6 @@ pub fn surrounding_word(map: &DisplaySnapshot, position: DisplayPoint) -> Range<
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{test::marked_display_snapshot, Buffer, DisplayMap, ExcerptRange, MultiBuffer};
|
use crate::{test::marked_display_snapshot, Buffer, DisplayMap, ExcerptRange, MultiBuffer};
|
||||||
use language::Point;
|
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
|
@ -4,12 +4,14 @@ pub use anchor::{Anchor, AnchorRangeExt};
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use clock::ReplicaId;
|
use clock::ReplicaId;
|
||||||
use collections::{BTreeMap, Bound, HashMap, HashSet};
|
use collections::{BTreeMap, Bound, HashMap, HashSet};
|
||||||
|
use git::diff::DiffHunk;
|
||||||
use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task};
|
use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task};
|
||||||
pub use language::Completion;
|
pub use language::Completion;
|
||||||
use language::{
|
use language::{
|
||||||
char_kind, AutoindentMode, Buffer, BufferChunks, BufferSnapshot, CharKind, Chunk,
|
char_kind, AutoindentMode, Buffer, BufferChunks, BufferSnapshot, CharKind, Chunk,
|
||||||
DiagnosticEntry, Event, File, IndentSize, Language, OffsetRangeExt, Outline, OutlineItem,
|
DiagnosticEntry, Event, File, IndentSize, Language, OffsetRangeExt, OffsetUtf16, Outline,
|
||||||
Selection, ToOffset as _, ToOffsetUtf16 as _, ToPoint as _, ToPointUtf16 as _, TransactionId,
|
OutlineItem, Point, PointUtf16, Selection, TextDimension, ToOffset as _, ToOffsetUtf16 as _,
|
||||||
|
ToPoint as _, ToPointUtf16 as _, TransactionId,
|
||||||
};
|
};
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
use std::{
|
use std::{
|
||||||
@ -26,9 +28,8 @@ use std::{
|
|||||||
use sum_tree::{Bias, Cursor, SumTree};
|
use sum_tree::{Bias, Cursor, SumTree};
|
||||||
use text::{
|
use text::{
|
||||||
locator::Locator,
|
locator::Locator,
|
||||||
rope::TextDimension,
|
|
||||||
subscription::{Subscription, Topic},
|
subscription::{Subscription, Topic},
|
||||||
Edit, OffsetUtf16, Point, PointUtf16, TextSummary,
|
Edit, TextSummary,
|
||||||
};
|
};
|
||||||
use theme::SyntaxTheme;
|
use theme::SyntaxTheme;
|
||||||
use util::post_inc;
|
use util::post_inc;
|
||||||
@ -90,6 +91,7 @@ struct BufferState {
|
|||||||
last_selections_update_count: usize,
|
last_selections_update_count: usize,
|
||||||
last_diagnostics_update_count: usize,
|
last_diagnostics_update_count: usize,
|
||||||
last_file_update_count: usize,
|
last_file_update_count: usize,
|
||||||
|
last_git_diff_update_count: usize,
|
||||||
excerpts: Vec<ExcerptId>,
|
excerpts: Vec<ExcerptId>,
|
||||||
_subscriptions: [gpui::Subscription; 2],
|
_subscriptions: [gpui::Subscription; 2],
|
||||||
}
|
}
|
||||||
@ -101,6 +103,7 @@ pub struct MultiBufferSnapshot {
|
|||||||
parse_count: usize,
|
parse_count: usize,
|
||||||
diagnostics_update_count: usize,
|
diagnostics_update_count: usize,
|
||||||
trailing_excerpt_update_count: usize,
|
trailing_excerpt_update_count: usize,
|
||||||
|
git_diff_update_count: usize,
|
||||||
edit_count: usize,
|
edit_count: usize,
|
||||||
is_dirty: bool,
|
is_dirty: bool,
|
||||||
has_conflict: bool,
|
has_conflict: bool,
|
||||||
@ -140,6 +143,7 @@ struct ExcerptSummary {
|
|||||||
text: TextSummary,
|
text: TextSummary,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct MultiBufferRows<'a> {
|
pub struct MultiBufferRows<'a> {
|
||||||
buffer_row_range: Range<u32>,
|
buffer_row_range: Range<u32>,
|
||||||
excerpts: Cursor<'a, Excerpt, Point>,
|
excerpts: Cursor<'a, Excerpt, Point>,
|
||||||
@ -165,7 +169,7 @@ struct ExcerptChunks<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct ExcerptBytes<'a> {
|
struct ExcerptBytes<'a> {
|
||||||
content_bytes: language::rope::Bytes<'a>,
|
content_bytes: text::Bytes<'a>,
|
||||||
footer_height: usize,
|
footer_height: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -202,6 +206,7 @@ impl MultiBuffer {
|
|||||||
last_selections_update_count: buffer_state.last_selections_update_count,
|
last_selections_update_count: buffer_state.last_selections_update_count,
|
||||||
last_diagnostics_update_count: buffer_state.last_diagnostics_update_count,
|
last_diagnostics_update_count: buffer_state.last_diagnostics_update_count,
|
||||||
last_file_update_count: buffer_state.last_file_update_count,
|
last_file_update_count: buffer_state.last_file_update_count,
|
||||||
|
last_git_diff_update_count: buffer_state.last_git_diff_update_count,
|
||||||
excerpts: buffer_state.excerpts.clone(),
|
excerpts: buffer_state.excerpts.clone(),
|
||||||
_subscriptions: [
|
_subscriptions: [
|
||||||
new_cx.observe(&buffer_state.buffer, |_, _, cx| cx.notify()),
|
new_cx.observe(&buffer_state.buffer, |_, _, cx| cx.notify()),
|
||||||
@ -308,6 +313,17 @@ impl MultiBuffer {
|
|||||||
self.read(cx).symbols_containing(offset, theme)
|
self.read(cx).symbols_containing(offset, theme)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn git_diff_recalc(&mut self, cx: &mut ModelContext<Self>) {
|
||||||
|
let buffers = self.buffers.borrow();
|
||||||
|
for buffer_state in buffers.values() {
|
||||||
|
if buffer_state.buffer.read(cx).needs_git_diff_recalc() {
|
||||||
|
buffer_state
|
||||||
|
.buffer
|
||||||
|
.update(cx, |buffer, cx| buffer.git_diff_recalc(cx))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn edit<I, S, T>(
|
pub fn edit<I, S, T>(
|
||||||
&mut self,
|
&mut self,
|
||||||
edits: I,
|
edits: I,
|
||||||
@ -827,6 +843,7 @@ impl MultiBuffer {
|
|||||||
last_selections_update_count: buffer_snapshot.selections_update_count(),
|
last_selections_update_count: buffer_snapshot.selections_update_count(),
|
||||||
last_diagnostics_update_count: buffer_snapshot.diagnostics_update_count(),
|
last_diagnostics_update_count: buffer_snapshot.diagnostics_update_count(),
|
||||||
last_file_update_count: buffer_snapshot.file_update_count(),
|
last_file_update_count: buffer_snapshot.file_update_count(),
|
||||||
|
last_git_diff_update_count: buffer_snapshot.git_diff_update_count(),
|
||||||
excerpts: Default::default(),
|
excerpts: Default::default(),
|
||||||
_subscriptions: [
|
_subscriptions: [
|
||||||
cx.observe(&buffer, |_, _, cx| cx.notify()),
|
cx.observe(&buffer, |_, _, cx| cx.notify()),
|
||||||
@ -1212,9 +1229,9 @@ impl MultiBuffer {
|
|||||||
&self,
|
&self,
|
||||||
point: T,
|
point: T,
|
||||||
cx: &'a AppContext,
|
cx: &'a AppContext,
|
||||||
) -> Option<&'a Arc<Language>> {
|
) -> Option<Arc<Language>> {
|
||||||
self.point_to_buffer_offset(point, cx)
|
self.point_to_buffer_offset(point, cx)
|
||||||
.and_then(|(buffer, _)| buffer.read(cx).language())
|
.and_then(|(buffer, offset)| buffer.read(cx).language_at(offset))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn files<'a>(&'a self, cx: &'a AppContext) -> SmallVec<[&'a dyn File; 2]> {
|
pub fn files<'a>(&'a self, cx: &'a AppContext) -> SmallVec<[&'a dyn File; 2]> {
|
||||||
@ -1249,6 +1266,7 @@ impl MultiBuffer {
|
|||||||
let mut excerpts_to_edit = Vec::new();
|
let mut excerpts_to_edit = Vec::new();
|
||||||
let mut reparsed = false;
|
let mut reparsed = false;
|
||||||
let mut diagnostics_updated = false;
|
let mut diagnostics_updated = false;
|
||||||
|
let mut git_diff_updated = false;
|
||||||
let mut is_dirty = false;
|
let mut is_dirty = false;
|
||||||
let mut has_conflict = false;
|
let mut has_conflict = false;
|
||||||
let mut edited = false;
|
let mut edited = false;
|
||||||
@ -1260,6 +1278,7 @@ impl MultiBuffer {
|
|||||||
let selections_update_count = buffer.selections_update_count();
|
let selections_update_count = buffer.selections_update_count();
|
||||||
let diagnostics_update_count = buffer.diagnostics_update_count();
|
let diagnostics_update_count = buffer.diagnostics_update_count();
|
||||||
let file_update_count = buffer.file_update_count();
|
let file_update_count = buffer.file_update_count();
|
||||||
|
let git_diff_update_count = buffer.git_diff_update_count();
|
||||||
|
|
||||||
let buffer_edited = version.changed_since(&buffer_state.last_version);
|
let buffer_edited = version.changed_since(&buffer_state.last_version);
|
||||||
let buffer_reparsed = parse_count > buffer_state.last_parse_count;
|
let buffer_reparsed = parse_count > buffer_state.last_parse_count;
|
||||||
@ -1268,17 +1287,21 @@ impl MultiBuffer {
|
|||||||
let buffer_diagnostics_updated =
|
let buffer_diagnostics_updated =
|
||||||
diagnostics_update_count > buffer_state.last_diagnostics_update_count;
|
diagnostics_update_count > buffer_state.last_diagnostics_update_count;
|
||||||
let buffer_file_updated = file_update_count > buffer_state.last_file_update_count;
|
let buffer_file_updated = file_update_count > buffer_state.last_file_update_count;
|
||||||
|
let buffer_git_diff_updated =
|
||||||
|
git_diff_update_count > buffer_state.last_git_diff_update_count;
|
||||||
if buffer_edited
|
if buffer_edited
|
||||||
|| buffer_reparsed
|
|| buffer_reparsed
|
||||||
|| buffer_selections_updated
|
|| buffer_selections_updated
|
||||||
|| buffer_diagnostics_updated
|
|| buffer_diagnostics_updated
|
||||||
|| buffer_file_updated
|
|| buffer_file_updated
|
||||||
|
|| buffer_git_diff_updated
|
||||||
{
|
{
|
||||||
buffer_state.last_version = version;
|
buffer_state.last_version = version;
|
||||||
buffer_state.last_parse_count = parse_count;
|
buffer_state.last_parse_count = parse_count;
|
||||||
buffer_state.last_selections_update_count = selections_update_count;
|
buffer_state.last_selections_update_count = selections_update_count;
|
||||||
buffer_state.last_diagnostics_update_count = diagnostics_update_count;
|
buffer_state.last_diagnostics_update_count = diagnostics_update_count;
|
||||||
buffer_state.last_file_update_count = file_update_count;
|
buffer_state.last_file_update_count = file_update_count;
|
||||||
|
buffer_state.last_git_diff_update_count = git_diff_update_count;
|
||||||
excerpts_to_edit.extend(
|
excerpts_to_edit.extend(
|
||||||
buffer_state
|
buffer_state
|
||||||
.excerpts
|
.excerpts
|
||||||
@ -1290,6 +1313,7 @@ impl MultiBuffer {
|
|||||||
edited |= buffer_edited;
|
edited |= buffer_edited;
|
||||||
reparsed |= buffer_reparsed;
|
reparsed |= buffer_reparsed;
|
||||||
diagnostics_updated |= buffer_diagnostics_updated;
|
diagnostics_updated |= buffer_diagnostics_updated;
|
||||||
|
git_diff_updated |= buffer_git_diff_updated;
|
||||||
is_dirty |= buffer.is_dirty();
|
is_dirty |= buffer.is_dirty();
|
||||||
has_conflict |= buffer.has_conflict();
|
has_conflict |= buffer.has_conflict();
|
||||||
}
|
}
|
||||||
@ -1302,6 +1326,9 @@ impl MultiBuffer {
|
|||||||
if diagnostics_updated {
|
if diagnostics_updated {
|
||||||
snapshot.diagnostics_update_count += 1;
|
snapshot.diagnostics_update_count += 1;
|
||||||
}
|
}
|
||||||
|
if git_diff_updated {
|
||||||
|
snapshot.git_diff_update_count += 1;
|
||||||
|
}
|
||||||
snapshot.is_dirty = is_dirty;
|
snapshot.is_dirty = is_dirty;
|
||||||
snapshot.has_conflict = has_conflict;
|
snapshot.has_conflict = has_conflict;
|
||||||
|
|
||||||
@ -1386,7 +1413,7 @@ impl MultiBuffer {
|
|||||||
edit_count: usize,
|
edit_count: usize,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) {
|
) {
|
||||||
use text::RandomCharIter;
|
use util::RandomCharIter;
|
||||||
|
|
||||||
let snapshot = self.read(cx);
|
let snapshot = self.read(cx);
|
||||||
let mut edits: Vec<(Range<usize>, Arc<str>)> = Vec::new();
|
let mut edits: Vec<(Range<usize>, Arc<str>)> = Vec::new();
|
||||||
@ -1425,7 +1452,7 @@ impl MultiBuffer {
|
|||||||
) {
|
) {
|
||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
use std::env;
|
use std::env;
|
||||||
use text::RandomCharIter;
|
use util::RandomCharIter;
|
||||||
|
|
||||||
let max_excerpts = env::var("MAX_EXCERPTS")
|
let max_excerpts = env::var("MAX_EXCERPTS")
|
||||||
.map(|i| i.parse().expect("invalid `MAX_EXCERPTS` variable"))
|
.map(|i| i.parse().expect("invalid `MAX_EXCERPTS` variable"))
|
||||||
@ -1940,6 +1967,24 @@ impl MultiBufferSnapshot {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn point_to_buffer_offset<T: ToOffset>(
|
||||||
|
&self,
|
||||||
|
point: T,
|
||||||
|
) -> Option<(&BufferSnapshot, usize)> {
|
||||||
|
let offset = point.to_offset(&self);
|
||||||
|
let mut cursor = self.excerpts.cursor::<usize>();
|
||||||
|
cursor.seek(&offset, Bias::Right, &());
|
||||||
|
if cursor.item().is_none() {
|
||||||
|
cursor.prev(&());
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor.item().map(|excerpt| {
|
||||||
|
let excerpt_start = excerpt.range.context.start.to_offset(&excerpt.buffer);
|
||||||
|
let buffer_point = excerpt_start + offset - *cursor.start();
|
||||||
|
(&excerpt.buffer, buffer_point)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn suggested_indents(
|
pub fn suggested_indents(
|
||||||
&self,
|
&self,
|
||||||
rows: impl IntoIterator<Item = u32>,
|
rows: impl IntoIterator<Item = u32>,
|
||||||
@ -1949,8 +1994,10 @@ impl MultiBufferSnapshot {
|
|||||||
|
|
||||||
let mut rows_for_excerpt = Vec::new();
|
let mut rows_for_excerpt = Vec::new();
|
||||||
let mut cursor = self.excerpts.cursor::<Point>();
|
let mut cursor = self.excerpts.cursor::<Point>();
|
||||||
|
|
||||||
let mut rows = rows.into_iter().peekable();
|
let mut rows = rows.into_iter().peekable();
|
||||||
|
let mut prev_row = u32::MAX;
|
||||||
|
let mut prev_language_indent_size = IndentSize::default();
|
||||||
|
|
||||||
while let Some(row) = rows.next() {
|
while let Some(row) = rows.next() {
|
||||||
cursor.seek(&Point::new(row, 0), Bias::Right, &());
|
cursor.seek(&Point::new(row, 0), Bias::Right, &());
|
||||||
let excerpt = match cursor.item() {
|
let excerpt = match cursor.item() {
|
||||||
@ -1958,7 +2005,17 @@ impl MultiBufferSnapshot {
|
|||||||
_ => continue,
|
_ => continue,
|
||||||
};
|
};
|
||||||
|
|
||||||
let single_indent_size = excerpt.buffer.single_indent_size(cx);
|
// Retrieve the language and indent size once for each disjoint region being indented.
|
||||||
|
let single_indent_size = if row.saturating_sub(1) == prev_row {
|
||||||
|
prev_language_indent_size
|
||||||
|
} else {
|
||||||
|
excerpt
|
||||||
|
.buffer
|
||||||
|
.language_indent_size_at(Point::new(row, 0), cx)
|
||||||
|
};
|
||||||
|
prev_language_indent_size = single_indent_size;
|
||||||
|
prev_row = row;
|
||||||
|
|
||||||
let start_buffer_row = excerpt.range.context.start.to_point(&excerpt.buffer).row;
|
let start_buffer_row = excerpt.range.context.start.to_point(&excerpt.buffer).row;
|
||||||
let start_multibuffer_row = cursor.start().row;
|
let start_multibuffer_row = cursor.start().row;
|
||||||
|
|
||||||
@ -2479,15 +2536,17 @@ impl MultiBufferSnapshot {
|
|||||||
self.diagnostics_update_count
|
self.diagnostics_update_count
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn git_diff_update_count(&self) -> usize {
|
||||||
|
self.git_diff_update_count
|
||||||
|
}
|
||||||
|
|
||||||
pub fn trailing_excerpt_update_count(&self) -> usize {
|
pub fn trailing_excerpt_update_count(&self) -> usize {
|
||||||
self.trailing_excerpt_update_count
|
self.trailing_excerpt_update_count
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn language(&self) -> Option<&Arc<Language>> {
|
pub fn language_at<'a, T: ToOffset>(&'a self, point: T) -> Option<&'a Arc<Language>> {
|
||||||
self.excerpts
|
self.point_to_buffer_offset(point)
|
||||||
.iter()
|
.and_then(|(buffer, offset)| buffer.language_at(offset))
|
||||||
.next()
|
|
||||||
.and_then(|excerpt| excerpt.buffer.language())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_dirty(&self) -> bool {
|
pub fn is_dirty(&self) -> bool {
|
||||||
@ -2529,6 +2588,15 @@ impl MultiBufferSnapshot {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn git_diff_hunks_in_range<'a>(
|
||||||
|
&'a self,
|
||||||
|
row_range: Range<u32>,
|
||||||
|
) -> impl 'a + Iterator<Item = DiffHunk<u32>> {
|
||||||
|
self.as_singleton()
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(move |(_, _, buffer)| buffer.git_diff_hunks_in_range(row_range.clone()))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn range_for_syntax_ancestor<T: ToOffset>(&self, range: Range<T>) -> Option<Range<usize>> {
|
pub fn range_for_syntax_ancestor<T: ToOffset>(&self, range: Range<T>) -> Option<Range<usize>> {
|
||||||
let range = range.start.to_offset(self)..range.end.to_offset(self);
|
let range = range.start.to_offset(self)..range.end.to_offset(self);
|
||||||
|
|
||||||
@ -3270,7 +3338,7 @@ mod tests {
|
|||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use std::{env, rc::Rc};
|
use std::{env, rc::Rc};
|
||||||
use text::{Point, RandomCharIter};
|
|
||||||
use util::test::sample_text;
|
use util::test::sample_text;
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
@ -3888,7 +3956,9 @@ mod tests {
|
|||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
let buffer_handle = if buffers.is_empty() || rng.gen_bool(0.4) {
|
let buffer_handle = if buffers.is_empty() || rng.gen_bool(0.4) {
|
||||||
let base_text = RandomCharIter::new(&mut rng).take(10).collect::<String>();
|
let base_text = util::RandomCharIter::new(&mut rng)
|
||||||
|
.take(10)
|
||||||
|
.collect::<String>();
|
||||||
buffers.push(cx.add_model(|cx| Buffer::new(0, base_text, cx)));
|
buffers.push(cx.add_model(|cx| Buffer::new(0, base_text, cx)));
|
||||||
buffers.last().unwrap()
|
buffers.last().unwrap()
|
||||||
} else {
|
} else {
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
use super::{ExcerptId, MultiBufferSnapshot, ToOffset, ToOffsetUtf16, ToPoint};
|
use super::{ExcerptId, MultiBufferSnapshot, ToOffset, ToOffsetUtf16, ToPoint};
|
||||||
|
use language::{OffsetUtf16, Point, TextDimension};
|
||||||
use std::{
|
use std::{
|
||||||
cmp::Ordering,
|
cmp::Ordering,
|
||||||
ops::{Range, Sub},
|
ops::{Range, Sub},
|
||||||
};
|
};
|
||||||
use sum_tree::Bias;
|
use sum_tree::Bias;
|
||||||
use text::{rope::TextDimension, OffsetUtf16, Point};
|
|
||||||
|
|
||||||
#[derive(Clone, Eq, PartialEq, Debug, Hash)]
|
#[derive(Clone, Eq, PartialEq, Debug, Hash)]
|
||||||
pub struct Anchor {
|
pub struct Anchor {
|
||||||
|
@ -8,7 +8,7 @@ use std::{
|
|||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use gpui::{AppContext, ModelHandle, MutableAppContext};
|
use gpui::{AppContext, ModelHandle, MutableAppContext};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use language::{rope::TextDimension, Bias, Point, Selection, SelectionGoal, ToPoint};
|
use language::{Bias, Point, Selection, SelectionGoal, TextDimension, ToPoint};
|
||||||
use util::post_inc;
|
use util::post_inc;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
@ -1,28 +1,14 @@
|
|||||||
|
pub mod editor_lsp_test_context;
|
||||||
|
pub mod editor_test_context;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint},
|
display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint},
|
||||||
multi_buffer::ToPointUtf16,
|
DisplayPoint, Editor, EditorMode, MultiBuffer,
|
||||||
AnchorRangeExt, Autoscroll, DisplayPoint, Editor, EditorMode, MultiBuffer, ToPoint,
|
|
||||||
};
|
};
|
||||||
use anyhow::Result;
|
|
||||||
use futures::{Future, StreamExt};
|
use gpui::{ModelHandle, ViewContext};
|
||||||
use gpui::{
|
|
||||||
json, keymap::Keystroke, AppContext, ModelContext, ModelHandle, ViewContext, ViewHandle,
|
use util::test::{marked_text_offsets, marked_text_ranges};
|
||||||
};
|
|
||||||
use indoc::indoc;
|
|
||||||
use language::{point_to_lsp, Buffer, BufferSnapshot, FakeLspAdapter, Language, LanguageConfig};
|
|
||||||
use lsp::{notification, request};
|
|
||||||
use project::Project;
|
|
||||||
use settings::Settings;
|
|
||||||
use std::{
|
|
||||||
any::TypeId,
|
|
||||||
ops::{Deref, DerefMut, Range},
|
|
||||||
sync::Arc,
|
|
||||||
};
|
|
||||||
use util::{
|
|
||||||
assert_set_eq, set_eq,
|
|
||||||
test::{generate_marked_text, marked_text_offsets, marked_text_ranges},
|
|
||||||
};
|
|
||||||
use workspace::{pane, AppState, Workspace, WorkspaceHandle};
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[ctor::ctor]
|
#[ctor::ctor]
|
||||||
@ -80,430 +66,3 @@ pub(crate) fn build_editor(
|
|||||||
) -> Editor {
|
) -> Editor {
|
||||||
Editor::new(EditorMode::Full, buffer, None, None, cx)
|
Editor::new(EditorMode::Full, buffer, None, None, cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct EditorTestContext<'a> {
|
|
||||||
pub cx: &'a mut gpui::TestAppContext,
|
|
||||||
pub window_id: usize,
|
|
||||||
pub editor: ViewHandle<Editor>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> EditorTestContext<'a> {
|
|
||||||
pub fn new(cx: &'a mut gpui::TestAppContext) -> EditorTestContext<'a> {
|
|
||||||
let (window_id, editor) = cx.update(|cx| {
|
|
||||||
cx.set_global(Settings::test(cx));
|
|
||||||
crate::init(cx);
|
|
||||||
|
|
||||||
let (window_id, editor) = cx.add_window(Default::default(), |cx| {
|
|
||||||
build_editor(MultiBuffer::build_simple("", cx), cx)
|
|
||||||
});
|
|
||||||
|
|
||||||
editor.update(cx, |_, cx| cx.focus_self());
|
|
||||||
|
|
||||||
(window_id, editor)
|
|
||||||
});
|
|
||||||
|
|
||||||
Self {
|
|
||||||
cx,
|
|
||||||
window_id,
|
|
||||||
editor,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn condition(
|
|
||||||
&self,
|
|
||||||
predicate: impl FnMut(&Editor, &AppContext) -> bool,
|
|
||||||
) -> impl Future<Output = ()> {
|
|
||||||
self.editor.condition(self.cx, predicate)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn editor<F, T>(&self, read: F) -> T
|
|
||||||
where
|
|
||||||
F: FnOnce(&Editor, &AppContext) -> T,
|
|
||||||
{
|
|
||||||
self.editor.read_with(self.cx, read)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_editor<F, T>(&mut self, update: F) -> T
|
|
||||||
where
|
|
||||||
F: FnOnce(&mut Editor, &mut ViewContext<Editor>) -> T,
|
|
||||||
{
|
|
||||||
self.editor.update(self.cx, update)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn multibuffer<F, T>(&self, read: F) -> T
|
|
||||||
where
|
|
||||||
F: FnOnce(&MultiBuffer, &AppContext) -> T,
|
|
||||||
{
|
|
||||||
self.editor(|editor, cx| read(editor.buffer().read(cx), cx))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_multibuffer<F, T>(&mut self, update: F) -> T
|
|
||||||
where
|
|
||||||
F: FnOnce(&mut MultiBuffer, &mut ModelContext<MultiBuffer>) -> T,
|
|
||||||
{
|
|
||||||
self.update_editor(|editor, cx| editor.buffer().update(cx, update))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn buffer_text(&self) -> String {
|
|
||||||
self.multibuffer(|buffer, cx| buffer.snapshot(cx).text())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn buffer<F, T>(&self, read: F) -> T
|
|
||||||
where
|
|
||||||
F: FnOnce(&Buffer, &AppContext) -> T,
|
|
||||||
{
|
|
||||||
self.multibuffer(|multibuffer, cx| {
|
|
||||||
let buffer = multibuffer.as_singleton().unwrap().read(cx);
|
|
||||||
read(buffer, cx)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_buffer<F, T>(&mut self, update: F) -> T
|
|
||||||
where
|
|
||||||
F: FnOnce(&mut Buffer, &mut ModelContext<Buffer>) -> T,
|
|
||||||
{
|
|
||||||
self.update_multibuffer(|multibuffer, cx| {
|
|
||||||
let buffer = multibuffer.as_singleton().unwrap();
|
|
||||||
buffer.update(cx, update)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn buffer_snapshot(&self) -> BufferSnapshot {
|
|
||||||
self.buffer(|buffer, _| buffer.snapshot())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn simulate_keystroke(&mut self, keystroke_text: &str) {
|
|
||||||
let keystroke = Keystroke::parse(keystroke_text).unwrap();
|
|
||||||
self.cx.dispatch_keystroke(self.window_id, keystroke, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn simulate_keystrokes<const COUNT: usize>(&mut self, keystroke_texts: [&str; COUNT]) {
|
|
||||||
for keystroke_text in keystroke_texts.into_iter() {
|
|
||||||
self.simulate_keystroke(keystroke_text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ranges(&self, marked_text: &str) -> Vec<Range<usize>> {
|
|
||||||
let (unmarked_text, ranges) = marked_text_ranges(marked_text, false);
|
|
||||||
assert_eq!(self.buffer_text(), unmarked_text);
|
|
||||||
ranges
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn display_point(&mut self, marked_text: &str) -> DisplayPoint {
|
|
||||||
let ranges = self.ranges(marked_text);
|
|
||||||
let snapshot = self
|
|
||||||
.editor
|
|
||||||
.update(self.cx, |editor, cx| editor.snapshot(cx));
|
|
||||||
ranges[0].start.to_display_point(&snapshot)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns anchors for the current buffer using `«` and `»`
|
|
||||||
pub fn text_anchor_range(&self, marked_text: &str) -> Range<language::Anchor> {
|
|
||||||
let ranges = self.ranges(marked_text);
|
|
||||||
let snapshot = self.buffer_snapshot();
|
|
||||||
snapshot.anchor_before(ranges[0].start)..snapshot.anchor_after(ranges[0].end)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Change the editor's text and selections using a string containing
|
|
||||||
/// embedded range markers that represent the ranges and directions of
|
|
||||||
/// each selection.
|
|
||||||
///
|
|
||||||
/// See the `util::test::marked_text_ranges` function for more information.
|
|
||||||
pub fn set_state(&mut self, marked_text: &str) {
|
|
||||||
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| {
|
|
||||||
s.select_ranges(selection_ranges)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Make an assertion about the editor's text and the ranges and directions
|
|
||||||
/// of its selections using a string containing embedded range markers.
|
|
||||||
///
|
|
||||||
/// See the `util::test::marked_text_ranges` function for more information.
|
|
||||||
pub fn assert_editor_state(&mut self, marked_text: &str) {
|
|
||||||
let (unmarked_text, expected_selections) = marked_text_ranges(marked_text, true);
|
|
||||||
let buffer_text = self.buffer_text();
|
|
||||||
assert_eq!(
|
|
||||||
buffer_text, unmarked_text,
|
|
||||||
"Unmarked text doesn't match buffer text"
|
|
||||||
);
|
|
||||||
self.assert_selections(expected_selections, marked_text.to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn assert_editor_background_highlights<Tag: 'static>(&mut self, marked_text: &str) {
|
|
||||||
let expected_ranges = self.ranges(marked_text);
|
|
||||||
let actual_ranges: Vec<Range<usize>> = self.update_editor(|editor, cx| {
|
|
||||||
let snapshot = editor.snapshot(cx);
|
|
||||||
editor
|
|
||||||
.background_highlights
|
|
||||||
.get(&TypeId::of::<Tag>())
|
|
||||||
.map(|h| h.1.clone())
|
|
||||||
.unwrap_or_default()
|
|
||||||
.into_iter()
|
|
||||||
.map(|range| range.to_offset(&snapshot.buffer_snapshot))
|
|
||||||
.collect()
|
|
||||||
});
|
|
||||||
assert_set_eq!(actual_ranges, expected_ranges);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn assert_editor_text_highlights<Tag: ?Sized + 'static>(&mut self, marked_text: &str) {
|
|
||||||
let expected_ranges = self.ranges(marked_text);
|
|
||||||
let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
|
|
||||||
let actual_ranges: Vec<Range<usize>> = snapshot
|
|
||||||
.highlight_ranges::<Tag>()
|
|
||||||
.map(|ranges| ranges.as_ref().clone().1)
|
|
||||||
.unwrap_or_default()
|
|
||||||
.into_iter()
|
|
||||||
.map(|range| range.to_offset(&snapshot.buffer_snapshot))
|
|
||||||
.collect();
|
|
||||||
assert_set_eq!(actual_ranges, expected_ranges);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn assert_editor_selections(&mut self, expected_selections: Vec<Range<usize>>) {
|
|
||||||
let expected_marked_text =
|
|
||||||
generate_marked_text(&self.buffer_text(), &expected_selections, true);
|
|
||||||
self.assert_selections(expected_selections, expected_marked_text)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn assert_selections(
|
|
||||||
&mut self,
|
|
||||||
expected_selections: Vec<Range<usize>>,
|
|
||||||
expected_marked_text: String,
|
|
||||||
) {
|
|
||||||
let actual_selections = self
|
|
||||||
.editor
|
|
||||||
.read_with(self.cx, |editor, cx| editor.selections.all::<usize>(cx))
|
|
||||||
.into_iter()
|
|
||||||
.map(|s| {
|
|
||||||
if s.reversed {
|
|
||||||
s.end..s.start
|
|
||||||
} else {
|
|
||||||
s.start..s.end
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
let actual_marked_text =
|
|
||||||
generate_marked_text(&self.buffer_text(), &actual_selections, true);
|
|
||||||
if expected_selections != actual_selections {
|
|
||||||
panic!(
|
|
||||||
indoc! {"
|
|
||||||
Editor has unexpected selections.
|
|
||||||
|
|
||||||
Expected selections:
|
|
||||||
{}
|
|
||||||
|
|
||||||
Actual selections:
|
|
||||||
{}
|
|
||||||
"},
|
|
||||||
expected_marked_text, actual_marked_text,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> Deref for EditorTestContext<'a> {
|
|
||||||
type Target = gpui::TestAppContext;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
self.cx
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> DerefMut for EditorTestContext<'a> {
|
|
||||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
|
||||||
&mut self.cx
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct EditorLspTestContext<'a> {
|
|
||||||
pub cx: EditorTestContext<'a>,
|
|
||||||
pub lsp: lsp::FakeLanguageServer,
|
|
||||||
pub workspace: ViewHandle<Workspace>,
|
|
||||||
pub buffer_lsp_url: lsp::Url,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> EditorLspTestContext<'a> {
|
|
||||||
pub async fn new(
|
|
||||||
mut language: Language,
|
|
||||||
capabilities: lsp::ServerCapabilities,
|
|
||||||
cx: &'a mut gpui::TestAppContext,
|
|
||||||
) -> EditorLspTestContext<'a> {
|
|
||||||
use json::json;
|
|
||||||
|
|
||||||
cx.update(|cx| {
|
|
||||||
crate::init(cx);
|
|
||||||
pane::init(cx);
|
|
||||||
});
|
|
||||||
|
|
||||||
let params = cx.update(AppState::test);
|
|
||||||
|
|
||||||
let file_name = format!(
|
|
||||||
"file.{}",
|
|
||||||
language
|
|
||||||
.path_suffixes()
|
|
||||||
.first()
|
|
||||||
.unwrap_or(&"txt".to_string())
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut fake_servers = language
|
|
||||||
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
|
|
||||||
capabilities,
|
|
||||||
..Default::default()
|
|
||||||
}))
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let project = Project::test(params.fs.clone(), [], cx).await;
|
|
||||||
project.update(cx, |project, _| project.languages().add(Arc::new(language)));
|
|
||||||
|
|
||||||
params
|
|
||||||
.fs
|
|
||||||
.as_fake()
|
|
||||||
.insert_tree("/root", json!({ "dir": { file_name: "" }}))
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let (window_id, workspace) =
|
|
||||||
cx.add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx));
|
|
||||||
project
|
|
||||||
.update(cx, |project, cx| {
|
|
||||||
project.find_or_create_local_worktree("/root", true, cx)
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone());
|
|
||||||
let item = workspace
|
|
||||||
.update(cx, |workspace, cx| workspace.open_path(file, true, cx))
|
|
||||||
.await
|
|
||||||
.expect("Could not open test file");
|
|
||||||
|
|
||||||
let editor = cx.update(|cx| {
|
|
||||||
item.act_as::<Editor>(cx)
|
|
||||||
.expect("Opened test file wasn't an editor")
|
|
||||||
});
|
|
||||||
editor.update(cx, |_, cx| cx.focus_self());
|
|
||||||
|
|
||||||
let lsp = fake_servers.next().await.unwrap();
|
|
||||||
|
|
||||||
Self {
|
|
||||||
cx: EditorTestContext {
|
|
||||||
cx,
|
|
||||||
window_id,
|
|
||||||
editor,
|
|
||||||
},
|
|
||||||
lsp,
|
|
||||||
workspace,
|
|
||||||
buffer_lsp_url: lsp::Url::from_file_path("/root/dir/file.rs").unwrap(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn new_rust(
|
|
||||||
capabilities: lsp::ServerCapabilities,
|
|
||||||
cx: &'a mut gpui::TestAppContext,
|
|
||||||
) -> EditorLspTestContext<'a> {
|
|
||||||
let language = Language::new(
|
|
||||||
LanguageConfig {
|
|
||||||
name: "Rust".into(),
|
|
||||||
path_suffixes: vec!["rs".to_string()],
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
Some(tree_sitter_rust::language()),
|
|
||||||
);
|
|
||||||
|
|
||||||
Self::new(language, capabilities, cx).await
|
|
||||||
}
|
|
||||||
|
|
||||||
// Constructs lsp range using a marked string with '[', ']' range delimiters
|
|
||||||
pub fn lsp_range(&mut self, marked_text: &str) -> lsp::Range {
|
|
||||||
let ranges = self.ranges(marked_text);
|
|
||||||
self.to_lsp_range(ranges[0].clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_lsp_range(&mut self, range: Range<usize>) -> lsp::Range {
|
|
||||||
let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
|
|
||||||
let start_point = range.start.to_point(&snapshot.buffer_snapshot);
|
|
||||||
let end_point = range.end.to_point(&snapshot.buffer_snapshot);
|
|
||||||
|
|
||||||
self.editor(|editor, cx| {
|
|
||||||
let buffer = editor.buffer().read(cx);
|
|
||||||
let start = point_to_lsp(
|
|
||||||
buffer
|
|
||||||
.point_to_buffer_offset(start_point, cx)
|
|
||||||
.unwrap()
|
|
||||||
.1
|
|
||||||
.to_point_utf16(&buffer.read(cx)),
|
|
||||||
);
|
|
||||||
let end = point_to_lsp(
|
|
||||||
buffer
|
|
||||||
.point_to_buffer_offset(end_point, cx)
|
|
||||||
.unwrap()
|
|
||||||
.1
|
|
||||||
.to_point_utf16(&buffer.read(cx)),
|
|
||||||
);
|
|
||||||
|
|
||||||
lsp::Range { start, end }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_lsp(&mut self, offset: usize) -> lsp::Position {
|
|
||||||
let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
|
|
||||||
let point = offset.to_point(&snapshot.buffer_snapshot);
|
|
||||||
|
|
||||||
self.editor(|editor, cx| {
|
|
||||||
let buffer = editor.buffer().read(cx);
|
|
||||||
point_to_lsp(
|
|
||||||
buffer
|
|
||||||
.point_to_buffer_offset(point, cx)
|
|
||||||
.unwrap()
|
|
||||||
.1
|
|
||||||
.to_point_utf16(&buffer.read(cx)),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_workspace<F, T>(&mut self, update: F) -> T
|
|
||||||
where
|
|
||||||
F: FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
|
|
||||||
{
|
|
||||||
self.workspace.update(self.cx.cx, update)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn handle_request<T, F, Fut>(
|
|
||||||
&self,
|
|
||||||
mut handler: F,
|
|
||||||
) -> futures::channel::mpsc::UnboundedReceiver<()>
|
|
||||||
where
|
|
||||||
T: 'static + request::Request,
|
|
||||||
T::Params: 'static + Send,
|
|
||||||
F: 'static + Send + FnMut(lsp::Url, T::Params, gpui::AsyncAppContext) -> Fut,
|
|
||||||
Fut: 'static + Send + Future<Output = Result<T::Result>>,
|
|
||||||
{
|
|
||||||
let url = self.buffer_lsp_url.clone();
|
|
||||||
self.lsp.handle_request::<T, _, _>(move |params, cx| {
|
|
||||||
let url = url.clone();
|
|
||||||
handler(url, params, cx)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn notify<T: notification::Notification>(&self, params: T::Params) {
|
|
||||||
self.lsp.notify::<T>(params);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> Deref for EditorLspTestContext<'a> {
|
|
||||||
type Target = EditorTestContext<'a>;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.cx
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> DerefMut for EditorLspTestContext<'a> {
|
|
||||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
|
||||||
&mut self.cx
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
208
crates/editor/src/test/editor_lsp_test_context.rs
Normal file
208
crates/editor/src/test/editor_lsp_test_context.rs
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
use std::{
|
||||||
|
ops::{Deref, DerefMut, Range},
|
||||||
|
sync::Arc,
|
||||||
|
};
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
|
||||||
|
use futures::Future;
|
||||||
|
use gpui::{json, ViewContext, ViewHandle};
|
||||||
|
use language::{point_to_lsp, FakeLspAdapter, Language, LanguageConfig};
|
||||||
|
use lsp::{notification, request};
|
||||||
|
use project::Project;
|
||||||
|
use smol::stream::StreamExt;
|
||||||
|
use workspace::{pane, AppState, Workspace, WorkspaceHandle};
|
||||||
|
|
||||||
|
use crate::{multi_buffer::ToPointUtf16, Editor, ToPoint};
|
||||||
|
|
||||||
|
use super::editor_test_context::EditorTestContext;
|
||||||
|
|
||||||
|
pub struct EditorLspTestContext<'a> {
|
||||||
|
pub cx: EditorTestContext<'a>,
|
||||||
|
pub lsp: lsp::FakeLanguageServer,
|
||||||
|
pub workspace: ViewHandle<Workspace>,
|
||||||
|
pub buffer_lsp_url: lsp::Url,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> EditorLspTestContext<'a> {
|
||||||
|
pub async fn new(
|
||||||
|
mut language: Language,
|
||||||
|
capabilities: lsp::ServerCapabilities,
|
||||||
|
cx: &'a mut gpui::TestAppContext,
|
||||||
|
) -> EditorLspTestContext<'a> {
|
||||||
|
use json::json;
|
||||||
|
|
||||||
|
cx.update(|cx| {
|
||||||
|
crate::init(cx);
|
||||||
|
pane::init(cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
let params = cx.update(AppState::test);
|
||||||
|
|
||||||
|
let file_name = format!(
|
||||||
|
"file.{}",
|
||||||
|
language
|
||||||
|
.path_suffixes()
|
||||||
|
.first()
|
||||||
|
.unwrap_or(&"txt".to_string())
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut fake_servers = language
|
||||||
|
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
|
||||||
|
capabilities,
|
||||||
|
..Default::default()
|
||||||
|
}))
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let project = Project::test(params.fs.clone(), [], cx).await;
|
||||||
|
project.update(cx, |project, _| project.languages().add(Arc::new(language)));
|
||||||
|
|
||||||
|
params
|
||||||
|
.fs
|
||||||
|
.as_fake()
|
||||||
|
.insert_tree("/root", json!({ "dir": { file_name: "" }}))
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let (window_id, workspace) =
|
||||||
|
cx.add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx));
|
||||||
|
project
|
||||||
|
.update(cx, |project, cx| {
|
||||||
|
project.find_or_create_local_worktree("/root", true, cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone());
|
||||||
|
let item = workspace
|
||||||
|
.update(cx, |workspace, cx| workspace.open_path(file, true, cx))
|
||||||
|
.await
|
||||||
|
.expect("Could not open test file");
|
||||||
|
|
||||||
|
let editor = cx.update(|cx| {
|
||||||
|
item.act_as::<Editor>(cx)
|
||||||
|
.expect("Opened test file wasn't an editor")
|
||||||
|
});
|
||||||
|
editor.update(cx, |_, cx| cx.focus_self());
|
||||||
|
|
||||||
|
let lsp = fake_servers.next().await.unwrap();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
cx: EditorTestContext {
|
||||||
|
cx,
|
||||||
|
window_id,
|
||||||
|
editor,
|
||||||
|
},
|
||||||
|
lsp,
|
||||||
|
workspace,
|
||||||
|
buffer_lsp_url: lsp::Url::from_file_path("/root/dir/file.rs").unwrap(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn new_rust(
|
||||||
|
capabilities: lsp::ServerCapabilities,
|
||||||
|
cx: &'a mut gpui::TestAppContext,
|
||||||
|
) -> EditorLspTestContext<'a> {
|
||||||
|
let language = Language::new(
|
||||||
|
LanguageConfig {
|
||||||
|
name: "Rust".into(),
|
||||||
|
path_suffixes: vec!["rs".to_string()],
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
Some(tree_sitter_rust::language()),
|
||||||
|
);
|
||||||
|
|
||||||
|
Self::new(language, capabilities, cx).await
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constructs lsp range using a marked string with '[', ']' range delimiters
|
||||||
|
pub fn lsp_range(&mut self, marked_text: &str) -> lsp::Range {
|
||||||
|
let ranges = self.ranges(marked_text);
|
||||||
|
self.to_lsp_range(ranges[0].clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_lsp_range(&mut self, range: Range<usize>) -> lsp::Range {
|
||||||
|
let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
|
||||||
|
let start_point = range.start.to_point(&snapshot.buffer_snapshot);
|
||||||
|
let end_point = range.end.to_point(&snapshot.buffer_snapshot);
|
||||||
|
|
||||||
|
self.editor(|editor, cx| {
|
||||||
|
let buffer = editor.buffer().read(cx);
|
||||||
|
let start = point_to_lsp(
|
||||||
|
buffer
|
||||||
|
.point_to_buffer_offset(start_point, cx)
|
||||||
|
.unwrap()
|
||||||
|
.1
|
||||||
|
.to_point_utf16(&buffer.read(cx)),
|
||||||
|
);
|
||||||
|
let end = point_to_lsp(
|
||||||
|
buffer
|
||||||
|
.point_to_buffer_offset(end_point, cx)
|
||||||
|
.unwrap()
|
||||||
|
.1
|
||||||
|
.to_point_utf16(&buffer.read(cx)),
|
||||||
|
);
|
||||||
|
|
||||||
|
lsp::Range { start, end }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_lsp(&mut self, offset: usize) -> lsp::Position {
|
||||||
|
let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
|
||||||
|
let point = offset.to_point(&snapshot.buffer_snapshot);
|
||||||
|
|
||||||
|
self.editor(|editor, cx| {
|
||||||
|
let buffer = editor.buffer().read(cx);
|
||||||
|
point_to_lsp(
|
||||||
|
buffer
|
||||||
|
.point_to_buffer_offset(point, cx)
|
||||||
|
.unwrap()
|
||||||
|
.1
|
||||||
|
.to_point_utf16(&buffer.read(cx)),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_workspace<F, T>(&mut self, update: F) -> T
|
||||||
|
where
|
||||||
|
F: FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
|
||||||
|
{
|
||||||
|
self.workspace.update(self.cx.cx, update)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_request<T, F, Fut>(
|
||||||
|
&self,
|
||||||
|
mut handler: F,
|
||||||
|
) -> futures::channel::mpsc::UnboundedReceiver<()>
|
||||||
|
where
|
||||||
|
T: 'static + request::Request,
|
||||||
|
T::Params: 'static + Send,
|
||||||
|
F: 'static + Send + FnMut(lsp::Url, T::Params, gpui::AsyncAppContext) -> Fut,
|
||||||
|
Fut: 'static + Send + Future<Output = Result<T::Result>>,
|
||||||
|
{
|
||||||
|
let url = self.buffer_lsp_url.clone();
|
||||||
|
self.lsp.handle_request::<T, _, _>(move |params, cx| {
|
||||||
|
let url = url.clone();
|
||||||
|
handler(url, params, cx)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn notify<T: notification::Notification>(&self, params: T::Params) {
|
||||||
|
self.lsp.notify::<T>(params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Deref for EditorLspTestContext<'a> {
|
||||||
|
type Target = EditorTestContext<'a>;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.cx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> DerefMut for EditorLspTestContext<'a> {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
&mut self.cx
|
||||||
|
}
|
||||||
|
}
|
273
crates/editor/src/test/editor_test_context.rs
Normal file
273
crates/editor/src/test/editor_test_context.rs
Normal file
@ -0,0 +1,273 @@
|
|||||||
|
use std::{
|
||||||
|
any::TypeId,
|
||||||
|
ops::{Deref, DerefMut, Range},
|
||||||
|
};
|
||||||
|
|
||||||
|
use futures::Future;
|
||||||
|
use indoc::indoc;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
display_map::ToDisplayPoint, AnchorRangeExt, Autoscroll, DisplayPoint, Editor, MultiBuffer,
|
||||||
|
};
|
||||||
|
use gpui::{keymap::Keystroke, AppContext, ContextHandle, ModelContext, ViewContext, ViewHandle};
|
||||||
|
use language::{Buffer, BufferSnapshot};
|
||||||
|
use settings::Settings;
|
||||||
|
use util::{
|
||||||
|
assert_set_eq,
|
||||||
|
test::{generate_marked_text, marked_text_ranges},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::build_editor;
|
||||||
|
|
||||||
|
pub struct EditorTestContext<'a> {
|
||||||
|
pub cx: &'a mut gpui::TestAppContext,
|
||||||
|
pub window_id: usize,
|
||||||
|
pub editor: ViewHandle<Editor>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> EditorTestContext<'a> {
|
||||||
|
pub fn new(cx: &'a mut gpui::TestAppContext) -> EditorTestContext<'a> {
|
||||||
|
let (window_id, editor) = cx.update(|cx| {
|
||||||
|
cx.set_global(Settings::test(cx));
|
||||||
|
crate::init(cx);
|
||||||
|
|
||||||
|
let (window_id, editor) = cx.add_window(Default::default(), |cx| {
|
||||||
|
build_editor(MultiBuffer::build_simple("", cx), cx)
|
||||||
|
});
|
||||||
|
|
||||||
|
editor.update(cx, |_, cx| cx.focus_self());
|
||||||
|
|
||||||
|
(window_id, editor)
|
||||||
|
});
|
||||||
|
|
||||||
|
Self {
|
||||||
|
cx,
|
||||||
|
window_id,
|
||||||
|
editor,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn condition(
|
||||||
|
&self,
|
||||||
|
predicate: impl FnMut(&Editor, &AppContext) -> bool,
|
||||||
|
) -> impl Future<Output = ()> {
|
||||||
|
self.editor.condition(self.cx, predicate)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn editor<F, T>(&self, read: F) -> T
|
||||||
|
where
|
||||||
|
F: FnOnce(&Editor, &AppContext) -> T,
|
||||||
|
{
|
||||||
|
self.editor.read_with(self.cx, read)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_editor<F, T>(&mut self, update: F) -> T
|
||||||
|
where
|
||||||
|
F: FnOnce(&mut Editor, &mut ViewContext<Editor>) -> T,
|
||||||
|
{
|
||||||
|
self.editor.update(self.cx, update)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn multibuffer<F, T>(&self, read: F) -> T
|
||||||
|
where
|
||||||
|
F: FnOnce(&MultiBuffer, &AppContext) -> T,
|
||||||
|
{
|
||||||
|
self.editor(|editor, cx| read(editor.buffer().read(cx), cx))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_multibuffer<F, T>(&mut self, update: F) -> T
|
||||||
|
where
|
||||||
|
F: FnOnce(&mut MultiBuffer, &mut ModelContext<MultiBuffer>) -> T,
|
||||||
|
{
|
||||||
|
self.update_editor(|editor, cx| editor.buffer().update(cx, update))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn buffer_text(&self) -> String {
|
||||||
|
self.multibuffer(|buffer, cx| buffer.snapshot(cx).text())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn buffer<F, T>(&self, read: F) -> T
|
||||||
|
where
|
||||||
|
F: FnOnce(&Buffer, &AppContext) -> T,
|
||||||
|
{
|
||||||
|
self.multibuffer(|multibuffer, cx| {
|
||||||
|
let buffer = multibuffer.as_singleton().unwrap().read(cx);
|
||||||
|
read(buffer, cx)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_buffer<F, T>(&mut self, update: F) -> T
|
||||||
|
where
|
||||||
|
F: FnOnce(&mut Buffer, &mut ModelContext<Buffer>) -> T,
|
||||||
|
{
|
||||||
|
self.update_multibuffer(|multibuffer, cx| {
|
||||||
|
let buffer = multibuffer.as_singleton().unwrap();
|
||||||
|
buffer.update(cx, update)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn buffer_snapshot(&self) -> BufferSnapshot {
|
||||||
|
self.buffer(|buffer, _| buffer.snapshot())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn simulate_keystroke(&mut self, keystroke_text: &str) -> ContextHandle {
|
||||||
|
let keystroke_under_test_handle =
|
||||||
|
self.add_assertion_context(format!("Simulated Keystroke: {:?}", keystroke_text));
|
||||||
|
let keystroke = Keystroke::parse(keystroke_text).unwrap();
|
||||||
|
self.cx.dispatch_keystroke(self.window_id, keystroke, false);
|
||||||
|
keystroke_under_test_handle
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn simulate_keystrokes<const COUNT: usize>(
|
||||||
|
&mut self,
|
||||||
|
keystroke_texts: [&str; COUNT],
|
||||||
|
) -> ContextHandle {
|
||||||
|
let keystrokes_under_test_handle =
|
||||||
|
self.add_assertion_context(format!("Simulated Keystrokes: {:?}", keystroke_texts));
|
||||||
|
for keystroke_text in keystroke_texts.into_iter() {
|
||||||
|
self.simulate_keystroke(keystroke_text);
|
||||||
|
}
|
||||||
|
keystrokes_under_test_handle
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ranges(&self, marked_text: &str) -> Vec<Range<usize>> {
|
||||||
|
let (unmarked_text, ranges) = marked_text_ranges(marked_text, false);
|
||||||
|
assert_eq!(self.buffer_text(), unmarked_text);
|
||||||
|
ranges
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn display_point(&mut self, marked_text: &str) -> DisplayPoint {
|
||||||
|
let ranges = self.ranges(marked_text);
|
||||||
|
let snapshot = self
|
||||||
|
.editor
|
||||||
|
.update(self.cx, |editor, cx| editor.snapshot(cx));
|
||||||
|
ranges[0].start.to_display_point(&snapshot)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns anchors for the current buffer using `«` and `»`
|
||||||
|
pub fn text_anchor_range(&self, marked_text: &str) -> Range<language::Anchor> {
|
||||||
|
let ranges = self.ranges(marked_text);
|
||||||
|
let snapshot = self.buffer_snapshot();
|
||||||
|
snapshot.anchor_before(ranges[0].start)..snapshot.anchor_after(ranges[0].end)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Change the editor's text and selections using a string containing
|
||||||
|
/// embedded range markers that represent the ranges and directions of
|
||||||
|
/// each selection.
|
||||||
|
///
|
||||||
|
/// See the `util::test::marked_text_ranges` function for more information.
|
||||||
|
pub fn set_state(&mut self, marked_text: &str) -> ContextHandle {
|
||||||
|
let _state_context = self.add_assertion_context(format!(
|
||||||
|
"Editor State: \"{}\"",
|
||||||
|
marked_text.escape_debug().to_string()
|
||||||
|
));
|
||||||
|
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| {
|
||||||
|
s.select_ranges(selection_ranges)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
_state_context
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Make an assertion about the editor's text and the ranges and directions
|
||||||
|
/// of its selections using a string containing embedded range markers.
|
||||||
|
///
|
||||||
|
/// See the `util::test::marked_text_ranges` function for more information.
|
||||||
|
pub fn assert_editor_state(&mut self, marked_text: &str) {
|
||||||
|
let (unmarked_text, expected_selections) = marked_text_ranges(marked_text, true);
|
||||||
|
let buffer_text = self.buffer_text();
|
||||||
|
assert_eq!(
|
||||||
|
buffer_text, unmarked_text,
|
||||||
|
"Unmarked text doesn't match buffer text"
|
||||||
|
);
|
||||||
|
self.assert_selections(expected_selections, marked_text.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn assert_editor_background_highlights<Tag: 'static>(&mut self, marked_text: &str) {
|
||||||
|
let expected_ranges = self.ranges(marked_text);
|
||||||
|
let actual_ranges: Vec<Range<usize>> = self.update_editor(|editor, cx| {
|
||||||
|
let snapshot = editor.snapshot(cx);
|
||||||
|
editor
|
||||||
|
.background_highlights
|
||||||
|
.get(&TypeId::of::<Tag>())
|
||||||
|
.map(|h| h.1.clone())
|
||||||
|
.unwrap_or_default()
|
||||||
|
.into_iter()
|
||||||
|
.map(|range| range.to_offset(&snapshot.buffer_snapshot))
|
||||||
|
.collect()
|
||||||
|
});
|
||||||
|
assert_set_eq!(actual_ranges, expected_ranges);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn assert_editor_text_highlights<Tag: ?Sized + 'static>(&mut self, marked_text: &str) {
|
||||||
|
let expected_ranges = self.ranges(marked_text);
|
||||||
|
let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
|
||||||
|
let actual_ranges: Vec<Range<usize>> = snapshot
|
||||||
|
.highlight_ranges::<Tag>()
|
||||||
|
.map(|ranges| ranges.as_ref().clone().1)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.into_iter()
|
||||||
|
.map(|range| range.to_offset(&snapshot.buffer_snapshot))
|
||||||
|
.collect();
|
||||||
|
assert_set_eq!(actual_ranges, expected_ranges);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn assert_editor_selections(&mut self, expected_selections: Vec<Range<usize>>) {
|
||||||
|
let expected_marked_text =
|
||||||
|
generate_marked_text(&self.buffer_text(), &expected_selections, true);
|
||||||
|
self.assert_selections(expected_selections, expected_marked_text)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assert_selections(
|
||||||
|
&mut self,
|
||||||
|
expected_selections: Vec<Range<usize>>,
|
||||||
|
expected_marked_text: String,
|
||||||
|
) {
|
||||||
|
let actual_selections = self
|
||||||
|
.editor
|
||||||
|
.read_with(self.cx, |editor, cx| editor.selections.all::<usize>(cx))
|
||||||
|
.into_iter()
|
||||||
|
.map(|s| {
|
||||||
|
if s.reversed {
|
||||||
|
s.end..s.start
|
||||||
|
} else {
|
||||||
|
s.start..s.end
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let actual_marked_text =
|
||||||
|
generate_marked_text(&self.buffer_text(), &actual_selections, true);
|
||||||
|
if expected_selections != actual_selections {
|
||||||
|
panic!(
|
||||||
|
indoc! {"
|
||||||
|
{}Editor has unexpected selections.
|
||||||
|
|
||||||
|
Expected selections:
|
||||||
|
{}
|
||||||
|
|
||||||
|
Actual selections:
|
||||||
|
{}
|
||||||
|
"},
|
||||||
|
self.assertion_context(),
|
||||||
|
expected_marked_text,
|
||||||
|
actual_marked_text,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Deref for EditorTestContext<'a> {
|
||||||
|
type Target = gpui::TestAppContext;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
self.cx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> DerefMut for EditorTestContext<'a> {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
&mut self.cx
|
||||||
|
}
|
||||||
|
}
|
@ -49,8 +49,8 @@ impl View for FileFinder {
|
|||||||
"FileFinder"
|
"FileFinder"
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
|
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||||
ChildView::new(self.picker.clone()).boxed()
|
ChildView::new(self.picker.clone(), cx).boxed()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
fn on_focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||||
@ -251,7 +251,7 @@ impl PickerDelegate for FileFinder {
|
|||||||
fn render_match(
|
fn render_match(
|
||||||
&self,
|
&self,
|
||||||
ix: usize,
|
ix: usize,
|
||||||
mouse_state: MouseState,
|
mouse_state: &mut MouseState,
|
||||||
selected: bool,
|
selected: bool,
|
||||||
cx: &AppContext,
|
cx: &AppContext,
|
||||||
) -> ElementBox {
|
) -> ElementBox {
|
||||||
|
31
crates/fs/Cargo.toml
Normal file
31
crates/fs/Cargo.toml
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
[package]
|
||||||
|
name = "fs"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "src/fs.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
collections = { path = "../collections" }
|
||||||
|
gpui = { path = "../gpui" }
|
||||||
|
lsp = { path = "../lsp" }
|
||||||
|
rope = { path = "../rope" }
|
||||||
|
util = { path = "../util" }
|
||||||
|
anyhow = "1.0.57"
|
||||||
|
async-trait = "0.1"
|
||||||
|
futures = "0.3"
|
||||||
|
tempfile = "3"
|
||||||
|
fsevent = { path = "../fsevent" }
|
||||||
|
lazy_static = "1.4.0"
|
||||||
|
parking_lot = "0.11.1"
|
||||||
|
smol = "1.2.5"
|
||||||
|
regex = "1.5"
|
||||||
|
git2 = { version = "0.15", default-features = false }
|
||||||
|
serde = { workspace = true }
|
||||||
|
serde_json = { workspace = true }
|
||||||
|
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
|
||||||
|
libc = "0.2"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
test-support = []
|
@ -1,8 +1,19 @@
|
|||||||
|
pub mod repository;
|
||||||
|
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use fsevent::EventStream;
|
use fsevent::EventStream;
|
||||||
use futures::{future::BoxFuture, Stream, StreamExt};
|
use futures::{future::BoxFuture, Stream, StreamExt};
|
||||||
use language::LineEnding;
|
use git2::Repository as LibGitRepository;
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use parking_lot::Mutex as SyncMutex;
|
||||||
|
use regex::Regex;
|
||||||
|
use repository::GitRepository;
|
||||||
|
use rope::Rope;
|
||||||
use smol::io::{AsyncReadExt, AsyncWriteExt};
|
use smol::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use std::cmp;
|
||||||
|
use std::io::Write;
|
||||||
|
use std::sync::Arc;
|
||||||
use std::{
|
use std::{
|
||||||
io,
|
io,
|
||||||
os::unix::fs::MetadataExt,
|
os::unix::fs::MetadataExt,
|
||||||
@ -10,15 +21,77 @@ use std::{
|
|||||||
pin::Pin,
|
pin::Pin,
|
||||||
time::{Duration, SystemTime},
|
time::{Duration, SystemTime},
|
||||||
};
|
};
|
||||||
use text::Rope;
|
use tempfile::NamedTempFile;
|
||||||
|
use util::ResultExt;
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
use collections::{btree_map, BTreeMap};
|
use collections::{btree_map, BTreeMap};
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
use futures::lock::Mutex;
|
use futures::lock::Mutex;
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
use std::sync::{Arc, Weak};
|
use repository::FakeGitRepositoryState;
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
use std::sync::Weak;
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref CARRIAGE_RETURNS_REGEX: Regex = Regex::new("\r\n|\r").unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub enum LineEnding {
|
||||||
|
Unix,
|
||||||
|
Windows,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for LineEnding {
|
||||||
|
fn default() -> Self {
|
||||||
|
#[cfg(unix)]
|
||||||
|
return Self::Unix;
|
||||||
|
|
||||||
|
#[cfg(not(unix))]
|
||||||
|
return Self::CRLF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LineEnding {
|
||||||
|
pub fn as_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
LineEnding::Unix => "\n",
|
||||||
|
LineEnding::Windows => "\r\n",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn detect(text: &str) -> Self {
|
||||||
|
let mut max_ix = cmp::min(text.len(), 1000);
|
||||||
|
while !text.is_char_boundary(max_ix) {
|
||||||
|
max_ix -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ix) = text[..max_ix].find(&['\n']) {
|
||||||
|
if ix > 0 && text.as_bytes()[ix - 1] == b'\r' {
|
||||||
|
Self::Windows
|
||||||
|
} else {
|
||||||
|
Self::Unix
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn normalize(text: &mut String) {
|
||||||
|
if let Cow::Owned(replaced) = CARRIAGE_RETURNS_REGEX.replace_all(text, "\n") {
|
||||||
|
*text = replaced;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn normalize_arc(text: Arc<str>) -> Arc<str> {
|
||||||
|
if let Cow::Owned(replaced) = CARRIAGE_RETURNS_REGEX.replace_all(&text, "\n") {
|
||||||
|
replaced.into()
|
||||||
|
} else {
|
||||||
|
text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
pub trait Fs: Send + Sync {
|
pub trait Fs: Send + Sync {
|
||||||
async fn create_dir(&self, path: &Path) -> Result<()>;
|
async fn create_dir(&self, path: &Path) -> Result<()>;
|
||||||
@ -29,6 +102,7 @@ pub trait Fs: Send + Sync {
|
|||||||
async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()>;
|
async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()>;
|
||||||
async fn open_sync(&self, path: &Path) -> Result<Box<dyn io::Read>>;
|
async fn open_sync(&self, path: &Path) -> Result<Box<dyn io::Read>>;
|
||||||
async fn load(&self, path: &Path) -> Result<String>;
|
async fn load(&self, path: &Path) -> Result<String>;
|
||||||
|
async fn atomic_write(&self, path: PathBuf, text: String) -> Result<()>;
|
||||||
async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()>;
|
async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()>;
|
||||||
async fn canonicalize(&self, path: &Path) -> Result<PathBuf>;
|
async fn canonicalize(&self, path: &Path) -> Result<PathBuf>;
|
||||||
async fn is_file(&self, path: &Path) -> bool;
|
async fn is_file(&self, path: &Path) -> bool;
|
||||||
@ -42,6 +116,7 @@ pub trait Fs: Send + Sync {
|
|||||||
path: &Path,
|
path: &Path,
|
||||||
latency: Duration,
|
latency: Duration,
|
||||||
) -> Pin<Box<dyn Send + Stream<Item = Vec<fsevent::Event>>>>;
|
) -> Pin<Box<dyn Send + Stream<Item = Vec<fsevent::Event>>>>;
|
||||||
|
fn open_repo(&self, abs_dot_git: &Path) -> Option<Arc<SyncMutex<dyn GitRepository>>>;
|
||||||
fn is_fake(&self) -> bool;
|
fn is_fake(&self) -> bool;
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
fn as_fake(&self) -> &FakeFs;
|
fn as_fake(&self) -> &FakeFs;
|
||||||
@ -79,6 +154,33 @@ pub struct Metadata {
|
|||||||
pub is_dir: bool,
|
pub is_dir: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<lsp::CreateFileOptions> for CreateOptions {
|
||||||
|
fn from(options: lsp::CreateFileOptions) -> Self {
|
||||||
|
Self {
|
||||||
|
overwrite: options.overwrite.unwrap_or(false),
|
||||||
|
ignore_if_exists: options.ignore_if_exists.unwrap_or(false),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<lsp::RenameFileOptions> for RenameOptions {
|
||||||
|
fn from(options: lsp::RenameFileOptions) -> Self {
|
||||||
|
Self {
|
||||||
|
overwrite: options.overwrite.unwrap_or(false),
|
||||||
|
ignore_if_exists: options.ignore_if_exists.unwrap_or(false),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<lsp::DeleteFileOptions> for RemoveOptions {
|
||||||
|
fn from(options: lsp::DeleteFileOptions) -> Self {
|
||||||
|
Self {
|
||||||
|
recursive: options.recursive.unwrap_or(false),
|
||||||
|
ignore_if_not_exists: options.ignore_if_not_exists.unwrap_or(false),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct RealFs;
|
pub struct RealFs;
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
@ -161,6 +263,18 @@ impl Fs for RealFs {
|
|||||||
Ok(text)
|
Ok(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn atomic_write(&self, path: PathBuf, data: String) -> Result<()> {
|
||||||
|
smol::unblock(move || {
|
||||||
|
let mut tmp_file = NamedTempFile::new()?;
|
||||||
|
tmp_file.write_all(data.as_bytes())?;
|
||||||
|
tmp_file.persist(path)?;
|
||||||
|
Ok::<(), anyhow::Error>(())
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()> {
|
async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()> {
|
||||||
let buffer_size = text.summary().len.min(10 * 1024);
|
let buffer_size = text.summary().len.min(10 * 1024);
|
||||||
let file = smol::fs::File::create(path).await?;
|
let file = smol::fs::File::create(path).await?;
|
||||||
@ -235,6 +349,14 @@ impl Fs for RealFs {
|
|||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn open_repo(&self, dotgit_path: &Path) -> Option<Arc<SyncMutex<dyn GitRepository>>> {
|
||||||
|
LibGitRepository::open(&dotgit_path)
|
||||||
|
.log_err()
|
||||||
|
.and_then::<Arc<SyncMutex<dyn GitRepository>>, _>(|libgit_repository| {
|
||||||
|
Some(Arc::new(SyncMutex::new(libgit_repository)))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn is_fake(&self) -> bool {
|
fn is_fake(&self) -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
@ -270,6 +392,7 @@ enum FakeFsEntry {
|
|||||||
inode: u64,
|
inode: u64,
|
||||||
mtime: SystemTime,
|
mtime: SystemTime,
|
||||||
entries: BTreeMap<String, Arc<Mutex<FakeFsEntry>>>,
|
entries: BTreeMap<String, Arc<Mutex<FakeFsEntry>>>,
|
||||||
|
git_repo_state: Option<Arc<SyncMutex<repository::FakeGitRepositoryState>>>,
|
||||||
},
|
},
|
||||||
Symlink {
|
Symlink {
|
||||||
target: PathBuf,
|
target: PathBuf,
|
||||||
@ -384,6 +507,7 @@ impl FakeFs {
|
|||||||
inode: 0,
|
inode: 0,
|
||||||
mtime: SystemTime::now(),
|
mtime: SystemTime::now(),
|
||||||
entries: Default::default(),
|
entries: Default::default(),
|
||||||
|
git_repo_state: None,
|
||||||
})),
|
})),
|
||||||
next_inode: 1,
|
next_inode: 1,
|
||||||
event_txs: Default::default(),
|
event_txs: Default::default(),
|
||||||
@ -473,6 +597,28 @@ impl FakeFs {
|
|||||||
.boxed()
|
.boxed()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn set_index_for_repo(&self, dot_git: &Path, head_state: &[(&Path, String)]) {
|
||||||
|
let mut state = self.state.lock().await;
|
||||||
|
let entry = state.read_path(dot_git).await.unwrap();
|
||||||
|
let mut entry = entry.lock().await;
|
||||||
|
|
||||||
|
if let FakeFsEntry::Dir { git_repo_state, .. } = &mut *entry {
|
||||||
|
let repo_state = git_repo_state.get_or_insert_with(Default::default);
|
||||||
|
let mut repo_state = repo_state.lock();
|
||||||
|
|
||||||
|
repo_state.index_contents.clear();
|
||||||
|
repo_state.index_contents.extend(
|
||||||
|
head_state
|
||||||
|
.iter()
|
||||||
|
.map(|(path, content)| (path.to_path_buf(), content.clone())),
|
||||||
|
);
|
||||||
|
|
||||||
|
state.emit_event([dot_git]);
|
||||||
|
} else {
|
||||||
|
panic!("not a directory");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn files(&self) -> Vec<PathBuf> {
|
pub async fn files(&self) -> Vec<PathBuf> {
|
||||||
let mut result = Vec::new();
|
let mut result = Vec::new();
|
||||||
let mut queue = collections::VecDeque::new();
|
let mut queue = collections::VecDeque::new();
|
||||||
@ -562,6 +708,7 @@ impl Fs for FakeFs {
|
|||||||
inode,
|
inode,
|
||||||
mtime: SystemTime::now(),
|
mtime: SystemTime::now(),
|
||||||
entries: Default::default(),
|
entries: Default::default(),
|
||||||
|
git_repo_state: None,
|
||||||
}))
|
}))
|
||||||
});
|
});
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -748,6 +895,14 @@ impl Fs for FakeFs {
|
|||||||
entry.file_content(&path).cloned()
|
entry.file_content(&path).cloned()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn atomic_write(&self, path: PathBuf, data: String) -> Result<()> {
|
||||||
|
self.simulate_random_delay().await;
|
||||||
|
let path = normalize_path(path.as_path());
|
||||||
|
self.insert_file(path, data.to_string()).await;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()> {
|
async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()> {
|
||||||
self.simulate_random_delay().await;
|
self.simulate_random_delay().await;
|
||||||
let path = normalize_path(path);
|
let path = normalize_path(path);
|
||||||
@ -846,6 +1001,24 @@ impl Fs for FakeFs {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn open_repo(&self, abs_dot_git: &Path) -> Option<Arc<SyncMutex<dyn GitRepository>>> {
|
||||||
|
smol::block_on(async move {
|
||||||
|
let state = self.state.lock().await;
|
||||||
|
let entry = state.read_path(abs_dot_git).await.unwrap();
|
||||||
|
let mut entry = entry.lock().await;
|
||||||
|
if let FakeFsEntry::Dir { git_repo_state, .. } = &mut *entry {
|
||||||
|
let state = git_repo_state
|
||||||
|
.get_or_insert_with(|| {
|
||||||
|
Arc::new(SyncMutex::new(FakeGitRepositoryState::default()))
|
||||||
|
})
|
||||||
|
.clone();
|
||||||
|
Some(repository::FakeGitRepository::open(state))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn is_fake(&self) -> bool {
|
fn is_fake(&self) -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
71
crates/fs/src/repository.rs
Normal file
71
crates/fs/src/repository.rs
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use collections::HashMap;
|
||||||
|
use parking_lot::Mutex;
|
||||||
|
use std::{
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
sync::Arc,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub use git2::Repository as LibGitRepository;
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
pub trait GitRepository: Send {
|
||||||
|
fn reload_index(&self);
|
||||||
|
|
||||||
|
fn load_index_text(&self, relative_file_path: &Path) -> Option<String>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl GitRepository for LibGitRepository {
|
||||||
|
fn reload_index(&self) {
|
||||||
|
if let Ok(mut index) = self.index() {
|
||||||
|
_ = index.read(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_index_text(&self, relative_file_path: &Path) -> Option<String> {
|
||||||
|
fn logic(repo: &LibGitRepository, relative_file_path: &Path) -> Result<Option<String>> {
|
||||||
|
const STAGE_NORMAL: i32 = 0;
|
||||||
|
let index = repo.index()?;
|
||||||
|
let oid = match index.get_path(relative_file_path, STAGE_NORMAL) {
|
||||||
|
Some(entry) => entry.id,
|
||||||
|
None => return Ok(None),
|
||||||
|
};
|
||||||
|
|
||||||
|
let content = repo.find_blob(oid)?.content().to_owned();
|
||||||
|
Ok(Some(String::from_utf8(content)?))
|
||||||
|
}
|
||||||
|
|
||||||
|
match logic(&self, relative_file_path) {
|
||||||
|
Ok(value) => return value,
|
||||||
|
Err(err) => log::error!("Error loading head text: {:?}", err),
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct FakeGitRepository {
|
||||||
|
state: Arc<Mutex<FakeGitRepositoryState>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct FakeGitRepositoryState {
|
||||||
|
pub index_contents: HashMap<PathBuf, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FakeGitRepository {
|
||||||
|
pub fn open(state: Arc<Mutex<FakeGitRepositoryState>>) -> Arc<Mutex<dyn GitRepository>> {
|
||||||
|
Arc::new(Mutex::new(FakeGitRepository { state }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl GitRepository for FakeGitRepository {
|
||||||
|
fn reload_index(&self) {}
|
||||||
|
|
||||||
|
fn load_index_text(&self, path: &Path) -> Option<String> {
|
||||||
|
let state = self.state.lock();
|
||||||
|
state.index_contents.get(path).cloned()
|
||||||
|
}
|
||||||
|
}
|
28
crates/git/Cargo.toml
Normal file
28
crates/git/Cargo.toml
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
[package]
|
||||||
|
name = "git"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "src/git.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = "1.0.38"
|
||||||
|
clock = { path = "../clock" }
|
||||||
|
lazy_static = "1.4.0"
|
||||||
|
sum_tree = { path = "../sum_tree" }
|
||||||
|
text = { path = "../text" }
|
||||||
|
collections = { path = "../collections" }
|
||||||
|
util = { path = "../util" }
|
||||||
|
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
|
||||||
|
smol = "1.2"
|
||||||
|
parking_lot = "0.11.1"
|
||||||
|
async-trait = "0.1"
|
||||||
|
futures = "0.3"
|
||||||
|
git2 = { version = "0.15", default-features = false }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
unindent = "0.1.7"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
test-support = []
|
362
crates/git/src/diff.rs
Normal file
362
crates/git/src/diff.rs
Normal file
@ -0,0 +1,362 @@
|
|||||||
|
use std::ops::Range;
|
||||||
|
use sum_tree::SumTree;
|
||||||
|
use text::{Anchor, BufferSnapshot, OffsetRangeExt, Point};
|
||||||
|
|
||||||
|
pub use git2 as libgit;
|
||||||
|
use libgit::{DiffLineType as GitDiffLineType, DiffOptions as GitOptions, Patch as GitPatch};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum DiffHunkStatus {
|
||||||
|
Added,
|
||||||
|
Modified,
|
||||||
|
Removed,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct DiffHunk<T> {
|
||||||
|
pub buffer_range: Range<T>,
|
||||||
|
pub head_byte_range: Range<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DiffHunk<u32> {
|
||||||
|
pub fn status(&self) -> DiffHunkStatus {
|
||||||
|
if self.head_byte_range.is_empty() {
|
||||||
|
DiffHunkStatus::Added
|
||||||
|
} else if self.buffer_range.is_empty() {
|
||||||
|
DiffHunkStatus::Removed
|
||||||
|
} else {
|
||||||
|
DiffHunkStatus::Modified
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl sum_tree::Item for DiffHunk<Anchor> {
|
||||||
|
type Summary = DiffHunkSummary;
|
||||||
|
|
||||||
|
fn summary(&self) -> Self::Summary {
|
||||||
|
DiffHunkSummary {
|
||||||
|
buffer_range: self.buffer_range.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone)]
|
||||||
|
pub struct DiffHunkSummary {
|
||||||
|
buffer_range: Range<Anchor>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl sum_tree::Summary for DiffHunkSummary {
|
||||||
|
type Context = text::BufferSnapshot;
|
||||||
|
|
||||||
|
fn add_summary(&mut self, other: &Self, buffer: &Self::Context) {
|
||||||
|
self.buffer_range.start = self
|
||||||
|
.buffer_range
|
||||||
|
.start
|
||||||
|
.min(&other.buffer_range.start, buffer);
|
||||||
|
self.buffer_range.end = self.buffer_range.end.max(&other.buffer_range.end, buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct BufferDiff {
|
||||||
|
last_buffer_version: Option<clock::Global>,
|
||||||
|
tree: SumTree<DiffHunk<Anchor>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BufferDiff {
|
||||||
|
pub fn new() -> BufferDiff {
|
||||||
|
BufferDiff {
|
||||||
|
last_buffer_version: None,
|
||||||
|
tree: SumTree::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hunks_in_range<'a>(
|
||||||
|
&'a self,
|
||||||
|
query_row_range: Range<u32>,
|
||||||
|
buffer: &'a BufferSnapshot,
|
||||||
|
) -> impl 'a + Iterator<Item = DiffHunk<u32>> {
|
||||||
|
let start = buffer.anchor_before(Point::new(query_row_range.start, 0));
|
||||||
|
let end = buffer.anchor_after(Point::new(query_row_range.end, 0));
|
||||||
|
|
||||||
|
let mut cursor = self.tree.filter::<_, DiffHunkSummary>(move |summary| {
|
||||||
|
let before_start = summary.buffer_range.end.cmp(&start, buffer).is_lt();
|
||||||
|
let after_end = summary.buffer_range.start.cmp(&end, buffer).is_gt();
|
||||||
|
!before_start && !after_end
|
||||||
|
});
|
||||||
|
|
||||||
|
std::iter::from_fn(move || {
|
||||||
|
cursor.next(buffer);
|
||||||
|
let hunk = cursor.item()?;
|
||||||
|
|
||||||
|
let range = hunk.buffer_range.to_point(buffer);
|
||||||
|
let end_row = if range.end.column > 0 {
|
||||||
|
range.end.row + 1
|
||||||
|
} else {
|
||||||
|
range.end.row
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(DiffHunk {
|
||||||
|
buffer_range: range.start.row..end_row,
|
||||||
|
head_byte_range: hunk.head_byte_range.clone(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear(&mut self, buffer: &text::BufferSnapshot) {
|
||||||
|
self.last_buffer_version = Some(buffer.version().clone());
|
||||||
|
self.tree = SumTree::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn needs_update(&self, buffer: &text::BufferSnapshot) -> bool {
|
||||||
|
match &self.last_buffer_version {
|
||||||
|
Some(last) => buffer.version().changed_since(last),
|
||||||
|
None => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update(&mut self, diff_base: &str, buffer: &text::BufferSnapshot) {
|
||||||
|
let mut tree = SumTree::new();
|
||||||
|
|
||||||
|
let buffer_text = buffer.as_rope().to_string();
|
||||||
|
let patch = Self::diff(&diff_base, &buffer_text);
|
||||||
|
|
||||||
|
if let Some(patch) = patch {
|
||||||
|
let mut divergence = 0;
|
||||||
|
for hunk_index in 0..patch.num_hunks() {
|
||||||
|
let hunk = Self::process_patch_hunk(&patch, hunk_index, buffer, &mut divergence);
|
||||||
|
tree.push(hunk, buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.tree = tree;
|
||||||
|
self.last_buffer_version = Some(buffer.version().clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
fn hunks<'a>(&'a self, text: &'a BufferSnapshot) -> impl 'a + Iterator<Item = DiffHunk<u32>> {
|
||||||
|
self.hunks_in_range(0..u32::MAX, text)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn diff<'a>(head: &'a str, current: &'a str) -> Option<GitPatch<'a>> {
|
||||||
|
let mut options = GitOptions::default();
|
||||||
|
options.context_lines(0);
|
||||||
|
|
||||||
|
let patch = GitPatch::from_buffers(
|
||||||
|
head.as_bytes(),
|
||||||
|
None,
|
||||||
|
current.as_bytes(),
|
||||||
|
None,
|
||||||
|
Some(&mut options),
|
||||||
|
);
|
||||||
|
|
||||||
|
match patch {
|
||||||
|
Ok(patch) => Some(patch),
|
||||||
|
|
||||||
|
Err(err) => {
|
||||||
|
log::error!("`GitPatch::from_buffers` failed: {}", err);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process_patch_hunk<'a>(
|
||||||
|
patch: &GitPatch<'a>,
|
||||||
|
hunk_index: usize,
|
||||||
|
buffer: &text::BufferSnapshot,
|
||||||
|
buffer_row_divergence: &mut i64,
|
||||||
|
) -> DiffHunk<Anchor> {
|
||||||
|
let line_item_count = patch.num_lines_in_hunk(hunk_index).unwrap();
|
||||||
|
assert!(line_item_count > 0);
|
||||||
|
|
||||||
|
let mut first_deletion_buffer_row: Option<u32> = None;
|
||||||
|
let mut buffer_row_range: Option<Range<u32>> = None;
|
||||||
|
let mut head_byte_range: Option<Range<usize>> = None;
|
||||||
|
|
||||||
|
for line_index in 0..line_item_count {
|
||||||
|
let line = patch.line_in_hunk(hunk_index, line_index).unwrap();
|
||||||
|
let kind = line.origin_value();
|
||||||
|
let content_offset = line.content_offset() as isize;
|
||||||
|
let content_len = line.content().len() as isize;
|
||||||
|
|
||||||
|
if kind == GitDiffLineType::Addition {
|
||||||
|
*buffer_row_divergence += 1;
|
||||||
|
let row = line.new_lineno().unwrap().saturating_sub(1);
|
||||||
|
|
||||||
|
match &mut buffer_row_range {
|
||||||
|
Some(buffer_row_range) => buffer_row_range.end = row + 1,
|
||||||
|
None => buffer_row_range = Some(row..row + 1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if kind == GitDiffLineType::Deletion {
|
||||||
|
let end = content_offset + content_len;
|
||||||
|
|
||||||
|
match &mut head_byte_range {
|
||||||
|
Some(head_byte_range) => head_byte_range.end = end as usize,
|
||||||
|
None => head_byte_range = Some(content_offset as usize..end as usize),
|
||||||
|
}
|
||||||
|
|
||||||
|
if first_deletion_buffer_row.is_none() {
|
||||||
|
let old_row = line.old_lineno().unwrap().saturating_sub(1);
|
||||||
|
let row = old_row as i64 + *buffer_row_divergence;
|
||||||
|
first_deletion_buffer_row = Some(row as u32);
|
||||||
|
}
|
||||||
|
|
||||||
|
*buffer_row_divergence -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//unwrap_or deletion without addition
|
||||||
|
let buffer_row_range = buffer_row_range.unwrap_or_else(|| {
|
||||||
|
//we cannot have an addition-less hunk without deletion(s) or else there would be no hunk
|
||||||
|
let row = first_deletion_buffer_row.unwrap();
|
||||||
|
row..row
|
||||||
|
});
|
||||||
|
|
||||||
|
//unwrap_or addition without deletion
|
||||||
|
let head_byte_range = head_byte_range.unwrap_or(0..0);
|
||||||
|
|
||||||
|
let start = Point::new(buffer_row_range.start, 0);
|
||||||
|
let end = Point::new(buffer_row_range.end, 0);
|
||||||
|
let buffer_range = buffer.anchor_before(start)..buffer.anchor_before(end);
|
||||||
|
DiffHunk {
|
||||||
|
buffer_range,
|
||||||
|
head_byte_range,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Range (crossing new lines), old, new
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
#[track_caller]
|
||||||
|
pub fn assert_hunks<Iter>(
|
||||||
|
diff_hunks: Iter,
|
||||||
|
buffer: &BufferSnapshot,
|
||||||
|
diff_base: &str,
|
||||||
|
expected_hunks: &[(Range<u32>, &str, &str)],
|
||||||
|
) where
|
||||||
|
Iter: Iterator<Item = DiffHunk<u32>>,
|
||||||
|
{
|
||||||
|
let actual_hunks = diff_hunks
|
||||||
|
.map(|hunk| {
|
||||||
|
(
|
||||||
|
hunk.buffer_range.clone(),
|
||||||
|
&diff_base[hunk.head_byte_range],
|
||||||
|
buffer
|
||||||
|
.text_for_range(
|
||||||
|
Point::new(hunk.buffer_range.start, 0)
|
||||||
|
..Point::new(hunk.buffer_range.end, 0),
|
||||||
|
)
|
||||||
|
.collect::<String>(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let expected_hunks: Vec<_> = expected_hunks
|
||||||
|
.iter()
|
||||||
|
.map(|(r, s, h)| (r.clone(), *s, h.to_string()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
assert_eq!(actual_hunks, expected_hunks);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use text::Buffer;
|
||||||
|
use unindent::Unindent as _;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_buffer_diff_simple() {
|
||||||
|
let diff_base = "
|
||||||
|
one
|
||||||
|
two
|
||||||
|
three
|
||||||
|
"
|
||||||
|
.unindent();
|
||||||
|
|
||||||
|
let buffer_text = "
|
||||||
|
one
|
||||||
|
HELLO
|
||||||
|
three
|
||||||
|
"
|
||||||
|
.unindent();
|
||||||
|
|
||||||
|
let mut buffer = Buffer::new(0, 0, buffer_text);
|
||||||
|
let mut diff = BufferDiff::new();
|
||||||
|
smol::block_on(diff.update(&diff_base, &buffer));
|
||||||
|
assert_hunks(
|
||||||
|
diff.hunks(&buffer),
|
||||||
|
&buffer,
|
||||||
|
&diff_base,
|
||||||
|
&[(1..2, "two\n", "HELLO\n")],
|
||||||
|
);
|
||||||
|
|
||||||
|
buffer.edit([(0..0, "point five\n")]);
|
||||||
|
smol::block_on(diff.update(&diff_base, &buffer));
|
||||||
|
assert_hunks(
|
||||||
|
diff.hunks(&buffer),
|
||||||
|
&buffer,
|
||||||
|
&diff_base,
|
||||||
|
&[(0..1, "", "point five\n"), (2..3, "two\n", "HELLO\n")],
|
||||||
|
);
|
||||||
|
|
||||||
|
diff.clear(&buffer);
|
||||||
|
assert_hunks(diff.hunks(&buffer), &buffer, &diff_base, &[]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_buffer_diff_range() {
|
||||||
|
let diff_base = "
|
||||||
|
one
|
||||||
|
two
|
||||||
|
three
|
||||||
|
four
|
||||||
|
five
|
||||||
|
six
|
||||||
|
seven
|
||||||
|
eight
|
||||||
|
nine
|
||||||
|
ten
|
||||||
|
"
|
||||||
|
.unindent();
|
||||||
|
|
||||||
|
let buffer_text = "
|
||||||
|
A
|
||||||
|
one
|
||||||
|
B
|
||||||
|
two
|
||||||
|
C
|
||||||
|
three
|
||||||
|
HELLO
|
||||||
|
four
|
||||||
|
five
|
||||||
|
SIXTEEN
|
||||||
|
seven
|
||||||
|
eight
|
||||||
|
WORLD
|
||||||
|
nine
|
||||||
|
|
||||||
|
ten
|
||||||
|
|
||||||
|
"
|
||||||
|
.unindent();
|
||||||
|
|
||||||
|
let buffer = Buffer::new(0, 0, buffer_text);
|
||||||
|
let mut diff = BufferDiff::new();
|
||||||
|
smol::block_on(diff.update(&diff_base, &buffer));
|
||||||
|
assert_eq!(diff.hunks(&buffer).count(), 8);
|
||||||
|
|
||||||
|
assert_hunks(
|
||||||
|
diff.hunks_in_range(7..12, &buffer),
|
||||||
|
&buffer,
|
||||||
|
&diff_base,
|
||||||
|
&[
|
||||||
|
(6..7, "", "HELLO\n"),
|
||||||
|
(9..10, "six\n", "SIXTEEN\n"),
|
||||||
|
(12..13, "", "WORLD\n"),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
11
crates/git/src/git.rs
Normal file
11
crates/git/src/git.rs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
use std::ffi::OsStr;
|
||||||
|
|
||||||
|
pub use git2 as libgit;
|
||||||
|
pub use lazy_static::lazy_static;
|
||||||
|
|
||||||
|
pub mod diff;
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
pub static ref DOT_GIT: &'static OsStr = OsStr::new(".git");
|
||||||
|
pub static ref GITIGNORE: &'static OsStr = OsStr::new(".gitignore");
|
||||||
|
}
|
@ -165,7 +165,7 @@ impl View for GoToLine {
|
|||||||
Container::new(
|
Container::new(
|
||||||
Flex::new(Axis::Vertical)
|
Flex::new(Axis::Vertical)
|
||||||
.with_child(
|
.with_child(
|
||||||
Container::new(ChildView::new(&self.line_editor).boxed())
|
Container::new(ChildView::new(&self.line_editor, cx).boxed())
|
||||||
.with_style(theme.input_editor.container)
|
.with_style(theme.input_editor.container)
|
||||||
.boxed(),
|
.boxed(),
|
||||||
)
|
)
|
||||||
|
@ -25,6 +25,7 @@ env_logger = { version = "0.9", optional = true }
|
|||||||
etagere = "0.2"
|
etagere = "0.2"
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
image = "0.23"
|
image = "0.23"
|
||||||
|
itertools = "0.10"
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4.0"
|
||||||
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
|
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
|
||||||
num_cpus = "1.13"
|
num_cpus = "1.13"
|
||||||
|
@ -1,28 +1,8 @@
|
|||||||
pub mod action;
|
pub mod action;
|
||||||
mod callback_collection;
|
mod callback_collection;
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
pub mod test_app_context;
|
||||||
|
|
||||||
use crate::{
|
|
||||||
elements::ElementBox,
|
|
||||||
executor::{self, Task},
|
|
||||||
geometry::rect::RectF,
|
|
||||||
keymap::{self, Binding, Keystroke},
|
|
||||||
platform::{self, KeyDownEvent, Platform, PromptLevel, WindowOptions},
|
|
||||||
presenter::Presenter,
|
|
||||||
util::post_inc,
|
|
||||||
Appearance, AssetCache, AssetSource, ClipboardItem, FontCache, InputHandler, MouseButton,
|
|
||||||
MouseRegionId, PathPromptOptions, TextLayoutCache,
|
|
||||||
};
|
|
||||||
pub use action::*;
|
|
||||||
use anyhow::{anyhow, Context, Result};
|
|
||||||
use callback_collection::CallbackCollection;
|
|
||||||
use collections::{btree_map, hash_map::Entry, BTreeMap, HashMap, HashSet, VecDeque};
|
|
||||||
use keymap::MatchResult;
|
|
||||||
use lazy_static::lazy_static;
|
|
||||||
use parking_lot::Mutex;
|
|
||||||
use platform::Event;
|
|
||||||
use postage::oneshot;
|
|
||||||
use smallvec::SmallVec;
|
|
||||||
use smol::prelude::*;
|
|
||||||
use std::{
|
use std::{
|
||||||
any::{type_name, Any, TypeId},
|
any::{type_name, Any, TypeId},
|
||||||
cell::RefCell,
|
cell::RefCell,
|
||||||
@ -38,7 +18,32 @@ use std::{
|
|||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
||||||
use self::callback_collection::Mapping;
|
use anyhow::{anyhow, Context, Result};
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use parking_lot::Mutex;
|
||||||
|
use postage::oneshot;
|
||||||
|
use smallvec::SmallVec;
|
||||||
|
use smol::prelude::*;
|
||||||
|
|
||||||
|
pub use action::*;
|
||||||
|
use callback_collection::{CallbackCollection, Mapping};
|
||||||
|
use collections::{btree_map, hash_map::Entry, BTreeMap, HashMap, HashSet, VecDeque};
|
||||||
|
use keymap::MatchResult;
|
||||||
|
use platform::Event;
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
pub use test_app_context::{ContextHandle, TestAppContext};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
elements::ElementBox,
|
||||||
|
executor::{self, Task},
|
||||||
|
geometry::rect::RectF,
|
||||||
|
keymap::{self, Binding, Keystroke},
|
||||||
|
platform::{self, KeyDownEvent, Platform, PromptLevel, WindowOptions},
|
||||||
|
presenter::Presenter,
|
||||||
|
util::post_inc,
|
||||||
|
Appearance, AssetCache, AssetSource, ClipboardItem, FontCache, InputHandler, MouseButton,
|
||||||
|
MouseRegionId, PathPromptOptions, TextLayoutCache,
|
||||||
|
};
|
||||||
|
|
||||||
pub trait Entity: 'static {
|
pub trait Entity: 'static {
|
||||||
type Event;
|
type Event;
|
||||||
@ -177,13 +182,6 @@ pub struct App(Rc<RefCell<MutableAppContext>>);
|
|||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct AsyncAppContext(Rc<RefCell<MutableAppContext>>);
|
pub struct AsyncAppContext(Rc<RefCell<MutableAppContext>>);
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
|
||||||
pub struct TestAppContext {
|
|
||||||
cx: Rc<RefCell<MutableAppContext>>,
|
|
||||||
foreground_platform: Rc<platform::test::ForegroundPlatform>,
|
|
||||||
condition_duration: Option<Duration>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct WindowInputHandler {
|
pub struct WindowInputHandler {
|
||||||
app: Rc<RefCell<MutableAppContext>>,
|
app: Rc<RefCell<MutableAppContext>>,
|
||||||
window_id: usize,
|
window_id: usize,
|
||||||
@ -427,327 +425,6 @@ impl InputHandler for WindowInputHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
|
||||||
impl TestAppContext {
|
|
||||||
pub fn new(
|
|
||||||
foreground_platform: Rc<platform::test::ForegroundPlatform>,
|
|
||||||
platform: Arc<dyn Platform>,
|
|
||||||
foreground: Rc<executor::Foreground>,
|
|
||||||
background: Arc<executor::Background>,
|
|
||||||
font_cache: Arc<FontCache>,
|
|
||||||
leak_detector: Arc<Mutex<LeakDetector>>,
|
|
||||||
first_entity_id: usize,
|
|
||||||
) -> Self {
|
|
||||||
let mut cx = MutableAppContext::new(
|
|
||||||
foreground,
|
|
||||||
background,
|
|
||||||
platform,
|
|
||||||
foreground_platform.clone(),
|
|
||||||
font_cache,
|
|
||||||
RefCounts {
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
|
||||||
leak_detector,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
(),
|
|
||||||
);
|
|
||||||
cx.next_entity_id = first_entity_id;
|
|
||||||
let cx = TestAppContext {
|
|
||||||
cx: Rc::new(RefCell::new(cx)),
|
|
||||||
foreground_platform,
|
|
||||||
condition_duration: None,
|
|
||||||
};
|
|
||||||
cx.cx.borrow_mut().weak_self = Some(Rc::downgrade(&cx.cx));
|
|
||||||
cx
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn dispatch_action<A: Action>(&self, window_id: usize, action: A) {
|
|
||||||
let mut cx = self.cx.borrow_mut();
|
|
||||||
if let Some(view_id) = cx.focused_view_id(window_id) {
|
|
||||||
cx.handle_dispatch_action_from_effect(window_id, Some(view_id), &action);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn dispatch_global_action<A: Action>(&self, action: A) {
|
|
||||||
self.cx.borrow_mut().dispatch_global_action(action);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn dispatch_keystroke(&mut self, window_id: usize, keystroke: Keystroke, is_held: bool) {
|
|
||||||
let handled = self.cx.borrow_mut().update(|cx| {
|
|
||||||
let presenter = cx
|
|
||||||
.presenters_and_platform_windows
|
|
||||||
.get(&window_id)
|
|
||||||
.unwrap()
|
|
||||||
.0
|
|
||||||
.clone();
|
|
||||||
|
|
||||||
if cx.dispatch_keystroke(window_id, &keystroke) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if presenter.borrow_mut().dispatch_event(
|
|
||||||
Event::KeyDown(KeyDownEvent {
|
|
||||||
keystroke: keystroke.clone(),
|
|
||||||
is_held,
|
|
||||||
}),
|
|
||||||
false,
|
|
||||||
cx,
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
false
|
|
||||||
});
|
|
||||||
|
|
||||||
if !handled && !keystroke.cmd && !keystroke.ctrl {
|
|
||||||
WindowInputHandler {
|
|
||||||
app: self.cx.clone(),
|
|
||||||
window_id,
|
|
||||||
}
|
|
||||||
.replace_text_in_range(None, &keystroke.key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_model<T, F>(&mut self, build_model: F) -> ModelHandle<T>
|
|
||||||
where
|
|
||||||
T: Entity,
|
|
||||||
F: FnOnce(&mut ModelContext<T>) -> T,
|
|
||||||
{
|
|
||||||
self.cx.borrow_mut().add_model(build_model)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_window<T, F>(&mut self, build_root_view: F) -> (usize, ViewHandle<T>)
|
|
||||||
where
|
|
||||||
T: View,
|
|
||||||
F: FnOnce(&mut ViewContext<T>) -> T,
|
|
||||||
{
|
|
||||||
let (window_id, view) = self
|
|
||||||
.cx
|
|
||||||
.borrow_mut()
|
|
||||||
.add_window(Default::default(), build_root_view);
|
|
||||||
self.simulate_window_activation(Some(window_id));
|
|
||||||
(window_id, view)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_view<T, F>(
|
|
||||||
&mut self,
|
|
||||||
parent_handle: impl Into<AnyViewHandle>,
|
|
||||||
build_view: F,
|
|
||||||
) -> ViewHandle<T>
|
|
||||||
where
|
|
||||||
T: View,
|
|
||||||
F: FnOnce(&mut ViewContext<T>) -> T,
|
|
||||||
{
|
|
||||||
self.cx.borrow_mut().add_view(parent_handle, build_view)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn window_ids(&self) -> Vec<usize> {
|
|
||||||
self.cx.borrow().window_ids().collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn root_view<T: View>(&self, window_id: usize) -> Option<ViewHandle<T>> {
|
|
||||||
self.cx.borrow().root_view(window_id)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn read<T, F: FnOnce(&AppContext) -> T>(&self, callback: F) -> T {
|
|
||||||
callback(self.cx.borrow().as_ref())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update<T, F: FnOnce(&mut MutableAppContext) -> T>(&mut self, callback: F) -> T {
|
|
||||||
let mut state = self.cx.borrow_mut();
|
|
||||||
// Don't increment pending flushes in order for effects to be flushed before the callback
|
|
||||||
// completes, which is helpful in tests.
|
|
||||||
let result = callback(&mut *state);
|
|
||||||
// Flush effects after the callback just in case there are any. This can happen in edge
|
|
||||||
// cases such as the closure dropping handles.
|
|
||||||
state.flush_effects();
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn render<F, V, T>(&mut self, handle: &ViewHandle<V>, f: F) -> T
|
|
||||||
where
|
|
||||||
F: FnOnce(&mut V, &mut RenderContext<V>) -> T,
|
|
||||||
V: View,
|
|
||||||
{
|
|
||||||
handle.update(&mut *self.cx.borrow_mut(), |view, cx| {
|
|
||||||
let mut render_cx = RenderContext {
|
|
||||||
app: cx,
|
|
||||||
window_id: handle.window_id(),
|
|
||||||
view_id: handle.id(),
|
|
||||||
view_type: PhantomData,
|
|
||||||
titlebar_height: 0.,
|
|
||||||
hovered_region_ids: Default::default(),
|
|
||||||
clicked_region_ids: None,
|
|
||||||
refreshing: false,
|
|
||||||
appearance: Appearance::Light,
|
|
||||||
};
|
|
||||||
f(view, &mut render_cx)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_async(&self) -> AsyncAppContext {
|
|
||||||
AsyncAppContext(self.cx.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn font_cache(&self) -> Arc<FontCache> {
|
|
||||||
self.cx.borrow().cx.font_cache.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn foreground_platform(&self) -> Rc<platform::test::ForegroundPlatform> {
|
|
||||||
self.foreground_platform.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn platform(&self) -> Arc<dyn platform::Platform> {
|
|
||||||
self.cx.borrow().cx.platform.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn foreground(&self) -> Rc<executor::Foreground> {
|
|
||||||
self.cx.borrow().foreground().clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn background(&self) -> Arc<executor::Background> {
|
|
||||||
self.cx.borrow().background().clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn spawn<F, Fut, T>(&self, f: F) -> Task<T>
|
|
||||||
where
|
|
||||||
F: FnOnce(AsyncAppContext) -> Fut,
|
|
||||||
Fut: 'static + Future<Output = T>,
|
|
||||||
T: 'static,
|
|
||||||
{
|
|
||||||
let foreground = self.foreground();
|
|
||||||
let future = f(self.to_async());
|
|
||||||
let cx = self.to_async();
|
|
||||||
foreground.spawn(async move {
|
|
||||||
let result = future.await;
|
|
||||||
cx.0.borrow_mut().flush_effects();
|
|
||||||
result
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn simulate_new_path_selection(&self, result: impl FnOnce(PathBuf) -> Option<PathBuf>) {
|
|
||||||
self.foreground_platform.simulate_new_path_selection(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn did_prompt_for_new_path(&self) -> bool {
|
|
||||||
self.foreground_platform.as_ref().did_prompt_for_new_path()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn simulate_prompt_answer(&self, window_id: usize, answer: usize) {
|
|
||||||
use postage::prelude::Sink as _;
|
|
||||||
|
|
||||||
let mut done_tx = self
|
|
||||||
.window_mut(window_id)
|
|
||||||
.pending_prompts
|
|
||||||
.borrow_mut()
|
|
||||||
.pop_front()
|
|
||||||
.expect("prompt was not called");
|
|
||||||
let _ = done_tx.try_send(answer);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn has_pending_prompt(&self, window_id: usize) -> bool {
|
|
||||||
let window = self.window_mut(window_id);
|
|
||||||
let prompts = window.pending_prompts.borrow_mut();
|
|
||||||
!prompts.is_empty()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn current_window_title(&self, window_id: usize) -> Option<String> {
|
|
||||||
self.window_mut(window_id).title.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn simulate_window_close(&self, window_id: usize) -> bool {
|
|
||||||
let handler = self.window_mut(window_id).should_close_handler.take();
|
|
||||||
if let Some(mut handler) = handler {
|
|
||||||
let should_close = handler();
|
|
||||||
self.window_mut(window_id).should_close_handler = Some(handler);
|
|
||||||
should_close
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn simulate_window_activation(&self, to_activate: Option<usize>) {
|
|
||||||
let mut handlers = BTreeMap::new();
|
|
||||||
{
|
|
||||||
let mut cx = self.cx.borrow_mut();
|
|
||||||
for (window_id, (_, window)) in &mut cx.presenters_and_platform_windows {
|
|
||||||
let window = window
|
|
||||||
.as_any_mut()
|
|
||||||
.downcast_mut::<platform::test::Window>()
|
|
||||||
.unwrap();
|
|
||||||
handlers.insert(
|
|
||||||
*window_id,
|
|
||||||
mem::take(&mut window.active_status_change_handlers),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let mut handlers = handlers.into_iter().collect::<Vec<_>>();
|
|
||||||
handlers.sort_unstable_by_key(|(window_id, _)| Some(*window_id) == to_activate);
|
|
||||||
|
|
||||||
for (window_id, mut window_handlers) in handlers {
|
|
||||||
for window_handler in &mut window_handlers {
|
|
||||||
window_handler(Some(window_id) == to_activate);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.window_mut(window_id)
|
|
||||||
.active_status_change_handlers
|
|
||||||
.extend(window_handlers);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_window_edited(&self, window_id: usize) -> bool {
|
|
||||||
self.window_mut(window_id).edited
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn leak_detector(&self) -> Arc<Mutex<LeakDetector>> {
|
|
||||||
self.cx.borrow().leak_detector()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn assert_dropped(&self, handle: impl WeakHandle) {
|
|
||||||
self.cx
|
|
||||||
.borrow()
|
|
||||||
.leak_detector()
|
|
||||||
.lock()
|
|
||||||
.assert_dropped(handle.id())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn window_mut(&self, window_id: usize) -> std::cell::RefMut<platform::test::Window> {
|
|
||||||
std::cell::RefMut::map(self.cx.borrow_mut(), |state| {
|
|
||||||
let (_, window) = state
|
|
||||||
.presenters_and_platform_windows
|
|
||||||
.get_mut(&window_id)
|
|
||||||
.unwrap();
|
|
||||||
let test_window = window
|
|
||||||
.as_any_mut()
|
|
||||||
.downcast_mut::<platform::test::Window>()
|
|
||||||
.unwrap();
|
|
||||||
test_window
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_condition_duration(&mut self, duration: Option<Duration>) {
|
|
||||||
self.condition_duration = duration;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn condition_duration(&self) -> Duration {
|
|
||||||
self.condition_duration.unwrap_or_else(|| {
|
|
||||||
if std::env::var("CI").is_ok() {
|
|
||||||
Duration::from_secs(2)
|
|
||||||
} else {
|
|
||||||
Duration::from_millis(500)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn assert_clipboard_content(&mut self, expected_content: Option<&str>) {
|
|
||||||
self.update(|cx| {
|
|
||||||
let actual_content = cx.read_from_clipboard().map(|item| item.text().to_owned());
|
|
||||||
let expected_content = expected_content.map(|content| content.to_owned());
|
|
||||||
assert_eq!(actual_content, expected_content);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AsyncAppContext {
|
impl AsyncAppContext {
|
||||||
pub fn spawn<F, Fut, T>(&self, f: F) -> Task<T>
|
pub fn spawn<F, Fut, T>(&self, f: F) -> Task<T>
|
||||||
where
|
where
|
||||||
@ -786,6 +463,24 @@ impl AsyncAppContext {
|
|||||||
self.update(|cx| cx.add_window(window_options, build_root_view))
|
self.update(|cx| cx.add_window(window_options, build_root_view))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn remove_window(&mut self, window_id: usize) {
|
||||||
|
self.update(|cx| cx.remove_window(window_id))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn activate_window(&mut self, window_id: usize) {
|
||||||
|
self.update(|cx| cx.activate_window(window_id))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn prompt(
|
||||||
|
&mut self,
|
||||||
|
window_id: usize,
|
||||||
|
level: PromptLevel,
|
||||||
|
msg: &str,
|
||||||
|
answers: &[&str],
|
||||||
|
) -> oneshot::Receiver<usize> {
|
||||||
|
self.update(|cx| cx.prompt(window_id, level, msg, answers))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn platform(&self) -> Arc<dyn Platform> {
|
pub fn platform(&self) -> Arc<dyn Platform> {
|
||||||
self.0.borrow().platform()
|
self.0.borrow().platform()
|
||||||
}
|
}
|
||||||
@ -876,60 +571,6 @@ impl ReadViewWith for AsyncAppContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
|
||||||
impl UpdateModel for TestAppContext {
|
|
||||||
fn update_model<T: Entity, O>(
|
|
||||||
&mut self,
|
|
||||||
handle: &ModelHandle<T>,
|
|
||||||
update: &mut dyn FnMut(&mut T, &mut ModelContext<T>) -> O,
|
|
||||||
) -> O {
|
|
||||||
self.cx.borrow_mut().update_model(handle, update)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
|
||||||
impl ReadModelWith for TestAppContext {
|
|
||||||
fn read_model_with<E: Entity, T>(
|
|
||||||
&self,
|
|
||||||
handle: &ModelHandle<E>,
|
|
||||||
read: &mut dyn FnMut(&E, &AppContext) -> T,
|
|
||||||
) -> T {
|
|
||||||
let cx = self.cx.borrow();
|
|
||||||
let cx = cx.as_ref();
|
|
||||||
read(handle.read(cx), cx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
|
||||||
impl UpdateView for TestAppContext {
|
|
||||||
fn update_view<T, S>(
|
|
||||||
&mut self,
|
|
||||||
handle: &ViewHandle<T>,
|
|
||||||
update: &mut dyn FnMut(&mut T, &mut ViewContext<T>) -> S,
|
|
||||||
) -> S
|
|
||||||
where
|
|
||||||
T: View,
|
|
||||||
{
|
|
||||||
self.cx.borrow_mut().update_view(handle, update)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
|
||||||
impl ReadViewWith for TestAppContext {
|
|
||||||
fn read_view_with<V, T>(
|
|
||||||
&self,
|
|
||||||
handle: &ViewHandle<V>,
|
|
||||||
read: &mut dyn FnMut(&V, &AppContext) -> T,
|
|
||||||
) -> T
|
|
||||||
where
|
|
||||||
V: View,
|
|
||||||
{
|
|
||||||
let cx = self.cx.borrow();
|
|
||||||
let cx = cx.as_ref();
|
|
||||||
read(handle.read(cx), cx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type ActionCallback =
|
type ActionCallback =
|
||||||
dyn FnMut(&mut dyn AnyView, &dyn Action, &mut MutableAppContext, usize, usize);
|
dyn FnMut(&mut dyn AnyView, &dyn Action, &mut MutableAppContext, usize, usize);
|
||||||
type GlobalActionCallback = dyn FnMut(&dyn Action, &mut MutableAppContext);
|
type GlobalActionCallback = dyn FnMut(&dyn Action, &mut MutableAppContext);
|
||||||
@ -977,7 +618,6 @@ pub struct MutableAppContext {
|
|||||||
HashMap<usize, (Rc<RefCell<Presenter>>, Box<dyn platform::Window>)>,
|
HashMap<usize, (Rc<RefCell<Presenter>>, Box<dyn platform::Window>)>,
|
||||||
foreground: Rc<executor::Foreground>,
|
foreground: Rc<executor::Foreground>,
|
||||||
pending_effects: VecDeque<Effect>,
|
pending_effects: VecDeque<Effect>,
|
||||||
pending_focus_index: Option<usize>,
|
|
||||||
pending_notifications: HashSet<usize>,
|
pending_notifications: HashSet<usize>,
|
||||||
pending_global_notifications: HashSet<TypeId>,
|
pending_global_notifications: HashSet<TypeId>,
|
||||||
pending_flushes: usize,
|
pending_flushes: usize,
|
||||||
@ -1032,7 +672,6 @@ impl MutableAppContext {
|
|||||||
presenters_and_platform_windows: Default::default(),
|
presenters_and_platform_windows: Default::default(),
|
||||||
foreground,
|
foreground,
|
||||||
pending_effects: VecDeque::new(),
|
pending_effects: VecDeque::new(),
|
||||||
pending_focus_index: None,
|
|
||||||
pending_notifications: Default::default(),
|
pending_notifications: Default::default(),
|
||||||
pending_global_notifications: Default::default(),
|
pending_global_notifications: Default::default(),
|
||||||
pending_flushes: 0,
|
pending_flushes: 0,
|
||||||
@ -1519,6 +1158,17 @@ impl MutableAppContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn observe_default_global<G, F>(&mut self, observe: F) -> Subscription
|
||||||
|
where
|
||||||
|
G: Any + Default,
|
||||||
|
F: 'static + FnMut(&mut MutableAppContext),
|
||||||
|
{
|
||||||
|
if !self.has_global::<G>() {
|
||||||
|
self.set_global(G::default());
|
||||||
|
}
|
||||||
|
self.observe_global::<G, F>(observe)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn observe_release<E, H, F>(&mut self, handle: &H, callback: F) -> Subscription
|
pub fn observe_release<E, H, F>(&mut self, handle: &H, callback: F) -> Subscription
|
||||||
where
|
where
|
||||||
E: Entity,
|
E: Entity,
|
||||||
@ -1887,6 +1537,10 @@ impl MutableAppContext {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn clear_globals(&mut self) {
|
||||||
|
self.cx.globals.clear();
|
||||||
|
}
|
||||||
|
|
||||||
pub fn add_model<T, F>(&mut self, build_model: F) -> ModelHandle<T>
|
pub fn add_model<T, F>(&mut self, build_model: F) -> ModelHandle<T>
|
||||||
where
|
where
|
||||||
T: Entity,
|
T: Entity,
|
||||||
@ -1967,6 +1621,10 @@ impl MutableAppContext {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn remove_status_bar_item(&mut self, id: usize) {
|
||||||
|
self.remove_window(id);
|
||||||
|
}
|
||||||
|
|
||||||
fn register_platform_window(
|
fn register_platform_window(
|
||||||
&mut self,
|
&mut self,
|
||||||
window_id: usize,
|
window_id: usize,
|
||||||
@ -2216,9 +1874,6 @@ impl MutableAppContext {
|
|||||||
let mut refreshing = false;
|
let mut refreshing = false;
|
||||||
loop {
|
loop {
|
||||||
if let Some(effect) = self.pending_effects.pop_front() {
|
if let Some(effect) = self.pending_effects.pop_front() {
|
||||||
if let Some(pending_focus_index) = self.pending_focus_index.as_mut() {
|
|
||||||
*pending_focus_index = pending_focus_index.saturating_sub(1);
|
|
||||||
}
|
|
||||||
match effect {
|
match effect {
|
||||||
Effect::Subscription {
|
Effect::Subscription {
|
||||||
entity_id,
|
entity_id,
|
||||||
@ -2599,8 +2254,6 @@ impl MutableAppContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn handle_focus_effect(&mut self, window_id: usize, focused_id: Option<usize>) {
|
fn handle_focus_effect(&mut self, window_id: usize, focused_id: Option<usize>) {
|
||||||
self.pending_focus_index.take();
|
|
||||||
|
|
||||||
if self
|
if self
|
||||||
.cx
|
.cx
|
||||||
.windows
|
.windows
|
||||||
@ -2723,10 +2376,6 @@ impl MutableAppContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn focus(&mut self, window_id: usize, view_id: Option<usize>) {
|
pub fn focus(&mut self, window_id: usize, view_id: Option<usize>) {
|
||||||
if let Some(pending_focus_index) = self.pending_focus_index {
|
|
||||||
self.pending_effects.remove(pending_focus_index);
|
|
||||||
}
|
|
||||||
self.pending_focus_index = Some(self.pending_effects.len());
|
|
||||||
self.pending_effects
|
self.pending_effects
|
||||||
.push_back(Effect::Focus { window_id, view_id });
|
.push_back(Effect::Focus { window_id, view_id });
|
||||||
}
|
}
|
||||||
@ -2922,6 +2571,10 @@ impl AppContext {
|
|||||||
.and_then(|window| window.focused_view_id)
|
.and_then(|window| window.focused_view_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn view_ui_name(&self, window_id: usize, view_id: usize) -> Option<&'static str> {
|
||||||
|
Some(self.views.get(&(window_id, view_id))?.ui_name())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn background(&self) -> &Arc<executor::Background> {
|
pub fn background(&self) -> &Arc<executor::Background> {
|
||||||
&self.background
|
&self.background
|
||||||
}
|
}
|
||||||
@ -3805,6 +3458,15 @@ impl<'a, T: View> ViewContext<'a, T> {
|
|||||||
self.app.focused_view_id(self.window_id) == Some(self.view_id)
|
self.app.focused_view_id(self.window_id) == Some(self.view_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_child(&self, view: impl Into<AnyViewHandle>) -> bool {
|
||||||
|
let view = view.into();
|
||||||
|
if self.window_id != view.window_id {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
self.parents(view.window_id, view.view_id)
|
||||||
|
.any(|parent| parent == self.view_id)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn blur(&mut self) {
|
pub fn blur(&mut self) {
|
||||||
self.app.focus(self.window_id, None);
|
self.app.focus(self.window_id, None);
|
||||||
}
|
}
|
||||||
@ -4112,10 +3774,32 @@ pub struct RenderContext<'a, T: View> {
|
|||||||
pub refreshing: bool,
|
pub refreshing: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Default)]
|
#[derive(Clone, Default)]
|
||||||
pub struct MouseState {
|
pub struct MouseState {
|
||||||
pub hovered: bool,
|
hovered: bool,
|
||||||
pub clicked: Option<MouseButton>,
|
clicked: Option<MouseButton>,
|
||||||
|
accessed_hovered: bool,
|
||||||
|
accessed_clicked: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MouseState {
|
||||||
|
pub fn hovered(&mut self) -> bool {
|
||||||
|
self.accessed_hovered = true;
|
||||||
|
self.hovered
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clicked(&mut self) -> Option<MouseButton> {
|
||||||
|
self.accessed_clicked = true;
|
||||||
|
self.clicked
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn accessed_hovered(&self) -> bool {
|
||||||
|
self.accessed_hovered
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn accessed_clicked(&self) -> bool {
|
||||||
|
self.accessed_clicked
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, V: View> RenderContext<'a, V> {
|
impl<'a, V: View> RenderContext<'a, V> {
|
||||||
@ -4156,6 +3840,8 @@ impl<'a, V: View> RenderContext<'a, V> {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
accessed_hovered: false,
|
||||||
|
accessed_clicked: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -4409,117 +4095,6 @@ impl<T: Entity> ModelHandle<T> {
|
|||||||
update(model, cx)
|
update(model, cx)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
|
||||||
pub fn next_notification(&self, cx: &TestAppContext) -> impl Future<Output = ()> {
|
|
||||||
let (tx, mut rx) = futures::channel::mpsc::unbounded();
|
|
||||||
let mut cx = cx.cx.borrow_mut();
|
|
||||||
let subscription = cx.observe(self, move |_, _| {
|
|
||||||
tx.unbounded_send(()).ok();
|
|
||||||
});
|
|
||||||
|
|
||||||
let duration = if std::env::var("CI").is_ok() {
|
|
||||||
Duration::from_secs(5)
|
|
||||||
} else {
|
|
||||||
Duration::from_secs(1)
|
|
||||||
};
|
|
||||||
|
|
||||||
async move {
|
|
||||||
let notification = crate::util::timeout(duration, rx.next())
|
|
||||||
.await
|
|
||||||
.expect("next notification timed out");
|
|
||||||
drop(subscription);
|
|
||||||
notification.expect("model dropped while test was waiting for its next notification")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
|
||||||
pub fn next_event(&self, cx: &TestAppContext) -> impl Future<Output = T::Event>
|
|
||||||
where
|
|
||||||
T::Event: Clone,
|
|
||||||
{
|
|
||||||
let (tx, mut rx) = futures::channel::mpsc::unbounded();
|
|
||||||
let mut cx = cx.cx.borrow_mut();
|
|
||||||
let subscription = cx.subscribe(self, move |_, event, _| {
|
|
||||||
tx.unbounded_send(event.clone()).ok();
|
|
||||||
});
|
|
||||||
|
|
||||||
let duration = if std::env::var("CI").is_ok() {
|
|
||||||
Duration::from_secs(5)
|
|
||||||
} else {
|
|
||||||
Duration::from_secs(1)
|
|
||||||
};
|
|
||||||
|
|
||||||
cx.foreground.start_waiting();
|
|
||||||
async move {
|
|
||||||
let event = crate::util::timeout(duration, rx.next())
|
|
||||||
.await
|
|
||||||
.expect("next event timed out");
|
|
||||||
drop(subscription);
|
|
||||||
event.expect("model dropped while test was waiting for its next event")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
|
||||||
pub fn condition(
|
|
||||||
&self,
|
|
||||||
cx: &TestAppContext,
|
|
||||||
mut predicate: impl FnMut(&T, &AppContext) -> bool,
|
|
||||||
) -> impl Future<Output = ()> {
|
|
||||||
let (tx, mut rx) = futures::channel::mpsc::unbounded();
|
|
||||||
|
|
||||||
let mut cx = cx.cx.borrow_mut();
|
|
||||||
let subscriptions = (
|
|
||||||
cx.observe(self, {
|
|
||||||
let tx = tx.clone();
|
|
||||||
move |_, _| {
|
|
||||||
tx.unbounded_send(()).ok();
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
cx.subscribe(self, {
|
|
||||||
move |_, _, _| {
|
|
||||||
tx.unbounded_send(()).ok();
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
let cx = cx.weak_self.as_ref().unwrap().upgrade().unwrap();
|
|
||||||
let handle = self.downgrade();
|
|
||||||
let duration = if std::env::var("CI").is_ok() {
|
|
||||||
Duration::from_secs(5)
|
|
||||||
} else {
|
|
||||||
Duration::from_secs(1)
|
|
||||||
};
|
|
||||||
|
|
||||||
async move {
|
|
||||||
crate::util::timeout(duration, async move {
|
|
||||||
loop {
|
|
||||||
{
|
|
||||||
let cx = cx.borrow();
|
|
||||||
let cx = cx.as_ref();
|
|
||||||
if predicate(
|
|
||||||
handle
|
|
||||||
.upgrade(cx)
|
|
||||||
.expect("model dropped with pending condition")
|
|
||||||
.read(cx),
|
|
||||||
cx,
|
|
||||||
) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cx.borrow().foreground().start_waiting();
|
|
||||||
rx.next()
|
|
||||||
.await
|
|
||||||
.expect("model dropped with pending condition");
|
|
||||||
cx.borrow().foreground().finish_waiting();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.expect("condition timed out");
|
|
||||||
drop(subscriptions);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Entity> Clone for ModelHandle<T> {
|
impl<T: Entity> Clone for ModelHandle<T> {
|
||||||
@ -4650,6 +4225,12 @@ impl<T> PartialEq for WeakModelHandle<T> {
|
|||||||
|
|
||||||
impl<T> Eq for WeakModelHandle<T> {}
|
impl<T> Eq for WeakModelHandle<T> {}
|
||||||
|
|
||||||
|
impl<T: Entity> PartialEq<ModelHandle<T>> for WeakModelHandle<T> {
|
||||||
|
fn eq(&self, other: &ModelHandle<T>) -> bool {
|
||||||
|
self.model_id == other.model_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<T> Clone for WeakModelHandle<T> {
|
impl<T> Clone for WeakModelHandle<T> {
|
||||||
fn clone(&self) -> Self {
|
fn clone(&self) -> Self {
|
||||||
Self {
|
Self {
|
||||||
@ -4746,93 +4327,6 @@ impl<T: View> ViewHandle<T> {
|
|||||||
cx.focused_view_id(self.window_id)
|
cx.focused_view_id(self.window_id)
|
||||||
.map_or(false, |focused_id| focused_id == self.view_id)
|
.map_or(false, |focused_id| focused_id == self.view_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
|
||||||
pub fn next_notification(&self, cx: &TestAppContext) -> impl Future<Output = ()> {
|
|
||||||
use postage::prelude::{Sink as _, Stream as _};
|
|
||||||
|
|
||||||
let (mut tx, mut rx) = postage::mpsc::channel(1);
|
|
||||||
let mut cx = cx.cx.borrow_mut();
|
|
||||||
let subscription = cx.observe(self, move |_, _| {
|
|
||||||
tx.try_send(()).ok();
|
|
||||||
});
|
|
||||||
|
|
||||||
let duration = if std::env::var("CI").is_ok() {
|
|
||||||
Duration::from_secs(5)
|
|
||||||
} else {
|
|
||||||
Duration::from_secs(1)
|
|
||||||
};
|
|
||||||
|
|
||||||
async move {
|
|
||||||
let notification = crate::util::timeout(duration, rx.recv())
|
|
||||||
.await
|
|
||||||
.expect("next notification timed out");
|
|
||||||
drop(subscription);
|
|
||||||
notification.expect("model dropped while test was waiting for its next notification")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
|
||||||
pub fn condition(
|
|
||||||
&self,
|
|
||||||
cx: &TestAppContext,
|
|
||||||
mut predicate: impl FnMut(&T, &AppContext) -> bool,
|
|
||||||
) -> impl Future<Output = ()> {
|
|
||||||
use postage::prelude::{Sink as _, Stream as _};
|
|
||||||
|
|
||||||
let (tx, mut rx) = postage::mpsc::channel(1024);
|
|
||||||
let timeout_duration = cx.condition_duration();
|
|
||||||
|
|
||||||
let mut cx = cx.cx.borrow_mut();
|
|
||||||
let subscriptions = self.update(&mut *cx, |_, cx| {
|
|
||||||
(
|
|
||||||
cx.observe(self, {
|
|
||||||
let mut tx = tx.clone();
|
|
||||||
move |_, _, _| {
|
|
||||||
tx.blocking_send(()).ok();
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
cx.subscribe(self, {
|
|
||||||
let mut tx = tx.clone();
|
|
||||||
move |_, _, _, _| {
|
|
||||||
tx.blocking_send(()).ok();
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
let cx = cx.weak_self.as_ref().unwrap().upgrade().unwrap();
|
|
||||||
let handle = self.downgrade();
|
|
||||||
|
|
||||||
async move {
|
|
||||||
crate::util::timeout(timeout_duration, async move {
|
|
||||||
loop {
|
|
||||||
{
|
|
||||||
let cx = cx.borrow();
|
|
||||||
let cx = cx.as_ref();
|
|
||||||
if predicate(
|
|
||||||
handle
|
|
||||||
.upgrade(cx)
|
|
||||||
.expect("view dropped with pending condition")
|
|
||||||
.read(cx),
|
|
||||||
cx,
|
|
||||||
) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cx.borrow().foreground().start_waiting();
|
|
||||||
rx.recv()
|
|
||||||
.await
|
|
||||||
.expect("view dropped with pending condition");
|
|
||||||
cx.borrow().foreground().finish_waiting();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.expect("condition timed out");
|
|
||||||
drop(subscriptions);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: View> Clone for ViewHandle<T> {
|
impl<T: View> Clone for ViewHandle<T> {
|
||||||
@ -4950,6 +4444,10 @@ impl AnyViewHandle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn window_id(&self) -> usize {
|
||||||
|
self.window_id
|
||||||
|
}
|
||||||
|
|
||||||
pub fn id(&self) -> usize {
|
pub fn id(&self) -> usize {
|
||||||
self.view_id
|
self.view_id
|
||||||
}
|
}
|
||||||
@ -5266,6 +4764,10 @@ pub struct AnyWeakViewHandle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl AnyWeakViewHandle {
|
impl AnyWeakViewHandle {
|
||||||
|
pub fn id(&self) -> usize {
|
||||||
|
self.view_id
|
||||||
|
}
|
||||||
|
|
||||||
pub fn upgrade(&self, cx: &impl UpgradeViewHandle) -> Option<AnyViewHandle> {
|
pub fn upgrade(&self, cx: &impl UpgradeViewHandle) -> Option<AnyViewHandle> {
|
||||||
cx.upgrade_any_view_handle(self)
|
cx.upgrade_any_view_handle(self)
|
||||||
}
|
}
|
||||||
@ -6910,18 +6412,29 @@ mod tests {
|
|||||||
assert_eq!(mem::take(&mut *observed_events.lock()), Vec::<&str>::new());
|
assert_eq!(mem::take(&mut *observed_events.lock()), Vec::<&str>::new());
|
||||||
|
|
||||||
view_1.update(cx, |_, cx| {
|
view_1.update(cx, |_, cx| {
|
||||||
// Ensure only the latest focus is honored.
|
// Ensure focus events are sent for all intermediate focuses
|
||||||
cx.focus(&view_2);
|
cx.focus(&view_2);
|
||||||
cx.focus(&view_1);
|
cx.focus(&view_1);
|
||||||
cx.focus(&view_2);
|
cx.focus(&view_2);
|
||||||
});
|
});
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
mem::take(&mut *view_events.lock()),
|
mem::take(&mut *view_events.lock()),
|
||||||
["view 1 blurred", "view 2 focused"],
|
[
|
||||||
|
"view 1 blurred",
|
||||||
|
"view 2 focused",
|
||||||
|
"view 2 blurred",
|
||||||
|
"view 1 focused",
|
||||||
|
"view 1 blurred",
|
||||||
|
"view 2 focused"
|
||||||
|
],
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
mem::take(&mut *observed_events.lock()),
|
mem::take(&mut *observed_events.lock()),
|
||||||
[
|
[
|
||||||
|
"view 2 observed view 1's blur",
|
||||||
|
"view 1 observed view 2's focus",
|
||||||
|
"view 1 observed view 2's blur",
|
||||||
|
"view 2 observed view 1's focus",
|
||||||
"view 2 observed view 1's blur",
|
"view 2 observed view 1's blur",
|
||||||
"view 1 observed view 2's focus"
|
"view 1 observed view 2's focus"
|
||||||
]
|
]
|
||||||
@ -7555,4 +7068,73 @@ mod tests {
|
|||||||
cx.simulate_window_activation(Some(window_3));
|
cx.simulate_window_activation(Some(window_3));
|
||||||
assert_eq!(mem::take(&mut *events.borrow_mut()), []);
|
assert_eq!(mem::take(&mut *events.borrow_mut()), []);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[crate::test(self)]
|
||||||
|
fn test_child_view(cx: &mut MutableAppContext) {
|
||||||
|
struct Child {
|
||||||
|
rendered: Rc<Cell<bool>>,
|
||||||
|
dropped: Rc<Cell<bool>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl super::Entity for Child {
|
||||||
|
type Event = ();
|
||||||
|
}
|
||||||
|
|
||||||
|
impl super::View for Child {
|
||||||
|
fn ui_name() -> &'static str {
|
||||||
|
"child view"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
|
||||||
|
self.rendered.set(true);
|
||||||
|
Empty::new().boxed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for Child {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.dropped.set(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Parent {
|
||||||
|
child: Option<ViewHandle<Child>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl super::Entity for Parent {
|
||||||
|
type Event = ();
|
||||||
|
}
|
||||||
|
|
||||||
|
impl super::View for Parent {
|
||||||
|
fn ui_name() -> &'static str {
|
||||||
|
"parent view"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||||
|
if let Some(child) = self.child.as_ref() {
|
||||||
|
ChildView::new(child, cx).boxed()
|
||||||
|
} else {
|
||||||
|
Empty::new().boxed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let child_rendered = Rc::new(Cell::new(false));
|
||||||
|
let child_dropped = Rc::new(Cell::new(false));
|
||||||
|
let (_, root_view) = cx.add_window(Default::default(), |cx| Parent {
|
||||||
|
child: Some(cx.add_view(|_| Child {
|
||||||
|
rendered: child_rendered.clone(),
|
||||||
|
dropped: child_dropped.clone(),
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
assert!(child_rendered.take());
|
||||||
|
assert!(!child_dropped.take());
|
||||||
|
|
||||||
|
root_view.update(cx, |view, cx| {
|
||||||
|
view.child.take();
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
assert!(!child_rendered.take());
|
||||||
|
assert!(child_dropped.take());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
667
crates/gpui/src/app/test_app_context.rs
Normal file
667
crates/gpui/src/app/test_app_context.rs
Normal file
@ -0,0 +1,667 @@
|
|||||||
|
use std::{
|
||||||
|
cell::RefCell,
|
||||||
|
marker::PhantomData,
|
||||||
|
mem,
|
||||||
|
path::PathBuf,
|
||||||
|
rc::Rc,
|
||||||
|
sync::{
|
||||||
|
atomic::{AtomicUsize, Ordering},
|
||||||
|
Arc,
|
||||||
|
},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
|
use futures::Future;
|
||||||
|
use itertools::Itertools;
|
||||||
|
use parking_lot::{Mutex, RwLock};
|
||||||
|
use smol::stream::StreamExt;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
executor, geometry::vector::Vector2F, keymap::Keystroke, platform, Action, AnyViewHandle,
|
||||||
|
AppContext, Appearance, Entity, Event, FontCache, InputHandler, KeyDownEvent, LeakDetector,
|
||||||
|
ModelContext, ModelHandle, MutableAppContext, Platform, ReadModelWith, ReadViewWith,
|
||||||
|
RenderContext, Task, UpdateModel, UpdateView, View, ViewContext, ViewHandle, WeakHandle,
|
||||||
|
WindowInputHandler,
|
||||||
|
};
|
||||||
|
use collections::BTreeMap;
|
||||||
|
|
||||||
|
use super::{AsyncAppContext, RefCounts};
|
||||||
|
|
||||||
|
pub struct TestAppContext {
|
||||||
|
cx: Rc<RefCell<MutableAppContext>>,
|
||||||
|
foreground_platform: Rc<platform::test::ForegroundPlatform>,
|
||||||
|
condition_duration: Option<Duration>,
|
||||||
|
pub function_name: String,
|
||||||
|
assertion_context: AssertionContextManager,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TestAppContext {
|
||||||
|
pub fn new(
|
||||||
|
foreground_platform: Rc<platform::test::ForegroundPlatform>,
|
||||||
|
platform: Arc<dyn Platform>,
|
||||||
|
foreground: Rc<executor::Foreground>,
|
||||||
|
background: Arc<executor::Background>,
|
||||||
|
font_cache: Arc<FontCache>,
|
||||||
|
leak_detector: Arc<Mutex<LeakDetector>>,
|
||||||
|
first_entity_id: usize,
|
||||||
|
function_name: String,
|
||||||
|
) -> Self {
|
||||||
|
let mut cx = MutableAppContext::new(
|
||||||
|
foreground,
|
||||||
|
background,
|
||||||
|
platform,
|
||||||
|
foreground_platform.clone(),
|
||||||
|
font_cache,
|
||||||
|
RefCounts {
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
leak_detector,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
(),
|
||||||
|
);
|
||||||
|
cx.next_entity_id = first_entity_id;
|
||||||
|
let cx = TestAppContext {
|
||||||
|
cx: Rc::new(RefCell::new(cx)),
|
||||||
|
foreground_platform,
|
||||||
|
condition_duration: None,
|
||||||
|
function_name,
|
||||||
|
assertion_context: AssertionContextManager::new(),
|
||||||
|
};
|
||||||
|
cx.cx.borrow_mut().weak_self = Some(Rc::downgrade(&cx.cx));
|
||||||
|
cx
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dispatch_action<A: Action>(&self, window_id: usize, action: A) {
|
||||||
|
let mut cx = self.cx.borrow_mut();
|
||||||
|
if let Some(view_id) = cx.focused_view_id(window_id) {
|
||||||
|
cx.handle_dispatch_action_from_effect(window_id, Some(view_id), &action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dispatch_global_action<A: Action>(&self, action: A) {
|
||||||
|
self.cx.borrow_mut().dispatch_global_action(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dispatch_keystroke(&mut self, window_id: usize, keystroke: Keystroke, is_held: bool) {
|
||||||
|
let handled = self.cx.borrow_mut().update(|cx| {
|
||||||
|
let presenter = cx
|
||||||
|
.presenters_and_platform_windows
|
||||||
|
.get(&window_id)
|
||||||
|
.unwrap()
|
||||||
|
.0
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
if cx.dispatch_keystroke(window_id, &keystroke) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if presenter.borrow_mut().dispatch_event(
|
||||||
|
Event::KeyDown(KeyDownEvent {
|
||||||
|
keystroke: keystroke.clone(),
|
||||||
|
is_held,
|
||||||
|
}),
|
||||||
|
false,
|
||||||
|
cx,
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
});
|
||||||
|
|
||||||
|
if !handled && !keystroke.cmd && !keystroke.ctrl {
|
||||||
|
WindowInputHandler {
|
||||||
|
app: self.cx.clone(),
|
||||||
|
window_id,
|
||||||
|
}
|
||||||
|
.replace_text_in_range(None, &keystroke.key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_model<T, F>(&mut self, build_model: F) -> ModelHandle<T>
|
||||||
|
where
|
||||||
|
T: Entity,
|
||||||
|
F: FnOnce(&mut ModelContext<T>) -> T,
|
||||||
|
{
|
||||||
|
self.cx.borrow_mut().add_model(build_model)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_window<T, F>(&mut self, build_root_view: F) -> (usize, ViewHandle<T>)
|
||||||
|
where
|
||||||
|
T: View,
|
||||||
|
F: FnOnce(&mut ViewContext<T>) -> T,
|
||||||
|
{
|
||||||
|
let (window_id, view) = self
|
||||||
|
.cx
|
||||||
|
.borrow_mut()
|
||||||
|
.add_window(Default::default(), build_root_view);
|
||||||
|
self.simulate_window_activation(Some(window_id));
|
||||||
|
(window_id, view)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_view<T, F>(
|
||||||
|
&mut self,
|
||||||
|
parent_handle: impl Into<AnyViewHandle>,
|
||||||
|
build_view: F,
|
||||||
|
) -> ViewHandle<T>
|
||||||
|
where
|
||||||
|
T: View,
|
||||||
|
F: FnOnce(&mut ViewContext<T>) -> T,
|
||||||
|
{
|
||||||
|
self.cx.borrow_mut().add_view(parent_handle, build_view)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn window_ids(&self) -> Vec<usize> {
|
||||||
|
self.cx.borrow().window_ids().collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn root_view<T: View>(&self, window_id: usize) -> Option<ViewHandle<T>> {
|
||||||
|
self.cx.borrow().root_view(window_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read<T, F: FnOnce(&AppContext) -> T>(&self, callback: F) -> T {
|
||||||
|
callback(self.cx.borrow().as_ref())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update<T, F: FnOnce(&mut MutableAppContext) -> T>(&mut self, callback: F) -> T {
|
||||||
|
let mut state = self.cx.borrow_mut();
|
||||||
|
// Don't increment pending flushes in order for effects to be flushed before the callback
|
||||||
|
// completes, which is helpful in tests.
|
||||||
|
let result = callback(&mut *state);
|
||||||
|
// Flush effects after the callback just in case there are any. This can happen in edge
|
||||||
|
// cases such as the closure dropping handles.
|
||||||
|
state.flush_effects();
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render<F, V, T>(&mut self, handle: &ViewHandle<V>, f: F) -> T
|
||||||
|
where
|
||||||
|
F: FnOnce(&mut V, &mut RenderContext<V>) -> T,
|
||||||
|
V: View,
|
||||||
|
{
|
||||||
|
handle.update(&mut *self.cx.borrow_mut(), |view, cx| {
|
||||||
|
let mut render_cx = RenderContext {
|
||||||
|
app: cx,
|
||||||
|
window_id: handle.window_id(),
|
||||||
|
view_id: handle.id(),
|
||||||
|
view_type: PhantomData,
|
||||||
|
titlebar_height: 0.,
|
||||||
|
hovered_region_ids: Default::default(),
|
||||||
|
clicked_region_ids: None,
|
||||||
|
refreshing: false,
|
||||||
|
appearance: Appearance::Light,
|
||||||
|
};
|
||||||
|
f(view, &mut render_cx)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_async(&self) -> AsyncAppContext {
|
||||||
|
AsyncAppContext(self.cx.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn font_cache(&self) -> Arc<FontCache> {
|
||||||
|
self.cx.borrow().cx.font_cache.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn foreground_platform(&self) -> Rc<platform::test::ForegroundPlatform> {
|
||||||
|
self.foreground_platform.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn platform(&self) -> Arc<dyn platform::Platform> {
|
||||||
|
self.cx.borrow().cx.platform.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn foreground(&self) -> Rc<executor::Foreground> {
|
||||||
|
self.cx.borrow().foreground().clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn background(&self) -> Arc<executor::Background> {
|
||||||
|
self.cx.borrow().background().clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn spawn<F, Fut, T>(&self, f: F) -> Task<T>
|
||||||
|
where
|
||||||
|
F: FnOnce(AsyncAppContext) -> Fut,
|
||||||
|
Fut: 'static + Future<Output = T>,
|
||||||
|
T: 'static,
|
||||||
|
{
|
||||||
|
let foreground = self.foreground();
|
||||||
|
let future = f(self.to_async());
|
||||||
|
let cx = self.to_async();
|
||||||
|
foreground.spawn(async move {
|
||||||
|
let result = future.await;
|
||||||
|
cx.0.borrow_mut().flush_effects();
|
||||||
|
result
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn simulate_new_path_selection(&self, result: impl FnOnce(PathBuf) -> Option<PathBuf>) {
|
||||||
|
self.foreground_platform.simulate_new_path_selection(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn did_prompt_for_new_path(&self) -> bool {
|
||||||
|
self.foreground_platform.as_ref().did_prompt_for_new_path()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn simulate_prompt_answer(&self, window_id: usize, answer: usize) {
|
||||||
|
use postage::prelude::Sink as _;
|
||||||
|
|
||||||
|
let mut done_tx = self
|
||||||
|
.window_mut(window_id)
|
||||||
|
.pending_prompts
|
||||||
|
.borrow_mut()
|
||||||
|
.pop_front()
|
||||||
|
.expect("prompt was not called");
|
||||||
|
let _ = done_tx.try_send(answer);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn has_pending_prompt(&self, window_id: usize) -> bool {
|
||||||
|
let window = self.window_mut(window_id);
|
||||||
|
let prompts = window.pending_prompts.borrow_mut();
|
||||||
|
!prompts.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn current_window_title(&self, window_id: usize) -> Option<String> {
|
||||||
|
self.window_mut(window_id).title.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn simulate_window_close(&self, window_id: usize) -> bool {
|
||||||
|
let handler = self.window_mut(window_id).should_close_handler.take();
|
||||||
|
if let Some(mut handler) = handler {
|
||||||
|
let should_close = handler();
|
||||||
|
self.window_mut(window_id).should_close_handler = Some(handler);
|
||||||
|
should_close
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn simulate_window_resize(&self, window_id: usize, size: Vector2F) {
|
||||||
|
let mut window = self.window_mut(window_id);
|
||||||
|
window.size = size;
|
||||||
|
let mut handlers = mem::take(&mut window.resize_handlers);
|
||||||
|
drop(window);
|
||||||
|
for handler in &mut handlers {
|
||||||
|
handler();
|
||||||
|
}
|
||||||
|
self.window_mut(window_id).resize_handlers = handlers;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn simulate_window_activation(&self, to_activate: Option<usize>) {
|
||||||
|
let mut handlers = BTreeMap::new();
|
||||||
|
{
|
||||||
|
let mut cx = self.cx.borrow_mut();
|
||||||
|
for (window_id, (_, window)) in &mut cx.presenters_and_platform_windows {
|
||||||
|
let window = window
|
||||||
|
.as_any_mut()
|
||||||
|
.downcast_mut::<platform::test::Window>()
|
||||||
|
.unwrap();
|
||||||
|
handlers.insert(
|
||||||
|
*window_id,
|
||||||
|
mem::take(&mut window.active_status_change_handlers),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut handlers = handlers.into_iter().collect::<Vec<_>>();
|
||||||
|
handlers.sort_unstable_by_key(|(window_id, _)| Some(*window_id) == to_activate);
|
||||||
|
|
||||||
|
for (window_id, mut window_handlers) in handlers {
|
||||||
|
for window_handler in &mut window_handlers {
|
||||||
|
window_handler(Some(window_id) == to_activate);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.window_mut(window_id)
|
||||||
|
.active_status_change_handlers
|
||||||
|
.extend(window_handlers);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_window_edited(&self, window_id: usize) -> bool {
|
||||||
|
self.window_mut(window_id).edited
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn leak_detector(&self) -> Arc<Mutex<LeakDetector>> {
|
||||||
|
self.cx.borrow().leak_detector()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn assert_dropped(&self, handle: impl WeakHandle) {
|
||||||
|
self.cx
|
||||||
|
.borrow()
|
||||||
|
.leak_detector()
|
||||||
|
.lock()
|
||||||
|
.assert_dropped(handle.id())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn window_mut(&self, window_id: usize) -> std::cell::RefMut<platform::test::Window> {
|
||||||
|
std::cell::RefMut::map(self.cx.borrow_mut(), |state| {
|
||||||
|
let (_, window) = state
|
||||||
|
.presenters_and_platform_windows
|
||||||
|
.get_mut(&window_id)
|
||||||
|
.unwrap();
|
||||||
|
let test_window = window
|
||||||
|
.as_any_mut()
|
||||||
|
.downcast_mut::<platform::test::Window>()
|
||||||
|
.unwrap();
|
||||||
|
test_window
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_condition_duration(&mut self, duration: Option<Duration>) {
|
||||||
|
self.condition_duration = duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn condition_duration(&self) -> Duration {
|
||||||
|
self.condition_duration.unwrap_or_else(|| {
|
||||||
|
if std::env::var("CI").is_ok() {
|
||||||
|
Duration::from_secs(2)
|
||||||
|
} else {
|
||||||
|
Duration::from_millis(500)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn assert_clipboard_content(&mut self, expected_content: Option<&str>) {
|
||||||
|
self.update(|cx| {
|
||||||
|
let actual_content = cx.read_from_clipboard().map(|item| item.text().to_owned());
|
||||||
|
let expected_content = expected_content.map(|content| content.to_owned());
|
||||||
|
assert_eq!(actual_content, expected_content);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_assertion_context(&self, context: String) -> ContextHandle {
|
||||||
|
self.assertion_context.add_context(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn assertion_context(&self) -> String {
|
||||||
|
self.assertion_context.context()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UpdateModel for TestAppContext {
|
||||||
|
fn update_model<T: Entity, O>(
|
||||||
|
&mut self,
|
||||||
|
handle: &ModelHandle<T>,
|
||||||
|
update: &mut dyn FnMut(&mut T, &mut ModelContext<T>) -> O,
|
||||||
|
) -> O {
|
||||||
|
self.cx.borrow_mut().update_model(handle, update)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ReadModelWith for TestAppContext {
|
||||||
|
fn read_model_with<E: Entity, T>(
|
||||||
|
&self,
|
||||||
|
handle: &ModelHandle<E>,
|
||||||
|
read: &mut dyn FnMut(&E, &AppContext) -> T,
|
||||||
|
) -> T {
|
||||||
|
let cx = self.cx.borrow();
|
||||||
|
let cx = cx.as_ref();
|
||||||
|
read(handle.read(cx), cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UpdateView for TestAppContext {
|
||||||
|
fn update_view<T, S>(
|
||||||
|
&mut self,
|
||||||
|
handle: &ViewHandle<T>,
|
||||||
|
update: &mut dyn FnMut(&mut T, &mut ViewContext<T>) -> S,
|
||||||
|
) -> S
|
||||||
|
where
|
||||||
|
T: View,
|
||||||
|
{
|
||||||
|
self.cx.borrow_mut().update_view(handle, update)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ReadViewWith for TestAppContext {
|
||||||
|
fn read_view_with<V, T>(
|
||||||
|
&self,
|
||||||
|
handle: &ViewHandle<V>,
|
||||||
|
read: &mut dyn FnMut(&V, &AppContext) -> T,
|
||||||
|
) -> T
|
||||||
|
where
|
||||||
|
V: View,
|
||||||
|
{
|
||||||
|
let cx = self.cx.borrow();
|
||||||
|
let cx = cx.as_ref();
|
||||||
|
read(handle.read(cx), cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Entity> ModelHandle<T> {
|
||||||
|
pub fn next_notification(&self, cx: &TestAppContext) -> impl Future<Output = ()> {
|
||||||
|
let (tx, mut rx) = futures::channel::mpsc::unbounded();
|
||||||
|
let mut cx = cx.cx.borrow_mut();
|
||||||
|
let subscription = cx.observe(self, move |_, _| {
|
||||||
|
tx.unbounded_send(()).ok();
|
||||||
|
});
|
||||||
|
|
||||||
|
let duration = if std::env::var("CI").is_ok() {
|
||||||
|
Duration::from_secs(5)
|
||||||
|
} else {
|
||||||
|
Duration::from_secs(1)
|
||||||
|
};
|
||||||
|
|
||||||
|
async move {
|
||||||
|
let notification = crate::util::timeout(duration, rx.next())
|
||||||
|
.await
|
||||||
|
.expect("next notification timed out");
|
||||||
|
drop(subscription);
|
||||||
|
notification.expect("model dropped while test was waiting for its next notification")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next_event(&self, cx: &TestAppContext) -> impl Future<Output = T::Event>
|
||||||
|
where
|
||||||
|
T::Event: Clone,
|
||||||
|
{
|
||||||
|
let (tx, mut rx) = futures::channel::mpsc::unbounded();
|
||||||
|
let mut cx = cx.cx.borrow_mut();
|
||||||
|
let subscription = cx.subscribe(self, move |_, event, _| {
|
||||||
|
tx.unbounded_send(event.clone()).ok();
|
||||||
|
});
|
||||||
|
|
||||||
|
let duration = if std::env::var("CI").is_ok() {
|
||||||
|
Duration::from_secs(5)
|
||||||
|
} else {
|
||||||
|
Duration::from_secs(1)
|
||||||
|
};
|
||||||
|
|
||||||
|
cx.foreground.start_waiting();
|
||||||
|
async move {
|
||||||
|
let event = crate::util::timeout(duration, rx.next())
|
||||||
|
.await
|
||||||
|
.expect("next event timed out");
|
||||||
|
drop(subscription);
|
||||||
|
event.expect("model dropped while test was waiting for its next event")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn condition(
|
||||||
|
&self,
|
||||||
|
cx: &TestAppContext,
|
||||||
|
mut predicate: impl FnMut(&T, &AppContext) -> bool,
|
||||||
|
) -> impl Future<Output = ()> {
|
||||||
|
let (tx, mut rx) = futures::channel::mpsc::unbounded();
|
||||||
|
|
||||||
|
let mut cx = cx.cx.borrow_mut();
|
||||||
|
let subscriptions = (
|
||||||
|
cx.observe(self, {
|
||||||
|
let tx = tx.clone();
|
||||||
|
move |_, _| {
|
||||||
|
tx.unbounded_send(()).ok();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
cx.subscribe(self, {
|
||||||
|
move |_, _, _| {
|
||||||
|
tx.unbounded_send(()).ok();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
let cx = cx.weak_self.as_ref().unwrap().upgrade().unwrap();
|
||||||
|
let handle = self.downgrade();
|
||||||
|
let duration = if std::env::var("CI").is_ok() {
|
||||||
|
Duration::from_secs(5)
|
||||||
|
} else {
|
||||||
|
Duration::from_secs(1)
|
||||||
|
};
|
||||||
|
|
||||||
|
async move {
|
||||||
|
crate::util::timeout(duration, async move {
|
||||||
|
loop {
|
||||||
|
{
|
||||||
|
let cx = cx.borrow();
|
||||||
|
let cx = cx.as_ref();
|
||||||
|
if predicate(
|
||||||
|
handle
|
||||||
|
.upgrade(cx)
|
||||||
|
.expect("model dropped with pending condition")
|
||||||
|
.read(cx),
|
||||||
|
cx,
|
||||||
|
) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cx.borrow().foreground().start_waiting();
|
||||||
|
rx.next()
|
||||||
|
.await
|
||||||
|
.expect("model dropped with pending condition");
|
||||||
|
cx.borrow().foreground().finish_waiting();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect("condition timed out");
|
||||||
|
drop(subscriptions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: View> ViewHandle<T> {
|
||||||
|
pub fn next_notification(&self, cx: &TestAppContext) -> impl Future<Output = ()> {
|
||||||
|
use postage::prelude::{Sink as _, Stream as _};
|
||||||
|
|
||||||
|
let (mut tx, mut rx) = postage::mpsc::channel(1);
|
||||||
|
let mut cx = cx.cx.borrow_mut();
|
||||||
|
let subscription = cx.observe(self, move |_, _| {
|
||||||
|
tx.try_send(()).ok();
|
||||||
|
});
|
||||||
|
|
||||||
|
let duration = if std::env::var("CI").is_ok() {
|
||||||
|
Duration::from_secs(5)
|
||||||
|
} else {
|
||||||
|
Duration::from_secs(1)
|
||||||
|
};
|
||||||
|
|
||||||
|
async move {
|
||||||
|
let notification = crate::util::timeout(duration, rx.recv())
|
||||||
|
.await
|
||||||
|
.expect("next notification timed out");
|
||||||
|
drop(subscription);
|
||||||
|
notification.expect("model dropped while test was waiting for its next notification")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn condition(
|
||||||
|
&self,
|
||||||
|
cx: &TestAppContext,
|
||||||
|
mut predicate: impl FnMut(&T, &AppContext) -> bool,
|
||||||
|
) -> impl Future<Output = ()> {
|
||||||
|
use postage::prelude::{Sink as _, Stream as _};
|
||||||
|
|
||||||
|
let (tx, mut rx) = postage::mpsc::channel(1024);
|
||||||
|
let timeout_duration = cx.condition_duration();
|
||||||
|
|
||||||
|
let mut cx = cx.cx.borrow_mut();
|
||||||
|
let subscriptions = self.update(&mut *cx, |_, cx| {
|
||||||
|
(
|
||||||
|
cx.observe(self, {
|
||||||
|
let mut tx = tx.clone();
|
||||||
|
move |_, _, _| {
|
||||||
|
tx.blocking_send(()).ok();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
cx.subscribe(self, {
|
||||||
|
let mut tx = tx.clone();
|
||||||
|
move |_, _, _, _| {
|
||||||
|
tx.blocking_send(()).ok();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
let cx = cx.weak_self.as_ref().unwrap().upgrade().unwrap();
|
||||||
|
let handle = self.downgrade();
|
||||||
|
|
||||||
|
async move {
|
||||||
|
crate::util::timeout(timeout_duration, async move {
|
||||||
|
loop {
|
||||||
|
{
|
||||||
|
let cx = cx.borrow();
|
||||||
|
let cx = cx.as_ref();
|
||||||
|
if predicate(
|
||||||
|
handle
|
||||||
|
.upgrade(cx)
|
||||||
|
.expect("view dropped with pending condition")
|
||||||
|
.read(cx),
|
||||||
|
cx,
|
||||||
|
) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cx.borrow().foreground().start_waiting();
|
||||||
|
rx.recv()
|
||||||
|
.await
|
||||||
|
.expect("view dropped with pending condition");
|
||||||
|
cx.borrow().foreground().finish_waiting();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect("condition timed out");
|
||||||
|
drop(subscriptions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct AssertionContextManager {
|
||||||
|
id: Arc<AtomicUsize>,
|
||||||
|
contexts: Arc<RwLock<BTreeMap<usize, String>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AssertionContextManager {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
id: Arc::new(AtomicUsize::new(0)),
|
||||||
|
contexts: Arc::new(RwLock::new(BTreeMap::new())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_context(&self, context: String) -> ContextHandle {
|
||||||
|
let id = self.id.fetch_add(1, Ordering::Relaxed);
|
||||||
|
let mut contexts = self.contexts.write();
|
||||||
|
contexts.insert(id, context);
|
||||||
|
ContextHandle {
|
||||||
|
id,
|
||||||
|
manager: self.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn context(&self) -> String {
|
||||||
|
let contexts = self.contexts.read();
|
||||||
|
format!("\n{}\n", contexts.values().join("\n"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ContextHandle {
|
||||||
|
id: usize,
|
||||||
|
manager: AssertionContextManager,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for ContextHandle {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let mut contexts = self.manager.contexts.write();
|
||||||
|
contexts.remove(&self.id);
|
||||||
|
}
|
||||||
|
}
|
@ -271,9 +271,6 @@ impl<T: Element> AnyElement for Lifecycle<T> {
|
|||||||
mut layout,
|
mut layout,
|
||||||
} => {
|
} => {
|
||||||
let bounds = RectF::new(origin, size);
|
let bounds = RectF::new(origin, size);
|
||||||
let visible_bounds = visible_bounds
|
|
||||||
.intersection(bounds)
|
|
||||||
.unwrap_or_else(|| RectF::new(bounds.origin(), Vector2F::default()));
|
|
||||||
let paint = element.paint(bounds, visible_bounds, &mut layout, cx);
|
let paint = element.paint(bounds, visible_bounds, &mut layout, cx);
|
||||||
Lifecycle::PostPaint {
|
Lifecycle::PostPaint {
|
||||||
element,
|
element,
|
||||||
@ -292,9 +289,6 @@ impl<T: Element> AnyElement for Lifecycle<T> {
|
|||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
let bounds = RectF::new(origin, bounds.size());
|
let bounds = RectF::new(origin, bounds.size());
|
||||||
let visible_bounds = visible_bounds
|
|
||||||
.intersection(bounds)
|
|
||||||
.unwrap_or_else(|| RectF::new(bounds.origin(), Vector2F::default()));
|
|
||||||
let paint = element.paint(bounds, visible_bounds, &mut layout, cx);
|
let paint = element.paint(bounds, visible_bounds, &mut layout, cx);
|
||||||
Lifecycle::PostPaint {
|
Lifecycle::PostPaint {
|
||||||
element,
|
element,
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
use std::{any::Any, f32::INFINITY, ops::Range};
|
use std::{any::Any, cell::Cell, f32::INFINITY, ops::Range, rc::Rc};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
json::{self, ToJson, Value},
|
json::{self, ToJson, Value},
|
||||||
presenter::MeasurementContext,
|
presenter::MeasurementContext,
|
||||||
Axis, DebugContext, Element, ElementBox, ElementStateHandle, Event, EventContext,
|
Axis, DebugContext, Element, ElementBox, ElementStateHandle, Event, EventContext,
|
||||||
LayoutContext, MouseMovedEvent, PaintContext, RenderContext, ScrollWheelEvent, SizeConstraint,
|
LayoutContext, PaintContext, RenderContext, SizeConstraint, Vector2FExt, View,
|
||||||
Vector2FExt, View,
|
|
||||||
};
|
};
|
||||||
use pathfinder_geometry::{
|
use pathfinder_geometry::{
|
||||||
rect::RectF,
|
rect::RectF,
|
||||||
@ -15,14 +14,14 @@ use serde_json::json;
|
|||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct ScrollState {
|
struct ScrollState {
|
||||||
scroll_to: Option<usize>,
|
scroll_to: Cell<Option<usize>>,
|
||||||
scroll_position: f32,
|
scroll_position: Cell<f32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Flex {
|
pub struct Flex {
|
||||||
axis: Axis,
|
axis: Axis,
|
||||||
children: Vec<ElementBox>,
|
children: Vec<ElementBox>,
|
||||||
scroll_state: Option<ElementStateHandle<ScrollState>>,
|
scroll_state: Option<(ElementStateHandle<Rc<ScrollState>>, usize)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Flex {
|
impl Flex {
|
||||||
@ -52,9 +51,9 @@ impl Flex {
|
|||||||
Tag: 'static,
|
Tag: 'static,
|
||||||
V: View,
|
V: View,
|
||||||
{
|
{
|
||||||
let scroll_state = cx.default_element_state::<Tag, ScrollState>(element_id);
|
let scroll_state = cx.default_element_state::<Tag, Rc<ScrollState>>(element_id);
|
||||||
scroll_state.update(cx, |scroll_state, _| scroll_state.scroll_to = scroll_to);
|
scroll_state.read(cx).scroll_to.set(scroll_to);
|
||||||
self.scroll_state = Some(scroll_state);
|
self.scroll_state = Some((scroll_state, cx.handle().id()));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -202,9 +201,9 @@ impl Element for Flex {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(scroll_state) = self.scroll_state.as_ref() {
|
if let Some(scroll_state) = self.scroll_state.as_ref() {
|
||||||
scroll_state.update(cx, |scroll_state, _| {
|
scroll_state.0.update(cx, |scroll_state, _| {
|
||||||
if let Some(scroll_to) = scroll_state.scroll_to.take() {
|
if let Some(scroll_to) = scroll_state.scroll_to.take() {
|
||||||
let visible_start = scroll_state.scroll_position;
|
let visible_start = scroll_state.scroll_position.get();
|
||||||
let visible_end = visible_start + size.along(self.axis);
|
let visible_end = visible_start + size.along(self.axis);
|
||||||
if let Some(child) = self.children.get(scroll_to) {
|
if let Some(child) = self.children.get(scroll_to) {
|
||||||
let child_start: f32 = self.children[..scroll_to]
|
let child_start: f32 = self.children[..scroll_to]
|
||||||
@ -213,15 +212,22 @@ impl Element for Flex {
|
|||||||
.sum();
|
.sum();
|
||||||
let child_end = child_start + child.size().along(self.axis);
|
let child_end = child_start + child.size().along(self.axis);
|
||||||
if child_start < visible_start {
|
if child_start < visible_start {
|
||||||
scroll_state.scroll_position = child_start;
|
scroll_state.scroll_position.set(child_start);
|
||||||
} else if child_end > visible_end {
|
} else if child_end > visible_end {
|
||||||
scroll_state.scroll_position = child_end - size.along(self.axis);
|
scroll_state
|
||||||
|
.scroll_position
|
||||||
|
.set(child_end - size.along(self.axis));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
scroll_state.scroll_position =
|
scroll_state.scroll_position.set(
|
||||||
scroll_state.scroll_position.min(-remaining_space).max(0.);
|
scroll_state
|
||||||
|
.scroll_position
|
||||||
|
.get()
|
||||||
|
.min(-remaining_space)
|
||||||
|
.max(0.),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -235,16 +241,53 @@ impl Element for Flex {
|
|||||||
remaining_space: &mut Self::LayoutState,
|
remaining_space: &mut Self::LayoutState,
|
||||||
cx: &mut PaintContext,
|
cx: &mut PaintContext,
|
||||||
) -> Self::PaintState {
|
) -> Self::PaintState {
|
||||||
let mut remaining_space = *remaining_space;
|
let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
|
||||||
|
|
||||||
|
let mut remaining_space = *remaining_space;
|
||||||
let overflowing = remaining_space < 0.;
|
let overflowing = remaining_space < 0.;
|
||||||
if overflowing {
|
if overflowing {
|
||||||
cx.scene.push_layer(Some(bounds));
|
cx.scene.push_layer(Some(visible_bounds));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(scroll_state) = &self.scroll_state {
|
||||||
|
cx.scene.push_mouse_region(
|
||||||
|
crate::MouseRegion::new::<Self>(scroll_state.1, 0, bounds)
|
||||||
|
.on_scroll({
|
||||||
|
let scroll_state = scroll_state.0.read(cx).clone();
|
||||||
|
let axis = self.axis;
|
||||||
|
move |e, cx| {
|
||||||
|
if remaining_space < 0. {
|
||||||
|
let mut delta = match axis {
|
||||||
|
Axis::Horizontal => {
|
||||||
|
if e.delta.x() != 0. {
|
||||||
|
e.delta.x()
|
||||||
|
} else {
|
||||||
|
e.delta.y()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Axis::Vertical => e.delta.y(),
|
||||||
|
};
|
||||||
|
if !e.precise {
|
||||||
|
delta *= 20.;
|
||||||
|
}
|
||||||
|
|
||||||
|
scroll_state
|
||||||
|
.scroll_position
|
||||||
|
.set(scroll_state.scroll_position.get() - delta);
|
||||||
|
|
||||||
|
cx.notify();
|
||||||
|
} else {
|
||||||
|
cx.propogate_event();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on_move(|_, _| { /* Capture move events */ }),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut child_origin = bounds.origin();
|
let mut child_origin = bounds.origin();
|
||||||
if let Some(scroll_state) = self.scroll_state.as_ref() {
|
if let Some(scroll_state) = self.scroll_state.as_ref() {
|
||||||
let scroll_position = scroll_state.read(cx).scroll_position;
|
let scroll_position = scroll_state.0.read(cx).scroll_position.get();
|
||||||
match self.axis {
|
match self.axis {
|
||||||
Axis::Horizontal => child_origin.set_x(child_origin.x() - scroll_position),
|
Axis::Horizontal => child_origin.set_x(child_origin.x() - scroll_position),
|
||||||
Axis::Vertical => child_origin.set_y(child_origin.y() - scroll_position),
|
Axis::Vertical => child_origin.set_y(child_origin.y() - scroll_position),
|
||||||
@ -278,9 +321,9 @@ impl Element for Flex {
|
|||||||
fn dispatch_event(
|
fn dispatch_event(
|
||||||
&mut self,
|
&mut self,
|
||||||
event: &Event,
|
event: &Event,
|
||||||
bounds: RectF,
|
|
||||||
_: RectF,
|
_: RectF,
|
||||||
remaining_space: &mut Self::LayoutState,
|
_: RectF,
|
||||||
|
_: &mut Self::LayoutState,
|
||||||
_: &mut Self::PaintState,
|
_: &mut Self::PaintState,
|
||||||
cx: &mut EventContext,
|
cx: &mut EventContext,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
@ -288,50 +331,6 @@ impl Element for Flex {
|
|||||||
for child in &mut self.children {
|
for child in &mut self.children {
|
||||||
handled = child.dispatch_event(event, cx) || handled;
|
handled = child.dispatch_event(event, cx) || handled;
|
||||||
}
|
}
|
||||||
if !handled {
|
|
||||||
if let &Event::ScrollWheel(ScrollWheelEvent {
|
|
||||||
position,
|
|
||||||
delta,
|
|
||||||
precise,
|
|
||||||
..
|
|
||||||
}) = event
|
|
||||||
{
|
|
||||||
if *remaining_space < 0. && bounds.contains_point(position) {
|
|
||||||
if let Some(scroll_state) = self.scroll_state.as_ref() {
|
|
||||||
scroll_state.update(cx, |scroll_state, cx| {
|
|
||||||
let mut delta = match self.axis {
|
|
||||||
Axis::Horizontal => {
|
|
||||||
if delta.x() != 0. {
|
|
||||||
delta.x()
|
|
||||||
} else {
|
|
||||||
delta.y()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Axis::Vertical => delta.y(),
|
|
||||||
};
|
|
||||||
if !precise {
|
|
||||||
delta *= 20.;
|
|
||||||
}
|
|
||||||
|
|
||||||
scroll_state.scroll_position -= delta;
|
|
||||||
|
|
||||||
handled = true;
|
|
||||||
cx.notify();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !handled {
|
|
||||||
if let &Event::MouseMoved(MouseMovedEvent { position, .. }) = event {
|
|
||||||
// If this is a scrollable flex, and the mouse is over it, eat the scroll event to prevent
|
|
||||||
// propogating it to the element below.
|
|
||||||
if self.scroll_state.is_some() && bounds.contains_point(position) {
|
|
||||||
handled = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handled
|
handled
|
||||||
}
|
}
|
||||||
|
@ -27,6 +27,8 @@ pub struct ImageStyle {
|
|||||||
pub height: Option<f32>,
|
pub height: Option<f32>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub width: Option<f32>,
|
pub width: Option<f32>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub grayscale: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Image {
|
impl Image {
|
||||||
@ -74,6 +76,7 @@ impl Element for Image {
|
|||||||
bounds,
|
bounds,
|
||||||
border: self.style.border,
|
border: self.style.border,
|
||||||
corner_radius: self.style.corner_radius,
|
corner_radius: self.style.corner_radius,
|
||||||
|
grayscale: self.style.grayscale,
|
||||||
data: self.data.clone(),
|
data: self.data.clone(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -5,8 +5,8 @@ use crate::{
|
|||||||
},
|
},
|
||||||
json::json,
|
json::json,
|
||||||
presenter::MeasurementContext,
|
presenter::MeasurementContext,
|
||||||
DebugContext, Element, ElementBox, ElementRc, Event, EventContext, LayoutContext, PaintContext,
|
DebugContext, Element, ElementBox, ElementRc, Event, EventContext, LayoutContext, MouseRegion,
|
||||||
RenderContext, ScrollWheelEvent, SizeConstraint, View, ViewContext,
|
PaintContext, RenderContext, SizeConstraint, View, ViewContext,
|
||||||
};
|
};
|
||||||
use std::{cell::RefCell, collections::VecDeque, ops::Range, rc::Rc};
|
use std::{cell::RefCell, collections::VecDeque, ops::Range, rc::Rc};
|
||||||
use sum_tree::{Bias, SumTree};
|
use sum_tree::{Bias, SumTree};
|
||||||
@ -261,7 +261,25 @@ impl Element for List {
|
|||||||
scroll_top: &mut ListOffset,
|
scroll_top: &mut ListOffset,
|
||||||
cx: &mut PaintContext,
|
cx: &mut PaintContext,
|
||||||
) {
|
) {
|
||||||
cx.scene.push_layer(Some(bounds));
|
let visible_bounds = visible_bounds.intersection(bounds).unwrap_or_default();
|
||||||
|
cx.scene.push_layer(Some(visible_bounds));
|
||||||
|
|
||||||
|
cx.scene.push_mouse_region(
|
||||||
|
MouseRegion::new::<Self>(cx.current_view_id(), 0, bounds).on_scroll({
|
||||||
|
let state = self.state.clone();
|
||||||
|
let height = bounds.height();
|
||||||
|
let scroll_top = scroll_top.clone();
|
||||||
|
move |e, cx| {
|
||||||
|
state.0.borrow_mut().scroll(
|
||||||
|
&scroll_top,
|
||||||
|
height,
|
||||||
|
e.platform_event.delta,
|
||||||
|
e.platform_event.precise,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
let state = &mut *self.state.0.borrow_mut();
|
let state = &mut *self.state.0.borrow_mut();
|
||||||
for (mut element, origin) in state.visible_elements(bounds, scroll_top) {
|
for (mut element, origin) in state.visible_elements(bounds, scroll_top) {
|
||||||
@ -312,20 +330,6 @@ impl Element for List {
|
|||||||
drop(cursor);
|
drop(cursor);
|
||||||
state.items = new_items;
|
state.items = new_items;
|
||||||
|
|
||||||
if let Event::ScrollWheel(ScrollWheelEvent {
|
|
||||||
position,
|
|
||||||
delta,
|
|
||||||
precise,
|
|
||||||
..
|
|
||||||
}) = event
|
|
||||||
{
|
|
||||||
if bounds.contains_point(*position)
|
|
||||||
&& state.scroll(scroll_top, bounds.height(), *delta, *precise, cx)
|
|
||||||
{
|
|
||||||
handled = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handled
|
handled
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -527,7 +531,7 @@ impl StateInner {
|
|||||||
mut delta: Vector2F,
|
mut delta: Vector2F,
|
||||||
precise: bool,
|
precise: bool,
|
||||||
cx: &mut EventContext,
|
cx: &mut EventContext,
|
||||||
) -> bool {
|
) {
|
||||||
if !precise {
|
if !precise {
|
||||||
delta *= 20.;
|
delta *= 20.;
|
||||||
}
|
}
|
||||||
@ -554,9 +558,8 @@ impl StateInner {
|
|||||||
let visible_range = self.visible_range(height, scroll_top);
|
let visible_range = self.visible_range(height, scroll_top);
|
||||||
self.scroll_handler.as_mut().unwrap()(visible_range, cx);
|
self.scroll_handler.as_mut().unwrap()(visible_range, cx);
|
||||||
}
|
}
|
||||||
cx.notify();
|
|
||||||
|
|
||||||
true
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn scroll_top(&self, logical_scroll_top: &ListOffset) -> f32 {
|
fn scroll_top(&self, logical_scroll_top: &ListOffset) -> f32 {
|
||||||
|
@ -7,7 +7,8 @@ use crate::{
|
|||||||
platform::CursorStyle,
|
platform::CursorStyle,
|
||||||
scene::{
|
scene::{
|
||||||
ClickRegionEvent, CursorRegion, DownOutRegionEvent, DownRegionEvent, DragRegionEvent,
|
ClickRegionEvent, CursorRegion, DownOutRegionEvent, DownRegionEvent, DragRegionEvent,
|
||||||
HandlerSet, HoverRegionEvent, MoveRegionEvent, UpOutRegionEvent, UpRegionEvent,
|
HandlerSet, HoverRegionEvent, MoveRegionEvent, ScrollWheelRegionEvent, UpOutRegionEvent,
|
||||||
|
UpRegionEvent,
|
||||||
},
|
},
|
||||||
DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, MeasurementContext,
|
DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, MeasurementContext,
|
||||||
MouseButton, MouseRegion, MouseState, PaintContext, RenderContext, SizeConstraint, View,
|
MouseButton, MouseRegion, MouseState, PaintContext, RenderContext, SizeConstraint, View,
|
||||||
@ -21,6 +22,8 @@ pub struct MouseEventHandler<Tag: 'static> {
|
|||||||
cursor_style: Option<CursorStyle>,
|
cursor_style: Option<CursorStyle>,
|
||||||
handlers: HandlerSet,
|
handlers: HandlerSet,
|
||||||
hoverable: bool,
|
hoverable: bool,
|
||||||
|
notify_on_hover: bool,
|
||||||
|
notify_on_click: bool,
|
||||||
padding: Padding,
|
padding: Padding,
|
||||||
_tag: PhantomData<Tag>,
|
_tag: PhantomData<Tag>,
|
||||||
}
|
}
|
||||||
@ -29,13 +32,19 @@ impl<Tag> MouseEventHandler<Tag> {
|
|||||||
pub fn new<V, F>(region_id: usize, cx: &mut RenderContext<V>, render_child: F) -> Self
|
pub fn new<V, F>(region_id: usize, cx: &mut RenderContext<V>, render_child: F) -> Self
|
||||||
where
|
where
|
||||||
V: View,
|
V: View,
|
||||||
F: FnOnce(MouseState, &mut RenderContext<V>) -> ElementBox,
|
F: FnOnce(&mut MouseState, &mut RenderContext<V>) -> ElementBox,
|
||||||
{
|
{
|
||||||
|
let mut mouse_state = cx.mouse_state::<Tag>(region_id);
|
||||||
|
let child = render_child(&mut mouse_state, cx);
|
||||||
|
let notify_on_hover = mouse_state.accessed_hovered();
|
||||||
|
let notify_on_click = mouse_state.accessed_clicked();
|
||||||
Self {
|
Self {
|
||||||
child: render_child(cx.mouse_state::<Tag>(region_id), cx),
|
child,
|
||||||
region_id,
|
region_id,
|
||||||
cursor_style: None,
|
cursor_style: None,
|
||||||
handlers: Default::default(),
|
handlers: Default::default(),
|
||||||
|
notify_on_hover,
|
||||||
|
notify_on_click,
|
||||||
hoverable: true,
|
hoverable: true,
|
||||||
padding: Default::default(),
|
padding: Default::default(),
|
||||||
_tag: PhantomData,
|
_tag: PhantomData,
|
||||||
@ -122,6 +131,14 @@ impl<Tag> MouseEventHandler<Tag> {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn on_scroll(
|
||||||
|
mut self,
|
||||||
|
handler: impl Fn(ScrollWheelRegionEvent, &mut EventContext) + 'static,
|
||||||
|
) -> Self {
|
||||||
|
self.handlers = self.handlers.on_scroll(handler);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn with_hoverable(mut self, is_hoverable: bool) -> Self {
|
pub fn with_hoverable(mut self, is_hoverable: bool) -> Self {
|
||||||
self.hoverable = is_hoverable;
|
self.hoverable = is_hoverable;
|
||||||
self
|
self
|
||||||
@ -160,6 +177,7 @@ impl<Tag> Element for MouseEventHandler<Tag> {
|
|||||||
_: &mut Self::LayoutState,
|
_: &mut Self::LayoutState,
|
||||||
cx: &mut PaintContext,
|
cx: &mut PaintContext,
|
||||||
) -> Self::PaintState {
|
) -> Self::PaintState {
|
||||||
|
let visible_bounds = visible_bounds.intersection(bounds).unwrap_or_default();
|
||||||
let hit_bounds = self.hit_bounds(visible_bounds);
|
let hit_bounds = self.hit_bounds(visible_bounds);
|
||||||
if let Some(style) = self.cursor_style {
|
if let Some(style) = self.cursor_style {
|
||||||
cx.scene.push_cursor_region(CursorRegion {
|
cx.scene.push_cursor_region(CursorRegion {
|
||||||
@ -175,7 +193,9 @@ impl<Tag> Element for MouseEventHandler<Tag> {
|
|||||||
hit_bounds,
|
hit_bounds,
|
||||||
self.handlers.clone(),
|
self.handlers.clone(),
|
||||||
)
|
)
|
||||||
.with_hoverable(self.hoverable),
|
.with_hoverable(self.hoverable)
|
||||||
|
.with_notify_on_hover(self.notify_on_hover)
|
||||||
|
.with_notify_on_click(self.notify_on_click),
|
||||||
);
|
);
|
||||||
|
|
||||||
self.child.paint(bounds.origin(), visible_bounds, cx);
|
self.child.paint(bounds.origin(), visible_bounds, cx);
|
||||||
|
@ -14,6 +14,7 @@ pub struct Overlay {
|
|||||||
anchor_position: Option<Vector2F>,
|
anchor_position: Option<Vector2F>,
|
||||||
anchor_corner: AnchorCorner,
|
anchor_corner: AnchorCorner,
|
||||||
fit_mode: OverlayFitMode,
|
fit_mode: OverlayFitMode,
|
||||||
|
position_mode: OverlayPositionMode,
|
||||||
hoverable: bool,
|
hoverable: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -24,6 +25,12 @@ pub enum OverlayFitMode {
|
|||||||
None,
|
None,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||||
|
pub enum OverlayPositionMode {
|
||||||
|
Window,
|
||||||
|
Local,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum AnchorCorner {
|
pub enum AnchorCorner {
|
||||||
TopLeft,
|
TopLeft,
|
||||||
@ -73,6 +80,7 @@ impl Overlay {
|
|||||||
anchor_position: None,
|
anchor_position: None,
|
||||||
anchor_corner: AnchorCorner::TopLeft,
|
anchor_corner: AnchorCorner::TopLeft,
|
||||||
fit_mode: OverlayFitMode::None,
|
fit_mode: OverlayFitMode::None,
|
||||||
|
position_mode: OverlayPositionMode::Window,
|
||||||
hoverable: false,
|
hoverable: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -92,6 +100,11 @@ impl Overlay {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn with_position_mode(mut self, position_mode: OverlayPositionMode) -> Self {
|
||||||
|
self.position_mode = position_mode;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn with_hoverable(mut self, hoverable: bool) -> Self {
|
pub fn with_hoverable(mut self, hoverable: bool) -> Self {
|
||||||
self.hoverable = hoverable;
|
self.hoverable = hoverable;
|
||||||
self
|
self
|
||||||
@ -123,8 +136,20 @@ impl Element for Overlay {
|
|||||||
size: &mut Self::LayoutState,
|
size: &mut Self::LayoutState,
|
||||||
cx: &mut PaintContext,
|
cx: &mut PaintContext,
|
||||||
) {
|
) {
|
||||||
|
let (anchor_position, mut bounds) = match self.position_mode {
|
||||||
|
OverlayPositionMode::Window => {
|
||||||
let anchor_position = self.anchor_position.unwrap_or_else(|| bounds.origin());
|
let anchor_position = self.anchor_position.unwrap_or_else(|| bounds.origin());
|
||||||
let mut bounds = self.anchor_corner.get_bounds(anchor_position, *size);
|
let bounds = self.anchor_corner.get_bounds(anchor_position, *size);
|
||||||
|
(anchor_position, bounds)
|
||||||
|
}
|
||||||
|
OverlayPositionMode::Local => {
|
||||||
|
let anchor_position = self.anchor_position.unwrap_or_default();
|
||||||
|
let bounds = self
|
||||||
|
.anchor_corner
|
||||||
|
.get_bounds(bounds.origin() + anchor_position, *size);
|
||||||
|
(anchor_position, bounds)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
match self.fit_mode {
|
match self.fit_mode {
|
||||||
OverlayFitMode::SnapToWindow => {
|
OverlayFitMode::SnapToWindow => {
|
||||||
@ -192,7 +217,11 @@ impl Element for Overlay {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
self.child.paint(bounds.origin(), bounds, cx);
|
self.child.paint(
|
||||||
|
bounds.origin(),
|
||||||
|
RectF::new(Vector2F::zero(), cx.window_size),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
cx.scene.pop_stacking_context();
|
cx.scene.pop_stacking_context();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,10 +36,10 @@ struct TooltipState {
|
|||||||
#[derive(Clone, Deserialize, Default)]
|
#[derive(Clone, Deserialize, Default)]
|
||||||
pub struct TooltipStyle {
|
pub struct TooltipStyle {
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
container: ContainerStyle,
|
pub container: ContainerStyle,
|
||||||
text: TextStyle,
|
pub text: TextStyle,
|
||||||
keystroke: KeystrokeStyle,
|
keystroke: KeystrokeStyle,
|
||||||
max_text_width: f32,
|
pub max_text_width: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Deserialize, Default)]
|
#[derive(Clone, Deserialize, Default)]
|
||||||
@ -126,7 +126,7 @@ impl Tooltip {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_tooltip(
|
pub fn render_tooltip(
|
||||||
text: String,
|
text: String,
|
||||||
style: TooltipStyle,
|
style: TooltipStyle,
|
||||||
action: Option<Box<dyn Action>>,
|
action: Option<Box<dyn Action>>,
|
||||||
|
@ -6,7 +6,8 @@ use crate::{
|
|||||||
},
|
},
|
||||||
json::{self, json},
|
json::{self, json},
|
||||||
presenter::MeasurementContext,
|
presenter::MeasurementContext,
|
||||||
ElementBox, RenderContext, ScrollWheelEvent, View,
|
scene::ScrollWheelRegionEvent,
|
||||||
|
ElementBox, MouseRegion, RenderContext, ScrollWheelEvent, View,
|
||||||
};
|
};
|
||||||
use json::ToJson;
|
use json::ToJson;
|
||||||
use std::{cell::RefCell, cmp, ops::Range, rc::Rc};
|
use std::{cell::RefCell, cmp, ops::Range, rc::Rc};
|
||||||
@ -50,6 +51,7 @@ pub struct UniformList {
|
|||||||
padding_top: f32,
|
padding_top: f32,
|
||||||
padding_bottom: f32,
|
padding_bottom: f32,
|
||||||
get_width_from_item: Option<usize>,
|
get_width_from_item: Option<usize>,
|
||||||
|
view_id: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UniformList {
|
impl UniformList {
|
||||||
@ -77,6 +79,7 @@ impl UniformList {
|
|||||||
padding_top: 0.,
|
padding_top: 0.,
|
||||||
padding_bottom: 0.,
|
padding_bottom: 0.,
|
||||||
get_width_from_item: None,
|
get_width_from_item: None,
|
||||||
|
view_id: cx.handle().id(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -96,7 +99,7 @@ impl UniformList {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn scroll(
|
fn scroll(
|
||||||
&self,
|
state: UniformListState,
|
||||||
_: Vector2F,
|
_: Vector2F,
|
||||||
mut delta: Vector2F,
|
mut delta: Vector2F,
|
||||||
precise: bool,
|
precise: bool,
|
||||||
@ -107,7 +110,7 @@ impl UniformList {
|
|||||||
delta *= 20.;
|
delta *= 20.;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut state = self.state.0.borrow_mut();
|
let mut state = state.0.borrow_mut();
|
||||||
state.scroll_top = (state.scroll_top - delta.y()).max(0.0).min(scroll_max);
|
state.scroll_top = (state.scroll_top - delta.y()).max(0.0).min(scroll_max);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
|
|
||||||
@ -281,7 +284,31 @@ impl Element for UniformList {
|
|||||||
layout: &mut Self::LayoutState,
|
layout: &mut Self::LayoutState,
|
||||||
cx: &mut PaintContext,
|
cx: &mut PaintContext,
|
||||||
) -> Self::PaintState {
|
) -> Self::PaintState {
|
||||||
cx.scene.push_layer(Some(bounds));
|
let visible_bounds = visible_bounds.intersection(bounds).unwrap_or_default();
|
||||||
|
|
||||||
|
cx.scene.push_layer(Some(visible_bounds));
|
||||||
|
|
||||||
|
cx.scene.push_mouse_region(
|
||||||
|
MouseRegion::new::<Self>(self.view_id, 0, visible_bounds).on_scroll({
|
||||||
|
let scroll_max = layout.scroll_max;
|
||||||
|
let state = self.state.clone();
|
||||||
|
move |ScrollWheelRegionEvent {
|
||||||
|
platform_event:
|
||||||
|
ScrollWheelEvent {
|
||||||
|
position,
|
||||||
|
delta,
|
||||||
|
precise,
|
||||||
|
..
|
||||||
|
},
|
||||||
|
..
|
||||||
|
},
|
||||||
|
cx| {
|
||||||
|
if !Self::scroll(state.clone(), position, delta, precise, scroll_max, cx) {
|
||||||
|
cx.propogate_event();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
let mut item_origin = bounds.origin()
|
let mut item_origin = bounds.origin()
|
||||||
- vec2f(
|
- vec2f(
|
||||||
@ -300,7 +327,7 @@ impl Element for UniformList {
|
|||||||
fn dispatch_event(
|
fn dispatch_event(
|
||||||
&mut self,
|
&mut self,
|
||||||
event: &Event,
|
event: &Event,
|
||||||
bounds: RectF,
|
_: RectF,
|
||||||
_: RectF,
|
_: RectF,
|
||||||
layout: &mut Self::LayoutState,
|
layout: &mut Self::LayoutState,
|
||||||
_: &mut Self::PaintState,
|
_: &mut Self::PaintState,
|
||||||
@ -311,20 +338,6 @@ impl Element for UniformList {
|
|||||||
handled = item.dispatch_event(event, cx) || handled;
|
handled = item.dispatch_event(event, cx) || handled;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Event::ScrollWheel(ScrollWheelEvent {
|
|
||||||
position,
|
|
||||||
delta,
|
|
||||||
precise,
|
|
||||||
..
|
|
||||||
}) = event
|
|
||||||
{
|
|
||||||
if bounds.contains_point(*position)
|
|
||||||
&& self.scroll(*position, *delta, *precise, layout.scroll_max, cx)
|
|
||||||
{
|
|
||||||
handled = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handled
|
handled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -325,7 +325,12 @@ impl Deterministic {
|
|||||||
let mut state = self.state.lock();
|
let mut state = self.state.lock();
|
||||||
let wakeup_at = state.now + duration;
|
let wakeup_at = state.now + duration;
|
||||||
let id = util::post_inc(&mut state.next_timer_id);
|
let id = util::post_inc(&mut state.next_timer_id);
|
||||||
state.pending_timers.push((id, wakeup_at, tx));
|
match state
|
||||||
|
.pending_timers
|
||||||
|
.binary_search_by_key(&wakeup_at, |e| e.1)
|
||||||
|
{
|
||||||
|
Ok(ix) | Err(ix) => state.pending_timers.insert(ix, (id, wakeup_at, tx)),
|
||||||
|
}
|
||||||
let state = self.state.clone();
|
let state = self.state.clone();
|
||||||
Timer::Deterministic(DeterministicTimer { rx, id, state })
|
Timer::Deterministic(DeterministicTimer { rx, id, state })
|
||||||
}
|
}
|
||||||
|
@ -44,6 +44,8 @@ pub trait Platform: Send + Sync {
|
|||||||
fn unhide_other_apps(&self);
|
fn unhide_other_apps(&self);
|
||||||
fn quit(&self);
|
fn quit(&self);
|
||||||
|
|
||||||
|
fn screen_size(&self) -> Vector2F;
|
||||||
|
|
||||||
fn open_window(
|
fn open_window(
|
||||||
&self,
|
&self,
|
||||||
id: usize,
|
id: usize,
|
||||||
@ -63,12 +65,15 @@ pub trait Platform: Send + Sync {
|
|||||||
fn delete_credentials(&self, url: &str) -> Result<()>;
|
fn delete_credentials(&self, url: &str) -> Result<()>;
|
||||||
|
|
||||||
fn set_cursor_style(&self, style: CursorStyle);
|
fn set_cursor_style(&self, style: CursorStyle);
|
||||||
|
fn should_auto_hide_scrollbars(&self) -> bool;
|
||||||
|
|
||||||
fn local_timezone(&self) -> UtcOffset;
|
fn local_timezone(&self) -> UtcOffset;
|
||||||
|
|
||||||
fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf>;
|
fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf>;
|
||||||
fn app_path(&self) -> Result<PathBuf>;
|
fn app_path(&self) -> Result<PathBuf>;
|
||||||
fn app_version(&self) -> Result<AppVersion>;
|
fn app_version(&self) -> Result<AppVersion>;
|
||||||
|
fn os_name(&self) -> &'static str;
|
||||||
|
fn os_version(&self) -> Result<AppVersion>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) trait ForegroundPlatform {
|
pub(crate) trait ForegroundPlatform {
|
||||||
|
@ -14,8 +14,10 @@ use core_graphics::{
|
|||||||
event::{CGEvent, CGEventFlags, CGKeyCode},
|
event::{CGEvent, CGEventFlags, CGKeyCode},
|
||||||
event_source::{CGEventSource, CGEventSourceStateID},
|
event_source::{CGEventSource, CGEventSourceStateID},
|
||||||
};
|
};
|
||||||
|
use ctor::ctor;
|
||||||
|
use foreign_types::ForeignType;
|
||||||
use objc::{class, msg_send, sel, sel_impl};
|
use objc::{class, msg_send, sel, sel_impl};
|
||||||
use std::{borrow::Cow, ffi::CStr, os::raw::c_char};
|
use std::{borrow::Cow, ffi::CStr, mem, os::raw::c_char, ptr};
|
||||||
|
|
||||||
const BACKSPACE_KEY: u16 = 0x7f;
|
const BACKSPACE_KEY: u16 = 0x7f;
|
||||||
const SPACE_KEY: u16 = b' ' as u16;
|
const SPACE_KEY: u16 = b' ' as u16;
|
||||||
@ -25,6 +27,15 @@ const ESCAPE_KEY: u16 = 0x1b;
|
|||||||
const TAB_KEY: u16 = 0x09;
|
const TAB_KEY: u16 = 0x09;
|
||||||
const SHIFT_TAB_KEY: u16 = 0x19;
|
const SHIFT_TAB_KEY: u16 = 0x19;
|
||||||
|
|
||||||
|
static mut EVENT_SOURCE: core_graphics::sys::CGEventSourceRef = ptr::null_mut();
|
||||||
|
|
||||||
|
#[ctor]
|
||||||
|
unsafe fn build_event_source() {
|
||||||
|
let source = CGEventSource::new(CGEventSourceStateID::Private).unwrap();
|
||||||
|
EVENT_SOURCE = source.as_ptr();
|
||||||
|
mem::forget(source);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn key_to_native(key: &str) -> Cow<str> {
|
pub fn key_to_native(key: &str) -> Cow<str> {
|
||||||
use cocoa::appkit::*;
|
use cocoa::appkit::*;
|
||||||
let code = match key {
|
let code = match key {
|
||||||
@ -228,7 +239,8 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
|
|||||||
let mut chars_ignoring_modifiers =
|
let mut chars_ignoring_modifiers =
|
||||||
CStr::from_ptr(native_event.charactersIgnoringModifiers().UTF8String() as *mut c_char)
|
CStr::from_ptr(native_event.charactersIgnoringModifiers().UTF8String() as *mut c_char)
|
||||||
.to_str()
|
.to_str()
|
||||||
.unwrap();
|
.unwrap()
|
||||||
|
.to_string();
|
||||||
let first_char = chars_ignoring_modifiers.chars().next().map(|ch| ch as u16);
|
let first_char = chars_ignoring_modifiers.chars().next().map(|ch| ch as u16);
|
||||||
let modifiers = native_event.modifierFlags();
|
let modifiers = native_event.modifierFlags();
|
||||||
|
|
||||||
@ -243,31 +255,31 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
|
|||||||
|
|
||||||
#[allow(non_upper_case_globals)]
|
#[allow(non_upper_case_globals)]
|
||||||
let key = match first_char {
|
let key = match first_char {
|
||||||
Some(SPACE_KEY) => "space",
|
Some(SPACE_KEY) => "space".to_string(),
|
||||||
Some(BACKSPACE_KEY) => "backspace",
|
Some(BACKSPACE_KEY) => "backspace".to_string(),
|
||||||
Some(ENTER_KEY) | Some(NUMPAD_ENTER_KEY) => "enter",
|
Some(ENTER_KEY) | Some(NUMPAD_ENTER_KEY) => "enter".to_string(),
|
||||||
Some(ESCAPE_KEY) => "escape",
|
Some(ESCAPE_KEY) => "escape".to_string(),
|
||||||
Some(TAB_KEY) => "tab",
|
Some(TAB_KEY) => "tab".to_string(),
|
||||||
Some(SHIFT_TAB_KEY) => "tab",
|
Some(SHIFT_TAB_KEY) => "tab".to_string(),
|
||||||
Some(NSUpArrowFunctionKey) => "up",
|
Some(NSUpArrowFunctionKey) => "up".to_string(),
|
||||||
Some(NSDownArrowFunctionKey) => "down",
|
Some(NSDownArrowFunctionKey) => "down".to_string(),
|
||||||
Some(NSLeftArrowFunctionKey) => "left",
|
Some(NSLeftArrowFunctionKey) => "left".to_string(),
|
||||||
Some(NSRightArrowFunctionKey) => "right",
|
Some(NSRightArrowFunctionKey) => "right".to_string(),
|
||||||
Some(NSPageUpFunctionKey) => "pageup",
|
Some(NSPageUpFunctionKey) => "pageup".to_string(),
|
||||||
Some(NSPageDownFunctionKey) => "pagedown",
|
Some(NSPageDownFunctionKey) => "pagedown".to_string(),
|
||||||
Some(NSDeleteFunctionKey) => "delete",
|
Some(NSDeleteFunctionKey) => "delete".to_string(),
|
||||||
Some(NSF1FunctionKey) => "f1",
|
Some(NSF1FunctionKey) => "f1".to_string(),
|
||||||
Some(NSF2FunctionKey) => "f2",
|
Some(NSF2FunctionKey) => "f2".to_string(),
|
||||||
Some(NSF3FunctionKey) => "f3",
|
Some(NSF3FunctionKey) => "f3".to_string(),
|
||||||
Some(NSF4FunctionKey) => "f4",
|
Some(NSF4FunctionKey) => "f4".to_string(),
|
||||||
Some(NSF5FunctionKey) => "f5",
|
Some(NSF5FunctionKey) => "f5".to_string(),
|
||||||
Some(NSF6FunctionKey) => "f6",
|
Some(NSF6FunctionKey) => "f6".to_string(),
|
||||||
Some(NSF7FunctionKey) => "f7",
|
Some(NSF7FunctionKey) => "f7".to_string(),
|
||||||
Some(NSF8FunctionKey) => "f8",
|
Some(NSF8FunctionKey) => "f8".to_string(),
|
||||||
Some(NSF9FunctionKey) => "f9",
|
Some(NSF9FunctionKey) => "f9".to_string(),
|
||||||
Some(NSF10FunctionKey) => "f10",
|
Some(NSF10FunctionKey) => "f10".to_string(),
|
||||||
Some(NSF11FunctionKey) => "f11",
|
Some(NSF11FunctionKey) => "f11".to_string(),
|
||||||
Some(NSF12FunctionKey) => "f12",
|
Some(NSF12FunctionKey) => "f12".to_string(),
|
||||||
_ => {
|
_ => {
|
||||||
let mut chars_ignoring_modifiers_and_shift =
|
let mut chars_ignoring_modifiers_and_shift =
|
||||||
chars_for_modified_key(native_event.keyCode(), false, false);
|
chars_for_modified_key(native_event.keyCode(), false, false);
|
||||||
@ -303,21 +315,19 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
|
|||||||
shift,
|
shift,
|
||||||
cmd,
|
cmd,
|
||||||
function,
|
function,
|
||||||
key: key.into(),
|
key,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn chars_for_modified_key<'a>(code: CGKeyCode, cmd: bool, shift: bool) -> &'a str {
|
fn chars_for_modified_key(code: CGKeyCode, cmd: bool, shift: bool) -> String {
|
||||||
// Ideally, we would use `[NSEvent charactersByApplyingModifiers]` but that
|
// Ideally, we would use `[NSEvent charactersByApplyingModifiers]` but that
|
||||||
// always returns an empty string with certain keyboards, e.g. Japanese. Synthesizing
|
// always returns an empty string with certain keyboards, e.g. Japanese. Synthesizing
|
||||||
// an event with the given flags instead lets us access `characters`, which always
|
// an event with the given flags instead lets us access `characters`, which always
|
||||||
// returns a valid string.
|
// returns a valid string.
|
||||||
let event = CGEvent::new_keyboard_event(
|
let source = unsafe { core_graphics::event_source::CGEventSource::from_ptr(EVENT_SOURCE) };
|
||||||
CGEventSource::new(CGEventSourceStateID::Private).unwrap(),
|
let event = CGEvent::new_keyboard_event(source.clone(), code, true).unwrap();
|
||||||
code,
|
mem::forget(source);
|
||||||
true,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
let mut flags = CGEventFlags::empty();
|
let mut flags = CGEventFlags::empty();
|
||||||
if cmd {
|
if cmd {
|
||||||
flags |= CGEventFlags::CGEventFlagCommand;
|
flags |= CGEventFlags::CGEventFlagCommand;
|
||||||
@ -327,10 +337,11 @@ fn chars_for_modified_key<'a>(code: CGKeyCode, cmd: bool, shift: bool) -> &'a st
|
|||||||
}
|
}
|
||||||
event.set_flags(flags);
|
event.set_flags(flags);
|
||||||
|
|
||||||
let event: id = unsafe { msg_send![class!(NSEvent), eventWithCGEvent: event] };
|
|
||||||
unsafe {
|
unsafe {
|
||||||
|
let event: id = msg_send![class!(NSEvent), eventWithCGEvent: &*event];
|
||||||
CStr::from_ptr(event.characters().UTF8String())
|
CStr::from_ptr(event.characters().UTF8String())
|
||||||
.to_str()
|
.to_str()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
.to_string()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,9 +2,11 @@ use super::{
|
|||||||
event::key_to_native, status_item::StatusItem, BoolExt as _, Dispatcher, FontSystem, Window,
|
event::key_to_native, status_item::StatusItem, BoolExt as _, Dispatcher, FontSystem, Window,
|
||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
executor, keymap,
|
executor,
|
||||||
|
geometry::vector::{vec2f, Vector2F},
|
||||||
|
keymap,
|
||||||
platform::{self, CursorStyle},
|
platform::{self, CursorStyle},
|
||||||
Action, ClipboardItem, Event, Menu, MenuItem,
|
Action, AppVersion, ClipboardItem, Event, Menu, MenuItem,
|
||||||
};
|
};
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use block::ConcreteBlock;
|
use block::ConcreteBlock;
|
||||||
@ -12,11 +14,12 @@ use cocoa::{
|
|||||||
appkit::{
|
appkit::{
|
||||||
NSApplication, NSApplicationActivationPolicy::NSApplicationActivationPolicyRegular,
|
NSApplication, NSApplicationActivationPolicy::NSApplicationActivationPolicyRegular,
|
||||||
NSEventModifierFlags, NSMenu, NSMenuItem, NSModalResponse, NSOpenPanel, NSPasteboard,
|
NSEventModifierFlags, NSMenu, NSMenuItem, NSModalResponse, NSOpenPanel, NSPasteboard,
|
||||||
NSPasteboardTypeString, NSSavePanel, NSWindow,
|
NSPasteboardTypeString, NSSavePanel, NSScreen, NSWindow,
|
||||||
},
|
},
|
||||||
base::{id, nil, selector, YES},
|
base::{id, nil, selector, YES},
|
||||||
foundation::{
|
foundation::{
|
||||||
NSArray, NSAutoreleasePool, NSBundle, NSData, NSInteger, NSString, NSUInteger, NSURL,
|
NSArray, NSAutoreleasePool, NSBundle, NSData, NSInteger, NSProcessInfo, NSString,
|
||||||
|
NSUInteger, NSURL,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use core_foundation::{
|
use core_foundation::{
|
||||||
@ -485,6 +488,14 @@ impl platform::Platform for MacPlatform {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn screen_size(&self) -> Vector2F {
|
||||||
|
unsafe {
|
||||||
|
let screen = NSScreen::mainScreen(nil);
|
||||||
|
let frame = NSScreen::frame(screen);
|
||||||
|
vec2f(frame.size.width as f32, frame.size.height as f32)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn open_window(
|
fn open_window(
|
||||||
&self,
|
&self,
|
||||||
id: usize,
|
id: usize,
|
||||||
@ -698,6 +709,16 @@ impl platform::Platform for MacPlatform {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn should_auto_hide_scrollbars(&self) -> bool {
|
||||||
|
#[allow(non_upper_case_globals)]
|
||||||
|
const NSScrollerStyleOverlay: NSInteger = 1;
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
let style: NSInteger = msg_send![class!(NSScroller), preferredScrollerStyle];
|
||||||
|
style == NSScrollerStyleOverlay
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn local_timezone(&self) -> UtcOffset {
|
fn local_timezone(&self) -> UtcOffset {
|
||||||
unsafe {
|
unsafe {
|
||||||
let local_timezone: id = msg_send![class!(NSTimeZone), localTimeZone];
|
let local_timezone: id = msg_send![class!(NSTimeZone), localTimeZone];
|
||||||
@ -748,6 +769,22 @@ impl platform::Platform for MacPlatform {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn os_name(&self) -> &'static str {
|
||||||
|
"macOS"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn os_version(&self) -> Result<crate::AppVersion> {
|
||||||
|
unsafe {
|
||||||
|
let process_info = NSProcessInfo::processInfo(nil);
|
||||||
|
let version = process_info.operatingSystemVersion();
|
||||||
|
Ok(AppVersion {
|
||||||
|
major: version.majorVersion as usize,
|
||||||
|
minor: version.minorVersion as usize,
|
||||||
|
patch: version.patchVersion as usize,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
unsafe fn path_from_objc(path: id) -> PathBuf {
|
unsafe fn path_from_objc(path: id) -> PathBuf {
|
||||||
|
@ -747,6 +747,7 @@ impl Renderer {
|
|||||||
border_left: border_width * (image.border.left as usize as f32),
|
border_left: border_width * (image.border.left as usize as f32),
|
||||||
border_color: image.border.color.to_uchar4(),
|
border_color: image.border.color.to_uchar4(),
|
||||||
corner_radius,
|
corner_radius,
|
||||||
|
grayscale: image.grayscale as u8,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -769,6 +770,7 @@ impl Renderer {
|
|||||||
border_left: 0.,
|
border_left: 0.,
|
||||||
border_color: Default::default(),
|
border_color: Default::default(),
|
||||||
corner_radius: 0.,
|
corner_radius: 0.,
|
||||||
|
grayscale: false as u8,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
log::warn!("could not render glyph with id {}", image_glyph.id);
|
log::warn!("could not render glyph with id {}", image_glyph.id);
|
||||||
|
@ -90,6 +90,7 @@ typedef struct {
|
|||||||
float border_left;
|
float border_left;
|
||||||
vector_uchar4 border_color;
|
vector_uchar4 border_color;
|
||||||
float corner_radius;
|
float corner_radius;
|
||||||
|
uint8_t grayscale;
|
||||||
} GPUIImage;
|
} GPUIImage;
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user