diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2c660b7a0d3883b0bac1e45bdce56a85fd0e6108..4340ac1cb4d23182ea95fc44c2dec2a3bf63e39f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,149 +1,148 @@ name: CI on: - push: - branches: - - main - - "v[0-9]+.[0-9]+.x" - tags: - - "v*" - pull_request: - branches: - - "**" + push: + branches: + - main + - "v[0-9]+.[0-9]+.x" + tags: + - "v*" + pull_request: + branches: + - "**" concurrency: - # Allow only one workflow per any non-`main` branch. - group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }} - cancel-in-progress: true + # Allow only one workflow per any non-`main` branch. + group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }} + cancel-in-progress: true env: - CARGO_TERM_COLOR: always - CARGO_INCREMENTAL: 0 - RUST_BACKTRACE: 1 + CARGO_TERM_COLOR: always + CARGO_INCREMENTAL: 0 + RUST_BACKTRACE: 1 jobs: - style: - name: Check formatting, Clippy lints, and spelling - runs-on: - - self-hosted - - test - steps: - - name: Checkout repo - uses: actions/checkout@v3 - with: - clean: false - submodules: "recursive" - fetch-depth: 0 - - - name: Set up default .cargo/config.toml - run: cp ./.cargo/ci-config.toml ~/.cargo/config.toml - - - name: Check spelling - run: | - if ! which typos > /dev/null; then - cargo install typos-cli - fi - typos - - - name: Run style checks - uses: ./.github/actions/check_style - - tests: - name: Run tests - runs-on: - - self-hosted - - test - needs: style - steps: - - name: Checkout repo - uses: actions/checkout@v3 - with: - clean: false - submodules: "recursive" - - - name: Run tests - uses: ./.github/actions/run_tests - - - name: Build collab - run: cargo build -p collab - - - name: Build other binaries - run: cargo build --workspace --bins --all-features - - bundle: - name: Bundle app - runs-on: - - self-hosted - - bundle - if: ${{ startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-build-dmg') }} - needs: tests + style: + name: Check formatting, Clippy lints, and spelling + runs-on: + - self-hosted + - test + steps: + - name: Checkout repo + uses: actions/checkout@v3 + with: + clean: false + submodules: "recursive" + fetch-depth: 0 + + - name: Set up default .cargo/config.toml + run: cp ./.cargo/ci-config.toml ~/.cargo/config.toml + + - name: Check spelling + run: | + if ! which typos > /dev/null; then + cargo install typos-cli + fi + typos + + - name: Run style checks + uses: ./.github/actions/check_style + + tests: + name: Run tests + runs-on: + - self-hosted + - test + steps: + - name: Checkout repo + uses: actions/checkout@v3 + with: + clean: false + submodules: "recursive" + + - name: Run tests + uses: ./.github/actions/run_tests + + - name: Build collab + run: cargo build -p collab + + - name: Build other binaries + run: cargo build --workspace --bins --all-features + + bundle: + name: Bundle app + runs-on: + - self-hosted + - bundle + if: ${{ startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-build-dmg') }} + needs: tests + env: + MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }} + MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} + APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }} + APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }} + steps: + - name: Install Node + uses: actions/setup-node@v3 + with: + node-version: "18" + + - name: Checkout repo + uses: actions/checkout@v3 + with: + clean: false + submodules: "recursive" + + - name: Limit target directory size + run: script/clear-target-dir-if-larger-than 100 + + - name: Determine version and release channel + if: ${{ startsWith(github.ref, 'refs/tags/v') }} + run: | + set -eu + + version=$(script/get-crate-version zed) + channel=$(cat crates/zed/RELEASE_CHANNEL) + echo "Publishing version: ${version} on release channel ${channel}" + echo "RELEASE_CHANNEL=${channel}" >> $GITHUB_ENV + + expected_tag_name="" + case ${channel} in + stable) + expected_tag_name="v${version}";; + preview) + expected_tag_name="v${version}-pre";; + nightly) + expected_tag_name="v${version}-nightly";; + *) + echo "can't publish a release on channel ${channel}" + exit 1;; + esac + if [[ $GITHUB_REF_NAME != $expected_tag_name ]]; then + echo "invalid release tag ${GITHUB_REF_NAME}. expected ${expected_tag_name}" + exit 1 + fi + + - name: Generate license file + run: script/generate-licenses + + - name: Create app bundle + run: script/bundle + + - name: Upload app bundle to workflow run if main branch or specific label + uses: actions/upload-artifact@v3 + if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-build-dmg') }} + with: + name: Zed_${{ github.event.pull_request.head.sha || github.sha }}.dmg + path: target/release/Zed.dmg + + - uses: softprops/action-gh-release@v1 + name: Upload app bundle to release + if: ${{ env.RELEASE_CHANNEL == 'preview' || env.RELEASE_CHANNEL == 'stable' }} + with: + draft: true + prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }} + files: target/release/Zed.dmg + body: "" env: - MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }} - MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} - APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }} - APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }} - steps: - - name: Install Node - uses: actions/setup-node@v3 - with: - node-version: "18" - - - name: Checkout repo - uses: actions/checkout@v3 - with: - clean: false - submodules: "recursive" - - - name: Limit target directory size - run: script/clear-target-dir-if-larger-than 100 - - - name: Determine version and release channel - if: ${{ startsWith(github.ref, 'refs/tags/v') }} - run: | - set -eu - - version=$(script/get-crate-version zed) - channel=$(cat crates/zed/RELEASE_CHANNEL) - echo "Publishing version: ${version} on release channel ${channel}" - echo "RELEASE_CHANNEL=${channel}" >> $GITHUB_ENV - - expected_tag_name="" - case ${channel} in - stable) - expected_tag_name="v${version}";; - preview) - expected_tag_name="v${version}-pre";; - nightly) - expected_tag_name="v${version}-nightly";; - *) - echo "can't publish a release on channel ${channel}" - exit 1;; - esac - if [[ $GITHUB_REF_NAME != $expected_tag_name ]]; then - echo "invalid release tag ${GITHUB_REF_NAME}. expected ${expected_tag_name}" - exit 1 - fi - - - name: Generate license file - run: script/generate-licenses - - - name: Create app bundle - run: script/bundle - - - name: Upload app bundle to workflow run if main branch or specific label - uses: actions/upload-artifact@v3 - if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-build-dmg') }} - with: - name: Zed_${{ github.event.pull_request.head.sha || github.sha }}.dmg - path: target/release/Zed.dmg - - - uses: softprops/action-gh-release@v1 - name: Upload app bundle to release - if: ${{ env.RELEASE_CHANNEL == 'preview' || env.RELEASE_CHANNEL == 'stable' }} - with: - draft: true - prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }} - files: target/release/Zed.dmg - body: "" - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 668019e696d7a0b4ac2e05382d46df143bddd53e..0000000000000000000000000000000000000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,69 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "type": "lldb", - "request": "launch", - "name": "Debug executable 'Zed'", - "env": { - "ZED_SERVER_URL": "http://localhost:8080" - }, - "cargo": { - "args": [ - "build", - "--bin=Zed", - "--package=zed" - ], - "filter": { - "name": "Zed", - "kind": "bin" - } - }, - "args": [ - "${workspaceFolder}" - ], - "cwd": "${workspaceFolder}" - }, - { - "type": "lldb", - "request": "launch", - "name": "Debug unit tests in executable 'zed'", - "cargo": { - "args": [ - "test", - "--no-run", - "--bin=zed", - "--package=zed" - ], - "filter": { - "name": "zed", - "kind": "bin" - } - }, - "args": [], - "cwd": "${workspaceFolder}" - }, - { - "type": "lldb", - "request": "launch", - "name": "Debug unit tests in library 'gpui'", - "cargo": { - "args": [ - "test", - "--no-run", - "--lib", - "--package=gpui" - ], - "filter": { - "name": "gpui", - "kind": "lib" - } - }, - "args": [], - "cwd": "${workspaceFolder}" - } - ] -} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 8457715563f20a6ae5c9b31414a2d935a9d049ab..898d76e05f8b87a0900c75fcdf98f08fd2a6d3fa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1452,7 +1452,7 @@ dependencies = [ [[package]] name = "collab" -version = "0.37.0" +version = "0.38.0" dependencies = [ "anyhow", "async-trait", @@ -1548,6 +1548,7 @@ dependencies = [ "log", "menu", "notifications", + "parking_lot 0.11.2", "picker", "postage", "pretty_assertions", @@ -6144,6 +6145,7 @@ dependencies = [ "smol", "sum_tree", "theme", + "ui", "util", ] @@ -9079,6 +9081,7 @@ dependencies = [ "nvim-rs", "parking_lot 0.11.2", "project", + "regex", "search", "serde", "serde_derive", diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index f18cc2a111ec432a283f9f08f3fd1acecdddda52..8679296733559a03c51a94d1f133c6bd922eadf9 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -183,6 +183,7 @@ "context": "Editor && mode == auto_height", "bindings": { "ctrl-enter": "editor::Newline", + "shift-enter": "editor::Newline", "ctrl-shift-enter": "editor::NewlineBelow" } }, @@ -349,7 +350,8 @@ "alt-cmd-]": "editor::UnfoldLines", "ctrl-space": "editor::ShowCompletions", "cmd-.": "editor::ToggleCodeActions", - "alt-cmd-r": "editor::RevealInFinder" + "alt-cmd-r": "editor::RevealInFinder", + "ctrl-cmd-c": "editor::DisplayCursorNames" } }, { diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 1da6f0ef8c5da25a60742fda933125c23eac81bf..32acb90d697837eecfda39efec54cb5f4420fbba 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -104,8 +104,6 @@ "shift-v": "vim::ToggleVisualLine", "ctrl-v": "vim::ToggleVisualBlock", "ctrl-q": "vim::ToggleVisualBlock", - "*": "vim::MoveToNext", - "#": "vim::MoveToPrev", "0": "vim::StartOfLine", // When no number operator present, use start of line motion "ctrl-f": "vim::PageDown", "pagedown": "vim::PageDown", @@ -329,6 +327,8 @@ "backwards": true } ], + "*": "vim::MoveToNext", + "#": "vim::MoveToPrev", ";": "vim::RepeatFind", ",": [ "vim::RepeatFind", @@ -421,6 +421,18 @@ "shift-r": "vim::SubstituteLine", "c": "vim::Substitute", "~": "vim::ChangeCase", + "*": [ + "vim::MoveToNext", + { + "partialWord": true + } + ], + "#": [ + "vim::MoveToPrev", + { + "partialWord": true + } + ], "ctrl-a": "vim::Increment", "ctrl-x": "vim::Decrement", "g ctrl-a": [ diff --git a/assets/settings/default.json b/assets/settings/default.json index 87cf0517a2b6f1a32ca8b0d33dadda2d3418f3db..a920a9c68a8935717806d09c9947c7cd146e8265 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -72,6 +72,9 @@ // Whether to use additional LSP queries to format (and amend) the code after // every "trigger" symbol input, defined by LSP server capabilities. "use_on_type_format": true, + // Whether to automatically type closing characters for you. For example, + // when you type (, Zed will automatically add a closing ) at the correct position. + "use_autoclose": true, // Controls whether copilot provides suggestion immediately // or waits for a `copilot::Toggle` "show_copilot_suggestions": true, diff --git a/crates/ai/src/providers/open_ai/completion.rs b/crates/ai/src/providers/open_ai/completion.rs index f99b7f95e346d275fade7628c0ea27375f84e7c9..0e325ee62489b41fb599a75b7b64efcbe864d269 100644 --- a/crates/ai/src/providers/open_ai/completion.rs +++ b/crates/ai/src/providers/open_ai/completion.rs @@ -134,7 +134,7 @@ pub async fn stream_completion( line: Result, ) -> Result> { if let Some(data) = line?.strip_prefix("data: ") { - let event = serde_json::from_str(&data)?; + let event = serde_json::from_str(data)?; Ok(Some(event)) } else { Ok(None) diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index f2988b907c07cddba5c1646178762e8b28a508a5..1f57e52032b1e4e76f7297f577d4db82cf970eb3 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -1172,23 +1172,25 @@ impl Render for AssistantPanel { .px_2() .child(Label::new(editor.read(cx).title(cx)).into_element()) })) - .end_child(if self.focus_handle.contains_focused(cx) { - h_flex() - .gap_2() - .child(h_flex().gap_1().children(self.render_editor_tools(cx))) - .child( - ui::Divider::vertical() - .inset() - .color(ui::DividerColor::Border), - ) - .child( - h_flex() - .gap_1() - .child(Self::render_plus_button(cx)) - .child(self.render_zoom_button(cx)), - ) - } else { - div() + .when(self.focus_handle.contains_focused(cx), |this| { + this.end_child( + h_flex() + .gap_2() + .when(self.active_editor().is_some(), |this| { + this.child(h_flex().gap_1().children(self.render_editor_tools(cx))) + .child( + ui::Divider::vertical() + .inset() + .color(ui::DividerColor::Border), + ) + }) + .child( + h_flex() + .gap_1() + .child(Self::render_plus_button(cx)) + .child(self.render_zoom_button(cx)), + ), + ) }); let contents = if self.active_editor().is_some() { diff --git a/crates/auto_update/src/auto_update.rs b/crates/auto_update/src/auto_update.rs index 3b8d1c6e61e2f42839930ac49311c8f60a38e341..61fa8dcb75c15341e97ddf36ac2a8855bc96f275 100644 --- a/crates/auto_update/src/auto_update.rs +++ b/crates/auto_update/src/auto_update.rs @@ -96,12 +96,6 @@ pub fn init(http_client: Arc, server_url: String, cx: &mut AppCo workspace.register_action(|_, action, cx| { view_release_notes(action, cx); }); - - // @nate - code to trigger update notification on launch - // todo!("remove this when Nate is done") - // workspace.show_notification(0, _cx, |cx| { - // cx.build_view(|_| UpdateNotification::new(SemanticVersion::from_str("1.1.1").unwrap())) - // }); }) .detach(); diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index 97ba2fc97d92c1d245a772f35784bf32511bb2e4..3e89238b0c8b16bfb17a185a1adec446bde473b6 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -1432,7 +1432,7 @@ impl Room { let display = displays .first() .ok_or_else(|| anyhow!("no display found"))?; - let track = LocalVideoTrack::screen_share_for_display(&display); + let track = LocalVideoTrack::screen_share_for_display(display); this.upgrade() .ok_or_else(|| anyhow!("room was dropped"))? .update(&mut cx, |this, _| { diff --git a/crates/channel/src/channel_buffer.rs b/crates/channel/src/channel_buffer.rs index 1aca05ec867a05e2125d451ef1b42266b765fd02..b5f4a06b972d8925735d0e692d4e682a32a19b5d 100644 --- a/crates/channel/src/channel_buffer.rs +++ b/crates/channel/src/channel_buffer.rs @@ -1,6 +1,6 @@ use crate::{Channel, ChannelId, ChannelStore}; use anyhow::Result; -use client::{Client, Collaborator, UserStore}; +use client::{Client, Collaborator, UserStore, ZED_ALWAYS_ACTIVE}; use collections::HashMap; use gpui::{AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Task}; use language::proto::serialize_version; @@ -181,6 +181,16 @@ impl ChannelBuffer { ) { match event { language::Event::Operation(operation) => { + if *ZED_ALWAYS_ACTIVE { + match operation { + language::Operation::UpdateSelections { selections, .. } => { + if selections.is_empty() { + return; + } + } + _ => {} + } + } let operation = language::proto::serialize_operation(operation); self.client .send(proto::UpdateChannelBuffer { diff --git a/crates/channel/src/channel_store/channel_index.rs b/crates/channel/src/channel_store/channel_index.rs index 2682cf6ae290b5729672645f9cdfc1a7e25b4933..88012a57c2374a32d8d52beef0f2e28949315cbb 100644 --- a/crates/channel/src/channel_store/channel_index.rs +++ b/crates/channel/src/channel_store/channel_index.rs @@ -86,11 +86,11 @@ pub struct ChannelPathsInsertGuard<'a> { impl<'a> ChannelPathsInsertGuard<'a> { pub fn note_changed(&mut self, channel_id: ChannelId, epoch: u64, version: &clock::Global) { - insert_note_changed(&mut self.channels_by_id, channel_id, epoch, &version); + insert_note_changed(self.channels_by_id, channel_id, epoch, version); } pub fn new_messages(&mut self, channel_id: ChannelId, message_id: u64) { - insert_new_message(&mut self.channels_by_id, channel_id, message_id) + insert_new_message(self.channels_by_id, channel_id, message_id) } pub fn insert(&mut self, channel_proto: proto::Channel) -> bool { @@ -131,8 +131,8 @@ impl<'a> ChannelPathsInsertGuard<'a> { impl<'a> Drop for ChannelPathsInsertGuard<'a> { fn drop(&mut self) { self.channels_ordered.sort_by(|a, b| { - let a = channel_path_sorting_key(*a, &self.channels_by_id); - let b = channel_path_sorting_key(*b, &self.channels_by_id); + let a = channel_path_sorting_key(*a, self.channels_by_id); + let b = channel_path_sorting_key(*b, self.channels_by_id); a.cmp(b) }); self.channels_ordered.dedup(); @@ -167,7 +167,7 @@ fn insert_note_changed( if epoch > unseen_version.0 { *unseen_version = (epoch, version.clone()); } else { - unseen_version.1.join(&version); + unseen_version.1.join(version); } } } diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 75bcc2512aa5601b3ee2b0719c383e75bf6a1fca..3a48fdba860d1ae9878462a9e31fd4e877e08366 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -1310,7 +1310,7 @@ impl Client { drop(state); if let Some(handler) = handler { - let future = handler(subscriber, message, &self, cx.clone()); + let future = handler(subscriber, message, self, cx.clone()); let client_id = self.id(); log::debug!( "rpc message received. client_id:{}, sender_id:{:?}, type:{}", diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index 313133ebef217f68e2817d8643c0c9ffcff1de39..3412910705433c4a69205717cf8a73241071118f 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -38,8 +38,9 @@ struct TelemetryState { flush_events_task: Option>, log_file: Option, is_staff: Option, - first_event_datetime: Option>, + first_event_date_time: Option>, event_coalescer: EventCoalescer, + max_queue_size: usize, } const EVENTS_URL_PATH: &'static str = "/api/events"; @@ -69,14 +70,14 @@ struct EventWrapper { event: Event, } -#[derive(Serialize, Debug)] +#[derive(Clone, Debug, PartialEq, Serialize)] #[serde(rename_all = "snake_case")] pub enum AssistantKind { Panel, Inline, } -#[derive(Serialize, Debug)] +#[derive(Clone, Debug, PartialEq, Serialize)] #[serde(tag = "type")] pub enum Event { Editor { @@ -168,8 +169,9 @@ impl Telemetry { flush_events_task: None, log_file: None, is_staff: None, - first_event_datetime: None, + first_event_date_time: None, event_coalescer: EventCoalescer::new(), + max_queue_size: MAX_QUEUE_LEN, })); #[cfg(not(debug_assertions))] @@ -310,7 +312,7 @@ impl Telemetry { operation, copilot_enabled, copilot_enabled_for_language, - milliseconds_since_first_event: self.milliseconds_since_first_event(), + milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()), }; self.report_event(event) @@ -326,7 +328,7 @@ impl Telemetry { suggestion_id, suggestion_accepted, file_extension, - milliseconds_since_first_event: self.milliseconds_since_first_event(), + milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()), }; self.report_event(event) @@ -342,7 +344,7 @@ impl Telemetry { conversation_id, kind, model, - milliseconds_since_first_event: self.milliseconds_since_first_event(), + milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()), }; self.report_event(event) @@ -358,7 +360,7 @@ impl Telemetry { operation, room_id, channel_id, - milliseconds_since_first_event: self.milliseconds_since_first_event(), + milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()), }; self.report_event(event) @@ -368,7 +370,7 @@ impl Telemetry { let event = Event::Cpu { usage_as_percentage, core_count, - milliseconds_since_first_event: self.milliseconds_since_first_event(), + milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()), }; self.report_event(event) @@ -382,26 +384,36 @@ impl Telemetry { let event = Event::Memory { memory_in_bytes, virtual_memory_in_bytes, - milliseconds_since_first_event: self.milliseconds_since_first_event(), + milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()), }; self.report_event(event) } pub fn report_app_event(self: &Arc, operation: String) { + self.report_app_event_with_date_time(operation, Utc::now()); + } + + fn report_app_event_with_date_time( + self: &Arc, + operation: String, + date_time: DateTime, + ) -> Event { let event = Event::App { operation, - milliseconds_since_first_event: self.milliseconds_since_first_event(), + milliseconds_since_first_event: self.milliseconds_since_first_event(date_time), }; - self.report_event(event) + self.report_event(event.clone()); + + event } pub fn report_setting_event(self: &Arc, setting: &'static str, value: String) { let event = Event::Setting { setting, value, - milliseconds_since_first_event: self.milliseconds_since_first_event(), + milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()), }; self.report_event(event) @@ -416,7 +428,7 @@ impl Telemetry { let event = Event::Edit { duration: end.timestamp_millis() - start.timestamp_millis(), environment, - milliseconds_since_first_event: self.milliseconds_since_first_event(), + milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()), }; self.report_event(event); @@ -427,22 +439,21 @@ impl Telemetry { let event = Event::Action { source, action, - milliseconds_since_first_event: self.milliseconds_since_first_event(), + milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()), }; self.report_event(event) } - fn milliseconds_since_first_event(&self) -> i64 { + fn milliseconds_since_first_event(self: &Arc, date_time: DateTime) -> i64 { let mut state = self.state.lock(); - match state.first_event_datetime { - Some(first_event_datetime) => { - let now: DateTime = Utc::now(); - now.timestamp_millis() - first_event_datetime.timestamp_millis() + match state.first_event_date_time { + Some(first_event_date_time) => { + date_time.timestamp_millis() - first_event_date_time.timestamp_millis() } None => { - state.first_event_datetime = Some(Utc::now()); + state.first_event_date_time = Some(date_time); 0 } } @@ -468,7 +479,7 @@ impl Telemetry { state.events_queue.push(EventWrapper { signed_in, event }); if state.installation_id.is_some() { - if state.events_queue.len() >= MAX_QUEUE_LEN { + if state.events_queue.len() >= state.max_queue_size { drop(state); self.flush_events(); } @@ -489,7 +500,7 @@ impl Telemetry { pub fn flush_events(self: &Arc) { let mut state = self.state.lock(); - state.first_event_datetime = None; + state.first_event_date_time = None; let mut events = mem::take(&mut state.events_queue); state.flush_events_task.take(); drop(state); @@ -548,3 +559,159 @@ impl Telemetry { .detach(); } } + +#[cfg(test)] +mod tests { + use super::*; + use chrono::TimeZone; + use gpui::TestAppContext; + use util::http::FakeHttpClient; + + #[gpui::test] + fn test_telemetry_flush_on_max_queue_size(cx: &mut TestAppContext) { + init_test(cx); + let http = FakeHttpClient::with_200_response(); + let installation_id = Some("installation_id".to_string()); + let session_id = "session_id".to_string(); + + cx.update(|cx| { + let telemetry = Telemetry::new(http, cx); + + telemetry.state.lock().max_queue_size = 4; + telemetry.start(installation_id, session_id, cx); + + assert!(is_empty_state(&telemetry)); + + let first_date_time = Utc.with_ymd_and_hms(1990, 4, 12, 12, 0, 0).unwrap(); + let operation = "test".to_string(); + + let event = + telemetry.report_app_event_with_date_time(operation.clone(), first_date_time); + assert_eq!( + event, + Event::App { + operation: operation.clone(), + milliseconds_since_first_event: 0 + } + ); + assert_eq!(telemetry.state.lock().events_queue.len(), 1); + assert!(telemetry.state.lock().flush_events_task.is_some()); + assert_eq!( + telemetry.state.lock().first_event_date_time, + Some(first_date_time) + ); + + let mut date_time = first_date_time + chrono::Duration::milliseconds(100); + + let event = telemetry.report_app_event_with_date_time(operation.clone(), date_time); + assert_eq!( + event, + Event::App { + operation: operation.clone(), + milliseconds_since_first_event: 100 + } + ); + assert_eq!(telemetry.state.lock().events_queue.len(), 2); + assert!(telemetry.state.lock().flush_events_task.is_some()); + assert_eq!( + telemetry.state.lock().first_event_date_time, + Some(first_date_time) + ); + + date_time += chrono::Duration::milliseconds(100); + + let event = telemetry.report_app_event_with_date_time(operation.clone(), date_time); + assert_eq!( + event, + Event::App { + operation: operation.clone(), + milliseconds_since_first_event: 200 + } + ); + assert_eq!(telemetry.state.lock().events_queue.len(), 3); + assert!(telemetry.state.lock().flush_events_task.is_some()); + assert_eq!( + telemetry.state.lock().first_event_date_time, + Some(first_date_time) + ); + + date_time += chrono::Duration::milliseconds(100); + + // Adding a 4th event should cause a flush + let event = telemetry.report_app_event_with_date_time(operation.clone(), date_time); + assert_eq!( + event, + Event::App { + operation: operation.clone(), + milliseconds_since_first_event: 300 + } + ); + + assert!(is_empty_state(&telemetry)); + }); + } + + #[gpui::test] + async fn test_connection_timeout(executor: BackgroundExecutor, cx: &mut TestAppContext) { + init_test(cx); + let http = FakeHttpClient::with_200_response(); + let installation_id = Some("installation_id".to_string()); + let session_id = "session_id".to_string(); + + cx.update(|cx| { + let telemetry = Telemetry::new(http, cx); + telemetry.state.lock().max_queue_size = 4; + telemetry.start(installation_id, session_id, cx); + + assert!(is_empty_state(&telemetry)); + + let first_date_time = Utc.with_ymd_and_hms(1990, 4, 12, 12, 0, 0).unwrap(); + let operation = "test".to_string(); + + let event = + telemetry.report_app_event_with_date_time(operation.clone(), first_date_time); + assert_eq!( + event, + Event::App { + operation: operation.clone(), + milliseconds_since_first_event: 0 + } + ); + assert_eq!(telemetry.state.lock().events_queue.len(), 1); + assert!(telemetry.state.lock().flush_events_task.is_some()); + assert_eq!( + telemetry.state.lock().first_event_date_time, + Some(first_date_time) + ); + + let duration = Duration::from_millis(1); + + // Test 1 millisecond before the flush interval limit is met + executor.advance_clock(FLUSH_INTERVAL - duration); + + assert!(!is_empty_state(&telemetry)); + + // Test the exact moment the flush interval limit is met + executor.advance_clock(duration); + + assert!(is_empty_state(&telemetry)); + }); + } + + // TODO: + // Test settings + // Update FakeHTTPClient to keep track of the number of requests and assert on it + + fn init_test(cx: &mut TestAppContext) { + cx.update(|cx| { + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); + }); + } + + fn is_empty_state(telemetry: &Telemetry) -> bool { + telemetry.state.lock().events_queue.is_empty() + && telemetry.state.lock().flush_events_task.is_none() + && telemetry.state.lock().first_event_date_time.is_none() + } +} diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 4453bb40eaaf34291cfa5b7f9cc917cd8606706f..dcab0e53942032045d5dcdc117e1c0e3adbe7c9e 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -3,7 +3,10 @@ use anyhow::{anyhow, Context, Result}; use collections::{hash_map::Entry, HashMap, HashSet}; use feature_flags::FeatureFlagAppExt; use futures::{channel::mpsc, Future, StreamExt}; -use gpui::{AsyncAppContext, EventEmitter, Model, ModelContext, SharedUrl, Task}; +use gpui::{ + AppContext, AsyncAppContext, EventEmitter, Model, ModelContext, SharedString, SharedUrl, Task, + WeakModel, +}; use postage::{sink::Sink, watch}; use rpc::proto::{RequestMessage, UsersResponse}; use std::sync::{Arc, Weak}; @@ -77,6 +80,7 @@ pub struct UserStore { client: Weak, _maintain_contacts: Task<()>, _maintain_current_user: Task>, + weak_self: WeakModel, } #[derive(Clone)] @@ -194,6 +198,7 @@ impl UserStore { Ok(()) }), pending_contact_requests: Default::default(), + weak_self: cx.weak_model(), } } @@ -579,6 +584,19 @@ impl UserStore { self.users.get(&user_id).cloned() } + pub fn get_user_optimistic( + &mut self, + user_id: u64, + cx: &mut ModelContext, + ) -> Option> { + if let Some(user) = self.users.get(&user_id).cloned() { + return Some(user); + } + + self.get_user(user_id, cx).detach_and_log_err(cx); + None + } + pub fn get_user( &mut self, user_id: u64, @@ -651,6 +669,31 @@ impl UserStore { pub fn participant_indices(&self) -> &HashMap { &self.participant_indices } + + pub fn participant_names( + &self, + user_ids: impl Iterator, + cx: &AppContext, + ) -> HashMap { + let mut ret = HashMap::default(); + let mut missing_user_ids = Vec::new(); + for id in user_ids { + if let Some(github_login) = self.get_cached_user(id).map(|u| u.github_login.clone()) { + ret.insert(id, github_login.into()); + } else { + missing_user_ids.push(id) + } + } + if !missing_user_ids.is_empty() { + let this = self.weak_self.clone(); + cx.spawn(|mut cx| async move { + this.update(&mut cx, |this, cx| this.get_users(missing_user_ids, cx))? + .await + }) + .detach_and_log_err(cx); + } + ret + } } impl User { diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index 9209d9ac2d3e24ffec20b628076e1c3709d92bb6..e30622adc3d360f40bb3c832cca063f6d4f6a84e 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] default-run = "collab" edition = "2021" name = "collab" -version = "0.37.0" +version = "0.38.0" publish = false [[bin]] diff --git a/crates/collab/src/db/queries/buffers.rs b/crates/collab/src/db/queries/buffers.rs index bdcaaab6ef62d8718d3b8903d9f16b4ce9d4b3f5..c19cd530a017fe93c89c68b4f5b814836cd1ef77 100644 --- a/crates/collab/src/db/queries/buffers.rs +++ b/crates/collab/src/db/queries/buffers.rs @@ -450,8 +450,21 @@ impl Database { )> { self.transaction(move |tx| async move { let channel = self.get_channel_internal(channel_id, &*tx).await?; - self.check_user_is_channel_member(&channel, user, &*tx) - .await?; + + let mut requires_write_permission = false; + for op in operations.iter() { + match op.variant { + None | Some(proto::operation::Variant::UpdateSelections(_)) => {} + Some(_) => requires_write_permission = true, + } + } + if requires_write_permission { + self.check_user_is_channel_member(&channel, user, &*tx) + .await?; + } else { + self.check_user_is_channel_participant(&channel, user, &*tx) + .await?; + } let buffer = buffer::Entity::find() .filter(buffer::Column::ChannelId.eq(channel_id)) diff --git a/crates/collab/src/db/queries/notifications.rs b/crates/collab/src/db/queries/notifications.rs index 57685e141b78e5aa6c4a541c385bf056b1802000..ccdda65342fcbcbf3a6837e0725cee8763e7f311 100644 --- a/crates/collab/src/db/queries/notifications.rs +++ b/crates/collab/src/db/queries/notifications.rs @@ -66,7 +66,7 @@ impl Database { .await } - /// Create a notification. If `avoid_duplicates` is set to true, then avoid + /// Creates a notification. If `avoid_duplicates` is set to true, then avoid /// creating a new notification if the given recipient already has an /// unread notification with the given kind and entity id. pub async fn create_notification( diff --git a/crates/collab/src/db/queries/users.rs b/crates/collab/src/db/queries/users.rs index 8f975b5cbe5d8b4239e34e8770b57b979d6ac378..d6dfe480427fc8ed6dce8f460b5b307e7735317e 100644 --- a/crates/collab/src/db/queries/users.rs +++ b/crates/collab/src/db/queries/users.rs @@ -153,7 +153,7 @@ impl Database { .await } - /// Set "connected_once" on the user for analytics. + /// Sets "connected_once" on the user for analytics. pub async fn set_user_connected_once(&self, id: UserId, connected_once: bool) -> Result<()> { self.transaction(|tx| async move { user::Entity::update_many() @@ -252,7 +252,7 @@ impl Database { .await } - /// Return the active flags for the user. + /// Returns the active flags for the user. pub async fn get_user_flags(&self, user: UserId) -> Result> { self.transaction(|tx| async move { #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 4e9807acfb562a67732147e26b12d36afaa07340..2a715fa4a456ccd0564b88a2afe662136e151d6a 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -950,7 +950,7 @@ async fn ping(_: proto::Ping, response: Response, _session: Session Ok(()) } -/// Create a new room for calling (outside of channels) +/// Creates a new room for calling (outside of channels) async fn create_room( _request: proto::CreateRoom, response: Response, @@ -1276,7 +1276,7 @@ async fn leave_room( Ok(()) } -/// Update the permissions of someone else in the room. +/// Updates the permissions of someone else in the room. async fn set_room_participant_role( request: proto::SetRoomParticipantRole, response: Response, @@ -1460,7 +1460,7 @@ async fn decline_call(message: proto::DeclineCall, session: Session) -> Result<( Ok(()) } -/// Update other participants in the room with your current location. +/// Updates other participants in the room with your current location. async fn update_participant_location( request: proto::UpdateParticipantLocation, response: Response, @@ -1673,7 +1673,7 @@ async fn leave_project(request: proto::LeaveProject, session: Session) -> Result Ok(()) } -/// Update other participants with changes to the project +/// Updates other participants with changes to the project async fn update_project( request: proto::UpdateProject, response: Response, @@ -1700,7 +1700,7 @@ async fn update_project( Ok(()) } -/// Update other participants with changes to the worktree +/// Updates other participants with changes to the worktree async fn update_worktree( request: proto::UpdateWorktree, response: Response, @@ -1725,7 +1725,7 @@ async fn update_worktree( Ok(()) } -/// Update other participants with changes to the diagnostics +/// Updates other participants with changes to the diagnostics async fn update_diagnostic_summary( message: proto::UpdateDiagnosticSummary, session: Session, @@ -1749,7 +1749,7 @@ async fn update_diagnostic_summary( Ok(()) } -/// Update other participants with changes to the worktree settings +/// Updates other participants with changes to the worktree settings async fn update_worktree_settings( message: proto::UpdateWorktreeSettings, session: Session, @@ -2293,7 +2293,7 @@ async fn remove_contact( Ok(()) } -/// Create a new channel. +/// Creates a new channel. async fn create_channel( request: proto::CreateChannel, response: Response, diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index a8e52f4094c83ac79a29c9b30eae666566de96e2..90fdc64e260cadd38e1148c66670059f70a68be4 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -34,6 +34,7 @@ use std::{ atomic::{AtomicBool, Ordering::SeqCst}, Arc, }, + time::Duration, }; use unindent::Unindent as _; @@ -5945,3 +5946,26 @@ async fn test_right_click_menu_behind_collab_panel(cx: &mut TestAppContext) { }); assert!(cx.debug_bounds("MENU_ITEM-Close").is_some()); } + +#[gpui::test] +async fn test_cmd_k_left(cx: &mut TestAppContext) { + let client = TestServer::start1(cx).await; + let (workspace, cx) = client.build_test_workspace(cx).await; + + cx.simulate_keystrokes("cmd-n"); + workspace.update(cx, |workspace, cx| { + assert!(workspace.items(cx).collect::>().len() == 1); + }); + cx.simulate_keystrokes("cmd-k left"); + workspace.update(cx, |workspace, cx| { + assert!(workspace.items(cx).collect::>().len() == 2); + }); + cx.simulate_keystrokes("cmd-k"); + // sleep for longer than the timeout in keyboard shortcut handling + // to verify that it doesn't fire in this case. + cx.executor().advance_clock(Duration::from_secs(2)); + cx.simulate_keystrokes("left"); + workspace.update(cx, |workspace, cx| { + assert!(workspace.items(cx).collect::>().len() == 3); + }); +} diff --git a/crates/collab/src/tests/test_server.rs b/crates/collab/src/tests/test_server.rs index ea08d83b6cbe71c4516c1c8bab4d46edce6cf60d..8efd9535b09bfb5e6243852a648c6cb1d5ad0450 100644 --- a/crates/collab/src/tests/test_server.rs +++ b/crates/collab/src/tests/test_server.rs @@ -127,6 +127,11 @@ impl TestServer { (client_a, client_b, channel_id) } + pub async fn start1<'a>(cx: &'a mut TestAppContext) -> TestClient { + let mut server = Self::start(cx.executor().clone()).await; + server.create_client(cx, "user_a").await + } + pub async fn reset(&self) { self.app_state.db.reset(); let epoch = self diff --git a/crates/collab_ui/Cargo.toml b/crates/collab_ui/Cargo.toml index 84c1810bc841d904a7a534fb562671dc9d7232c9..0fbf7deb7836c36be047a10f14cec419eadc5d8d 100644 --- a/crates/collab_ui/Cargo.toml +++ b/crates/collab_ui/Cargo.toml @@ -60,6 +60,7 @@ anyhow.workspace = true futures.workspace = true lazy_static.workspace = true log.workspace = true +parking_lot.workspace = true schemars.workspace = true postage.workspace = true serde.workspace = true diff --git a/crates/collab_ui/src/channel_view.rs b/crates/collab_ui/src/channel_view.rs index 033889f771d84bafc13dae5005957096e61cb3c6..b2c243dc898b529b42b63876882361a8901b8dbf 100644 --- a/crates/collab_ui/src/channel_view.rs +++ b/crates/collab_ui/src/channel_view.rs @@ -442,4 +442,13 @@ impl CollaborationHub for ChannelBufferCollaborationHub { ) -> &'a HashMap { self.0.read(cx).user_store().read(cx).participant_indices() } + + fn user_names(&self, cx: &AppContext) -> HashMap { + let user_ids = self.collaborators(cx).values().map(|c| c.user_id); + self.0 + .read(cx) + .user_store() + .read(cx) + .participant_names(user_ids, cx) + } } diff --git a/crates/collab_ui/src/chat_panel.rs b/crates/collab_ui/src/chat_panel.rs index 44b0669c307bcb491e53e0f07ab48ae461d7f6b3..aee181e1dfeeea2518496e18495961985b4b60be 100644 --- a/crates/collab_ui/src/chat_panel.rs +++ b/crates/collab_ui/src/chat_panel.rs @@ -18,7 +18,7 @@ use project::Fs; use rich_text::RichText; use serde::{Deserialize, Serialize}; use settings::Settings; -use std::sync::Arc; +use std::{sync::Arc, time::Duration}; use time::{OffsetDateTime, UtcOffset}; use ui::{ popover_menu, prelude::*, Avatar, Button, ContextMenu, IconButton, IconName, KeyBinding, Label, @@ -304,8 +304,11 @@ impl ChatPanel { let last_message = active_chat.message(ix.saturating_sub(1)); let this_message = active_chat.message(ix).clone(); - let is_continuation_from_previous = last_message.id != this_message.id - && last_message.sender.id == this_message.sender.id; + let duration_since_last_message = this_message.timestamp - last_message.timestamp; + let is_continuation_from_previous = last_message.sender.id + == this_message.sender.id + && last_message.id != this_message.id + && duration_since_last_message < Duration::from_secs(5 * 60); if let ChannelMessageId::Saved(id) = this_message.id { if this_message @@ -325,8 +328,6 @@ impl ChatPanel { Self::render_markdown_with_mentions(&self.languages, self.client.id(), &message) }); - let now = OffsetDateTime::now_utc(); - let belongs_to_user = Some(message.sender.id) == self.client.user_id(); let message_id_to_remove = if let (ChannelMessageId::Saved(id), true) = (message.id, belongs_to_user || is_admin) @@ -349,23 +350,21 @@ impl ChatPanel { .when(!is_continuation_from_previous, |this| { this.pt_3().child( h_flex() - .child( - div().absolute().child( - Avatar::new(message.sender.avatar_uri.clone()) - .size(cx.rem_size() * 1.5), - ), - ) + .text_ui_sm() + .child(div().absolute().child( + Avatar::new(message.sender.avatar_uri.clone()).size(cx.rem_size()), + )) .child( div() - .pl(cx.rem_size() * 1.5 + px(6.0)) + .pl(cx.rem_size() + px(6.0)) .pr(px(8.0)) .font_weight(FontWeight::BOLD) .child(Label::new(message.sender.github_login.clone())), ) .child( Label::new(format_timestamp( + OffsetDateTime::now_utc(), message.timestamp, - now, self.local_timezone, )) .size(LabelSize::Small) @@ -559,6 +558,7 @@ impl Render for ChatPanel { } else { this.child( div() + .full() .p_4() .child( Label::new("Select a channel to chat in.") @@ -596,7 +596,7 @@ impl Render for ChatPanel { el.child( div() .rounded_md() - .h_7() + .h_6() .w_full() .bg(cx.theme().colors().editor_background), ) @@ -670,28 +670,44 @@ impl Panel for ChatPanel { impl EventEmitter for ChatPanel {} fn format_timestamp( - mut timestamp: OffsetDateTime, - mut now: OffsetDateTime, - local_timezone: UtcOffset, + reference: OffsetDateTime, + timestamp: OffsetDateTime, + timezone: UtcOffset, ) -> String { - timestamp = timestamp.to_offset(local_timezone); - now = now.to_offset(local_timezone); - - let today = now.date(); - let date = timestamp.date(); - let mut hour = timestamp.hour(); - let mut part = "am"; - if hour > 12 { - hour -= 12; - part = "pm"; - } - if date == today { - format!("{:02}:{:02}{}", hour, timestamp.minute(), part) - } else if date.next_day() == Some(today) { - format!("yesterday at {:02}:{:02}{}", hour, timestamp.minute(), part) + let timestamp_local = timestamp.to_offset(timezone); + let timestamp_local_hour = timestamp_local.hour(); + + let hour_12 = match timestamp_local_hour { + 0 => 12, // Midnight + 13..=23 => timestamp_local_hour - 12, // PM hours + _ => timestamp_local_hour, // AM hours + }; + let meridiem = if timestamp_local_hour >= 12 { + "pm" } else { - format!("{:02}/{}/{}", date.month() as u32, date.day(), date.year()) + "am" + }; + let timestamp_local_minute = timestamp_local.minute(); + let formatted_time = format!("{:02}:{:02} {}", hour_12, timestamp_local_minute, meridiem); + + let reference_local = reference.to_offset(timezone); + let reference_local_date = reference_local.date(); + let timestamp_local_date = timestamp_local.date(); + + if timestamp_local_date == reference_local_date { + return formatted_time; } + + if reference_local_date.previous_day() == Some(timestamp_local_date) { + return format!("yesterday at {}", formatted_time); + } + + format!( + "{:02}/{:02}/{}", + timestamp_local_date.month() as u32, + timestamp_local_date.day(), + timestamp_local_date.year() + ) } #[cfg(test)] @@ -700,6 +716,7 @@ mod tests { use gpui::HighlightStyle; use pretty_assertions::assert_eq; use rich_text::Highlight; + use time::{Date, OffsetDateTime, Time, UtcOffset}; use util::test::marked_text_ranges; #[gpui::test] @@ -748,4 +765,99 @@ mod tests { ] ); } + + #[test] + fn test_format_today() { + let reference = create_offset_datetime(1990, 4, 12, 16, 45, 0); + let timestamp = create_offset_datetime(1990, 4, 12, 15, 30, 0); + + assert_eq!( + format_timestamp(reference, timestamp, test_timezone()), + "03:30 pm" + ); + } + + #[test] + fn test_format_yesterday() { + let reference = create_offset_datetime(1990, 4, 12, 10, 30, 0); + let timestamp = create_offset_datetime(1990, 4, 11, 9, 0, 0); + + assert_eq!( + format_timestamp(reference, timestamp, test_timezone()), + "yesterday at 09:00 am" + ); + } + + #[test] + fn test_format_yesterday_less_than_24_hours_ago() { + let reference = create_offset_datetime(1990, 4, 12, 19, 59, 0); + let timestamp = create_offset_datetime(1990, 4, 11, 20, 0, 0); + + assert_eq!( + format_timestamp(reference, timestamp, test_timezone()), + "yesterday at 08:00 pm" + ); + } + + #[test] + fn test_format_yesterday_more_than_24_hours_ago() { + let reference = create_offset_datetime(1990, 4, 12, 19, 59, 0); + let timestamp = create_offset_datetime(1990, 4, 11, 18, 0, 0); + + assert_eq!( + format_timestamp(reference, timestamp, test_timezone()), + "yesterday at 06:00 pm" + ); + } + + #[test] + fn test_format_yesterday_over_midnight() { + let reference = create_offset_datetime(1990, 4, 12, 0, 5, 0); + let timestamp = create_offset_datetime(1990, 4, 11, 23, 55, 0); + + assert_eq!( + format_timestamp(reference, timestamp, test_timezone()), + "yesterday at 11:55 pm" + ); + } + + #[test] + fn test_format_yesterday_over_month() { + let reference = create_offset_datetime(1990, 4, 2, 9, 0, 0); + let timestamp = create_offset_datetime(1990, 4, 1, 20, 0, 0); + + assert_eq!( + format_timestamp(reference, timestamp, test_timezone()), + "yesterday at 08:00 pm" + ); + } + + #[test] + fn test_format_before_yesterday() { + let reference = create_offset_datetime(1990, 4, 12, 10, 30, 0); + let timestamp = create_offset_datetime(1990, 4, 10, 20, 20, 0); + + assert_eq!( + format_timestamp(reference, timestamp, test_timezone()), + "04/10/1990" + ); + } + + fn test_timezone() -> UtcOffset { + UtcOffset::from_hms(0, 0, 0).expect("Valid timezone offset") + } + + fn create_offset_datetime( + year: i32, + month: u8, + day: u8, + hour: u8, + minute: u8, + second: u8, + ) -> OffsetDateTime { + let date = + Date::from_calendar_date(year, time::Month::try_from(month).unwrap(), day).unwrap(); + let time = Time::from_hms(hour, minute, second).unwrap(); + date.with_time(time).assume_utc() // Assume UTC for simplicity + } } diff --git a/crates/collab_ui/src/chat_panel/message_editor.rs b/crates/collab_ui/src/chat_panel/message_editor.rs index 7999db529a43985ae2b52cdde9f2108f9620b35c..9bbf512877c85615a874ff7cd0c2d0e6dacd5ce9 100644 --- a/crates/collab_ui/src/chat_panel/message_editor.rs +++ b/crates/collab_ui/src/chat_panel/message_editor.rs @@ -1,19 +1,24 @@ -use std::{sync::Arc, time::Duration}; - +use anyhow::Result; use channel::{ChannelId, ChannelMembership, ChannelStore, MessageParams}; use client::UserId; use collections::HashMap; -use editor::{AnchorRangeExt, Editor, EditorElement, EditorStyle}; +use editor::{AnchorRangeExt, CompletionProvider, Editor, EditorElement, EditorStyle}; +use fuzzy::StringMatchCandidate; use gpui::{ AsyncWindowContext, FocusableView, FontStyle, FontWeight, HighlightStyle, IntoElement, Model, Render, SharedString, Task, TextStyle, View, ViewContext, WeakView, WhiteSpace, }; -use language::{language_settings::SoftWrap, Buffer, BufferSnapshot, LanguageRegistry}; +use language::{ + language_settings::SoftWrap, Anchor, Buffer, BufferSnapshot, CodeLabel, Completion, + LanguageRegistry, LanguageServerId, ToOffset, +}; use lazy_static::lazy_static; +use parking_lot::RwLock; use project::search::SearchQuery; use settings::Settings; +use std::{sync::Arc, time::Duration}; use theme::ThemeSettings; -use ui::prelude::*; +use ui::{prelude::*, UiTextSize}; const MENTIONS_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(50); @@ -31,6 +36,43 @@ pub struct MessageEditor { channel_id: Option, } +struct MessageEditorCompletionProvider(WeakView); + +impl CompletionProvider for MessageEditorCompletionProvider { + fn completions( + &self, + buffer: &Model, + buffer_position: language::Anchor, + cx: &mut ViewContext, + ) -> Task>> { + let Some(handle) = self.0.upgrade() else { + return Task::ready(Ok(Vec::new())); + }; + handle.update(cx, |message_editor, cx| { + message_editor.completions(buffer, buffer_position, cx) + }) + } + + fn resolve_completions( + &self, + _completion_indices: Vec, + _completions: Arc>>, + _cx: &mut ViewContext, + ) -> Task> { + Task::ready(Ok(false)) + } + + fn apply_additional_edits_for_completion( + &self, + _buffer: Model, + _completion: Completion, + _push_to_history: bool, + _cx: &mut ViewContext, + ) -> Task>> { + Task::ready(Ok(None)) + } +} + impl MessageEditor { pub fn new( language_registry: Arc, @@ -38,8 +80,11 @@ impl MessageEditor { editor: View, cx: &mut ViewContext, ) -> Self { + let this = cx.view().downgrade(); editor.update(cx, |editor, cx| { editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx); + editor.set_use_autoclose(false); + editor.set_completion_provider(Box::new(MessageEditorCompletionProvider(this))); }); let buffer = editor @@ -149,6 +194,71 @@ impl MessageEditor { } } + fn completions( + &mut self, + buffer: &Model, + end_anchor: Anchor, + cx: &mut ViewContext, + ) -> Task>> { + let end_offset = end_anchor.to_offset(buffer.read(cx)); + + let Some(query) = buffer.update(cx, |buffer, _| { + let mut query = String::new(); + for ch in buffer.reversed_chars_at(end_offset).take(100) { + if ch == '@' { + return Some(query.chars().rev().collect::()); + } + if ch.is_whitespace() || !ch.is_ascii() { + break; + } + query.push(ch); + } + return None; + }) else { + return Task::ready(Ok(vec![])); + }; + + let start_offset = end_offset - query.len(); + let start_anchor = buffer.read(cx).anchor_before(start_offset); + + let candidates = self + .users + .keys() + .map(|user| StringMatchCandidate { + id: 0, + string: user.clone(), + char_bag: user.chars().collect(), + }) + .collect::>(); + cx.spawn(|_, cx| async move { + let matches = fuzzy::match_strings( + &candidates, + &query, + true, + 10, + &Default::default(), + cx.background_executor().clone(), + ) + .await; + + Ok(matches + .into_iter() + .map(|mat| Completion { + old_range: start_anchor..end_anchor, + new_text: mat.string.clone(), + label: CodeLabel { + filter_range: 1..mat.string.len() + 1, + text: format!("@{}", mat.string), + runs: Vec::new(), + }, + documentation: None, + server_id: LanguageServerId(0), // TODO: Make this optional or something? + lsp_completion: Default::default(), // TODO: Make this optional or something? + }) + .collect()) + }) + } + async fn find_mentions( this: WeakView, buffer: BufferSnapshot, @@ -216,7 +326,7 @@ impl Render for MessageEditor { }, font_family: settings.ui_font.family.clone(), font_features: settings.ui_font.features, - font_size: rems(0.875).into(), + font_size: UiTextSize::Small.rems().into(), font_weight: FontWeight::NORMAL, font_style: FontStyle::Normal, line_height: relative(1.3).into(), diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 4bfb5fe3ae9988f0ad8a34769d803fa18dcbb8dd..28c76b155eabbefdd087f1732e794ee706e5bf2f 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -20,7 +20,7 @@ use gpui::{ Model, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Render, SharedString, Styled, Subscription, Task, TextStyle, View, ViewContext, VisualContext, WeakView, WhiteSpace, }; -use menu::{Cancel, Confirm, SelectNext, SelectPrev}; +use menu::{Cancel, Confirm, SecondaryConfirm, SelectNext, SelectPrev}; use project::{Fs, Project}; use rpc::proto::{self, PeerId}; use serde_derive::{Deserialize, Serialize}; @@ -1124,7 +1124,7 @@ impl CollabPanel { ) .entry( "Rename", - None, + Some(Box::new(SecondaryConfirm)), cx.handler_for(&this, move |this, cx| this.rename_channel(channel_id, cx)), ) .entry( @@ -1492,7 +1492,7 @@ impl CollabPanel { } } - fn rename_selected_channel(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext) { + fn rename_selected_channel(&mut self, _: &SecondaryConfirm, cx: &mut ViewContext) { if let Some(channel) = self.selected_channel() { self.rename_channel(channel.id, cx); } @@ -2214,15 +2214,15 @@ impl CollabPanel { let face_pile = if !participants.is_empty() { let extra_count = participants.len().saturating_sub(FACEPILE_LIMIT); - let result = FacePile { - faces: participants + let result = FacePile::new( + participants .iter() .map(|user| Avatar::new(user.avatar_uri.clone()).into_any_element()) .take(FACEPILE_LIMIT) .chain(if extra_count > 0 { Some( div() - .ml_1() + .ml_2() .child(Label::new(format!("+{extra_count}"))) .into_any_element(), ) @@ -2230,7 +2230,7 @@ impl CollabPanel { None }) .collect::>(), - }; + ); Some(result) } else { @@ -2295,7 +2295,7 @@ impl CollabPanel { h_flex() .id(channel_id as usize) .child(Label::new(channel.name.clone())) - .children(face_pile.map(|face_pile| face_pile.render().p_1())), + .children(face_pile.map(|face_pile| face_pile.p_1())), ), ) .child( diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index e864c4c54add0ff4235928983c0fd8b3c801b071..43a749ec9536a4a420a81b17f7e00ffb94846998 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -85,7 +85,14 @@ impl Render for CollabTitlebarItem { .gap_1() .children(self.render_project_host(cx)) .child(self.render_project_name(cx)) - .child(div().pr_1().children(self.render_project_branch(cx))) + .children(self.render_project_branch(cx)), + ) + .child( + h_flex() + .id("collaborator-list") + .w_full() + .gap_1() + .overflow_x_scroll() .when_some( current_user.clone().zip(client.peer_id()).zip(room.clone()), |this, ((current_user, peer_id), room)| { @@ -495,7 +502,7 @@ impl CollabTitlebarItem { div.rounded_md().bg(color) }) .child( - FacePile::default() + FacePile::empty() .child( Avatar::new(user.avatar_uri.clone()) .grayscale(!is_present) @@ -547,8 +554,7 @@ impl CollabTitlebarItem { ) } else { None - }) - .render(), + }), ), ) } diff --git a/crates/collab_ui/src/face_pile.rs b/crates/collab_ui/src/face_pile.rs index 985c1944f465c9683cba94ddf4a5b88d974a7c96..71d15eb155101b2ec91fa61dab44744e55f51375 100644 --- a/crates/collab_ui/src/face_pile.rs +++ b/crates/collab_ui/src/face_pile.rs @@ -1,28 +1,49 @@ -use gpui::{div, AnyElement, Div, IntoElement, ParentElement, Styled}; +use gpui::AnyElement; use smallvec::SmallVec; +use ui::prelude::*; -#[derive(Default)] +#[derive(IntoElement)] pub struct FacePile { - pub faces: SmallVec<[AnyElement; 2]>, + base: Div, + faces: SmallVec<[AnyElement; 2]>, } impl FacePile { - pub fn render(self) -> Div { + pub fn empty() -> Self { + Self::new(SmallVec::new()) + } + + pub fn new(faces: SmallVec<[AnyElement; 2]>) -> Self { + Self { + base: h_flex(), + faces, + } + } +} + +impl RenderOnce for FacePile { + fn render(self, _cx: &mut WindowContext) -> impl IntoElement { let player_count = self.faces.len(); let player_list = self.faces.into_iter().enumerate().map(|(ix, player)| { let isnt_last = ix < player_count - 1; div() - .z_index((player_count - ix) as u8) + .z_index((player_count - ix) as u16) .when(isnt_last, |div| div.neg_mr_1()) .child(player) }); - div().flex().items_center().children(player_list) + self.base.children(player_list) } } impl ParentElement for FacePile { - fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { - &mut self.faces + fn extend(&mut self, elements: impl IntoIterator) { + self.faces.extend(elements); + } +} + +impl Styled for FacePile { + fn style(&mut self) -> &mut gpui::StyleRefinement { + self.base.style() } } diff --git a/crates/collab_ui/src/notification_panel.rs b/crates/collab_ui/src/notification_panel.rs index b30f8d15f035b5bc49e08449d20efd21b0e5b8c9..6f4ee1baaec8e2b93cdd47373a96f79ebd931be7 100644 --- a/crates/collab_ui/src/notification_panel.rs +++ b/crates/collab_ui/src/notification_panel.rs @@ -713,6 +713,9 @@ impl Render for NotificationToast { h_flex() .id("notification_panel_toast") + .elevation_3(cx) + .p_2() + .gap_2() .children(user.map(|user| Avatar::new(user.avatar_uri.clone()))) .child(Label::new(self.text.clone())) .child( diff --git a/crates/collab_ui/src/notifications/collab_notification.rs b/crates/collab_ui/src/notifications/collab_notification.rs index 8157bc1318dac52710732880d565e4914d01d8de..69d26e50fd3e1948d5c9b1e6682bfb71eddffdc2 100644 --- a/crates/collab_ui/src/notifications/collab_notification.rs +++ b/crates/collab_ui/src/notifications/collab_notification.rs @@ -26,8 +26,8 @@ impl CollabNotification { } impl ParentElement for CollabNotification { - fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { - &mut self.children + fn extend(&mut self, elements: impl Iterator) { + self.children.extend(elements) } } diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index c90e44886568b6def4e7c66cced834553cf5bb96..a130947793717934c33c233a989e20abe4093196 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/crates/command_palette/src/command_palette.rs @@ -311,7 +311,7 @@ impl PickerDelegate for CommandPaletteDelegate { let action = command.action; cx.focus(&self.previous_focus_handle); cx.window_context() - .spawn(move |mut cx| async move { cx.update(|_, cx| cx.dispatch_action(action)) }) + .spawn(move |mut cx| async move { cx.update(|cx| cx.dispatch_action(action)) }) .detach_and_log_err(cx); self.dismissed(cx); } diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index f36567c6b9439d59dd92ffb61aebaf93059e926b..86b721006df2bcb92485def99ab68460e025d51e 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -974,7 +974,7 @@ async fn get_copilot_lsp(http: Arc) -> anyhow::Result { .browser_download_url; let mut response = http - .get(&url, Default::default(), true) + .get(url, Default::default(), true) .await .map_err(|err| anyhow!("error downloading copilot release: {}", err))?; let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut())); diff --git a/crates/copilot_ui/src/copilot_button.rs b/crates/copilot_ui/src/copilot_button.rs index 9dc4e75cb1cced8c5097bb3b7ee474fba9ed2249..28b28ffe9afec835eb5d6877aad0dfef2f05c017 100644 --- a/crates/copilot_ui/src/copilot_button.rs +++ b/crates/copilot_ui/src/copilot_button.rs @@ -355,7 +355,7 @@ fn initiate_sign_in(cx: &mut WindowContext) { cx.spawn(|mut cx| async move { task.await; - if let Some(copilot) = cx.update(|_, cx| Copilot::global(cx)).ok().flatten() { + if let Some(copilot) = cx.update(|cx| Copilot::global(cx)).ok().flatten() { workspace .update(&mut cx, |workspace, cx| match copilot.read(cx).status() { Status::Authorized => workspace.show_toast( diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 8504d3de5e6ff6a2531f7a146db230ee9e840d14..bf753a1784d02e38c9e4589157b47e02b0bfe53d 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -1584,27 +1584,34 @@ mod tests { } fn editor_blocks(editor: &View, cx: &mut WindowContext) -> Vec<(u32, SharedString)> { + let editor_view = editor.clone(); editor.update(cx, |editor, cx| { let snapshot = editor.snapshot(cx); snapshot .blocks_in_range(0..snapshot.max_point().row()) .enumerate() .filter_map(|(ix, (row, block))| { - let name = match block { - TransformBlock::Custom(block) => block - .render(&mut BlockContext { - view_context: cx, - anchor_x: px(0.), - gutter_padding: px(0.), - gutter_width: px(0.), - line_height: px(0.), - em_width: px(0.), - block_id: ix, - editor_style: &editor::EditorStyle::default(), - }) - .inner_id()? - .try_into() - .ok()?, + let name: SharedString = match block { + TransformBlock::Custom(block) => cx.with_element_context({ + let editor_view = editor_view.clone(); + |cx| -> Option { + block + .render(&mut BlockContext { + context: cx, + anchor_x: px(0.), + gutter_padding: px(0.), + gutter_width: px(0.), + line_height: px(0.), + em_width: px(0.), + block_id: ix, + view: editor_view, + editor_style: &editor::EditorStyle::default(), + }) + .inner_id()? + .try_into() + .ok() + } + })?, TransformBlock::ExcerptHeader { starts_new_buffer, .. diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index 9532bb642d85b15ae5cd8edf68e2338b1cefa174..4edc1d12ea744c53f4e9878573684a95387c0ea5 100644 --- a/crates/editor/src/actions.rs +++ b/crates/editor/src/actions.rs @@ -214,5 +214,6 @@ gpui::actions!( Undo, UndoSelection, UnfoldLines, + DisplayCursorNames ] ); diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index dbbcbccb6e52b1596c408fb45d82c4798a871f46..1b51e55352b1feb08079c54eb8f45800ae7241d4 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -4,7 +4,7 @@ use super::{ }; use crate::{Anchor, Editor, EditorStyle, ExcerptId, ExcerptRange, ToPoint as _}; use collections::{Bound, HashMap, HashSet}; -use gpui::{AnyElement, Pixels, ViewContext}; +use gpui::{AnyElement, ElementContext, Pixels, View}; use language::{BufferSnapshot, Chunk, Patch, Point}; use parking_lot::Mutex; use std::{ @@ -81,7 +81,8 @@ pub enum BlockStyle { } pub struct BlockContext<'a, 'b> { - pub view_context: &'b mut ViewContext<'a, Editor>, + pub context: &'b mut ElementContext<'a>, + pub view: View, pub anchor_x: Pixels, pub gutter_width: Pixels, pub gutter_padding: Pixels, @@ -933,16 +934,16 @@ impl BlockDisposition { } impl<'a> Deref for BlockContext<'a, '_> { - type Target = ViewContext<'a, Editor>; + type Target = ElementContext<'a>; fn deref(&self) -> &Self::Target { - self.view_context + self.context } } impl DerefMut for BlockContext<'_, '_> { fn deref_mut(&mut self) -> &mut Self::Target { - self.view_context + self.context } } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 0e894c5f3f1293d6c8aa5b66b595ff200c0dc59b..e378425666c9550e30c18d447d67baf3b0e05c76 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -40,7 +40,7 @@ pub(crate) use actions::*; use aho_corasick::AhoCorasick; use anyhow::{anyhow, Context as _, Result}; use blink_manager::BlinkManager; -use client::{Client, Collaborator, ParticipantIndex}; +use client::{Collaborator, ParticipantIndex}; use clock::ReplicaId; use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque}; use convert_case::{Case, Casing}; @@ -56,10 +56,11 @@ use git::diff_hunk_to_display; use gpui::{ div, impl_actions, point, prelude::*, px, relative, rems, size, uniform_list, Action, AnyElement, AppContext, AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, Context, - DispatchPhase, ElementId, EventEmitter, FocusHandle, FocusableView, FontStyle, FontWeight, - HighlightStyle, Hsla, InputHandler, InteractiveText, KeyContext, Model, MouseButton, + DispatchPhase, ElementId, EventEmitter, FocusHandle, FocusableView, FontId, FontStyle, + FontWeight, HighlightStyle, Hsla, InteractiveText, KeyContext, Model, MouseButton, ParentElement, Pixels, Render, SharedString, Styled, StyledText, Subscription, Task, TextStyle, - UniformListScrollHandle, View, ViewContext, VisualContext, WeakView, WhiteSpace, WindowContext, + UniformListScrollHandle, View, ViewContext, ViewInputHandler, VisualContext, WeakView, + WhiteSpace, WindowContext, }; use highlight_matching_bracket::refresh_matching_bracket_highlights; use hover_popover::{hide_hover, HoverState}; @@ -71,8 +72,7 @@ use language::{ language_settings::{self, all_language_settings, InlayHintSettings}, markdown, point_from_lsp, AutoindentMode, BracketPair, Buffer, Capability, CodeAction, CodeLabel, Completion, CursorShape, Diagnostic, Documentation, IndentKind, IndentSize, - Language, LanguageRegistry, LanguageServerName, OffsetRangeExt, Point, Selection, - SelectionGoal, TransactionId, + Language, LanguageServerName, OffsetRangeExt, Point, Selection, SelectionGoal, TransactionId, }; use link_go_to_definition::{GoToDefinitionLink, InlayHighlight, LinkGoToDefinitionState}; @@ -88,7 +88,7 @@ use ordered_float::OrderedFloat; use parking_lot::RwLock; use project::{FormatTrigger, Location, Project, ProjectPath, ProjectTransaction}; use rand::prelude::*; -use rpc::proto::{self, *}; +use rpc::proto::*; use scroll::{Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide}; use selections_collection::{resolve_multiple, MutableSelectionsCollection, SelectionsCollection}; use serde::{Deserialize, Serialize}; @@ -365,8 +365,11 @@ pub struct Editor { active_diagnostics: Option, soft_wrap_mode_override: Option, project: Option>, + completion_provider: Option>, collaboration_hub: Option>, blink_manager: Model, + show_cursor_names: bool, + hovered_cursor: Option, pub show_local_selections: bool, mode: EditorMode, show_gutter: bool, @@ -406,11 +409,12 @@ pub struct Editor { style: Option, editor_actions: Vec)>>, show_copilot_suggestions: bool, + use_autoclose: bool, } pub struct EditorSnapshot { pub mode: EditorMode, - pub show_gutter: bool, + show_gutter: bool, pub display_snapshot: DisplaySnapshot, pub placeholder_text: Option>, is_focused: bool, @@ -418,6 +422,23 @@ pub struct EditorSnapshot { ongoing_scroll: OngoingScroll, } +pub struct GutterDimensions { + pub padding: Pixels, + pub width: Pixels, + pub margin: Pixels, +} + +impl Default for GutterDimensions { + fn default() -> Self { + Self { + padding: Pixels::ZERO, + width: Pixels::ZERO, + margin: Pixels::ZERO, + } + } +} + +#[derive(Debug)] pub struct RemoteSelection { pub replica_id: ReplicaId, pub selection: Selection, @@ -425,6 +446,7 @@ pub struct RemoteSelection { pub peer_id: PeerId, pub line_mode: bool, pub participant_index: Option, + pub user_name: Option, } #[derive(Clone, Debug)] @@ -441,6 +463,11 @@ enum SelectionHistoryMode { Redoing, } +struct HoveredCursor { + replica_id: u16, + selection_id: usize, +} + impl Default for SelectionHistoryMode { fn default() -> Self { Self::Normal @@ -722,85 +749,21 @@ impl CompletionsMenu { return None; } - let Some(project) = editor.project.clone() else { + let Some(provider) = editor.completion_provider.as_ref() else { return None; }; - let client = project.read(cx).client(); - let language_registry = project.read(cx).languages().clone(); - - let is_remote = project.read(cx).is_remote(); - let project_id = project.read(cx).remote_id(); - - let completions = self.completions.clone(); - let completion_indices: Vec<_> = self.matches.iter().map(|m| m.candidate_id).collect(); - - Some(cx.spawn(move |this, mut cx| async move { - if is_remote { - let Some(project_id) = project_id else { - log::error!("Remote project without remote_id"); - return; - }; - - for completion_index in completion_indices { - let completions_guard = completions.read(); - let completion = &completions_guard[completion_index]; - if completion.documentation.is_some() { - continue; - } - - let server_id = completion.server_id; - let completion = completion.lsp_completion.clone(); - drop(completions_guard); - - Self::resolve_completion_documentation_remote( - project_id, - server_id, - completions.clone(), - completion_index, - completion, - client.clone(), - language_registry.clone(), - ) - .await; - - _ = this.update(&mut cx, |_, cx| cx.notify()); - } - } else { - for completion_index in completion_indices { - let completions_guard = completions.read(); - let completion = &completions_guard[completion_index]; - if completion.documentation.is_some() { - continue; - } - - let server_id = completion.server_id; - let completion = completion.lsp_completion.clone(); - drop(completions_guard); - - let server = project - .read_with(&mut cx, |project, _| { - project.language_server_for_id(server_id) - }) - .ok() - .flatten(); - let Some(server) = server else { - return; - }; - - Self::resolve_completion_documentation_local( - server, - completions.clone(), - completion_index, - completion, - language_registry.clone(), - ) - .await; + let resolve_task = provider.resolve_completions( + self.matches.iter().map(|m| m.candidate_id).collect(), + self.completions.clone(), + cx, + ); - _ = this.update(&mut cx, |_, cx| cx.notify()); - } + return Some(cx.spawn(move |this, mut cx| async move { + if let Some(true) = resolve_task.await.log_err() { + this.update(&mut cx, |_, cx| cx.notify()).ok(); } - })) + })); } fn attempt_resolve_selected_completion_documentation( @@ -817,146 +780,16 @@ impl CompletionsMenu { let Some(project) = project else { return; }; - let language_registry = project.read(cx).languages().clone(); - - let completions = self.completions.clone(); - let completions_guard = completions.read(); - let completion = &completions_guard[completion_index]; - if completion.documentation.is_some() { - return; - } - - let server_id = completion.server_id; - let completion = completion.lsp_completion.clone(); - drop(completions_guard); - - if project.read(cx).is_remote() { - let Some(project_id) = project.read(cx).remote_id() else { - log::error!("Remote project without remote_id"); - return; - }; - - let client = project.read(cx).client(); - - cx.spawn(move |this, mut cx| async move { - Self::resolve_completion_documentation_remote( - project_id, - server_id, - completions.clone(), - completion_index, - completion, - client, - language_registry.clone(), - ) - .await; - - _ = this.update(&mut cx, |_, cx| cx.notify()); - }) - .detach(); - } else { - let Some(server) = project.read(cx).language_server_for_id(server_id) else { - return; - }; - - cx.spawn(move |this, mut cx| async move { - Self::resolve_completion_documentation_local( - server, - completions, - completion_index, - completion, - language_registry, - ) - .await; - - _ = this.update(&mut cx, |_, cx| cx.notify()); - }) - .detach(); - } - } - - async fn resolve_completion_documentation_remote( - project_id: u64, - server_id: LanguageServerId, - completions: Arc>>, - completion_index: usize, - completion: lsp::CompletionItem, - client: Arc, - language_registry: Arc, - ) { - let request = proto::ResolveCompletionDocumentation { - project_id, - language_server_id: server_id.0 as u64, - lsp_completion: serde_json::to_string(&completion).unwrap().into_bytes(), - }; - - let Some(response) = client - .request(request) - .await - .context("completion documentation resolve proto request") - .log_err() - else { - return; - }; - - if response.text.is_empty() { - let mut completions = completions.write(); - let completion = &mut completions[completion_index]; - completion.documentation = Some(Documentation::Undocumented); - } - - let documentation = if response.is_markdown { - Documentation::MultiLineMarkdown( - markdown::parse_markdown(&response.text, &language_registry, None).await, - ) - } else if response.text.lines().count() <= 1 { - Documentation::SingleLine(response.text) - } else { - Documentation::MultiLinePlainText(response.text) - }; - - let mut completions = completions.write(); - let completion = &mut completions[completion_index]; - completion.documentation = Some(documentation); - } - - async fn resolve_completion_documentation_local( - server: Arc, - completions: Arc>>, - completion_index: usize, - completion: lsp::CompletionItem, - language_registry: Arc, - ) { - let can_resolve = server - .capabilities() - .completion_provider - .as_ref() - .and_then(|options| options.resolve_provider) - .unwrap_or(false); - if !can_resolve { - return; - } - - let request = server.request::(completion); - let Some(completion_item) = request.await.log_err() else { - return; - }; - - if let Some(lsp_documentation) = completion_item.documentation { - let documentation = language::prepare_completion_documentation( - &lsp_documentation, - &language_registry, - None, // TODO: Try to reasonably work out which language the completion is for - ) - .await; - let mut completions = completions.write(); - let completion = &mut completions[completion_index]; - completion.documentation = Some(documentation); - } else { - let mut completions = completions.write(); - let completion = &mut completions[completion_index]; - completion.documentation = Some(Documentation::Undocumented); - } + let resolve_task = project.update(cx, |project, cx| { + project.resolve_completions(vec![completion_index], self.completions.clone(), cx) + }); + cx.spawn(move |this, mut cx| async move { + if let Some(true) = resolve_task.await.log_err() { + this.update(&mut cx, |_, cx| cx.notify()).ok(); + } + }) + .detach(); } fn visible(&self) -> bool { @@ -1565,6 +1398,7 @@ impl Editor { ime_transaction: Default::default(), active_diagnostics: None, soft_wrap_mode_override, + completion_provider: project.clone().map(|project| Box::new(project) as _), collaboration_hub: project.clone().map(|project| Box::new(project) as _), project, blink_manager: blink_manager.clone(), @@ -1594,6 +1428,7 @@ impl Editor { keymap_context_layers: Default::default(), input_enabled: true, read_only: false, + use_autoclose: true, leader_peer_id: None, remote_id: None, hover_state: Default::default(), @@ -1604,6 +1439,8 @@ impl Editor { pixel_position_of_newest_cursor: None, gutter_width: Default::default(), style: None, + show_cursor_names: false, + hovered_cursor: Default::default(), editor_actions: Default::default(), show_copilot_suggestions: mode == EditorMode::Full, _subscriptions: vec![ @@ -1795,6 +1632,10 @@ impl Editor { self.collaboration_hub = Some(hub); } + pub fn set_completion_provider(&mut self, hub: Box) { + self.completion_provider = Some(hub); + } + pub fn placeholder_text(&self) -> Option<&str> { self.placeholder_text.as_deref() } @@ -1869,6 +1710,10 @@ impl Editor { self.read_only = read_only; } + pub fn set_use_autoclose(&mut self, autoclose: bool) { + self.use_autoclose = autoclose; + } + pub fn set_show_copilot_suggestions(&mut self, show_copilot_suggestions: bool) { self.show_copilot_suggestions = show_copilot_suggestions; } @@ -2467,7 +2312,12 @@ impl Editor { ), &bracket_pair.start[..prefix_len], )); - if following_text_allows_autoclose && preceding_text_matches_prefix { + let autoclose = self.use_autoclose + && snapshot.settings_at(selection.start, cx).use_autoclose; + if autoclose + && following_text_allows_autoclose + && preceding_text_matches_prefix + { let anchor = snapshot.anchor_before(selection.end); new_selections.push((selection.map(|_| anchor), text.len())); new_autoclose_regions.push(( @@ -3241,9 +3091,7 @@ impl Editor { return; } - let project = if let Some(project) = self.project.clone() { - project - } else { + let Some(provider) = self.completion_provider.as_ref() else { return; }; @@ -3259,9 +3107,7 @@ impl Editor { }; let query = Self::completion_query(&self.buffer.read(cx).read(cx), position.clone()); - let completions = project.update(cx, |project, cx| { - project.completions(&buffer, buffer_position, cx) - }); + let completions = provider.completions(&buffer, buffer_position, cx); let id = post_inc(&mut self.next_completion_id); let task = cx.spawn(|this, mut cx| { @@ -3370,6 +3216,7 @@ impl Editor { let buffer_handle = completions_menu.buffer; let completions = completions_menu.completions.read(); let completion = completions.get(mat.candidate_id)?; + cx.stop_propagation(); let snippet; let text; @@ -3466,15 +3313,13 @@ impl Editor { this.refresh_copilot_suggestions(true, cx); }); - let project = self.project.clone()?; - let apply_edits = project.update(cx, |project, cx| { - project.apply_additional_edits_for_completion( - buffer_handle, - completion.clone(), - true, - cx, - ) - }); + let provider = self.completion_provider.as_ref()?; + let apply_edits = provider.apply_additional_edits_for_completion( + buffer_handle, + completion.clone(), + true, + cx, + ); Some(cx.foreground_executor().spawn(async move { apply_edits.await?; Ok(()) @@ -3561,7 +3406,7 @@ impl Editor { let replica_id = this.update(&mut cx, |this, cx| this.replica_id(cx))?; let mut entries = transaction.0.into_iter().collect::>(); - cx.update(|_, cx| { + cx.update(|cx| { entries.sort_unstable_by_key(|(buffer, _)| { buffer.read(cx).file().map(|f| f.path().clone()) }); @@ -3888,6 +3733,24 @@ impl Editor { self.update_visible_copilot_suggestion(cx); } + pub fn display_cursor_names(&mut self, _: &DisplayCursorNames, cx: &mut ViewContext) { + self.show_cursor_names(cx); + } + + fn show_cursor_names(&mut self, cx: &mut ViewContext) { + self.show_cursor_names = true; + cx.notify(); + cx.spawn(|this, mut cx| async move { + cx.background_executor().timer(Duration::from_secs(3)).await; + this.update(&mut cx, |this, cx| { + this.show_cursor_names = false; + cx.notify() + }) + .ok() + }) + .detach(); + } + fn next_copilot_suggestion(&mut self, _: &copilot::NextSuggestion, cx: &mut ViewContext) { if self.has_active_copilot_suggestion(cx) { self.cycle_copilot_suggestions(Direction::Next, cx); @@ -4065,7 +3928,7 @@ impl Editor { gutter_hovered: bool, _line_height: Pixels, _gutter_margin: Pixels, - cx: &mut ViewContext, + editor_view: View, ) -> Vec> { fold_data .iter() @@ -4075,14 +3938,19 @@ impl Editor { .map(|(fold_status, buffer_row, active)| { (active || gutter_hovered || fold_status == FoldStatus::Folded).then(|| { IconButton::new(ix as usize, ui::IconName::ChevronDown) - .on_click(cx.listener(move |editor, _e, cx| match fold_status { - FoldStatus::Folded => { - editor.unfold_at(&UnfoldAt { buffer_row }, cx); + .on_click({ + let view = editor_view.clone(); + move |_e, cx| { + view.update(cx, |editor, cx| match fold_status { + FoldStatus::Folded => { + editor.unfold_at(&UnfoldAt { buffer_row }, cx); + } + FoldStatus::Foldable => { + editor.fold_at(&FoldAt { buffer_row }, cx); + } + }) } - FoldStatus::Foldable => { - editor.fold_at(&FoldAt { buffer_row }, cx); - } - })) + }) .icon_color(ui::Color::Muted) .icon_size(ui::IconSize::Small) .selected(fold_status == FoldStatus::Folded) @@ -8992,6 +8860,7 @@ impl Editor { cx.focus(&rename_editor_focus_handle); } else { self.blink_manager.update(cx, BlinkManager::enable); + self.show_cursor_names(cx); self.buffer.update(cx, |buffer, cx| { buffer.finalize_last_transaction(cx); if self.leader_peer_id.is_none() { @@ -9043,6 +8912,7 @@ pub trait CollaborationHub { &self, cx: &'a AppContext, ) -> &'a HashMap; + fn user_names(&self, cx: &AppContext) -> HashMap; } impl CollaborationHub for Model { @@ -9056,6 +8926,74 @@ impl CollaborationHub for Model { ) -> &'a HashMap { self.read(cx).user_store().read(cx).participant_indices() } + + fn user_names(&self, cx: &AppContext) -> HashMap { + let this = self.read(cx); + let user_ids = this.collaborators().values().map(|c| c.user_id); + this.user_store().read_with(cx, |user_store, cx| { + user_store.participant_names(user_ids, cx) + }) + } +} + +pub trait CompletionProvider { + fn completions( + &self, + buffer: &Model, + buffer_position: text::Anchor, + cx: &mut ViewContext, + ) -> Task>>; + + fn resolve_completions( + &self, + completion_indices: Vec, + completions: Arc>>, + cx: &mut ViewContext, + ) -> Task>; + + fn apply_additional_edits_for_completion( + &self, + buffer: Model, + completion: Completion, + push_to_history: bool, + cx: &mut ViewContext, + ) -> Task>>; +} + +impl CompletionProvider for Model { + fn completions( + &self, + buffer: &Model, + buffer_position: text::Anchor, + cx: &mut ViewContext, + ) -> Task>> { + self.update(cx, |project, cx| { + project.completions(&buffer, buffer_position, cx) + }) + } + + fn resolve_completions( + &self, + completion_indices: Vec, + completions: Arc>>, + cx: &mut ViewContext, + ) -> Task> { + self.update(cx, |project, cx| { + project.resolve_completions(completion_indices, completions, cx) + }) + } + + fn apply_additional_edits_for_completion( + &self, + buffer: Model, + completion: Completion, + push_to_history: bool, + cx: &mut ViewContext, + ) -> Task>> { + self.update(cx, |project, cx| { + project.apply_additional_edits_for_completion(buffer, completion, push_to_history, cx) + }) + } } fn inlay_hint_settings( @@ -9107,6 +9045,7 @@ impl EditorSnapshot { collaboration_hub: &dyn CollaborationHub, cx: &'a AppContext, ) -> impl 'a + Iterator { + let participant_names = collaboration_hub.user_names(cx); let participant_indices = collaboration_hub.user_participant_indices(cx); let collaborators_by_peer_id = collaboration_hub.collaborators(cx); let collaborators_by_replica_id = collaborators_by_peer_id @@ -9118,6 +9057,7 @@ impl EditorSnapshot { .filter_map(move |(replica_id, line_mode, cursor_shape, selection)| { let collaborator = collaborators_by_replica_id.get(&replica_id)?; let participant_index = participant_indices.get(&collaborator.user_id).copied(); + let user_name = participant_names.get(&collaborator.user_id).cloned(); Some(RemoteSelection { replica_id, selection, @@ -9125,6 +9065,7 @@ impl EditorSnapshot { line_mode, participant_index, peer_id: collaborator.peer_id, + user_name, }) }) } @@ -9144,6 +9085,34 @@ impl EditorSnapshot { pub fn scroll_position(&self) -> gpui::Point { self.scroll_anchor.scroll_position(&self.display_snapshot) } + + pub fn gutter_dimensions( + &self, + font_id: FontId, + font_size: Pixels, + em_width: Pixels, + max_line_number_width: Pixels, + cx: &AppContext, + ) -> GutterDimensions { + if self.show_gutter { + let descent = cx.text_system().descent(font_id, font_size); + let gutter_padding_factor = 4.0; + let gutter_padding = (em_width * gutter_padding_factor).round(); + // Avoid flicker-like gutter resizes when the line number gains another digit and only resize the gutter on files with N*10^5 lines. + let min_width_for_number_on_gutter = em_width * 4.0; + let gutter_width = + max_line_number_width.max(min_width_for_number_on_gutter) + gutter_padding * 2.0; + let gutter_margin = -descent; + + GutterDimensions { + padding: gutter_padding, + width: gutter_width, + margin: gutter_margin, + } + } else { + GutterDimensions::default() + } + } } impl Deref for EditorSnapshot { @@ -9258,7 +9227,7 @@ impl Render for Editor { } } -impl InputHandler for Editor { +impl ViewInputHandler for Editor { fn text_for_range( &mut self, range_utf16: Range, @@ -9655,10 +9624,10 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, _is_valid: bool) -> Ren .size(ButtonSize::Compact) .style(ButtonStyle::Transparent) .visible_on_hover(group_id) - .on_click(cx.listener({ + .on_click({ let message = diagnostic.message.clone(); - move |_, _, cx| cx.write_to_clipboard(ClipboardItem::new(message.clone())) - })) + move |_click, cx| cx.write_to_clipboard(ClipboardItem::new(message.clone())) + }) .tooltip(|cx| Tooltip::text("Copy diagnostic message", cx)), ) .into_any_element() diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 06faf3265ebdffdc54bccc19529b008e4059c46b..eeb8263f30fa1449350bf7872d45866cadadb54f 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -25,12 +25,12 @@ use collections::{BTreeMap, HashMap}; use git::diff::DiffHunkStatus; use gpui::{ div, fill, outline, overlay, point, px, quad, relative, size, transparent_black, Action, - AnchorCorner, AnyElement, AvailableSpace, BorrowWindow, Bounds, ContentMask, Corners, - CursorStyle, DispatchPhase, Edges, Element, ElementInputHandler, Entity, Hsla, - InteractiveBounds, InteractiveElement, IntoElement, ModifiersChangedEvent, MouseButton, - MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, ScrollDelta, - ScrollWheelEvent, ShapedLine, SharedString, Size, StackingOrder, StatefulInteractiveElement, - Style, Styled, TextRun, TextStyle, View, ViewContext, WindowContext, + AnchorCorner, AnyElement, AvailableSpace, Bounds, ContentMask, Corners, CursorStyle, + DispatchPhase, Edges, Element, ElementInputHandler, Entity, Hsla, InteractiveBounds, + InteractiveElement, IntoElement, ModifiersChangedEvent, MouseButton, MouseDownEvent, + MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, ScrollDelta, ScrollWheelEvent, ShapedLine, + SharedString, Size, StackingOrder, StatefulInteractiveElement, Style, Styled, TextRun, + TextStyle, View, ViewContext, WindowContext, }; use itertools::Itertools; use language::language_settings::ShowWhitespaceSetting; @@ -64,6 +64,7 @@ struct SelectionLayout { is_local: bool, range: Range, active_rows: Range, + user_name: Option, } impl SelectionLayout { @@ -74,6 +75,7 @@ impl SelectionLayout { map: &DisplaySnapshot, is_newest: bool, is_local: bool, + user_name: Option, ) -> Self { let point_selection = selection.map(|p| p.to_point(&map.buffer_snapshot)); let display_selection = point_selection.map(|p| p.to_display_point(map)); @@ -113,6 +115,7 @@ impl SelectionLayout { is_local, range, active_rows, + user_name, } } } @@ -324,9 +327,10 @@ impl EditorElement { register_action(view, cx, Editor::context_menu_prev); register_action(view, cx, Editor::context_menu_next); register_action(view, cx, Editor::context_menu_last); + register_action(view, cx, Editor::display_cursor_names); } - fn register_key_listeners(&self, cx: &mut WindowContext) { + fn register_key_listeners(&self, cx: &mut ElementContext) { cx.on_key_event({ let editor = self.editor.clone(); move |event: &ModifiersChangedEvent, phase, cx| { @@ -564,6 +568,7 @@ impl EditorElement { cx, ); hover_at(editor, Some(point), cx); + Self::update_visible_cursor(editor, point, cx); } None => { update_inlay_link_and_hover_points( @@ -585,12 +590,45 @@ impl EditorElement { } } + fn update_visible_cursor( + editor: &mut Editor, + point: DisplayPoint, + cx: &mut ViewContext, + ) { + let snapshot = editor.snapshot(cx); + let Some(hub) = editor.collaboration_hub() else { + return; + }; + let range = DisplayPoint::new(point.row(), point.column().saturating_sub(1)) + ..DisplayPoint::new( + point.row(), + (point.column() + 1).min(snapshot.line_len(point.row())), + ); + + let range = snapshot + .buffer_snapshot + .anchor_at(range.start.to_point(&snapshot.display_snapshot), Bias::Left) + ..snapshot + .buffer_snapshot + .anchor_at(range.end.to_point(&snapshot.display_snapshot), Bias::Right); + + let Some(selection) = snapshot.remote_selections_in_range(&range, hub, cx).next() else { + editor.hovered_cursor.take(); + return; + }; + editor.hovered_cursor.replace(crate::HoveredCursor { + replica_id: selection.replica_id, + selection_id: selection.selection.id, + }); + cx.notify() + } + fn paint_background( &self, gutter_bounds: Bounds, text_bounds: Bounds, layout: &LayoutState, - cx: &mut WindowContext, + cx: &mut ElementContext, ) { let bounds = gutter_bounds.union(&text_bounds); let scroll_top = @@ -673,7 +711,7 @@ impl EditorElement { &mut self, bounds: Bounds, layout: &mut LayoutState, - cx: &mut WindowContext, + cx: &mut ElementContext, ) { let line_height = layout.position_map.line_height; @@ -744,7 +782,7 @@ impl EditorElement { }); } - fn paint_diff_hunks(bounds: Bounds, layout: &LayoutState, cx: &mut WindowContext) { + fn paint_diff_hunks(bounds: Bounds, layout: &LayoutState, cx: &mut ElementContext) { let line_height = layout.position_map.line_height; let scroll_position = layout.position_map.snapshot.scroll_position(); @@ -848,7 +886,7 @@ impl EditorElement { &mut self, text_bounds: Bounds, layout: &mut LayoutState, - cx: &mut WindowContext, + cx: &mut ElementContext, ) { let start_row = layout.visible_display_row_range.start; let content_origin = text_bounds.origin + point(layout.gutter_margin, Pixels::ZERO); @@ -982,8 +1020,10 @@ impl EditorElement { let corner_radius = 0.15 * layout.position_map.line_height; let mut invisible_display_ranges = SmallVec::<[Range; 32]>::new(); - for (selection_style, selections) in &layout.selections { - for selection in selections { + for (participant_ix, (selection_style, selections)) in + layout.selections.iter().enumerate() + { + for selection in selections.into_iter() { self.paint_highlighted_range( selection.range.clone(), selection_style.selection, @@ -1064,6 +1104,7 @@ impl EditorElement { )) }); } + cursors.push(Cursor { color: selection_style.cursor, block_width, @@ -1071,6 +1112,14 @@ impl EditorElement { line_height: layout.position_map.line_height, shape: selection.cursor_shape, block_text, + cursor_name: selection.user_name.clone().map(|name| { + CursorName { + string: name, + color: self.style.background, + is_top_row: cursor_position.row() == 0, + z_index: (participant_ix % 256).try_into().unwrap(), + } + }), }); } } @@ -1104,7 +1153,7 @@ impl EditorElement { &mut self, text_bounds: Bounds, layout: &mut LayoutState, - cx: &mut WindowContext, + cx: &mut ElementContext, ) { let content_origin = text_bounds.origin + point(layout.gutter_margin, Pixels::ZERO); let start_row = layout.visible_display_row_range.start; @@ -1169,9 +1218,11 @@ impl EditorElement { popover_origin.x = popover_origin.x + x_out_of_bounds; } - cx.break_content_mask(|cx| { - hover_popover.draw(popover_origin, available_space, cx) - }); + if cx.was_top_layer(&popover_origin, cx.stacking_order()) { + cx.break_content_mask(|cx| { + hover_popover.draw(popover_origin, available_space, cx) + }); + } current_y = popover_origin.y - HOVER_POPOVER_GAP; } @@ -1217,7 +1268,7 @@ impl EditorElement { &mut self, bounds: Bounds, layout: &mut LayoutState, - cx: &mut WindowContext, + cx: &mut ElementContext, ) { if layout.mode != EditorMode::Full { return; @@ -1461,7 +1512,7 @@ impl EditorElement { layout: &LayoutState, content_origin: gpui::Point, bounds: Bounds, - cx: &mut WindowContext, + cx: &mut ElementContext, ) { let start_row = layout.visible_display_row_range.start; let end_row = layout.visible_display_row_range.end; @@ -1513,7 +1564,7 @@ impl EditorElement { &mut self, bounds: Bounds, layout: &mut LayoutState, - cx: &mut WindowContext, + cx: &mut ElementContext, ) { let scroll_position = layout.position_map.snapshot.scroll_position(); let scroll_left = scroll_position.x * layout.position_map.em_width; @@ -1763,7 +1814,7 @@ impl EditorElement { } } - fn compute_layout(&mut self, bounds: Bounds, cx: &mut WindowContext) -> LayoutState { + fn compute_layout(&mut self, bounds: Bounds, cx: &mut ElementContext) -> LayoutState { self.editor.update(cx, |editor, cx| { let snapshot = editor.snapshot(cx); let style = self.style.clone(); @@ -1783,30 +1834,16 @@ impl EditorElement { .unwrap() .width; - let gutter_padding; - let gutter_width; - let gutter_margin; - if snapshot.show_gutter { - let descent = cx.text_system().descent(font_id, font_size); + let gutter_dimensions = snapshot.gutter_dimensions(font_id, font_size, em_width, self.max_line_number_width(&snapshot, cx), cx); - let gutter_padding_factor = 3.5; - gutter_padding = (em_width * gutter_padding_factor).round(); - gutter_width = self.max_line_number_width(&snapshot, cx) + gutter_padding * 2.0; - gutter_margin = -descent; - } else { - gutter_padding = Pixels::ZERO; - gutter_width = Pixels::ZERO; - gutter_margin = Pixels::ZERO; - }; - - editor.gutter_width = gutter_width; + editor.gutter_width = gutter_dimensions.width; - let text_width = bounds.size.width - gutter_width; + let text_width = bounds.size.width - gutter_dimensions.width; let overscroll = size(em_width, px(0.)); let _snapshot = { editor.set_visible_line_count((bounds.size.height / line_height).into(), cx); - let editor_width = text_width - gutter_margin - overscroll.width - em_width; + let editor_width = text_width - gutter_dimensions.margin - overscroll.width - em_width; let wrap_width = match editor.soft_wrap_mode(cx) { SoftWrap::None => (MAX_LINE_LEN / 2) as f32 * em_advance, SoftWrap::EditorWidth => editor_width, @@ -1826,7 +1863,7 @@ impl EditorElement { .map(|(guide, active)| (self.column_pixels(*guide, cx), *active)) .collect::>(); - let gutter_size = size(gutter_width, bounds.size.height); + let gutter_size = size(gutter_dimensions.width, bounds.size.height); let text_size = size(text_width, bounds.size.height); let autoscroll_horizontally = @@ -1889,6 +1926,7 @@ impl EditorElement { &snapshot.display_snapshot, is_newest, true, + None, ); if is_newest { newest_selection_head = Some(layout.head); @@ -1949,6 +1987,7 @@ impl EditorElement { if Some(selection.peer_id) == editor.leader_peer_id { continue; } + let is_shown = editor.show_cursor_names || editor.hovered_cursor.as_ref().is_some_and(|c| c.replica_id == selection.replica_id && c.selection_id == selection.selection.id); remote_selections .entry(selection.replica_id) @@ -1961,6 +2000,11 @@ impl EditorElement { &snapshot.display_snapshot, false, false, + if is_shown { + selection.user_name + } else { + None + }, )); } @@ -1992,6 +2036,7 @@ impl EditorElement { &snapshot.display_snapshot, true, true, + None, ) .head }); @@ -2022,22 +2067,26 @@ impl EditorElement { .width; let scroll_width = longest_line_width.max(max_visible_line_width) + overscroll.width; - let (scroll_width, blocks) = cx.with_element_id(Some("editor_blocks"), |cx| { + let editor_view = cx.view().clone(); + let (scroll_width, blocks) = cx.with_element_context(|cx| { + cx.with_element_id(Some("editor_blocks"), |cx| { self.layout_blocks( start_row..end_row, &snapshot, bounds.size.width, scroll_width, - gutter_padding, - gutter_width, + gutter_dimensions.padding, + gutter_dimensions.width, em_width, - gutter_width + gutter_margin, + gutter_dimensions.width + gutter_dimensions.margin, line_height, &style, &line_layouts, editor, + editor_view, cx, ) + }) }); let scroll_max = point( @@ -2069,7 +2118,13 @@ impl EditorElement { if let Some(newest_selection_head) = newest_selection_head { if (start_row..end_row).contains(&newest_selection_head.row()) { if editor.context_menu_visible() { - let max_height = (12. * line_height).min((bounds.size.height - line_height) / 2.); + let max_height = cmp::min( + 12. * line_height, + cmp::max( + 3. * line_height, + (bounds.size.height - line_height) / 2., + ) + ); context_menu = editor.render_context_menu(newest_selection_head, &self.style, max_height, cx); } @@ -2107,15 +2162,19 @@ impl EditorElement { cx, ); - let fold_indicators = cx.with_element_id(Some("gutter_fold_indicators"), |cx| { + let editor_view = cx.view().clone(); + let fold_indicators = cx.with_element_context(|cx| { + + cx.with_element_id(Some("gutter_fold_indicators"), |_cx| { editor.render_fold_indicators( fold_statuses, &style, editor.gutter_hovered, line_height, - gutter_margin, - cx, + gutter_dimensions.margin, + editor_view, ) + }) }); let invisible_symbol_font_size = font_size / 2.; @@ -2167,13 +2226,13 @@ impl EditorElement { visible_display_row_range: start_row..end_row, wrap_guides, gutter_size, - gutter_padding, + gutter_padding: gutter_dimensions.padding, text_size, scrollbar_row_range, show_scrollbars, is_singleton, max_row, - gutter_margin, + gutter_margin: gutter_dimensions.margin, active_rows, highlighted_rows, highlighted_ranges, @@ -2206,7 +2265,8 @@ impl EditorElement { style: &EditorStyle, line_layouts: &[LineWithInvisibles], editor: &mut Editor, - cx: &mut ViewContext, + editor_view: View, + cx: &mut ElementContext, ) -> (Pixels, Vec) { let mut block_id = 0; let (fixed_blocks, non_fixed_blocks) = snapshot @@ -2220,7 +2280,7 @@ impl EditorElement { available_space: Size, block_id: usize, editor: &mut Editor, - cx: &mut ViewContext| { + cx: &mut ElementContext| { let mut element = match block { TransformBlock::Custom(block) => { let align_to = block @@ -2239,13 +2299,14 @@ impl EditorElement { }; block.render(&mut BlockContext { - view_context: cx, + context: cx, anchor_x, gutter_padding, line_height, gutter_width, em_width, block_id, + view: editor_view.clone(), editor_style: &self.style, }) } @@ -2437,7 +2498,7 @@ impl EditorElement { &mut self, interactive_bounds: &InteractiveBounds, layout: &LayoutState, - cx: &mut WindowContext, + cx: &mut ElementContext, ) { cx.on_mouse_event({ let position_map = layout.position_map.clone(); @@ -2497,7 +2558,7 @@ impl EditorElement { gutter_bounds: Bounds, text_bounds: Bounds, layout: &LayoutState, - cx: &mut WindowContext, + cx: &mut ElementContext, ) { let interactive_bounds = InteractiveBounds { bounds: bounds.intersect(&cx.content_mask().bounds), @@ -2720,7 +2781,7 @@ impl LineWithInvisibles { content_origin: gpui::Point, whitespace_setting: ShowWhitespaceSetting, selection_ranges: &[Range], - cx: &mut WindowContext, + cx: &mut ElementContext, ) { let line_height = layout.position_map.line_height; let line_y = line_height * row as f32 - layout.position_map.scroll_position.y; @@ -2754,7 +2815,7 @@ impl LineWithInvisibles { row: u32, line_height: Pixels, whitespace_setting: ShowWhitespaceSetting, - cx: &mut WindowContext, + cx: &mut ElementContext, ) { let allowed_invisibles_regions = match whitespace_setting { ShowWhitespaceSetting::None => return, @@ -2803,7 +2864,7 @@ impl Element for EditorElement { fn request_layout( &mut self, _element_state: Option, - cx: &mut gpui::WindowContext, + cx: &mut gpui::ElementContext, ) -> (gpui::LayoutId, Self::State) { cx.with_view_id(self.editor.entity_id(), |cx| { self.editor.update(cx, |editor, cx| { @@ -2815,34 +2876,36 @@ impl Element for EditorElement { let mut style = Style::default(); style.size.width = relative(1.).into(); style.size.height = self.style.text.line_height_in_pixels(rem_size).into(); - cx.request_layout(&style, None) + cx.with_element_context(|cx| cx.request_layout(&style, None)) } EditorMode::AutoHeight { max_lines } => { let editor_handle = cx.view().clone(); let max_line_number_width = self.max_line_number_width(&editor.snapshot(cx), cx); - cx.request_measured_layout( - Style::default(), - move |known_dimensions, _, cx| { - editor_handle - .update(cx, |editor, cx| { - compute_auto_height_layout( - editor, - max_lines, - max_line_number_width, - known_dimensions, - cx, - ) - }) - .unwrap_or_default() - }, - ) + cx.with_element_context(|cx| { + cx.request_measured_layout( + Style::default(), + move |known_dimensions, _, cx| { + editor_handle + .update(cx, |editor, cx| { + compute_auto_height_layout( + editor, + max_lines, + max_line_number_width, + known_dimensions, + cx, + ) + }) + .unwrap_or_default() + }, + ) + }) } EditorMode::Full => { let mut style = Style::default(); style.size.width = relative(1.).into(); style.size.height = relative(1.).into(); - cx.request_layout(&style, None) + cx.with_element_context(|cx| cx.request_layout(&style, None)) } }; @@ -2855,7 +2918,7 @@ impl Element for EditorElement { &mut self, bounds: Bounds, _element_state: &mut Self::State, - cx: &mut gpui::WindowContext, + cx: &mut gpui::ElementContext, ) { let editor = self.editor.clone(); @@ -2884,9 +2947,10 @@ impl Element for EditorElement { self.register_key_listeners(cx); cx.with_content_mask(Some(ContentMask { bounds }), |cx| { - let input_handler = - ElementInputHandler::new(bounds, self.editor.clone(), cx); - cx.handle_input(&focus_handle, input_handler); + cx.handle_input( + &focus_handle, + ElementInputHandler::new(bounds, self.editor.clone()), + ); self.paint_background(gutter_bounds, text_bounds, &layout, cx); if layout.gutter_size.width > Pixels::ZERO { @@ -3097,6 +3161,15 @@ pub struct Cursor { color: Hsla, shape: CursorShape, block_text: Option, + cursor_name: Option, +} + +#[derive(Debug)] +pub struct CursorName { + string: SharedString, + color: Hsla, + is_top_row: bool, + z_index: u16, } impl Cursor { @@ -3107,6 +3180,7 @@ impl Cursor { color: Hsla, shape: CursorShape, block_text: Option, + cursor_name: Option, ) -> Cursor { Cursor { origin, @@ -3115,6 +3189,7 @@ impl Cursor { color, shape, block_text, + cursor_name, } } @@ -3125,7 +3200,7 @@ impl Cursor { } } - pub fn paint(&self, origin: gpui::Point, cx: &mut WindowContext) { + pub fn paint(&self, origin: gpui::Point, cx: &mut ElementContext) { let bounds = match self.shape { CursorShape::Bar => Bounds { origin: self.origin + origin, @@ -3150,6 +3225,31 @@ impl Cursor { fill(bounds, self.color) }; + if let Some(name) = &self.cursor_name { + let text_size = self.line_height / 1.5; + + let name_origin = if name.is_top_row { + point(bounds.right() - px(1.), bounds.top()) + } else { + point(bounds.left(), bounds.top() - text_size / 2. - px(1.)) + }; + cx.with_z_index(name.z_index, |cx| { + div() + .bg(self.color) + .text_size(text_size) + .px_0p5() + .line_height(text_size + px(2.)) + .text_color(name.color) + .child(name.string.clone()) + .into_any_element() + .draw( + name_origin, + size(AvailableSpace::MinContent, AvailableSpace::MinContent), + cx, + ) + }) + } + cx.paint_quad(cursor); if let Some(block_text) = &self.block_text { @@ -3180,7 +3280,7 @@ pub struct HighlightedRangeLine { } impl HighlightedRange { - pub fn paint(&self, bounds: Bounds, cx: &mut WindowContext) { + pub fn paint(&self, bounds: Bounds, cx: &mut ElementContext) { if self.lines.len() >= 2 && self.lines[0].start_x > self.lines[1].end_x { self.paint_lines(self.start_y, &self.lines[0..1], bounds, cx); self.paint_lines( @@ -3199,7 +3299,7 @@ impl HighlightedRange { start_y: Pixels, lines: &[HighlightedRangeLine], _bounds: Bounds, - cx: &mut WindowContext, + cx: &mut ElementContext, ) { if lines.is_empty() { return; @@ -3417,14 +3517,16 @@ mod tests { .unwrap(); let state = cx .update_window(window.into(), |view, cx| { - cx.with_view_id(view.entity_id(), |cx| { - element.compute_layout( - Bounds { - origin: point(px(500.), px(500.)), - size: size(px(500.), px(500.)), - }, - cx, - ) + cx.with_element_context(|cx| { + cx.with_view_id(view.entity_id(), |cx| { + element.compute_layout( + Bounds { + origin: point(px(500.), px(500.)), + size: size(px(500.), px(500.)), + }, + cx, + ) + }) }) }) .unwrap(); @@ -3511,14 +3613,16 @@ mod tests { let state = cx .update_window(window.into(), |view, cx| { - cx.with_view_id(view.entity_id(), |cx| { - element.compute_layout( - Bounds { - origin: point(px(500.), px(500.)), - size: size(px(500.), px(500.)), - }, - cx, - ) + cx.with_element_context(|cx| { + cx.with_view_id(view.entity_id(), |cx| { + element.compute_layout( + Bounds { + origin: point(px(500.), px(500.)), + size: size(px(500.), px(500.)), + }, + cx, + ) + }) }) }) .unwrap(); @@ -3575,14 +3679,16 @@ mod tests { let mut element = EditorElement::new(&editor, style); let state = cx .update_window(window.into(), |view, cx| { - cx.with_view_id(view.entity_id(), |cx| { - element.compute_layout( - Bounds { - origin: point(px(500.), px(500.)), - size: size(px(500.), px(500.)), - }, - cx, - ) + cx.with_element_context(|cx| { + cx.with_view_id(view.entity_id(), |cx| { + element.compute_layout( + Bounds { + origin: point(px(500.), px(500.)), + size: size(px(500.), px(500.)), + }, + cx, + ) + }) }) }) .unwrap(); @@ -3600,8 +3706,10 @@ mod tests { // Don't panic. let bounds = Bounds::::new(Default::default(), size); - cx.update_window(window.into(), |_, cx| element.paint(bounds, &mut (), cx)) - .unwrap() + cx.update_window(window.into(), |_, cx| { + cx.with_element_context(|cx| element.paint(bounds, &mut (), cx)) + }) + .unwrap() } #[gpui::test] @@ -3776,13 +3884,15 @@ mod tests { .unwrap(); let layout_state = cx .update_window(window.into(), |_, cx| { - element.compute_layout( - Bounds { - origin: point(px(500.), px(500.)), - size: size(px(500.), px(500.)), - }, - cx, - ) + cx.with_element_context(|cx| { + element.compute_layout( + Bounds { + origin: point(px(500.), px(500.)), + size: size(px(500.), px(500.)), + }, + cx, + ) + }) }) .unwrap(); @@ -3837,24 +3947,14 @@ fn compute_auto_height_layout( .width; let mut snapshot = editor.snapshot(cx); - let gutter_width; - let gutter_margin; - if snapshot.show_gutter { - let descent = cx.text_system().descent(font_id, font_size); - let gutter_padding_factor = 3.5; - let gutter_padding = (em_width * gutter_padding_factor).round(); - gutter_width = max_line_number_width + gutter_padding * 2.0; - gutter_margin = -descent; - } else { - gutter_width = Pixels::ZERO; - gutter_margin = Pixels::ZERO; - }; + let gutter_dimensions = + snapshot.gutter_dimensions(font_id, font_size, em_width, max_line_number_width, cx); - editor.gutter_width = gutter_width; - let text_width = width - gutter_width; + editor.gutter_width = gutter_dimensions.width; + let text_width = width - gutter_dimensions.width; let overscroll = size(em_width, px(0.)); - let editor_width = text_width - gutter_margin - overscroll.width - em_width; + let editor_width = text_width - gutter_dimensions.margin - overscroll.width - em_width; if editor.set_wrap_width(Some(editor_width), cx) { snapshot = editor.snapshot(cx); } diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 609c20ac680819ff738f4a4f3ab08ffa58762146..f311f20ae6d0faf9bc42193245fa42ae14cc5fac 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -247,7 +247,7 @@ fn show_hover( }; // query the LSP for hover info - let hover_request = cx.update(|_, cx| { + let hover_request = cx.update(|cx| { project.update(cx, |project, cx| { project.hover(&buffer, buffer_position, cx) }) @@ -545,6 +545,7 @@ impl DiagnosticPopover { div() .id("diagnostic") + .elevation_2(cx) .overflow_y_scroll() .px_2() .py_1() diff --git a/crates/fs/src/repository.rs b/crates/fs/src/repository.rs index ecb2a93577f20de437ea3762aa1d4a740848e294..620ea72accd1fb9ab94f0747f105e396dfcb26ff 100644 --- a/crates/fs/src/repository.rs +++ b/crates/fs/src/repository.rs @@ -72,7 +72,7 @@ impl GitRepository for LibGitRepository { // This check is required because index.get_path() unwraps internally :( check_path_to_repo_path_errors(relative_file_path)?; - let oid = match index.get_path(&relative_file_path, STAGE_NORMAL) { + let oid = match index.get_path(relative_file_path, STAGE_NORMAL) { Some(entry) => entry.id, None => return Ok(None), }; @@ -81,7 +81,7 @@ impl GitRepository for LibGitRepository { Ok(Some(String::from_utf8(content)?)) } - match logic(&self, relative_file_path) { + match logic(self, relative_file_path) { Ok(value) => return value, Err(err) => log::error!("Error loading head text: {:?}", err), } @@ -199,7 +199,7 @@ impl GitRepository for LibGitRepository { fn matches_index(repo: &LibGitRepository, path: &RepoPath, mtime: SystemTime) -> bool { if let Some(index) = repo.index().log_err() { - if let Some(entry) = index.get_path(&path, 0) { + if let Some(entry) = index.get_path(path, 0) { if let Some(mtime) = mtime.duration_since(SystemTime::UNIX_EPOCH).log_err() { if entry.mtime.seconds() == mtime.as_secs() as i32 && entry.mtime.nanoseconds() == mtime.subsec_nanos() diff --git a/crates/git/src/diff.rs b/crates/git/src/diff.rs index 39383cfc78b297e355c1d4c096219069cd92b1da..6e83bab22082e15681e2806b9cab5302ea4e14b5 100644 --- a/crates/git/src/diff.rs +++ b/crates/git/src/diff.rs @@ -165,7 +165,7 @@ impl BufferDiff { let mut tree = SumTree::new(); let buffer_text = buffer.as_rope().to_string(); - let patch = Self::diff(&diff_base, &buffer_text); + let patch = Self::diff(diff_base, &buffer_text); if let Some(patch) = patch { let mut divergence = 0; diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index ee7549287378f537de71b08abf59bc6af2ab4a98..70608ccb0c549e72b0757a72381e38681ebc4335 100644 --- a/crates/gpui/Cargo.toml +++ b/crates/gpui/Cargo.toml @@ -3,7 +3,7 @@ name = "gpui" version = "0.1.0" edition = "2021" authors = ["Nathan Sobo "] -description = "The next version of Zed's GPU-accelerated UI framework" +description = "Zed's GPU-accelerated UI framework" publish = false [features] diff --git a/crates/gpui/src/action.rs b/crates/gpui/src/action.rs index 9caa0da4823f64f5c30c32619a7b6950b305e676..c6ea705b570167d3c994cc821967bdee6df1f7f6 100644 --- a/crates/gpui/src/action.rs +++ b/crates/gpui/src/action.rs @@ -40,14 +40,25 @@ use std::any::{Any, TypeId}; /// register_action!(Paste); /// ``` pub trait Action: 'static { + /// Clone the action into a new box fn boxed_clone(&self) -> Box; + + /// Cast the action to the any type fn as_any(&self) -> &dyn Any; + + /// Do a partial equality check on this action and the other fn partial_eq(&self, action: &dyn Action) -> bool; + + /// Get the name of this action, for displaying in UI fn name(&self) -> &str; + /// Get the name of this action for debugging fn debug_name() -> &'static str where Self: Sized; + + /// Build this action from a JSON value. This is used to construct actions from the keymap. + /// A value of `{}` will be passed for actions that don't have any parameters. fn build(value: serde_json::Value) -> Result> where Self: Sized; @@ -62,6 +73,7 @@ impl std::fmt::Debug for dyn Action { } impl dyn Action { + /// Get the type id of this action pub fn type_id(&self) -> TypeId { self.as_any().type_id() } @@ -170,6 +182,7 @@ impl ActionRegistry { macro_rules! actions { ($namespace:path, [ $($name:ident),* $(,)? ]) => { $( + /// The `$name` action see [`gpui::actions!`] #[derive(::std::cmp::PartialEq, ::std::clone::Clone, ::std::default::Default, ::std::fmt::Debug, gpui::private::serde_derive::Deserialize)] #[serde(crate = "gpui::private::serde")] pub struct $name; diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index c7a6a92a17dce204412fa2c088132789d33b0cc4..7b349d92568e72b9b4929655e93dd1683636964e 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1,5 +1,3 @@ -#![deny(missing_docs)] - mod async_context; mod entity_map; mod model_context; @@ -864,7 +862,7 @@ impl AppContext { .unwrap() } - /// Set the value of the global of the given type. + /// Sets the value of the global of the given type. pub fn set_global(&mut self, global: G) { let global_type = TypeId::of::(); self.push_effect(Effect::NotifyGlobalObservers { global_type }); @@ -889,7 +887,7 @@ impl AppContext { .unwrap() } - /// Update the global of the given type with a closure. Unlike `global_mut`, this method provides + /// Updates the global of the given type with a closure. Unlike `global_mut`, this method provides /// your closure with mutable access to the `AppContext` and the global simultaneously. pub fn update_global(&mut self, f: impl FnOnce(&mut G, &mut Self) -> R) -> R { self.update(|cx| { @@ -1106,7 +1104,7 @@ impl AppContext { .contains_key(&action.as_any().type_id()) } - /// Set the menu bar for this application. This will replace any existing menu bar. + /// Sets the menu bar for this application. This will replace any existing menu bar. pub fn set_menus(&mut self, menus: Vec) { self.platform.set_menus(menus, &self.keymap.lock()); } @@ -1190,7 +1188,7 @@ impl Context for AppContext { }) } - /// Update the entity referenced by the given model. The function is passed a mutable reference to the + /// Updates the entity referenced by the given model. The function is passed a mutable reference to the /// entity along with a `ModelContext` for the entity. fn update_model( &mut self, diff --git a/crates/gpui/src/app/async_context.rs b/crates/gpui/src/app/async_context.rs index 1ee01d90dfac22632f718088bbd7bbe54364136c..7c36aebf57b0236d54f286789d03b60ec547cab5 100644 --- a/crates/gpui/src/app/async_context.rs +++ b/crates/gpui/src/app/async_context.rs @@ -213,7 +213,12 @@ impl AsyncWindowContext { } /// A convenience method for [WindowContext::update()] - pub fn update( + pub fn update(&mut self, update: impl FnOnce(&mut WindowContext) -> R) -> Result { + self.app.update_window(self.window, |_, cx| update(cx)) + } + + /// A convenience method for [WindowContext::update()] + pub fn update_root( &mut self, update: impl FnOnce(AnyView, &mut WindowContext) -> R, ) -> Result { diff --git a/crates/gpui/src/app/entity_map.rs b/crates/gpui/src/app/entity_map.rs index 7ab21a5477526d064d70283a04442b33cffcedfa..db5d844d17c173bbc0541d49aaf5f67e70f139a0 100644 --- a/crates/gpui/src/app/entity_map.rs +++ b/crates/gpui/src/app/entity_map.rs @@ -104,7 +104,7 @@ impl EntityMap { } } - /// Return an entity after moving it to the stack. + /// Returns an entity after moving it to the stack. pub fn end_lease(&mut self, mut lease: Lease) { self.entities .insert(lease.model.entity_id, lease.entity.take().unwrap()); @@ -391,7 +391,7 @@ impl Model { cx.read_model(self, f) } - /// Update the entity referenced by this model with the given function. + /// Updates the entity referenced by this model with the given function. /// /// The update function receives a context appropriate for its environment. /// When updating in an `AppContext`, it receives a `ModelContext`. @@ -571,7 +571,7 @@ impl WeakModel { Model::upgrade_from(self) } - /// Update the entity referenced by this model with the given function if + /// Updates the entity referenced by this model with the given function if /// the referenced entity still exists. Returns an error if the entity has /// been released. pub fn update( diff --git a/crates/gpui/src/app/model_context.rs b/crates/gpui/src/app/model_context.rs index e2aad9fee93aeba5c0c022a5d619f8cdd57b0e63..268410245e95aae8e1f1a091f9b98d0ce359dfdb 100644 --- a/crates/gpui/src/app/model_context.rs +++ b/crates/gpui/src/app/model_context.rs @@ -189,7 +189,7 @@ impl<'a, T: 'static> ModelContext<'a, T> { } } - /// Update the given global + /// Updates the given global pub fn update_global(&mut self, f: impl FnOnce(&mut G, &mut Self) -> R) -> R where G: 'static, diff --git a/crates/gpui/src/app/test_context.rs b/crates/gpui/src/app/test_context.rs index d11c1239dd576d9db944f605cad7d4140624feb9..4d588a668c1a06a688b15cc56514f3989b2ee78a 100644 --- a/crates/gpui/src/app/test_context.rs +++ b/crates/gpui/src/app/test_context.rs @@ -1,5 +1,3 @@ -#![deny(missing_docs)] - use crate::{ Action, AnyElement, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext, AvailableSpace, BackgroundExecutor, Bounds, ClipboardItem, Context, Entity, EventEmitter, @@ -352,7 +350,7 @@ impl TestAppContext { } /// Returns the `TestWindow` backing the given handle. - pub fn test_window(&self, window: AnyWindowHandle) -> TestWindow { + pub(crate) fn test_window(&self, window: AnyWindowHandle) -> TestWindow { self.app .borrow_mut() .windows @@ -578,7 +576,7 @@ impl<'a> VisualTestContext { self.cx.update_window(self.window, |_, cx| f(cx)).unwrap() } - /// Create a new VisualTestContext. You would typically shadow the passed in + /// Creates a new VisualTestContext. You would typically shadow the passed in /// TestAppContext with this, as this is typically more useful. /// `let cx = VisualTestContext::from_window(window, cx);` pub fn from_window(window: AnyWindowHandle, cx: &TestAppContext) -> Self { @@ -642,8 +640,11 @@ impl<'a> VisualTestContext { .as_ref() .expect("Can't draw to this window without a root view") .entity_id(); - cx.with_view_id(entity_id, |cx| { - f(cx).draw(origin, space, cx); + + cx.with_element_context(|cx| { + cx.with_view_id(entity_id, |cx| { + f(cx).draw(origin, space, cx); + }) }); cx.refresh(); diff --git a/crates/gpui/src/assets.rs b/crates/gpui/src/assets.rs index 39c8562b69703a959fcbd3ad75bc8a6601b0a839..b5e3735eb585914c8e88c47660f82c214059e041 100644 --- a/crates/gpui/src/assets.rs +++ b/crates/gpui/src/assets.rs @@ -8,8 +8,12 @@ use std::{ sync::atomic::{AtomicUsize, Ordering::SeqCst}, }; +/// A source of assets for this app to use. pub trait AssetSource: 'static + Send + Sync { + /// Load the given asset from the source path. fn load(&self, path: &str) -> Result>; + + /// List the assets at the given path. fn list(&self, path: &str) -> Result>; } @@ -26,15 +30,19 @@ impl AssetSource for () { } } +/// A unique identifier for the image cache #[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct ImageId(usize); +/// A cached and processed image. pub struct ImageData { + /// The ID associated with this image pub id: ImageId, data: ImageBuffer, Vec>, } impl ImageData { + /// Create a new image from the given data. pub fn new(data: ImageBuffer, Vec>) -> Self { static NEXT_ID: AtomicUsize = AtomicUsize::new(0); @@ -44,10 +52,12 @@ impl ImageData { } } + /// Convert this image into a byte slice. pub fn as_bytes(&self) -> &[u8] { &self.data } + /// Get the size of this image, in pixels pub fn size(&self) -> Size { let (width, height) = self.data.dimensions(); size(width.into(), height.into()) diff --git a/crates/gpui/src/color.rs b/crates/gpui/src/color.rs index 23fcc25f6aeed436309a3670393d4cbca10536e6..e76c31d6f15db5a7690aab3bafd0b950f79e2824 100644 --- a/crates/gpui/src/color.rs +++ b/crates/gpui/src/color.rs @@ -2,6 +2,7 @@ use anyhow::bail; use serde::de::{self, Deserialize, Deserializer, Visitor}; use std::fmt; +/// Convert an RGB hex color code number to a color type pub fn rgb>(hex: u32) -> C { let r = ((hex >> 16) & 0xFF) as f32 / 255.0; let g = ((hex >> 8) & 0xFF) as f32 / 255.0; @@ -9,6 +10,7 @@ pub fn rgb>(hex: u32) -> C { Rgba { r, g, b, a: 1.0 }.into() } +/// Convert an RGBA hex color code number to [`Rgba`] pub fn rgba(hex: u32) -> Rgba { let r = ((hex >> 24) & 0xFF) as f32 / 255.0; let g = ((hex >> 16) & 0xFF) as f32 / 255.0; @@ -17,11 +19,16 @@ pub fn rgba(hex: u32) -> Rgba { Rgba { r, g, b, a } } +/// An RGBA color #[derive(PartialEq, Clone, Copy, Default)] pub struct Rgba { + /// The red component of the color, in the range 0.0 to 1.0 pub r: f32, + /// The green component of the color, in the range 0.0 to 1.0 pub g: f32, + /// The blue component of the color, in the range 0.0 to 1.0 pub b: f32, + /// The alpha component of the color, in the range 0.0 to 1.0 pub a: f32, } @@ -32,6 +39,8 @@ impl fmt::Debug for Rgba { } impl Rgba { + /// Create a new [`Rgba`] color by blending this and another color together + /// TODO!(docs): find the source for this algorithm pub fn blend(&self, other: Rgba) -> Self { if other.a >= 1.0 { other @@ -165,12 +174,20 @@ impl TryFrom<&'_ str> for Rgba { } } +/// An HSLA color #[derive(Default, Copy, Clone, Debug)] #[repr(C)] pub struct Hsla { + /// Hue, in a range from 0 to 1 pub h: f32, + + /// Saturation, in a range from 0 to 1 pub s: f32, + + /// Lightness, in a range from 0 to 1 pub l: f32, + + /// Alpha, in a range from 0 to 1 pub a: f32, } @@ -203,38 +220,9 @@ impl Ord for Hsla { } } -impl Hsla { - pub fn to_rgb(self) -> Rgba { - self.into() - } - - pub fn red() -> Self { - red() - } - - pub fn green() -> Self { - green() - } - - pub fn blue() -> Self { - blue() - } - - pub fn black() -> Self { - black() - } - - pub fn white() -> Self { - white() - } - - pub fn transparent_black() -> Self { - transparent_black() - } -} - impl Eq for Hsla {} +/// Construct an [`Hsla`] object from plain values pub fn hsla(h: f32, s: f32, l: f32, a: f32) -> Hsla { Hsla { h: h.clamp(0., 1.), @@ -244,6 +232,7 @@ pub fn hsla(h: f32, s: f32, l: f32, a: f32) -> Hsla { } } +/// Pure black in [`Hsla`] pub fn black() -> Hsla { Hsla { h: 0., @@ -253,6 +242,7 @@ pub fn black() -> Hsla { } } +/// Transparent black in [`Hsla`] pub fn transparent_black() -> Hsla { Hsla { h: 0., @@ -262,6 +252,7 @@ pub fn transparent_black() -> Hsla { } } +/// Pure white in [`Hsla`] pub fn white() -> Hsla { Hsla { h: 0., @@ -271,6 +262,7 @@ pub fn white() -> Hsla { } } +/// The color red in [`Hsla`] pub fn red() -> Hsla { Hsla { h: 0., @@ -280,6 +272,7 @@ pub fn red() -> Hsla { } } +/// The color blue in [`Hsla`] pub fn blue() -> Hsla { Hsla { h: 0.6, @@ -289,6 +282,7 @@ pub fn blue() -> Hsla { } } +/// The color green in [`Hsla`] pub fn green() -> Hsla { Hsla { h: 0.33, @@ -298,6 +292,7 @@ pub fn green() -> Hsla { } } +/// The color yellow in [`Hsla`] pub fn yellow() -> Hsla { Hsla { h: 0.16, @@ -308,6 +303,41 @@ pub fn yellow() -> Hsla { } impl Hsla { + /// Converts this HSLA color to an RGBA color. + pub fn to_rgb(self) -> Rgba { + self.into() + } + + /// The color red + pub fn red() -> Self { + red() + } + + /// The color green + pub fn green() -> Self { + green() + } + + /// The color blue + pub fn blue() -> Self { + blue() + } + + /// The color black + pub fn black() -> Self { + black() + } + + /// The color white + pub fn white() -> Self { + white() + } + + /// The color transparent black + pub fn transparent_black() -> Self { + transparent_black() + } + /// Returns true if the HSLA color is fully transparent, false otherwise. pub fn is_transparent(&self) -> bool { self.a == 0.0 @@ -339,6 +369,7 @@ impl Hsla { } } + /// Returns a new HSLA color with the same hue, and lightness, but with no saturation. pub fn grayscale(&self) -> Self { Hsla { h: self.h, diff --git a/crates/gpui/src/element.rs b/crates/gpui/src/element.rs index 3022f9f30a5fa48d3a3d9b14b06011bdde2cc610..cd5e6ea9dcd2af407e1de2cfa2f6f25e3e6c392c 100644 --- a/crates/gpui/src/element.rs +++ b/crates/gpui/src/element.rs @@ -1,26 +1,69 @@ +//! Elements are the workhorses of GPUI. They are responsible for laying out and painting all of +//! the contents of a window. Elements form a tree and are laid out according to the web layout +//! standards as implemented by [taffy](https://github.com/DioxusLabs/taffy). Most of the time, +//! you won't need to interact with this module or these APIs directly. Elements provide their +//! own APIs and GPUI, or other element implementation, uses the APIs in this module to convert +//! that element tree into the pixels you see on the screen. +//! +//! # Element Basics +//! +//! Elements are constructed by calling [`Render::render()`] on the root view of the window, which +//! which recursively constructs the element tree from the current state of the application,. +//! These elements are then laid out by Taffy, and painted to the screen according to their own +//! implementation of [`Element::paint()`]. Before the start of the next frame, the entire element +//! tree and any callbacks they have registered with GPUI are dropped and the process repeats. +//! +//! But some state is too simple and voluminous to store in every view that needs it, e.g. +//! whether a hover has been started or not. For this, GPUI provides the [`Element::State`], associated type. +//! If an element returns an [`ElementId`] from [`IntoElement::element_id()`], and that element id +//! appears in the same place relative to other views and ElementIds in the frame, then the previous +//! frame's state will be passed to the element's layout and paint methods. +//! +//! # Implementing your own elements +//! +//! Elements are intended to be the low level, imperative API to GPUI. They are responsible for upholding, +//! or breaking, GPUI's features as they deem necessary. As an example, most GPUI elements are expected +//! to stay in the bounds that their parent element gives them. But with [`WindowContext::break_content_mask`], +//! you can ignore this restriction and paint anywhere inside of the window's bounds. This is useful for overlays +//! and popups and anything else that shows up 'on top' of other elements. +//! With great power, comes great responsibility. +//! +//! However, most of the time, you won't need to implement your own elements. GPUI provides a number of +//! elements that should cover most common use cases out of the box and it's recommended that you use those +//! to construct `components`, using the [`RenderOnce`] trait and the `#[derive(IntoElement)]` macro. Only implement +//! elements when you need to take manual control of the layout and painting process, such as when using +//! your own custom layout algorithm or rendering a code editor. + use crate::{ - ArenaBox, AvailableSpace, BorrowWindow, Bounds, ElementId, LayoutId, Pixels, Point, Size, - ViewContext, WindowContext, ELEMENT_ARENA, + util::FluentBuilder, ArenaBox, AvailableSpace, Bounds, ElementContext, ElementId, LayoutId, + Pixels, Point, Size, ViewContext, WindowContext, ELEMENT_ARENA, }; use derive_more::{Deref, DerefMut}; pub(crate) use smallvec::SmallVec; -use std::{any::Any, fmt::Debug}; +use std::{any::Any, fmt::Debug, ops::DerefMut}; /// Implemented by types that participate in laying out and painting the contents of a window. -/// Elements form a tree and are laid out according to web-based layout rules. -/// Rather than calling methods on implementers of this trait directly, you'll usually call `into_any` to convert them into an AnyElement, which manages state internally. -/// You can create custom elements by implementing this trait. +/// Elements form a tree and are laid out according to web-based layout rules, as implemented by Taffy. +/// You can create custom elements by implementing this trait, see the module-level documentation +/// for more details. pub trait Element: 'static + IntoElement { + /// The type of state to store for this element between frames. See the module-level documentation + /// for details. type State: 'static; + /// Before an element can be painted, we need to know where it's going to be and how big it is. + /// Use this method to request a layout from Taffy and initialize the element's state. fn request_layout( &mut self, state: Option, - cx: &mut WindowContext, + cx: &mut ElementContext, ) -> (LayoutId, Self::State); - fn paint(&mut self, bounds: Bounds, state: &mut Self::State, cx: &mut WindowContext); + /// Once layout has been completed, this method will be called to paint the element to the screen. + /// The state argument is the same state that was returned from [`Element::request_layout()`]. + fn paint(&mut self, bounds: Bounds, state: &mut Self::State, cx: &mut ElementContext); + /// Convert this element into a dynamically-typed [`AnyElement`]. fn into_any(self) -> AnyElement { AnyElement::new(self) } @@ -29,6 +72,7 @@ pub trait Element: 'static + IntoElement { /// Implemented by any type that can be converted into an element. pub trait IntoElement: Sized { /// The specific type of element into which the implementing type is converted. + /// Useful for converting other types into elements automatically, like Strings type Element: Element; /// The [`ElementId`] of self once converted into an [`Element`]. @@ -51,8 +95,8 @@ pub trait IntoElement: Sized { self, origin: Point, available_space: Size, - cx: &mut WindowContext, - f: impl FnOnce(&mut ::State, &mut WindowContext) -> R, + cx: &mut ElementContext, + f: impl FnOnce(&mut ::State, &mut ElementContext) -> R, ) -> R where T: Clone + Default + Debug + Into, @@ -77,41 +121,14 @@ pub trait IntoElement: Sized { }) } } - - /// Convert self to another type by calling the given closure. Useful in rendering code. - fn map(self, f: impl FnOnce(Self) -> U) -> U - where - Self: Sized, - U: IntoElement, - { - f(self) - } - - /// Conditionally chain onto self with the given closure. Useful in rendering code. - fn when(self, condition: bool, then: impl FnOnce(Self) -> Self) -> Self - where - Self: Sized, - { - self.map(|this| if condition { then(this) } else { this }) - } - - /// Conditionally chain onto self with the given closure if the given option is Some. - /// The contents of the option are provided to the closure. - fn when_some(self, option: Option, then: impl FnOnce(Self, T) -> Self) -> Self - where - Self: Sized, - { - self.map(|this| { - if let Some(value) = option { - then(this, value) - } else { - this - } - }) - } } +impl FluentBuilder for T {} + +/// An object that can be drawn to the screen. This is the trait that distinguishes `Views` from +/// models. Views are drawn to the screen and care about the current window's state, models are not and do not. pub trait Render: 'static + Sized { + /// Render this view into an element tree. fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement; } @@ -122,35 +139,49 @@ impl Render for () { } /// You can derive [`IntoElement`] on any type that implements this trait. -/// It is used to allow views to be expressed in terms of abstract data. +/// It is used to construct reusable `components` out of plain data. Think of +/// components as a recipe for a certain pattern of elements. RenderOnce allows +/// you to invoke this pattern, without breaking the fluent builder pattern of +/// the element APIs. pub trait RenderOnce: 'static { + /// Render this component into an element tree. Note that this method + /// takes ownership of self, as compared to [`Render::render()`] method + /// which takes a mutable reference. fn render(self, cx: &mut WindowContext) -> impl IntoElement; } +/// This is a helper trait to provide a uniform interface for constructing elements that +/// can accept any number of any kind of child elements pub trait ParentElement { - fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]>; + /// Extend this element's children with the given child elements. + fn extend(&mut self, elements: impl Iterator); + /// Add a single child element to this element. fn child(mut self, child: impl IntoElement) -> Self where Self: Sized, { - self.children_mut().push(child.into_element().into_any()); + self.extend(std::iter::once(child.into_element().into_any())); self } + /// Add multiple child elements to this element. fn children(mut self, children: impl IntoIterator) -> Self where Self: Sized, { - self.children_mut() - .extend(children.into_iter().map(|child| child.into_any_element())); + self.extend(children.into_iter().map(|child| child.into_any_element())); self } } +/// An element for rendering components. An implementation detail of the [`IntoElement`] derive macro +/// for [`RenderOnce`] +#[doc(hidden)] pub struct Component(Option); impl Component { + /// Create a new component from the given RenderOnce type. pub fn new(component: C) -> Self { Component(Some(component)) } @@ -162,14 +193,19 @@ impl Element for Component { fn request_layout( &mut self, _: Option, - cx: &mut WindowContext, + cx: &mut ElementContext, ) -> (LayoutId, Self::State) { - let mut element = self.0.take().unwrap().render(cx).into_any_element(); + let mut element = self + .0 + .take() + .unwrap() + .render(cx.deref_mut()) + .into_any_element(); let layout_id = element.request_layout(cx); (layout_id, element) } - fn paint(&mut self, _: Bounds, element: &mut Self::State, cx: &mut WindowContext) { + fn paint(&mut self, _: Bounds, element: &mut Self::State, cx: &mut ElementContext) { element.paint(cx) } } @@ -186,31 +222,33 @@ impl IntoElement for Component { } } +/// A globally unique identifier for an element, used to track state across frames. #[derive(Deref, DerefMut, Default, Clone, Debug, Eq, PartialEq, Hash)] -pub struct GlobalElementId(SmallVec<[ElementId; 32]>); +pub(crate) struct GlobalElementId(SmallVec<[ElementId; 32]>); trait ElementObject { fn element_id(&self) -> Option; - fn request_layout(&mut self, cx: &mut WindowContext) -> LayoutId; + fn request_layout(&mut self, cx: &mut ElementContext) -> LayoutId; - fn paint(&mut self, cx: &mut WindowContext); + fn paint(&mut self, cx: &mut ElementContext); fn measure( &mut self, available_space: Size, - cx: &mut WindowContext, + cx: &mut ElementContext, ) -> Size; fn draw( &mut self, origin: Point, available_space: Size, - cx: &mut WindowContext, + cx: &mut ElementContext, ); } -pub struct DrawableElement { +/// A wrapper around an implementer of [`Element`] that allows it to be drawn in a window. +pub(crate) struct DrawableElement { element: Option, phase: ElementDrawPhase, } @@ -243,7 +281,7 @@ impl DrawableElement { self.element.as_ref()?.element_id() } - fn request_layout(&mut self, cx: &mut WindowContext) -> LayoutId { + fn request_layout(&mut self, cx: &mut ElementContext) -> LayoutId { let (layout_id, frame_state) = if let Some(id) = self.element.as_ref().unwrap().element_id() { let layout_id = cx.with_element_state(id, |element_state, cx| { @@ -265,7 +303,7 @@ impl DrawableElement { layout_id } - fn paint(mut self, cx: &mut WindowContext) -> Option { + fn paint(mut self, cx: &mut ElementContext) -> Option { match self.phase { ElementDrawPhase::LayoutRequested { layout_id, @@ -310,7 +348,7 @@ impl DrawableElement { fn measure( &mut self, available_space: Size, - cx: &mut WindowContext, + cx: &mut ElementContext, ) -> Size { if matches!(&self.phase, ElementDrawPhase::Start) { self.request_layout(cx); @@ -351,7 +389,7 @@ impl DrawableElement { mut self, origin: Point, available_space: Size, - cx: &mut WindowContext, + cx: &mut ElementContext, ) -> Option { self.measure(available_space, cx); cx.with_absolute_element_offset(origin, |cx| self.paint(cx)) @@ -367,18 +405,18 @@ where self.as_ref().unwrap().element_id() } - fn request_layout(&mut self, cx: &mut WindowContext) -> LayoutId { + fn request_layout(&mut self, cx: &mut ElementContext) -> LayoutId { DrawableElement::request_layout(self.as_mut().unwrap(), cx) } - fn paint(&mut self, cx: &mut WindowContext) { + fn paint(&mut self, cx: &mut ElementContext) { DrawableElement::paint(self.take().unwrap(), cx); } fn measure( &mut self, available_space: Size, - cx: &mut WindowContext, + cx: &mut ElementContext, ) -> Size { DrawableElement::measure(self.as_mut().unwrap(), available_space, cx) } @@ -387,16 +425,17 @@ where &mut self, origin: Point, available_space: Size, - cx: &mut WindowContext, + cx: &mut ElementContext, ) { DrawableElement::draw(self.take().unwrap(), origin, available_space, cx); } } +/// A dynamically typed element that can be used to store any element type. pub struct AnyElement(ArenaBox); impl AnyElement { - pub fn new(element: E) -> Self + pub(crate) fn new(element: E) -> Self where E: 'static + Element, E::State: Any, @@ -407,11 +446,14 @@ impl AnyElement { AnyElement(element) } - pub fn request_layout(&mut self, cx: &mut WindowContext) -> LayoutId { + /// Request the layout ID of the element stored in this `AnyElement`. + /// Used for laying out child elements in a parent element. + pub fn request_layout(&mut self, cx: &mut ElementContext) -> LayoutId { self.0.request_layout(cx) } - pub fn paint(&mut self, cx: &mut WindowContext) { + /// Paints the element stored in this `AnyElement`. + pub fn paint(&mut self, cx: &mut ElementContext) { self.0.paint(cx) } @@ -419,7 +461,7 @@ impl AnyElement { pub fn measure( &mut self, available_space: Size, - cx: &mut WindowContext, + cx: &mut ElementContext, ) -> Size { self.0.measure(available_space, cx) } @@ -429,11 +471,12 @@ impl AnyElement { &mut self, origin: Point, available_space: Size, - cx: &mut WindowContext, + cx: &mut ElementContext, ) { self.0.draw(origin, available_space, cx) } + /// Returns the element ID of the element stored in this `AnyElement`, if any. pub fn inner_id(&self) -> Option { self.0.element_id() } @@ -445,13 +488,13 @@ impl Element for AnyElement { fn request_layout( &mut self, _: Option, - cx: &mut WindowContext, + cx: &mut ElementContext, ) -> (LayoutId, Self::State) { let layout_id = self.request_layout(cx); (layout_id, ()) } - fn paint(&mut self, _: Bounds, _: &mut Self::State, cx: &mut WindowContext) { + fn paint(&mut self, _: Bounds, _: &mut Self::State, cx: &mut ElementContext) { self.paint(cx) } } @@ -493,7 +536,7 @@ impl Element for () { fn request_layout( &mut self, _state: Option, - cx: &mut WindowContext, + cx: &mut ElementContext, ) -> (LayoutId, Self::State) { (cx.request_layout(&crate::Style::default(), None), ()) } @@ -502,7 +545,7 @@ impl Element for () { &mut self, _bounds: Bounds, _state: &mut Self::State, - _cx: &mut WindowContext, + _cx: &mut ElementContext, ) { } } diff --git a/crates/gpui/src/elements/canvas.rs b/crates/gpui/src/elements/canvas.rs index 767417ae40100a7ef3ff8ea6fecea0a4f9eca53f..8011f51e0c9706dd2ae7a95163f5a076944d6c54 100644 --- a/crates/gpui/src/elements/canvas.rs +++ b/crates/gpui/src/elements/canvas.rs @@ -1,16 +1,20 @@ use refineable::Refineable as _; -use crate::{Bounds, Element, IntoElement, Pixels, Style, StyleRefinement, Styled, WindowContext}; +use crate::{Bounds, Element, ElementContext, IntoElement, Pixels, Style, StyleRefinement, Styled}; -pub fn canvas(callback: impl 'static + FnOnce(&Bounds, &mut WindowContext)) -> Canvas { +/// Construct a canvas element with the given paint callback. +/// Useful for adding short term custom drawing to a view. +pub fn canvas(callback: impl 'static + FnOnce(&Bounds, &mut ElementContext)) -> Canvas { Canvas { paint_callback: Some(Box::new(callback)), style: StyleRefinement::default(), } } +/// A canvas element, meant for accessing the low level paint API without defining a whole +/// custom element pub struct Canvas { - paint_callback: Option, &mut WindowContext)>>, + paint_callback: Option, &mut ElementContext)>>, style: StyleRefinement, } @@ -32,7 +36,7 @@ impl Element for Canvas { fn request_layout( &mut self, _: Option, - cx: &mut WindowContext, + cx: &mut ElementContext, ) -> (crate::LayoutId, Self::State) { let mut style = Style::default(); style.refine(&self.style); @@ -40,7 +44,7 @@ impl Element for Canvas { (layout_id, style) } - fn paint(&mut self, bounds: Bounds, style: &mut Style, cx: &mut WindowContext) { + fn paint(&mut self, bounds: Bounds, style: &mut Style, cx: &mut ElementContext) { style.paint(bounds, cx, |cx| { (self.paint_callback.take().unwrap())(&bounds, cx) }); diff --git a/crates/gpui/src/elements/div.rs b/crates/gpui/src/elements/div.rs index aa912eadbe9c986969dd197927af8963a3c58d0c..b1d936546b91f0387bfacf907d309a78d79d4a44 100644 --- a/crates/gpui/src/elements/div.rs +++ b/crates/gpui/src/elements/div.rs @@ -1,6 +1,30 @@ +//! Div is the central, reusable element that most GPUI trees will be built from. +//! It functions as a container for other elements, and provides a number of +//! useful features for laying out and styling its children as well as binding +//! mouse events and action handlers. It is meant to be similar to the HTML
+//! element, but for GPUI. +//! +//! # Build your own div +//! +//! GPUI does not directly provide APIs for stateful, multi step events like `click` +//! and `drag`. We want GPUI users to be able to build their own abstractions for +//! their own needs. However, as a UI framework, we're also obliged to provide some +//! building blocks to make the process of building your own elements easier. +//! For this we have the [`Interactivity`] and the [`StyleRefinement`] structs, as well +//! as several associated traits. Together, these provide the full suite of Dom-like events +//! and Tailwind-like styling that you can use to build your own custom elements. Div is +//! constructed by combining these two systems into an all-in-one element. +//! +//! # Capturing and bubbling +//! +//! Note that while event dispatch in GPUI uses similar names and concepts to the web +//! even API, the details are very different. See the documentation in [TODO!(docs) +//! DOCUMENT EVENT DISPATCH SOMEWHERE IN WINDOW CONTEXT] for more details +//! + use crate::{ - point, px, Action, AnyDrag, AnyElement, AnyTooltip, AnyView, AppContext, BorrowAppContext, - BorrowWindow, Bounds, ClickEvent, DispatchPhase, Element, ElementId, FocusHandle, IntoElement, + point, px, size, Action, AnyDrag, AnyElement, AnyTooltip, AnyView, AppContext, Bounds, + ClickEvent, DispatchPhase, Element, ElementContext, ElementId, FocusHandle, IntoElement, IsZero, KeyContext, KeyDownEvent, KeyUpEvent, LayoutId, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, Point, Render, ScrollWheelEvent, SharedString, Size, StackingOrder, Style, StyleRefinement, Styled, Task, View, Visibility, @@ -17,6 +41,7 @@ use std::{ fmt::Debug, marker::PhantomData, mem, + ops::DerefMut, rc::Rc, time::Duration, }; @@ -24,20 +49,30 @@ use taffy::style::Overflow; use util::ResultExt; const DRAG_THRESHOLD: f64 = 2.; -const TOOLTIP_DELAY: Duration = Duration::from_millis(500); +pub(crate) const TOOLTIP_DELAY: Duration = Duration::from_millis(500); +/// The styling information for a given group. pub struct GroupStyle { + /// The identifier for this group. pub group: SharedString, + + /// The specific style refinement that this group would apply + /// to its children. pub style: Box, } +/// An event for when a drag is moving over this element, with the given state type. pub struct DragMoveEvent { + /// The mouse move event that triggered this drag move event. pub event: MouseMoveEvent, + + /// The bounds of this element. pub bounds: Bounds, drag: PhantomData, } impl DragMoveEvent { + /// Returns the drag state for this event. pub fn drag<'b>(&self, cx: &'b AppContext) -> &'b T { cx.active_drag .as_ref() @@ -47,6 +82,10 @@ impl DragMoveEvent { } impl Interactivity { + /// Bind the given callback to the mouse down event for the given mouse button, during the bubble phase + /// The imperative API equivalent of [`InteractiveElement::on_mouse_down`] + /// + /// See [`ViewContext::listener()`] to get access to the view state from this callback pub fn on_mouse_down( &mut self, button: MouseButton, @@ -63,6 +102,10 @@ impl Interactivity { })); } + /// Bind the given callback to the mouse down event for any button, during the capture phase + /// The imperative API equivalent of [`InteractiveElement::capture_any_mouse_down`] + /// + /// See [`ViewContext::listener()`] to get access to a view's state from this callback pub fn capture_any_mouse_down( &mut self, listener: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static, @@ -75,6 +118,10 @@ impl Interactivity { })); } + /// Bind the given callback to the mouse down event for any button, during the bubble phase + /// the imperative API equivalent to [`InteractiveElement::on_any_mouse_down()`] + /// + /// See [`ViewContext::listener()`] to get access to a view's state from this callback pub fn on_any_mouse_down( &mut self, listener: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static, @@ -87,6 +134,10 @@ impl Interactivity { })); } + /// Bind the given callback to the mouse up event for the given button, during the bubble phase + /// the imperative API equivalent to [`InteractiveElement::on_mouse_up()`] + /// + /// See [`ViewContext::listener()`] to get access to a view's state from this callback pub fn on_mouse_up( &mut self, button: MouseButton, @@ -103,6 +154,10 @@ impl Interactivity { })); } + /// Bind the given callback to the mouse up event for any button, during the capture phase + /// the imperative API equivalent to [`InteractiveElement::capture_any_mouse_up()`] + /// + /// See [`ViewContext::listener()`] to get access to a view's state from this callback pub fn capture_any_mouse_up( &mut self, listener: impl Fn(&MouseUpEvent, &mut WindowContext) + 'static, @@ -115,6 +170,10 @@ impl Interactivity { })); } + /// Bind the given callback to the mouse up event for any button, during the bubble phase + /// the imperative API equivalent to [`InteractiveElement::on_any_mouse_up()`] + /// + /// See [`ViewContext::listener()`] to get access to a view's state from this callback pub fn on_any_mouse_up( &mut self, listener: impl Fn(&MouseUpEvent, &mut WindowContext) + 'static, @@ -127,6 +186,11 @@ impl Interactivity { })); } + /// Bind the given callback to the mouse down event, on any button, during the capture phase, + /// when the mouse is outside of the bounds of this element. + /// The imperative API equivalent to [`InteractiveElement::on_mouse_down_out()`] + /// + /// See [`ViewContext::listener()`] to get access to a view's state from this callback pub fn on_mouse_down_out( &mut self, listener: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static, @@ -140,6 +204,11 @@ impl Interactivity { })); } + /// Bind the given callback to the mouse up event, for the given button, during the capture phase, + /// when the mouse is outside of the bounds of this element. + /// The imperative API equivalent to [`InteractiveElement::on_mouse_up_out()`] + /// + /// See [`ViewContext::listener()`] to get access to a view's state from this callback pub fn on_mouse_up_out( &mut self, button: MouseButton, @@ -156,6 +225,10 @@ impl Interactivity { })); } + /// Bind the given callback to the mouse move event, during the bubble phase + /// The imperative API equivalent to [`InteractiveElement::on_mouse_move()`] + /// + /// See [`ViewContext::listener()`] to get access to a view's state from this callback pub fn on_mouse_move( &mut self, listener: impl Fn(&MouseMoveEvent, &mut WindowContext) + 'static, @@ -168,6 +241,13 @@ impl Interactivity { })); } + /// Bind the given callback to the mouse drag event of the given type. Note that this + /// will be called for all move events, inside or outside of this element, as long as the + /// drag was started with this element under the mouse. Useful for implementing draggable + /// UIs that don't conform to a drag and drop style interaction, like resizing. + /// The imperative API equivalent to [`InteractiveElement::on_drag_move()`] + /// + /// See [`ViewContext::listener()`] to get access to a view's state from this callback pub fn on_drag_move( &mut self, listener: impl Fn(&DragMoveEvent, &mut WindowContext) + 'static, @@ -194,6 +274,10 @@ impl Interactivity { })); } + /// Bind the given callback to scroll wheel events during the bubble phase + /// The imperative API equivalent to [`InteractiveElement::on_scroll_wheel()`] + /// + /// See [`ViewContext::listener()`] to get access to a view's state from this callback pub fn on_scroll_wheel( &mut self, listener: impl Fn(&ScrollWheelEvent, &mut WindowContext) + 'static, @@ -206,6 +290,10 @@ impl Interactivity { })); } + /// Bind the given callback to an action dispatch during the capture phase + /// The imperative API equivalent to [`InteractiveElement::capture_action()`] + /// + /// See [`ViewContext::listener()`] to get access to a view's state from this callback pub fn capture_action( &mut self, listener: impl Fn(&A, &mut WindowContext) + 'static, @@ -221,6 +309,10 @@ impl Interactivity { )); } + /// Bind the given callback to an action dispatch during the bubble phase + /// The imperative API equivalent to [`InteractiveElement::on_action()`] + /// + /// See [`ViewContext::listener()`] to get access to a view's state from this callback pub fn on_action(&mut self, listener: impl Fn(&A, &mut WindowContext) + 'static) { self.action_listeners.push(( TypeId::of::(), @@ -233,6 +325,12 @@ impl Interactivity { )); } + /// Bind the given callback to an action dispatch, based on a dynamic action parameter + /// instead of a type parameter. Useful for component libraries that want to expose + /// action bindings to their users. + /// The imperative API equivalent to [`InteractiveElement::on_boxed_action()`] + /// + /// See [`ViewContext::listener()`] to get access to a view's state from this callback pub fn on_boxed_action( &mut self, action: &dyn Action, @@ -249,6 +347,10 @@ impl Interactivity { )); } + /// Bind the given callback to key down events during the bubble phase + /// The imperative API equivalent to [`InteractiveElement::on_key_down()`] + /// + /// See [`ViewContext::listener()`] to get access to a view's state from this callback pub fn on_key_down(&mut self, listener: impl Fn(&KeyDownEvent, &mut WindowContext) + 'static) { self.key_down_listeners .push(Box::new(move |event, phase, cx| { @@ -258,6 +360,10 @@ impl Interactivity { })); } + /// Bind the given callback to key down events during the capture phase + /// The imperative API equivalent to [`InteractiveElement::capture_key_down()`] + /// + /// See [`ViewContext::listener()`] to get access to a view's state from this callback pub fn capture_key_down( &mut self, listener: impl Fn(&KeyDownEvent, &mut WindowContext) + 'static, @@ -270,6 +376,10 @@ impl Interactivity { })); } + /// Bind the given callback to key up events during the bubble phase + /// The imperative API equivalent to [`InteractiveElement::on_key_up()`] + /// + /// See [`ViewContext::listener()`] to get access to a view's state from this callback pub fn on_key_up(&mut self, listener: impl Fn(&KeyUpEvent, &mut WindowContext) + 'static) { self.key_up_listeners .push(Box::new(move |event, phase, cx| { @@ -279,6 +389,10 @@ impl Interactivity { })); } + /// Bind the given callback to key up events during the capture phase + /// The imperative API equivalent to [`InteractiveElement::on_key_up()`] + /// + /// See [`ViewContext::listener()`] to get access to a view's state from this callback pub fn capture_key_up(&mut self, listener: impl Fn(&KeyUpEvent, &mut WindowContext) + 'static) { self.key_up_listeners .push(Box::new(move |event, phase, cx| { @@ -288,6 +402,10 @@ impl Interactivity { })); } + /// Bind the given callback to drop events of the given type, whether or not the drag started on this element + /// The imperative API equivalent to [`InteractiveElement::on_drop()`] + /// + /// See [`ViewContext::listener()`] to get access to a view's state from this callback pub fn on_drop(&mut self, listener: impl Fn(&T, &mut WindowContext) + 'static) { self.drop_listeners.push(( TypeId::of::(), @@ -297,10 +415,16 @@ impl Interactivity { )); } + /// Use the given predicate to determine whether or not a drop event should be dispatched to this element + /// The imperative API equivalent to [`InteractiveElement::can_drop()`] pub fn can_drop(&mut self, predicate: impl Fn(&dyn Any, &mut WindowContext) -> bool + 'static) { self.can_drop_predicate = Some(Box::new(predicate)); } + /// Bind the given callback to click events of this element + /// The imperative API equivalent to [`InteractiveElement::on_click()`] + /// + /// See [`ViewContext::listener()`] to get access to a view's state from this callback pub fn on_click(&mut self, listener: impl Fn(&ClickEvent, &mut WindowContext) + 'static) where Self: Sized, @@ -309,6 +433,12 @@ impl Interactivity { .push(Box::new(move |event, cx| listener(event, cx))); } + /// On drag initiation, this callback will be used to create a new view to render the dragged value for a + /// drag and drop operation. This API should also be used as the equivalent of 'on drag start' with + /// the [`Self::on_drag_move()`] API + /// The imperative API equivalent to [`InteractiveElement::on_drag()`] + /// + /// See [`ViewContext::listener()`] to get access to a view's state from this callback pub fn on_drag( &mut self, value: T, @@ -328,6 +458,11 @@ impl Interactivity { )); } + /// Bind the given callback on the hover start and end events of this element. Note that the boolean + /// passed to the callback is true when the hover starts and false when it ends. + /// The imperative API equivalent to [`InteractiveElement::on_drag()`] + /// + /// See [`ViewContext::listener()`] to get access to a view's state from this callback pub fn on_hover(&mut self, listener: impl Fn(&bool, &mut WindowContext) + 'static) where Self: Sized, @@ -339,6 +474,8 @@ impl Interactivity { self.hover_listener = Some(Box::new(listener)); } + /// Use the given callback to construct a new tooltip view when the mouse hovers over this element. + /// The imperative API equivalent to [`InteractiveElement::tooltip()`] pub fn tooltip(&mut self, build_tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) where Self: Sized, @@ -350,31 +487,43 @@ impl Interactivity { self.tooltip_builder = Some(Rc::new(build_tooltip)); } + /// Block the mouse from interacting with this element or any of it's children + /// The imperative API equivalent to [`InteractiveElement::block_mouse()`] pub fn block_mouse(&mut self) { self.block_mouse = true; } } +/// A trait for elements that want to use the standard GPUI event handlers that don't +/// require any state. pub trait InteractiveElement: Sized { + /// Retrieve the interactivity state associated with this element fn interactivity(&mut self) -> &mut Interactivity; + /// Assign this element to a group of elements that can be styled together fn group(mut self, group: impl Into) -> Self { self.interactivity().group = Some(group.into()); self } + /// Assign this elements fn id(mut self, id: impl Into) -> Stateful { self.interactivity().element_id = Some(id.into()); Stateful { element: self } } + /// Track the focus state of the given focus handle on this element. + /// If the focus handle is focused by the application, this element will + /// apply it's focused styles. fn track_focus(mut self, focus_handle: &FocusHandle) -> Focusable { self.interactivity().focusable = true; self.interactivity().tracked_focus_handle = Some(focus_handle.clone()); Focusable { element: self } } + /// Set the keymap context for this element. This will be used to determine + /// which action to dispatch from the keymap. fn key_context(mut self, key_context: C) -> Self where C: TryInto, @@ -386,6 +535,7 @@ pub trait InteractiveElement: Sized { self } + /// Apply the given style to this element when the mouse hovers over it fn hover(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self { debug_assert!( self.interactivity().hover_style.is_none(), @@ -395,6 +545,7 @@ pub trait InteractiveElement: Sized { self } + /// Apply the given style to this element when the mouse hovers over a group member fn group_hover( mut self, group_name: impl Into, @@ -407,6 +558,10 @@ pub trait InteractiveElement: Sized { self } + /// Bind the given callback to the mouse down event for the given mouse button, + /// the fluent API equivalent to [`Interactivity::on_mouse_down()`] + /// + /// See [`ViewContext::listener()`] to get access to the view state from this callback fn on_mouse_down( mut self, button: MouseButton, @@ -417,17 +572,27 @@ pub trait InteractiveElement: Sized { } #[cfg(any(test, feature = "test-support"))] + /// Set a key that can be used to look up this element's bounds + /// in the [`VisualTestContext::debug_bounds()`] map + /// This is a noop in release builds fn debug_selector(mut self, f: impl FnOnce() -> String) -> Self { self.interactivity().debug_selector = Some(f()); self } #[cfg(not(any(test, feature = "test-support")))] + /// Set a key that can be used to look up this element's bounds + /// in the [`VisualTestContext::debug_bounds()`] map + /// This is a noop in release builds #[inline] fn debug_selector(self, _: impl FnOnce() -> String) -> Self { self } + /// Bind the given callback to the mouse down event for any button, during the capture phase + /// the fluent API equivalent to [`Interactivity::capture_any_mouse_down()`] + /// + /// See [`ViewContext::listener()`] to get access to a view's state from this callback fn capture_any_mouse_down( mut self, listener: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static, @@ -436,6 +601,10 @@ pub trait InteractiveElement: Sized { self } + /// Bind the given callback to the mouse down event for any button, during the capture phase + /// the fluent API equivalent to [`Interactivity::on_any_mouse_down()`] + /// + /// See [`ViewContext::listener()`] to get access to a view's state from this callback fn on_any_mouse_down( mut self, listener: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static, @@ -444,6 +613,10 @@ pub trait InteractiveElement: Sized { self } + /// Bind the given callback to the mouse up event for the given button, during the bubble phase + /// the fluent API equivalent to [`Interactivity::on_mouse_up()`] + /// + /// See [`ViewContext::listener()`] to get access to a view's state from this callback fn on_mouse_up( mut self, button: MouseButton, @@ -453,6 +626,10 @@ pub trait InteractiveElement: Sized { self } + /// Bind the given callback to the mouse up event for any button, during the capture phase + /// the fluent API equivalent to [`Interactivity::capture_any_mouse_up()`] + /// + /// See [`ViewContext::listener()`] to get access to a view's state from this callback fn capture_any_mouse_up( mut self, listener: impl Fn(&MouseUpEvent, &mut WindowContext) + 'static, @@ -461,6 +638,11 @@ pub trait InteractiveElement: Sized { self } + /// Bind the given callback to the mouse down event, on any button, during the capture phase, + /// when the mouse is outside of the bounds of this element. + /// The fluent API equivalent to [`Interactivity::on_mouse_down_out()`] + /// + /// See [`ViewContext::listener()`] to get access to a view's state from this callback fn on_mouse_down_out( mut self, listener: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static, @@ -469,6 +651,11 @@ pub trait InteractiveElement: Sized { self } + /// Bind the given callback to the mouse up event, for the given button, during the capture phase, + /// when the mouse is outside of the bounds of this element. + /// The fluent API equivalent to [`Interactivity::on_mouse_up_out()`] + /// + /// See [`ViewContext::listener()`] to get access to a view's state from this callback fn on_mouse_up_out( mut self, button: MouseButton, @@ -478,6 +665,10 @@ pub trait InteractiveElement: Sized { self } + /// Bind the given callback to the mouse move event, during the bubble phase + /// The fluent API equivalent to [`Interactivity::on_mouse_move()`] + /// + /// See [`ViewContext::listener()`] to get access to a view's state from this callback fn on_mouse_move( mut self, listener: impl Fn(&MouseMoveEvent, &mut WindowContext) + 'static, @@ -486,6 +677,13 @@ pub trait InteractiveElement: Sized { self } + /// Bind the given callback to the mouse drag event of the given type. Note that this + /// will be called for all move events, inside or outside of this element, as long as the + /// drag was started with this element under the mouse. Useful for implementing draggable + /// UIs that don't conform to a drag and drop style interaction, like resizing. + /// The fluent API equivalent to [`Interactivity::on_drag_move()`] + /// + /// See [`ViewContext::listener()`] to get access to a view's state from this callback fn on_drag_move( mut self, listener: impl Fn(&DragMoveEvent, &mut WindowContext) + 'static, @@ -497,6 +695,10 @@ pub trait InteractiveElement: Sized { self } + /// Bind the given callback to scroll wheel events during the bubble phase + /// The fluent API equivalent to [`Interactivity::on_scroll_wheel()`] + /// + /// See [`ViewContext::listener()`] to get access to a view's state from this callback fn on_scroll_wheel( mut self, listener: impl Fn(&ScrollWheelEvent, &mut WindowContext) + 'static, @@ -506,6 +708,9 @@ pub trait InteractiveElement: Sized { } /// Capture the given action, before normal action dispatch can fire + /// The fluent API equivalent to [`Interactivity::on_scroll_wheel()`] + /// + /// See [`ViewContext::listener()`] to get access to a view's state from this callback fn capture_action( mut self, listener: impl Fn(&A, &mut WindowContext) + 'static, @@ -514,12 +719,21 @@ pub trait InteractiveElement: Sized { self } - /// Add a listener for the given action, fires during the bubble event phase + /// Bind the given callback to an action dispatch during the bubble phase + /// The fluent API equivalent to [`Interactivity::on_action()`] + /// + /// See [`ViewContext::listener()`] to get access to a view's state from this callback fn on_action(mut self, listener: impl Fn(&A, &mut WindowContext) + 'static) -> Self { self.interactivity().on_action(listener); self } + /// Bind the given callback to an action dispatch, based on a dynamic action parameter + /// instead of a type parameter. Useful for component libraries that want to expose + /// action bindings to their users. + /// The fluent API equivalent to [`Interactivity::on_boxed_action()`] + /// + /// See [`ViewContext::listener()`] to get access to a view's state from this callback fn on_boxed_action( mut self, action: &dyn Action, @@ -529,6 +743,10 @@ pub trait InteractiveElement: Sized { self } + /// Bind the given callback to key down events during the bubble phase + /// The fluent API equivalent to [`Interactivity::on_key_down()`] + /// + /// See [`ViewContext::listener()`] to get access to a view's state from this callback fn on_key_down( mut self, listener: impl Fn(&KeyDownEvent, &mut WindowContext) + 'static, @@ -537,6 +755,10 @@ pub trait InteractiveElement: Sized { self } + /// Bind the given callback to key down events during the capture phase + /// The fluent API equivalent to [`Interactivity::capture_key_down()`] + /// + /// See [`ViewContext::listener()`] to get access to a view's state from this callback fn capture_key_down( mut self, listener: impl Fn(&KeyDownEvent, &mut WindowContext) + 'static, @@ -545,11 +767,19 @@ pub trait InteractiveElement: Sized { self } + /// Bind the given callback to key up events during the bubble phase + /// The fluent API equivalent to [`Interactivity::on_key_up()`] + /// + /// See [`ViewContext::listener()`] to get access to a view's state from this callback fn on_key_up(mut self, listener: impl Fn(&KeyUpEvent, &mut WindowContext) + 'static) -> Self { self.interactivity().on_key_up(listener); self } + /// Bind the given callback to key up events during the capture phase + /// The fluent API equivalent to [`Interactivity::capture_key_up()`] + /// + /// See [`ViewContext::listener()`] to get access to a view's state from this callback fn capture_key_up( mut self, listener: impl Fn(&KeyUpEvent, &mut WindowContext) + 'static, @@ -558,6 +788,7 @@ pub trait InteractiveElement: Sized { self } + /// Apply the given style when the given data type is dragged over this element fn drag_over(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self { self.interactivity() .drag_over_styles @@ -565,6 +796,7 @@ pub trait InteractiveElement: Sized { self } + /// Apply the given style when the given data type is dragged over this element's group fn group_drag_over( mut self, group_name: impl Into, @@ -580,11 +812,17 @@ pub trait InteractiveElement: Sized { self } + /// Bind the given callback to drop events of the given type, whether or not the drag started on this element + /// The fluent API equivalent to [`Interactivity::on_drop()`] + /// + /// See [`ViewContext::listener()`] to get access to a view's state from this callback fn on_drop(mut self, listener: impl Fn(&T, &mut WindowContext) + 'static) -> Self { self.interactivity().on_drop(listener); self } + /// Use the given predicate to determine whether or not a drop event should be dispatched to this element + /// The fluent API equivalent to [`Interactivity::can_drop()`] fn can_drop( mut self, predicate: impl Fn(&dyn Any, &mut WindowContext) -> bool + 'static, @@ -593,39 +831,49 @@ pub trait InteractiveElement: Sized { self } + /// Block the mouse from interacting with this element or any of it's children + /// The fluent API equivalent to [`Interactivity::block_mouse()`] fn block_mouse(mut self) -> Self { self.interactivity().block_mouse(); self } } +/// A trait for elements that want to use the standard GPUI interactivity features +/// that require state. pub trait StatefulInteractiveElement: InteractiveElement { + /// Set this element to focusable. fn focusable(mut self) -> Focusable { self.interactivity().focusable = true; Focusable { element: self } } + /// Set the overflow x and y to scroll. fn overflow_scroll(mut self) -> Self { self.interactivity().base_style.overflow.x = Some(Overflow::Scroll); self.interactivity().base_style.overflow.y = Some(Overflow::Scroll); self } + /// Set the overflow x to scroll. fn overflow_x_scroll(mut self) -> Self { self.interactivity().base_style.overflow.x = Some(Overflow::Scroll); self } + /// Set the overflow y to scroll. fn overflow_y_scroll(mut self) -> Self { self.interactivity().base_style.overflow.y = Some(Overflow::Scroll); self } + /// Track the scroll state of this element with the given handle. fn track_scroll(mut self, scroll_handle: &ScrollHandle) -> Self { self.interactivity().scroll_handle = Some(scroll_handle.clone()); self } + /// Set the given styles to be applied when this element is active. fn active(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self where Self: Sized, @@ -634,6 +882,7 @@ pub trait StatefulInteractiveElement: InteractiveElement { self } + /// Set the given styles to be applied when this element's group is active. fn group_active( mut self, group_name: impl Into, @@ -649,6 +898,10 @@ pub trait StatefulInteractiveElement: InteractiveElement { self } + /// Bind the given callback to click events of this element + /// The fluent API equivalent to [`Interactivity::on_click()`] + /// + /// See [`ViewContext::listener()`] to get access to a view's state from this callback fn on_click(mut self, listener: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self where Self: Sized, @@ -657,6 +910,12 @@ pub trait StatefulInteractiveElement: InteractiveElement { self } + /// On drag initiation, this callback will be used to create a new view to render the dragged value for a + /// drag and drop operation. This API should also be used as the equivalent of 'on drag start' with + /// the [`Self::on_drag_move()`] API + /// The fluent API equivalent to [`Interactivity::on_drag()`] + /// + /// See [`ViewContext::listener()`] to get access to a view's state from this callback fn on_drag( mut self, value: T, @@ -671,6 +930,11 @@ pub trait StatefulInteractiveElement: InteractiveElement { self } + /// Bind the given callback on the hover start and end events of this element. Note that the boolean + /// passed to the callback is true when the hover starts and false when it ends. + /// The fluent API equivalent to [`Interactivity::on_hover()`] + /// + /// See [`ViewContext::listener()`] to get access to a view's state from this callback fn on_hover(mut self, listener: impl Fn(&bool, &mut WindowContext) + 'static) -> Self where Self: Sized, @@ -679,6 +943,8 @@ pub trait StatefulInteractiveElement: InteractiveElement { self } + /// Use the given callback to construct a new tooltip view when the mouse hovers over this element. + /// The fluent API equivalent to [`Interactivity::tooltip()`] fn tooltip(mut self, build_tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self where Self: Sized, @@ -688,7 +954,9 @@ pub trait StatefulInteractiveElement: InteractiveElement { } } +/// A trait for providing focus related APIs to interactive elements pub trait FocusableElement: InteractiveElement { + /// Set the given styles to be applied when this element, specifically, is focused. fn focus(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self where Self: Sized, @@ -697,6 +965,7 @@ pub trait FocusableElement: InteractiveElement { self } + /// Set the given styles to be applied when this element is inside another element that is focused. fn in_focus(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self where Self: Sized, @@ -706,35 +975,36 @@ pub trait FocusableElement: InteractiveElement { } } -pub type MouseDownListener = +pub(crate) type MouseDownListener = Box; -pub type MouseUpListener = +pub(crate) type MouseUpListener = Box; -pub type MouseMoveListener = +pub(crate) type MouseMoveListener = Box; -pub type ScrollWheelListener = +pub(crate) type ScrollWheelListener = Box; -pub type ClickListener = Box; +pub(crate) type ClickListener = Box; -pub type DragListener = Box AnyView + 'static>; +pub(crate) type DragListener = Box AnyView + 'static>; type DropListener = Box; type CanDropPredicate = Box bool + 'static>; -pub type TooltipBuilder = Rc AnyView + 'static>; +pub(crate) type TooltipBuilder = Rc AnyView + 'static>; -pub type KeyDownListener = Box; +pub(crate) type KeyDownListener = + Box; -pub type KeyUpListener = Box; +pub(crate) type KeyUpListener = + Box; -pub type DragEventListener = Box; - -pub type ActionListener = Box; +pub(crate) type ActionListener = Box; +/// Construct a new [`Div`] element #[track_caller] pub fn div() -> Div { #[cfg(debug_assertions)] @@ -753,6 +1023,7 @@ pub fn div() -> Div { } } +/// A [`Div`] element, the all-in-one element for building complex UIs in GPUI pub struct Div { interactivity: Interactivity, children: SmallVec<[AnyElement; 2]>, @@ -771,8 +1042,8 @@ impl InteractiveElement for Div { } impl ParentElement for Div { - fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { - &mut self.children + fn extend(&mut self, elements: impl IntoIterator) { + self.children.extend(elements) } } @@ -782,7 +1053,7 @@ impl Element for Div { fn request_layout( &mut self, element_state: Option, - cx: &mut WindowContext, + cx: &mut ElementContext, ) -> (LayoutId, Self::State) { let mut child_layout_ids = SmallVec::new(); let (layout_id, interactive_state) = self.interactivity.layout( @@ -812,7 +1083,7 @@ impl Element for Div { &mut self, bounds: Bounds, element_state: &mut Self::State, - cx: &mut WindowContext, + cx: &mut ElementContext, ) { let mut child_min = point(Pixels::MAX, Pixels::MAX); let mut child_max = Point::default(); @@ -875,12 +1146,14 @@ impl IntoElement for Div { } } +/// The state a div needs to keep track of between frames. pub struct DivState { child_layout_ids: SmallVec<[LayoutId; 2]>, interactive_state: InteractiveElementState, } impl DivState { + /// Is the div currently being clicked on? pub fn is_active(&self) -> bool { self.interactive_state .pending_mouse_down @@ -889,56 +1162,67 @@ impl DivState { } } +/// The interactivity struct. Powers all of the general-purpose +/// interactivity in the `Div` element. #[derive(Default)] pub struct Interactivity { + /// The element ID of the element pub element_id: Option, - pub key_context: Option, - pub focusable: bool, - pub tracked_focus_handle: Option, - pub scroll_handle: Option, - pub group: Option, + pub(crate) key_context: Option, + pub(crate) focusable: bool, + pub(crate) tracked_focus_handle: Option, + pub(crate) scroll_handle: Option, + pub(crate) group: Option, + /// The base style of the element, before any modifications are applied + /// by focus, active, etc. pub base_style: Box, - pub focus_style: Option>, - pub in_focus_style: Option>, - pub hover_style: Option>, - pub group_hover_style: Option, - pub active_style: Option>, - pub group_active_style: Option, - pub drag_over_styles: Vec<(TypeId, StyleRefinement)>, - pub group_drag_over_styles: Vec<(TypeId, GroupStyle)>, - pub mouse_down_listeners: Vec, - pub mouse_up_listeners: Vec, - pub mouse_move_listeners: Vec, - pub scroll_wheel_listeners: Vec, - pub key_down_listeners: Vec, - pub key_up_listeners: Vec, - pub action_listeners: Vec<(TypeId, ActionListener)>, - pub drop_listeners: Vec<(TypeId, DropListener)>, - pub can_drop_predicate: Option, - pub click_listeners: Vec, - pub drag_listener: Option<(Box, DragListener)>, - pub hover_listener: Option>, - pub tooltip_builder: Option, - pub block_mouse: bool, + pub(crate) focus_style: Option>, + pub(crate) in_focus_style: Option>, + pub(crate) hover_style: Option>, + pub(crate) group_hover_style: Option, + pub(crate) active_style: Option>, + pub(crate) group_active_style: Option, + pub(crate) drag_over_styles: Vec<(TypeId, StyleRefinement)>, + pub(crate) group_drag_over_styles: Vec<(TypeId, GroupStyle)>, + pub(crate) mouse_down_listeners: Vec, + pub(crate) mouse_up_listeners: Vec, + pub(crate) mouse_move_listeners: Vec, + pub(crate) scroll_wheel_listeners: Vec, + pub(crate) key_down_listeners: Vec, + pub(crate) key_up_listeners: Vec, + pub(crate) action_listeners: Vec<(TypeId, ActionListener)>, + pub(crate) drop_listeners: Vec<(TypeId, DropListener)>, + pub(crate) can_drop_predicate: Option, + pub(crate) click_listeners: Vec, + pub(crate) drag_listener: Option<(Box, DragListener)>, + pub(crate) hover_listener: Option>, + pub(crate) tooltip_builder: Option, + pub(crate) block_mouse: bool, #[cfg(debug_assertions)] - pub location: Option>, + pub(crate) location: Option>, #[cfg(any(test, feature = "test-support"))] - pub debug_selector: Option, + pub(crate) debug_selector: Option, } +/// The bounds and depth of an element in the computed element tree. #[derive(Clone, Debug)] pub struct InteractiveBounds { + /// The 2D bounds of the element pub bounds: Bounds, + /// The 'stacking order', or depth, for this element pub stacking_order: StackingOrder, } impl InteractiveBounds { + /// Checks whether this point was inside these bounds, and that these bounds where the topmost layer pub fn visibly_contains(&self, point: &Point, cx: &WindowContext) -> bool { self.bounds.contains(point) && cx.was_top_layer(point, &self.stacking_order) } + /// Checks whether this point was inside these bounds, and that these bounds where the topmost layer + /// under an active drag pub fn drag_target_contains(&self, point: &Point, cx: &WindowContext) -> bool { self.bounds.contains(point) && cx.was_top_layer_under_active_drag(point, &self.stacking_order) @@ -946,11 +1230,12 @@ impl InteractiveBounds { } impl Interactivity { + /// Layout this element according to this interactivity state's configured styles pub fn layout( &mut self, element_state: Option, - cx: &mut WindowContext, - f: impl FnOnce(Style, &mut WindowContext) -> LayoutId, + cx: &mut ElementContext, + f: impl FnOnce(Style, &mut ElementContext) -> LayoutId, ) -> (LayoutId, InteractiveElementState) { let mut element_state = element_state.unwrap_or_default(); @@ -984,13 +1269,21 @@ impl Interactivity { (layout_id, element_state) } + /// Paint this element according to this interactivity state's configured styles + /// and bind the element's mouse and keyboard events. + /// + /// content_size is the size of the content of the element, which may be larger than the + /// element's bounds if the element is scrollable. + /// + /// the final computed style will be passed to the provided function, along + /// with the current scroll offset pub fn paint( &mut self, bounds: Bounds, content_size: Size, element_state: &mut InteractiveElementState, - cx: &mut WindowContext, - f: impl FnOnce(&Style, Point, &mut WindowContext), + cx: &mut ElementContext, + f: impl FnOnce(&Style, Point, &mut ElementContext), ) { let style = self.compute_style(Some(bounds), element_state, cx); let z_index = style.z_index.unwrap_or(0); @@ -1003,7 +1296,7 @@ impl Interactivity { .insert(debug_selector.clone(), bounds); } - let paint_hover_group_handler = |cx: &mut WindowContext| { + let paint_hover_group_handler = |cx: &mut ElementContext| { let hover_group_bounds = self .group_hover_style .as_ref() @@ -1027,7 +1320,7 @@ impl Interactivity { } cx.with_z_index(z_index, |cx| { - style.paint(bounds, cx, |cx| { + style.paint(bounds, cx, |cx: &mut ElementContext| { cx.with_text_style(style.text_style().cloned(), |cx| { cx.with_content_mask(style.overflow_mask(bounds, cx.rem_size()), |cx| { #[cfg(debug_assertions)] @@ -1041,7 +1334,7 @@ impl Interactivity { let element_id = format!("{:?}", self.element_id.as_ref().unwrap()); let str_len = element_id.len(); - let render_debug_text = |cx: &mut WindowContext| { + let render_debug_text = |cx: &mut ElementContext| { if let Some(text) = cx .text_system() .shape_text( @@ -1248,12 +1541,17 @@ impl Interactivity { let mut can_drop = true; if let Some(predicate) = &can_drop_predicate { - can_drop = - predicate(drag.value.as_ref(), cx); + can_drop = predicate( + drag.value.as_ref(), + cx.deref_mut(), + ); } if can_drop { - listener(drag.value.as_ref(), cx); + listener( + drag.value.as_ref(), + cx.deref_mut(), + ); cx.refresh(); cx.stop_propagation(); } @@ -1384,7 +1682,7 @@ impl Interactivity { *was_hovered = is_hovered; drop(was_hovered); - hover_listener(&is_hovered, cx); + hover_listener(&is_hovered, cx.deref_mut()); } }); } @@ -1420,7 +1718,7 @@ impl Interactivity { move |mut cx| async move { cx.background_executor().timer(TOOLTIP_DELAY).await; - cx.update(|_, cx| { + cx.update(|cx| { active_tooltip.borrow_mut().replace( ActiveTooltip { tooltip: Some(AnyTooltip { @@ -1505,7 +1803,27 @@ impl Interactivity { .get_or_insert_with(Rc::default) .clone(); let line_height = cx.line_height(); - let scroll_max = (content_size - bounds.size).max(&Size::default()); + let rem_size = cx.rem_size(); + let padding_size = size( + style + .padding + .left + .to_pixels(bounds.size.width.into(), rem_size) + + style + .padding + .right + .to_pixels(bounds.size.width.into(), rem_size), + style + .padding + .top + .to_pixels(bounds.size.height.into(), rem_size) + + style + .padding + .bottom + .to_pixels(bounds.size.height.into(), rem_size), + ); + let scroll_max = + (content_size + padding_size - bounds.size).max(&Size::default()); // Clamp scroll offset in case scroll max is smaller now (e.g., if children // were removed or the bounds became larger). { @@ -1600,11 +1918,12 @@ impl Interactivity { }); } + /// Compute the visual style for this element, based on the current bounds and the element's state. pub fn compute_style( &self, bounds: Option>, element_state: &mut InteractiveElementState, - cx: &mut WindowContext, + cx: &mut ElementContext, ) -> Style { let mut style = Style::default(); style.refine(&self.base_style); @@ -1628,7 +1947,9 @@ impl Interactivity { let mouse_position = cx.mouse_position(); if !cx.has_active_drag() { if let Some(group_hover) = self.group_hover_style.as_ref() { - if let Some(group_bounds) = GroupBounds::get(&group_hover.group, cx) { + if let Some(group_bounds) = + GroupBounds::get(&group_hover.group, cx.deref_mut()) + { if group_bounds.contains(&mouse_position) && cx.was_top_layer(&mouse_position, cx.stacking_order()) { @@ -1651,13 +1972,13 @@ impl Interactivity { if let Some(drag) = cx.active_drag.take() { let mut can_drop = true; if let Some(can_drop_predicate) = &self.can_drop_predicate { - can_drop = can_drop_predicate(drag.value.as_ref(), cx); + can_drop = can_drop_predicate(drag.value.as_ref(), cx.deref_mut()); } if can_drop { for (state_type, group_drag_style) in &self.group_drag_over_styles { if let Some(group_bounds) = - GroupBounds::get(&group_drag_style.group, cx) + GroupBounds::get(&group_drag_style.group, cx.deref_mut()) { if *state_type == drag.value.as_ref().type_id() && group_bounds.contains(&mouse_position) @@ -1707,25 +2028,31 @@ impl Interactivity { } } +/// The per-frame state of an interactive element. Used for tracking stateful interactions like clicks +/// and scroll offsets. #[derive(Default)] pub struct InteractiveElementState { - pub focus_handle: Option, - pub clicked_state: Option>>, - pub hover_state: Option>>, - pub pending_mouse_down: Option>>>, - pub scroll_offset: Option>>>, - pub active_tooltip: Option>>>, + pub(crate) focus_handle: Option, + pub(crate) clicked_state: Option>>, + pub(crate) hover_state: Option>>, + pub(crate) pending_mouse_down: Option>>>, + pub(crate) scroll_offset: Option>>>, + pub(crate) active_tooltip: Option>>>, } +/// The current active tooltip pub struct ActiveTooltip { - tooltip: Option, - _task: Option>, + pub(crate) tooltip: Option, + pub(crate) _task: Option>, } /// Whether or not the element or a group that contains it is clicked by the mouse. #[derive(Copy, Clone, Default, Eq, PartialEq)] pub struct ElementClickedState { + /// True if this element's group has been clicked, false otherwise pub group: bool, + + /// True if this element has been clicked, false otherwise pub element: bool, } @@ -1736,7 +2063,7 @@ impl ElementClickedState { } #[derive(Default)] -pub struct GroupBounds(HashMap; 1]>>); +pub(crate) struct GroupBounds(HashMap; 1]>>); impl GroupBounds { pub fn get(name: &SharedString, cx: &mut AppContext) -> Option> { @@ -1760,7 +2087,9 @@ impl GroupBounds { } } +/// A wrapper around an element that can be focused. pub struct Focusable { + /// The element that is focusable pub element: E, } @@ -1795,12 +2124,12 @@ where fn request_layout( &mut self, state: Option, - cx: &mut WindowContext, + cx: &mut ElementContext, ) -> (LayoutId, Self::State) { self.element.request_layout(state, cx) } - fn paint(&mut self, bounds: Bounds, state: &mut Self::State, cx: &mut WindowContext) { + fn paint(&mut self, bounds: Bounds, state: &mut Self::State, cx: &mut ElementContext) { self.element.paint(bounds, state, cx) } } @@ -1824,11 +2153,12 @@ impl ParentElement for Focusable where E: ParentElement, { - fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { - self.element.children_mut() + fn extend(&mut self, elements: impl Iterator) { + self.element.extend(elements) } } +/// A wrapper around an element that can store state, produced after assigning an ElementId. pub struct Stateful { element: E, } @@ -1869,12 +2199,12 @@ where fn request_layout( &mut self, state: Option, - cx: &mut WindowContext, + cx: &mut ElementContext, ) -> (LayoutId, Self::State) { self.element.request_layout(state, cx) } - fn paint(&mut self, bounds: Bounds, state: &mut Self::State, cx: &mut WindowContext) { + fn paint(&mut self, bounds: Bounds, state: &mut Self::State, cx: &mut ElementContext) { self.element.paint(bounds, state, cx) } } @@ -1898,14 +2228,13 @@ impl ParentElement for Stateful where E: ParentElement, { - fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { - self.element.children_mut() + fn extend(&mut self, elements: impl Iterator) { + self.element.extend(elements) } } #[derive(Default)] struct ScrollHandleState { - // not great to have the nested rc's... offset: Rc>>, bounds: Bounds, child_bounds: Vec>, @@ -1913,6 +2242,9 @@ struct ScrollHandleState { overflow: Point, } +/// A handle to the scrollable aspects of an element. +/// Used for accessing scroll state, like the current scroll offset, +/// and for mutating the scroll state, like scrolling to a specific child. #[derive(Clone)] pub struct ScrollHandle(Rc>); @@ -1923,14 +2255,17 @@ impl Default for ScrollHandle { } impl ScrollHandle { + /// Construct a new scroll handle. pub fn new() -> Self { Self(Rc::default()) } + /// Get the current scroll offset. pub fn offset(&self) -> Point { *self.0.borrow().offset.borrow() } + /// Get the top child that's scrolled into view. pub fn top_item(&self) -> usize { let state = self.0.borrow(); let top = state.bounds.top() - state.offset.borrow().y; @@ -1949,11 +2284,12 @@ impl ScrollHandle { } } + /// Get the bounds for a specific child. pub fn bounds_for_item(&self, ix: usize) -> Option> { self.0.borrow().child_bounds.get(ix).cloned() } - /// scroll_to_item scrolls the minimal amount to ensure that the item is + /// scroll_to_item scrolls the minimal amount to ensure that the child is /// fully visible pub fn scroll_to_item(&self, ix: usize) { let state = self.0.borrow(); @@ -1981,6 +2317,7 @@ impl ScrollHandle { } } + /// Get the logical scroll top, based on a child index and a pixel offset. pub fn logical_scroll_top(&self) -> (usize, Pixels) { let ix = self.top_item(); let state = self.0.borrow(); @@ -1995,6 +2332,7 @@ impl ScrollHandle { } } + /// Set the logical scroll top, based on a child index and a pixel offset. pub fn set_logical_scroll_top(&self, ix: usize, px: Pixels) { self.0.borrow_mut().requested_scroll_top = Some((ix, px)); } diff --git a/crates/gpui/src/elements/img.rs b/crates/gpui/src/elements/img.rs index 5a656db9fbe8a104ee8c92d46d152cd146dbadfe..191cadf2939834eb68c4b70abf18b92c8bdc6ff8 100644 --- a/crates/gpui/src/elements/img.rs +++ b/crates/gpui/src/elements/img.rs @@ -1,19 +1,23 @@ use std::sync::Arc; use crate::{ - point, size, BorrowWindow, Bounds, DevicePixels, Element, ImageData, InteractiveElement, + point, size, Bounds, DevicePixels, Element, ElementContext, ImageData, InteractiveElement, InteractiveElementState, Interactivity, IntoElement, LayoutId, Pixels, SharedUrl, Size, - StyleRefinement, Styled, WindowContext, + StyleRefinement, Styled, }; use futures::FutureExt; use media::core_video::CVImageBuffer; use util::ResultExt; +/// A source of image content. #[derive(Clone, Debug)] pub enum ImageSource { /// Image content will be loaded from provided URI at render time. Uri(SharedUrl), + /// Cached image data Data(Arc), + // TODO: move surface definitions into mac platform module + /// A CoreVideo image buffer Surface(CVImageBuffer), } @@ -47,12 +51,14 @@ impl From for ImageSource { } } +/// An image element. pub struct Img { interactivity: Interactivity, source: ImageSource, grayscale: bool, } +/// Create a new image element. pub fn img(source: impl Into) -> Img { Img { interactivity: Interactivity::default(), @@ -62,6 +68,7 @@ pub fn img(source: impl Into) -> Img { } impl Img { + /// Set the image to be displayed in grayscale. pub fn grayscale(mut self, grayscale: bool) -> Self { self.grayscale = grayscale; self @@ -74,7 +81,7 @@ impl Element for Img { fn request_layout( &mut self, element_state: Option, - cx: &mut WindowContext, + cx: &mut ElementContext, ) -> (LayoutId, Self::State) { self.interactivity .layout(element_state, cx, |style, cx| cx.request_layout(&style, [])) @@ -84,7 +91,7 @@ impl Element for Img { &mut self, bounds: Bounds, element_state: &mut Self::State, - cx: &mut WindowContext, + cx: &mut ElementContext, ) { let source = self.source.clone(); self.interactivity.paint( diff --git a/crates/gpui/src/elements/list.rs b/crates/gpui/src/elements/list.rs index 2921d90a10088d8a4f03018b6f29f8d2416e110a..c16fb32a52c0e4d4aa3aadd08ee0c740a64a8387 100644 --- a/crates/gpui/src/elements/list.rs +++ b/crates/gpui/src/elements/list.rs @@ -1,13 +1,22 @@ +//! A list element that can be used to render a large number of differently sized elements +//! efficiently. Clients of this API need to ensure that elements outside of the scrolled +//! area do not change their height for this element to function correctly. In order to minimize +//! re-renders, this element's state is stored intrusively on your own views, so that your code +//! can coordinate directly with the list element's cached state. +//! +//! If all of your elements are the same height, see [`UniformList`] for a simpler API + use crate::{ - point, px, AnyElement, AvailableSpace, BorrowAppContext, BorrowWindow, Bounds, ContentMask, - DispatchPhase, Element, IntoElement, Pixels, Point, ScrollWheelEvent, Size, Style, - StyleRefinement, Styled, WindowContext, + point, px, AnyElement, AvailableSpace, Bounds, ContentMask, DispatchPhase, Element, + IntoElement, Pixels, Point, ScrollWheelEvent, Size, Style, StyleRefinement, Styled, + WindowContext, }; use collections::VecDeque; use refineable::Refineable as _; use std::{cell::RefCell, ops::Range, rc::Rc}; use sum_tree::{Bias, SumTree}; +/// Construct a new list element pub fn list(state: ListState) -> List { List { state, @@ -15,11 +24,13 @@ pub fn list(state: ListState) -> List { } } +/// A list element pub struct List { state: ListState, style: StyleRefinement, } +/// The list state that views must hold on behalf of the list element. #[derive(Clone)] pub struct ListState(Rc>); @@ -35,15 +46,24 @@ struct StateInner { scroll_handler: Option>, } +/// Whether the list is scrolling from top to bottom or bottom to top. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum ListAlignment { + /// The list is scrolling from top to bottom, like most lists. Top, + /// The list is scrolling from bottom to top, like a chat log. Bottom, } +/// A scroll event that has been converted to be in terms of the list's items. pub struct ListScrollEvent { + /// The range of items currently visible in the list, after applying the scroll event. pub visible_range: Range, + + /// The number of items that are currently visible in the list, after applying the scroll event. pub count: usize, + + /// Whether the list has been scrolled. pub is_scrolled: bool, } @@ -74,6 +94,11 @@ struct UnrenderedCount(usize); struct Height(Pixels); impl ListState { + /// Construct a new list state, for storage on a view. + /// + /// the overdraw parameter controls how much extra space is rendered + /// above and below the visible area. This can help ensure that the list + /// doesn't flicker or pop in when scrolling. pub fn new( element_count: usize, orientation: ListAlignment, @@ -111,10 +136,13 @@ impl ListState { .extend((0..element_count).map(|_| ListItem::Unrendered), &()); } + /// The number of items in this list. pub fn item_count(&self) -> usize { self.0.borrow().items.summary().count } + /// Register with the list state that the items in `old_range` have been replaced + /// by `count` new items that must be recalculated. pub fn splice(&self, old_range: Range, count: usize) { let state = &mut *self.0.borrow_mut(); @@ -141,6 +169,7 @@ impl ListState { state.items = new_heights; } + /// Set a handler that will be called when the list is scrolled. pub fn set_scroll_handler( &self, handler: impl FnMut(&ListScrollEvent, &mut WindowContext) + 'static, @@ -148,10 +177,12 @@ impl ListState { self.0.borrow_mut().scroll_handler = Some(Box::new(handler)) } + /// Get the current scroll offset, in terms of the list's items. pub fn logical_scroll_top(&self) -> ListOffset { self.0.borrow().logical_scroll_top() } + /// Scroll the list to the given offset pub fn scroll_to(&self, mut scroll_top: ListOffset) { let state = &mut *self.0.borrow_mut(); let item_count = state.items.summary().count; @@ -163,6 +194,7 @@ impl ListState { state.logical_scroll_top = Some(scroll_top); } + /// Scroll the list to the given item, such that the item is fully visible. pub fn scroll_to_reveal_item(&self, ix: usize) { let state = &mut *self.0.borrow_mut(); @@ -193,7 +225,8 @@ impl ListState { state.logical_scroll_top = Some(scroll_top); } - /// Get the bounds for the given item in window coordinates. + /// Get the bounds for the given item in window coordinates, if it's + /// been rendered. pub fn bounds_for_item(&self, ix: usize) -> Option> { let state = &*self.0.borrow(); @@ -310,9 +343,13 @@ impl std::fmt::Debug for ListItem { } } +/// An offset into the list's items, in terms of the item index and the number +/// of pixels off the top left of the item. #[derive(Debug, Clone, Copy, Default)] pub struct ListOffset { + /// The index of an item in the list pub item_ix: usize, + /// The number of pixels to offset from the item index. pub offset_in_item: Pixels, } @@ -322,7 +359,7 @@ impl Element for List { fn request_layout( &mut self, _state: Option, - cx: &mut crate::WindowContext, + cx: &mut crate::ElementContext, ) -> (crate::LayoutId, Self::State) { let mut style = Style::default(); style.refine(&self.style); @@ -336,7 +373,7 @@ impl Element for List { &mut self, bounds: Bounds, _state: &mut Self::State, - cx: &mut crate::WindowContext, + cx: &mut crate::ElementContext, ) { let state = &mut *self.state.0.borrow_mut(); diff --git a/crates/gpui/src/elements/overlay.rs b/crates/gpui/src/elements/overlay.rs index 6996a13cfaac61b84e9ba7df10f240710ca2d162..61c34bd9385f6580feb3930dd3b3ea4b1494e919 100644 --- a/crates/gpui/src/elements/overlay.rs +++ b/crates/gpui/src/elements/overlay.rs @@ -2,14 +2,17 @@ use smallvec::SmallVec; use taffy::style::{Display, Position}; use crate::{ - point, AnyElement, BorrowWindow, Bounds, Element, IntoElement, LayoutId, ParentElement, Pixels, - Point, Size, Style, WindowContext, + point, AnyElement, Bounds, Element, ElementContext, IntoElement, LayoutId, ParentElement, + Pixels, Point, Size, Style, }; +/// The state that the overlay element uses to track its children. pub struct OverlayState { child_layout_ids: SmallVec<[LayoutId; 4]>, } +/// An overlay element that can be used to display UI that +/// floats on top of other UI elements. pub struct Overlay { children: SmallVec<[AnyElement; 2]>, anchor_corner: AnchorCorner, @@ -60,8 +63,8 @@ impl Overlay { } impl ParentElement for Overlay { - fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { - &mut self.children + fn extend(&mut self, elements: impl Iterator) { + self.children.extend(elements) } } @@ -71,7 +74,7 @@ impl Element for Overlay { fn request_layout( &mut self, _: Option, - cx: &mut WindowContext, + cx: &mut ElementContext, ) -> (crate::LayoutId, Self::State) { let child_layout_ids = self .children @@ -94,7 +97,7 @@ impl Element for Overlay { &mut self, bounds: crate::Bounds, element_state: &mut Self::State, - cx: &mut WindowContext, + cx: &mut ElementContext, ) { if element_state.child_layout_ids.is_empty() { return; @@ -191,15 +194,21 @@ enum Axis { Vertical, } +/// Which algorithm to use when fitting the overlay to be inside the window. #[derive(Copy, Clone, PartialEq)] pub enum OverlayFitMode { + /// Snap the overlay to the window edge SnapToWindow, + /// Switch which corner anchor this overlay is attached to SwitchAnchor, } +/// Which algorithm to use when positioning the overlay. #[derive(Copy, Clone, PartialEq)] pub enum OverlayPositionMode { + /// Position the overlay relative to the window Window, + /// Position the overlay relative to its parent Local, } @@ -226,11 +235,16 @@ impl OverlayPositionMode { } } +/// Which corner of the overlay should be considered the anchor. #[derive(Clone, Copy, PartialEq, Eq)] pub enum AnchorCorner { + /// The top left corner TopLeft, + /// The top right corner TopRight, + /// The bottom left corner BottomLeft, + /// The bottom right corner BottomRight, } @@ -255,6 +269,7 @@ impl AnchorCorner { Bounds { origin, size } } + /// Get the point corresponding to this anchor corner in `bounds`. pub fn corner(&self, bounds: Bounds) -> Point { match self { Self::TopLeft => bounds.origin, diff --git a/crates/gpui/src/elements/svg.rs b/crates/gpui/src/elements/svg.rs index 5ea7f9bd7825a0142fd38f07a8223ff3a973e9dc..2ef0888563c05a67bd1fb4d696bf046ba87c9b51 100644 --- a/crates/gpui/src/elements/svg.rs +++ b/crates/gpui/src/elements/svg.rs @@ -1,14 +1,16 @@ use crate::{ - Bounds, Element, ElementId, InteractiveElement, InteractiveElementState, Interactivity, - IntoElement, LayoutId, Pixels, SharedString, StyleRefinement, Styled, WindowContext, + Bounds, Element, ElementContext, ElementId, InteractiveElement, InteractiveElementState, + Interactivity, IntoElement, LayoutId, Pixels, SharedString, StyleRefinement, Styled, }; use util::ResultExt; +/// An SVG element. pub struct Svg { interactivity: Interactivity, path: Option, } +/// Create a new SVG element. pub fn svg() -> Svg { Svg { interactivity: Interactivity::default(), @@ -17,6 +19,7 @@ pub fn svg() -> Svg { } impl Svg { + /// Set the path to the SVG file for this element. pub fn path(mut self, path: impl Into) -> Self { self.path = Some(path.into()); self @@ -29,7 +32,7 @@ impl Element for Svg { fn request_layout( &mut self, element_state: Option, - cx: &mut WindowContext, + cx: &mut ElementContext, ) -> (LayoutId, Self::State) { self.interactivity.layout(element_state, cx, |style, cx| { cx.request_layout(&style, None) @@ -40,7 +43,7 @@ impl Element for Svg { &mut self, bounds: Bounds, element_state: &mut Self::State, - cx: &mut WindowContext, + cx: &mut ElementContext, ) where Self: Sized, { diff --git a/crates/gpui/src/elements/text.rs b/crates/gpui/src/elements/text.rs index f72b7c6fa9111c4735fe95241ab9b63d25e39a37..13fc6200766edc068af5f23defc3d9c2e73c4214 100644 --- a/crates/gpui/src/elements/text.rs +++ b/crates/gpui/src/elements/text.rs @@ -1,12 +1,19 @@ use crate::{ - Bounds, DispatchPhase, Element, ElementId, HighlightStyle, IntoElement, LayoutId, - MouseDownEvent, MouseUpEvent, Pixels, Point, SharedString, Size, TextRun, TextStyle, - WhiteSpace, WindowContext, WrappedLine, + ActiveTooltip, AnyTooltip, AnyView, Bounds, DispatchPhase, Element, ElementContext, ElementId, + HighlightStyle, IntoElement, LayoutId, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, + Point, SharedString, Size, TextRun, TextStyle, WhiteSpace, WindowContext, WrappedLine, + TOOLTIP_DELAY, }; use anyhow::anyhow; use parking_lot::{Mutex, MutexGuard}; use smallvec::SmallVec; -use std::{cell::Cell, mem, ops::Range, rc::Rc, sync::Arc}; +use std::{ + cell::{Cell, RefCell}, + mem, + ops::Range, + rc::Rc, + sync::Arc, +}; use util::ResultExt; impl Element for &'static str { @@ -15,14 +22,14 @@ impl Element for &'static str { fn request_layout( &mut self, _: Option, - cx: &mut WindowContext, + cx: &mut ElementContext, ) -> (LayoutId, Self::State) { let mut state = TextState::default(); let layout_id = state.layout(SharedString::from(*self), None, cx); (layout_id, state) } - fn paint(&mut self, bounds: Bounds, state: &mut TextState, cx: &mut WindowContext) { + fn paint(&mut self, bounds: Bounds, state: &mut TextState, cx: &mut ElementContext) { state.paint(bounds, self, cx) } } @@ -45,14 +52,14 @@ impl Element for SharedString { fn request_layout( &mut self, _: Option, - cx: &mut WindowContext, + cx: &mut ElementContext, ) -> (LayoutId, Self::State) { let mut state = TextState::default(); let layout_id = state.layout(self.clone(), None, cx); (layout_id, state) } - fn paint(&mut self, bounds: Bounds, state: &mut TextState, cx: &mut WindowContext) { + fn paint(&mut self, bounds: Bounds, state: &mut TextState, cx: &mut ElementContext) { let text_str: &str = self.as_ref(); state.paint(bounds, text_str, cx) } @@ -81,6 +88,7 @@ pub struct StyledText { } impl StyledText { + /// Construct a new styled text element from the given string. pub fn new(text: impl Into) -> Self { StyledText { text: text.into(), @@ -88,6 +96,8 @@ impl StyledText { } } + /// Set the styling attributes for the given text, as well as + /// as any ranges of text that have had their style customized. pub fn with_highlights( mut self, default_style: &TextStyle, @@ -121,14 +131,14 @@ impl Element for StyledText { fn request_layout( &mut self, _: Option, - cx: &mut WindowContext, + cx: &mut ElementContext, ) -> (LayoutId, Self::State) { let mut state = TextState::default(); let layout_id = state.layout(self.text.clone(), self.runs.take(), cx); (layout_id, state) } - fn paint(&mut self, bounds: Bounds, state: &mut Self::State, cx: &mut WindowContext) { + fn paint(&mut self, bounds: Bounds, state: &mut Self::State, cx: &mut ElementContext) { state.paint(bounds, &self.text, cx) } } @@ -145,6 +155,7 @@ impl IntoElement for StyledText { } } +#[doc(hidden)] #[derive(Default, Clone)] pub struct TextState(Arc>>); @@ -164,7 +175,7 @@ impl TextState { &mut self, text: SharedString, runs: Option>, - cx: &mut WindowContext, + cx: &mut ElementContext, ) -> LayoutId { let text_style = cx.text_style(); let font_size = text_style.font_size.to_pixels(cx.rem_size()); @@ -239,7 +250,7 @@ impl TextState { layout_id } - fn paint(&mut self, bounds: Bounds, text: &str, cx: &mut WindowContext) { + fn paint(&mut self, bounds: Bounds, text: &str, cx: &mut ElementContext) { let element_state = self.lock(); let element_state = element_state .as_ref() @@ -284,11 +295,14 @@ impl TextState { } } +/// A text element that can be interacted with. pub struct InteractiveText { element_id: ElementId, text: StyledText, click_listener: Option], InteractiveTextClickEvent, &mut WindowContext<'_>)>>, + hover_listener: Option, MouseMoveEvent, &mut WindowContext<'_>)>>, + tooltip_builder: Option) -> Option>>, clickable_ranges: Vec>, } @@ -297,21 +311,30 @@ struct InteractiveTextClickEvent { mouse_up_index: usize, } +#[doc(hidden)] pub struct InteractiveTextState { text_state: TextState, mouse_down_index: Rc>>, + hovered_index: Rc>>, + active_tooltip: Rc>>, } +/// InteractiveTest is a wrapper around StyledText that adds mouse interactions. impl InteractiveText { + /// Creates a new InteractiveText from the given text. pub fn new(id: impl Into, text: StyledText) -> Self { Self { element_id: id.into(), text, click_listener: None, + hover_listener: None, + tooltip_builder: None, clickable_ranges: Vec::new(), } } + /// on_click is called when the user clicks on one of the given ranges, passing the index of + /// the clicked range. pub fn on_click( mut self, ranges: Vec>, @@ -328,6 +351,25 @@ impl InteractiveText { self.clickable_ranges = ranges; self } + + /// on_hover is called when the mouse moves over a character within the text, passing the + /// index of the hovered character, or None if the mouse leaves the text. + pub fn on_hover( + mut self, + listener: impl Fn(Option, MouseMoveEvent, &mut WindowContext<'_>) + 'static, + ) -> Self { + self.hover_listener = Some(Box::new(listener)); + self + } + + /// tooltip lets you specify a tooltip for a given character index in the string. + pub fn tooltip( + mut self, + builder: impl Fn(usize, &mut WindowContext<'_>) -> Option + 'static, + ) -> Self { + self.tooltip_builder = Some(Rc::new(builder)); + self + } } impl Element for InteractiveText { @@ -336,16 +378,21 @@ impl Element for InteractiveText { fn request_layout( &mut self, state: Option, - cx: &mut WindowContext, + cx: &mut ElementContext, ) -> (LayoutId, Self::State) { if let Some(InteractiveTextState { - mouse_down_index, .. + mouse_down_index, + hovered_index, + active_tooltip, + .. }) = state { let (layout_id, text_state) = self.text.request_layout(None, cx); let element_state = InteractiveTextState { text_state, mouse_down_index, + hovered_index, + active_tooltip, }; (layout_id, element_state) } else { @@ -353,12 +400,14 @@ impl Element for InteractiveText { let element_state = InteractiveTextState { text_state, mouse_down_index: Rc::default(), + hovered_index: Rc::default(), + active_tooltip: Rc::default(), }; (layout_id, element_state) } } - fn paint(&mut self, bounds: Bounds, state: &mut Self::State, cx: &mut WindowContext) { + fn paint(&mut self, bounds: Bounds, state: &mut Self::State, cx: &mut ElementContext) { if let Some(click_listener) = self.click_listener.take() { let mouse_position = cx.mouse_position(); if let Some(ix) = state.text_state.index_for_position(bounds, mouse_position) { @@ -408,6 +457,83 @@ impl Element for InteractiveText { }); } } + if let Some(hover_listener) = self.hover_listener.take() { + let text_state = state.text_state.clone(); + let hovered_index = state.hovered_index.clone(); + cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| { + if phase == DispatchPhase::Bubble { + let current = hovered_index.get(); + let updated = text_state.index_for_position(bounds, event.position); + if current != updated { + hovered_index.set(updated); + hover_listener(updated, event.clone(), cx); + cx.refresh(); + } + } + }); + } + if let Some(tooltip_builder) = self.tooltip_builder.clone() { + let active_tooltip = state.active_tooltip.clone(); + let pending_mouse_down = state.mouse_down_index.clone(); + let text_state = state.text_state.clone(); + + cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| { + let position = text_state.index_for_position(bounds, event.position); + let is_hovered = position.is_some() && pending_mouse_down.get().is_none(); + if !is_hovered { + active_tooltip.take(); + return; + } + let position = position.unwrap(); + + if phase != DispatchPhase::Bubble { + return; + } + + if active_tooltip.borrow().is_none() { + let task = cx.spawn({ + let active_tooltip = active_tooltip.clone(); + let tooltip_builder = tooltip_builder.clone(); + + move |mut cx| async move { + cx.background_executor().timer(TOOLTIP_DELAY).await; + cx.update(|cx| { + let new_tooltip = + tooltip_builder(position, cx).map(|tooltip| ActiveTooltip { + tooltip: Some(AnyTooltip { + view: tooltip, + cursor_offset: cx.mouse_position(), + }), + _task: None, + }); + *active_tooltip.borrow_mut() = new_tooltip; + cx.refresh(); + }) + .ok(); + } + }); + *active_tooltip.borrow_mut() = Some(ActiveTooltip { + tooltip: None, + _task: Some(task), + }); + } + }); + + let active_tooltip = state.active_tooltip.clone(); + cx.on_mouse_event(move |_: &MouseDownEvent, _, _| { + active_tooltip.take(); + }); + + if let Some(tooltip) = state + .active_tooltip + .clone() + .borrow() + .as_ref() + .and_then(|at| at.tooltip.clone()) + { + cx.set_tooltip(tooltip); + } + } self.text.paint(bounds, &mut state.text_state, cx) } diff --git a/crates/gpui/src/elements/uniform_list.rs b/crates/gpui/src/elements/uniform_list.rs index 77ef7df10c206c35ff59d354e1c90c88e7cbec5f..108e669f7550a2e6387641aaa4c1dae6e59f6313 100644 --- a/crates/gpui/src/elements/uniform_list.rs +++ b/crates/gpui/src/elements/uniform_list.rs @@ -1,5 +1,11 @@ +//! A scrollable list of elements with uniform height, optimized for large lists. +//! Rather than use the full taffy layout system, uniform_list simply measures +//! the first element and then lays out all remaining elements in a line based on that +//! measurement. This is much faster than the full layout system, but only works for +//! elements with uniform height. + use crate::{ - point, px, size, AnyElement, AvailableSpace, BorrowWindow, Bounds, ContentMask, Element, + point, px, size, AnyElement, AvailableSpace, Bounds, ContentMask, Element, ElementContext, ElementId, InteractiveElement, InteractiveElementState, Interactivity, IntoElement, LayoutId, Pixels, Render, Size, StyleRefinement, Styled, View, ViewContext, WindowContext, }; @@ -53,6 +59,7 @@ where } } +/// A list element for efficiently laying out and displaying a list of uniform-height elements. pub struct UniformList { id: ElementId, item_count: usize, @@ -63,18 +70,22 @@ pub struct UniformList { scroll_handle: Option, } +/// A handle for controlling the scroll position of a uniform list. +/// This should be stored in your view and passed to the uniform_list on each frame. #[derive(Clone, Default)] pub struct UniformListScrollHandle { deferred_scroll_to_item: Rc>>, } impl UniformListScrollHandle { + /// Create a new scroll handle to bind to a uniform list. pub fn new() -> Self { Self { deferred_scroll_to_item: Rc::new(RefCell::new(None)), } } + /// Scroll the list to the given item index. pub fn scroll_to_item(&mut self, ix: usize) { self.deferred_scroll_to_item.replace(Some(ix)); } @@ -86,6 +97,7 @@ impl Styled for UniformList { } } +#[doc(hidden)] #[derive(Default)] pub struct UniformListState { interactive: InteractiveElementState, @@ -98,7 +110,7 @@ impl Element for UniformList { fn request_layout( &mut self, state: Option, - cx: &mut WindowContext, + cx: &mut ElementContext, ) -> (LayoutId, Self::State) { let max_items = self.item_count; let item_size = state @@ -146,7 +158,7 @@ impl Element for UniformList { &mut self, bounds: Bounds, element_state: &mut Self::State, - cx: &mut WindowContext, + cx: &mut ElementContext, ) { let style = self.interactivity @@ -262,12 +274,13 @@ impl IntoElement for UniformList { } impl UniformList { + /// Selects a specific list item for measurement. pub fn with_width_from_item(mut self, item_index: Option) -> Self { self.item_to_measure_index = item_index.unwrap_or(0); self } - fn measure_item(&self, list_width: Option, cx: &mut WindowContext) -> Size { + fn measure_item(&self, list_width: Option, cx: &mut ElementContext) -> Size { if self.item_count == 0 { return Size::default(); } @@ -284,6 +297,7 @@ impl UniformList { item_to_measure.measure(available_space, cx) } + /// Track and render scroll state of this list with reference to the given scroll handle. pub fn track_scroll(mut self, handle: UniformListScrollHandle) -> Self { self.scroll_handle = Some(handle); self diff --git a/crates/gpui/src/executor.rs b/crates/gpui/src/executor.rs index 9ed643a5ab29e899329739b7ebd336e6c83884f6..1ca3c494ed5b3967dd741ab76a1352c779852ef5 100644 --- a/crates/gpui/src/executor.rs +++ b/crates/gpui/src/executor.rs @@ -21,11 +21,15 @@ use waker_fn::waker_fn; #[cfg(any(test, feature = "test-support"))] use rand::rngs::StdRng; +/// A pointer to the executor that is currently running, +/// for spawning background tasks. #[derive(Clone)] pub struct BackgroundExecutor { dispatcher: Arc, } +/// A pointer to the executor that is currently running, +/// for spawning tasks on the main thread. #[derive(Clone)] pub struct ForegroundExecutor { dispatcher: Arc, @@ -37,16 +41,19 @@ pub struct ForegroundExecutor { /// It implements [`Future`] so you can `.await` on it. /// /// If you drop a task it will be cancelled immediately. Calling [`Task::detach`] allows -/// the task to continue running in the background, but with no way to return a value. +/// the task to continue running, but with no way to return a value. #[must_use] #[derive(Debug)] pub enum Task { + /// A task that is ready to return a value Ready(Option), + + /// A task that is currently running. Spawned(async_task::Task), } impl Task { - /// Create a new task that will resolve with the value + /// Creates a new task that will resolve with the value pub fn ready(val: T) -> Self { Task::Ready(Some(val)) } @@ -68,7 +75,7 @@ where /// Run the task to completion in the background and log any /// errors that occur. #[track_caller] - pub fn detach_and_log_err(self, cx: &mut AppContext) { + pub fn detach_and_log_err(self, cx: &AppContext) { let location = core::panic::Location::caller(); cx.foreground_executor() .spawn(self.log_tracked_err(*location)) @@ -87,6 +94,8 @@ impl Future for Task { } } +/// A task label is an opaque identifier that you can use to +/// refer to a task in tests. #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] pub struct TaskLabel(NonZeroUsize); @@ -97,6 +106,7 @@ impl Default for TaskLabel { } impl TaskLabel { + /// Construct a new task label. pub fn new() -> Self { static NEXT_TASK_LABEL: AtomicUsize = AtomicUsize::new(1); Self(NEXT_TASK_LABEL.fetch_add(1, SeqCst).try_into().unwrap()) @@ -363,6 +373,7 @@ impl BackgroundExecutor { /// ForegroundExecutor runs things on the main thread. impl ForegroundExecutor { + /// Creates a new ForegroundExecutor from the given PlatformDispatcher. pub fn new(dispatcher: Arc) -> Self { Self { dispatcher, @@ -411,13 +422,14 @@ impl<'a> Scope<'a> { } } + /// Spawn a future into this scope. pub fn spawn(&mut self, f: F) where F: Future + Send + 'a, { let tx = self.tx.clone().unwrap(); - // Safety: The 'a lifetime is guaranteed to outlive any of these futures because + // SAFETY: The 'a lifetime is guaranteed to outlive any of these futures because // dropping this `Scope` blocks until all of the futures have resolved. let f = unsafe { mem::transmute::< diff --git a/crates/gpui/src/geometry.rs b/crates/gpui/src/geometry.rs index 89e47994a34b76d4310e3d8afe0c15be411608be..d986f26be4475a72b0da8b75de8b3847e3fce588 100644 --- a/crates/gpui/src/geometry.rs +++ b/crates/gpui/src/geometry.rs @@ -497,6 +497,20 @@ where } } +impl Add for Size +where + T: Add + Clone + Default + Debug, +{ + type Output = Size; + + fn add(self, rhs: Self) -> Self::Output { + Size { + width: self.width + rhs.width, + height: self.height + rhs.height, + } + } +} + impl Mul for Size where T: Mul + Clone + Default + Debug, diff --git a/crates/gpui/src/gpui.rs b/crates/gpui/src/gpui.rs index 6f5e30149d9691b3c364d62ab2e3ce6ec7da1b4c..929e9ebfb4f262088e90e7ab5b99477a9933d998 100644 --- a/crates/gpui/src/gpui.rs +++ b/crates/gpui/src/gpui.rs @@ -1,3 +1,31 @@ +//! # Welcome to GPUI! +//! +//! GPUI is a hybrid immediate and retained mode, GPU accelerated, UI framework +//! for Rust, designed to support a wide variety of applications. GPUI is currently +//! being actively developed and improved for the [Zed code editor](https://zed.dev/), and new versions +//! will have breaking changes. You'll probably need to use the latest stable version +//! of rust to use GPUI. +//! +//! # Getting started with GPUI +//! +//! TODO!(docs): Write a code sample showing how to create a window and render a simple +//! div +//! +//! # Drawing interesting things +//! +//! TODO!(docs): Expand demo to show how to draw a more interesting scene, with +//! a counter to store state and a button to increment it. +//! +//! # Interacting with your application state +//! +//! TODO!(docs): Expand demo to show GPUI entity interactions, like subscriptions and entities +//! maybe make a network request to show async stuff? +//! +//! # Conclusion +//! +//! TODO!(docs): Wrap up with a conclusion and links to other places? Zed / GPUI website? +//! Discord for chatting about it? Other tutorials or references? + #[macro_use] mod action; mod app; @@ -58,10 +86,10 @@ pub use elements::*; pub use executor::*; pub use geometry::*; pub use gpui_macros::{register_action, test, IntoElement, Render}; -pub use image_cache::*; +use image_cache::*; pub use input::*; pub use interactive::*; -pub use key_dispatch::*; +use key_dispatch::*; pub use keymap::*; pub use platform::*; pub use refineable::*; @@ -73,7 +101,7 @@ pub use smol::Timer; pub use style::*; pub use styled::*; pub use subscription::*; -pub use svg_renderer::*; +use svg_renderer::*; pub use taffy::{AvailableSpace, LayoutId}; #[cfg(any(test, feature = "test-support"))] pub use test::*; @@ -82,20 +110,23 @@ pub use util::arc_cow::ArcCow; pub use view::*; pub use window::*; -use std::{ - any::{Any, TypeId}, - borrow::BorrowMut, -}; +use std::{any::Any, borrow::BorrowMut}; use taffy::TaffyLayoutEngine; +/// The context trait, allows the different contexts in GPUI to be used +/// interchangeably for certain operations. pub trait Context { + /// The result type for this context, used for async contexts that + /// can't hold a direct reference to the application context. type Result; + /// Create a new model in the app context. fn new_model( &mut self, build_model: impl FnOnce(&mut ModelContext<'_, T>) -> T, ) -> Self::Result>; + /// Update a model in the app context. fn update_model( &mut self, handle: &Model, @@ -104,6 +135,7 @@ pub trait Context { where T: 'static; + /// Read a model from the app context. fn read_model( &self, handle: &Model, @@ -112,10 +144,12 @@ pub trait Context { where T: 'static; + /// Update a window for the given handle. fn update_window(&mut self, window: AnyWindowHandle, f: F) -> Result where F: FnOnce(AnyView, &mut WindowContext<'_>) -> T; + /// Read a window off of the application context. fn read_window( &self, window: &WindowHandle, @@ -125,7 +159,10 @@ pub trait Context { T: 'static; } +/// This trait is used for the different visual contexts in GPUI that +/// require a window to be present. pub trait VisualContext: Context { + /// Construct a new view in the window referenced by this context. fn new_view( &mut self, build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V, @@ -133,12 +170,14 @@ pub trait VisualContext: Context { where V: 'static + Render; + /// Update a view with the given callback fn update_view( &mut self, view: &View, update: impl FnOnce(&mut V, &mut ViewContext<'_, V>) -> R, ) -> Self::Result; + /// Replace the root view of a window with a new view. fn replace_root_view( &mut self, build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V, @@ -146,38 +185,42 @@ pub trait VisualContext: Context { where V: 'static + Render; + /// Focus a view in the window, if it implements the [`FocusableView`] trait. fn focus_view(&mut self, view: &View) -> Self::Result<()> where V: FocusableView; + /// Dismiss a view in the window, if it implements the [`ManagedView`] trait. fn dismiss_view(&mut self, view: &View) -> Self::Result<()> where V: ManagedView; } +/// A trait that allows models and views to be interchangeable in certain operations pub trait Entity: Sealed { + /// The weak reference type for this entity. type Weak: 'static; + /// The ID for this entity fn entity_id(&self) -> EntityId; + + /// Downgrade this entity to a weak reference. fn downgrade(&self) -> Self::Weak; + + /// Upgrade this entity from a weak reference. fn upgrade_from(weak: &Self::Weak) -> Option where Self: Sized; } +/// A trait for tying together the types of a GPUI entity and the events it can +/// emit. pub trait EventEmitter: 'static {} -pub enum GlobalKey { - Numeric(usize), - View(EntityId), - Type(TypeId), -} - +/// A helper trait for auto-implementing certain methods on contexts that +/// can be used interchangeably. pub trait BorrowAppContext { - fn with_text_style(&mut self, style: Option, f: F) -> R - where - F: FnOnce(&mut Self) -> R; - + /// Set a global value on the context. fn set_global(&mut self, global: T); } @@ -185,26 +228,14 @@ impl BorrowAppContext for C where C: BorrowMut, { - fn with_text_style(&mut self, style: Option, f: F) -> R - where - F: FnOnce(&mut Self) -> R, - { - if let Some(style) = style { - self.borrow_mut().push_text_style(style); - let result = f(self); - self.borrow_mut().pop_text_style(); - result - } else { - f(self) - } - } - fn set_global(&mut self, global: G) { self.borrow_mut().set_global(global) } } +/// A flatten equivalent for anyhow `Result`s. pub trait Flatten { + /// Convert this type into a simple `Result`. fn flatten(self) -> Result; } diff --git a/crates/gpui/src/image_cache.rs b/crates/gpui/src/image_cache.rs index 0d6ec81557aa7b21165a628cf707f0d1caded9d0..dd7b7b571e43c20609e6920f0da59093b5aa010f 100644 --- a/crates/gpui/src/image_cache.rs +++ b/crates/gpui/src/image_cache.rs @@ -11,12 +11,12 @@ use thiserror::Error; use util::http::{self, HttpClient}; #[derive(PartialEq, Eq, Hash, Clone)] -pub struct RenderImageParams { +pub(crate) struct RenderImageParams { pub(crate) image_id: ImageId, } #[derive(Debug, Error, Clone)] -pub enum Error { +pub(crate) enum Error { #[error("http error: {0}")] Client(#[from] http::Error), #[error("IO error: {0}")] @@ -42,7 +42,7 @@ impl From for Error { } } -pub struct ImageCache { +pub(crate) struct ImageCache { client: Arc, images: Arc>>, } diff --git a/crates/gpui/src/input.rs b/crates/gpui/src/input.rs index 7290b48abd7157cdbf11ffa74e6eed294bcc271e..440d6d17fdf74492e9190f161ce1bd3730884002 100644 --- a/crates/gpui/src/input.rs +++ b/crates/gpui/src/input.rs @@ -1,24 +1,35 @@ -use crate::{ - AsyncWindowContext, Bounds, Pixels, PlatformInputHandler, View, ViewContext, WindowContext, -}; +use crate::{Bounds, InputHandler, Pixels, View, ViewContext, WindowContext}; use std::ops::Range; /// Implement this trait to allow views to handle textual input when implementing an editor, field, etc. /// -/// Once your view `V` implements this trait, you can use it to construct an [`ElementInputHandler`]. +/// Once your view implements this trait, you can use it to construct an [`ElementInputHandler`]. /// This input handler can then be assigned during paint by calling [`WindowContext::handle_input`]. -pub trait InputHandler: 'static + Sized { +/// +/// See [`InputHandler`] for details on how to implement each method. +pub trait ViewInputHandler: 'static + Sized { + /// See [`InputHandler::text_for_range`] for details fn text_for_range(&mut self, range: Range, cx: &mut ViewContext) -> Option; + + /// See [`InputHandler::selected_text_range`] for details fn selected_text_range(&mut self, cx: &mut ViewContext) -> Option>; + + /// See [`InputHandler::marked_text_range`] for details fn marked_text_range(&self, cx: &mut ViewContext) -> Option>; + + /// See [`InputHandler::unmark_text`] for details fn unmark_text(&mut self, cx: &mut ViewContext); + + /// See [`InputHandler::replace_text_in_range`] for details fn replace_text_in_range( &mut self, range: Option>, text: &str, cx: &mut ViewContext, ); + + /// See [`InputHandler::replace_and_mark_text_in_range`] for details fn replace_and_mark_text_in_range( &mut self, range: Option>, @@ -26,6 +37,8 @@ pub trait InputHandler: 'static + Sized { new_selected_range: Option>, cx: &mut ViewContext, ); + + /// See [`InputHandler::bounds_for_range`] for details fn bounds_for_range( &mut self, range_utf16: Range, @@ -39,7 +52,6 @@ pub trait InputHandler: 'static + Sized { pub struct ElementInputHandler { view: View, element_bounds: Bounds, - cx: AsyncWindowContext, } impl ElementInputHandler { @@ -47,45 +59,42 @@ impl ElementInputHandler { /// containing view. /// /// [element_paint]: crate::Element::paint - pub fn new(element_bounds: Bounds, view: View, cx: &mut WindowContext) -> Self { + pub fn new(element_bounds: Bounds, view: View) -> Self { ElementInputHandler { view, element_bounds, - cx: cx.to_async(), } } } -impl PlatformInputHandler for ElementInputHandler { - fn selected_text_range(&mut self) -> Option> { +impl InputHandler for ElementInputHandler { + fn selected_text_range(&mut self, cx: &mut WindowContext) -> Option> { self.view - .update(&mut self.cx, |view, cx| view.selected_text_range(cx)) - .ok() - .flatten() + .update(cx, |view, cx| view.selected_text_range(cx)) } - fn marked_text_range(&mut self) -> Option> { - self.view - .update(&mut self.cx, |view, cx| view.marked_text_range(cx)) - .ok() - .flatten() + fn marked_text_range(&mut self, cx: &mut WindowContext) -> Option> { + self.view.update(cx, |view, cx| view.marked_text_range(cx)) } - fn text_for_range(&mut self, range_utf16: Range) -> Option { + fn text_for_range( + &mut self, + range_utf16: Range, + cx: &mut WindowContext, + ) -> Option { self.view - .update(&mut self.cx, |view, cx| { - view.text_for_range(range_utf16, cx) - }) - .ok() - .flatten() + .update(cx, |view, cx| view.text_for_range(range_utf16, cx)) } - fn replace_text_in_range(&mut self, replacement_range: Option>, text: &str) { - self.view - .update(&mut self.cx, |view, cx| { - view.replace_text_in_range(replacement_range, text, cx) - }) - .ok(); + fn replace_text_in_range( + &mut self, + replacement_range: Option>, + text: &str, + cx: &mut WindowContext, + ) { + self.view.update(cx, |view, cx| { + view.replace_text_in_range(replacement_range, text, cx) + }); } fn replace_and_mark_text_in_range( @@ -93,26 +102,24 @@ impl PlatformInputHandler for ElementInputHandler { range_utf16: Option>, new_text: &str, new_selected_range: Option>, + cx: &mut WindowContext, ) { - self.view - .update(&mut self.cx, |view, cx| { - view.replace_and_mark_text_in_range(range_utf16, new_text, new_selected_range, cx) - }) - .ok(); + self.view.update(cx, |view, cx| { + view.replace_and_mark_text_in_range(range_utf16, new_text, new_selected_range, cx) + }); } - fn unmark_text(&mut self) { - self.view - .update(&mut self.cx, |view, cx| view.unmark_text(cx)) - .ok(); + fn unmark_text(&mut self, cx: &mut WindowContext) { + self.view.update(cx, |view, cx| view.unmark_text(cx)); } - fn bounds_for_range(&mut self, range_utf16: Range) -> Option> { - self.view - .update(&mut self.cx, |view, cx| { - view.bounds_for_range(range_utf16, self.element_bounds, cx) - }) - .ok() - .flatten() + fn bounds_for_range( + &mut self, + range_utf16: Range, + cx: &mut WindowContext, + ) -> Option> { + self.view.update(cx, |view, cx| { + view.bounds_for_range(range_utf16, self.element_bounds, cx) + }) } } diff --git a/crates/gpui/src/interactive.rs b/crates/gpui/src/interactive.rs index 86e0a6378e29c290aa7e83ab712f2455997d1d4f..4faa92ce043ae32e6e795b296daeb9ad281475f1 100644 --- a/crates/gpui/src/interactive.rs +++ b/crates/gpui/src/interactive.rs @@ -4,15 +4,25 @@ use crate::{ use smallvec::SmallVec; use std::{any::Any, fmt::Debug, ops::Deref, path::PathBuf}; +/// An event from a platform input source. pub trait InputEvent: Sealed + 'static { + /// Convert this event into the platform input enum. fn to_platform_input(self) -> PlatformInput; } + +/// A key event from the platform. pub trait KeyEvent: InputEvent {} + +/// A mouse event from the platform. pub trait MouseEvent: InputEvent {} +/// The key down event equivalent for the platform. #[derive(Clone, Debug, Eq, PartialEq)] pub struct KeyDownEvent { + /// The keystroke that was generated. pub keystroke: Keystroke, + + /// Whether the key is currently held down. pub is_held: bool, } @@ -24,8 +34,10 @@ impl InputEvent for KeyDownEvent { } impl KeyEvent for KeyDownEvent {} +/// The key up event equivalent for the platform. #[derive(Clone, Debug)] pub struct KeyUpEvent { + /// The keystroke that was released. pub keystroke: Keystroke, } @@ -37,8 +49,10 @@ impl InputEvent for KeyUpEvent { } impl KeyEvent for KeyUpEvent {} +/// The modifiers changed event equivalent for the platform. #[derive(Clone, Debug, Default)] pub struct ModifiersChangedEvent { + /// The new state of the modifier keys pub modifiers: Modifiers, } @@ -62,17 +76,28 @@ impl Deref for ModifiersChangedEvent { /// Based on the winit enum of the same name. #[derive(Clone, Copy, Debug, Default)] pub enum TouchPhase { + /// The touch started. Started, + /// The touch event is moving. #[default] Moved, + /// The touch phase has ended Ended, } +/// A mouse down event from the platform #[derive(Clone, Debug, Default)] pub struct MouseDownEvent { + /// Which mouse button was pressed. pub button: MouseButton, + + /// The position of the mouse on the window. pub position: Point, + + /// The modifiers that were held down when the mouse was pressed. pub modifiers: Modifiers, + + /// The number of times the button has been clicked. pub click_count: usize, } @@ -84,11 +109,19 @@ impl InputEvent for MouseDownEvent { } impl MouseEvent for MouseDownEvent {} +/// A mouse up event from the platform #[derive(Clone, Debug, Default)] pub struct MouseUpEvent { + /// Which mouse button was released. pub button: MouseButton, + + /// The position of the mouse on the window. pub position: Point, + + /// The modifiers that were held down when the mouse was released. pub modifiers: Modifiers, + + /// The number of times the button has been clicked. pub click_count: usize, } @@ -100,21 +133,34 @@ impl InputEvent for MouseUpEvent { } impl MouseEvent for MouseUpEvent {} +/// A click event, generated when a mouse button is pressed and released. #[derive(Clone, Debug, Default)] pub struct ClickEvent { + /// The mouse event when the button was pressed. pub down: MouseDownEvent, + + /// The mouse event when the button was released. pub up: MouseUpEvent, } +/// An enum representing the mouse button that was pressed. #[derive(Hash, PartialEq, Eq, Copy, Clone, Debug)] pub enum MouseButton { + /// The left mouse button. Left, + + /// The right mouse button. Right, + + /// The middle mouse button. Middle, + + /// A navigation button, such as back or forward. Navigate(NavigationDirection), } impl MouseButton { + /// Get all the mouse buttons in a list. pub fn all() -> Vec { vec![ MouseButton::Left, @@ -132,9 +178,13 @@ impl Default for MouseButton { } } +/// A navigation direction, such as back or forward. #[derive(Hash, PartialEq, Eq, Copy, Clone, Debug)] pub enum NavigationDirection { + /// The back button. Back, + + /// The forward button. Forward, } @@ -144,10 +194,16 @@ impl Default for NavigationDirection { } } +/// A mouse move event from the platform #[derive(Clone, Debug, Default)] pub struct MouseMoveEvent { + /// The position of the mouse on the window. pub position: Point, + + /// The mouse button that was pressed, if any. pub pressed_button: Option, + + /// The modifiers that were held down when the mouse was moved. pub modifiers: Modifiers, } @@ -160,16 +216,25 @@ impl InputEvent for MouseMoveEvent { impl MouseEvent for MouseMoveEvent {} impl MouseMoveEvent { + /// Returns true if the left mouse button is currently held down. pub fn dragging(&self) -> bool { self.pressed_button == Some(MouseButton::Left) } } +/// A mouse wheel event from the platform #[derive(Clone, Debug, Default)] pub struct ScrollWheelEvent { + /// The position of the mouse on the window. pub position: Point, + + /// The change in scroll wheel position for this event. pub delta: ScrollDelta, + + /// The modifiers that were held down when the mouse was moved. pub modifiers: Modifiers, + + /// The phase of the touch event. pub touch_phase: TouchPhase, } @@ -189,9 +254,12 @@ impl Deref for ScrollWheelEvent { } } +/// The scroll delta for a scroll wheel event. #[derive(Clone, Copy, Debug)] pub enum ScrollDelta { + /// An exact scroll delta in pixels. Pixels(Point), + /// An inexact scroll delta in lines. Lines(Point), } @@ -202,6 +270,7 @@ impl Default for ScrollDelta { } impl ScrollDelta { + /// Returns true if this is a precise scroll delta in pixels. pub fn precise(&self) -> bool { match self { ScrollDelta::Pixels(_) => true, @@ -209,6 +278,7 @@ impl ScrollDelta { } } + /// Converts this scroll event into exact pixels. pub fn pixel_delta(&self, line_height: Pixels) -> Point { match self { ScrollDelta::Pixels(delta) => *delta, @@ -216,6 +286,7 @@ impl ScrollDelta { } } + /// Combines two scroll deltas into one. pub fn coalesce(self, other: ScrollDelta) -> ScrollDelta { match (self, other) { (ScrollDelta::Pixels(px_a), ScrollDelta::Pixels(px_b)) => { @@ -231,10 +302,15 @@ impl ScrollDelta { } } +/// A mouse exit event from the platform, generated when the mouse leaves the window. +/// The position generated should be just outside of the window's bounds. #[derive(Clone, Debug, Default)] pub struct MouseExitEvent { + /// The position of the mouse relative to the window. pub position: Point, + /// The mouse button that was pressed, if any. pub pressed_button: Option, + /// The modifiers that were held down when the mouse was moved. pub modifiers: Modifiers, } @@ -254,10 +330,12 @@ impl Deref for MouseExitEvent { } } +/// A collection of paths from the platform, such as from a file drop. #[derive(Debug, Clone, Default)] pub struct ExternalPaths(pub(crate) SmallVec<[PathBuf; 2]>); impl ExternalPaths { + /// Convert this collection of paths into a slice. pub fn paths(&self) -> &[PathBuf] { &self.0 } @@ -269,18 +347,27 @@ impl Render for ExternalPaths { } } +/// A file drop event from the platform, generated when files are dragged and dropped onto the window. #[derive(Debug, Clone)] pub enum FileDropEvent { + /// The files have entered the window. Entered { + /// The position of the mouse relative to the window. position: Point, + /// The paths of the files that are being dragged. paths: ExternalPaths, }, + /// The files are being dragged over the window Pending { + /// The position of the mouse relative to the window. position: Point, }, + /// The files have been dropped onto the window. Submit { + /// The position of the mouse relative to the window. position: Point, }, + /// The user has stopped dragging the files over the window. Exited, } @@ -292,40 +379,31 @@ impl InputEvent for FileDropEvent { } impl MouseEvent for FileDropEvent {} +/// An enum corresponding to all kinds of platform input events. #[derive(Clone, Debug)] pub enum PlatformInput { + /// A key was pressed. KeyDown(KeyDownEvent), + /// A key was released. KeyUp(KeyUpEvent), + /// The keyboard modifiers were changed. ModifiersChanged(ModifiersChangedEvent), + /// The mouse was pressed. MouseDown(MouseDownEvent), + /// The mouse was released. MouseUp(MouseUpEvent), + /// The mouse was moved. MouseMove(MouseMoveEvent), + /// The mouse exited the window. MouseExited(MouseExitEvent), + /// The scroll wheel was used. ScrollWheel(ScrollWheelEvent), + /// Files were dragged and dropped onto the window. FileDrop(FileDropEvent), } impl PlatformInput { - pub fn position(&self) -> Option> { - match self { - PlatformInput::KeyDown { .. } => None, - PlatformInput::KeyUp { .. } => None, - PlatformInput::ModifiersChanged { .. } => None, - PlatformInput::MouseDown(event) => Some(event.position), - PlatformInput::MouseUp(event) => Some(event.position), - PlatformInput::MouseMove(event) => Some(event.position), - PlatformInput::MouseExited(event) => Some(event.position), - PlatformInput::ScrollWheel(event) => Some(event.position), - PlatformInput::FileDrop(FileDropEvent::Exited) => None, - PlatformInput::FileDrop( - FileDropEvent::Entered { position, .. } - | FileDropEvent::Pending { position, .. } - | FileDropEvent::Submit { position, .. }, - ) => Some(*position), - } - } - - pub fn mouse_event(&self) -> Option<&dyn Any> { + pub(crate) fn mouse_event(&self) -> Option<&dyn Any> { match self { PlatformInput::KeyDown { .. } => None, PlatformInput::KeyUp { .. } => None, @@ -339,7 +417,7 @@ impl PlatformInput { } } - pub fn keyboard_event(&self) -> Option<&dyn Any> { + pub(crate) fn keyboard_event(&self) -> Option<&dyn Any> { match self { PlatformInput::KeyDown(event) => Some(event), PlatformInput::KeyUp(event) => Some(event), diff --git a/crates/gpui/src/key_dispatch.rs b/crates/gpui/src/key_dispatch.rs index 85a67168e5e1d89588b6d58088a8f381a195e0eb..dd5a7ab84e25e267012de5e9dfb216404b8dfa7d 100644 --- a/crates/gpui/src/key_dispatch.rs +++ b/crates/gpui/src/key_dispatch.rs @@ -1,6 +1,57 @@ +/// KeyDispatch is where GPUI deals with binding actions to key events. +/// +/// The key pieces to making a key binding work are to define an action, +/// implement a method that takes that action as a type parameter, +/// and then to register the action during render on a focused node +/// with a keymap context: +/// +/// ```rust +/// actions!(editor,[Undo, Redo]);; +/// +/// impl Editor { +/// fn undo(&mut self, _: &Undo, _cx: &mut ViewContext) { ... } +/// fn redo(&mut self, _: &Redo, _cx: &mut ViewContext) { ... } +/// } +/// +/// impl Render for Editor { +/// fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { +/// div() +/// .track_focus(&self.focus_handle) +/// .keymap_context("Editor") +/// .on_action(cx.listener(Editor::undo)) +/// .on_action(cx.listener(Editor::redo)) +/// ... +/// } +/// } +///``` +/// +/// The keybindings themselves are managed independently by calling cx.bind_keys(). +/// (Though mostly when developing Zed itself, you just need to add a new line to +/// assets/keymaps/default.json). +/// +/// ```rust +/// cx.bind_keys([ +/// KeyBinding::new("cmd-z", Editor::undo, Some("Editor")), +/// KeyBinding::new("cmd-shift-z", Editor::redo, Some("Editor")), +/// ]) +/// ``` +/// +/// With all of this in place, GPUI will ensure that if you have an Editor that contains +/// the focus, hitting cmd-z will Undo. +/// +/// In real apps, it is a little more complicated than this, because typically you have +/// several nested views that each register keyboard handlers. In this case action matching +/// bubbles up from the bottom. For example in Zed, the Workspace is the top-level view, which contains Pane's, which contain Editors. If there are conflicting keybindings defined +/// then the Editor's bindings take precedence over the Pane's bindings, which take precedence over the Workspace. +/// +/// In GPUI, keybindings are not limited to just single keystrokes, you can define +/// sequences by separating the keys with a space: +/// +/// KeyBinding::new("cmd-k left", pane::SplitLeft, Some("Pane")) +/// use crate::{ - Action, ActionRegistry, DispatchPhase, EntityId, FocusId, KeyBinding, KeyContext, KeyMatch, - Keymap, Keystroke, KeystrokeMatcher, WindowContext, + Action, ActionRegistry, DispatchPhase, ElementContext, EntityId, FocusId, KeyBinding, + KeyContext, Keymap, KeymatchResult, Keystroke, KeystrokeMatcher, WindowContext, }; use collections::FxHashMap; use parking_lot::Mutex; @@ -13,7 +64,7 @@ use std::{ }; #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] -pub struct DispatchNodeId(usize); +pub(crate) struct DispatchNodeId(usize); pub(crate) struct DispatchTree { node_stack: Vec, @@ -36,7 +87,7 @@ pub(crate) struct DispatchNode { parent: Option, } -type KeyListener = Rc; +type KeyListener = Rc; #[derive(Clone)] pub(crate) struct DispatchActionListener { @@ -272,30 +323,51 @@ impl DispatchTree { .collect() } + // dispatch_key pushses the next keystroke into any key binding matchers. + // any matching bindings are returned in the order that they should be dispatched: + // * First by length of binding (so if you have a binding for "b" and "ab", the "ab" binding fires first) + // * Secondly by depth in the tree (so if Editor has a binding for "b" and workspace a + // binding for "b", the Editor action fires first). pub fn dispatch_key( &mut self, keystroke: &Keystroke, - context: &[KeyContext], - ) -> Vec> { - if !self.keystroke_matchers.contains_key(context) { - let keystroke_contexts = context.iter().cloned().collect(); - self.keystroke_matchers.insert( - keystroke_contexts, - KeystrokeMatcher::new(self.keymap.clone()), - ); - } + dispatch_path: &SmallVec<[DispatchNodeId; 32]>, + ) -> KeymatchResult { + let mut bindings = SmallVec::<[KeyBinding; 1]>::new(); + let mut pending = false; + + let mut context_stack: SmallVec<[KeyContext; 4]> = SmallVec::new(); + for node_id in dispatch_path { + let node = self.node(*node_id); - let keystroke_matcher = self.keystroke_matchers.get_mut(context).unwrap(); - if let KeyMatch::Some(actions) = keystroke_matcher.match_keystroke(keystroke, context) { - // Clear all pending keystrokes when an action has been found. - for keystroke_matcher in self.keystroke_matchers.values_mut() { - keystroke_matcher.clear_pending(); + if let Some(context) = node.context.clone() { + context_stack.push(context); } + } - actions - } else { - vec![] + while !context_stack.is_empty() { + let keystroke_matcher = self + .keystroke_matchers + .entry(context_stack.clone()) + .or_insert_with(|| KeystrokeMatcher::new(self.keymap.clone())); + + let result = keystroke_matcher.match_keystroke(keystroke, &context_stack); + pending = result.pending || pending; + for new_binding in result.bindings { + match bindings + .iter() + .position(|el| el.keystrokes.len() < new_binding.keystrokes.len()) + { + Some(idx) => { + bindings.insert(idx, new_binding); + } + None => bindings.push(new_binding), + } + } + context_stack.pop(); } + + KeymatchResult { bindings, pending } } pub fn has_pending_keystrokes(&self) -> bool { diff --git a/crates/gpui/src/keymap/binding.rs b/crates/gpui/src/keymap/binding.rs index 766e54f4734c0075e40a4ff4699a1735eb483c80..221f5d09f411747f0ddd805c4cc17b2dcfe5c632 100644 --- a/crates/gpui/src/keymap/binding.rs +++ b/crates/gpui/src/keymap/binding.rs @@ -2,6 +2,7 @@ use crate::{Action, KeyBindingContextPredicate, KeyMatch, Keystroke}; use anyhow::Result; use smallvec::SmallVec; +/// A keybinding and it's associated metadata, from the keymap. pub struct KeyBinding { pub(crate) action: Box, pub(crate) keystrokes: SmallVec<[Keystroke; 2]>, @@ -19,10 +20,12 @@ impl Clone for KeyBinding { } impl KeyBinding { + /// Construct a new keybinding from the given data. pub fn new(keystrokes: &str, action: A, context_predicate: Option<&str>) -> Self { Self::load(keystrokes, Box::new(action), context_predicate).unwrap() } + /// Load a keybinding from the given raw data. pub fn load(keystrokes: &str, action: Box, context: Option<&str>) -> Result { let context = if let Some(context) = context { Some(KeyBindingContextPredicate::parse(context)?) @@ -42,11 +45,12 @@ impl KeyBinding { }) } + /// Check if the given keystrokes match this binding. pub fn match_keystrokes(&self, pending_keystrokes: &[Keystroke]) -> KeyMatch { if self.keystrokes.as_ref().starts_with(pending_keystrokes) { // If the binding is completed, push it onto the matches list if self.keystrokes.as_ref().len() == pending_keystrokes.len() { - KeyMatch::Some(vec![self.action.boxed_clone()]) + KeyMatch::Matched } else { KeyMatch::Pending } @@ -55,10 +59,12 @@ impl KeyBinding { } } + /// Get the keystrokes associated with this binding pub fn keystrokes(&self) -> &[Keystroke] { self.keystrokes.as_slice() } + /// Get the action associated with this binding pub fn action(&self) -> &dyn Action { self.action.as_ref() } diff --git a/crates/gpui/src/keymap/context.rs b/crates/gpui/src/keymap/context.rs index bfa97b7afe4e33706da3b35ef099341c219f80fb..05ef67ba2bc9d260215fc0df89a8a6e3039450f5 100644 --- a/crates/gpui/src/keymap/context.rs +++ b/crates/gpui/src/keymap/context.rs @@ -3,6 +3,10 @@ use anyhow::{anyhow, Result}; use smallvec::SmallVec; use std::fmt; +/// A datastructure for resolving whether an action should be dispatched +/// at this point in the element tree. Contains a set of identifiers +/// and/or key value pairs representing the current context for the +/// keymap. #[derive(Clone, Default, Eq, PartialEq, Hash)] pub struct KeyContext(SmallVec<[ContextEntry; 1]>); @@ -21,6 +25,11 @@ impl<'a> TryFrom<&'a str> for KeyContext { } impl KeyContext { + /// Parse a key context from a string. + /// The key context format is very simple: + /// - either a single identifier, such as `StatusBar` + /// - or a key value pair, such as `mode = visible` + /// - separated by whitespace, such as `StatusBar mode = visible` pub fn parse(source: &str) -> Result { let mut context = Self::default(); let source = skip_whitespace(source); @@ -53,14 +62,17 @@ impl KeyContext { Self::parse_expr(source, context) } + /// Check if this context is empty. pub fn is_empty(&self) -> bool { self.0.is_empty() } + /// Clear this context. pub fn clear(&mut self) { self.0.clear(); } + /// Extend this context with another context. pub fn extend(&mut self, other: &Self) { for entry in &other.0 { if !self.contains(&entry.key) { @@ -69,6 +81,7 @@ impl KeyContext { } } + /// Add an identifier to this context, if it's not already in this context. pub fn add>(&mut self, identifier: I) { let key = identifier.into(); @@ -77,6 +90,7 @@ impl KeyContext { } } + /// Set a key value pair in this context, if it's not already set. pub fn set, S2: Into>(&mut self, key: S1, value: S2) { let key = key.into(); if !self.contains(&key) { @@ -87,10 +101,12 @@ impl KeyContext { } } + /// Check if this context contains a given identifier or key. pub fn contains(&self, key: &str) -> bool { self.0.iter().any(|entry| entry.key.as_ref() == key) } + /// Get the associated value for a given identifier or key. pub fn get(&self, key: &str) -> Option<&SharedString> { self.0 .iter() @@ -117,20 +133,31 @@ impl fmt::Debug for KeyContext { } } +/// A datastructure for resolving whether an action should be dispatched +/// Representing a small language for describing which contexts correspond +/// to which actions. #[derive(Clone, Debug, Eq, PartialEq, Hash)] pub enum KeyBindingContextPredicate { + /// A predicate that will match a given identifier. Identifier(SharedString), + /// A predicate that will match a given key-value pair. Equal(SharedString, SharedString), + /// A predicate that will match a given key-value pair not being present. NotEqual(SharedString, SharedString), + /// A predicate that will match a given predicate appearing below another predicate. + /// in the element tree Child( Box, Box, ), + /// Predicate that will invert another predicate. Not(Box), + /// A predicate that will match if both of its children match. And( Box, Box, ), + /// A predicate that will match if either of its children match. Or( Box, Box, @@ -138,6 +165,34 @@ pub enum KeyBindingContextPredicate { } impl KeyBindingContextPredicate { + /// Parse a string in the same format as the keymap's context field. + /// + /// A basic equivalence check against a set of identifiers can performed by + /// simply writing a string: + /// + /// `StatusBar` -> A predicate that will match a context with the identifier `StatusBar` + /// + /// You can also specify a key-value pair: + /// + /// `mode == visible` -> A predicate that will match a context with the key `mode` + /// with the value `visible` + /// + /// And a logical operations combining these two checks: + /// + /// `StatusBar && mode == visible` -> A predicate that will match a context with the + /// identifier `StatusBar` and the key `mode` + /// with the value `visible` + /// + /// + /// There is also a special child `>` operator that will match a predicate that is + /// below another predicate: + /// + /// `StatusBar > mode == visible` -> A predicate that will match a context identifier `StatusBar` + /// and a child context that has the key `mode` with the + /// value `visible` + /// + /// This syntax supports `!=`, `||` and `&&` as logical operators. + /// You can also preface an operation or check with a `!` to negate it. pub fn parse(source: &str) -> Result { let source = skip_whitespace(source); let (predicate, rest) = Self::parse_expr(source, 0)?; @@ -148,6 +203,7 @@ impl KeyBindingContextPredicate { } } + /// Eval a predicate against a set of contexts, arranged from lowest to highest. pub fn eval(&self, contexts: &[KeyContext]) -> bool { let Some(context) = contexts.last() else { return false; diff --git a/crates/gpui/src/keymap/keymap.rs b/crates/gpui/src/keymap/keymap.rs index 8c74e12e08e8e67e9e0784521b6df164ec25044b..2eefbb841ee5be0b499e7b32aaf28aa7da36a072 100644 --- a/crates/gpui/src/keymap/keymap.rs +++ b/crates/gpui/src/keymap/keymap.rs @@ -6,9 +6,12 @@ use std::{ collections::HashMap, }; +/// An opaque identifier of which version of the keymap is currently active. +/// The keymap's version is changed whenever bindings are added or removed. #[derive(Copy, Clone, Eq, PartialEq, Default)] pub struct KeymapVersion(usize); +/// A collection of key bindings for the user's application. #[derive(Default)] pub struct Keymap { bindings: Vec, @@ -19,16 +22,19 @@ pub struct Keymap { } impl Keymap { + /// Create a new keymap with the given bindings. pub fn new(bindings: Vec) -> Self { let mut this = Self::default(); this.add_bindings(bindings); this } + /// Get the current version of the keymap. pub fn version(&self) -> KeymapVersion { self.version } + /// Add more bindings to the keymap. pub fn add_bindings>(&mut self, bindings: T) { let no_action_id = (NoAction {}).type_id(); @@ -51,6 +57,7 @@ impl Keymap { self.version.0 += 1; } + /// Reset this keymap to its initial state. pub fn clear(&mut self) { self.bindings.clear(); self.binding_indices_by_action_id.clear(); @@ -77,6 +84,7 @@ impl Keymap { .filter(move |binding| binding.action().partial_eq(action)) } + /// Check if the given binding is enabled, given a certain key context. pub fn binding_enabled(&self, binding: &KeyBinding, context: &[KeyContext]) -> bool { // If binding has a context predicate, it must match the current context, if let Some(predicate) = &binding.context_predicate { diff --git a/crates/gpui/src/keymap/matcher.rs b/crates/gpui/src/keymap/matcher.rs index 934becb0d1520ae747845f94d22d066cac6fc59f..09ba281a0d7ebc1e032100eb9463f32767afa403 100644 --- a/crates/gpui/src/keymap/matcher.rs +++ b/crates/gpui/src/keymap/matcher.rs @@ -1,13 +1,19 @@ -use crate::{Action, KeyContext, Keymap, KeymapVersion, Keystroke}; +use crate::{KeyBinding, KeyContext, Keymap, KeymapVersion, Keystroke}; use parking_lot::Mutex; +use smallvec::SmallVec; use std::sync::Arc; -pub struct KeystrokeMatcher { +pub(crate) struct KeystrokeMatcher { pending_keystrokes: Vec, keymap: Arc>, keymap_version: KeymapVersion, } +pub struct KeymatchResult { + pub bindings: SmallVec<[KeyBinding; 1]>, + pub pending: bool, +} + impl KeystrokeMatcher { pub fn new(keymap: Arc>) -> Self { let keymap_version = keymap.lock().version(); @@ -18,10 +24,6 @@ impl KeystrokeMatcher { } } - pub fn clear_pending(&mut self) { - self.pending_keystrokes.clear(); - } - pub fn has_pending_keystrokes(&self) -> bool { !self.pending_keystrokes.is_empty() } @@ -35,11 +37,11 @@ impl KeystrokeMatcher { /// - KeyMatch::Complete(matches) => /// One or more bindings have received the necessary key presses. /// Bindings added later will take precedence over earlier bindings. - pub fn match_keystroke( + pub(crate) fn match_keystroke( &mut self, keystroke: &Keystroke, context_stack: &[KeyContext], - ) -> KeyMatch { + ) -> KeymatchResult { let keymap = self.keymap.lock(); // Clear pending keystrokes if the keymap has changed since the last matched keystroke. if keymap.version() != self.keymap_version { @@ -48,7 +50,7 @@ impl KeystrokeMatcher { } let mut pending_key = None; - let mut found_actions = Vec::new(); + let mut bindings = SmallVec::new(); for binding in keymap.bindings().rev() { if !keymap.binding_enabled(binding, context_stack) { @@ -58,8 +60,8 @@ impl KeystrokeMatcher { for candidate in keystroke.match_candidates() { self.pending_keystrokes.push(candidate.clone()); match binding.match_keystrokes(&self.pending_keystrokes) { - KeyMatch::Some(mut actions) => { - found_actions.append(&mut actions); + KeyMatch::Matched => { + bindings.push(binding.clone()); } KeyMatch::Pending => { pending_key.get_or_insert(candidate); @@ -70,399 +72,31 @@ impl KeystrokeMatcher { } } - if !found_actions.is_empty() { - self.pending_keystrokes.clear(); - return KeyMatch::Some(found_actions); + if bindings.len() == 0 && pending_key.is_none() && self.pending_keystrokes.len() > 0 { + drop(keymap); + self.pending_keystrokes.remove(0); + return self.match_keystroke(keystroke, context_stack); } - if let Some(pending_key) = pending_key { + let pending = if let Some(pending_key) = pending_key { self.pending_keystrokes.push(pending_key); - KeyMatch::Pending + true } else { self.pending_keystrokes.clear(); - KeyMatch::None - } + false + }; + + KeymatchResult { bindings, pending } } } -#[derive(Debug)] +/// The result of matching a keystroke against a given keybinding. +/// - KeyMatch::None => No match is valid for this key given any pending keystrokes. +/// - KeyMatch::Pending => There exist bindings that is still waiting for more keys. +/// - KeyMatch::Some(matches) => One or more bindings have received the necessary key presses. +#[derive(Debug, PartialEq)] pub enum KeyMatch { None, Pending, - Some(Vec>), -} - -impl KeyMatch { - pub fn is_some(&self) -> bool { - matches!(self, KeyMatch::Some(_)) - } - - pub fn matches(self) -> Option>> { - match self { - KeyMatch::Some(matches) => Some(matches), - _ => None, - } - } -} - -impl PartialEq for KeyMatch { - fn eq(&self, other: &Self) -> bool { - match (self, other) { - (KeyMatch::None, KeyMatch::None) => true, - (KeyMatch::Pending, KeyMatch::Pending) => true, - (KeyMatch::Some(a), KeyMatch::Some(b)) => { - if a.len() != b.len() { - return false; - } - - for (a, b) in a.iter().zip(b.iter()) { - if !a.partial_eq(b.as_ref()) { - return false; - } - } - - true - } - _ => false, - } - } -} - -#[cfg(test)] -mod tests { - - use serde_derive::Deserialize; - - use super::*; - use crate::{self as gpui, KeyBindingContextPredicate, Modifiers}; - use crate::{actions, KeyBinding}; - - #[test] - fn test_keymap_and_view_ordering() { - actions!(test, [EditorAction, ProjectPanelAction]); - - let mut editor = KeyContext::default(); - editor.add("Editor"); - - let mut project_panel = KeyContext::default(); - project_panel.add("ProjectPanel"); - - // Editor 'deeper' in than project panel - let dispatch_path = vec![project_panel, editor]; - - // But editor actions 'higher' up in keymap - let keymap = Keymap::new(vec![ - KeyBinding::new("left", EditorAction, Some("Editor")), - KeyBinding::new("left", ProjectPanelAction, Some("ProjectPanel")), - ]); - - let mut matcher = KeystrokeMatcher::new(Arc::new(Mutex::new(keymap))); - - let matches = matcher - .match_keystroke(&Keystroke::parse("left").unwrap(), &dispatch_path) - .matches() - .unwrap(); - - assert!(matches[0].partial_eq(&EditorAction)); - assert!(matches.get(1).is_none()); - } - - #[test] - fn test_multi_keystroke_match() { - actions!(test, [B, AB, C, D, DA, E, EF]); - - let mut context1 = KeyContext::default(); - context1.add("1"); - - let mut context2 = KeyContext::default(); - context2.add("2"); - - let dispatch_path = vec![context2, context1]; - - let keymap = Keymap::new(vec![ - KeyBinding::new("a b", AB, Some("1")), - KeyBinding::new("b", B, Some("2")), - KeyBinding::new("c", C, Some("2")), - KeyBinding::new("d", D, Some("1")), - KeyBinding::new("d", D, Some("2")), - KeyBinding::new("d a", DA, Some("2")), - ]); - - let mut matcher = KeystrokeMatcher::new(Arc::new(Mutex::new(keymap))); - - // Binding with pending prefix always takes precedence - assert_eq!( - matcher.match_keystroke(&Keystroke::parse("a").unwrap(), &dispatch_path), - KeyMatch::Pending, - ); - // B alone doesn't match because a was pending, so AB is returned instead - assert_eq!( - matcher.match_keystroke(&Keystroke::parse("b").unwrap(), &dispatch_path), - KeyMatch::Some(vec![Box::new(AB)]), - ); - assert!(!matcher.has_pending_keystrokes()); - - // Without an a prefix, B is dispatched like expected - assert_eq!( - matcher.match_keystroke(&Keystroke::parse("b").unwrap(), &dispatch_path[0..1]), - KeyMatch::Some(vec![Box::new(B)]), - ); - assert!(!matcher.has_pending_keystrokes()); - - // If a is prefixed, C will not be dispatched because there - // was a pending binding for it - assert_eq!( - matcher.match_keystroke(&Keystroke::parse("a").unwrap(), &dispatch_path), - KeyMatch::Pending, - ); - assert_eq!( - matcher.match_keystroke(&Keystroke::parse("c").unwrap(), &dispatch_path), - KeyMatch::None, - ); - assert!(!matcher.has_pending_keystrokes()); - - // If a single keystroke matches multiple bindings in the tree - // only one of them is returned. - assert_eq!( - matcher.match_keystroke(&Keystroke::parse("d").unwrap(), &dispatch_path), - KeyMatch::Some(vec![Box::new(D)]), - ); - } - - #[test] - fn test_keystroke_parsing() { - assert_eq!( - Keystroke::parse("ctrl-p").unwrap(), - Keystroke { - key: "p".into(), - modifiers: Modifiers { - control: true, - alt: false, - shift: false, - command: false, - function: false, - }, - ime_key: None, - } - ); - - assert_eq!( - Keystroke::parse("alt-shift-down").unwrap(), - Keystroke { - key: "down".into(), - modifiers: Modifiers { - control: false, - alt: true, - shift: true, - command: false, - function: false, - }, - ime_key: None, - } - ); - - assert_eq!( - Keystroke::parse("shift-cmd--").unwrap(), - Keystroke { - key: "-".into(), - modifiers: Modifiers { - control: false, - alt: false, - shift: true, - command: true, - function: false, - }, - ime_key: None, - } - ); - } - - #[test] - fn test_context_predicate_parsing() { - use KeyBindingContextPredicate::*; - - assert_eq!( - KeyBindingContextPredicate::parse("a && (b == c || d != e)").unwrap(), - And( - Box::new(Identifier("a".into())), - Box::new(Or( - Box::new(Equal("b".into(), "c".into())), - Box::new(NotEqual("d".into(), "e".into())), - )) - ) - ); - - assert_eq!( - KeyBindingContextPredicate::parse("!a").unwrap(), - Not(Box::new(Identifier("a".into())),) - ); - } - - #[test] - fn test_context_predicate_eval() { - let predicate = KeyBindingContextPredicate::parse("a && b || c == d").unwrap(); - - let mut context = KeyContext::default(); - context.add("a"); - assert!(!predicate.eval(&[context])); - - let mut context = KeyContext::default(); - context.add("a"); - context.add("b"); - assert!(predicate.eval(&[context])); - - let mut context = KeyContext::default(); - context.add("a"); - context.set("c", "x"); - assert!(!predicate.eval(&[context])); - - let mut context = KeyContext::default(); - context.add("a"); - context.set("c", "d"); - assert!(predicate.eval(&[context])); - - let predicate = KeyBindingContextPredicate::parse("!a").unwrap(); - assert!(predicate.eval(&[KeyContext::default()])); - } - - #[test] - fn test_context_child_predicate_eval() { - let predicate = KeyBindingContextPredicate::parse("a && b > c").unwrap(); - let contexts = [ - context_set(&["a", "b"]), - context_set(&["c", "d"]), // match this context - context_set(&["e", "f"]), - ]; - - assert!(!predicate.eval(&contexts[..=0])); - assert!(predicate.eval(&contexts[..=1])); - assert!(!predicate.eval(&contexts[..=2])); - - let predicate = KeyBindingContextPredicate::parse("a && b > c && !d > e").unwrap(); - let contexts = [ - context_set(&["a", "b"]), - context_set(&["c", "d"]), - context_set(&["e"]), - context_set(&["a", "b"]), - context_set(&["c"]), - context_set(&["e"]), // only match this context - context_set(&["f"]), - ]; - - assert!(!predicate.eval(&contexts[..=0])); - assert!(!predicate.eval(&contexts[..=1])); - assert!(!predicate.eval(&contexts[..=2])); - assert!(!predicate.eval(&contexts[..=3])); - assert!(!predicate.eval(&contexts[..=4])); - assert!(predicate.eval(&contexts[..=5])); - assert!(!predicate.eval(&contexts[..=6])); - - fn context_set(names: &[&str]) -> KeyContext { - let mut keymap = KeyContext::default(); - names.iter().for_each(|name| keymap.add(name.to_string())); - keymap - } - } - - #[test] - fn test_matcher() { - #[derive(Clone, Deserialize, PartialEq, Eq, Debug)] - pub struct A(pub String); - impl_actions!(test, [A]); - actions!(test, [B, Ab, Dollar, Quote, Ess, Backtick]); - - #[derive(Clone, Debug, Eq, PartialEq)] - struct ActionArg { - a: &'static str, - } - - let keymap = Keymap::new(vec![ - KeyBinding::new("a", A("x".to_string()), Some("a")), - KeyBinding::new("b", B, Some("a")), - KeyBinding::new("a b", Ab, Some("a || b")), - KeyBinding::new("$", Dollar, Some("a")), - KeyBinding::new("\"", Quote, Some("a")), - KeyBinding::new("alt-s", Ess, Some("a")), - KeyBinding::new("ctrl-`", Backtick, Some("a")), - ]); - - let mut context_a = KeyContext::default(); - context_a.add("a"); - - let mut context_b = KeyContext::default(); - context_b.add("b"); - - let mut matcher = KeystrokeMatcher::new(Arc::new(Mutex::new(keymap))); - - // Basic match - assert_eq!( - matcher.match_keystroke(&Keystroke::parse("a").unwrap(), &[context_a.clone()]), - KeyMatch::Some(vec![Box::new(A("x".to_string()))]) - ); - matcher.clear_pending(); - - // Multi-keystroke match - assert_eq!( - matcher.match_keystroke(&Keystroke::parse("a").unwrap(), &[context_b.clone()]), - KeyMatch::Pending - ); - assert_eq!( - matcher.match_keystroke(&Keystroke::parse("b").unwrap(), &[context_b.clone()]), - KeyMatch::Some(vec![Box::new(Ab)]) - ); - matcher.clear_pending(); - - // Failed matches don't interfere with matching subsequent keys - assert_eq!( - matcher.match_keystroke(&Keystroke::parse("x").unwrap(), &[context_a.clone()]), - KeyMatch::None - ); - assert_eq!( - matcher.match_keystroke(&Keystroke::parse("a").unwrap(), &[context_a.clone()]), - KeyMatch::Some(vec![Box::new(A("x".to_string()))]) - ); - matcher.clear_pending(); - - let mut context_c = KeyContext::default(); - context_c.add("c"); - - assert_eq!( - matcher.match_keystroke( - &Keystroke::parse("a").unwrap(), - &[context_c.clone(), context_b.clone()] - ), - KeyMatch::Pending - ); - assert_eq!( - matcher.match_keystroke(&Keystroke::parse("b").unwrap(), &[context_b.clone()]), - KeyMatch::Some(vec![Box::new(Ab)]) - ); - - // handle Czech $ (option + 4 key) - assert_eq!( - matcher.match_keystroke(&Keystroke::parse("alt-ç->$").unwrap(), &[context_a.clone()]), - KeyMatch::Some(vec![Box::new(Dollar)]) - ); - - // handle Brazilian quote (quote key then space key) - assert_eq!( - matcher.match_keystroke( - &Keystroke::parse("space->\"").unwrap(), - &[context_a.clone()] - ), - KeyMatch::Some(vec![Box::new(Quote)]) - ); - - // handle ctrl+` on a brazilian keyboard - assert_eq!( - matcher.match_keystroke(&Keystroke::parse("ctrl-->`").unwrap(), &[context_a.clone()]), - KeyMatch::Some(vec![Box::new(Backtick)]) - ); - - // handle alt-s on a US keyboard - assert_eq!( - matcher.match_keystroke(&Keystroke::parse("alt-s->ß").unwrap(), &[context_a.clone()]), - KeyMatch::Some(vec![Box::new(Ess)]) - ); - } + Matched, } diff --git a/crates/gpui/src/keymap/mod.rs b/crates/gpui/src/keymap/mod.rs index 09e222c09552a80e7d187ec3303d1993d28979e8..6f1a018322b1a7f55349af70729860b9d85bde60 100644 --- a/crates/gpui/src/keymap/mod.rs +++ b/crates/gpui/src/keymap/mod.rs @@ -6,4 +6,4 @@ mod matcher; pub use binding::*; pub use context::*; pub use keymap::*; -pub use matcher::*; +pub(crate) use matcher::*; diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index e08d7a85521c9f24d4c25ab6decb1f3fb99d9882..e623742740259f62db547b6b2c4791cec15051ca 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -6,10 +6,10 @@ mod mac; mod test; use crate::{ - Action, AnyWindowHandle, BackgroundExecutor, Bounds, DevicePixels, Font, FontId, FontMetrics, - FontRun, ForegroundExecutor, GlobalPixels, GlyphId, Keymap, LineLayout, Pixels, PlatformInput, - Point, RenderGlyphParams, RenderImageParams, RenderSvgParams, Result, Scene, SharedString, - Size, TaskLabel, + Action, AnyWindowHandle, AsyncWindowContext, BackgroundExecutor, Bounds, DevicePixels, Font, + FontId, FontMetrics, FontRun, ForegroundExecutor, GlobalPixels, GlyphId, Keymap, LineLayout, + Pixels, PlatformInput, Point, RenderGlyphParams, RenderImageParams, RenderSvgParams, Result, + Scene, SharedString, Size, TaskLabel, WindowContext, }; use anyhow::anyhow; use async_task::Runnable; @@ -34,9 +34,9 @@ use uuid::Uuid; pub use app_menu::*; pub use keystroke::*; #[cfg(target_os = "macos")] -pub use mac::*; +pub(crate) use mac::*; #[cfg(any(test, feature = "test-support"))] -pub use test::*; +pub(crate) use test::*; use time::UtcOffset; #[cfg(target_os = "macos")] @@ -69,11 +69,10 @@ pub(crate) trait Platform: 'static { fn set_display_link_output_callback( &self, display_id: DisplayId, - callback: Box, + callback: Box, ); fn start_display_link(&self, display_id: DisplayId); fn stop_display_link(&self, display_id: DisplayId); - // fn add_status_item(&self, _handle: AnyWindowHandle) -> Box; fn open_url(&self, url: &str); fn on_open_urls(&self, callback: Box)>); @@ -149,8 +148,8 @@ pub(crate) trait PlatformWindow { fn mouse_position(&self) -> Point; fn modifiers(&self) -> Modifiers; fn as_any_mut(&mut self) -> &mut dyn Any; - fn set_input_handler(&mut self, input_handler: Box); - fn take_input_handler(&mut self) -> Option>; + fn set_input_handler(&mut self, input_handler: PlatformInputHandler); + fn take_input_handler(&mut self) -> Option; fn prompt(&self, level: PromptLevel, msg: &str, answers: &[&str]) -> oneshot::Receiver; fn activate(&self); fn set_title(&mut self, title: &str); @@ -325,30 +324,175 @@ impl From for etagere::AllocId { } } -pub trait PlatformInputHandler: 'static { - fn selected_text_range(&mut self) -> Option>; - fn marked_text_range(&mut self) -> Option>; - fn text_for_range(&mut self, range_utf16: Range) -> Option; - fn replace_text_in_range(&mut self, replacement_range: Option>, text: &str); +pub(crate) struct PlatformInputHandler { + cx: AsyncWindowContext, + handler: Box, +} + +impl PlatformInputHandler { + pub fn new(cx: AsyncWindowContext, handler: Box) -> Self { + Self { cx, handler } + } + + fn selected_text_range(&mut self) -> Option> { + self.cx + .update(|cx| self.handler.selected_text_range(cx)) + .ok() + .flatten() + } + + fn marked_text_range(&mut self) -> Option> { + self.cx + .update(|cx| self.handler.marked_text_range(cx)) + .ok() + .flatten() + } + + fn text_for_range(&mut self, range_utf16: Range) -> Option { + self.cx + .update(|cx| self.handler.text_for_range(range_utf16, cx)) + .ok() + .flatten() + } + + fn replace_text_in_range(&mut self, replacement_range: Option>, text: &str) { + self.cx + .update(|cx| { + self.handler + .replace_text_in_range(replacement_range, text, cx); + }) + .ok(); + } + + fn replace_and_mark_text_in_range( + &mut self, + range_utf16: Option>, + new_text: &str, + new_selected_range: Option>, + ) { + self.cx + .update(|cx| { + self.handler.replace_and_mark_text_in_range( + range_utf16, + new_text, + new_selected_range, + cx, + ) + }) + .ok(); + } + + fn unmark_text(&mut self) { + self.cx.update(|cx| self.handler.unmark_text(cx)).ok(); + } + + fn bounds_for_range(&mut self, range_utf16: Range) -> Option> { + self.cx + .update(|cx| self.handler.bounds_for_range(range_utf16, cx)) + .ok() + .flatten() + } + + pub(crate) fn flush_pending_input(&mut self, input: &str, cx: &mut WindowContext) { + let Some(range) = self.handler.selected_text_range(cx) else { + return; + }; + self.handler.replace_text_in_range(Some(range), &input, cx); + } +} + +/// Zed's interface for handling text input from the platform's IME system +/// This is currently a 1:1 exposure of the NSTextInputClient API: +/// +/// +pub trait InputHandler: 'static { + /// Get the range of the user's currently selected text, if any + /// Corresponds to [selectedRange()](https://developer.apple.com/documentation/appkit/nstextinputclient/1438242-selectedrange) + /// + /// Return value is in terms of UTF-16 characters, from 0 to the length of the document + fn selected_text_range(&mut self, cx: &mut WindowContext) -> Option>; + + /// Get the range of the currently marked text, if any + /// Corresponds to [markedRange()](https://developer.apple.com/documentation/appkit/nstextinputclient/1438250-markedrange) + /// + /// Return value is in terms of UTF-16 characters, from 0 to the length of the document + fn marked_text_range(&mut self, cx: &mut WindowContext) -> Option>; + + /// Get the text for the given document range in UTF-16 characters + /// Corresponds to [attributedSubstring(forProposedRange: actualRange:)](https://developer.apple.com/documentation/appkit/nstextinputclient/1438238-attributedsubstring) + /// + /// range_utf16 is in terms of UTF-16 characters + fn text_for_range( + &mut self, + range_utf16: Range, + cx: &mut WindowContext, + ) -> Option; + + /// Replace the text in the given document range with the given text + /// Corresponds to [insertText(_:replacementRange:)](https://developer.apple.com/documentation/appkit/nstextinputclient/1438258-inserttext) + /// + /// replacement_range is in terms of UTF-16 characters + fn replace_text_in_range( + &mut self, + replacement_range: Option>, + text: &str, + cx: &mut WindowContext, + ); + + /// Replace the text in the given document range with the given text, + /// and mark the given text as part of of an IME 'composing' state + /// Corresponds to [setMarkedText(_:selectedRange:replacementRange:)](https://developer.apple.com/documentation/appkit/nstextinputclient/1438246-setmarkedtext) + /// + /// range_utf16 is in terms of UTF-16 characters + /// new_selected_range is in terms of UTF-16 characters fn replace_and_mark_text_in_range( &mut self, range_utf16: Option>, new_text: &str, new_selected_range: Option>, + cx: &mut WindowContext, ); - fn unmark_text(&mut self); - fn bounds_for_range(&mut self, range_utf16: Range) -> Option>; + + /// Remove the IME 'composing' state from the document + /// Corresponds to [unmarkText()](https://developer.apple.com/documentation/appkit/nstextinputclient/1438239-unmarktext) + fn unmark_text(&mut self, cx: &mut WindowContext); + + /// Get the bounds of the given document range in screen coordinates + /// Corresponds to [firstRect(forCharacterRange:actualRange:)](https://developer.apple.com/documentation/appkit/nstextinputclient/1438240-firstrect) + /// + /// This is used for positioning the IME candidate window + fn bounds_for_range( + &mut self, + range_utf16: Range, + cx: &mut WindowContext, + ) -> Option>; } +/// The variables that can be configured when creating a new window #[derive(Debug)] pub struct WindowOptions { + /// The initial bounds of the window pub bounds: WindowBounds, + + /// The titlebar configuration of the window pub titlebar: Option, + + /// Whether the window should be centered on the screen pub center: bool, + + /// Whether the window should be focused when created pub focus: bool, + + /// Whether the window should be shown when created pub show: bool, + + /// The kind of window to create pub kind: WindowKind, + + /// Whether the window should be movable by the user pub is_movable: bool, + + /// The display to create the window on pub display_id: Option, } @@ -371,46 +515,67 @@ impl Default for WindowOptions { } } +/// The options that can be configured for a window's titlebar #[derive(Debug, Default)] pub struct TitlebarOptions { + /// The initial title of the window pub title: Option, - pub appears_transparent: bool, - pub traffic_light_position: Option>, -} -#[derive(Copy, Clone, Debug)] -pub enum Appearance { - Light, - VibrantLight, - Dark, - VibrantDark, -} + /// Whether the titlebar should appear transparent + pub appears_transparent: bool, -impl Default for Appearance { - fn default() -> Self { - Self::Light - } + /// The position of the macOS traffic light buttons + pub traffic_light_position: Option>, } +/// The kind of window to create #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum WindowKind { + /// A normal application window Normal, + + /// A window that appears above all other windows, usually used for alerts or popups + /// use sparingly! PopUp, } +/// Which bounds algorithm to use for the initial size a window #[derive(Copy, Clone, Debug, PartialEq, Default)] pub enum WindowBounds { + /// The window should be full screen, on macOS this corresponds to the full screen feature Fullscreen, + + /// Make the window as large as the current display's size. #[default] Maximized, + + /// Set the window to the given size in pixels Fixed(Bounds), } +/// The appearance of the window, as defined by the operating system +/// On macOS, this corresponds to named [NSAppearance](https://developer.apple.com/documentation/appkit/nsappearance) +/// values #[derive(Copy, Clone, Debug)] pub enum WindowAppearance { + /// A light appearance + /// + /// on macOS, this corresponds to the `aqua` appearance Light, + + /// A light appearance with vibrant colors + /// + /// on macOS, this corresponds to the `NSAppearanceNameVibrantLight` appearance VibrantLight, + + /// A dark appearance + /// + /// on macOS, this corresponds to the `darkAqua` appearance Dark, + + /// A dark appearance with vibrant colors + /// + /// on macOS, this corresponds to the `NSAppearanceNameVibrantDark` appearance VibrantDark, } @@ -420,40 +585,102 @@ impl Default for WindowAppearance { } } +/// The options that can be configured for a file dialog prompt #[derive(Copy, Clone, Debug)] pub struct PathPromptOptions { + /// Should the prompt allow files to be selected? pub files: bool, + /// Should the prompt allow directories to be selected? pub directories: bool, + /// Should the prompt allow multiple files to be selected? pub multiple: bool, } +/// What kind of prompt styling to show #[derive(Copy, Clone, Debug)] pub enum PromptLevel { + /// A prompt that is shown when the user should be notified of something Info, + + /// A prompt that is shown when the user needs to be warned of a potential problem Warning, + + /// A prompt that is shown when a critical problem has occurred Critical, } /// The style of the cursor (pointer) #[derive(Copy, Clone, Debug)] pub enum CursorStyle { + /// The default cursor Arrow, + + /// A text input cursor + /// corresponds to the CSS cursor value `text` IBeam, + + /// A crosshair cursor + /// corresponds to the CSS cursor value `crosshair` Crosshair, + + /// A closed hand cursor + /// corresponds to the CSS cursor value `grabbing` ClosedHand, + + /// An open hand cursor + /// corresponds to the CSS cursor value `grab` OpenHand, + + /// A pointing hand cursor + /// corresponds to the CSS cursor value `pointer` PointingHand, + + /// A resize left cursor + /// corresponds to the CSS cursor value `w-resize` ResizeLeft, + + /// A resize right cursor + /// corresponds to the CSS cursor value `e-resize` ResizeRight, + + /// A resize cursor to the left and right + /// corresponds to the CSS cursor value `col-resize` ResizeLeftRight, + + /// A resize up cursor + /// corresponds to the CSS cursor value `n-resize` ResizeUp, + + /// A resize down cursor + /// corresponds to the CSS cursor value `s-resize` ResizeDown, + + /// A resize cursor directing up and down + /// corresponds to the CSS cursor value `row-resize` ResizeUpDown, + + /// A cursor indicating that something will disappear if moved here + /// Does not correspond to a CSS cursor value DisappearingItem, + + /// A text input cursor for vertical layout + /// corresponds to the CSS cursor value `vertical-text` IBeamCursorForVerticalLayout, + + /// A cursor indicating that the operation is not allowed + /// corresponds to the CSS cursor value `not-allowed` OperationNotAllowed, + + /// A cursor indicating that the operation will result in a link + /// corresponds to the CSS cursor value `alias` DragLink, + + /// A cursor indicating that the operation will result in a copy + /// corresponds to the CSS cursor value `copy` DragCopy, + + /// A cursor indicating that the operation will result in a context menu + /// corresponds to the CSS cursor value `context-menu` ContextualMenu, } @@ -463,6 +690,7 @@ impl Default for CursorStyle { } } +/// A datastructure representing a semantic version number #[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd, Serialize)] pub struct SemanticVersion { major: usize, @@ -501,6 +729,7 @@ impl Display for SemanticVersion { } } +/// A clipboard item that should be copied to the clipboard #[derive(Clone, Debug, Eq, PartialEq)] pub struct ClipboardItem { pub(crate) text: String, @@ -508,6 +737,7 @@ pub struct ClipboardItem { } impl ClipboardItem { + /// Create a new clipboard item with the given text pub fn new(text: String) -> Self { Self { text, @@ -515,15 +745,18 @@ impl ClipboardItem { } } + /// Create a new clipboard item with the given text and metadata pub fn with_metadata(mut self, metadata: T) -> Self { self.metadata = Some(serde_json::to_string(&metadata).unwrap()); self } + /// Get the text of the clipboard item pub fn text(&self) -> &String { &self.text } + /// Get the metadata of the clipboard item pub fn metadata(&self) -> Option where T: for<'a> Deserialize<'a>, diff --git a/crates/gpui/src/platform/app_menu.rs b/crates/gpui/src/platform/app_menu.rs index 10fe2cf33ae6d8b830efc92f72bba7f5157de4b9..91fe358931b643ceb4365cd9a5545ca8ee4c8c3f 100644 --- a/crates/gpui/src/platform/app_menu.rs +++ b/crates/gpui/src/platform/app_menu.rs @@ -1,30 +1,49 @@ use crate::{Action, AppContext, Platform}; use util::ResultExt; +/// A menu of the application, either a main menu or a submenu pub struct Menu<'a> { + /// The name of the menu pub name: &'a str, + + /// The items in the menu pub items: Vec>, } +/// The different kinds of items that can be in a menu pub enum MenuItem<'a> { + /// A separator between items Separator, + + /// A submenu Submenu(Menu<'a>), + + /// An action that can be performed Action { + /// The name of this menu item name: &'a str, + + /// the action to perform when this menu item is selected action: Box, + + /// The OS Action that corresponds to this action, if any + /// See [`OsAction`] for more information os_action: Option, }, } impl<'a> MenuItem<'a> { + /// Creates a new menu item that is a separator pub fn separator() -> Self { Self::Separator } + /// Creates a new menu item that is a submenu pub fn submenu(menu: Menu<'a>) -> Self { Self::Submenu(menu) } + /// Creates a new menu item that invokes an action pub fn action(name: &'a str, action: impl Action) -> Self { Self::Action { name, @@ -33,6 +52,7 @@ impl<'a> MenuItem<'a> { } } + /// Creates a new menu item that invokes an action and has an OS action pub fn os_action(name: &'a str, action: impl Action, os_action: OsAction) -> Self { Self::Action { name, @@ -42,13 +62,31 @@ impl<'a> MenuItem<'a> { } } +// TODO: As part of the global selections refactor, these should +// be moved to GPUI-provided actions that make this association +// without leaking the platform details to GPUI users + +/// OS actions are actions that are recognized by the operating system +/// This allows the operating system to provide specialized behavior for +/// these actions #[derive(Copy, Clone, Eq, PartialEq)] pub enum OsAction { + /// The 'cut' action Cut, + + /// The 'copy' action Copy, + + /// The 'paste' action Paste, + + /// The 'select all' action SelectAll, + + /// The 'undo' action Undo, + + /// The 'redo' action Redo, } diff --git a/crates/gpui/src/platform/keystroke.rs b/crates/gpui/src/platform/keystroke.rs index 64a901789abb688c36c8dd2f2eef9c5fb16a34e8..2e1acfa630627da23d939ae8eb1c1e11eebce425 100644 --- a/crates/gpui/src/platform/keystroke.rs +++ b/crates/gpui/src/platform/keystroke.rs @@ -3,51 +3,60 @@ use serde::Deserialize; use smallvec::SmallVec; use std::fmt::Write; +/// A keystroke and associated metadata generated by the platform #[derive(Clone, Debug, Eq, PartialEq, Default, Deserialize, Hash)] pub struct Keystroke { + /// the state of the modifier keys at the time the keystroke was generated pub modifiers: Modifiers, + /// key is the character printed on the key that was pressed /// e.g. for option-s, key is "s" pub key: String, + /// ime_key is the character inserted by the IME engine when that key was pressed. /// e.g. for option-s, ime_key is "ß" pub ime_key: Option, } impl Keystroke { - // When matching a key we cannot know whether the user intended to type - // the ime_key or the key. On some non-US keyboards keys we use in our - // bindings are behind option (for example `$` is typed `alt-ç` on a Czech keyboard), - // and on some keyboards the IME handler converts a sequence of keys into a - // specific character (for example `"` is typed as `" space` on a brazilian keyboard). - pub fn match_candidates(&self) -> SmallVec<[Keystroke; 2]> { + /// When matching a key we cannot know whether the user intended to type + /// the ime_key or the key itself. On some non-US keyboards keys we use in our + /// bindings are behind option (for example `$` is typed `alt-ç` on a Czech keyboard), + /// and on some keyboards the IME handler converts a sequence of keys into a + /// specific character (for example `"` is typed as `" space` on a brazilian keyboard). + /// + /// This method generates a list of potential keystroke candidates that could be matched + /// against when resolving a keybinding. + pub(crate) fn match_candidates(&self) -> SmallVec<[Keystroke; 2]> { let mut possibilities = SmallVec::new(); match self.ime_key.as_ref() { - None => possibilities.push(self.clone()), Some(ime_key) => { - possibilities.push(Keystroke { - modifiers: Modifiers { - control: self.modifiers.control, - alt: false, - shift: false, - command: false, - function: false, - }, - key: ime_key.to_string(), - ime_key: None, - }); + if ime_key != &self.key { + possibilities.push(Keystroke { + modifiers: Modifiers { + control: self.modifiers.control, + alt: false, + shift: false, + command: false, + function: false, + }, + key: ime_key.to_string(), + ime_key: None, + }); + } possibilities.push(Keystroke { ime_key: None, ..self.clone() }); } + None => possibilities.push(self.clone()), } possibilities } /// key syntax is: /// [ctrl-][alt-][shift-][cmd-][fn-]key[->ime_key] - /// ime_key is only used for generating test events, + /// ime_key syntax is only used for generating test events, /// when matching a key with an ime_key set will be matched without it. pub fn parse(source: &str) -> anyhow::Result { let mut control = false; @@ -135,16 +144,29 @@ impl std::fmt::Display for Keystroke { } } +/// The state of the modifier keys at some point in time #[derive(Copy, Clone, Debug, Eq, PartialEq, Default, Deserialize, Hash)] pub struct Modifiers { + /// The control key pub control: bool, + + /// The alt key + /// Sometimes also known as the 'meta' key pub alt: bool, + + /// The shift key pub shift: bool, + + /// The command key, on macos + /// the windows key, on windows pub command: bool, + + /// The function key pub function: bool, } impl Modifiers { + /// Returns true if any modifier key is pressed pub fn modified(&self) -> bool { self.control || self.alt || self.shift || self.command || self.function } diff --git a/crates/gpui/src/platform/mac.rs b/crates/gpui/src/platform/mac.rs index 3cc74a968399dcc0fffcf8c795137262e66df8de..2194ae41e791f135de34e312653f2c2fc13489d9 100644 --- a/crates/gpui/src/platform/mac.rs +++ b/crates/gpui/src/platform/mac.rs @@ -21,13 +21,13 @@ use metal_renderer::*; use objc::runtime::{BOOL, NO, YES}; use std::ops::Range; -pub use dispatcher::*; -pub use display::*; -pub use display_linker::*; -pub use metal_atlas::*; -pub use platform::*; -pub use text_system::*; -pub use window::*; +pub(crate) use dispatcher::*; +pub(crate) use display::*; +pub(crate) use display_linker::*; +pub(crate) use metal_atlas::*; +pub(crate) use platform::*; +pub(crate) use text_system::*; +pub(crate) use window::*; trait BoolExt { fn to_objc(self) -> BOOL; diff --git a/crates/gpui/src/platform/mac/dispatcher.rs b/crates/gpui/src/platform/mac/dispatcher.rs index 18e361885e71d58a8328bb6dc38c1b6b2d286041..72daa8c4404f52836d45ad39517b9b4bb3db57dd 100644 --- a/crates/gpui/src/platform/mac/dispatcher.rs +++ b/crates/gpui/src/platform/mac/dispatcher.rs @@ -24,7 +24,7 @@ pub(crate) fn dispatch_get_main_queue() -> dispatch_queue_t { unsafe { &_dispatch_main_q as *const _ as dispatch_queue_t } } -pub struct MacDispatcher { +pub(crate) struct MacDispatcher { parker: Arc>, } diff --git a/crates/gpui/src/platform/mac/display.rs b/crates/gpui/src/platform/mac/display.rs index 95ec83cd5a9d120fbcbe531a5cec3c9dafa3c95a..d5eb089300cdae400d03075f0288b0f1b57b054c 100644 --- a/crates/gpui/src/platform/mac/display.rs +++ b/crates/gpui/src/platform/mac/display.rs @@ -3,18 +3,15 @@ use anyhow::Result; use cocoa::{ appkit::NSScreen, base::{id, nil}, - foundation::{NSDictionary, NSString}, + foundation::{NSDictionary, NSPoint, NSRect, NSSize, NSString}, }; use core_foundation::uuid::{CFUUIDGetUUIDBytes, CFUUIDRef}; -use core_graphics::{ - display::{CGDirectDisplayID, CGDisplayBounds, CGGetActiveDisplayList}, - geometry::{CGPoint, CGRect, CGSize}, -}; +use core_graphics::display::{CGDirectDisplayID, CGDisplayBounds, CGGetActiveDisplayList}; use objc::{msg_send, sel, sel_impl}; use uuid::Uuid; #[derive(Debug)] -pub struct MacDisplay(pub(crate) CGDirectDisplayID); +pub(crate) struct MacDisplay(pub(crate) CGDirectDisplayID); unsafe impl Send for MacDisplay {} @@ -24,11 +21,6 @@ impl MacDisplay { Self::all().find(|screen| screen.id() == id) } - /// Get the screen with the given persistent [`Uuid`]. - pub fn find_by_uuid(uuid: Uuid) -> Option { - Self::all().find(|screen| screen.uuid().ok() == Some(uuid)) - } - /// Get the primary screen - the one with the menu bar, and whose bottom left /// corner is at the origin of the AppKit coordinate system. pub fn primary() -> Self { @@ -77,14 +69,14 @@ extern "C" { fn CGDisplayCreateUUIDFromDisplayID(display: CGDirectDisplayID) -> CFUUIDRef; } -/// Convert the given rectangle from CoreGraphics' native coordinate space to GPUI's coordinate space. +/// Convert the given rectangle from Cocoa's coordinate space to GPUI's coordinate space. /// -/// CoreGraphics' coordinate space has its origin at the bottom left of the primary screen, +/// Cocoa's coordinate space has its origin at the bottom left of the primary screen, /// with the Y axis pointing upwards. /// /// Conversely, in GPUI's coordinate system, the origin is placed at the top left of the primary -/// screen, with the Y axis pointing downwards. -pub(crate) fn display_bounds_from_native(rect: CGRect) -> Bounds { +/// screen, with the Y axis pointing downwards (matching CoreGraphics) +pub(crate) fn global_bounds_from_ns_rect(rect: NSRect) -> Bounds { let primary_screen_size = unsafe { CGDisplayBounds(MacDisplay::primary().id().0) }.size; Bounds { @@ -101,22 +93,22 @@ pub(crate) fn display_bounds_from_native(rect: CGRect) -> Bounds { } } -/// Convert the given rectangle from GPUI's coordinate system to CoreGraphics' native coordinate space. +/// Convert the given rectangle from GPUI's coordinate system to Cocoa's native coordinate space. /// -/// CoreGraphics' coordinate space has its origin at the bottom left of the primary screen, +/// Cocoa's coordinate space has its origin at the bottom left of the primary screen, /// with the Y axis pointing upwards. /// /// Conversely, in GPUI's coordinate system, the origin is placed at the top left of the primary -/// screen, with the Y axis pointing downwards. -pub(crate) fn display_bounds_to_native(bounds: Bounds) -> CGRect { +/// screen, with the Y axis pointing downwards (matching CoreGraphics) +pub(crate) fn global_bounds_to_ns_rect(bounds: Bounds) -> NSRect { let primary_screen_height = MacDisplay::primary().bounds().size.height; - CGRect::new( - &CGPoint::new( + NSRect::new( + NSPoint::new( bounds.origin.x.into(), (primary_screen_height - bounds.origin.y - bounds.size.height).into(), ), - &CGSize::new(bounds.size.width.into(), bounds.size.height.into()), + NSSize::new(bounds.size.width.into(), bounds.size.height.into()), ) } @@ -155,8 +147,20 @@ impl PlatformDisplay for MacDisplay { fn bounds(&self) -> Bounds { unsafe { - let native_bounds = CGDisplayBounds(self.0); - display_bounds_from_native(native_bounds) + // CGDisplayBounds is in "global display" coordinates, where 0 is + // the top left of the primary display. + let bounds = CGDisplayBounds(self.0); + + Bounds { + origin: point( + GlobalPixels(bounds.origin.x as f32), + GlobalPixels(bounds.origin.y as f32), + ), + size: size( + GlobalPixels(bounds.size.width as f32), + GlobalPixels(bounds.size.height as f32), + ), + } } } } diff --git a/crates/gpui/src/platform/mac/display_linker.rs b/crates/gpui/src/platform/mac/display_linker.rs index 8f1b233046fd85ffc4d3b875e73e5f891841c75a..e25487ec0bb623cbc6714cf1e06d83343f25f7fd 100644 --- a/crates/gpui/src/platform/mac/display_linker.rs +++ b/crates/gpui/src/platform/mac/display_linker.rs @@ -7,8 +7,6 @@ use std::{ use crate::DisplayId; use collections::HashMap; use parking_lot::Mutex; -pub use sys::CVSMPTETime as SmtpeTime; -pub use sys::CVTimeStamp as VideoTimestamp; pub(crate) struct MacDisplayLinker { links: HashMap, @@ -27,13 +25,13 @@ impl MacDisplayLinker { } } -type OutputCallback = Mutex>; +type OutputCallback = Mutex>; impl MacDisplayLinker { pub fn set_output_callback( &mut self, display_id: DisplayId, - output_callback: Box, + output_callback: Box, ) { if let Some(mut system_link) = unsafe { sys::DisplayLink::on_display(display_id.0) } { let callback = Arc::new(Mutex::new(output_callback)); @@ -81,11 +79,11 @@ unsafe extern "C" fn trampoline( _flags_out: *mut i64, user_data: *mut c_void, ) -> i32 { - if let Some((current_time, output_time)) = current_time.as_ref().zip(output_time.as_ref()) { + if let Some((_current_time, _output_time)) = current_time.as_ref().zip(output_time.as_ref()) { let output_callback: Weak = Weak::from_raw(user_data as *mut OutputCallback); if let Some(output_callback) = output_callback.upgrade() { - (output_callback.lock())(current_time, output_time) + (output_callback.lock())() } mem::forget(output_callback); } @@ -126,7 +124,7 @@ mod sys { #[repr(C)] #[derive(Clone, Copy)] - pub struct CVTimeStamp { + pub(crate) struct CVTimeStamp { pub version: u32, pub video_time_scale: i32, pub video_time: i64, @@ -154,7 +152,7 @@ mod sys { #[repr(C)] #[derive(Clone, Copy, Default)] - pub struct CVSMPTETime { + pub(crate) struct CVSMPTETime { pub subframes: i16, pub subframe_divisor: i16, pub counter: u32, diff --git a/crates/gpui/src/platform/mac/events.rs b/crates/gpui/src/platform/mac/events.rs index f84833d3cbb1678c1ee1ee9b0c1793bb9df8bae0..4653ce89b45f6b79f538d98ac32c498f684a5c86 100644 --- a/crates/gpui/src/platform/mac/events.rs +++ b/crates/gpui/src/platform/mac/events.rs @@ -83,7 +83,10 @@ unsafe fn read_modifiers(native_event: id) -> Modifiers { } impl PlatformInput { - pub unsafe fn from_native(native_event: id, window_height: Option) -> Option { + pub(crate) unsafe fn from_native( + native_event: id, + window_height: Option, + ) -> Option { let event_type = native_event.eventType(); // Filter out event types that aren't in the NSEventType enum. diff --git a/crates/gpui/src/platform/mac/metal_atlas.rs b/crates/gpui/src/platform/mac/metal_atlas.rs index d3caeba5222e6a4739fc63ef54358eb3589debbf..95f78a446539de117938113f010201afd62cbb71 100644 --- a/crates/gpui/src/platform/mac/metal_atlas.rs +++ b/crates/gpui/src/platform/mac/metal_atlas.rs @@ -10,10 +10,10 @@ use metal::Device; use parking_lot::Mutex; use std::borrow::Cow; -pub struct MetalAtlas(Mutex); +pub(crate) struct MetalAtlas(Mutex); impl MetalAtlas { - pub fn new(device: Device) -> Self { + pub(crate) fn new(device: Device) -> Self { MetalAtlas(Mutex::new(MetalAtlasState { device: AssertSend(device), monochrome_textures: Default::default(), diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index 499ac0b59104d9ab0b8204a95c01371a228a6b47..e4a688c2fdb35c1dd3adec829fd66a4baf1e4441 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/crates/gpui/src/platform/mac/platform.rs @@ -3,7 +3,7 @@ use crate::{ Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId, ForegroundExecutor, Keymap, MacDispatcher, MacDisplay, MacDisplayLinker, MacTextSystem, MacWindow, Menu, MenuItem, PathPromptOptions, Platform, PlatformDisplay, PlatformInput, - PlatformTextSystem, PlatformWindow, Result, SemanticVersion, VideoTimestamp, WindowOptions, + PlatformTextSystem, PlatformWindow, Result, SemanticVersion, WindowOptions, }; use anyhow::anyhow; use block::ConcreteBlock; @@ -139,9 +139,9 @@ unsafe fn build_classes() { } } -pub struct MacPlatform(Mutex); +pub(crate) struct MacPlatform(Mutex); -pub struct MacPlatformState { +pub(crate) struct MacPlatformState { background_executor: BackgroundExecutor, foreground_executor: ForegroundExecutor, text_system: Arc, @@ -169,7 +169,7 @@ impl Default for MacPlatform { } impl MacPlatform { - pub fn new() -> Self { + pub(crate) fn new() -> Self { let dispatcher = Arc::new(MacDispatcher::new()); Self(Mutex::new(MacPlatformState { background_executor: BackgroundExecutor::new(dispatcher.clone()), @@ -475,10 +475,6 @@ impl Platform for MacPlatform { } } - // fn add_status_item(&self, _handle: AnyWindowHandle) -> Box { - // Box::new(StatusItem::add(self.fonts())) - // } - fn displays(&self) -> Vec> { MacDisplay::all() .map(|screen| Rc::new(screen) as Rc<_>) @@ -504,7 +500,7 @@ impl Platform for MacPlatform { fn set_display_link_output_callback( &self, display_id: DisplayId, - callback: Box, + callback: Box, ) { self.0 .lock() diff --git a/crates/gpui/src/platform/mac/text_system.rs b/crates/gpui/src/platform/mac/text_system.rs index d11efa902ae7c270ff0331b74b1860032ff5f212..ba434026f65f9573acaf73c14c6abb21d6448b35 100644 --- a/crates/gpui/src/platform/mac/text_system.rs +++ b/crates/gpui/src/platform/mac/text_system.rs @@ -41,7 +41,7 @@ use super::open_type; #[allow(non_upper_case_globals)] const kCGImageAlphaOnly: u32 = 7; -pub struct MacTextSystem(RwLock); +pub(crate) struct MacTextSystem(RwLock); struct MacTextSystemState { memory_source: MemSource, @@ -54,7 +54,7 @@ struct MacTextSystemState { } impl MacTextSystem { - pub fn new() -> Self { + pub(crate) fn new() -> Self { Self(RwLock::new(MacTextSystemState { memory_source: MemSource::empty(), system_source: SystemSource::new(), diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index 134390bb79900b0cc09efba720b373303d8cd26b..5b454d9b4e06c958c59feb75b2304c977902a047 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -1,9 +1,9 @@ -use super::{display_bounds_from_native, ns_string, MacDisplay, MetalRenderer, NSRange}; +use super::{global_bounds_from_ns_rect, ns_string, MacDisplay, MetalRenderer, NSRange}; use crate::{ - display_bounds_to_native, point, px, size, AnyWindowHandle, Bounds, ExternalPaths, - FileDropEvent, ForegroundExecutor, GlobalPixels, KeyDownEvent, Keystroke, Modifiers, - ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, - PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point, + global_bounds_to_ns_rect, platform::PlatformInputHandler, point, px, size, AnyWindowHandle, + Bounds, ExternalPaths, FileDropEvent, ForegroundExecutor, GlobalPixels, KeyDownEvent, + Keystroke, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, + MouseUpEvent, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformWindow, Point, PromptLevel, Size, Timer, WindowAppearance, WindowBounds, WindowKind, WindowOptions, }; use block::ConcreteBlock; @@ -220,7 +220,7 @@ unsafe fn build_classes() { }; } -pub fn convert_mouse_position(position: NSPoint, window_height: Pixels) -> Point { +pub(crate) fn convert_mouse_position(position: NSPoint, window_height: Pixels) -> Point { point( px(position.x as f32), // MacOS screen coordinates are relative to bottom left @@ -327,7 +327,7 @@ struct MacWindowState { should_close_callback: Option bool>>, close_callback: Option>, appearance_changed_callback: Option>, - input_handler: Option>, + input_handler: Option, pending_key_down: Option<(KeyDownEvent, Option)>, last_key_equivalent: Option, synthetic_drag_counter: usize, @@ -411,10 +411,8 @@ impl MacWindowState { } fn frame(&self) -> Bounds { - unsafe { - let frame = NSWindow::frame(self.native_window); - display_bounds_from_native(mem::transmute::(frame)) - } + let frame = unsafe { NSWindow::frame(self.native_window) }; + global_bounds_from_ns_rect(frame) } fn content_size(&self) -> Size { @@ -448,7 +446,7 @@ impl MacWindowState { unsafe impl Send for MacWindowState {} -pub struct MacWindow(Arc>); +pub(crate) struct MacWindow(Arc>); impl MacWindow { pub fn open( @@ -516,25 +514,6 @@ impl MacWindow { NSArray::arrayWithObject(nil, NSFilenamesPboardType) ]; - let screen = native_window.screen(); - match options.bounds { - WindowBounds::Fullscreen => { - native_window.toggleFullScreen_(nil); - } - WindowBounds::Maximized => { - native_window.setFrame_display_(screen.visibleFrame(), YES); - } - WindowBounds::Fixed(bounds) => { - let display_bounds = display.bounds(); - let frame = if bounds.intersects(&display_bounds) { - display_bounds_to_native(bounds) - } else { - display_bounds_to_native(display_bounds) - }; - native_window.setFrame_display_(mem::transmute::(frame), YES); - } - } - let native_view: id = msg_send![VIEW_CLASS, alloc]; let native_view = NSView::init(native_view); @@ -656,6 +635,27 @@ impl MacWindow { native_window.orderFront_(nil); } + let screen = native_window.screen(); + match options.bounds { + WindowBounds::Fullscreen => { + // We need to toggle full screen asynchronously as doing so may + // call back into the platform handlers. + window.toggle_full_screen() + } + WindowBounds::Maximized => { + native_window.setFrame_display_(screen.visibleFrame(), YES); + } + WindowBounds::Fixed(bounds) => { + let display_bounds = display.bounds(); + let frame = if bounds.intersects(&display_bounds) { + global_bounds_to_ns_rect(bounds) + } else { + global_bounds_to_ns_rect(display_bounds) + }; + native_window.setFrame_display_(frame, YES); + } + } + window.0.lock().move_traffic_light(); pool.drain(); @@ -764,11 +764,11 @@ impl PlatformWindow for MacWindow { self } - fn set_input_handler(&mut self, input_handler: Box) { + fn set_input_handler(&mut self, input_handler: PlatformInputHandler) { self.0.as_ref().lock().input_handler = Some(input_handler); } - fn take_input_handler(&mut self) -> Option> { + fn take_input_handler(&mut self) -> Option { self.0.as_ref().lock().input_handler.take() } @@ -1003,9 +1003,21 @@ impl PlatformWindow for MacWindow { } fn get_scale_factor(native_window: id) -> f32 { - unsafe { + let factor = unsafe { let screen: id = msg_send![native_window, screen]; NSScreen::backingScaleFactor(screen) as f32 + }; + + // We are not certain what triggers this, but it seems that sometimes + // this method would return 0 (https://github.com/zed-industries/community/issues/2422) + // It seems most likely that this would happen if the window has no screen + // (if it is off-screen), though we'd expect to see viewDidChangeBackingProperties before + // it was rendered for real. + // Regardless, attempt to avoid the issue here. + if factor == 0.0 { + 2. + } else { + factor } } @@ -1542,9 +1554,7 @@ extern "C" fn insert_text(this: &Object, _: Sel, text: id, replacement_range: NS replacement_range, text: text.to_string(), }); - if text.to_string().to_ascii_lowercase() != pending_key_down.0.keystroke.key { - pending_key_down.0.keystroke.ime_key = Some(text.to_string()); - } + pending_key_down.0.keystroke.ime_key = Some(text.to_string()); window_state.lock().pending_key_down = Some(pending_key_down); } } @@ -1761,13 +1771,13 @@ fn drag_event_position(window_state: &Mutex, dragging_info: id) fn with_input_handler(window: &Object, f: F) -> Option where - F: FnOnce(&mut dyn PlatformInputHandler) -> R, + F: FnOnce(&mut PlatformInputHandler) -> R, { let window_state = unsafe { get_window_state(window) }; let mut lock = window_state.as_ref().lock(); if let Some(mut input_handler) = lock.input_handler.take() { drop(lock); - let result = f(input_handler.as_mut()); + let result = f(&mut input_handler); window_state.lock().input_handler = Some(input_handler); Some(result) } else { diff --git a/crates/gpui/src/platform/mac/window_appearance.rs b/crates/gpui/src/platform/mac/window_appearance.rs index 2edc896289ef8056424a0399d38ff937155adad2..0a5df85a446859880a2a04e27b77bedcf44cb925 100644 --- a/crates/gpui/src/platform/mac/window_appearance.rs +++ b/crates/gpui/src/platform/mac/window_appearance.rs @@ -8,7 +8,7 @@ use objc::{msg_send, sel, sel_impl}; use std::ffi::CStr; impl WindowAppearance { - pub unsafe fn from_native(appearance: id) -> Self { + pub(crate) unsafe fn from_native(appearance: id) -> Self { let name: id = msg_send![appearance, name]; if name == NSAppearanceNameVibrantLight { Self::VibrantLight diff --git a/crates/gpui/src/platform/test.rs b/crates/gpui/src/platform/test.rs index acc8ffe41ce423e7b16fd9e17e86ef3b5f71dfd4..d17739239eede2e3b0aa2d5f91028dadd94d3bdd 100644 --- a/crates/gpui/src/platform/test.rs +++ b/crates/gpui/src/platform/test.rs @@ -3,7 +3,7 @@ mod display; mod platform; mod window; -pub use dispatcher::*; -pub use display::*; -pub use platform::*; -pub use window::*; +pub(crate) use dispatcher::*; +pub(crate) use display::*; +pub(crate) use platform::*; +pub(crate) use window::*; diff --git a/crates/gpui/src/platform/test/dispatcher.rs b/crates/gpui/src/platform/test/dispatcher.rs index 9023627d1e2d777188d1b5bc96f89cabc4fbe903..9c59fce48c43ba06d5887ea87b4a0d5cd15ae4f8 100644 --- a/crates/gpui/src/platform/test/dispatcher.rs +++ b/crates/gpui/src/platform/test/dispatcher.rs @@ -18,6 +18,7 @@ use util::post_inc; #[derive(Copy, Clone, PartialEq, Eq, Hash)] struct TestDispatcherId(usize); +#[doc(hidden)] pub struct TestDispatcher { id: TestDispatcherId, state: Arc>, diff --git a/crates/gpui/src/platform/test/display.rs b/crates/gpui/src/platform/test/display.rs index 838d600147b86e62d2f8197cf695590dbdefd750..488731607084ad1647eec53e151299e97c04c424 100644 --- a/crates/gpui/src/platform/test/display.rs +++ b/crates/gpui/src/platform/test/display.rs @@ -3,7 +3,7 @@ use anyhow::{Ok, Result}; use crate::{Bounds, DisplayId, GlobalPixels, PlatformDisplay, Point}; #[derive(Debug)] -pub struct TestDisplay { +pub(crate) struct TestDisplay { id: DisplayId, uuid: uuid::Uuid, bounds: Bounds, diff --git a/crates/gpui/src/platform/test/platform.rs b/crates/gpui/src/platform/test/platform.rs index f5e2170b28acdeae304495172c7b6bbac568bcf4..9a33b4b3b58bf67591746d1c1f000089db315098 100644 --- a/crates/gpui/src/platform/test/platform.rs +++ b/crates/gpui/src/platform/test/platform.rs @@ -15,7 +15,7 @@ use std::{ }; /// TestPlatform implements the Platform trait for use in tests. -pub struct TestPlatform { +pub(crate) struct TestPlatform { background_executor: BackgroundExecutor, foreground_executor: ForegroundExecutor, @@ -178,20 +178,9 @@ impl Platform for TestPlatform { fn set_display_link_output_callback( &self, _display_id: DisplayId, - mut callback: Box, + mut callback: Box, ) { - let timestamp = crate::VideoTimestamp { - version: 0, - video_time_scale: 0, - video_time: 0, - host_time: 0, - rate_scalar: 0.0, - video_refresh_period: 0, - smpte_time: crate::SmtpeTime::default(), - flags: 0, - reserved: 0, - }; - callback(×tamp, ×tamp) + callback() } fn start_display_link(&self, _display_id: DisplayId) {} diff --git a/crates/gpui/src/platform/test/window.rs b/crates/gpui/src/platform/test/window.rs index 2f080bd7098bd42cf309c52e86df754e0d153a35..c03384aadfcdcbf7bcebf0eddd298a55e0dab2a4 100644 --- a/crates/gpui/src/platform/test/window.rs +++ b/crates/gpui/src/platform/test/window.rs @@ -10,7 +10,7 @@ use std::{ sync::{self, Arc}, }; -pub struct TestWindowState { +pub(crate) struct TestWindowState { pub(crate) bounds: WindowBounds, pub(crate) handle: AnyWindowHandle, display: Rc, @@ -23,11 +23,11 @@ pub struct TestWindowState { active_status_change_callback: Option>, resize_callback: Option, f32)>>, moved_callback: Option>, - input_handler: Option>, + input_handler: Option, } #[derive(Clone)] -pub struct TestWindow(pub(crate) Arc>); +pub(crate) struct TestWindow(pub(crate) Arc>); impl TestWindow { pub fn new( @@ -96,7 +96,19 @@ impl TestWindow { result } - pub fn simulate_keystroke(&mut self, keystroke: Keystroke, is_held: bool) { + pub fn simulate_keystroke(&mut self, mut keystroke: Keystroke, is_held: bool) { + if keystroke.ime_key.is_none() + && !keystroke.modifiers.command + && !keystroke.modifiers.control + && !keystroke.modifiers.function + { + keystroke.ime_key = Some(if keystroke.modifiers.shift { + keystroke.key.to_ascii_uppercase().clone() + } else { + keystroke.key.clone() + }) + } + if self.simulate_input(PlatformInput::KeyDown(KeyDownEvent { keystroke: keystroke.clone(), is_held, @@ -112,14 +124,12 @@ impl TestWindow { ); }; drop(lock); - let text = keystroke.ime_key.unwrap_or(keystroke.key); - input_handler.replace_text_in_range(None, &text); + if let Some(text) = keystroke.ime_key.as_ref() { + input_handler.replace_text_in_range(None, &text); + } self.0.lock().input_handler = Some(input_handler); } - pub fn edited(&self) -> bool { - self.0.lock().edited - } } impl PlatformWindow for TestWindow { @@ -163,11 +173,11 @@ impl PlatformWindow for TestWindow { self } - fn set_input_handler(&mut self, input_handler: Box) { + fn set_input_handler(&mut self, input_handler: PlatformInputHandler) { self.0.lock().input_handler = Some(input_handler); } - fn take_input_handler(&mut self) -> Option> { + fn take_input_handler(&mut self) -> Option { self.0.lock().input_handler.take() } @@ -269,12 +279,12 @@ impl PlatformWindow for TestWindow { } } -pub struct TestAtlasState { +pub(crate) struct TestAtlasState { next_id: u32, tiles: HashMap, } -pub struct TestAtlas(Mutex); +pub(crate) struct TestAtlas(Mutex); impl TestAtlas { pub fn new() -> Self { diff --git a/crates/gpui/src/prelude.rs b/crates/gpui/src/prelude.rs index 90d09b3fc53db18c02dd067dbb8916042aa020fc..2ab115fa626a6750a45f4ceab1d8496282802bf9 100644 --- a/crates/gpui/src/prelude.rs +++ b/crates/gpui/src/prelude.rs @@ -1,5 +1,9 @@ +//! The GPUI prelude is a collection of traits and types that are widely used +//! throughout the library. It is recommended to import this prelude into your +//! application to avoid having to import each trait individually. + pub use crate::{ - BorrowAppContext, BorrowWindow, Context, Element, FocusableElement, InteractiveElement, - IntoElement, ParentElement, Refineable, Render, RenderOnce, StatefulInteractiveElement, Styled, - VisualContext, + util::FluentBuilder, BorrowAppContext, BorrowWindow, Context, Element, FocusableElement, + InteractiveElement, IntoElement, ParentElement, Refineable, Render, RenderOnce, + StatefulInteractiveElement, Styled, VisualContext, }; diff --git a/crates/gpui/src/scene.rs b/crates/gpui/src/scene.rs index b69c10c752296cb6ef765fc924ae030d46a450b4..4ba65ef1e98e3c16761fc3da886d6de27edad60e 100644 --- a/crates/gpui/src/scene.rs +++ b/crates/gpui/src/scene.rs @@ -10,12 +10,12 @@ pub(crate) type PointF = Point; #[allow(non_camel_case_types, unused)] pub(crate) type PathVertex_ScaledPixels = PathVertex; -pub type LayerId = u32; -pub type DrawOrder = u32; +pub(crate) type LayerId = u32; +pub(crate) type DrawOrder = u32; #[derive(Default, Copy, Clone, Debug, Eq, PartialEq, Hash)] #[repr(C)] -pub struct ViewId { +pub(crate) struct ViewId { low_bits: u32, high_bits: u32, } @@ -38,7 +38,7 @@ impl From for EntityId { } #[derive(Default)] -pub struct Scene { +pub(crate) struct Scene { layers_by_order: BTreeMap, orders_by_layer: BTreeMap, pub(crate) shadows: Vec, @@ -153,49 +153,49 @@ impl Scene { for shadow in prev_scene.shadows.drain(..) { if views.contains(&shadow.view_id.into()) { let order = &prev_scene.orders_by_layer[&shadow.layer_id]; - self.insert(&order, shadow); + self.insert(order, shadow); } } for quad in prev_scene.quads.drain(..) { if views.contains(&quad.view_id.into()) { let order = &prev_scene.orders_by_layer[&quad.layer_id]; - self.insert(&order, quad); + self.insert(order, quad); } } for path in prev_scene.paths.drain(..) { if views.contains(&path.view_id.into()) { let order = &prev_scene.orders_by_layer[&path.layer_id]; - self.insert(&order, path); + self.insert(order, path); } } for underline in prev_scene.underlines.drain(..) { if views.contains(&underline.view_id.into()) { let order = &prev_scene.orders_by_layer[&underline.layer_id]; - self.insert(&order, underline); + self.insert(order, underline); } } for sprite in prev_scene.monochrome_sprites.drain(..) { if views.contains(&sprite.view_id.into()) { let order = &prev_scene.orders_by_layer[&sprite.layer_id]; - self.insert(&order, sprite); + self.insert(order, sprite); } } for sprite in prev_scene.polychrome_sprites.drain(..) { if views.contains(&sprite.view_id.into()) { let order = &prev_scene.orders_by_layer[&sprite.layer_id]; - self.insert(&order, sprite); + self.insert(order, sprite); } } for surface in prev_scene.surfaces.drain(..) { if views.contains(&surface.view_id.into()) { let order = &prev_scene.orders_by_layer[&surface.layer_id]; - self.insert(&order, surface); + self.insert(order, surface); } } } @@ -429,7 +429,7 @@ impl<'a> Iterator for BatchIterator<'a> { } #[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Default)] -pub enum PrimitiveKind { +pub(crate) enum PrimitiveKind { Shadow, #[default] Quad, @@ -495,7 +495,7 @@ pub(crate) enum PrimitiveBatch<'a> { #[derive(Default, Debug, Clone, Eq, PartialEq)] #[repr(C)] -pub struct Quad { +pub(crate) struct Quad { pub view_id: ViewId, pub layer_id: LayerId, pub order: DrawOrder, @@ -527,7 +527,7 @@ impl From for Primitive { #[derive(Debug, Clone, Eq, PartialEq)] #[repr(C)] -pub struct Underline { +pub(crate) struct Underline { pub view_id: ViewId, pub layer_id: LayerId, pub order: DrawOrder, @@ -558,7 +558,7 @@ impl From for Primitive { #[derive(Debug, Clone, Eq, PartialEq)] #[repr(C)] -pub struct Shadow { +pub(crate) struct Shadow { pub view_id: ViewId, pub layer_id: LayerId, pub order: DrawOrder, @@ -655,7 +655,7 @@ impl From for Primitive { } #[derive(Clone, Debug, Eq, PartialEq)] -pub struct Surface { +pub(crate) struct Surface { pub view_id: ViewId, pub layer_id: LayerId, pub order: DrawOrder, @@ -685,6 +685,7 @@ impl From for Primitive { #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] pub(crate) struct PathId(pub(crate) usize); +/// A line made up of a series of vertices and control points. #[derive(Debug)] pub struct Path { pub(crate) id: PathId, @@ -701,6 +702,7 @@ pub struct Path { } impl Path { + /// Create a new path with the given starting point. pub fn new(start: Point) -> Self { Self { id: PathId(0), @@ -720,6 +722,7 @@ impl Path { } } + /// Scale this path by the given factor. pub fn scale(&self, factor: f32) -> Path { Path { id: self.id, @@ -740,6 +743,7 @@ impl Path { } } + /// Draw a straight line from the current point to the given point. pub fn line_to(&mut self, to: Point) { self.contour_count += 1; if self.contour_count > 1 { @@ -751,6 +755,7 @@ impl Path { self.current = to; } + /// Draw a curve from the current point to the given point, using the given control point. pub fn curve_to(&mut self, to: Point, ctrl: Point) { self.contour_count += 1; if self.contour_count > 1 { @@ -833,7 +838,7 @@ impl From> for Primitive { #[derive(Clone, Debug)] #[repr(C)] -pub struct PathVertex { +pub(crate) struct PathVertex { pub(crate) xy_position: Point

, pub(crate) st_position: Point, pub(crate) content_mask: ContentMask

, @@ -850,4 +855,4 @@ impl PathVertex { } #[derive(Copy, Clone, Debug)] -pub struct AtlasId(pub(crate) usize); +pub(crate) struct AtlasId(pub(crate) usize); diff --git a/crates/gpui/src/shared_string.rs b/crates/gpui/src/shared_string.rs index 97db4bd1912dae5635d69bb1a4dfe1008ebe071c..d196b19636f3d2ecf0c62fa6bc457c3af1087e48 100644 --- a/crates/gpui/src/shared_string.rs +++ b/crates/gpui/src/shared_string.rs @@ -3,6 +3,8 @@ use serde::{Deserialize, Serialize}; use std::{borrow::Borrow, sync::Arc}; use util::arc_cow::ArcCow; +/// A shared string is an immutable string that can be cheaply cloned in GPUI +/// tasks. Essentially an abstraction over an Arc and &'static str, #[derive(Deref, DerefMut, Eq, PartialEq, Hash, Clone)] pub struct SharedString(ArcCow<'static, str>); diff --git a/crates/gpui/src/style.rs b/crates/gpui/src/style.rs index 095233280edefc0b11d85e3a4ee255f54c8da13d..e1d82adea8751a8f0baf60c40dac4efbec993b1b 100644 --- a/crates/gpui/src/style.rs +++ b/crates/gpui/src/style.rs @@ -1,10 +1,10 @@ use std::{iter, mem, ops::Range}; use crate::{ - black, phi, point, quad, rems, AbsoluteLength, BorrowAppContext, BorrowWindow, Bounds, - ContentMask, Corners, CornersRefinement, CursorStyle, DefiniteLength, Edges, EdgesRefinement, - Font, FontFeatures, FontStyle, FontWeight, Hsla, Length, Pixels, Point, PointRefinement, Rgba, - SharedString, Size, SizeRefinement, Styled, TextRun, WindowContext, + black, phi, point, quad, rems, AbsoluteLength, Bounds, ContentMask, Corners, CornersRefinement, + CursorStyle, DefiniteLength, Edges, EdgesRefinement, ElementContext, Font, FontFeatures, + FontStyle, FontWeight, Hsla, Length, Pixels, Point, PointRefinement, Rgba, SharedString, Size, + SizeRefinement, Styled, TextRun, }; use collections::HashSet; use refineable::{Cascade, Refineable}; @@ -110,7 +110,7 @@ pub struct Style { /// The mouse cursor style shown when the mouse pointer is over an element. pub mouse_cursor: Option, - pub z_index: Option, + pub z_index: Option, #[cfg(debug_assertions)] pub debug: bool, @@ -308,61 +308,12 @@ impl Style { } } - pub fn apply_text_style(&self, cx: &mut C, f: F) -> R - where - C: BorrowAppContext, - F: FnOnce(&mut C) -> R, - { - if self.text.is_some() { - cx.with_text_style(Some(self.text.clone()), f) - } else { - f(cx) - } - } - - /// Apply overflow to content mask - pub fn apply_overflow(&self, bounds: Bounds, cx: &mut C, f: F) -> R - where - C: BorrowWindow, - F: FnOnce(&mut C) -> R, - { - let current_mask = cx.content_mask(); - - let min = current_mask.bounds.origin; - let max = current_mask.bounds.lower_right(); - - let mask_bounds = match ( - self.overflow.x == Overflow::Visible, - self.overflow.y == Overflow::Visible, - ) { - // x and y both visible - (true, true) => return f(cx), - // x visible, y hidden - (true, false) => Bounds::from_corners( - point(min.x, bounds.origin.y), - point(max.x, bounds.lower_right().y), - ), - // x hidden, y visible - (false, true) => Bounds::from_corners( - point(bounds.origin.x, min.y), - point(bounds.lower_right().x, max.y), - ), - // both hidden - (false, false) => bounds, - }; - let mask = ContentMask { - bounds: mask_bounds, - }; - - cx.with_content_mask(Some(mask), f) - } - /// Paints the background of an element styled with this style. pub fn paint( &self, bounds: Bounds, - cx: &mut WindowContext, - continuation: impl FnOnce(&mut WindowContext), + cx: &mut ElementContext, + continuation: impl FnOnce(&mut ElementContext), ) { #[cfg(debug_assertions)] if self.debug_below { @@ -386,7 +337,7 @@ impl Style { let background_color = self.background.as_ref().and_then(Fill::color); if background_color.map_or(false, |color| !color.is_transparent()) { - cx.with_z_index(0, |cx| { + cx.with_z_index(1, |cx| { let mut border_color = background_color.unwrap_or_default(); border_color.a = 0.; cx.paint_quad(quad( @@ -399,12 +350,12 @@ impl Style { }); } - cx.with_z_index(0, |cx| { + cx.with_z_index(2, |cx| { continuation(cx); }); if self.is_border_visible() { - cx.with_z_index(0, |cx| { + cx.with_z_index(3, |cx| { let corner_radii = self.corner_radii.to_pixels(bounds.size, rem_size); let border_widths = self.border_widths.to_pixels(rem_size); let max_border_width = border_widths.max(); diff --git a/crates/gpui/src/styled.rs b/crates/gpui/src/styled.rs index 0eba1771f52d47bde32f465a887e52547f3a89b2..3866dc65c7b400caaba55da22dca704ff893cbc0 100644 --- a/crates/gpui/src/styled.rs +++ b/crates/gpui/src/styled.rs @@ -1,18 +1,18 @@ use crate::{ self as gpui, hsla, point, px, relative, rems, AbsoluteLength, AlignItems, CursorStyle, - DefiniteLength, Display, Fill, FlexDirection, FontWeight, Hsla, JustifyContent, Length, - Position, SharedString, StyleRefinement, Visibility, WhiteSpace, + DefiniteLength, Fill, FlexDirection, FontWeight, Hsla, JustifyContent, Length, Position, + SharedString, StyleRefinement, Visibility, WhiteSpace, }; use crate::{BoxShadow, TextStyleRefinement}; use smallvec::{smallvec, SmallVec}; -use taffy::style::Overflow; +use taffy::style::{Display, Overflow}; pub trait Styled: Sized { fn style(&mut self) -> &mut StyleRefinement; gpui_macros::style_helpers!(); - fn z_index(mut self, z_index: u8) -> Self { + fn z_index(mut self, z_index: u16) -> Self { self.style().z_index = Some(z_index); self } @@ -66,18 +66,24 @@ pub trait Styled: Sized { self } + /// Sets the behavior of content that overflows the container to be hidden. + /// [Docs](https://tailwindcss.com/docs/overflow#hiding-content-that-overflows) fn overflow_hidden(mut self) -> Self { self.style().overflow.x = Some(Overflow::Hidden); self.style().overflow.y = Some(Overflow::Hidden); self } - fn overflow_hidden_x(mut self) -> Self { + /// Sets the behavior of content that overflows the container on the X axis to be hidden. + /// [Docs](https://tailwindcss.com/docs/overflow#hiding-content-that-overflows) + fn overflow_x_hidden(mut self) -> Self { self.style().overflow.x = Some(Overflow::Hidden); self } - fn overflow_hidden_y(mut self) -> Self { + /// Sets the behavior of content that overflows the container on the Y axis to be hidden. + /// [Docs](https://tailwindcss.com/docs/overflow#hiding-content-that-overflows) + fn overflow_y_hidden(mut self) -> Self { self.style().overflow.y = Some(Overflow::Hidden); self } @@ -301,6 +307,13 @@ pub trait Styled: Sized { self } + /// Sets the initial size of flex items for this element. + /// [Docs](https://tailwindcss.com/docs/flex-basis) + fn flex_basis(mut self, basis: impl Into) -> Self { + self.style().flex_basis = Some(basis.into()); + self + } + /// Sets the element to allow a flex item to grow to fill any available space. /// [Docs](https://tailwindcss.com/docs/flex-grow) fn flex_grow(mut self) -> Self { @@ -308,6 +321,20 @@ pub trait Styled: Sized { self } + /// Sets the element to allow a flex item to shrink if needed. + /// [Docs](https://tailwindcss.com/docs/flex-shrink) + fn flex_shrink(mut self) -> Self { + self.style().flex_shrink = Some(1.); + self + } + + /// Sets the element to prevent a flex item from shrinking. + /// [Docs](https://tailwindcss.com/docs/flex-shrink#dont-shrink) + fn flex_shrink_0(mut self) -> Self { + self.style().flex_shrink = Some(0.); + self + } + /// Sets the element to align flex items to the start of the container's cross axis. /// [Docs](https://tailwindcss.com/docs/align-items#start) fn items_start(mut self) -> Self { diff --git a/crates/gpui/src/subscription.rs b/crates/gpui/src/subscription.rs index 887283d09486ae139e219f5910206eda317b741b..9dca2e3a4841cc6c4ccd85c16700d08201218bd2 100644 --- a/crates/gpui/src/subscription.rs +++ b/crates/gpui/src/subscription.rs @@ -147,12 +147,17 @@ where } } +/// A handle to a subscription created by GPUI. When dropped, the subscription +/// is cancelled and the callback will no longer be invoked. #[must_use] pub struct Subscription { unsubscribe: Option>, } impl Subscription { + /// Detaches the subscription from this handle. The callback will + /// continue to be invoked until the views or models it has been + /// subscribed to are dropped pub fn detach(mut self) { self.unsubscribe.take(); } diff --git a/crates/gpui/src/svg_renderer.rs b/crates/gpui/src/svg_renderer.rs index 96e6658ab1e20290d6e1d19b6cf30263d69b6b3c..978d5a626bfb81a04f29bfc047279fa05f6ef00d 100644 --- a/crates/gpui/src/svg_renderer.rs +++ b/crates/gpui/src/svg_renderer.rs @@ -3,12 +3,12 @@ use anyhow::anyhow; use std::{hash::Hash, sync::Arc}; #[derive(Clone, PartialEq, Hash, Eq)] -pub struct RenderSvgParams { +pub(crate) struct RenderSvgParams { pub(crate) path: SharedString, pub(crate) size: Size, } -pub struct SvgRenderer { +pub(crate) struct SvgRenderer { asset_source: Arc, } diff --git a/crates/gpui/src/taffy.rs b/crates/gpui/src/taffy.rs index 26d5a2e69ea2dcc8e1664b4fa8ef885f84e8feeb..ea7a4575cc3a22ae820e37f7a26e1f4f1a78a2d7 100644 --- a/crates/gpui/src/taffy.rs +++ b/crates/gpui/src/taffy.rs @@ -229,6 +229,7 @@ impl TaffyLayoutEngine { } } +/// A unique identifier for a layout node, generated when requesting a layout from Taffy #[derive(Copy, Clone, Eq, PartialEq, Debug)] #[repr(transparent)] pub struct LayoutId(NodeId); @@ -440,6 +441,7 @@ where } } +/// The space available for an element to be laid out in #[derive(Copy, Clone, Default, Debug, Eq, PartialEq)] pub enum AvailableSpace { /// The amount of space available is the specified number of pixels diff --git a/crates/gpui/src/test.rs b/crates/gpui/src/test.rs index f53d19fdc8b1028bdddba0957719e05b3af4d69d..77540704f9c9e2543b102a3ee352570b144d62f2 100644 --- a/crates/gpui/src/test.rs +++ b/crates/gpui/src/test.rs @@ -34,6 +34,9 @@ use std::{ panic::{self, RefUnwindSafe}, }; +/// Run the given test function with the configured parameters. +/// This is intended for use with the `gpui::test` macro +/// and generally should not be used directly. pub fn run_test( mut num_iterations: u64, max_retries: usize, @@ -78,6 +81,7 @@ pub fn run_test( } } +/// A test struct for converting an observation callback into a stream. pub struct Observation { rx: channel::Receiver, _subscription: Subscription, diff --git a/crates/gpui/src/text_system.rs b/crates/gpui/src/text_system.rs index 1c9de5ea0495f60fc8d219891ad8a0c7406ae9fe..cadc000f9a203f879a83e529c325061f7db0eeba 100644 --- a/crates/gpui/src/text_system.rs +++ b/crates/gpui/src/text_system.rs @@ -377,7 +377,7 @@ impl TextSystem { Ok(lines) } - pub fn finish_frame(&self, reused_views: &FxHashSet) { + pub(crate) fn finish_frame(&self, reused_views: &FxHashSet) { self.line_layout_cache.finish_frame(reused_views) } diff --git a/crates/gpui/src/text_system/line.rs b/crates/gpui/src/text_system/line.rs index a58d77f585ef79485fa264d4e57f69c1ee4cb0c8..8013f5a1e05e3e61276d89db4d3cb76c26b7ba80 100644 --- a/crates/gpui/src/text_system/line.rs +++ b/crates/gpui/src/text_system/line.rs @@ -1,6 +1,6 @@ use crate::{ - black, fill, point, px, size, BorrowWindow, Bounds, Hsla, LineLayout, Pixels, Point, Result, - SharedString, UnderlineStyle, WindowContext, WrapBoundary, WrappedLineLayout, + black, fill, point, px, size, Bounds, ElementContext, Hsla, LineLayout, Pixels, Point, Result, + SharedString, UnderlineStyle, WrapBoundary, WrappedLineLayout, }; use derive_more::{Deref, DerefMut}; use smallvec::SmallVec; @@ -24,6 +24,7 @@ pub struct ShapedLine { } impl ShapedLine { + /// The length of the line in utf-8 bytes. pub fn len(&self) -> usize { self.layout.len } @@ -32,7 +33,7 @@ impl ShapedLine { &self, origin: Point, line_height: Pixels, - cx: &mut WindowContext, + cx: &mut ElementContext, ) -> Result<()> { paint_line( origin, @@ -65,7 +66,7 @@ impl WrappedLine { &self, origin: Point, line_height: Pixels, - cx: &mut WindowContext, + cx: &mut ElementContext, ) -> Result<()> { paint_line( origin, @@ -86,7 +87,7 @@ fn paint_line( line_height: Pixels, decoration_runs: &[DecorationRun], wrap_boundaries: &[WrapBoundary], - cx: &mut WindowContext<'_>, + cx: &mut ElementContext<'_>, ) -> Result<()> { let padding_top = (line_height - layout.ascent - layout.descent) / 2.; let baseline_offset = point(px(0.), padding_top + layout.ascent); diff --git a/crates/gpui/src/util.rs b/crates/gpui/src/util.rs index cba7ed84b58b77f6491380277ace81341e8041c5..4bff3da740b4d60458f74427cbdfb2eb25418a0e 100644 --- a/crates/gpui/src/util.rs +++ b/crates/gpui/src/util.rs @@ -9,6 +9,58 @@ use smol::future::FutureExt; pub use util::*; +/// A helper trait for building complex objects with imperative conditionals in a fluent style. +pub trait FluentBuilder { + /// Imperatively modify self with the given closure. + fn map(self, f: impl FnOnce(Self) -> U) -> U + where + Self: Sized, + { + f(self) + } + + /// Conditionally modify self with the given closure. + fn when(self, condition: bool, then: impl FnOnce(Self) -> Self) -> Self + where + Self: Sized, + { + self.map(|this| if condition { then(this) } else { this }) + } + + /// Conditionally unwrap and modify self with the given closure, if the given option is Some. + fn when_some(self, option: Option, then: impl FnOnce(Self, T) -> Self) -> Self + where + Self: Sized, + { + self.map(|this| { + if let Some(value) = option { + then(this, value) + } else { + this + } + }) + } + + /// Conditionally modify self with one closure or another + fn when_else( + self, + condition: bool, + then: impl FnOnce(Self) -> Self, + otherwise: impl FnOnce(Self) -> Self, + ) -> Self + where + Self: Sized, + { + self.map(|this| { + if condition { + then(this) + } else { + otherwise(this) + } + }) + } +} + #[cfg(any(test, feature = "test-support"))] pub async fn timeout(timeout: Duration, f: F) -> Result where diff --git a/crates/gpui/src/view.rs b/crates/gpui/src/view.rs index e485aa3e57525118ea777f402a44d7d45928c5b6..47f2809038d650e1f74de96019f134e05f9d6a20 100644 --- a/crates/gpui/src/view.rs +++ b/crates/gpui/src/view.rs @@ -1,10 +1,8 @@ -#![deny(missing_docs)] - use crate::{ - seal::Sealed, AnyElement, AnyModel, AnyWeakModel, AppContext, AvailableSpace, BorrowWindow, - Bounds, ContentMask, Element, ElementId, Entity, EntityId, Flatten, FocusHandle, FocusableView, - IntoElement, LayoutId, Model, Pixels, Point, Render, Size, StackingOrder, Style, TextStyle, - ViewContext, VisualContext, WeakModel, WindowContext, + seal::Sealed, AnyElement, AnyModel, AnyWeakModel, AppContext, AvailableSpace, Bounds, + ContentMask, Element, ElementContext, ElementId, Entity, EntityId, Flatten, FocusHandle, + FocusableView, IntoElement, LayoutId, Model, Pixels, Point, Render, Size, StackingOrder, Style, + TextStyle, ViewContext, VisualContext, WeakModel, }; use anyhow::{Context, Result}; use std::{ @@ -25,6 +23,7 @@ impl Sealed for View {} #[doc(hidden)] pub struct AnyViewState { root_style: Style, + next_stacking_order_id: u16, cache_key: Option, element: Option, } @@ -64,7 +63,7 @@ impl View { Entity::downgrade(self) } - /// Update the view's state with the given function, which is passed a mutable reference and a context. + /// Updates the view's state with the given function, which is passed a mutable reference and a context. pub fn update( &self, cx: &mut C, @@ -96,7 +95,7 @@ impl Element for View { fn request_layout( &mut self, _state: Option, - cx: &mut WindowContext, + cx: &mut ElementContext, ) -> (LayoutId, Self::State) { cx.with_view_id(self.entity_id(), |cx| { let mut element = self.update(cx, |view, cx| view.render(cx).into_any_element()); @@ -105,7 +104,7 @@ impl Element for View { }) } - fn paint(&mut self, _: Bounds, element: &mut Self::State, cx: &mut WindowContext) { + fn paint(&mut self, _: Bounds, element: &mut Self::State, cx: &mut ElementContext) { cx.paint_view(self.entity_id(), |cx| element.take().unwrap().paint(cx)); } } @@ -156,7 +155,7 @@ impl WeakView { Entity::upgrade_from(self) } - /// Update this view's state if it hasn't been released. + /// Updates this view's state if it hasn't been released. /// Returns an error if this view has been released. pub fn update( &self, @@ -204,7 +203,7 @@ impl Eq for WeakView {} #[derive(Clone, Debug)] pub struct AnyView { model: AnyModel, - request_layout: fn(&AnyView, &mut WindowContext) -> (LayoutId, AnyElement), + request_layout: fn(&AnyView, &mut ElementContext) -> (LayoutId, AnyElement), cache: bool, } @@ -252,7 +251,7 @@ impl AnyView { &self, origin: Point, available_space: Size, - cx: &mut WindowContext, + cx: &mut ElementContext, ) { cx.paint_view(self.entity_id(), |cx| { cx.with_absolute_element_offset(origin, |cx| { @@ -280,7 +279,7 @@ impl Element for AnyView { fn request_layout( &mut self, state: Option, - cx: &mut WindowContext, + cx: &mut ElementContext, ) -> (LayoutId, Self::State) { cx.with_view_id(self.entity_id(), |cx| { if self.cache { @@ -294,6 +293,7 @@ impl Element for AnyView { let root_style = cx.layout_style(layout_id).unwrap().clone(); let state = AnyViewState { root_style, + next_stacking_order_id: 0, cache_key: None, element: Some(element), }; @@ -301,7 +301,7 @@ impl Element for AnyView { }) } - fn paint(&mut self, bounds: Bounds, state: &mut Self::State, cx: &mut WindowContext) { + fn paint(&mut self, bounds: Bounds, state: &mut Self::State, cx: &mut ElementContext) { cx.paint_view(self.entity_id(), |cx| { if !self.cache { state.element.take().unwrap().paint(cx); @@ -316,14 +316,19 @@ impl Element for AnyView { && !cx.window.dirty_views.contains(&self.entity_id()) && !cx.window.refreshing { - cx.reuse_view(); + cx.reuse_view(state.next_stacking_order_id); return; } } - let mut element = (self.request_layout)(self, cx).1; - element.draw(bounds.origin, bounds.size.into(), cx); + if let Some(mut element) = state.element.take() { + element.paint(cx); + } else { + let mut element = (self.request_layout)(self, cx).1; + element.draw(bounds.origin, bounds.size.into(), cx); + } + state.next_stacking_order_id = cx.window.next_frame.next_stacking_order_id; state.cache_key = Some(ViewCacheKey { bounds, stacking_order: cx.stacking_order().clone(), @@ -361,7 +366,7 @@ impl IntoElement for AnyView { /// A weak, dynamically-typed view handle that does not prevent the view from being released. pub struct AnyWeakView { model: AnyWeakModel, - layout: fn(&AnyView, &mut WindowContext) -> (LayoutId, AnyElement), + layout: fn(&AnyView, &mut ElementContext) -> (LayoutId, AnyElement), } impl AnyWeakView { @@ -400,11 +405,11 @@ impl std::fmt::Debug for AnyWeakView { } mod any_view { - use crate::{AnyElement, AnyView, IntoElement, LayoutId, Render, WindowContext}; + use crate::{AnyElement, AnyView, ElementContext, IntoElement, LayoutId, Render}; pub(crate) fn request_layout( view: &AnyView, - cx: &mut WindowContext, + cx: &mut ElementContext, ) -> (LayoutId, AnyElement) { let view = view.clone().downcast::().unwrap(); let mut element = view.update(cx, |view, cx| view.render(cx).into_any_element()); diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 2329a5251ed010791e043bc08b70fb2e5e19d895..5796d139f91f0ca822dec66efb6b048fbb1c6af4 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -1,33 +1,26 @@ -#![deny(missing_docs)] - use crate::{ - px, size, transparent_black, Action, AnyDrag, AnyTooltip, AnyView, AppContext, Arena, - AsyncWindowContext, AvailableSpace, Bounds, BoxShadow, Context, Corners, CursorStyle, - DevicePixels, DispatchActionListener, DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, - Entity, EntityId, EventEmitter, FileDropEvent, Flatten, FontId, GlobalElementId, GlyphId, Hsla, - ImageData, IsZero, KeyBinding, KeyContext, KeyDownEvent, KeyEvent, KeystrokeEvent, LayoutId, - Model, ModelContext, Modifiers, MonochromeSprite, MouseButton, MouseEvent, MouseMoveEvent, - MouseUpEvent, Path, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, - PlatformInputHandler, PlatformWindow, Point, PolychromeSprite, PromptLevel, Quad, Render, - RenderGlyphParams, RenderImageParams, RenderSvgParams, ScaledPixels, Scene, Shadow, - SharedString, Size, Style, SubscriberSet, Subscription, Surface, TaffyLayoutEngine, Task, - Underline, UnderlineStyle, View, VisualContext, WeakView, WindowBounds, WindowOptions, - SUBPIXEL_VARIANTS, + px, size, transparent_black, Action, AnyDrag, AnyView, AppContext, Arena, AsyncWindowContext, + AvailableSpace, Bounds, Context, Corners, CursorStyle, DispatchActionListener, DispatchNodeId, + DispatchTree, DisplayId, Edges, Effect, Entity, EntityId, EventEmitter, FileDropEvent, Flatten, + GlobalElementId, Hsla, KeyBinding, KeyContext, KeyDownEvent, KeyMatch, KeymatchResult, + Keystroke, KeystrokeEvent, Model, ModelContext, Modifiers, MouseButton, MouseMoveEvent, + MouseUpEvent, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformWindow, Point, + PromptLevel, Render, ScaledPixels, SharedString, Size, SubscriberSet, Subscription, + TaffyLayoutEngine, Task, View, VisualContext, WeakView, WindowBounds, WindowOptions, }; use anyhow::{anyhow, Context as _, Result}; -use collections::{FxHashMap, FxHashSet}; +use collections::FxHashSet; use derive_more::{Deref, DerefMut}; use futures::{ channel::{mpsc, oneshot}, StreamExt, }; -use media::core_video::CVImageBuffer; use parking_lot::RwLock; use slotmap::SlotMap; use smallvec::SmallVec; use std::{ any::{Any, TypeId}, - borrow::{Borrow, BorrowMut, Cow}, + borrow::{Borrow, BorrowMut}, cell::RefCell, collections::hash_map::Entry, fmt::{Debug, Display}, @@ -40,33 +33,30 @@ use std::{ atomic::{AtomicUsize, Ordering::SeqCst}, Arc, }, + time::Duration, }; -use util::{post_inc, ResultExt}; +use util::{measure, ResultExt}; + +mod element_cx; +pub use element_cx::*; -const ACTIVE_DRAG_Z_INDEX: u8 = 1; +const ACTIVE_DRAG_Z_INDEX: u16 = 1; /// A global stacking order, which is created by stacking successive z-index values. /// Each z-index will always be interpreted in the context of its parent z-index. -#[derive(Deref, DerefMut, Clone, Ord, PartialOrd, PartialEq, Eq, Default)] -pub struct StackingOrder { - #[deref] - #[deref_mut] - context_stack: SmallVec<[u8; 64]>, - id: u32, +#[derive(Debug, Deref, DerefMut, Clone, Ord, PartialOrd, PartialEq, Eq, Default)] +pub struct StackingOrder(SmallVec<[StackingContext; 64]>); + +/// A single entry in a primitive's z-index stacking order +#[derive(Clone, Ord, PartialOrd, PartialEq, Eq, Default)] +pub struct StackingContext { + pub(crate) z_index: u16, + pub(crate) id: u16, } -impl std::fmt::Debug for StackingOrder { +impl std::fmt::Debug for StackingContext { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let mut stacks = self.context_stack.iter().peekable(); - write!(f, "[({}): ", self.id)?; - while let Some(z_index) = stacks.next() { - write!(f, "{z_index}")?; - if stacks.peek().is_some() { - write!(f, "->")?; - } - } - write!(f, "]")?; - Ok(()) + write!(f, "{{{}.{}}} ", self.z_index, self.id) } } @@ -100,7 +90,7 @@ impl DispatchPhase { } type AnyObserver = Box bool + 'static>; -type AnyMouseListener = Box; + type AnyWindowFocusListener = Box bool + 'static>; struct FocusEvent { @@ -260,8 +250,8 @@ pub struct Window { pub(crate) platform_window: Box, display_id: DisplayId, sprite_atlas: Arc, - rem_size: Pixels, - viewport_size: Size, + pub(crate) rem_size: Pixels, + pub(crate) viewport_size: Size, layout_engine: Option, pub(crate) root_view: Option, pub(crate) element_id_stack: GlobalElementId, @@ -284,138 +274,54 @@ pub struct Window { activation_observers: SubscriberSet<(), AnyObserver>, pub(crate) focus: Option, focus_enabled: bool, + pending_input: Option, #[cfg(any(test, feature = "test-support"))] pub(crate) focus_invalidated: bool, } -pub(crate) struct ElementStateBox { - inner: Box, - parent_view_id: EntityId, - #[cfg(debug_assertions)] - type_name: &'static str, -} - -struct RequestedInputHandler { - view_id: EntityId, - handler: Option>, -} - -struct TooltipRequest { - view_id: EntityId, - tooltip: AnyTooltip, -} - -pub(crate) struct Frame { +#[derive(Default, Debug)] +struct PendingInput { + keystrokes: SmallVec<[Keystroke; 1]>, + bindings: SmallVec<[KeyBinding; 1]>, focus: Option, - window_active: bool, - pub(crate) element_states: FxHashMap, - mouse_listeners: FxHashMap>, - pub(crate) dispatch_tree: DispatchTree, - pub(crate) scene: Scene, - pub(crate) depth_map: Vec<(StackingOrder, EntityId, Bounds)>, - pub(crate) z_index_stack: StackingOrder, - pub(crate) next_stacking_order_id: u32, - next_root_z_index: u8, - content_mask_stack: Vec>, - element_offset_stack: Vec>, - requested_input_handler: Option, - tooltip_request: Option, - cursor_styles: FxHashMap, - requested_cursor_style: Option, - pub(crate) view_stack: Vec, - pub(crate) reused_views: FxHashSet, - - #[cfg(any(test, feature = "test-support"))] - pub(crate) debug_bounds: collections::FxHashMap>, + timer: Option>, } -impl Frame { - fn new(dispatch_tree: DispatchTree) -> Self { - Frame { - focus: None, - window_active: false, - element_states: FxHashMap::default(), - mouse_listeners: FxHashMap::default(), - dispatch_tree, - scene: Scene::default(), - depth_map: Vec::new(), - z_index_stack: StackingOrder::default(), - next_stacking_order_id: 0, - next_root_z_index: 0, - content_mask_stack: Vec::new(), - element_offset_stack: Vec::new(), - requested_input_handler: None, - tooltip_request: None, - cursor_styles: FxHashMap::default(), - requested_cursor_style: None, - view_stack: Vec::new(), - reused_views: FxHashSet::default(), +impl PendingInput { + fn is_noop(&self) -> bool { + self.bindings.is_empty() && (self.keystrokes.iter().all(|k| k.ime_key.is_none())) + } - #[cfg(any(test, feature = "test-support"))] - debug_bounds: FxHashMap::default(), - } + fn input(&self) -> String { + self.keystrokes + .iter() + .flat_map(|k| k.ime_key.clone()) + .collect::>() + .join("") } - fn clear(&mut self) { - self.element_states.clear(); - self.mouse_listeners.values_mut().for_each(Vec::clear); - self.dispatch_tree.clear(); - self.depth_map.clear(); - self.next_stacking_order_id = 0; - self.next_root_z_index = 0; - self.reused_views.clear(); - self.scene.clear(); - self.requested_input_handler.take(); - self.tooltip_request.take(); - self.cursor_styles.clear(); - self.requested_cursor_style.take(); - debug_assert_eq!(self.view_stack.len(), 0); - } - - fn focus_path(&self) -> SmallVec<[FocusId; 8]> { - self.focus - .map(|focus_id| self.dispatch_tree.focus_path(focus_id)) - .unwrap_or_default() - } - - fn finish(&mut self, prev_frame: &mut Self) { - // Reuse mouse listeners that didn't change since the last frame. - for (type_id, listeners) in &mut prev_frame.mouse_listeners { - let next_listeners = self.mouse_listeners.entry(*type_id).or_default(); - for (order, view_id, listener) in listeners.drain(..) { - if self.reused_views.contains(&view_id) { - next_listeners.push((order, view_id, listener)); - } - } + fn used_by_binding(&self, binding: &KeyBinding) -> bool { + if self.keystrokes.is_empty() { + return true; } - - // Reuse entries in the depth map that didn't change since the last frame. - for (order, view_id, bounds) in prev_frame.depth_map.drain(..) { - if self.reused_views.contains(&view_id) { - match self - .depth_map - .binary_search_by(|(level, _, _)| order.cmp(level)) - { - Ok(i) | Err(i) => self.depth_map.insert(i, (order, view_id, bounds)), - } - } - } - - // Retain element states for views that didn't change since the last frame. - for (element_id, state) in prev_frame.element_states.drain() { - if self.reused_views.contains(&state.parent_view_id) { - self.element_states.entry(element_id).or_insert(state); + let keystroke = &self.keystrokes[0]; + for candidate in keystroke.match_candidates() { + if binding.match_keystrokes(&[candidate]) == KeyMatch::Pending { + return true; } } - - // Reuse geometry that didn't change since the last frame. - self.scene - .reuse_views(&self.reused_views, &mut prev_frame.scene); - self.scene.finish(); + false } } +pub(crate) struct ElementStateBox { + pub(crate) inner: Box, + pub(crate) parent_view_id: EntityId, + #[cfg(debug_assertions)] + pub(crate) type_name: &'static str, +} + impl Window { pub(crate) fn new( handle: AnyWindowHandle, @@ -434,7 +340,9 @@ impl Window { platform_window.on_request_frame(Box::new({ let mut cx = cx.to_async(); move || { - handle.update(&mut cx, |_, cx| cx.draw()).log_err(); + measure("frame duration", || { + handle.update(&mut cx, |_, cx| cx.draw()).log_err(); + }) } })); platform_window.on_resize(Box::new({ @@ -508,6 +416,7 @@ impl Window { activation_observers: SubscriberSet::new(), focus: None, focus_enabled: true, + pending_input: None, #[cfg(any(test, feature = "test-support"))] focus_invalidated: false, @@ -721,7 +630,7 @@ impl<'a> WindowContext<'a> { subscription } - /// Create an `AsyncWindowContext`, which has a static lifetime and can be held across + /// Creates an [`AsyncWindowContext`], which has a static lifetime and can be held across /// await points in async code. pub fn to_async(&self) -> AsyncWindowContext { AsyncWindowContext::new(self.app.to_async(), self.window.handle) @@ -737,7 +646,7 @@ impl<'a> WindowContext<'a> { let (tx, mut rx) = mpsc::unbounded::<()>(); self.platform.set_display_link_output_callback( display_id, - Box::new(move |_current_time, _output_time| _ = tx.unbounded_send(())), + Box::new(move || _ = tx.unbounded_send(())), ); let consumer_task = self.app.spawn(|cx| async move { @@ -794,7 +703,7 @@ impl<'a> WindowContext<'a> { .spawn(|app| f(AsyncWindowContext::new(app, self.window.handle))) } - /// Update the global of the given type. The given closure is given simultaneous mutable + /// Updates the global of the given type. The given closure is given simultaneous mutable /// access both to the global and the context. pub fn update_global(&mut self, f: impl FnOnce(&mut G, &mut Self) -> R) -> R where @@ -806,80 +715,6 @@ impl<'a> WindowContext<'a> { result } - #[must_use] - /// Add a node to the layout tree for the current frame. Takes the `Style` of the element for which - /// layout is being requested, along with the layout ids of any children. This method is called during - /// calls to the `Element::layout` trait method and enables any element to participate in layout. - pub fn request_layout( - &mut self, - style: &Style, - children: impl IntoIterator, - ) -> LayoutId { - self.app.layout_id_buffer.clear(); - self.app.layout_id_buffer.extend(children); - let rem_size = self.rem_size(); - - self.window.layout_engine.as_mut().unwrap().request_layout( - style, - rem_size, - &self.app.layout_id_buffer, - ) - } - - /// Add a node to the layout tree for the current frame. Instead of taking a `Style` and children, - /// this variant takes a function that is invoked during layout so you can use arbitrary logic to - /// determine the element's size. One place this is used internally is when measuring text. - /// - /// The given closure is invoked at layout time with the known dimensions and available space and - /// returns a `Size`. - pub fn request_measured_layout< - F: FnMut(Size>, Size, &mut WindowContext) -> Size - + 'static, - >( - &mut self, - style: Style, - measure: F, - ) -> LayoutId { - let rem_size = self.rem_size(); - self.window - .layout_engine - .as_mut() - .unwrap() - .request_measured_layout(style, rem_size, measure) - } - - pub(crate) fn layout_style(&self, layout_id: LayoutId) -> Option<&Style> { - self.window - .layout_engine - .as_ref() - .unwrap() - .requested_style(layout_id) - } - - /// Compute the layout for the given id within the given available space. - /// This method is called for its side effect, typically by the framework prior to painting. - /// After calling it, you can request the bounds of the given layout node id or any descendant. - pub fn compute_layout(&mut self, layout_id: LayoutId, available_space: Size) { - let mut layout_engine = self.window.layout_engine.take().unwrap(); - layout_engine.compute_layout(layout_id, available_space, self); - self.window.layout_engine = Some(layout_engine); - } - - /// Obtain the bounds computed for the given LayoutId relative to the window. This method should not - /// be invoked until the paint phase begins, and will usually be invoked by GPUI itself automatically - /// in order to pass your element its `Bounds` automatically. - pub fn layout_bounds(&mut self, layout_id: LayoutId) -> Bounds { - let mut bounds = self - .window - .layout_engine - .as_mut() - .unwrap() - .layout_bounds(layout_id) - .map(Into::into); - bounds.origin += self.element_offset(); - bounds - } - fn window_bounds_changed(&mut self) { self.window.scale_factor = self.window.platform_window.scale_factor(); self.window.viewport_size = self.window.platform_window.content_size(); @@ -913,7 +748,7 @@ impl<'a> WindowContext<'a> { self.window.platform_window.zoom(); } - /// Update the window's title at the platform level. + /// Updates the window's title at the platform level. pub fn set_window_title(&mut self, title: &str) { self.window.platform_window.set_title(title); } @@ -975,67 +810,6 @@ impl<'a> WindowContext<'a> { self.window.default_prevented } - /// Register a mouse event listener on the window for the next frame. The type of event - /// is determined by the first parameter of the given listener. When the next frame is rendered - /// the listener will be cleared. - pub fn on_mouse_event( - &mut self, - mut handler: impl FnMut(&Event, DispatchPhase, &mut WindowContext) + 'static, - ) { - let view_id = self.parent_view_id(); - let order = self.window.next_frame.z_index_stack.clone(); - self.window - .next_frame - .mouse_listeners - .entry(TypeId::of::()) - .or_default() - .push(( - order, - view_id, - Box::new( - move |event: &dyn Any, phase: DispatchPhase, cx: &mut WindowContext<'_>| { - handler(event.downcast_ref().unwrap(), phase, cx) - }, - ), - )) - } - - /// Register a key event listener on the window for the next frame. The type of event - /// is determined by the first parameter of the given listener. When the next frame is rendered - /// the listener will be cleared. - /// - /// This is a fairly low-level method, so prefer using event handlers on elements unless you have - /// a specific need to register a global listener. - pub fn on_key_event( - &mut self, - listener: impl Fn(&Event, DispatchPhase, &mut WindowContext) + 'static, - ) { - self.window.next_frame.dispatch_tree.on_key_event(Rc::new( - move |event: &dyn Any, phase, cx: &mut WindowContext<'_>| { - if let Some(event) = event.downcast_ref::() { - listener(event, phase, cx) - } - }, - )); - } - - /// Register an action listener on the window for the next frame. The type of action - /// is determined by the first parameter of the given listener. When the next frame is rendered - /// the listener will be cleared. - /// - /// This is a fairly low-level method, so prefer using action handlers on elements unless you have - /// a specific need to register a global listener. - pub fn on_action( - &mut self, - action_type: TypeId, - listener: impl Fn(&dyn Any, DispatchPhase, &mut WindowContext) + 'static, - ) { - self.window - .next_frame - .dispatch_tree - .on_action(action_type, Rc::new(listener)); - } - /// Determine whether the given action is available along the dispatch path to the currently focused element. pub fn is_action_available(&self, action: &dyn Action) -> bool { let target = self @@ -1063,42 +837,40 @@ impl<'a> WindowContext<'a> { self.window.modifiers } - /// Update the cursor style at the platform level. - pub fn set_cursor_style(&mut self, style: CursorStyle) { - let view_id = self.parent_view_id(); - self.window.next_frame.cursor_styles.insert(view_id, style); - self.window.next_frame.requested_cursor_style = Some(style); - } - - /// Set a tooltip to be rendered for the upcoming frame - pub fn set_tooltip(&mut self, tooltip: AnyTooltip) { - let view_id = self.parent_view_id(); - self.window.next_frame.tooltip_request = Some(TooltipRequest { view_id, tooltip }); - } - - /// Called during painting to track which z-index is on top at each pixel position - pub fn add_opaque_layer(&mut self, bounds: Bounds) { - let stacking_order = self.window.next_frame.z_index_stack.clone(); - let view_id = self.parent_view_id(); - let depth_map = &mut self.window.next_frame.depth_map; - match depth_map.binary_search_by(|(level, _, _)| stacking_order.cmp(level)) { - Ok(i) | Err(i) => depth_map.insert(i, (stacking_order, view_id, bounds)), - } - } - /// Returns true if there is no opaque layer containing the given point - /// on top of the given level. Layers whose level is an extension of the - /// level are not considered to be on top of the level. - pub fn was_top_layer(&self, point: &Point, level: &StackingOrder) -> bool { - for (opaque_level, _, bounds) in self.window.rendered_frame.depth_map.iter() { - if level >= opaque_level { - break; + /// on top of the given level. Layers who are extensions of the queried layer + /// are not considered to be on top of queried layer. + pub fn was_top_layer(&self, point: &Point, layer: &StackingOrder) -> bool { + // Precondition: the depth map is ordered from topmost to bottomost. + + for (opaque_layer, _, bounds) in self.window.rendered_frame.depth_map.iter() { + if layer >= opaque_layer { + // The queried layer is either above or is the same as the this opaque layer. + // Anything after this point is guaranteed to be below the queried layer. + return true; } - if bounds.contains(point) && !opaque_level.starts_with(level) { + if !bounds.contains(point) { + // This opaque layer is above the queried layer but it doesn't contain + // the given position, so we can ignore it even if it's above. + continue; + } + + // At this point, we've established that this opaque layer is on top of the queried layer + // and contains the position: + // - If the opaque layer is an extension of the queried layer, we don't want + // to consider the opaque layer to be on top and so we ignore it. + // - Else, we will bail early and say that the queried layer wasn't the top one. + let opaque_layer_is_extension_of_queried_layer = opaque_layer.len() >= layer.len() + && opaque_layer + .iter() + .zip(layer.iter()) + .all(|(a, b)| a.z_index == b.z_index); + if !opaque_layer_is_extension_of_queried_layer { return false; } } + true } @@ -1111,11 +883,16 @@ impl<'a> WindowContext<'a> { if level >= opaque_level { break; } - if opaque_level.starts_with(&[ACTIVE_DRAG_Z_INDEX]) { + + if opaque_level + .first() + .map(|c| c.z_index == ACTIVE_DRAG_Z_INDEX) + .unwrap_or(false) + { continue; } - if bounds.contains(point) && !opaque_level.starts_with(level) { + if bounds.contains(point) { return false; } } @@ -1127,371 +904,6 @@ impl<'a> WindowContext<'a> { &self.window.next_frame.z_index_stack } - /// Paint one or more drop shadows into the scene for the next frame at the current z-index. - pub fn paint_shadows( - &mut self, - bounds: Bounds, - corner_radii: Corners, - shadows: &[BoxShadow], - ) { - let scale_factor = self.scale_factor(); - let content_mask = self.content_mask(); - let view_id = self.parent_view_id(); - let window = &mut *self.window; - for shadow in shadows { - let mut shadow_bounds = bounds; - shadow_bounds.origin += shadow.offset; - shadow_bounds.dilate(shadow.spread_radius); - window.next_frame.scene.insert( - &window.next_frame.z_index_stack, - Shadow { - view_id: view_id.into(), - layer_id: 0, - order: 0, - bounds: shadow_bounds.scale(scale_factor), - content_mask: content_mask.scale(scale_factor), - corner_radii: corner_radii.scale(scale_factor), - color: shadow.color, - blur_radius: shadow.blur_radius.scale(scale_factor), - }, - ); - } - } - - /// Paint one or more quads into the scene for the next frame at the current stacking context. - /// Quads are colored rectangular regions with an optional background, border, and corner radius. - /// see [`fill`], [`outline`], and [`quad`] to construct this type. - pub fn paint_quad(&mut self, quad: PaintQuad) { - let scale_factor = self.scale_factor(); - let content_mask = self.content_mask(); - let view_id = self.parent_view_id(); - - let window = &mut *self.window; - window.next_frame.scene.insert( - &window.next_frame.z_index_stack, - Quad { - view_id: view_id.into(), - layer_id: 0, - order: 0, - bounds: quad.bounds.scale(scale_factor), - content_mask: content_mask.scale(scale_factor), - background: quad.background, - border_color: quad.border_color, - corner_radii: quad.corner_radii.scale(scale_factor), - border_widths: quad.border_widths.scale(scale_factor), - }, - ); - } - - /// Paint the given `Path` into the scene for the next frame at the current z-index. - pub fn paint_path(&mut self, mut path: Path, color: impl Into) { - let scale_factor = self.scale_factor(); - let content_mask = self.content_mask(); - let view_id = self.parent_view_id(); - - path.content_mask = content_mask; - path.color = color.into(); - path.view_id = view_id.into(); - let window = &mut *self.window; - window - .next_frame - .scene - .insert(&window.next_frame.z_index_stack, path.scale(scale_factor)); - } - - /// Paint an underline into the scene for the next frame at the current z-index. - pub fn paint_underline( - &mut self, - origin: Point, - width: Pixels, - style: &UnderlineStyle, - ) { - let scale_factor = self.scale_factor(); - let height = if style.wavy { - style.thickness * 3. - } else { - style.thickness - }; - let bounds = Bounds { - origin, - size: size(width, height), - }; - let content_mask = self.content_mask(); - let view_id = self.parent_view_id(); - - let window = &mut *self.window; - window.next_frame.scene.insert( - &window.next_frame.z_index_stack, - Underline { - view_id: view_id.into(), - layer_id: 0, - order: 0, - bounds: bounds.scale(scale_factor), - content_mask: content_mask.scale(scale_factor), - thickness: style.thickness.scale(scale_factor), - color: style.color.unwrap_or_default(), - wavy: style.wavy, - }, - ); - } - - /// Paint a monochrome (non-emoji) glyph into the scene for the next frame at the current z-index. - /// The y component of the origin is the baseline of the glyph. - pub fn paint_glyph( - &mut self, - origin: Point, - font_id: FontId, - glyph_id: GlyphId, - font_size: Pixels, - color: Hsla, - ) -> Result<()> { - let scale_factor = self.scale_factor(); - let glyph_origin = origin.scale(scale_factor); - let subpixel_variant = Point { - x: (glyph_origin.x.0.fract() * SUBPIXEL_VARIANTS as f32).floor() as u8, - y: (glyph_origin.y.0.fract() * SUBPIXEL_VARIANTS as f32).floor() as u8, - }; - let params = RenderGlyphParams { - font_id, - glyph_id, - font_size, - subpixel_variant, - scale_factor, - is_emoji: false, - }; - - let raster_bounds = self.text_system().raster_bounds(¶ms)?; - if !raster_bounds.is_zero() { - let tile = - self.window - .sprite_atlas - .get_or_insert_with(¶ms.clone().into(), &mut || { - let (size, bytes) = self.text_system().rasterize_glyph(¶ms)?; - Ok((size, Cow::Owned(bytes))) - })?; - let bounds = Bounds { - origin: glyph_origin.map(|px| px.floor()) + raster_bounds.origin.map(Into::into), - size: tile.bounds.size.map(Into::into), - }; - let content_mask = self.content_mask().scale(scale_factor); - let view_id = self.parent_view_id(); - let window = &mut *self.window; - window.next_frame.scene.insert( - &window.next_frame.z_index_stack, - MonochromeSprite { - view_id: view_id.into(), - layer_id: 0, - order: 0, - bounds, - content_mask, - color, - tile, - }, - ); - } - Ok(()) - } - - /// Paint an emoji glyph into the scene for the next frame at the current z-index. - /// The y component of the origin is the baseline of the glyph. - pub fn paint_emoji( - &mut self, - origin: Point, - font_id: FontId, - glyph_id: GlyphId, - font_size: Pixels, - ) -> Result<()> { - let scale_factor = self.scale_factor(); - let glyph_origin = origin.scale(scale_factor); - let params = RenderGlyphParams { - font_id, - glyph_id, - font_size, - // We don't render emojis with subpixel variants. - subpixel_variant: Default::default(), - scale_factor, - is_emoji: true, - }; - - let raster_bounds = self.text_system().raster_bounds(¶ms)?; - if !raster_bounds.is_zero() { - let tile = - self.window - .sprite_atlas - .get_or_insert_with(¶ms.clone().into(), &mut || { - let (size, bytes) = self.text_system().rasterize_glyph(¶ms)?; - Ok((size, Cow::Owned(bytes))) - })?; - let bounds = Bounds { - origin: glyph_origin.map(|px| px.floor()) + raster_bounds.origin.map(Into::into), - size: tile.bounds.size.map(Into::into), - }; - let content_mask = self.content_mask().scale(scale_factor); - let view_id = self.parent_view_id(); - let window = &mut *self.window; - - window.next_frame.scene.insert( - &window.next_frame.z_index_stack, - PolychromeSprite { - view_id: view_id.into(), - layer_id: 0, - order: 0, - bounds, - corner_radii: Default::default(), - content_mask, - tile, - grayscale: false, - }, - ); - } - Ok(()) - } - - /// Paint a monochrome SVG into the scene for the next frame at the current stacking context. - pub fn paint_svg( - &mut self, - bounds: Bounds, - path: SharedString, - color: Hsla, - ) -> Result<()> { - let scale_factor = self.scale_factor(); - let bounds = bounds.scale(scale_factor); - // Render the SVG at twice the size to get a higher quality result. - let params = RenderSvgParams { - path, - size: bounds - .size - .map(|pixels| DevicePixels::from((pixels.0 * 2.).ceil() as i32)), - }; - - let tile = - self.window - .sprite_atlas - .get_or_insert_with(¶ms.clone().into(), &mut || { - let bytes = self.svg_renderer.render(¶ms)?; - Ok((params.size, Cow::Owned(bytes))) - })?; - let content_mask = self.content_mask().scale(scale_factor); - let view_id = self.parent_view_id(); - - let window = &mut *self.window; - window.next_frame.scene.insert( - &window.next_frame.z_index_stack, - MonochromeSprite { - view_id: view_id.into(), - layer_id: 0, - order: 0, - bounds, - content_mask, - color, - tile, - }, - ); - - Ok(()) - } - - /// Paint an image into the scene for the next frame at the current z-index. - pub fn paint_image( - &mut self, - bounds: Bounds, - corner_radii: Corners, - data: Arc, - grayscale: bool, - ) -> Result<()> { - let scale_factor = self.scale_factor(); - let bounds = bounds.scale(scale_factor); - let params = RenderImageParams { image_id: data.id }; - - let tile = self - .window - .sprite_atlas - .get_or_insert_with(¶ms.clone().into(), &mut || { - Ok((data.size(), Cow::Borrowed(data.as_bytes()))) - })?; - let content_mask = self.content_mask().scale(scale_factor); - let corner_radii = corner_radii.scale(scale_factor); - let view_id = self.parent_view_id(); - - let window = &mut *self.window; - window.next_frame.scene.insert( - &window.next_frame.z_index_stack, - PolychromeSprite { - view_id: view_id.into(), - layer_id: 0, - order: 0, - bounds, - content_mask, - corner_radii, - tile, - grayscale, - }, - ); - Ok(()) - } - - /// Paint a surface into the scene for the next frame at the current z-index. - pub fn paint_surface(&mut self, bounds: Bounds, image_buffer: CVImageBuffer) { - let scale_factor = self.scale_factor(); - let bounds = bounds.scale(scale_factor); - let content_mask = self.content_mask().scale(scale_factor); - let view_id = self.parent_view_id(); - let window = &mut *self.window; - window.next_frame.scene.insert( - &window.next_frame.z_index_stack, - Surface { - view_id: view_id.into(), - layer_id: 0, - order: 0, - bounds, - content_mask, - image_buffer, - }, - ); - } - - pub(crate) fn reuse_view(&mut self) { - let view_id = self.parent_view_id(); - let grafted_view_ids = self - .window - .next_frame - .dispatch_tree - .reuse_view(view_id, &mut self.window.rendered_frame.dispatch_tree); - for view_id in grafted_view_ids { - assert!(self.window.next_frame.reused_views.insert(view_id)); - - // Reuse the previous input handler requested during painting of the reused view. - if self - .window - .rendered_frame - .requested_input_handler - .as_ref() - .map_or(false, |requested| requested.view_id == view_id) - { - self.window.next_frame.requested_input_handler = - self.window.rendered_frame.requested_input_handler.take(); - } - - // Reuse the tooltip previously requested during painting of the reused view. - if self - .window - .rendered_frame - .tooltip_request - .as_ref() - .map_or(false, |requested| requested.view_id == view_id) - { - self.window.next_frame.tooltip_request = - self.window.rendered_frame.tooltip_request.take(); - } - - // Reuse the cursor styles previously requested during painting of the reused view. - if let Some(style) = self.window.rendered_frame.cursor_styles.remove(&view_id) { - self.window.next_frame.cursor_styles.insert(view_id, style); - self.window.next_frame.requested_cursor_style = Some(style); - } - } - } - /// Draw pixels to the display for this window based on the contents of its scene. pub(crate) fn draw(&mut self) { self.window.dirty = false; @@ -1504,44 +916,55 @@ impl<'a> WindowContext<'a> { if let Some(requested_handler) = self.window.rendered_frame.requested_input_handler.as_mut() { - requested_handler.handler = self.window.platform_window.take_input_handler(); + let input_handler = self.window.platform_window.take_input_handler(); + requested_handler.handler = input_handler; } let root_view = self.window.root_view.take().unwrap(); - - self.with_z_index(0, |cx| { - cx.with_key_dispatch(Some(KeyContext::default()), None, |_, cx| { - for (action_type, action_listeners) in &cx.app.global_action_listeners { - for action_listener in action_listeners.iter().cloned() { - cx.window.next_frame.dispatch_tree.on_action( - *action_type, - Rc::new(move |action: &dyn Any, phase, cx: &mut WindowContext<'_>| { - action_listener(action, phase, cx) - }), - ) + self.with_element_context(|cx| { + cx.with_z_index(0, |cx| { + cx.with_key_dispatch(Some(KeyContext::default()), None, |_, cx| { + // We need to use cx.cx here so we can utilize borrow splitting + for (action_type, action_listeners) in &cx.cx.app.global_action_listeners { + for action_listener in action_listeners.iter().cloned() { + cx.cx.window.next_frame.dispatch_tree.on_action( + *action_type, + Rc::new( + move |action: &dyn Any, phase, cx: &mut WindowContext<'_>| { + action_listener(action, phase, cx) + }, + ), + ) + } } - } - let available_space = cx.window.viewport_size.map(Into::into); - root_view.draw(Point::default(), available_space, cx); + let available_space = cx.window.viewport_size.map(Into::into); + root_view.draw(Point::default(), available_space, cx); + }) }) }); if let Some(active_drag) = self.app.active_drag.take() { - self.with_z_index(ACTIVE_DRAG_Z_INDEX, |cx| { - let offset = cx.mouse_position() - active_drag.cursor_offset; - let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent); - active_drag.view.draw(offset, available_space, cx); + self.with_element_context(|cx| { + cx.with_z_index(ACTIVE_DRAG_Z_INDEX, |cx| { + let offset = cx.mouse_position() - active_drag.cursor_offset; + let available_space = + size(AvailableSpace::MinContent, AvailableSpace::MinContent); + active_drag.view.draw(offset, available_space, cx); + }) }); self.active_drag = Some(active_drag); } else if let Some(tooltip_request) = self.window.next_frame.tooltip_request.take() { - self.with_z_index(1, |cx| { - let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent); - tooltip_request.tooltip.view.draw( - tooltip_request.tooltip.cursor_offset, - available_space, - cx, - ); + self.with_element_context(|cx| { + cx.with_z_index(1, |cx| { + let available_space = + size(AvailableSpace::MinContent, AvailableSpace::MinContent); + tooltip_request.tooltip.view.draw( + tooltip_request.tooltip.cursor_offset, + available_space, + cx, + ); + }) }); self.window.next_frame.tooltip_request = Some(tooltip_request); } @@ -1732,7 +1155,9 @@ impl<'a> WindowContext<'a> { // Capture phase, events bubble from back to front. Handlers for this phase are used for // special purposes, such as detecting events outside of a given Bounds. for (_, _, handler) in &mut handlers { - handler(event, DispatchPhase::Capture, self); + self.with_element_context(|cx| { + handler(event, DispatchPhase::Capture, cx); + }); if !self.app.propagate_event { break; } @@ -1741,7 +1166,9 @@ impl<'a> WindowContext<'a> { // Bubble phase, where most normal handlers do their work. if self.app.propagate_event { for (_, _, handler) in handlers.iter_mut().rev() { - handler(event, DispatchPhase::Bubble, self); + self.with_element_context(|cx| { + handler(event, DispatchPhase::Bubble, cx); + }); if !self.app.propagate_event { break; } @@ -1786,44 +1213,67 @@ impl<'a> WindowContext<'a> { .dispatch_tree .dispatch_path(node_id); - let mut actions: Vec> = Vec::new(); - - let mut context_stack: SmallVec<[KeyContext; 16]> = SmallVec::new(); - for node_id in &dispatch_path { - let node = self.window.rendered_frame.dispatch_tree.node(*node_id); + if let Some(key_down_event) = event.downcast_ref::() { + let KeymatchResult { bindings, pending } = self + .window + .rendered_frame + .dispatch_tree + .dispatch_key(&key_down_event.keystroke, &dispatch_path); - if let Some(context) = node.context.clone() { - context_stack.push(context); - } - } + if pending { + let mut currently_pending = self.window.pending_input.take().unwrap_or_default(); + if currently_pending.focus.is_some() && currently_pending.focus != self.window.focus + { + currently_pending = PendingInput::default(); + } + currently_pending.focus = self.window.focus; + currently_pending + .keystrokes + .push(key_down_event.keystroke.clone()); + for binding in bindings { + currently_pending.bindings.push(binding); + } - for node_id in dispatch_path.iter().rev() { - // Match keystrokes - let node = self.window.rendered_frame.dispatch_tree.node(*node_id); - if node.context.is_some() { - if let Some(key_down_event) = event.downcast_ref::() { - let mut new_actions = self - .window - .rendered_frame - .dispatch_tree - .dispatch_key(&key_down_event.keystroke, &context_stack); - actions.append(&mut new_actions); + // for vim compatibility, we also should check "is input handler enabled" + if !currently_pending.is_noop() { + currently_pending.timer = Some(self.spawn(|mut cx| async move { + cx.background_executor.timer(Duration::from_secs(1)).await; + cx.update(move |cx| { + cx.clear_pending_keystrokes(); + let Some(currently_pending) = cx.window.pending_input.take() else { + return; + }; + cx.replay_pending_input(currently_pending) + }) + .log_err(); + })); + } else { + currently_pending.timer = None; } + self.window.pending_input = Some(currently_pending); - context_stack.pop(); + self.propagate_event = false; + return; + } else if let Some(currently_pending) = self.window.pending_input.take() { + if bindings + .iter() + .all(|binding| !currently_pending.used_by_binding(&binding)) + { + self.replay_pending_input(currently_pending) + } } - } - if !actions.is_empty() { - self.clear_pending_keystrokes(); - } + if !bindings.is_empty() { + self.clear_pending_keystrokes(); + } - self.propagate_event = true; - for action in actions { - self.dispatch_action_on_node(node_id, action.boxed_clone()); - if !self.propagate_event { - self.dispatch_keystroke_observers(event, Some(action)); - return; + self.propagate_event = true; + for binding in bindings { + self.dispatch_action_on_node(node_id, binding.action.boxed_clone()); + if !self.propagate_event { + self.dispatch_keystroke_observers(event, Some(binding.action)); + return; + } } } @@ -1832,7 +1282,9 @@ impl<'a> WindowContext<'a> { let node = self.window.rendered_frame.dispatch_tree.node(*node_id); for key_listener in node.key_listeners.clone() { - key_listener(event, DispatchPhase::Capture, self); + self.with_element_context(|cx| { + key_listener(event, DispatchPhase::Capture, cx); + }); if !self.propagate_event { return; } @@ -1844,7 +1296,9 @@ impl<'a> WindowContext<'a> { // Handle low level key events let node = self.window.rendered_frame.dispatch_tree.node(*node_id); for key_listener in node.key_listeners.clone() { - key_listener(event, DispatchPhase::Bubble, self); + self.with_element_context(|cx| { + key_listener(event, DispatchPhase::Bubble, cx); + }); if !self.propagate_event { return; } @@ -1862,6 +1316,40 @@ impl<'a> WindowContext<'a> { .has_pending_keystrokes() } + fn replay_pending_input(&mut self, currently_pending: PendingInput) { + let node_id = self + .window + .focus + .and_then(|focus_id| { + self.window + .rendered_frame + .dispatch_tree + .focusable_node_id(focus_id) + }) + .unwrap_or_else(|| self.window.rendered_frame.dispatch_tree.root_node_id()); + + if self.window.focus != currently_pending.focus { + return; + } + + let input = currently_pending.input(); + + self.propagate_event = true; + for binding in currently_pending.bindings { + self.dispatch_action_on_node(node_id, binding.action.boxed_clone()); + if !self.propagate_event { + return; + } + } + + if !input.is_empty() { + if let Some(mut input_handler) = self.window.platform_window.take_input_handler() { + input_handler.flush_pending_input(&input, self); + self.window.platform_window.set_input_handler(input_handler) + } + } + } + fn dispatch_action_on_node(&mut self, node_id: DispatchNodeId, action: Box) { let dispatch_path = self .window @@ -1879,7 +1367,10 @@ impl<'a> WindowContext<'a> { { let any_action = action.as_any(); if action_type == any_action.type_id() { - listener(any_action, DispatchPhase::Capture, self); + self.with_element_context(|cx| { + listener(any_action, DispatchPhase::Capture, cx); + }); + if !self.propagate_event { return; } @@ -1897,7 +1388,11 @@ impl<'a> WindowContext<'a> { let any_action = action.as_any(); if action_type == any_action.type_id() { self.propagate_event = false; // Actions stop propagation by default during the bubble phase - listener(any_action, DispatchPhase::Bubble, self); + + self.with_element_context(|cx| { + listener(any_action, DispatchPhase::Bubble, cx); + }); + if !self.propagate_event { return; } @@ -2021,187 +1516,6 @@ impl<'a> WindowContext<'a> { } } - /// Invoke the given function with the given focus handle present on the key dispatch stack. - /// If you want an element to participate in key dispatch, use this method to push its key context and focus handle into the stack during paint. - pub fn with_key_dispatch( - &mut self, - context: Option, - focus_handle: Option, - f: impl FnOnce(Option, &mut Self) -> R, - ) -> R { - let window = &mut self.window; - let focus_id = focus_handle.as_ref().map(|handle| handle.id); - window - .next_frame - .dispatch_tree - .push_node(context.clone(), focus_id, None); - - let result = f(focus_handle, self); - - self.window.next_frame.dispatch_tree.pop_node(); - - result - } - - /// Invoke the given function with the given view id present on the view stack. - /// This is a fairly low-level method used to layout views. - pub fn with_view_id(&mut self, view_id: EntityId, f: impl FnOnce(&mut Self) -> R) -> R { - let text_system = self.text_system().clone(); - text_system.with_view(view_id, || { - if self.window.next_frame.view_stack.last() == Some(&view_id) { - return f(self); - } else { - self.window.next_frame.view_stack.push(view_id); - let result = f(self); - self.window.next_frame.view_stack.pop(); - result - } - }) - } - - /// Invoke the given function with the given view id present on the view stack. - /// This is a fairly low-level method used to paint views. - pub fn paint_view(&mut self, view_id: EntityId, f: impl FnOnce(&mut Self) -> R) -> R { - let text_system = self.text_system().clone(); - text_system.with_view(view_id, || { - if self.window.next_frame.view_stack.last() == Some(&view_id) { - return f(self); - } else { - self.window.next_frame.view_stack.push(view_id); - self.window - .next_frame - .dispatch_tree - .push_node(None, None, Some(view_id)); - let result = f(self); - self.window.next_frame.dispatch_tree.pop_node(); - self.window.next_frame.view_stack.pop(); - result - } - }) - } - - /// Update or initialize state for an element with the given id that lives across multiple - /// frames. If an element with this id existed in the rendered frame, its state will be passed - /// to the given closure. The state returned by the closure will be stored so it can be referenced - /// when drawing the next frame. - pub(crate) fn with_element_state( - &mut self, - id: ElementId, - f: impl FnOnce(Option, &mut Self) -> (R, S), - ) -> R - where - S: 'static, - { - self.with_element_id(Some(id), |cx| { - let global_id = cx.window().element_id_stack.clone(); - - if let Some(any) = cx - .window_mut() - .next_frame - .element_states - .remove(&global_id) - .or_else(|| { - cx.window_mut() - .rendered_frame - .element_states - .remove(&global_id) - }) - { - let ElementStateBox { - inner, - parent_view_id, - #[cfg(debug_assertions)] - type_name - } = any; - // Using the extra inner option to avoid needing to reallocate a new box. - let mut state_box = inner - .downcast::>() - .map_err(|_| { - #[cfg(debug_assertions)] - { - anyhow!( - "invalid element state type for id, requested_type {:?}, actual type: {:?}", - std::any::type_name::(), - type_name - ) - } - - #[cfg(not(debug_assertions))] - { - anyhow!( - "invalid element state type for id, requested_type {:?}", - std::any::type_name::(), - ) - } - }) - .unwrap(); - - // Actual: Option <- View - // Requested: () <- AnyElement - let state = state_box - .take() - .expect("element state is already on the stack"); - let (result, state) = f(Some(state), cx); - state_box.replace(state); - cx.window_mut() - .next_frame - .element_states - .insert(global_id, ElementStateBox { - inner: state_box, - parent_view_id, - #[cfg(debug_assertions)] - type_name - }); - result - } else { - let (result, state) = f(None, cx); - let parent_view_id = cx.parent_view_id(); - cx.window_mut() - .next_frame - .element_states - .insert(global_id, - ElementStateBox { - inner: Box::new(Some(state)), - parent_view_id, - #[cfg(debug_assertions)] - type_name: std::any::type_name::() - } - - ); - result - } - }) - } - - fn parent_view_id(&self) -> EntityId { - *self - .window - .next_frame - .view_stack - .last() - .expect("a view should always be on the stack while drawing") - } - - /// Set an input handler, such as [`ElementInputHandler`][element_input_handler], which interfaces with the - /// platform to receive textual input with proper integration with concerns such - /// as IME interactions. This handler will be active for the upcoming frame until the following frame is - /// rendered. - /// - /// [element_input_handler]: crate::ElementInputHandler - pub fn handle_input( - &mut self, - focus_handle: &FocusHandle, - input_handler: impl PlatformInputHandler, - ) { - if focus_handle.is_focused(self) { - let view_id = self.parent_view_id(); - self.window.next_frame.requested_input_handler = Some(RequestedInputHandler { - view_id, - handler: Some(Box::new(input_handler)), - }) - } - } - /// Register a callback that can interrupt the closing of the current window based the returned boolean. /// If the callback returns false, the window won't be closed. pub fn on_window_should_close(&mut self, f: impl Fn(&mut WindowContext) -> bool + 'static) { @@ -2209,7 +1523,7 @@ impl<'a> WindowContext<'a> { self.window .platform_window .on_should_close(Box::new(move || { - this.update(|_, cx| { + this.update(|cx| { // Ensure that the window is removed from the app if it's been closed // by always pre-empting the system close event. if f(cx) { @@ -2220,6 +1534,32 @@ impl<'a> WindowContext<'a> { .unwrap_or(true) })) } + + pub(crate) fn parent_view_id(&self) -> EntityId { + *self + .window + .next_frame + .view_stack + .last() + .expect("a view should always be on the stack while drawing") + } + + /// Register an action listener on the window for the next frame. The type of action + /// is determined by the first parameter of the given listener. When the next frame is rendered + /// the listener will be cleared. + /// + /// This is a fairly low-level method, so prefer using action handlers on elements unless you have + /// a specific need to register a global listener. + pub fn on_action( + &mut self, + action_type: TypeId, + listener: impl Fn(&dyn Any, DispatchPhase, &mut WindowContext) + 'static, + ) { + self.window + .next_frame + .dispatch_tree + .on_action(action_type, Rc::new(listener)); + } } impl Context for WindowContext<'_> { @@ -2322,7 +1662,7 @@ impl VisualContext for WindowContext<'_> { view } - /// Update the given view. Prefer calling `View::update` instead, which calls this method. + /// Updates the given view. Prefer calling [`View::update`] instead, which calls this method. fn update_view( &mut self, view: &View, @@ -2409,149 +1749,6 @@ pub trait BorrowWindow: BorrowMut + BorrowMut { fn window_mut(&mut self) -> &mut Window { self.borrow_mut() } - - /// Pushes the given element id onto the global stack and invokes the given closure - /// with a `GlobalElementId`, which disambiguates the given id in the context of its ancestor - /// ids. Because elements are discarded and recreated on each frame, the `GlobalElementId` is - /// used to associate state with identified elements across separate frames. - fn with_element_id( - &mut self, - id: Option>, - f: impl FnOnce(&mut Self) -> R, - ) -> R { - if let Some(id) = id.map(Into::into) { - let window = self.window_mut(); - window.element_id_stack.push(id); - let result = f(self); - let window: &mut Window = self.borrow_mut(); - window.element_id_stack.pop(); - result - } else { - f(self) - } - } - - /// Invoke the given function with the given content mask after intersecting it - /// with the current mask. - fn with_content_mask( - &mut self, - mask: Option>, - f: impl FnOnce(&mut Self) -> R, - ) -> R { - if let Some(mask) = mask { - let mask = mask.intersect(&self.content_mask()); - self.window_mut().next_frame.content_mask_stack.push(mask); - let result = f(self); - self.window_mut().next_frame.content_mask_stack.pop(); - result - } else { - f(self) - } - } - - /// Invoke the given function with the content mask reset to that - /// of the window. - fn break_content_mask(&mut self, f: impl FnOnce(&mut Self) -> R) -> R { - let mask = ContentMask { - bounds: Bounds { - origin: Point::default(), - size: self.window().viewport_size, - }, - }; - let new_stacking_order_id = - post_inc(&mut self.window_mut().next_frame.next_stacking_order_id); - let new_root_z_index = post_inc(&mut self.window_mut().next_frame.next_root_z_index); - let old_stacking_order = mem::take(&mut self.window_mut().next_frame.z_index_stack); - self.window_mut().next_frame.z_index_stack.id = new_stacking_order_id; - self.window_mut() - .next_frame - .z_index_stack - .push(new_root_z_index); - self.window_mut().next_frame.content_mask_stack.push(mask); - let result = f(self); - self.window_mut().next_frame.content_mask_stack.pop(); - self.window_mut().next_frame.z_index_stack = old_stacking_order; - result - } - - /// Called during painting to invoke the given closure in a new stacking context. The given - /// z-index is interpreted relative to the previous call to `stack`. - fn with_z_index(&mut self, z_index: u8, f: impl FnOnce(&mut Self) -> R) -> R { - let new_stacking_order_id = - post_inc(&mut self.window_mut().next_frame.next_stacking_order_id); - let old_stacking_order_id = mem::replace( - &mut self.window_mut().next_frame.z_index_stack.id, - new_stacking_order_id, - ); - self.window_mut().next_frame.z_index_stack.id = new_stacking_order_id; - self.window_mut().next_frame.z_index_stack.push(z_index); - let result = f(self); - self.window_mut().next_frame.z_index_stack.id = old_stacking_order_id; - self.window_mut().next_frame.z_index_stack.pop(); - result - } - - /// Update the global element offset relative to the current offset. This is used to implement - /// scrolling. - fn with_element_offset( - &mut self, - offset: Point, - f: impl FnOnce(&mut Self) -> R, - ) -> R { - if offset.is_zero() { - return f(self); - }; - - let abs_offset = self.element_offset() + offset; - self.with_absolute_element_offset(abs_offset, f) - } - - /// Update the global element offset based on the given offset. This is used to implement - /// drag handles and other manual painting of elements. - fn with_absolute_element_offset( - &mut self, - offset: Point, - f: impl FnOnce(&mut Self) -> R, - ) -> R { - self.window_mut() - .next_frame - .element_offset_stack - .push(offset); - let result = f(self); - self.window_mut().next_frame.element_offset_stack.pop(); - result - } - - /// Obtain the current element offset. - fn element_offset(&self) -> Point { - self.window() - .next_frame - .element_offset_stack - .last() - .copied() - .unwrap_or_default() - } - - /// Obtain the current content mask. - fn content_mask(&self) -> ContentMask { - self.window() - .next_frame - .content_mask_stack - .last() - .cloned() - .unwrap_or_else(|| ContentMask { - bounds: Bounds { - origin: Point::default(), - size: self.window().viewport_size, - }, - }) - } - - /// The size of an em for the base font of the application. Adjusting this value allows the - /// UI to scale, just like zooming a web page. - fn rem_size(&self) -> Pixels { - self.window().rem_size - } } impl Borrow for WindowContext<'_> { @@ -2629,7 +1826,7 @@ impl<'a, V: 'static> ViewContext<'a, V> { &mut self.window_cx } - /// Set a given callback to be run on the next frame. + /// Sets a given callback to be run on the next frame. pub fn on_next_frame(&mut self, f: impl FnOnce(&mut V, &mut ViewContext) + 'static) where V: 'static, @@ -2957,7 +2154,7 @@ impl<'a, V: 'static> ViewContext<'a, V> { self.window_cx.spawn(|cx| f(view, cx)) } - /// Update the global state of the given type. + /// Updates the global state of the given type. pub fn update_global(&mut self, f: impl FnOnce(&mut G, &mut Self) -> R) -> R where G: 'static, @@ -2987,34 +2184,6 @@ impl<'a, V: 'static> ViewContext<'a, V> { subscription } - /// Add a listener for any mouse event that occurs in the window. - /// This is a fairly low level method. - /// Typically, you'll want to use methods on UI elements, which perform bounds checking etc. - pub fn on_mouse_event( - &mut self, - handler: impl Fn(&mut V, &Event, DispatchPhase, &mut ViewContext) + 'static, - ) { - let handle = self.view().clone(); - self.window_cx.on_mouse_event(move |event, phase, cx| { - handle.update(cx, |view, cx| { - handler(view, event, phase, cx); - }) - }); - } - - /// Register a callback to be invoked when the given Key Event is dispatched to the window. - pub fn on_key_event( - &mut self, - handler: impl Fn(&mut V, &Event, DispatchPhase, &mut ViewContext) + 'static, - ) { - let handle = self.view().clone(); - self.window_cx.on_key_event(move |event, phase, cx| { - handle.update(cx, |view, cx| { - handler(view, event, phase, cx); - }) - }); - } - /// Register a callback to be invoked when the given Action type is dispatched to the window. pub fn on_action( &mut self, @@ -3189,7 +2358,7 @@ pub struct WindowHandle { } impl WindowHandle { - /// Create a new handle from a window ID. + /// Creates a new handle from a window ID. /// This does not check if the root type of the window is `V`. pub fn new(id: WindowId) -> Self { WindowHandle { @@ -3215,7 +2384,7 @@ impl WindowHandle { })) } - /// Update the root view of this window. + /// Updates the root view of this window. /// /// This will fail if the window has been closed or if the root view's type does not match pub fn update( @@ -3337,7 +2506,7 @@ impl AnyWindowHandle { } } - /// Update the state of the root view of this window. + /// Updates the state of the root view of this window. /// /// This will fail if the window has been closed. pub fn update( @@ -3477,7 +2646,7 @@ pub struct PaintQuad { } impl PaintQuad { - /// Set the corner radii of the quad. + /// Sets the corner radii of the quad. pub fn corner_radii(self, corner_radii: impl Into>) -> Self { PaintQuad { corner_radii: corner_radii.into(), @@ -3485,7 +2654,7 @@ impl PaintQuad { } } - /// Set the border widths of the quad. + /// Sets the border widths of the quad. pub fn border_widths(self, border_widths: impl Into>) -> Self { PaintQuad { border_widths: border_widths.into(), @@ -3493,7 +2662,7 @@ impl PaintQuad { } } - /// Set the border color of the quad. + /// Sets the border color of the quad. pub fn border_color(self, border_color: impl Into) -> Self { PaintQuad { border_color: border_color.into(), @@ -3501,7 +2670,7 @@ impl PaintQuad { } } - /// Set the background color of the quad. + /// Sets the background color of the quad. pub fn background(self, background: impl Into) -> Self { PaintQuad { background: background.into(), @@ -3510,7 +2679,7 @@ impl PaintQuad { } } -/// Create a quad with the given parameters. +/// Creates a quad with the given parameters. pub fn quad( bounds: Bounds, corner_radii: impl Into>, @@ -3527,7 +2696,7 @@ pub fn quad( } } -/// Create a filled quad with the given bounds and background color. +/// Creates a filled quad with the given bounds and background color. pub fn fill(bounds: impl Into>, background: impl Into) -> PaintQuad { PaintQuad { bounds: bounds.into(), @@ -3538,7 +2707,7 @@ pub fn fill(bounds: impl Into>, background: impl Into) -> P } } -/// Create a rectangle outline with the given bounds, border color, and a 1px border width +/// Creates a rectangle outline with the given bounds, border color, and a 1px border width pub fn outline(bounds: impl Into>, border_color: impl Into) -> PaintQuad { PaintQuad { bounds: bounds.into(), diff --git a/crates/gpui/src/window/element_cx.rs b/crates/gpui/src/window/element_cx.rs new file mode 100644 index 0000000000000000000000000000000000000000..132cc72a5c7d344cf3dedbbc693ff2306b5bb8b9 --- /dev/null +++ b/crates/gpui/src/window/element_cx.rs @@ -0,0 +1,1136 @@ +use std::{ + any::{Any, TypeId}, + borrow::{Borrow, BorrowMut, Cow}, + mem, + rc::Rc, + sync::Arc, +}; + +use anyhow::Result; +use collections::{FxHashMap, FxHashSet}; +use derive_more::{Deref, DerefMut}; +use media::core_video::CVImageBuffer; +use smallvec::SmallVec; +use util::post_inc; + +use crate::{ + prelude::*, size, AnyTooltip, AppContext, AvailableSpace, Bounds, BoxShadow, ContentMask, + Corners, CursorStyle, DevicePixels, DispatchPhase, DispatchTree, ElementId, ElementStateBox, + EntityId, FocusHandle, FocusId, FontId, GlobalElementId, GlyphId, Hsla, ImageData, + InputHandler, IsZero, KeyContext, KeyEvent, LayoutId, MonochromeSprite, MouseEvent, PaintQuad, + Path, Pixels, PlatformInputHandler, Point, PolychromeSprite, Quad, RenderGlyphParams, + RenderImageParams, RenderSvgParams, Scene, Shadow, SharedString, Size, StackingContext, + StackingOrder, Style, Surface, TextStyleRefinement, Underline, UnderlineStyle, Window, + WindowContext, SUBPIXEL_VARIANTS, +}; + +type AnyMouseListener = Box; + +pub(crate) struct RequestedInputHandler { + pub(crate) view_id: EntityId, + pub(crate) handler: Option, +} + +pub(crate) struct TooltipRequest { + pub(crate) view_id: EntityId, + pub(crate) tooltip: AnyTooltip, +} + +pub(crate) struct Frame { + pub(crate) focus: Option, + pub(crate) window_active: bool, + pub(crate) element_states: FxHashMap, + pub(crate) mouse_listeners: FxHashMap>, + pub(crate) dispatch_tree: DispatchTree, + pub(crate) scene: Scene, + pub(crate) depth_map: Vec<(StackingOrder, EntityId, Bounds)>, + pub(crate) z_index_stack: StackingOrder, + pub(crate) next_stacking_order_id: u16, + pub(crate) next_root_z_index: u16, + pub(crate) content_mask_stack: Vec>, + pub(crate) element_offset_stack: Vec>, + pub(crate) requested_input_handler: Option, + pub(crate) tooltip_request: Option, + pub(crate) cursor_styles: FxHashMap, + pub(crate) requested_cursor_style: Option, + pub(crate) view_stack: Vec, + pub(crate) reused_views: FxHashSet, + + #[cfg(any(test, feature = "test-support"))] + pub(crate) debug_bounds: collections::FxHashMap>, +} + +impl Frame { + pub(crate) fn new(dispatch_tree: DispatchTree) -> Self { + Frame { + focus: None, + window_active: false, + element_states: FxHashMap::default(), + mouse_listeners: FxHashMap::default(), + dispatch_tree, + scene: Scene::default(), + depth_map: Vec::new(), + z_index_stack: StackingOrder::default(), + next_stacking_order_id: 0, + next_root_z_index: 0, + content_mask_stack: Vec::new(), + element_offset_stack: Vec::new(), + requested_input_handler: None, + tooltip_request: None, + cursor_styles: FxHashMap::default(), + requested_cursor_style: None, + view_stack: Vec::new(), + reused_views: FxHashSet::default(), + + #[cfg(any(test, feature = "test-support"))] + debug_bounds: FxHashMap::default(), + } + } + + pub(crate) fn clear(&mut self) { + self.element_states.clear(); + self.mouse_listeners.values_mut().for_each(Vec::clear); + self.dispatch_tree.clear(); + self.depth_map.clear(); + self.next_stacking_order_id = 0; + self.next_root_z_index = 0; + self.reused_views.clear(); + self.scene.clear(); + self.requested_input_handler.take(); + self.tooltip_request.take(); + self.cursor_styles.clear(); + self.requested_cursor_style.take(); + debug_assert_eq!(self.view_stack.len(), 0); + } + + pub(crate) fn focus_path(&self) -> SmallVec<[FocusId; 8]> { + self.focus + .map(|focus_id| self.dispatch_tree.focus_path(focus_id)) + .unwrap_or_default() + } + + pub(crate) fn finish(&mut self, prev_frame: &mut Self) { + // Reuse mouse listeners that didn't change since the last frame. + for (type_id, listeners) in &mut prev_frame.mouse_listeners { + let next_listeners = self.mouse_listeners.entry(*type_id).or_default(); + for (order, view_id, listener) in listeners.drain(..) { + if self.reused_views.contains(&view_id) { + next_listeners.push((order, view_id, listener)); + } + } + } + + // Reuse entries in the depth map that didn't change since the last frame. + for (order, view_id, bounds) in prev_frame.depth_map.drain(..) { + if self.reused_views.contains(&view_id) { + match self + .depth_map + .binary_search_by(|(level, _, _)| order.cmp(level)) + { + Ok(i) | Err(i) => self.depth_map.insert(i, (order, view_id, bounds)), + } + } + } + + // Retain element states for views that didn't change since the last frame. + for (element_id, state) in prev_frame.element_states.drain() { + if self.reused_views.contains(&state.parent_view_id) { + self.element_states.entry(element_id).or_insert(state); + } + } + + // Reuse geometry that didn't change since the last frame. + self.scene + .reuse_views(&self.reused_views, &mut prev_frame.scene); + self.scene.finish(); + } +} + +/// This context is used for assisting in the implementation of the element trait +#[derive(Deref, DerefMut)] +pub struct ElementContext<'a> { + pub(crate) cx: WindowContext<'a>, +} + +impl<'a> WindowContext<'a> { + pub fn with_element_context(&mut self, f: impl FnOnce(&mut ElementContext) -> R) -> R { + f(&mut ElementContext { + cx: WindowContext::new(self.app, self.window), + }) + } +} + +impl<'a> Borrow for ElementContext<'a> { + fn borrow(&self) -> &AppContext { + self.cx.app + } +} + +impl<'a> BorrowMut for ElementContext<'a> { + fn borrow_mut(&mut self) -> &mut AppContext { + self.cx.borrow_mut() + } +} + +impl<'a> Borrow> for ElementContext<'a> { + fn borrow(&self) -> &WindowContext<'a> { + &self.cx + } +} + +impl<'a> BorrowMut> for ElementContext<'a> { + fn borrow_mut(&mut self) -> &mut WindowContext<'a> { + &mut self.cx + } +} + +impl<'a> Borrow for ElementContext<'a> { + fn borrow(&self) -> &Window { + self.cx.window + } +} + +impl<'a> BorrowMut for ElementContext<'a> { + fn borrow_mut(&mut self) -> &mut Window { + self.cx.borrow_mut() + } +} + +impl<'a> Context for ElementContext<'a> { + type Result = as Context>::Result; + + fn new_model( + &mut self, + build_model: impl FnOnce(&mut crate::ModelContext<'_, T>) -> T, + ) -> Self::Result> { + self.cx.new_model(build_model) + } + + fn update_model( + &mut self, + handle: &crate::Model, + update: impl FnOnce(&mut T, &mut crate::ModelContext<'_, T>) -> R, + ) -> Self::Result + where + T: 'static, + { + self.cx.update_model(handle, update) + } + + fn read_model( + &self, + handle: &crate::Model, + read: impl FnOnce(&T, &AppContext) -> R, + ) -> Self::Result + where + T: 'static, + { + self.cx.read_model(handle, read) + } + + fn update_window(&mut self, window: crate::AnyWindowHandle, f: F) -> Result + where + F: FnOnce(crate::AnyView, &mut WindowContext<'_>) -> T, + { + self.cx.update_window(window, f) + } + + fn read_window( + &self, + window: &crate::WindowHandle, + read: impl FnOnce(crate::View, &AppContext) -> R, + ) -> Result + where + T: 'static, + { + self.cx.read_window(window, read) + } +} + +impl<'a> VisualContext for ElementContext<'a> { + fn new_view( + &mut self, + build_view: impl FnOnce(&mut crate::ViewContext<'_, V>) -> V, + ) -> Self::Result> + where + V: 'static + Render, + { + self.cx.new_view(build_view) + } + + fn update_view( + &mut self, + view: &crate::View, + update: impl FnOnce(&mut V, &mut crate::ViewContext<'_, V>) -> R, + ) -> Self::Result { + self.cx.update_view(view, update) + } + + fn replace_root_view( + &mut self, + build_view: impl FnOnce(&mut crate::ViewContext<'_, V>) -> V, + ) -> Self::Result> + where + V: 'static + Render, + { + self.cx.replace_root_view(build_view) + } + + fn focus_view(&mut self, view: &crate::View) -> Self::Result<()> + where + V: crate::FocusableView, + { + self.cx.focus_view(view) + } + + fn dismiss_view(&mut self, view: &crate::View) -> Self::Result<()> + where + V: crate::ManagedView, + { + self.cx.dismiss_view(view) + } +} + +impl<'a> ElementContext<'a> { + pub(crate) fn reuse_view(&mut self, next_stacking_order_id: u16) { + let view_id = self.parent_view_id(); + let grafted_view_ids = self + .cx + .window + .next_frame + .dispatch_tree + .reuse_view(view_id, &mut self.cx.window.rendered_frame.dispatch_tree); + for view_id in grafted_view_ids { + assert!(self.window.next_frame.reused_views.insert(view_id)); + + // Reuse the previous input handler requested during painting of the reused view. + if self + .window + .rendered_frame + .requested_input_handler + .as_ref() + .map_or(false, |requested| requested.view_id == view_id) + { + self.window.next_frame.requested_input_handler = + self.window.rendered_frame.requested_input_handler.take(); + } + + // Reuse the tooltip previously requested during painting of the reused view. + if self + .window + .rendered_frame + .tooltip_request + .as_ref() + .map_or(false, |requested| requested.view_id == view_id) + { + self.window.next_frame.tooltip_request = + self.window.rendered_frame.tooltip_request.take(); + } + + // Reuse the cursor styles previously requested during painting of the reused view. + if let Some(style) = self.window.rendered_frame.cursor_styles.remove(&view_id) { + self.window.next_frame.cursor_styles.insert(view_id, style); + self.window.next_frame.requested_cursor_style = Some(style); + } + } + + debug_assert!(next_stacking_order_id >= self.window.next_frame.next_stacking_order_id); + self.window.next_frame.next_stacking_order_id = next_stacking_order_id; + } + + pub fn with_text_style(&mut self, style: Option, f: F) -> R + where + F: FnOnce(&mut Self) -> R, + { + if let Some(style) = style { + self.push_text_style(style); + let result = f(self); + self.pop_text_style(); + result + } else { + f(self) + } + } + + /// Updates the cursor style at the platform level. + pub fn set_cursor_style(&mut self, style: CursorStyle) { + let view_id = self.parent_view_id(); + self.window.next_frame.cursor_styles.insert(view_id, style); + self.window.next_frame.requested_cursor_style = Some(style); + } + + /// Sets a tooltip to be rendered for the upcoming frame + pub fn set_tooltip(&mut self, tooltip: AnyTooltip) { + let view_id = self.parent_view_id(); + self.window.next_frame.tooltip_request = Some(TooltipRequest { view_id, tooltip }); + } + + /// Pushes the given element id onto the global stack and invokes the given closure + /// with a `GlobalElementId`, which disambiguates the given id in the context of its ancestor + /// ids. Because elements are discarded and recreated on each frame, the `GlobalElementId` is + /// used to associate state with identified elements across separate frames. + pub fn with_element_id( + &mut self, + id: Option>, + f: impl FnOnce(&mut Self) -> R, + ) -> R { + if let Some(id) = id.map(Into::into) { + let window = self.window_mut(); + window.element_id_stack.push(id); + let result = f(self); + let window: &mut Window = self.borrow_mut(); + window.element_id_stack.pop(); + result + } else { + f(self) + } + } + + /// Invoke the given function with the given content mask after intersecting it + /// with the current mask. + pub fn with_content_mask( + &mut self, + mask: Option>, + f: impl FnOnce(&mut Self) -> R, + ) -> R { + if let Some(mask) = mask { + let mask = mask.intersect(&self.content_mask()); + self.window_mut().next_frame.content_mask_stack.push(mask); + let result = f(self); + self.window_mut().next_frame.content_mask_stack.pop(); + result + } else { + f(self) + } + } + + /// Invoke the given function with the content mask reset to that + /// of the window. + pub fn break_content_mask(&mut self, f: impl FnOnce(&mut Self) -> R) -> R { + let mask = ContentMask { + bounds: Bounds { + origin: Point::default(), + size: self.window().viewport_size, + }, + }; + + let new_root_z_index = post_inc(&mut self.window_mut().next_frame.next_root_z_index); + let new_stacking_order_id = + post_inc(&mut self.window_mut().next_frame.next_stacking_order_id); + let new_context = StackingContext { + z_index: new_root_z_index, + id: new_stacking_order_id, + }; + + let old_stacking_order = mem::take(&mut self.window_mut().next_frame.z_index_stack); + + self.window_mut().next_frame.z_index_stack.push(new_context); + self.window_mut().next_frame.content_mask_stack.push(mask); + let result = f(self); + self.window_mut().next_frame.content_mask_stack.pop(); + self.window_mut().next_frame.z_index_stack = old_stacking_order; + + result + } + + /// Called during painting to invoke the given closure in a new stacking context. The given + /// z-index is interpreted relative to the previous call to `stack`. + pub fn with_z_index(&mut self, z_index: u16, f: impl FnOnce(&mut Self) -> R) -> R { + let new_stacking_order_id = + post_inc(&mut self.window_mut().next_frame.next_stacking_order_id); + let new_context = StackingContext { + z_index, + id: new_stacking_order_id, + }; + + self.window_mut().next_frame.z_index_stack.push(new_context); + let result = f(self); + self.window_mut().next_frame.z_index_stack.pop(); + + result + } + + /// Updates the global element offset relative to the current offset. This is used to implement + /// scrolling. + pub fn with_element_offset( + &mut self, + offset: Point, + f: impl FnOnce(&mut Self) -> R, + ) -> R { + if offset.is_zero() { + return f(self); + }; + + let abs_offset = self.element_offset() + offset; + self.with_absolute_element_offset(abs_offset, f) + } + + /// Updates the global element offset based on the given offset. This is used to implement + /// drag handles and other manual painting of elements. + pub fn with_absolute_element_offset( + &mut self, + offset: Point, + f: impl FnOnce(&mut Self) -> R, + ) -> R { + self.window_mut() + .next_frame + .element_offset_stack + .push(offset); + let result = f(self); + self.window_mut().next_frame.element_offset_stack.pop(); + result + } + + /// Obtain the current element offset. + pub fn element_offset(&self) -> Point { + self.window() + .next_frame + .element_offset_stack + .last() + .copied() + .unwrap_or_default() + } + + /// Obtain the current content mask. + pub fn content_mask(&self) -> ContentMask { + self.window() + .next_frame + .content_mask_stack + .last() + .cloned() + .unwrap_or_else(|| ContentMask { + bounds: Bounds { + origin: Point::default(), + size: self.window().viewport_size, + }, + }) + } + + /// The size of an em for the base font of the application. Adjusting this value allows the + /// UI to scale, just like zooming a web page. + pub fn rem_size(&self) -> Pixels { + self.window().rem_size + } + + /// Updates or initializes state for an element with the given id that lives across multiple + /// frames. If an element with this ID existed in the rendered frame, its state will be passed + /// to the given closure. The state returned by the closure will be stored so it can be referenced + /// when drawing the next frame. + pub fn with_element_state( + &mut self, + id: ElementId, + f: impl FnOnce(Option, &mut Self) -> (R, S), + ) -> R + where + S: 'static, + { + self.with_element_id(Some(id), |cx| { + let global_id = cx.window().element_id_stack.clone(); + + if let Some(any) = cx + .window_mut() + .next_frame + .element_states + .remove(&global_id) + .or_else(|| { + cx.window_mut() + .rendered_frame + .element_states + .remove(&global_id) + }) + { + let ElementStateBox { + inner, + parent_view_id, + #[cfg(debug_assertions)] + type_name + } = any; + // Using the extra inner option to avoid needing to reallocate a new box. + let mut state_box = inner + .downcast::>() + .map_err(|_| { + #[cfg(debug_assertions)] + { + anyhow::anyhow!( + "invalid element state type for id, requested_type {:?}, actual type: {:?}", + std::any::type_name::(), + type_name + ) + } + + #[cfg(not(debug_assertions))] + { + anyhow::anyhow!( + "invalid element state type for id, requested_type {:?}", + std::any::type_name::(), + ) + } + }) + .unwrap(); + + // Actual: Option <- View + // Requested: () <- AnyElement + let state = state_box + .take() + .expect("element state is already on the stack"); + let (result, state) = f(Some(state), cx); + state_box.replace(state); + cx.window_mut() + .next_frame + .element_states + .insert(global_id, ElementStateBox { + inner: state_box, + parent_view_id, + #[cfg(debug_assertions)] + type_name + }); + result + } else { + let (result, state) = f(None, cx); + let parent_view_id = cx.parent_view_id(); + cx.window_mut() + .next_frame + .element_states + .insert(global_id, + ElementStateBox { + inner: Box::new(Some(state)), + parent_view_id, + #[cfg(debug_assertions)] + type_name: std::any::type_name::() + } + + ); + result + } + }) + } + /// Paint one or more drop shadows into the scene for the next frame at the current z-index. + pub fn paint_shadows( + &mut self, + bounds: Bounds, + corner_radii: Corners, + shadows: &[BoxShadow], + ) { + let scale_factor = self.scale_factor(); + let content_mask = self.content_mask(); + let view_id = self.parent_view_id(); + let window = &mut *self.window; + for shadow in shadows { + let mut shadow_bounds = bounds; + shadow_bounds.origin += shadow.offset; + shadow_bounds.dilate(shadow.spread_radius); + window.next_frame.scene.insert( + &window.next_frame.z_index_stack, + Shadow { + view_id: view_id.into(), + layer_id: 0, + order: 0, + bounds: shadow_bounds.scale(scale_factor), + content_mask: content_mask.scale(scale_factor), + corner_radii: corner_radii.scale(scale_factor), + color: shadow.color, + blur_radius: shadow.blur_radius.scale(scale_factor), + }, + ); + } + } + + /// Paint one or more quads into the scene for the next frame at the current stacking context. + /// Quads are colored rectangular regions with an optional background, border, and corner radius. + /// see [`fill`], [`outline`], and [`quad`] to construct this type. + pub fn paint_quad(&mut self, quad: PaintQuad) { + let scale_factor = self.scale_factor(); + let content_mask = self.content_mask(); + let view_id = self.parent_view_id(); + + let window = &mut *self.window; + window.next_frame.scene.insert( + &window.next_frame.z_index_stack, + Quad { + view_id: view_id.into(), + layer_id: 0, + order: 0, + bounds: quad.bounds.scale(scale_factor), + content_mask: content_mask.scale(scale_factor), + background: quad.background, + border_color: quad.border_color, + corner_radii: quad.corner_radii.scale(scale_factor), + border_widths: quad.border_widths.scale(scale_factor), + }, + ); + } + + /// Paint the given `Path` into the scene for the next frame at the current z-index. + pub fn paint_path(&mut self, mut path: Path, color: impl Into) { + let scale_factor = self.scale_factor(); + let content_mask = self.content_mask(); + let view_id = self.parent_view_id(); + + path.content_mask = content_mask; + path.color = color.into(); + path.view_id = view_id.into(); + let window = &mut *self.window; + window + .next_frame + .scene + .insert(&window.next_frame.z_index_stack, path.scale(scale_factor)); + } + + /// Paint an underline into the scene for the next frame at the current z-index. + pub fn paint_underline( + &mut self, + origin: Point, + width: Pixels, + style: &UnderlineStyle, + ) { + let scale_factor = self.scale_factor(); + let height = if style.wavy { + style.thickness * 3. + } else { + style.thickness + }; + let bounds = Bounds { + origin, + size: size(width, height), + }; + let content_mask = self.content_mask(); + let view_id = self.parent_view_id(); + + let window = &mut *self.window; + window.next_frame.scene.insert( + &window.next_frame.z_index_stack, + Underline { + view_id: view_id.into(), + layer_id: 0, + order: 0, + bounds: bounds.scale(scale_factor), + content_mask: content_mask.scale(scale_factor), + thickness: style.thickness.scale(scale_factor), + color: style.color.unwrap_or_default(), + wavy: style.wavy, + }, + ); + } + + /// Paint a monochrome (non-emoji) glyph into the scene for the next frame at the current z-index. + /// The y component of the origin is the baseline of the glyph. + pub fn paint_glyph( + &mut self, + origin: Point, + font_id: FontId, + glyph_id: GlyphId, + font_size: Pixels, + color: Hsla, + ) -> Result<()> { + let scale_factor = self.scale_factor(); + let glyph_origin = origin.scale(scale_factor); + let subpixel_variant = Point { + x: (glyph_origin.x.0.fract() * SUBPIXEL_VARIANTS as f32).floor() as u8, + y: (glyph_origin.y.0.fract() * SUBPIXEL_VARIANTS as f32).floor() as u8, + }; + let params = RenderGlyphParams { + font_id, + glyph_id, + font_size, + subpixel_variant, + scale_factor, + is_emoji: false, + }; + + let raster_bounds = self.text_system().raster_bounds(¶ms)?; + if !raster_bounds.is_zero() { + let tile = + self.window + .sprite_atlas + .get_or_insert_with(¶ms.clone().into(), &mut || { + let (size, bytes) = self.text_system().rasterize_glyph(¶ms)?; + Ok((size, Cow::Owned(bytes))) + })?; + let bounds = Bounds { + origin: glyph_origin.map(|px| px.floor()) + raster_bounds.origin.map(Into::into), + size: tile.bounds.size.map(Into::into), + }; + let content_mask = self.content_mask().scale(scale_factor); + let view_id = self.parent_view_id(); + let window = &mut *self.window; + window.next_frame.scene.insert( + &window.next_frame.z_index_stack, + MonochromeSprite { + view_id: view_id.into(), + layer_id: 0, + order: 0, + bounds, + content_mask, + color, + tile, + }, + ); + } + Ok(()) + } + + /// Paint an emoji glyph into the scene for the next frame at the current z-index. + /// The y component of the origin is the baseline of the glyph. + pub fn paint_emoji( + &mut self, + origin: Point, + font_id: FontId, + glyph_id: GlyphId, + font_size: Pixels, + ) -> Result<()> { + let scale_factor = self.scale_factor(); + let glyph_origin = origin.scale(scale_factor); + let params = RenderGlyphParams { + font_id, + glyph_id, + font_size, + // We don't render emojis with subpixel variants. + subpixel_variant: Default::default(), + scale_factor, + is_emoji: true, + }; + + let raster_bounds = self.text_system().raster_bounds(¶ms)?; + if !raster_bounds.is_zero() { + let tile = + self.window + .sprite_atlas + .get_or_insert_with(¶ms.clone().into(), &mut || { + let (size, bytes) = self.text_system().rasterize_glyph(¶ms)?; + Ok((size, Cow::Owned(bytes))) + })?; + let bounds = Bounds { + origin: glyph_origin.map(|px| px.floor()) + raster_bounds.origin.map(Into::into), + size: tile.bounds.size.map(Into::into), + }; + let content_mask = self.content_mask().scale(scale_factor); + let view_id = self.parent_view_id(); + let window = &mut *self.window; + + window.next_frame.scene.insert( + &window.next_frame.z_index_stack, + PolychromeSprite { + view_id: view_id.into(), + layer_id: 0, + order: 0, + bounds, + corner_radii: Default::default(), + content_mask, + tile, + grayscale: false, + }, + ); + } + Ok(()) + } + + /// Paint a monochrome SVG into the scene for the next frame at the current stacking context. + pub fn paint_svg( + &mut self, + bounds: Bounds, + path: SharedString, + color: Hsla, + ) -> Result<()> { + let scale_factor = self.scale_factor(); + let bounds = bounds.scale(scale_factor); + // Render the SVG at twice the size to get a higher quality result. + let params = RenderSvgParams { + path, + size: bounds + .size + .map(|pixels| DevicePixels::from((pixels.0 * 2.).ceil() as i32)), + }; + + let tile = + self.window + .sprite_atlas + .get_or_insert_with(¶ms.clone().into(), &mut || { + let bytes = self.svg_renderer.render(¶ms)?; + Ok((params.size, Cow::Owned(bytes))) + })?; + let content_mask = self.content_mask().scale(scale_factor); + let view_id = self.parent_view_id(); + + let window = &mut *self.window; + window.next_frame.scene.insert( + &window.next_frame.z_index_stack, + MonochromeSprite { + view_id: view_id.into(), + layer_id: 0, + order: 0, + bounds, + content_mask, + color, + tile, + }, + ); + + Ok(()) + } + + /// Paint an image into the scene for the next frame at the current z-index. + pub fn paint_image( + &mut self, + bounds: Bounds, + corner_radii: Corners, + data: Arc, + grayscale: bool, + ) -> Result<()> { + let scale_factor = self.scale_factor(); + let bounds = bounds.scale(scale_factor); + let params = RenderImageParams { image_id: data.id }; + + let tile = self + .window + .sprite_atlas + .get_or_insert_with(¶ms.clone().into(), &mut || { + Ok((data.size(), Cow::Borrowed(data.as_bytes()))) + })?; + let content_mask = self.content_mask().scale(scale_factor); + let corner_radii = corner_radii.scale(scale_factor); + let view_id = self.parent_view_id(); + + let window = &mut *self.window; + window.next_frame.scene.insert( + &window.next_frame.z_index_stack, + PolychromeSprite { + view_id: view_id.into(), + layer_id: 0, + order: 0, + bounds, + content_mask, + corner_radii, + tile, + grayscale, + }, + ); + Ok(()) + } + + /// Paint a surface into the scene for the next frame at the current z-index. + pub fn paint_surface(&mut self, bounds: Bounds, image_buffer: CVImageBuffer) { + let scale_factor = self.scale_factor(); + let bounds = bounds.scale(scale_factor); + let content_mask = self.content_mask().scale(scale_factor); + let view_id = self.parent_view_id(); + let window = &mut *self.window; + window.next_frame.scene.insert( + &window.next_frame.z_index_stack, + Surface { + view_id: view_id.into(), + layer_id: 0, + order: 0, + bounds, + content_mask, + image_buffer, + }, + ); + } + + #[must_use] + /// Add a node to the layout tree for the current frame. Takes the `Style` of the element for which + /// layout is being requested, along with the layout ids of any children. This method is called during + /// calls to the `Element::layout` trait method and enables any element to participate in layout. + pub fn request_layout( + &mut self, + style: &Style, + children: impl IntoIterator, + ) -> LayoutId { + self.app.layout_id_buffer.clear(); + self.app.layout_id_buffer.extend(children); + let rem_size = self.rem_size(); + + self.cx + .window + .layout_engine + .as_mut() + .unwrap() + .request_layout(style, rem_size, &self.cx.app.layout_id_buffer) + } + + /// Add a node to the layout tree for the current frame. Instead of taking a `Style` and children, + /// this variant takes a function that is invoked during layout so you can use arbitrary logic to + /// determine the element's size. One place this is used internally is when measuring text. + /// + /// The given closure is invoked at layout time with the known dimensions and available space and + /// returns a `Size`. + pub fn request_measured_layout< + F: FnMut(Size>, Size, &mut WindowContext) -> Size + + 'static, + >( + &mut self, + style: Style, + measure: F, + ) -> LayoutId { + let rem_size = self.rem_size(); + self.window + .layout_engine + .as_mut() + .unwrap() + .request_measured_layout(style, rem_size, measure) + } + + /// Compute the layout for the given id within the given available space. + /// This method is called for its side effect, typically by the framework prior to painting. + /// After calling it, you can request the bounds of the given layout node id or any descendant. + pub fn compute_layout(&mut self, layout_id: LayoutId, available_space: Size) { + let mut layout_engine = self.window.layout_engine.take().unwrap(); + layout_engine.compute_layout(layout_id, available_space, self); + self.window.layout_engine = Some(layout_engine); + } + + /// Obtain the bounds computed for the given LayoutId relative to the window. This method should not + /// be invoked until the paint phase begins, and will usually be invoked by GPUI itself automatically + /// in order to pass your element its `Bounds` automatically. + pub fn layout_bounds(&mut self, layout_id: LayoutId) -> Bounds { + let mut bounds = self + .window + .layout_engine + .as_mut() + .unwrap() + .layout_bounds(layout_id) + .map(Into::into); + bounds.origin += self.element_offset(); + bounds + } + + pub(crate) fn layout_style(&self, layout_id: LayoutId) -> Option<&Style> { + self.window + .layout_engine + .as_ref() + .unwrap() + .requested_style(layout_id) + } + + /// Called during painting to track which z-index is on top at each pixel position + pub fn add_opaque_layer(&mut self, bounds: Bounds) { + let stacking_order = self.window.next_frame.z_index_stack.clone(); + let view_id = self.parent_view_id(); + let depth_map = &mut self.window.next_frame.depth_map; + match depth_map.binary_search_by(|(level, _, _)| stacking_order.cmp(level)) { + Ok(i) | Err(i) => depth_map.insert(i, (stacking_order, view_id, bounds)), + } + } + + /// Invoke the given function with the given focus handle present on the key dispatch stack. + /// If you want an element to participate in key dispatch, use this method to push its key context and focus handle into the stack during paint. + pub fn with_key_dispatch( + &mut self, + context: Option, + focus_handle: Option, + f: impl FnOnce(Option, &mut Self) -> R, + ) -> R { + let window = &mut self.window; + let focus_id = focus_handle.as_ref().map(|handle| handle.id); + window + .next_frame + .dispatch_tree + .push_node(context.clone(), focus_id, None); + + let result = f(focus_handle, self); + + self.window.next_frame.dispatch_tree.pop_node(); + + result + } + + /// Invoke the given function with the given view id present on the view stack. + /// This is a fairly low-level method used to layout views. + pub fn with_view_id(&mut self, view_id: EntityId, f: impl FnOnce(&mut Self) -> R) -> R { + let text_system = self.text_system().clone(); + text_system.with_view(view_id, || { + if self.window.next_frame.view_stack.last() == Some(&view_id) { + return f(self); + } else { + self.window.next_frame.view_stack.push(view_id); + let result = f(self); + self.window.next_frame.view_stack.pop(); + result + } + }) + } + + /// Invoke the given function with the given view id present on the view stack. + /// This is a fairly low-level method used to paint views. + pub fn paint_view(&mut self, view_id: EntityId, f: impl FnOnce(&mut Self) -> R) -> R { + let text_system = self.text_system().clone(); + text_system.with_view(view_id, || { + if self.window.next_frame.view_stack.last() == Some(&view_id) { + return f(self); + } else { + self.window.next_frame.view_stack.push(view_id); + self.window + .next_frame + .dispatch_tree + .push_node(None, None, Some(view_id)); + let result = f(self); + self.window.next_frame.dispatch_tree.pop_node(); + self.window.next_frame.view_stack.pop(); + result + } + }) + } + + /// Sets an input handler, such as [`ElementInputHandler`][element_input_handler], which interfaces with the + /// platform to receive textual input with proper integration with concerns such + /// as IME interactions. This handler will be active for the upcoming frame until the following frame is + /// rendered. + /// + /// [element_input_handler]: crate::ElementInputHandler + pub fn handle_input(&mut self, focus_handle: &FocusHandle, input_handler: impl InputHandler) { + if focus_handle.is_focused(self) { + let view_id = self.parent_view_id(); + self.window.next_frame.requested_input_handler = Some(RequestedInputHandler { + view_id, + handler: Some(PlatformInputHandler::new( + self.to_async(), + Box::new(input_handler), + )), + }) + } + } + + /// Register a mouse event listener on the window for the next frame. The type of event + /// is determined by the first parameter of the given listener. When the next frame is rendered + /// the listener will be cleared. + pub fn on_mouse_event( + &mut self, + mut handler: impl FnMut(&Event, DispatchPhase, &mut ElementContext) + 'static, + ) { + let view_id = self.parent_view_id(); + let order = self.window.next_frame.z_index_stack.clone(); + self.window + .next_frame + .mouse_listeners + .entry(TypeId::of::()) + .or_default() + .push(( + order, + view_id, + Box::new( + move |event: &dyn Any, phase: DispatchPhase, cx: &mut ElementContext<'_>| { + handler(event.downcast_ref().unwrap(), phase, cx) + }, + ), + )) + } + + /// Register a key event listener on the window for the next frame. The type of event + /// is determined by the first parameter of the given listener. When the next frame is rendered + /// the listener will be cleared. + /// + /// This is a fairly low-level method, so prefer using event handlers on elements unless you have + /// a specific need to register a global listener. + pub fn on_key_event( + &mut self, + listener: impl Fn(&Event, DispatchPhase, &mut ElementContext) + 'static, + ) { + self.window.next_frame.dispatch_tree.on_key_event(Rc::new( + move |event: &dyn Any, phase, cx: &mut ElementContext<'_>| { + if let Some(event) = event.downcast_ref::() { + listener(event, phase, cx) + } + }, + )); + } +} diff --git a/crates/journal/src/journal.rs b/crates/journal/src/journal.rs index b15da05e1737d3e21a7c3e84fb2a8e5184330be9..7383a0ee55a61a8cfb8bab62e1c8b1c816813f10 100644 --- a/crates/journal/src/journal.rs +++ b/crates/journal/src/journal.rs @@ -102,7 +102,7 @@ pub fn new_journal_entry(app_state: Arc, cx: &mut WindowContext) { cx.spawn(|mut cx| async move { let (journal_dir, entry_path) = create_entry.await?; let (workspace, _) = cx - .update(|_, cx| workspace::open_paths(&[journal_dir], &app_state, None, cx))? + .update(|cx| workspace::open_paths(&[journal_dir], &app_state, None, cx))? .await?; let opened = workspace diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 08210875b841cb744b2b9734d9f8c3622da86bc2..67d66341e2a13725a17ca16bdc56e460d7d5cae1 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -10,7 +10,7 @@ use crate::{ markdown::parse_markdown, outline::OutlineItem, syntax_map::{ - SyntaxLayerInfo, SyntaxMap, SyntaxMapCapture, SyntaxMapCaptures, SyntaxMapMatches, + SyntaxLayer, SyntaxMap, SyntaxMapCapture, SyntaxMapCaptures, SyntaxMapMatches, SyntaxSnapshot, ToTreeSitterPoint, }, CodeLabel, LanguageScope, Outline, @@ -42,7 +42,13 @@ use std::{ }; use sum_tree::TreeMap; use text::operation_queue::OperationQueue; -pub use text::{Buffer as TextBuffer, BufferSnapshot as TextBufferSnapshot, *}; +use text::*; +pub use text::{ + Anchor, Bias, Buffer as TextBuffer, BufferSnapshot as TextBufferSnapshot, Edit, OffsetRangeExt, + OffsetUtf16, Patch, Point, PointUtf16, Rope, RopeFingerprint, Selection, SelectionGoal, + Subscription, TextDimension, TextSummary, ToOffset, ToOffsetUtf16, ToPoint, ToPointUtf16, + Transaction, TransactionId, Unclipped, +}; use theme::SyntaxTheme; #[cfg(any(test, feature = "test-support"))] use util::RandomCharIter; @@ -54,15 +60,22 @@ pub use {tree_sitter_rust, tree_sitter_typescript}; pub use lsp::DiagnosticSeverity; lazy_static! { + /// A label for the background task spawned by the buffer to compute + /// a diff against the contents of its file. pub static ref BUFFER_DIFF_TASK: TaskLabel = TaskLabel::new(); } +/// Indicate whether a [Buffer] has permissions to edit. #[derive(PartialEq, Clone, Copy, Debug)] pub enum Capability { + /// The buffer is a mutable replica. ReadWrite, + /// The buffer is a read-only replica. ReadOnly, } +/// An in-memory representation of a source code file, including its text, +/// syntax trees, git status, and diagnostics. pub struct Buffer { text: TextBuffer, diff_base: Option, @@ -99,9 +112,11 @@ pub struct Buffer { capability: Capability, } +/// An immutable, cheaply cloneable representation of a fixed +/// state of a buffer. pub struct BufferSnapshot { text: text::BufferSnapshot, - pub git_diff: git::diff::BufferDiff, + git_diff: git::diff::BufferDiff, pub(crate) syntax: SyntaxSnapshot, file: Option>, diagnostics: SmallVec<[(LanguageServerId, DiagnosticSet); 2]>, @@ -114,25 +129,37 @@ pub struct BufferSnapshot { parse_count: usize, } +/// The kind and amount of indentation in a particular line. For now, +/// assumes that indentation is all the same character. #[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] pub struct IndentSize { + /// The number of bytes that comprise the indentation. pub len: u32, + /// The kind of whitespace used for indentation. pub kind: IndentKind, } +/// A whitespace character that's used for indentation. #[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] pub enum IndentKind { + /// An ASCII space character. #[default] Space, + /// An ASCII tab character. Tab, } +/// The shape of a selection cursor. #[derive(Copy, Clone, PartialEq, Eq, Debug, Default)] pub enum CursorShape { + /// A vertical bar #[default] Bar, + /// A block that surrounds the following character Block, + /// An underline that runs along the following character Underscore, + /// A box drawn around the following character Hollow, } @@ -144,25 +171,40 @@ struct SelectionSet { lamport_timestamp: clock::Lamport, } -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct GroupId { - source: Arc, - id: usize, -} - +/// A diagnostic associated with a certain range of a buffer. #[derive(Clone, Debug, PartialEq, Eq)] pub struct Diagnostic { + /// The name of the service that produced this diagnostic. pub source: Option, + /// A machine-readable code that identifies this diagnostic. pub code: Option, + /// Whether this diagnostic is a hint, warning, or error. pub severity: DiagnosticSeverity, + /// The human-readable message associated with this diagnostic. pub message: String, + /// An id that identifies the group to which this diagnostic belongs. + /// + /// When a language server produces a diagnostic with + /// one or more associated diagnostics, those diagnostics are all + /// assigned a single group id. pub group_id: usize, - pub is_valid: bool, + /// Whether this diagnostic is the primary diagnostic for its group. + /// + /// In a given group, the primary diagnostic is the top-level diagnostic + /// returned by the language server. The non-primary diagnostics are the + /// associated diagnostics. pub is_primary: bool, + /// Whether this diagnostic is considered to originate from an analysis of + /// files on disk, as opposed to any unsaved buffer contents. This is a + /// property of a given diagnostic source, and is configured for a given + /// language server via the [LspAdapter::disk_based_diagnostic_sources] method + /// for the language server. pub is_disk_based: bool, + /// Whether this diagnostic marks unnecessary code. pub is_unnecessary: bool, } +/// TODO - move this into the `project` crate and make it private. pub async fn prepare_completion_documentation( documentation: &lsp::Documentation, language_registry: &Arc, @@ -194,77 +236,127 @@ pub async fn prepare_completion_documentation( } } +/// Documentation associated with a [Completion]. #[derive(Clone, Debug)] pub enum Documentation { + /// There is no documentation for this completion. Undocumented, + /// A single line of documentation. SingleLine(String), + /// Multiple lines of plain text documentation. MultiLinePlainText(String), + /// Markdown documentation. MultiLineMarkdown(ParsedMarkdown), } +/// A completion provided by a language server #[derive(Clone, Debug)] pub struct Completion { + /// The range of the buffer that will be replaced. pub old_range: Range, + /// The new text that will be inserted. pub new_text: String, + /// A label for this completion that is shown in the menu. pub label: CodeLabel, + /// The id of the language server that produced this completion. pub server_id: LanguageServerId, + /// The documentation for this completion. pub documentation: Option, + /// The raw completion provided by the language server. pub lsp_completion: lsp::CompletionItem, } +/// A code action provided by a language server. #[derive(Clone, Debug)] pub struct CodeAction { + /// The id of the language server that produced this code action. pub server_id: LanguageServerId, + /// The range of the buffer where this code action is applicable. pub range: Range, + /// The raw code action provided by the language server. pub lsp_action: lsp::CodeAction, } +/// An operation used to synchronize this buffer with its other replicas. #[derive(Clone, Debug, PartialEq)] pub enum Operation { + /// A text operation. Buffer(text::Operation), + /// An update to the buffer's diagnostics. UpdateDiagnostics { + /// The id of the language server that produced the new diagnostics. server_id: LanguageServerId, + /// The diagnostics. diagnostics: Arc<[DiagnosticEntry]>, + /// The buffer's lamport timestamp. lamport_timestamp: clock::Lamport, }, + /// An update to the most recent selections in this buffer. UpdateSelections { + /// The selections. selections: Arc<[Selection]>, + /// The buffer's lamport timestamp. lamport_timestamp: clock::Lamport, + /// Whether the selections are in 'line mode'. line_mode: bool, + /// The [CursorShape] associated with these selections. cursor_shape: CursorShape, }, + /// An update to the characters that should trigger autocompletion + /// for this buffer. UpdateCompletionTriggers { + /// The characters that trigger autocompletion. triggers: Vec, + /// The buffer's lamport timestamp. lamport_timestamp: clock::Lamport, }, } +/// An event that occurs in a buffer. #[derive(Clone, Debug, PartialEq)] pub enum Event { + /// The buffer was changed in a way that must be + /// propagated to its other replicas. Operation(Operation), + /// The buffer was edited. Edited, + /// The buffer's `dirty` bit changed. DirtyChanged, + /// The buffer was saved. Saved, + /// The buffer's file was changed on disk. FileHandleChanged, + /// The buffer was reloaded. Reloaded, + /// The buffer's diff_base changed. DiffBaseChanged, + /// The buffer's language was changed. LanguageChanged, + /// The buffer's syntax trees were updated. Reparsed, + /// The buffer's diagnostics were updated. DiagnosticsUpdated, + /// The buffer gained or lost editing capabilities. CapabilityChanged, + /// The buffer was explicitly requested to close. Closed, } +/// The file associated with a buffer. pub trait File: Send + Sync { + /// Returns the [LocalFile] associated with this file, if the + /// file is local. fn as_local(&self) -> Option<&dyn LocalFile>; + /// Returns whether this file is local. fn is_local(&self) -> bool { self.as_local().is_some() } + /// Returns the file's mtime. fn mtime(&self) -> SystemTime; /// Returns the path of this file relative to the worktree's root directory. @@ -283,19 +375,25 @@ pub trait File: Send + Sync { /// This is needed for looking up project-specific settings. fn worktree_id(&self) -> usize; + /// Returns whether the file has been deleted. fn is_deleted(&self) -> bool; + /// Converts this file into an [Any] trait object. fn as_any(&self) -> &dyn Any; + /// Converts this file into a protobuf message. fn to_proto(&self) -> rpc::proto::File; } +/// The file associated with a buffer, in the case where the file is on the local disk. pub trait LocalFile: File { /// Returns the absolute path of this file. fn abs_path(&self, cx: &AppContext) -> PathBuf; + /// Loads the file's contents from disk. fn load(&self, cx: &AppContext) -> Task>; + /// Called when the buffer is reloaded from disk. fn buffer_reloaded( &self, buffer_id: u64, @@ -307,6 +405,10 @@ pub trait LocalFile: File { ); } +/// The auto-indent behavior associated with an editing operation. +/// For some editing operations, each affected line of text has its +/// indentation recomputed. For other operations, the entire block +/// of edited text is adjusted uniformly. #[derive(Clone, Debug)] pub enum AutoindentMode { /// Indent each line of inserted text. @@ -354,6 +456,8 @@ struct BufferChunkHighlights<'a> { highlight_maps: Vec, } +/// An iterator that yields chunks of a buffer's text, along with their +/// syntax highlights and diagnostic status. pub struct BufferChunks<'a> { range: Range, chunks: text::Chunks<'a>, @@ -366,16 +470,26 @@ pub struct BufferChunks<'a> { highlights: Option>, } +/// A chunk of a buffer's text, along with its syntax highlight and +/// diagnostic status. #[derive(Clone, Copy, Debug, Default)] pub struct Chunk<'a> { + /// The text of the chunk. pub text: &'a str, + /// The syntax highlighting style of the chunk. pub syntax_highlight_id: Option, + /// The highlight style that has been applied to this chunk in + /// the editor. pub highlight_style: Option, + /// The severity of diagnostic associated with this chunk, if any. pub diagnostic_severity: Option, + /// Whether this chunk of text is marked as unnecessary. pub is_unnecessary: bool, + /// Whether this chunk of text was originally a tab character. pub is_tab: bool, } +/// A set of edits to a given version of a buffer, computed asynchronously. pub struct Diff { pub(crate) base_version: clock::Global, line_ending: LineEnding, @@ -390,24 +504,19 @@ pub(crate) struct DiagnosticEndpoint { is_unnecessary: bool, } +/// A class of characters, used for characterizing a run of text. #[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Debug)] pub enum CharKind { + /// Whitespace. Whitespace, + /// Punctuation. Punctuation, + /// Word. Word, } -impl CharKind { - pub fn coerce_punctuation(self, treat_punctuation_as_word: bool) -> Self { - if treat_punctuation_as_word && self == CharKind::Punctuation { - CharKind::Word - } else { - self - } - } -} - impl Buffer { + /// Create a new buffer with the given base text. pub fn new>(replica_id: ReplicaId, id: u64, base_text: T) -> Self { Self::build( TextBuffer::new(replica_id, id, base_text.into()), @@ -417,6 +526,7 @@ impl Buffer { ) } + /// Create a new buffer that is a replica of a remote buffer. pub fn remote( remote_id: u64, replica_id: ReplicaId, @@ -431,6 +541,8 @@ impl Buffer { ) } + /// Create a new buffer that is a replica of a remote buffer, populating its + /// state from the given protobuf message. pub fn from_proto( replica_id: ReplicaId, capability: Capability, @@ -457,6 +569,7 @@ impl Buffer { Ok(this) } + /// Serialize the buffer's state to a protobuf message. pub fn to_proto(&self) -> proto::BufferState { proto::BufferState { id: self.remote_id(), @@ -470,6 +583,7 @@ impl Buffer { } } + /// Serialize as protobufs all of the changes to the buffer since the given version. pub fn serialize_ops( &self, since: Option, @@ -516,19 +630,23 @@ impl Buffer { }) } + /// Assign a language to the buffer, returning the buffer. pub fn with_language(mut self, language: Arc, cx: &mut ModelContext) -> Self { self.set_language(Some(language), cx); self } + /// Returns the [Capability] of this buffer. pub fn capability(&self) -> Capability { self.capability } + /// Whether this buffer can only be read. pub fn read_only(&self) -> bool { self.capability == Capability::ReadOnly } + /// Builds a [Buffer] with the given underlying [TextBuffer], diff base, [File] and [Capability]. pub fn build( buffer: TextBuffer, diff_base: Option, @@ -573,6 +691,8 @@ impl Buffer { } } + /// Retrieve a snapshot of the buffer's current state. This is computationally + /// cheap, and allows reading from the buffer on a background thread. pub fn snapshot(&self) -> BufferSnapshot { let text = self.text.snapshot(); let mut syntax_map = self.syntax_map.lock(); @@ -595,30 +715,38 @@ impl Buffer { } } - pub fn as_text_snapshot(&self) -> &text::BufferSnapshot { + #[cfg(test)] + pub(crate) fn as_text_snapshot(&self) -> &text::BufferSnapshot { &self.text } + /// Retrieve a snapshot of the buffer's raw text, without any + /// language-related state like the syntax tree or diagnostics. pub fn text_snapshot(&self) -> text::BufferSnapshot { self.text.snapshot() } + /// The file associated with the buffer, if any. pub fn file(&self) -> Option<&Arc> { self.file.as_ref() } + /// The version of the buffer that was last saved or reloaded from disk. pub fn saved_version(&self) -> &clock::Global { &self.saved_version } + /// The fingerprint of the buffer's text when the buffer was last saved or reloaded from disk. pub fn saved_version_fingerprint(&self) -> RopeFingerprint { self.file_fingerprint } + /// The mtime of the buffer's file when the buffer was last saved or reloaded from disk. pub fn saved_mtime(&self) -> SystemTime { self.saved_mtime } + /// Assign a language to the buffer. pub fn set_language(&mut self, language: Option>, cx: &mut ModelContext) { self.syntax_map.lock().clear(); self.language = language; @@ -626,17 +754,21 @@ impl Buffer { cx.emit(Event::LanguageChanged); } + /// Assign a language registry to the buffer. This allows the buffer to retrieve + /// other languages if parts of the buffer are written in different languages. pub fn set_language_registry(&mut self, language_registry: Arc) { self.syntax_map .lock() .set_language_registry(language_registry); } + /// Assign the buffer a new [Capability]. pub fn set_capability(&mut self, capability: Capability, cx: &mut ModelContext) { self.capability = capability; cx.emit(Event::CapabilityChanged) } + /// This method is called to signal that the buffer has been saved. pub fn did_save( &mut self, version: clock::Global, @@ -651,6 +783,7 @@ impl Buffer { cx.notify(); } + /// Reloads the contents of the buffer from disk. pub fn reload( &mut self, cx: &mut ModelContext, @@ -699,6 +832,7 @@ impl Buffer { rx } + /// This method is called to signal that the buffer has been reloaded. pub fn did_reload( &mut self, version: clock::Global, @@ -725,6 +859,8 @@ impl Buffer { cx.notify(); } + /// Updates the [File] backing this buffer. This should be called when + /// the file has changed or has been deleted. pub fn file_updated(&mut self, new_file: Arc, cx: &mut ModelContext) { let mut file_changed = false; @@ -762,16 +898,20 @@ impl Buffer { } } + /// Returns the current diff base, see [Buffer::set_diff_base]. pub fn diff_base(&self) -> Option<&str> { self.diff_base.as_deref() } + /// Sets the text that will be used to compute a Git diff + /// against the buffer text. pub fn set_diff_base(&mut self, diff_base: Option, cx: &mut ModelContext) { self.diff_base = diff_base; self.git_diff_recalc(cx); cx.emit(Event::DiffBaseChanged); } + /// Recomputes the Git diff status. pub fn git_diff_recalc(&mut self, cx: &mut ModelContext) -> Option> { let diff_base = self.diff_base.clone()?; // TODO: Make this an Arc let snapshot = self.snapshot(); @@ -792,14 +932,12 @@ impl Buffer { })) } - pub fn close(&mut self, cx: &mut ModelContext) { - cx.emit(Event::Closed); - } - + /// Returns the primary [Language] assigned to this [Buffer]. pub fn language(&self) -> Option<&Arc> { self.language.as_ref() } + /// Returns the [Language] at the given location. pub fn language_at(&self, position: D) -> Option> { let offset = position.to_offset(self); self.syntax_map @@ -810,31 +948,39 @@ impl Buffer { .or_else(|| self.language.clone()) } + /// The number of times the buffer was parsed. pub fn parse_count(&self) -> usize { self.parse_count } + /// The number of times selections were updated. pub fn selections_update_count(&self) -> usize { self.selections_update_count } + /// The number of times diagnostics were updated. pub fn diagnostics_update_count(&self) -> usize { self.diagnostics_update_count } + /// The number of times the underlying file was updated. pub fn file_update_count(&self) -> usize { self.file_update_count } + /// The number of times the git diff status was updated. pub fn git_diff_update_count(&self) -> usize { self.git_diff_update_count } + /// Whether the buffer is being parsed in the background. #[cfg(any(test, feature = "test-support"))] pub fn is_parsing(&self) -> bool { self.parsing_in_background } + /// Indicates whether the buffer contains any regions that may be + /// written in a language that hasn't been loaded yet. pub fn contains_unknown_injections(&self) -> bool { self.syntax_map.lock().contains_unknown_injections() } @@ -941,6 +1087,7 @@ impl Buffer { cx.notify(); } + /// Assign to the buffer a set of diagnostics created by a given language server. pub fn update_diagnostics( &mut self, server_id: LanguageServerId, @@ -1170,9 +1317,9 @@ impl Buffer { self.edit(edits, None, cx); } - // Create a minimal edit that will cause the the given row to be indented - // with the given size. After applying this edit, the length of the line - // will always be at least `new_size.len`. + /// Create a minimal edit that will cause the the given row to be indented + /// with the given size. After applying this edit, the length of the line + /// will always be at least `new_size.len`. pub fn edit_for_indent_size_adjustment( row: u32, current_size: IndentSize, @@ -1207,6 +1354,8 @@ impl Buffer { } } + /// Spawns a background task that asynchronously computes a `Diff` between the buffer's text + /// and the given new text. pub fn diff(&self, mut new_text: String, cx: &AppContext) -> Task { let old_text = self.as_rope().clone(); let base_version = self.version(); @@ -1278,7 +1427,7 @@ impl Buffer { }) } - /// Spawn a background task that searches the buffer for any whitespace + /// Spawns a background task that searches the buffer for any whitespace /// at the ends of a lines, and returns a `Diff` that removes that whitespace. pub fn remove_trailing_whitespace(&self, cx: &AppContext) -> Task { let old_text = self.as_rope().clone(); @@ -1298,7 +1447,7 @@ impl Buffer { }) } - /// Ensure that the buffer ends with a single newline character, and + /// Ensures that the buffer ends with a single newline character, and /// no other whitespace. pub fn ensure_final_newline(&mut self, cx: &mut ModelContext) { let len = self.len(); @@ -1319,7 +1468,7 @@ impl Buffer { self.edit([(offset..len, "\n")], None, cx); } - /// Apply a diff to the buffer. If the buffer has changed since the given diff was + /// Applies a diff to the buffer. If the buffer has changed since the given diff was /// calculated, then adjust the diff to account for those changes, and discard any /// parts of the diff that conflict with those changes. pub fn apply_diff(&mut self, diff: Diff, cx: &mut ModelContext) -> Option { @@ -1358,11 +1507,14 @@ impl Buffer { self.end_transaction(cx) } + /// Checks if the buffer has unsaved changes. pub fn is_dirty(&self) -> bool { self.file_fingerprint != self.as_rope().fingerprint() || self.file.as_ref().map_or(false, |file| file.is_deleted()) } + /// Checks if the buffer and its file have both changed since the buffer + /// was last saved or reloaded. pub fn has_conflict(&self) -> bool { self.file_fingerprint != self.as_rope().fingerprint() && self @@ -1371,14 +1523,23 @@ impl Buffer { .map_or(false, |file| file.mtime() > self.saved_mtime) } + /// Gets a [`Subscription`] that tracks all of the changes to the buffer's text. pub fn subscribe(&mut self) -> Subscription { self.text.subscribe() } + /// Starts a transaction, if one is not already in-progress. When undoing or + /// redoing edits, all of the edits performed within a transaction are undone + /// or redone together. pub fn start_transaction(&mut self) -> Option { self.start_transaction_at(Instant::now()) } + /// Starts a transaction, providing the current time. Subsequent transactions + /// that occur within a short period of time will be grouped together. This + /// is controlled by the buffer's undo grouping duration. + /// + /// See [`Buffer::set_group_interval`]. pub fn start_transaction_at(&mut self, now: Instant) -> Option { self.transaction_depth += 1; if self.was_dirty_before_starting_transaction.is_none() { @@ -1387,10 +1548,16 @@ impl Buffer { self.text.start_transaction_at(now) } + /// Terminates the current transaction, if this is the outermost transaction. pub fn end_transaction(&mut self, cx: &mut ModelContext) -> Option { self.end_transaction_at(Instant::now(), cx) } + /// Terminates the current transaction, providing the current time. Subsequent transactions + /// that occur within a short period of time will be grouped together. This + /// is controlled by the buffer's undo grouping duration. + /// + /// See [`Buffer::set_group_interval`]. pub fn end_transaction_at( &mut self, now: Instant, @@ -1411,26 +1578,33 @@ impl Buffer { } } + /// Manually add a transaction to the buffer's undo history. pub fn push_transaction(&mut self, transaction: Transaction, now: Instant) { self.text.push_transaction(transaction, now); } + /// Prevent the last transaction from being grouped with any subsequent transactions, + /// even if they occur with the buffer's undo grouping duration. pub fn finalize_last_transaction(&mut self) -> Option<&Transaction> { self.text.finalize_last_transaction() } + /// Manually group all changes since a given transaction. pub fn group_until_transaction(&mut self, transaction_id: TransactionId) { self.text.group_until_transaction(transaction_id); } + /// Manually remove a transaction from the buffer's undo history pub fn forget_transaction(&mut self, transaction_id: TransactionId) { self.text.forget_transaction(transaction_id); } + /// Manually merge two adjacent transactions in the buffer's undo history. pub fn merge_transactions(&mut self, transaction: TransactionId, destination: TransactionId) { self.text.merge_transactions(transaction, destination); } + /// Waits for the buffer to receive operations with the given timestamps. pub fn wait_for_edits( &mut self, edit_ids: impl IntoIterator, @@ -1438,6 +1612,7 @@ impl Buffer { self.text.wait_for_edits(edit_ids) } + /// Waits for the buffer to receive the operations necessary for resolving the given anchors. pub fn wait_for_anchors( &mut self, anchors: impl IntoIterator, @@ -1445,14 +1620,18 @@ impl Buffer { self.text.wait_for_anchors(anchors) } + /// Waits for the buffer to receive operations up to the given version. pub fn wait_for_version(&mut self, version: clock::Global) -> impl Future> { self.text.wait_for_version(version) } + /// Forces all futures returned by [`Buffer::wait_for_version`], [`Buffer::wait_for_edits`], or + /// [`Buffer::wait_for_version`] to resolve with an error. pub fn give_up_waiting(&mut self) { self.text.give_up_waiting(); } + /// Stores a set of selections that should be broadcasted to all of the buffer's replicas. pub fn set_active_selections( &mut self, selections: Arc<[Selection]>, @@ -1481,6 +1660,8 @@ impl Buffer { ); } + /// Clears the selections, so that other replicas of the buffer do not see any selections for + /// this replica. pub fn remove_active_selections(&mut self, cx: &mut ModelContext) { if self .remote_selections @@ -1491,6 +1672,7 @@ impl Buffer { } } + /// Replaces the buffer's entire text. pub fn set_text(&mut self, text: T, cx: &mut ModelContext) -> Option where T: Into>, @@ -1499,6 +1681,15 @@ impl Buffer { self.edit([(0..self.len(), text)], None, cx) } + /// Applies the given edits to the buffer. Each edit is specified as a range of text to + /// delete, and a string of text to insert at that location. + /// + /// If an [`AutoindentMode`] is provided, then the buffer will enqueue an auto-indent + /// request for the edited ranges, which will be processed when the buffer finishes + /// parsing. + /// + /// Parsing takes place at the end of a transaction, and may compute synchronously + /// or asynchronously, depending on the changes. pub fn edit( &mut self, edits_iter: I, @@ -1632,6 +1823,7 @@ impl Buffer { cx.notify(); } + /// Applies the given remote operations to the buffer. pub fn apply_ops>( &mut self, ops: I, @@ -1779,11 +1971,13 @@ impl Buffer { cx.emit(Event::Operation(operation)); } + /// Removes the selections for a given peer. pub fn remove_peer(&mut self, replica_id: ReplicaId, cx: &mut ModelContext) { self.remote_selections.remove(&replica_id); cx.notify(); } + /// Undoes the most recent transaction. pub fn undo(&mut self, cx: &mut ModelContext) -> Option { let was_dirty = self.is_dirty(); let old_version = self.version.clone(); @@ -1797,6 +1991,7 @@ impl Buffer { } } + /// Manually undoes a specific transaction in the buffer's undo history. pub fn undo_transaction( &mut self, transaction_id: TransactionId, @@ -1813,6 +2008,7 @@ impl Buffer { } } + /// Manually undoes all changes after a given transaction in the buffer's undo history. pub fn undo_to_transaction( &mut self, transaction_id: TransactionId, @@ -1832,6 +2028,7 @@ impl Buffer { undone } + /// Manually redoes a specific transaction in the buffer's redo history. pub fn redo(&mut self, cx: &mut ModelContext) -> Option { let was_dirty = self.is_dirty(); let old_version = self.version.clone(); @@ -1845,6 +2042,7 @@ impl Buffer { } } + /// Manually undoes all changes until a given transaction in the buffer's redo history. pub fn redo_to_transaction( &mut self, transaction_id: TransactionId, @@ -1864,6 +2062,7 @@ impl Buffer { redone } + /// Override current completion triggers with the user-provided completion triggers. pub fn set_completion_triggers(&mut self, triggers: Vec, cx: &mut ModelContext) { self.completion_triggers = triggers.clone(); self.completion_triggers_timestamp = self.text.lamport_clock.tick(); @@ -1877,11 +2076,14 @@ impl Buffer { cx.notify(); } + /// Returns a list of strings which trigger a completion menu for this language. + /// Usually this is driven by LSP server which returns a list of trigger characters for completions. pub fn completion_triggers(&self) -> &[String] { &self.completion_triggers } } +#[doc(hidden)] #[cfg(any(test, feature = "test-support"))] impl Buffer { pub fn edit_via_marked_text( @@ -1954,10 +2156,12 @@ impl Deref for Buffer { } impl BufferSnapshot { + /// Returns [`IndentSize`] for a given line that respects user settings and /// language preferences. pub fn indent_size_for_line(&self, row: u32) -> IndentSize { indent_size_for_line(self, row) } - + /// Returns [`IndentSize`] for a given position that respects user settings + /// and language preferences. pub fn language_indent_size_at(&self, position: T, cx: &AppContext) -> IndentSize { let settings = language_settings(self.language_at(position), self.file(), cx); if settings.hard_tabs { @@ -1967,6 +2171,8 @@ impl BufferSnapshot { } } + /// Retrieve the suggested indent size for all of the given rows. The unit of indentation + /// is passed in as `single_indent_size`. pub fn suggested_indents( &self, rows: impl Iterator, @@ -2213,6 +2419,10 @@ impl BufferSnapshot { None } + /// Iterates over chunks of text in the given range of the buffer. Text is chunked + /// in an arbitrary way due to being stored in a [`rope::Rope`]. The text is also + /// returned in chunks where each chunk has a single syntax highlighting style and + /// diagnostic status. pub fn chunks(&self, range: Range, language_aware: bool) -> BufferChunks { let range = range.start.to_offset(self)..range.end.to_offset(self); @@ -2249,7 +2459,9 @@ impl BufferSnapshot { BufferChunks::new(self.text.as_rope(), range, syntax, diagnostic_endpoints) } - pub fn for_each_line(&self, range: Range, mut callback: impl FnMut(u32, &str)) { + /// Invokes the given callback for each line of text in the given range of the buffer. + /// Uses callback to avoid allocating a string for each line. + fn for_each_line(&self, range: Range, mut callback: impl FnMut(u32, &str)) { let mut line = String::new(); let mut row = range.start.row; for chunk in self @@ -2268,11 +2480,12 @@ impl BufferSnapshot { } } - pub fn syntax_layers(&self) -> impl Iterator + '_ { + /// Iterates over every [`SyntaxLayer`] in the buffer. + pub fn syntax_layers(&self) -> impl Iterator + '_ { self.syntax.layers_for_range(0..self.len(), &self.text) } - pub fn syntax_layer_at(&self, position: D) -> Option { + fn syntax_layer_at(&self, position: D) -> Option { let offset = position.to_offset(self); self.syntax .layers_for_range(offset..offset, &self.text) @@ -2280,12 +2493,14 @@ impl BufferSnapshot { .last() } + /// Returns the [Language] at the given location. pub fn language_at(&self, position: D) -> Option<&Arc> { self.syntax_layer_at(position) .map(|info| info.language) .or(self.language.as_ref()) } + /// Returns the settings for the language at the given location. pub fn settings_at<'a, D: ToOffset>( &self, position: D, @@ -2294,6 +2509,7 @@ impl BufferSnapshot { language_settings(self.language_at(position), self.file.as_ref(), cx) } + /// Returns the [LanguageScope] at the given location. pub fn language_scope_at(&self, position: D) -> Option { let offset = position.to_offset(self); let mut scope = None; @@ -2338,6 +2554,8 @@ impl BufferSnapshot { }) } + /// Returns a tuple of the range and character kind of the word + /// surrounding the given position. pub fn surrounding_word(&self, start: T) -> (Range, Option) { let mut start = start.to_offset(self); let mut end = start; @@ -2370,6 +2588,7 @@ impl BufferSnapshot { (start..end, word_kind) } + /// Returns the range for the closes syntax node enclosing the given range. pub fn range_for_syntax_ancestor(&self, range: Range) -> Option> { let range = range.start.to_offset(self)..range.end.to_offset(self); let mut result: Option> = None; @@ -2438,11 +2657,19 @@ impl BufferSnapshot { result } + /// Returns the outline for the buffer. + /// + /// This method allows passing an optional [SyntaxTheme] to + /// syntax-highlight the returned symbols. pub fn outline(&self, theme: Option<&SyntaxTheme>) -> Option> { self.outline_items_containing(0..self.len(), true, theme) .map(Outline::new) } + /// Returns all the symbols that contain the given position. + /// + /// This method allows passing an optional [SyntaxTheme] to + /// syntax-highlight the returned symbols. pub fn symbols_containing( &self, position: T, @@ -2594,6 +2821,8 @@ impl BufferSnapshot { Some(items) } + /// For each grammar in the language, runs the provided + /// [tree_sitter::Query] against the given range. pub fn matches( &self, range: Range, @@ -2650,6 +2879,7 @@ impl BufferSnapshot { }) } + /// Returns selections for remote peers intersecting the given range. #[allow(clippy::type_complexity)] pub fn remote_selections_in_range( &self, @@ -2688,6 +2918,13 @@ impl BufferSnapshot { }) } + /// Whether the buffer contains any git changes. + pub fn has_git_diff(&self) -> bool { + !self.git_diff.is_empty() + } + + /// Returns all the Git diff hunks intersecting the given + /// row range. pub fn git_diff_hunks_in_row_range<'a>( &'a self, range: Range, @@ -2695,6 +2932,8 @@ impl BufferSnapshot { self.git_diff.hunks_in_row_range(range, self) } + /// Returns all the Git diff hunks intersecting the given + /// range. pub fn git_diff_hunks_intersecting_range<'a>( &'a self, range: Range, @@ -2702,6 +2941,8 @@ impl BufferSnapshot { self.git_diff.hunks_intersecting_range(range, self) } + /// Returns all the Git diff hunks intersecting the given + /// range, in reverse order. pub fn git_diff_hunks_intersecting_range_rev<'a>( &'a self, range: Range, @@ -2709,6 +2950,7 @@ impl BufferSnapshot { self.git_diff.hunks_intersecting_range_rev(range, self) } + /// Returns all the diagnostics intersecting the given range. pub fn diagnostics_in_range<'a, T, O>( &'a self, search_range: Range, @@ -2738,6 +2980,9 @@ impl BufferSnapshot { }) } + /// Returns all the diagnostic groups associated with the given + /// language server id. If no language server id is provided, + /// all diagnostics groups are returned. pub fn diagnostic_groups( &self, language_server_id: Option, @@ -2762,12 +3007,13 @@ impl BufferSnapshot { groups.sort_by(|(id_a, group_a), (id_b, group_b)| { let a_start = &group_a.entries[group_a.primary_ix].range.start; let b_start = &group_b.entries[group_b.primary_ix].range.start; - a_start.cmp(b_start, self).then_with(|| id_a.cmp(&id_b)) + a_start.cmp(b_start, self).then_with(|| id_a.cmp(id_b)) }); groups } + /// Returns an iterator over the diagnostics for the given group. pub fn diagnostic_group<'a, O>( &'a self, group_id: usize, @@ -2780,22 +3026,27 @@ impl BufferSnapshot { .flat_map(move |(_, set)| set.group(group_id, self)) } + /// The number of times diagnostics were updated. pub fn diagnostics_update_count(&self) -> usize { self.diagnostics_update_count } + /// The number of times the buffer was parsed. pub fn parse_count(&self) -> usize { self.parse_count } + /// The number of times selections were updated. pub fn selections_update_count(&self) -> usize { self.selections_update_count } + /// Returns a snapshot of underlying file. pub fn file(&self) -> Option<&Arc> { self.file.as_ref() } + /// Resolves the file path (relative to the worktree root) associated with the underlying file. pub fn resolve_file_path(&self, cx: &AppContext, include_root: bool) -> Option { if let Some(file) = self.file() { if file.path().file_name().is_none() || include_root { @@ -2808,10 +3059,12 @@ impl BufferSnapshot { } } + /// The number of times the underlying file was updated. pub fn file_update_count(&self) -> usize { self.file_update_count } + /// The number of times the git diff status was updated. pub fn git_diff_update_count(&self) -> usize { self.git_diff_update_count } @@ -2821,7 +3074,7 @@ fn indent_size_for_line(text: &text::BufferSnapshot, row: u32) -> IndentSize { indent_size_for_text(text.chars_at(Point::new(row, 0))) } -pub fn indent_size_for_text(text: impl Iterator) -> IndentSize { +fn indent_size_for_text(text: impl Iterator) -> IndentSize { let mut result = IndentSize::spaces(0); for c in text { let kind = match c { @@ -2899,6 +3152,7 @@ impl<'a> BufferChunks<'a> { } } + /// Seeks to the given byte offset in the buffer. pub fn seek(&mut self, offset: usize) { self.range.start = offset; self.chunks.seek(self.range.start); @@ -2922,6 +3176,7 @@ impl<'a> BufferChunks<'a> { } } + /// The current byte offset in the buffer. pub fn offset(&self) -> usize { self.range.start } @@ -3074,7 +3329,6 @@ impl Default for Diagnostic { message: Default::default(), group_id: 0, is_primary: false, - is_valid: true, is_disk_based: false, is_unnecessary: false, } @@ -3082,6 +3336,7 @@ impl Default for Diagnostic { } impl IndentSize { + /// Returns an [IndentSize] representing the given spaces. pub fn spaces(len: u32) -> Self { Self { len, @@ -3089,6 +3344,7 @@ impl IndentSize { } } + /// Returns an [IndentSize] representing a tab. pub fn tab() -> Self { Self { len: 1, @@ -3096,10 +3352,12 @@ impl IndentSize { } } + /// An iterator over the characters represented by this [IndentSize]. pub fn chars(&self) -> impl Iterator { iter::repeat(self.char()).take(self.len as usize) } + /// The character representation of this [IndentSize]. pub fn char(&self) -> char { match self.kind { IndentKind::Space => ' ', @@ -3107,6 +3365,8 @@ impl IndentSize { } } + /// Consumes the current [IndentSize] and returns a new one that has + /// been shrunk or enlarged by the given size along the given direction. pub fn with_delta(mut self, direction: Ordering, size: IndentSize) -> Self { match direction { Ordering::Less => { @@ -3128,6 +3388,8 @@ impl IndentSize { } impl Completion { + /// A key that can be used to sort completions when displaying + /// them to the user. pub fn sort_key(&self) -> (usize, &str) { let kind_key = match self.lsp_completion.kind { Some(lsp::CompletionItemKind::VARIABLE) => 0, @@ -3136,12 +3398,13 @@ impl Completion { (kind_key, &self.label.text[self.label.filter_range.clone()]) } + /// Whether this completion is a snippet. pub fn is_snippet(&self) -> bool { self.lsp_completion.insert_text_format == Some(lsp::InsertTextFormat::SNIPPET) } } -pub fn contiguous_ranges( +pub(crate) fn contiguous_ranges( values: impl Iterator, max_len: usize, ) -> impl Iterator> { @@ -3167,6 +3430,9 @@ pub fn contiguous_ranges( }) } +/// Returns the [CharKind] for the given character. When a scope is provided, +/// the function checks if the character is considered a word character +/// based on the language scope's word character settings. pub fn char_kind(scope: &Option, c: char) -> CharKind { if c.is_whitespace() { return CharKind::Whitespace; diff --git a/crates/language/src/diagnostic_set.rs b/crates/language/src/diagnostic_set.rs index f269fce88d694c8efe2f255e302bbef7aed865ea..e04406f28e8c5d0b9553dc27a0ca99154d5474c6 100644 --- a/crates/language/src/diagnostic_set.rs +++ b/crates/language/src/diagnostic_set.rs @@ -9,20 +9,36 @@ use std::{ use sum_tree::{self, Bias, SumTree}; use text::{Anchor, FromAnchor, PointUtf16, ToOffset}; +/// A set of diagnostics associated with a given buffer, provided +/// by a single language server. +/// +/// The diagnostics are stored in a [SumTree], which allows this struct +/// to be cheaply copied, and allows for efficient retrieval of the +/// diagnostics that intersect a given range of the buffer. #[derive(Clone, Debug, Default)] pub struct DiagnosticSet { diagnostics: SumTree>, } +/// A single diagnostic in a set. Generic over its range type, because +/// the diagnostics are stored internally as [Anchor]s, but can be +/// resolved to different coordinates types like [usize] byte offsets or +/// [Point]s. #[derive(Clone, Debug, PartialEq, Eq)] pub struct DiagnosticEntry { + /// The range of the buffer where the diagnostic applies. pub range: Range, + /// The information about the diagnostic. pub diagnostic: Diagnostic, } +/// A group of related diagnostics, ordered by their start position +/// in the buffer. #[derive(Debug)] pub struct DiagnosticGroup { + /// The diagnostics. pub entries: Vec>, + /// The index into `entries` where the primary diagnostic is stored. pub primary_ix: usize, } @@ -36,7 +52,8 @@ pub struct Summary { } impl DiagnosticEntry { - // Used to provide diagnostic context to lsp codeAction request + /// Returns a raw LSP diagnostic ssed to provide diagnostic context to lsp + /// codeAction request pub fn to_lsp_diagnostic_stub(&self) -> lsp::Diagnostic { let code = self .diagnostic @@ -53,6 +70,8 @@ impl DiagnosticEntry { } impl DiagnosticSet { + /// Constructs a [DiagnosticSet] from a sequence of entries, ordered by + /// their position in the buffer. pub fn from_sorted_entries(iter: I, buffer: &text::BufferSnapshot) -> Self where I: IntoIterator>, @@ -62,6 +81,7 @@ impl DiagnosticSet { } } + /// Constructs a [DiagnosticSet] from a sequence of entries in an arbitrary order. pub fn new(iter: I, buffer: &text::BufferSnapshot) -> Self where I: IntoIterator>, @@ -80,14 +100,18 @@ impl DiagnosticSet { } } + /// Returns the number of diagnostics in the set. pub fn len(&self) -> usize { self.diagnostics.summary().count } + /// Returns an iterator over the diagnostic entries in the set. pub fn iter(&self) -> impl Iterator> { self.diagnostics.iter() } + /// Returns an iterator over the diagnostic entries that intersect the + /// given range of the buffer. pub fn range<'a, T, O>( &'a self, range: Range, @@ -134,6 +158,7 @@ impl DiagnosticSet { }) } + /// Adds all of this set's diagnostic groups to the given output vector. pub fn groups( &self, language_server_id: LanguageServerId, @@ -169,10 +194,12 @@ impl DiagnosticSet { .range .start .cmp(&group_b.entries[group_b.primary_ix].range.start, buffer) - .then_with(|| id_a.cmp(&id_b)) + .then_with(|| id_a.cmp(id_b)) }); } + /// Returns all of the diagnostics in a particular diagnostic group, + /// in order of their position in the buffer. pub fn group<'a, O: FromAnchor>( &'a self, group_id: usize, @@ -183,6 +210,7 @@ impl DiagnosticSet { .map(|entry| entry.resolve(buffer)) } } + impl sum_tree::Item for DiagnosticEntry { type Summary = Summary; @@ -198,6 +226,7 @@ impl sum_tree::Item for DiagnosticEntry { } impl DiagnosticEntry { + /// Converts the [DiagnosticEntry] to a different buffer coordinate type. pub fn resolve(&self, buffer: &text::BufferSnapshot) -> DiagnosticEntry { DiagnosticEntry { range: O::from_anchor(&self.range.start, buffer) diff --git a/crates/language/src/highlight_map.rs b/crates/language/src/highlight_map.rs index 270ac259c9d78eff8d36a2ee8c8038f117d33260..8829eb94ac576be4b1ad7bc6512fd4720cf81dcb 100644 --- a/crates/language/src/highlight_map.rs +++ b/crates/language/src/highlight_map.rs @@ -11,7 +11,7 @@ pub struct HighlightId(pub u32); const DEFAULT_SYNTAX_HIGHLIGHT_ID: HighlightId = HighlightId(u32::MAX); impl HighlightMap { - pub fn new(capture_names: &[&str], theme: &SyntaxTheme) -> Self { + pub(crate) fn new(capture_names: &[&str], theme: &SyntaxTheme) -> Self { // For each capture name in the highlight query, find the longest // key in the theme's syntax styles that matches all of the // dot-separated components of the capture name. @@ -51,7 +51,7 @@ impl HighlightMap { } impl HighlightId { - pub fn is_default(&self) -> bool { + pub(crate) fn is_default(&self) -> bool { *self == DEFAULT_SYNTAX_HIGHLIGHT_ID } diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 57b76eddadc69c10201d0f2ba2430a1ea4abb07d..c7327b63d7cfae43f1f0c017c08a78d57b3b75b7 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -1,3 +1,11 @@ +//! The `language` crate provides a large chunk of Zed's language-related +//! features (the other big contributors being project and lsp crates that revolve around LSP features). +//! Namely, this crate: +//! - Provides [`Language`], [`Grammar`] and [`LanguageRegistry`] types that +//! use Tree-sitter to provide syntax highlighting to the editor; note though that `language` doesn't perform the highlighting by itself. It only maps ranges in a buffer to colors. Treesitter is also used for buffer outlines (lists of symbols in a buffer) +//! - Exposes [`LanguageConfig`] that describes how constructs (like brackets or line comments) should be handled by the editor for a source file of a particular language. +//! +//! Notably we do *not* assign a single language to a single file; in real world a single file can consist of multiple programming languages - HTML is a good example of that - and `language` crate tends to reflect that status quo in it's API. mod buffer; mod diagnostic_set; mod highlight_map; @@ -54,10 +62,13 @@ pub use buffer::*; pub use diagnostic_set::DiagnosticEntry; pub use lsp::LanguageServerId; pub use outline::{Outline, OutlineItem}; -pub use syntax_map::{OwnedSyntaxLayerInfo, SyntaxLayerInfo}; +pub use syntax_map::{OwnedSyntaxLayer, SyntaxLayer}; pub use text::LineEnding; pub use tree_sitter::{Parser, Tree}; +/// Initializes the `language` crate. +/// +/// This should be called before making use of items from the create. pub fn init(cx: &mut AppContext) { language_settings::init(cx); } @@ -90,7 +101,9 @@ thread_local! { } lazy_static! { - pub static ref NEXT_GRAMMAR_ID: AtomicUsize = Default::default(); + pub(crate) static ref NEXT_GRAMMAR_ID: AtomicUsize = Default::default(); + /// A shared grammar for plain text, exposed for reuse by downstream crates. + #[doc(hidden)] pub static ref PLAIN_TEXT: Arc = Arc::new(Language::new( LanguageConfig { name: "Plain Text".into(), @@ -100,10 +113,14 @@ lazy_static! { )); } +/// Types that represent a position in a buffer, and can be converted into +/// an LSP position, to send to a language server. pub trait ToLspPosition { + /// Converts the value into an LSP position. fn to_lsp_position(self) -> lsp::Position; } +/// A name of a language server. #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct LanguageServerName(pub Arc); @@ -239,6 +256,8 @@ impl CachedLspAdapter { } } +/// [`LspAdapterDelegate`] allows [`LspAdapter]` implementations to interface with the application +// e.g. to display a notification or fetch data from the web. pub trait LspAdapterDelegate: Send + Sync { fn show_notification(&self, message: &str, cx: &mut AppContext); fn http_client(&self) -> Arc; @@ -284,6 +303,10 @@ pub trait LspAdapter: 'static + Send + Sync { delegate: &dyn LspAdapterDelegate, ) -> Option; + /// Returns true if a language server can be reinstalled. + /// If language server initialization fails, a reinstallation will be attempted unless the value returned from this method is false. + /// Implementations that rely on software already installed on user's system + /// should have [`can_be_reinstalled`] return false. fn can_be_reinstalled(&self) -> bool { true } @@ -295,6 +318,9 @@ pub trait LspAdapter: 'static + Send + Sync { fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {} + /// A callback called for each [`lsp_types::CompletionItem`] obtained from LSP server. + /// Some LspAdapter implementations might want to modify the obtained item to + /// change how it's displayed. async fn process_completion(&self, _: &mut lsp::CompletionItem) {} async fn label_for_completion( @@ -314,6 +340,7 @@ pub trait LspAdapter: 'static + Send + Sync { None } + /// Returns initialization options that are going to be sent to a LSP server as a part of [`lsp_types::InitializeParams`] async fn initialization_options(&self) -> Option { None } @@ -322,6 +349,7 @@ pub trait LspAdapter: 'static + Send + Sync { futures::future::ready(serde_json::json!({})).boxed() } + /// Returns a list of code actions supported by a given LspAdapter fn code_action_kinds(&self) -> Option> { Some(vec![ CodeActionKind::EMPTY, @@ -351,43 +379,69 @@ pub trait LspAdapter: 'static + Send + Sync { #[derive(Clone, Debug, PartialEq, Eq)] pub struct CodeLabel { + /// The text to display. pub text: String, + /// Syntax highlighting runs. pub runs: Vec<(Range, HighlightId)>, + /// The portion of the text that should be used in fuzzy filtering. pub filter_range: Range, } #[derive(Clone, Deserialize)] pub struct LanguageConfig { + /// Human-readable name of the language. pub name: Arc, + // The name of the grammar in a WASM bundle (experimental). pub grammar_name: Option>, + /// Given a list of `LanguageConfig`'s, the language of a file can be determined based on the path extension matching any of the `path_suffixes`. pub path_suffixes: Vec, + /// List of bracket types in a language. pub brackets: BracketPairConfig, + /// A regex pattern that determines whether the language should be assigned to a file or not. #[serde(default, deserialize_with = "deserialize_regex")] pub first_line_pattern: Option, + /// If set to true, auto indentation uses last non empty line to determine + /// the indentation level for a new line. #[serde(default = "auto_indent_using_last_non_empty_line_default")] pub auto_indent_using_last_non_empty_line: bool, + /// A regex that is used to determine whether the indentation level should be + /// increased in the following line. #[serde(default, deserialize_with = "deserialize_regex")] pub increase_indent_pattern: Option, + /// A regex that is used to determine whether the indentation level should be + /// decreased in the following line. #[serde(default, deserialize_with = "deserialize_regex")] pub decrease_indent_pattern: Option, + /// A list of characters that trigger the automatic insertion of a closing + /// bracket when they immediately precede the point where an opening + /// bracket is inserted. #[serde(default)] pub autoclose_before: String, - #[serde(default)] - pub line_comment: Option>, + /// A placeholder used internally by Semantic Index. #[serde(default)] pub collapsed_placeholder: String, + /// A line comment string that is inserted in e.g. `toggle comments` action. + #[serde(default)] + pub line_comment: Option>, + /// Starting and closing characters of a block comment. #[serde(default)] pub block_comment: Option<(Arc, Arc)>, + /// A list of language servers that are allowed to run on subranges of a given language. #[serde(default)] pub scope_opt_in_language_servers: Vec, #[serde(default)] pub overrides: HashMap, + /// A list of characters that Zed should treat as word characters for the + /// purpose of features that operate on word boundaries, like 'move to next word end' + /// or a whole-word search in buffer search. #[serde(default)] pub word_characters: HashSet, + /// The name of a Prettier parser that should be used for this language. #[serde(default)] pub prettier_parser_name: Option, } +/// Tree-sitter language queries for a given language. #[derive(Debug, Default)] pub struct LanguageQueries { pub highlights: Option>, @@ -399,6 +453,9 @@ pub struct LanguageQueries { pub overrides: Option>, } +/// Represents a language for the given range. Some languages (e.g. HTML) +/// interleave several languages together, thus a single buffer might actually contain +/// several nested scopes. #[derive(Clone, Debug)] pub struct LanguageScope { language: Arc, @@ -458,9 +515,9 @@ impl Default for LanguageConfig { block_comment: Default::default(), scope_opt_in_language_servers: Default::default(), overrides: Default::default(), - collapsed_placeholder: Default::default(), word_characters: Default::default(), prettier_parser_name: None, + collapsed_placeholder: Default::default(), } } } @@ -478,6 +535,7 @@ fn deserialize_regex<'de, D: Deserializer<'de>>(d: D) -> Result, D } } +#[doc(hidden)] #[cfg(any(test, feature = "test-support"))] pub struct FakeLspAdapter { pub name: &'static str, @@ -489,9 +547,16 @@ pub struct FakeLspAdapter { pub prettier_plugins: Vec<&'static str>, } +/// Configuration of handling bracket pairs for a given language. +/// +/// This struct includes settings for defining which pairs of characters are considered brackets and +/// also specifies any language-specific scopes where these pairs should be ignored for bracket matching purposes. #[derive(Clone, Debug, Default)] pub struct BracketPairConfig { + /// A list of character pairs that should be treated as brackets in the context of a given language. pub pairs: Vec, + /// A list of tree-sitter scopes for which a given bracket should not be active. + /// N-th entry in `[Self::disabled_scopes_by_bracket_ix]` contains a list of disabled scopes for an n-th entry in `[Self::pairs]` pub disabled_scopes_by_bracket_ix: Vec>, } @@ -523,11 +588,18 @@ impl<'de> Deserialize<'de> for BracketPairConfig { } } +/// Describes a single bracket pair and how an editor should react to e.g. inserting +/// an opening bracket or to a newline character insertion inbetween `start` and `end` characters. #[derive(Clone, Debug, Default, Deserialize, PartialEq)] pub struct BracketPair { + /// Starting substring for a bracket. pub start: String, + /// Ending substring for a bracket. pub end: String, + /// True if `end` should be automatically inserted right after `start` characters. pub close: bool, + /// True if an extra newline should be inserted while the cursor is in the middle + /// of that bracket pair. pub newline: bool, } @@ -780,7 +852,7 @@ impl LanguageRegistry { let mut state = self.state.write(); state.theme = Some(theme.clone()); for language in &state.languages { - language.set_theme(&theme.syntax()); + language.set_theme(theme.syntax()); } } @@ -1094,7 +1166,7 @@ impl LanguageRegistryState { fn add(&mut self, language: Arc) { if let Some(theme) = self.theme.as_ref() { - language.set_theme(&theme.syntax()); + language.set_theme(theme.syntax()); } self.languages.push(language); self.version += 1; @@ -1641,6 +1713,8 @@ impl LanguageScope { self.language.config.collapsed_placeholder.as_ref() } + /// Returns line prefix that is inserted in e.g. line continuations or + /// in `toggle comments` action. pub fn line_comment_prefix(&self) -> Option<&Arc> { Override::as_option( self.config_override().map(|o| &o.line_comment), @@ -1656,6 +1730,11 @@ impl LanguageScope { .map(|e| (&e.0, &e.1)) } + /// Returns a list of language-specific word characters. + /// + /// By default, Zed treats alphanumeric characters (and '_') as word characters for + /// the purpose of actions like 'move to next word end` or whole-word search. + /// It additionally accounts for language's additional word characters. pub fn word_characters(&self) -> Option<&HashSet> { Override::as_option( self.config_override().map(|o| &o.word_characters), @@ -1663,6 +1742,8 @@ impl LanguageScope { ) } + /// Returns a list of bracket pairs for a given language with an additional + /// piece of information about whether the particular bracket pair is currently active for a given language. pub fn brackets(&self) -> impl Iterator { let mut disabled_ids = self .config_override() diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index 5359d184d65a9249f8fc82b1eae9c95d71beda6c..eeb674189fc93d443cce07f87fad25e53c1de553 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -1,3 +1,5 @@ +//! Provides `language`-related settings. + use crate::{File, Language}; use anyhow::Result; use collections::{HashMap, HashSet}; @@ -11,10 +13,12 @@ use serde::{Deserialize, Serialize}; use settings::Settings; use std::{num::NonZeroU32, path::Path, sync::Arc}; +/// Initializes the language settings. pub fn init(cx: &mut AppContext) { AllLanguageSettings::register(cx); } +/// Returns the settings for the specified language from the provided file. pub fn language_settings<'a>( language: Option<&Arc>, file: Option<&Arc>, @@ -24,6 +28,7 @@ pub fn language_settings<'a>( all_language_settings(file, cx).language(language_name.as_deref()) } +/// Returns the settings for all languages from the provided file. pub fn all_language_settings<'a>( file: Option<&Arc>, cx: &'a AppContext, @@ -32,51 +37,91 @@ pub fn all_language_settings<'a>( AllLanguageSettings::get(location, cx) } +/// The settings for all languages. #[derive(Debug, Clone)] pub struct AllLanguageSettings { + /// The settings for GitHub Copilot. pub copilot: CopilotSettings, defaults: LanguageSettings, languages: HashMap, LanguageSettings>, } +/// The settings for a particular language. #[derive(Debug, Clone, Deserialize)] pub struct LanguageSettings { + /// How many columns a tab should occupy. pub tab_size: NonZeroU32, + /// Whether to indent lines using tab characters, as opposed to multiple + /// spaces. pub hard_tabs: bool, + /// How to soft-wrap long lines of text. pub soft_wrap: SoftWrap, + /// The column at which to soft-wrap lines, for buffers where soft-wrap + /// is enabled. pub preferred_line_length: u32, + /// Whether to show wrap guides in the editor. Setting this to true will + /// show a guide at the 'preferred_line_length' value if softwrap is set to + /// 'preferred_line_length', and will show any additional guides as specified + /// by the 'wrap_guides' setting. pub show_wrap_guides: bool, + /// Character counts at which to show wrap guides in the editor. pub wrap_guides: Vec, + /// Whether or not to perform a buffer format before saving. pub format_on_save: FormatOnSave, + /// Whether or not to remove any trailing whitespace from lines of a buffer + /// before saving it. pub remove_trailing_whitespace_on_save: bool, + /// Whether or not to ensure there's a single newline at the end of a buffer + /// when saving it. pub ensure_final_newline_on_save: bool, + /// How to perform a buffer format. pub formatter: Formatter, + /// Zed's Prettier integration settings. + /// If Prettier is enabled, Zed will use this its Prettier instance for any applicable file, if + /// the project has no other Prettier installed. pub prettier: HashMap, + /// Whether to use language servers to provide code intelligence. pub enable_language_server: bool, + /// Controls whether Copilot provides suggestion immediately (true) + /// or waits for a `copilot::Toggle` (false). pub show_copilot_suggestions: bool, + /// Whether to show tabs and spaces in the editor. pub show_whitespaces: ShowWhitespaceSetting, + /// Whether to start a new line with a comment when a previous line is a comment as well. pub extend_comment_on_newline: bool, + /// Inlay hint related settings. pub inlay_hints: InlayHintSettings, + /// Whether to automatically close brackets. + pub use_autoclose: bool, } +/// The settings for [GitHub Copilot](https://github.com/features/copilot). #[derive(Clone, Debug, Default)] pub struct CopilotSettings { + /// Whether Copilot is enabled. pub feature_enabled: bool, + /// A list of globs representing files that Copilot should be disabled for. pub disabled_globs: Vec, } +/// The settings for all languages. #[derive(Clone, Default, Serialize, Deserialize, JsonSchema)] pub struct AllLanguageSettingsContent { + /// The settings for enabling/disabling features. #[serde(default)] pub features: Option, + /// The settings for GitHub Copilot. #[serde(default)] pub copilot: Option, + /// The default language settings. #[serde(flatten)] pub defaults: LanguageSettingsContent, + /// The settings for individual languages. #[serde(default, alias = "language_overrides")] pub languages: HashMap, LanguageSettingsContent>, } +/// The settings for a particular language. #[derive(Clone, Default, Serialize, Deserialize, JsonSchema)] pub struct LanguageSettingsContent { /// How many columns a tab should occupy. @@ -138,7 +183,7 @@ pub struct LanguageSettingsContent { pub formatter: Option, /// Zed's Prettier integration settings. /// If Prettier is enabled, Zed will use this its Prettier instance for any applicable file, if - /// project has no other Prettier installed. + /// the project has no other Prettier installed. /// /// Default: {} #[serde(default)] @@ -148,7 +193,7 @@ pub struct LanguageSettingsContent { /// Default: true #[serde(default)] pub enable_language_server: Option, - /// Controls whether copilot provides suggestion immediately (true) + /// Controls whether Copilot provides suggestion immediately (true) /// or waits for a `copilot::Toggle` (false). /// /// Default: true @@ -165,20 +210,30 @@ pub struct LanguageSettingsContent { /// Inlay hint related settings. #[serde(default)] pub inlay_hints: Option, + /// Whether to automatically type closing characters for you. For example, + /// when you type (, Zed will automatically add a closing ) at the correct position. + /// + /// Default: true + pub use_autoclose: Option, } +/// The contents of the GitHub Copilot settings. #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] pub struct CopilotSettingsContent { + /// A list of globs representing files that Copilot should be disabled for. #[serde(default)] pub disabled_globs: Option>, } +/// The settings for enabling/disabling features. #[derive(Clone, Default, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] pub struct FeaturesContent { + /// Whether the GitHub Copilot feature is enabled. pub copilot: Option, } +/// Controls the soft-wrapping behavior in the editor. #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum SoftWrap { @@ -190,29 +245,38 @@ pub enum SoftWrap { PreferredLineLength, } +/// Controls the behavior of formatting files when they are saved. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum FormatOnSave { + /// Files should be formatted on save. On, + /// Files should not be formatted on save. Off, + /// Files should be formatted using the current language server. LanguageServer, + /// The external program to use to format the files on save. External { + /// The external program to run. command: Arc, + /// The arguments to pass to the program. arguments: Arc<[String]>, }, } +/// Controls how whitespace should be displayedin the editor. #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum ShowWhitespaceSetting { - /// Draw tabs and spaces only for the selected text. + /// Draw whitespace only for the selected text. Selection, - /// Do not draw any tabs or spaces + /// Do not draw any tabs or spaces. None, - /// Draw all invisible symbols + /// Draw all invisible symbols. All, } +/// Controls which formatter should be used when formatting code. #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum Formatter { @@ -226,11 +290,14 @@ pub enum Formatter { Prettier, /// Format code using an external command. External { + /// The external program to run. command: Arc, + /// The arguments to pass to the program. arguments: Arc<[String]>, }, } +/// The settings for inlay hints. #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] pub struct InlayHintSettings { /// Global switch to toggle hints on and off. @@ -238,10 +305,19 @@ pub struct InlayHintSettings { /// Default: false #[serde(default)] pub enabled: bool, + /// Whether type hints should be shown. + /// + /// Default: true #[serde(default = "default_true")] pub show_type_hints: bool, + /// Whether parameter hints should be shown. + /// + /// Default: true #[serde(default = "default_true")] pub show_parameter_hints: bool, + /// Whether other hints should be shown. + /// + /// Default: true #[serde(default = "default_true")] pub show_other_hints: bool, } @@ -251,6 +327,7 @@ fn default_true() -> bool { } impl InlayHintSettings { + /// Returns the kinds of inlay hints that are enabled based on the settings. pub fn enabled_inlay_hint_kinds(&self) -> HashSet> { let mut kinds = HashSet::default(); if self.show_type_hints { @@ -267,6 +344,7 @@ impl InlayHintSettings { } impl AllLanguageSettings { + /// Returns the [`LanguageSettings`] for the language with the specified name. pub fn language<'a>(&'a self, language_name: Option<&str>) -> &'a LanguageSettings { if let Some(name) = language_name { if let Some(overrides) = self.languages.get(name) { @@ -276,6 +354,7 @@ impl AllLanguageSettings { &self.defaults } + /// Returns whether GitHub Copilot is enabled for the given path. pub fn copilot_enabled_for_path(&self, path: &Path) -> bool { !self .copilot @@ -284,6 +363,7 @@ impl AllLanguageSettings { .any(|glob| glob.is_match(path)) } + /// Returns whether GitHub Copilot is enabled for the given language and path. pub fn copilot_enabled(&self, language: Option<&Arc>, path: Option<&Path>) -> bool { if !self.copilot.feature_enabled { return false; @@ -300,13 +380,20 @@ impl AllLanguageSettings { } } +/// The kind of an inlay hint. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum InlayHintKind { + /// An inlay hint for a type. Type, + /// An inlay hint for a parameter. Parameter, } impl InlayHintKind { + /// Returns the [`InlayHintKind`] from the given name. + /// + /// Returns `None` if `name` does not match any of the expected + /// string representations. pub fn from_name(name: &str) -> Option { match name { "type" => Some(InlayHintKind::Type), @@ -315,6 +402,7 @@ impl InlayHintKind { } } + /// Returns the name of this [`InlayHintKind`]. pub fn name(&self) -> &'static str { match self { InlayHintKind::Type => "type", @@ -340,7 +428,7 @@ impl settings::Settings for AllLanguageSettings { let mut languages = HashMap::default(); for (language_name, settings) in &default_value.languages { let mut language_settings = defaults.clone(); - merge_settings(&mut language_settings, &settings); + merge_settings(&mut language_settings, settings); languages.insert(language_name.clone(), language_settings); } @@ -380,7 +468,7 @@ impl settings::Settings for AllLanguageSettings { languages .entry(language_name.clone()) .or_insert_with(|| defaults.clone()), - &user_language_settings, + user_language_settings, ); } } @@ -459,6 +547,7 @@ fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent merge(&mut settings.tab_size, src.tab_size); merge(&mut settings.hard_tabs, src.hard_tabs); merge(&mut settings.soft_wrap, src.soft_wrap); + merge(&mut settings.use_autoclose, src.use_autoclose); merge(&mut settings.show_wrap_guides, src.show_wrap_guides); merge(&mut settings.wrap_guides, src.wrap_guides.clone()); diff --git a/crates/language/src/markdown.rs b/crates/language/src/markdown.rs index df75b610ef844ec858d74ae7a3a8bafa5a59edbe..0ff78e49a9b9a7a089f9223e468e938c139b3104 100644 --- a/crates/language/src/markdown.rs +++ b/crates/language/src/markdown.rs @@ -1,3 +1,5 @@ +//! Provides Markdown-related constructs. + use std::sync::Arc; use std::{ops::Range, path::PathBuf}; @@ -5,21 +7,30 @@ use crate::{HighlightId, Language, LanguageRegistry}; use gpui::{px, FontStyle, FontWeight, HighlightStyle, UnderlineStyle}; use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag}; +/// Parsed Markdown content. #[derive(Debug, Clone)] pub struct ParsedMarkdown { + /// The Markdown text. pub text: String, + /// The list of highlights contained in the Markdown document. pub highlights: Vec<(Range, MarkdownHighlight)>, + /// The regions of the various ranges in the Markdown document. pub region_ranges: Vec>, + /// The regions of the Markdown document. pub regions: Vec, } +/// A run of highlighted Markdown text. #[derive(Debug, Clone, PartialEq, Eq)] pub enum MarkdownHighlight { + /// A styled Markdown highlight. Style(MarkdownHighlightStyle), + /// A highlighted code block. Code(HighlightId), } impl MarkdownHighlight { + /// Converts this [`MarkdownHighlight`] to a [`HighlightStyle`]. pub fn to_highlight_style(&self, theme: &theme::SyntaxTheme) -> Option { match self { MarkdownHighlight::Style(style) => { @@ -48,23 +59,39 @@ impl MarkdownHighlight { } } +/// The style for a Markdown highlight. #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct MarkdownHighlightStyle { + /// Whether the text should be italicized. pub italic: bool, + /// Whether the text should be underlined. pub underline: bool, + /// The weight of the text. pub weight: FontWeight, } +/// A parsed region in a Markdown document. #[derive(Debug, Clone)] pub struct ParsedRegion { + /// Whether the region is a code block. pub code: bool, + /// The link contained in this region, if it has one. pub link: Option, } +/// A Markdown link. #[derive(Debug, Clone)] pub enum Link { - Web { url: String }, - Path { path: PathBuf }, + /// A link to a webpage. + Web { + /// The URL of the webpage. + url: String, + }, + /// A link to a path on the filesystem. + Path { + /// The path to the item. + path: PathBuf, + }, } impl Link { @@ -82,6 +109,7 @@ impl Link { } } +/// Parses a string of Markdown. pub async fn parse_markdown( markdown: &str, language_registry: &Arc, @@ -111,6 +139,7 @@ pub async fn parse_markdown( } } +/// Parses a Markdown block. pub async fn parse_markdown_block( markdown: &str, language_registry: &Arc, @@ -126,7 +155,7 @@ pub async fn parse_markdown_block( let mut current_language = None; let mut list_stack = Vec::new(); - for event in Parser::new_ext(&markdown, Options::all()) { + for event in Parser::new_ext(markdown, Options::all()) { let prev_len = text.len(); match event { Event::Text(t) => { @@ -261,6 +290,7 @@ pub async fn parse_markdown_block( } } +/// Appends a highlighted run of text to the provided `text` buffer. pub fn highlight_code( text: &mut String, highlights: &mut Vec<(Range, MarkdownHighlight)>, @@ -275,6 +305,7 @@ pub fn highlight_code( } } +/// Appends a new paragraph to the provided `text` buffer. pub fn new_paragraph(text: &mut String, list_stack: &mut Vec<(Option, bool)>) { let mut is_subsequent_paragraph_of_list = false; if let Some((_, has_content)) = list_stack.last_mut() { diff --git a/crates/language/src/outline.rs b/crates/language/src/outline.rs index df1a3c629e75e7695fcf9bd1f6c6a796df2c01f1..014b32676af586bb3b0f69f3b14e4ee8ec99f6c9 100644 --- a/crates/language/src/outline.rs +++ b/crates/language/src/outline.rs @@ -2,6 +2,7 @@ use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{BackgroundExecutor, HighlightStyle}; use std::ops::Range; +/// An outline of all the symbols contained in a buffer. #[derive(Debug)] pub struct Outline { pub items: Vec>, diff --git a/crates/language/src/proto.rs b/crates/language/src/proto.rs index 957f4ee7fbc4da251698fd640d33d37d5cf4a06b..d4b553de47966847916794a327b315272bbff3d1 100644 --- a/crates/language/src/proto.rs +++ b/crates/language/src/proto.rs @@ -1,3 +1,5 @@ +//! Handles conversions of `language` items to and from the [`rpc`] protocol. + use crate::{ diagnostic_set::DiagnosticEntry, CodeAction, CodeLabel, Completion, CursorShape, Diagnostic, Language, @@ -11,15 +13,18 @@ use text::*; pub use proto::{BufferState, Operation}; +/// Serializes a [`RopeFingerprint`] to be sent over RPC. pub fn serialize_fingerprint(fingerprint: RopeFingerprint) -> String { fingerprint.to_hex() } +/// Deserializes a [`RopeFingerprint`] from the RPC representation. pub fn deserialize_fingerprint(fingerprint: &str) -> Result { RopeFingerprint::from_hex(fingerprint) .map_err(|error| anyhow!("invalid fingerprint: {}", error)) } +/// Deserializes a `[text::LineEnding]` from the RPC representation. pub fn deserialize_line_ending(message: proto::LineEnding) -> text::LineEnding { match message { proto::LineEnding::Unix => text::LineEnding::Unix, @@ -27,6 +32,7 @@ pub fn deserialize_line_ending(message: proto::LineEnding) -> text::LineEnding { } } +/// Serializes a [`text::LineEnding`] to be sent over RPC. pub fn serialize_line_ending(message: text::LineEnding) -> proto::LineEnding { match message { text::LineEnding::Unix => proto::LineEnding::Unix, @@ -34,6 +40,7 @@ pub fn serialize_line_ending(message: text::LineEnding) -> proto::LineEnding { } } +/// Serializes a [`crate::Operation`] to be sent over RPC. pub fn serialize_operation(operation: &crate::Operation) -> proto::Operation { proto::Operation { variant: Some(match operation { @@ -96,6 +103,7 @@ pub fn serialize_operation(operation: &crate::Operation) -> proto::Operation { } } +/// Serializes an [`operation::EditOperation`] to be sent over RPC. pub fn serialize_edit_operation(operation: &EditOperation) -> proto::operation::Edit { proto::operation::Edit { replica_id: operation.timestamp.replica_id as u32, @@ -110,6 +118,7 @@ pub fn serialize_edit_operation(operation: &EditOperation) -> proto::operation:: } } +/// Serializes an entry in the undo map to be sent over RPC. pub fn serialize_undo_map_entry( (edit_id, counts): (&clock::Lamport, &[(clock::Lamport, u32)]), ) -> proto::UndoMapEntry { @@ -127,6 +136,7 @@ pub fn serialize_undo_map_entry( } } +/// Splits the given list of operations into chunks. pub fn split_operations( mut operations: Vec, ) -> impl Iterator> { @@ -152,10 +162,12 @@ pub fn split_operations( }) } +/// Serializes selections to be sent over RPC. pub fn serialize_selections(selections: &Arc<[Selection]>) -> Vec { selections.iter().map(serialize_selection).collect() } +/// Serializes a [`Selection`] to be sent over RPC. pub fn serialize_selection(selection: &Selection) -> proto::Selection { proto::Selection { id: selection.id as u64, @@ -171,6 +183,7 @@ pub fn serialize_selection(selection: &Selection) -> proto::Selection { } } +/// Serializes a [`CursorShape`] to be sent over RPC. pub fn serialize_cursor_shape(cursor_shape: &CursorShape) -> proto::CursorShape { match cursor_shape { CursorShape::Bar => proto::CursorShape::CursorBar, @@ -180,6 +193,7 @@ pub fn serialize_cursor_shape(cursor_shape: &CursorShape) -> proto::CursorShape } } +/// Deserializes a [`CursorShape`] from the RPC representation. pub fn deserialize_cursor_shape(cursor_shape: proto::CursorShape) -> CursorShape { match cursor_shape { proto::CursorShape::CursorBar => CursorShape::Bar, @@ -189,6 +203,7 @@ pub fn deserialize_cursor_shape(cursor_shape: proto::CursorShape) -> CursorShape } } +/// Serializes a list of diagnostics to be sent over RPC. pub fn serialize_diagnostics<'a>( diagnostics: impl IntoIterator>, ) -> Vec { @@ -208,7 +223,7 @@ pub fn serialize_diagnostics<'a>( } as i32, group_id: entry.diagnostic.group_id as u64, is_primary: entry.diagnostic.is_primary, - is_valid: entry.diagnostic.is_valid, + is_valid: true, code: entry.diagnostic.code.clone(), is_disk_based: entry.diagnostic.is_disk_based, is_unnecessary: entry.diagnostic.is_unnecessary, @@ -216,6 +231,7 @@ pub fn serialize_diagnostics<'a>( .collect() } +/// Serializes an [`Anchor`] to be sent over RPC. pub fn serialize_anchor(anchor: &Anchor) -> proto::Anchor { proto::Anchor { replica_id: anchor.timestamp.replica_id as u32, @@ -230,6 +246,7 @@ pub fn serialize_anchor(anchor: &Anchor) -> proto::Anchor { } // This behavior is currently copied in the collab database, for snapshotting channel notes +/// Deserializes an [`crate::Operation`] from the RPC representation. pub fn deserialize_operation(message: proto::Operation) -> Result { Ok( match message @@ -312,6 +329,7 @@ pub fn deserialize_operation(message: proto::Operation) -> Result EditOperation { EditOperation { timestamp: clock::Lamport { @@ -324,6 +342,7 @@ pub fn deserialize_edit_operation(edit: proto::operation::Edit) -> EditOperation } } +/// Deserializes an entry in the undo map from the RPC representation. pub fn deserialize_undo_map_entry( entry: proto::UndoMapEntry, ) -> (clock::Lamport, Vec<(clock::Lamport, u32)>) { @@ -348,6 +367,7 @@ pub fn deserialize_undo_map_entry( ) } +/// Deserializes selections from the RPC representation. pub fn deserialize_selections(selections: Vec) -> Arc<[Selection]> { Arc::from( selections @@ -357,6 +377,7 @@ pub fn deserialize_selections(selections: Vec) -> Arc<[Selecti ) } +/// Deserializes a [`Selection`] from the RPC representation. pub fn deserialize_selection(selection: proto::Selection) -> Option> { Some(Selection { id: selection.id as usize, @@ -367,6 +388,7 @@ pub fn deserialize_selection(selection: proto::Selection) -> Option, ) -> Arc<[DiagnosticEntry]> { @@ -387,7 +409,6 @@ pub fn deserialize_diagnostics( message: diagnostic.message, group_id: diagnostic.group_id as usize, code: diagnostic.code, - is_valid: diagnostic.is_valid, is_primary: diagnostic.is_primary, is_disk_based: diagnostic.is_disk_based, is_unnecessary: diagnostic.is_unnecessary, @@ -397,6 +418,7 @@ pub fn deserialize_diagnostics( .collect() } +/// Deserializes an [`Anchor`] from the RPC representation. pub fn deserialize_anchor(anchor: proto::Anchor) -> Option { Some(Anchor { timestamp: clock::Lamport { @@ -412,6 +434,7 @@ pub fn deserialize_anchor(anchor: proto::Anchor) -> Option { }) } +/// Returns a `[clock::Lamport`] timestamp for the given [`proto::Operation`]. pub fn lamport_timestamp_for_operation(operation: &proto::Operation) -> Option { let replica_id; let value; @@ -444,6 +467,7 @@ pub fn lamport_timestamp_for_operation(operation: &proto::Operation) -> Option proto::Completion { proto::Completion { old_start: Some(serialize_anchor(&completion.old_range.start)), @@ -454,6 +478,7 @@ pub fn serialize_completion(completion: &Completion) -> proto::Completion { } } +/// Deserializes a [`Completion`] from the RPC representation. pub async fn deserialize_completion( completion: proto::Completion, language: Option>, @@ -488,6 +513,7 @@ pub async fn deserialize_completion( }) } +/// Serializes a [`CodeAction`] to be sent over RPC. pub fn serialize_code_action(action: &CodeAction) -> proto::CodeAction { proto::CodeAction { server_id: action.server_id.0 as u64, @@ -497,6 +523,7 @@ pub fn serialize_code_action(action: &CodeAction) -> proto::CodeAction { } } +/// Deserializes a [`CodeAction`] from the RPC representation. pub fn deserialize_code_action(action: proto::CodeAction) -> Result { let start = action .start @@ -514,6 +541,7 @@ pub fn deserialize_code_action(action: proto::CodeAction) -> Result }) } +/// Serializes a [`Transaction`] to be sent over RPC. pub fn serialize_transaction(transaction: &Transaction) -> proto::Transaction { proto::Transaction { id: Some(serialize_timestamp(transaction.id)), @@ -527,6 +555,7 @@ pub fn serialize_transaction(transaction: &Transaction) -> proto::Transaction { } } +/// Deserializes a [`Transaction`] from the RPC representation. pub fn deserialize_transaction(transaction: proto::Transaction) -> Result { Ok(Transaction { id: deserialize_timestamp( @@ -543,6 +572,7 @@ pub fn deserialize_transaction(transaction: proto::Transaction) -> Result proto::LamportTimestamp { proto::LamportTimestamp { replica_id: timestamp.replica_id as u32, @@ -550,6 +580,7 @@ pub fn serialize_timestamp(timestamp: clock::Lamport) -> proto::LamportTimestamp } } +/// Deserializes a [`clock::Lamport`] timestamp from the RPC representation. pub fn deserialize_timestamp(timestamp: proto::LamportTimestamp) -> clock::Lamport { clock::Lamport { replica_id: timestamp.replica_id as ReplicaId, @@ -557,6 +588,7 @@ pub fn deserialize_timestamp(timestamp: proto::LamportTimestamp) -> clock::Lampo } } +/// Serializes a range of [`FullOffset`]s to be sent over RPC. pub fn serialize_range(range: &Range) -> proto::Range { proto::Range { start: range.start.0 as u64, @@ -564,10 +596,12 @@ pub fn serialize_range(range: &Range) -> proto::Range { } } +/// Deserializes a range of [`FullOffset`]s from the RPC representation. pub fn deserialize_range(range: proto::Range) -> Range { FullOffset(range.start as usize)..FullOffset(range.end as usize) } +/// Deserializes a clock version from the RPC representation. pub fn deserialize_version(message: &[proto::VectorClockEntry]) -> clock::Global { let mut version = clock::Global::new(); for entry in message { @@ -579,6 +613,7 @@ pub fn deserialize_version(message: &[proto::VectorClockEntry]) -> clock::Global version } +/// Serializes a clock version to be sent over RPC. pub fn serialize_version(version: &clock::Global) -> Vec { version .iter() diff --git a/crates/language/src/syntax_map.rs b/crates/language/src/syntax_map.rs index c22ece48aff0b41a089538cba70424f81cd4e042..6ca6a70e4a630099f576aca00f141533168fc412 100644 --- a/crates/language/src/syntax_map.rs +++ b/crates/language/src/syntax_map.rs @@ -29,7 +29,7 @@ pub struct SyntaxMap { #[derive(Clone, Default)] pub struct SyntaxSnapshot { - layers: SumTree, + layers: SumTree, parsed_version: clock::Global, interpolated_version: clock::Global, language_registry_version: usize, @@ -84,7 +84,7 @@ struct SyntaxMapMatchesLayer<'a> { } #[derive(Clone)] -struct SyntaxLayer { +struct SyntaxLayerEntry { depth: usize, range: Range, content: SyntaxLayerContent, @@ -117,17 +117,22 @@ impl SyntaxLayerContent { } } +/// A layer of syntax highlighting, corresponding to a single syntax +/// tree in a particular language. #[derive(Debug)] -pub struct SyntaxLayerInfo<'a> { - pub depth: usize, +pub struct SyntaxLayer<'a> { + /// The language for this layer. pub language: &'a Arc, + depth: usize, tree: &'a Tree, offset: (usize, tree_sitter::Point), } +/// A layer of syntax highlighting. Like [SyntaxLayer], but holding +/// owned data instead of references. #[derive(Clone)] -pub struct OwnedSyntaxLayerInfo { - pub depth: usize, +pub struct OwnedSyntaxLayer { + /// The language for this layer. pub language: Arc, tree: tree_sitter::Tree, offset: (usize, tree_sitter::Point), @@ -278,7 +283,7 @@ impl SyntaxSnapshot { depth, position: edit_range.start, }; - if target.cmp(&cursor.start(), text).is_gt() { + if target.cmp(cursor.start(), text).is_gt() { let slice = cursor.slice(&target, Bias::Left, text); layers.append(slice, text); } @@ -363,7 +368,7 @@ impl SyntaxSnapshot { cursor.next(text); } - layers.append(cursor.suffix(&text), &text); + layers.append(cursor.suffix(text), text); drop(cursor); self.layers = layers; } @@ -428,7 +433,7 @@ impl SyntaxSnapshot { let max_depth = self.layers.summary().max_depth; let mut cursor = self.layers.cursor::(); - cursor.next(&text); + cursor.next(text); let mut layers = SumTree::new(); let mut changed_regions = ChangeRegionSet::default(); @@ -466,17 +471,17 @@ impl SyntaxSnapshot { }; let mut done = cursor.item().is_none(); - while !done && position.cmp(&cursor.end(text), &text).is_gt() { + while !done && position.cmp(&cursor.end(text), text).is_gt() { done = true; let bounded_position = SyntaxLayerPositionBeforeChange { position: position.clone(), change: changed_regions.start_position(), }; - if bounded_position.cmp(&cursor.start(), &text).is_gt() { + if bounded_position.cmp(cursor.start(), text).is_gt() { let slice = cursor.slice(&bounded_position, Bias::Left, text); if !slice.is_empty() { - layers.append(slice, &text); + layers.append(slice, text); if changed_regions.prune(cursor.end(text), text) { done = false; } @@ -486,7 +491,7 @@ impl SyntaxSnapshot { while position.cmp(&cursor.end(text), text).is_gt() { let Some(layer) = cursor.item() else { break }; - if changed_regions.intersects(&layer, text) { + if changed_regions.intersects(layer, text) { if let SyntaxLayerContent::Parsed { language, .. } = &layer.content { log::trace!( "discard layer. language:{}, range:{:?}. changed_regions:{:?}", @@ -524,7 +529,7 @@ impl SyntaxSnapshot { if layer.range.to_offset(text) == (step_start_byte..step_end_byte) && layer.content.language_id() == step.language.id() { - cursor.next(&text); + cursor.next(text); } else { old_layer = None; } @@ -556,7 +561,7 @@ impl SyntaxSnapshot { log::trace!( "existing layer. language:{}, start:{:?}, ranges:{:?}", language.name(), - LogPoint(layer_start.to_point(&text)), + LogPoint(layer_start.to_point(text)), LogIncludedRanges(&old_tree.included_ranges()) ); @@ -579,7 +584,7 @@ impl SyntaxSnapshot { insert_newlines_between_ranges( changed_indices, &mut included_ranges, - &text, + text, step_start_byte, step_start_point, ); @@ -691,12 +696,12 @@ impl SyntaxSnapshot { }; layers.push( - SyntaxLayer { + SyntaxLayerEntry { depth: step.depth, range: step.range, content, }, - &text, + text, ); } @@ -741,7 +746,7 @@ impl SyntaxSnapshot { SyntaxMapCaptures::new( range.clone(), text, - [SyntaxLayerInfo { + [SyntaxLayer { language, tree, depth: 0, @@ -781,7 +786,7 @@ impl SyntaxSnapshot { } #[cfg(test)] - pub fn layers<'a>(&'a self, buffer: &'a BufferSnapshot) -> Vec { + pub fn layers<'a>(&'a self, buffer: &'a BufferSnapshot) -> Vec { self.layers_for_range(0..buffer.len(), buffer).collect() } @@ -789,7 +794,7 @@ impl SyntaxSnapshot { &'a self, range: Range, buffer: &'a BufferSnapshot, - ) -> impl 'a + Iterator { + ) -> impl 'a + Iterator { let start_offset = range.start.to_offset(buffer); let end_offset = range.end.to_offset(buffer); let start = buffer.anchor_before(start_offset); @@ -813,7 +818,7 @@ impl SyntaxSnapshot { let layer_start_offset = layer.range.start.to_offset(buffer); let layer_start_point = layer.range.start.to_point(buffer).to_ts_point(); - info = Some(SyntaxLayerInfo { + info = Some(SyntaxLayer { tree, language, depth: layer.depth, @@ -842,7 +847,7 @@ impl<'a> SyntaxMapCaptures<'a> { fn new( range: Range, text: &'a Rope, - layers: impl Iterator>, + layers: impl Iterator>, query: fn(&Grammar) -> Option<&Query>, ) -> Self { let mut result = Self { @@ -855,7 +860,7 @@ impl<'a> SyntaxMapCaptures<'a> { Some(grammar) => grammar, None => continue, }; - let query = match query(&grammar) { + let query = match query(grammar) { Some(query) => query, None => continue, }; @@ -964,7 +969,7 @@ impl<'a> SyntaxMapMatches<'a> { fn new( range: Range, text: &'a Rope, - layers: impl Iterator>, + layers: impl Iterator>, query: fn(&Grammar) -> Option<&Query>, ) -> Self { let mut result = Self::default(); @@ -973,7 +978,7 @@ impl<'a> SyntaxMapMatches<'a> { Some(grammar) => grammar, None => continue, }; - let query = match query(&grammar) { + let query = match query(grammar) { Some(query) => query, None => continue, }; @@ -1082,7 +1087,7 @@ impl<'a> SyntaxMapMatchesLayer<'a> { fn advance(&mut self) { if let Some(mat) = self.matches.next() { self.next_captures.clear(); - self.next_captures.extend_from_slice(&mat.captures); + self.next_captures.extend_from_slice(mat.captures); self.next_pattern_index = mat.pattern_index; self.has_next = true; } else { @@ -1296,7 +1301,7 @@ fn get_injections( } } -/// Update the given list of included `ranges`, removing any ranges that intersect +/// Updates the given list of included `ranges`, removing any ranges that intersect /// `removed_ranges`, and inserting the given `new_ranges`. /// /// Returns a new vector of ranges, and the range of the vector that was changed, @@ -1436,23 +1441,25 @@ fn insert_newlines_between_ranges( } } -impl OwnedSyntaxLayerInfo { +impl OwnedSyntaxLayer { + /// Returns the root syntax node for this layer. pub fn node(&self) -> Node { self.tree .root_node_with_offset(self.offset.0, self.offset.1) } } -impl<'a> SyntaxLayerInfo<'a> { - pub fn to_owned(&self) -> OwnedSyntaxLayerInfo { - OwnedSyntaxLayerInfo { +impl<'a> SyntaxLayer<'a> { + /// Returns an owned version of this layer. + pub fn to_owned(&self) -> OwnedSyntaxLayer { + OwnedSyntaxLayer { tree: self.tree.clone(), offset: self.offset, - depth: self.depth, language: self.language.clone(), } } + /// Returns the root node for this layer. pub fn node(&self) -> Node<'a> { self.tree .root_node_with_offset(self.offset.0, self.offset.1) @@ -1510,7 +1517,7 @@ impl Eq for ParseStep {} impl PartialOrd for ParseStep { fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(&other)) + Some(self.cmp(other)) } } @@ -1564,7 +1571,7 @@ impl ChangeRegionSet { ) } - fn intersects(&self, layer: &SyntaxLayer, text: &BufferSnapshot) -> bool { + fn intersects(&self, layer: &SyntaxLayerEntry, text: &BufferSnapshot) -> bool { for region in &self.0 { if region.depth < layer.depth { continue; @@ -1675,7 +1682,7 @@ impl<'a> SeekTarget<'a, SyntaxLayerSummary, SyntaxLayerSummary> } } -impl sum_tree::Item for SyntaxLayer { +impl sum_tree::Item for SyntaxLayerEntry { type Summary = SyntaxLayerSummary; fn summary(&self) -> Self::Summary { @@ -1690,7 +1697,7 @@ impl sum_tree::Item for SyntaxLayer { } } -impl std::fmt::Debug for SyntaxLayer { +impl std::fmt::Debug for SyntaxLayerEntry { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("SyntaxLayer") .field("depth", &self.depth) diff --git a/crates/language_selector/src/active_buffer_language.rs b/crates/language_selector/src/active_buffer_language.rs index d5f177f7d61a649b6b438782e265c0e5eb9a4b77..647ff93b818f37fff6396deb15edd12b59ede9b7 100644 --- a/crates/language_selector/src/active_buffer_language.rs +++ b/crates/language_selector/src/active_buffer_language.rs @@ -1,7 +1,7 @@ use editor::Editor; use gpui::{div, IntoElement, ParentElement, Render, Subscription, View, ViewContext, WeakView}; use std::sync::Arc; -use ui::{Button, ButtonCommon, Clickable, LabelSize, Tooltip}; +use ui::{Button, ButtonCommon, Clickable, FluentBuilder, LabelSize, Tooltip}; use workspace::{item::ItemHandle, StatusItemView, Workspace}; use crate::LanguageSelector; diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index b4e2b37e83fd52a26c63f97857a93e444323bc0a..f469eb402c4f548369af8948c5a1a84cee29cfe6 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -45,7 +45,7 @@ struct LanguageServerRpcState { pub struct LspLogView { pub(crate) editor: View, - editor_subscription: Subscription, + editor_subscriptions: Vec, log_store: Model, current_server_id: Option, is_showing_rpc_trace: bool, @@ -419,7 +419,7 @@ impl LspLogView { } } }); - let (editor, editor_subscription) = Self::editor_for_logs(String::new(), cx); + let (editor, editor_subscriptions) = Self::editor_for_logs(String::new(), cx); let focus_handle = cx.focus_handle(); let focus_subscription = cx.on_focus(&focus_handle, |log_view, cx| { @@ -429,7 +429,7 @@ impl LspLogView { let mut this = Self { focus_handle, editor, - editor_subscription, + editor_subscriptions, project, log_store, current_server_id: None, @@ -449,7 +449,7 @@ impl LspLogView { fn editor_for_logs( log_contents: String, cx: &mut ViewContext, - ) -> (View, Subscription) { + ) -> (View, Vec) { let editor = cx.new_view(|cx| { let mut editor = Editor::multi_line(cx); editor.set_text(log_contents, cx); @@ -464,7 +464,13 @@ impl LspLogView { cx.emit(event.clone()) }, ); - (editor, editor_subscription) + let search_subscription = cx.subscribe( + &editor, + |_, _, event: &SearchEvent, cx: &mut ViewContext<'_, LspLogView>| { + cx.emit(event.clone()) + }, + ); + (editor, vec![editor_subscription, search_subscription]) } pub(crate) fn menu_items<'a>(&'a self, cx: &'a AppContext) -> Option> { @@ -521,9 +527,9 @@ impl LspLogView { if let Some(log_contents) = log_contents { self.current_server_id = Some(server_id); self.is_showing_rpc_trace = false; - let (editor, editor_subscription) = Self::editor_for_logs(log_contents, cx); + let (editor, editor_subscriptions) = Self::editor_for_logs(log_contents, cx); self.editor = editor; - self.editor_subscription = editor_subscription; + self.editor_subscriptions = editor_subscriptions; cx.notify(); } cx.focus(&self.focus_handle); @@ -542,7 +548,7 @@ impl LspLogView { if let Some(rpc_log) = rpc_log { self.current_server_id = Some(server_id); self.is_showing_rpc_trace = true; - let (editor, editor_subscription) = Self::editor_for_logs(rpc_log, cx); + let (editor, editor_subscriptions) = Self::editor_for_logs(rpc_log, cx); let language = self.project.read(cx).languages().language_for_name("JSON"); editor .read(cx) @@ -564,7 +570,7 @@ impl LspLogView { }); self.editor = editor; - self.editor_subscription = editor_subscription; + self.editor_subscriptions = editor_subscriptions; cx.notify(); } diff --git a/crates/language_tools/src/syntax_tree_view.rs b/crates/language_tools/src/syntax_tree_view.rs index be677b215b744c239e0a290ad54c7bf23bfca2d4..082e77fc365182e19cd8cd95bc47d32b6a861f60 100644 --- a/crates/language_tools/src/syntax_tree_view.rs +++ b/crates/language_tools/src/syntax_tree_view.rs @@ -5,7 +5,7 @@ use gpui::{ MouseButton, MouseDownEvent, MouseMoveEvent, ParentElement, Render, Styled, UniformListScrollHandle, View, ViewContext, VisualContext, WeakView, WindowContext, }; -use language::{Buffer, OwnedSyntaxLayerInfo}; +use language::{Buffer, OwnedSyntaxLayer}; use std::{mem, ops::Range}; use theme::ActiveTheme; use tree_sitter::{Node, TreeCursor}; @@ -54,7 +54,7 @@ struct EditorState { struct BufferState { buffer: Model, excerpt_id: ExcerptId, - active_layer: Option, + active_layer: Option, } impl SyntaxTreeView { @@ -477,7 +477,7 @@ impl SyntaxTreeToolbarItemView { }) } - fn render_header(active_layer: &OwnedSyntaxLayerInfo) -> ButtonLike { + fn render_header(active_layer: &OwnedSyntaxLayer) -> ButtonLike { ButtonLike::new("syntax tree header") .child(Label::new(active_layer.language.name())) .child(Label::new(format_node_range(active_layer.node()))) diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index a70422008ccd86a4db70c33e4ac8cdface6c9217..bf7c3e79116ed2d8f5cc7941566d74ad237a40b8 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -345,7 +345,7 @@ impl LanguageServer { if let Some(handler) = notification_handlers.lock().get_mut(msg.method) { handler( msg.id, - &msg.params.map(|params| params.get()).unwrap_or("null"), + msg.params.map(|params| params.get()).unwrap_or("null"), cx.clone(), ); } else { @@ -863,17 +863,31 @@ impl LanguageServer { .try_send(message) .context("failed to write to language server's stdin"); + let outbound_tx = outbound_tx.downgrade(); let mut timeout = executor.timer(LSP_REQUEST_TIMEOUT).fuse(); let started = Instant::now(); async move { handle_response?; send?; + let cancel_on_drop = util::defer(move || { + if let Some(outbound_tx) = outbound_tx.upgrade() { + Self::notify_internal::( + &outbound_tx, + CancelParams { + id: NumberOrString::Number(id as i32), + }, + ) + .log_err(); + } + }); + let method = T::METHOD; futures::select! { response = rx.fuse() => { let elapsed = started.elapsed(); log::trace!("Took {elapsed:?} to receive response to {method:?} id {id}"); + cancel_on_drop.abort(); response? } diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index 9f78480136e2e6eff71fb3016a2c9c99d4297b6f..d83a547dcd7167000eab5b877c10e8eb109b97d8 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -2316,7 +2316,7 @@ impl MultiBufferSnapshot { &self, point: T, ) -> Option<(&BufferSnapshot, usize)> { - let offset = point.to_offset(&self); + let offset = point.to_offset(self); let mut cursor = self.excerpts.cursor::(); cursor.seek(&offset, Bias::Right, &()); if cursor.item().is_none() { @@ -3028,7 +3028,7 @@ impl MultiBufferSnapshot { pub fn has_git_diffs(&self) -> bool { for excerpt in self.excerpts.iter() { - if !excerpt.buffer.git_diff.is_empty() { + if excerpt.buffer.has_git_diff() { return true; } } @@ -3694,7 +3694,7 @@ impl ExcerptId { pub fn cmp(&self, other: &Self, snapshot: &MultiBufferSnapshot) -> cmp::Ordering { let a = snapshot.excerpt_locator_for_id(*self); let b = snapshot.excerpt_locator_for_id(*other); - a.cmp(&b).then_with(|| self.0.cmp(&other.0)) + a.cmp(b).then_with(|| self.0.cmp(&other.0)) } } diff --git a/crates/plugin_runtime/src/plugin.rs b/crates/plugin_runtime/src/plugin.rs index 8070539b35da3ab2ec438c3886defe3e8cff8578..a19a49c47a678e8f0fe78877de5e7a177c340c60 100644 --- a/crates/plugin_runtime/src/plugin.rs +++ b/crates/plugin_runtime/src/plugin.rs @@ -69,7 +69,7 @@ impl Default for Metering { } /// This struct is used to build a new [`Plugin`], using the builder pattern. -/// Create a new default plugin with `PluginBuilder::new_with_default_ctx`, +/// Creates a new default plugin with `PluginBuilder::new_with_default_ctx`, /// and add host-side exported functions using `host_function` and `host_function_async`. /// Finalize the plugin by calling [`init`]. pub struct PluginBuilder { @@ -90,7 +90,7 @@ fn create_default_engine() -> Result { } impl PluginBuilder { - /// Create a new [`PluginBuilder`] with the given WASI context. + /// Creates a new [`PluginBuilder`] with the given WASI context. /// Using the default context is a safe bet, see [`new_with_default_context`]. /// This plugin will yield after a configurable amount of fuel is consumed. pub fn new(wasi_ctx: WasiCtx, metering: Metering) -> Result { @@ -105,7 +105,7 @@ impl PluginBuilder { }) } - /// Create a new `PluginBuilder` with the default `WasiCtx` (see [`default_ctx`]). + /// Creates a new `PluginBuilder` with the default `WasiCtx` (see [`default_ctx`]). /// This plugin will yield after a configurable amount of fuel is consumed. pub fn new_default() -> Result { let default_ctx = WasiCtxBuilder::new() diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 2c2bed87173e2d3e7fff124815e4f89f852a103b..5536b4d84d086cc55b60e936123108ca97a8921e 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -1532,7 +1532,7 @@ impl LspCommand for GetCompletions { .iter() .map(language::proto::serialize_completion) .collect(), - version: serialize_version(&buffer_version), + version: serialize_version(buffer_version), } } @@ -1672,7 +1672,7 @@ impl LspCommand for GetCodeActions { .iter() .map(language::proto::serialize_code_action) .collect(), - version: serialize_version(&buffer_version), + version: serialize_version(buffer_version), } } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index c5dc88d4479ef1627a7da56344c16695077756b3..de69a93ba6fd90047cd312f220ec4fb7545aa89e 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -34,16 +34,16 @@ use gpui::{ use itertools::Itertools; use language::{ language_settings::{language_settings, FormatOnSave, Formatter, InlayHintKind}, - point_to_lsp, + markdown, point_to_lsp, proto::{ deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version, serialize_anchor, serialize_version, split_operations, }, range_from_lsp, range_to_lsp, Bias, Buffer, BufferSnapshot, CachedLspAdapter, Capability, CodeAction, CodeLabel, Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, - Event as BufferEvent, File as _, Language, LanguageRegistry, LanguageServerName, LocalFile, - LspAdapterDelegate, OffsetRangeExt, Operation, Patch, PendingLanguageServer, PointUtf16, - TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, Unclipped, + Documentation, Event as BufferEvent, File as _, Language, LanguageRegistry, LanguageServerName, + LocalFile, LspAdapterDelegate, OffsetRangeExt, Operation, Patch, PendingLanguageServer, + PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, Unclipped, }; use log::error; use lsp::{ @@ -52,7 +52,7 @@ use lsp::{ }; use lsp_command::*; use node_runtime::NodeRuntime; -use parking_lot::Mutex; +use parking_lot::{Mutex, RwLock}; use postage::watch; use prettier_support::{DefaultPrettier, PrettierInstance}; use project_settings::{LspSettings, ProjectSettings}; @@ -2947,7 +2947,7 @@ impl Project { }; task.await; - this.update(&mut cx, |this, mut cx| { + this.update(&mut cx, |this, cx| { let worktrees = this.worktrees.clone(); for worktree in worktrees { let worktree = match worktree.upgrade() { @@ -2962,7 +2962,7 @@ impl Project { root_path, adapter.clone(), language.clone(), - &mut cx, + cx, ); } }) @@ -3544,8 +3544,8 @@ impl Project { if errored { log::warn!("test binary check failed"); let task = this - .update(&mut cx, move |this, mut cx| { - this.reinstall_language_server(language, adapter, server_id, &mut cx) + .update(&mut cx, move |this, cx| { + this.reinstall_language_server(language, adapter, server_id, cx) }) .ok() .flatten(); @@ -3768,8 +3768,7 @@ impl Project { } }; if let Some(relative_glob_pattern) = relative_glob_pattern { - let literal_prefix = - glob_literal_prefix(&relative_glob_pattern); + let literal_prefix = glob_literal_prefix(relative_glob_pattern); tree.as_local_mut() .unwrap() .add_path_prefix_to_scan(Path::new(literal_prefix).into()); @@ -3910,7 +3909,6 @@ impl Project { message: diagnostic.message.clone(), group_id, is_primary: true, - is_valid: true, is_disk_based, is_unnecessary, }, @@ -3928,7 +3926,6 @@ impl Project { message: info.message.clone(), group_id, is_primary: false, - is_valid: true, is_disk_based, is_unnecessary: false, }, @@ -4233,9 +4230,9 @@ impl Project { format_operation = Some(FormatOperation::Lsp( Self::format_via_lsp( &project, - &buffer, + buffer, buffer_abs_path, - &language_server, + language_server, tab_size, &mut cx, ) @@ -4254,8 +4251,8 @@ impl Project { format_operation = Self::format_via_external_command( buffer, buffer_abs_path, - &command, - &arguments, + command, + arguments, &mut cx, ) .await @@ -4278,9 +4275,9 @@ impl Project { format_operation = Some(FormatOperation::Lsp( Self::format_via_lsp( &project, - &buffer, + buffer, buffer_abs_path, - &language_server, + language_server, tab_size, &mut cx, ) @@ -4830,6 +4827,170 @@ impl Project { } } + pub fn resolve_completions( + &self, + completion_indices: Vec, + completions: Arc>>, + cx: &mut ModelContext, + ) -> Task> { + let client = self.client(); + let language_registry = self.languages().clone(); + + let is_remote = self.is_remote(); + let project_id = self.remote_id(); + + cx.spawn(move |this, mut cx| async move { + let mut did_resolve = false; + if is_remote { + let project_id = + project_id.ok_or_else(|| anyhow!("Remote project without remote_id"))?; + + for completion_index in completion_indices { + let completions_guard = completions.read(); + let completion = &completions_guard[completion_index]; + if completion.documentation.is_some() { + continue; + } + + did_resolve = true; + let server_id = completion.server_id; + let completion = completion.lsp_completion.clone(); + drop(completions_guard); + + Self::resolve_completion_documentation_remote( + project_id, + server_id, + completions.clone(), + completion_index, + completion, + client.clone(), + language_registry.clone(), + ) + .await; + } + } else { + for completion_index in completion_indices { + let completions_guard = completions.read(); + let completion = &completions_guard[completion_index]; + if completion.documentation.is_some() { + continue; + } + + let server_id = completion.server_id; + let completion = completion.lsp_completion.clone(); + drop(completions_guard); + + let server = this + .read_with(&mut cx, |project, _| { + project.language_server_for_id(server_id) + }) + .ok() + .flatten(); + let Some(server) = server else { + continue; + }; + + did_resolve = true; + Self::resolve_completion_documentation_local( + server, + completions.clone(), + completion_index, + completion, + language_registry.clone(), + ) + .await; + } + } + + Ok(did_resolve) + }) + } + + async fn resolve_completion_documentation_local( + server: Arc, + completions: Arc>>, + completion_index: usize, + completion: lsp::CompletionItem, + language_registry: Arc, + ) { + let can_resolve = server + .capabilities() + .completion_provider + .as_ref() + .and_then(|options| options.resolve_provider) + .unwrap_or(false); + if !can_resolve { + return; + } + + let request = server.request::(completion); + let Some(completion_item) = request.await.log_err() else { + return; + }; + + if let Some(lsp_documentation) = completion_item.documentation { + let documentation = language::prepare_completion_documentation( + &lsp_documentation, + &language_registry, + None, // TODO: Try to reasonably work out which language the completion is for + ) + .await; + + let mut completions = completions.write(); + let completion = &mut completions[completion_index]; + completion.documentation = Some(documentation); + } else { + let mut completions = completions.write(); + let completion = &mut completions[completion_index]; + completion.documentation = Some(Documentation::Undocumented); + } + } + + async fn resolve_completion_documentation_remote( + project_id: u64, + server_id: LanguageServerId, + completions: Arc>>, + completion_index: usize, + completion: lsp::CompletionItem, + client: Arc, + language_registry: Arc, + ) { + let request = proto::ResolveCompletionDocumentation { + project_id, + language_server_id: server_id.0 as u64, + lsp_completion: serde_json::to_string(&completion).unwrap().into_bytes(), + }; + + let Some(response) = client + .request(request) + .await + .context("completion documentation resolve proto request") + .log_err() + else { + return; + }; + + if response.text.is_empty() { + let mut completions = completions.write(); + let completion = &mut completions[completion_index]; + completion.documentation = Some(Documentation::Undocumented); + } + + let documentation = if response.is_markdown { + Documentation::MultiLineMarkdown( + markdown::parse_markdown(&response.text, &language_registry, None).await, + ) + } else if response.text.lines().count() <= 1 { + Documentation::SingleLine(response.text) + } else { + Documentation::MultiLinePlainText(response.text) + }; + + let mut completions = completions.write(); + let completion = &mut completions[completion_index]; + completion.documentation = Some(documentation); + } + pub fn apply_additional_edits_for_completion( &self, buffer_handle: Model, @@ -5685,7 +5846,7 @@ impl Project { snapshot.file().map(|file| file.path().as_ref()), ) { query - .search(&snapshot, None) + .search(snapshot, None) .await .iter() .map(|range| { @@ -6528,7 +6689,7 @@ impl Project { snapshot.repository_and_work_directory_for_path(&path)?; let repo = snapshot.get_local_repo(&repo)?; let relative_path = path.strip_prefix(&work_directory).ok()?; - let base_text = repo.repo_ptr.lock().load_index_text(&relative_path); + let base_text = repo.repo_ptr.lock().load_index_text(relative_path); Some((buffer, base_text)) }) .collect::>() @@ -6661,7 +6822,7 @@ impl Project { for (_, _, path_summary) in self.diagnostic_summaries(include_ignored, cx) .filter(|(path, _, _)| { - let worktree = self.entry_for_path(&path, cx).map(|entry| entry.is_ignored); + let worktree = self.entry_for_path(path, cx).map(|entry| entry.is_ignored); include_ignored || worktree == Some(false) }) { @@ -6687,7 +6848,7 @@ impl Project { }) }) .filter(move |(path, _, _)| { - let worktree = self.entry_for_path(&path, cx).map(|entry| entry.is_ignored); + let worktree = self.entry_for_path(path, cx).map(|entry| entry.is_ignored); include_ignored || worktree == Some(false) }) } diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index 3184a428c955a6951a5ba84afab84fec8a417196..411a943aa489ff75f1e5114136b91111594bebb9 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -61,7 +61,7 @@ impl Project { if let Some(python_settings) = &python_settings.as_option() { let activate_script_path = - self.find_activate_script_path(&python_settings, working_directory); + self.find_activate_script_path(python_settings, working_directory); self.activate_python_virtual_environment( activate_script_path, &terminal_handle, diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 461ea303b3d42cedb577d53f22c9e1de75a14b6a..4bd5e629946be4b0ce88a838c99a1ec8952b8660 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -1437,7 +1437,7 @@ impl LocalWorktree { if let Err(e) = self.client.send(proto::UpdateDiagnosticSummary { project_id, worktree_id: cx.entity_id().as_u64(), - summary: Some(summary.to_proto(server_id, &path)), + summary: Some(summary.to_proto(server_id, path)), }) { return Task::ready(Err(e)); } @@ -1866,7 +1866,7 @@ impl Snapshot { }) } - /// Update the `git_status` of the given entries such that files' + /// Updates the `git_status` of the given entries such that files' /// statuses bubble up to their ancestor directories. pub fn propagate_git_statuses(&self, result: &mut [Entry]) { let mut cursor = self @@ -2309,7 +2309,7 @@ impl LocalSnapshot { impl BackgroundScannerState { fn should_scan_directory(&self, entry: &Entry) -> bool { (!entry.is_external && !entry.is_ignored) - || entry.path.file_name() == Some(&*DOT_GIT) + || entry.path.file_name() == Some(*DOT_GIT) || self.scanned_dirs.contains(&entry.id) // If we've ever scanned it, keep scanning || self .paths_to_scan @@ -3374,7 +3374,7 @@ impl BackgroundScanner { let mut is_git_related = false; if let Some(dot_git_dir) = abs_path .ancestors() - .find(|ancestor| ancestor.file_name() == Some(&*DOT_GIT)) + .find(|ancestor| ancestor.file_name() == Some(*DOT_GIT)) { let dot_git_path = dot_git_dir .strip_prefix(&root_canonical_path) @@ -3772,7 +3772,7 @@ impl BackgroundScanner { for entry in &mut new_entries { state.reuse_entry_id(entry); if entry.is_dir() { - if state.should_scan_directory(&entry) { + if state.should_scan_directory(entry) { job_ix += 1; } else { log::debug!("defer scanning directory {:?}", entry.path); @@ -3814,9 +3814,9 @@ impl BackgroundScanner { abs_paths .iter() .map(|abs_path| async move { - let metadata = self.fs.metadata(&abs_path).await?; + let metadata = self.fs.metadata(abs_path).await?; if let Some(metadata) = metadata { - let canonical_path = self.fs.canonicalize(&abs_path).await?; + let canonical_path = self.fs.canonicalize(abs_path).await?; anyhow::Ok(Some((metadata, canonical_path))) } else { Ok(None) @@ -3864,7 +3864,7 @@ impl BackgroundScanner { fs_entry.is_external = !canonical_path.starts_with(&root_canonical_path); if !is_dir && !fs_entry.is_ignored { - if let Some((work_dir, repo)) = state.snapshot.local_repo_for_path(&path) { + if let Some((work_dir, repo)) = state.snapshot.local_repo_for_path(path) { if let Ok(repo_path) = path.strip_prefix(work_dir.0) { let repo_path = RepoPath(repo_path.into()); let repo = repo.repo_ptr.lock(); @@ -3884,7 +3884,7 @@ impl BackgroundScanner { state.insert_entry(fs_entry, self.fs.as_ref()); } Ok(None) => { - self.remove_repo_path(&path, &mut state.snapshot); + self.remove_repo_path(path, &mut state.snapshot); } Err(err) => { // TODO - create a special 'error' entry in the entries tree to mark this diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 79c158048ee7e08b79f9adcc1d31fa1eea587e44..abfc2d581883bf865e1cf70cd7721502860668b5 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -381,67 +381,57 @@ impl ProjectPanel { let is_local = project.is_local(); let is_read_only = project.is_read_only(); - let context_menu = ContextMenu::build(cx, |mut menu, cx| { - if is_read_only { - menu = menu.action("Copy Relative Path", Box::new(CopyRelativePath)); - if is_dir { - menu = menu.action("Search Inside", Box::new(NewSearchInDirectory)) - } - - return menu; - } - - if is_local { - menu = menu.action( - "Add Folder to Project", - Box::new(workspace::AddFolderToProject), - ); - if is_root { - menu = menu.entry( - "Remove from Project", - None, - cx.handler_for(&this, move |this, cx| { - this.project.update(cx, |project, cx| { - project.remove_worktree(worktree_id, cx) - }); - }), - ); - } - } - - menu = menu - .action("New File", Box::new(NewFile)) - .action("New Folder", Box::new(NewDirectory)) - .separator() - .action("Cut", Box::new(Cut)) - .action("Copy", Box::new(Copy)); - - if let Some(clipboard_entry) = self.clipboard_entry { - if clipboard_entry.worktree_id() == worktree_id { - menu = menu.action("Paste", Box::new(Paste)); - } - } - - menu = menu - .separator() - .action("Copy Path", Box::new(CopyPath)) - .action("Copy Relative Path", Box::new(CopyRelativePath)) - .separator() - .action("Reveal in Finder", Box::new(RevealInFinder)); - - if is_dir { - menu = menu - .action("Open in Terminal", Box::new(OpenInTerminal)) - .action("Search Inside", Box::new(NewSearchInDirectory)) - } - - menu = menu.separator().action("Rename", Box::new(Rename)); - - if !is_root { - menu = menu.action("Delete", Box::new(Delete)); - } - - menu + let context_menu = ContextMenu::build(cx, |menu, cx| { + menu.context(self.focus_handle.clone()).when_else( + is_read_only, + |menu| { + menu.action("Copy Relative Path", Box::new(CopyRelativePath)) + .when(is_dir, |menu| { + menu.action("Search Inside", Box::new(NewSearchInDirectory)) + }) + }, + |menu| { + menu.when(is_local, |menu| { + menu.action( + "Add Folder to Project", + Box::new(workspace::AddFolderToProject), + ) + .when(is_root, |menu| { + menu.entry( + "Remove from Project", + None, + cx.handler_for(&this, move |this, cx| { + this.project.update(cx, |project, cx| { + project.remove_worktree(worktree_id, cx) + }); + }), + ) + }) + }) + .action("New File", Box::new(NewFile)) + .action("New Folder", Box::new(NewDirectory)) + .separator() + .action("Cut", Box::new(Cut)) + .action("Copy", Box::new(Copy)) + .when_some(self.clipboard_entry, |menu, entry| { + menu.when(entry.worktree_id() == worktree_id, |menu| { + menu.action("Paste", Box::new(Paste)) + }) + }) + .separator() + .action("Copy Path", Box::new(CopyPath)) + .action("Copy Relative Path", Box::new(CopyRelativePath)) + .separator() + .action("Reveal in Finder", Box::new(RevealInFinder)) + .when(is_dir, |menu| { + menu.action("Open in Terminal", Box::new(OpenInTerminal)) + .action("Search Inside", Box::new(NewSearchInDirectory)) + }) + .separator() + .action("Rename", Box::new(Rename)) + .when(!is_root, |menu| menu.action("Delete", Box::new(Delete))) + }, + ) }); cx.focus_view(&context_menu); diff --git a/crates/rich_text/Cargo.toml b/crates/rich_text/Cargo.toml index 609272cdf3de733b47c104d90c016e43c1047459..efec13c7601cb6698e01d71d45621e3e0b7228f5 100644 --- a/crates/rich_text/Cargo.toml +++ b/crates/rich_text/Cargo.toml @@ -21,6 +21,7 @@ sum_tree = { path = "../sum_tree" } theme = { path = "../theme" } language = { path = "../language" } util = { path = "../util" } +ui = { path = "../ui" } anyhow.workspace = true futures.workspace = true lazy_static.workspace = true diff --git a/crates/rich_text/src/rich_text.rs b/crates/rich_text/src/rich_text.rs index 8bc0d73fd4dc80c583ac532896004f97c1365e94..12188c5031a1287b5f4ed4b12c249ed15d4b6f57 100644 --- a/crates/rich_text/src/rich_text.rs +++ b/crates/rich_text/src/rich_text.rs @@ -6,6 +6,7 @@ use gpui::{ use language::{HighlightId, Language, LanguageRegistry}; use std::{ops::Range, sync::Arc}; use theme::ActiveTheme; +use ui::LinkPreview; use util::RangeExt; #[derive(Debug, Clone, PartialEq, Eq)] @@ -64,7 +65,7 @@ impl RichText { }, Highlight::Id(id) => HighlightStyle { background_color: Some(code_background), - ..id.style(&theme.syntax()).unwrap_or_default() + ..id.style(theme.syntax()).unwrap_or_default() }, Highlight::Highlight(highlight) => *highlight, Highlight::Mention => HighlightStyle { @@ -84,6 +85,18 @@ impl RichText { let link_urls = self.link_urls.clone(); move |ix, cx| cx.open_url(&link_urls[ix]) }) + .tooltip({ + let link_ranges = self.link_ranges.clone(); + let link_urls = self.link_urls.clone(); + move |idx, cx| { + for (ix, range) in link_ranges.iter().enumerate() { + if range.contains(&idx) { + return Some(LinkPreview::new(&link_urls[ix], cx)); + } + } + None + } + }) .into_any_element() } } @@ -107,7 +120,7 @@ pub fn render_markdown_mut( let mut list_stack = Vec::new(); let options = Options::all(); - for (event, source_range) in Parser::new_ext(&block, options).into_offset_iter() { + for (event, source_range) in Parser::new_ext(block, options).into_offset_iter() { let prev_len = text.len(); match event { Event::Text(t) => { @@ -237,7 +250,7 @@ pub fn render_markdown_mut( _ => {} }, Event::HardBreak => text.push('\n'), - Event::SoftBreak => text.push(' '), + Event::SoftBreak => text.push('\n'), _ => {} } } diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index dd5d77440edaaae98876f91a4fc5fa85999dd6ed..941053be538b6f50b110e2a069bca6f4e5d0518f 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -1472,7 +1472,10 @@ message Diagnostic { optional string code = 6; uint64 group_id = 7; bool is_primary = 8; + + // TODO: remove this field bool is_valid = 9; + bool is_disk_based = 10; bool is_unnecessary = 11; diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index aa351bc8c2e0572231280de300c32f7c5165edc3..c55ec5a686779768988dfc543c81e65337b2f5f3 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -246,7 +246,7 @@ impl SettingsStore { this } - /// Update the value of a setting in the user's global configuration. + /// Updates the value of a setting in the user's global configuration. /// /// This is only for tests. Normally, settings are only loaded from /// JSON files. @@ -261,7 +261,7 @@ impl SettingsStore { self.set_user_settings(&new_text, cx).unwrap(); } - /// Update the value of a setting in a JSON file, returning the new text + /// Updates the value of a setting in a JSON file, returning the new text /// for that JSON file. pub fn new_text_for_update( &self, @@ -276,7 +276,7 @@ impl SettingsStore { new_text } - /// Update the value of a setting in a JSON file, returning a list + /// Updates the value of a setting in a JSON file, returning a list /// of edits to apply to the JSON file. pub fn edits_for_update( &self, @@ -344,7 +344,7 @@ impl SettingsStore { DEFAULT_JSON_TAB_SIZE } - /// Set the default settings via a JSON string. + /// Sets the default settings via a JSON string. /// /// The string should contain a JSON object with a default value for every setting. pub fn set_default_settings( @@ -362,7 +362,7 @@ impl SettingsStore { } } - /// Set the user settings via a JSON string. + /// Sets the user settings via a JSON string. pub fn set_user_settings( &mut self, user_settings_content: &str, @@ -533,7 +533,7 @@ impl SettingsStore { } if let Some(local_settings) = - setting_value.deserialize_setting(&local_settings).log_err() + setting_value.deserialize_setting(local_settings).log_err() { paths_stack.push(Some((*root_id, path.as_ref()))); user_settings_stack.push(local_settings); @@ -697,8 +697,7 @@ fn update_value_in_json_text<'a>( if let Some(new_object) = new_value.as_object_mut() { new_object.retain(|_, v| !v.is_null()); } - let (range, replacement) = - replace_value_in_json_text(text, &key_path, tab_size, &new_value); + let (range, replacement) = replace_value_in_json_text(text, key_path, tab_size, &new_value); text.replace_range(range.clone(), &replacement); edits.push((range, replacement)); } diff --git a/crates/story/src/story.rs b/crates/story/src/story.rs index f5448831cb168c240d4e8d8c038cd3f02a0bd2dd..ff7b2bed86b7a7a397692a5dfc554c569f7a774b 100644 --- a/crates/story/src/story.rs +++ b/crates/story/src/story.rs @@ -67,8 +67,8 @@ impl StoryContainer { } impl ParentElement for StoryContainer { - fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { - &mut self.children + fn extend(&mut self, elements: impl Iterator) { + self.children.extend(elements) } } @@ -104,7 +104,7 @@ impl RenderOnce for StoryContainer { .h_px() .flex_1() .id("story_body") - .overflow_hidden_x() + .overflow_x_hidden() .overflow_y_scroll() .flex() .flex_col() @@ -372,7 +372,7 @@ impl RenderOnce for StorySection { } impl ParentElement for StorySection { - fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { - &mut self.children + fn extend(&mut self, elements: impl Iterator) { + self.children.extend(elements) } } diff --git a/crates/storybook/src/stories/text.rs b/crates/storybook/src/stories/text.rs index b7445ef95aac857a258c903a64ccde0c25497461..6cf6d4609abc97166d12dd0b97f136a5dfcbf649 100644 --- a/crates/storybook/src/stories/text.rs +++ b/crates/storybook/src/stories/text.rs @@ -110,66 +110,3 @@ impl Render for TextStory { ).into_element() } } - -// TODO: Check all were updated to new style and remove - -// impl Render for TextStory { -// type Element = Div; - -// fn render(&mut self, cx: &mut gpui::ViewContext) -> Self::Element { -// v_flex() -// .bg(blue()) -// .child( -// div() -// .flex() -// .child(div().max_w_96().bg(white()).child(concat!( -// "max-width: 96. The quick brown fox jumps over the lazy dog. ", -// "Meanwhile, the lazy dog decided it was time for a change. ", -// "He started daily workout routines, ate healthier and became the fastest dog in town.", -// ))), -// ) -// .child(div().h_5()) -// .child(div().flex().flex_col().w_96().bg(white()).child(concat!( -// "flex-col. width: 96; The quick brown fox jumps over the lazy dog. ", -// "Meanwhile, the lazy dog decided it was time for a change. ", -// "He started daily workout routines, ate healthier and became the fastest dog in town.", -// ))) -// .child(div().h_5()) -// .child( -// div() -// .flex() -// .child(div().min_w_96().bg(white()).child(concat!( -// "min-width: 96. The quick brown fox jumps over the lazy dog. ", -// "Meanwhile, the lazy dog decided it was time for a change. ", -// "He started daily workout routines, ate healthier and became the fastest dog in town.", -// )))) -// .child(div().h_5()) -// .child(div().flex().w_96().bg(white()).child(div().overflow_hidden().child(concat!( -// "flex-row. width 96. overflow-hidden. The quick brown fox jumps over the lazy dog. ", -// "Meanwhile, the lazy dog decided it was time for a change. ", -// "He started daily workout routines, ate healthier and became the fastest dog in town.", -// )))) -// // NOTE: When rendering text in a horizontal flex container, -// // Taffy will not pass width constraints down from the parent. -// // To fix this, render text in a parent with overflow: hidden -// .child(div().h_5()) -// .child(div().flex().w_96().bg(red()).child(concat!( -// "flex-row. width 96. The quick brown fox jumps over the lazy dog. ", -// "Meanwhile, the lazy dog decided it was time for a change. ", -// "He started daily workout routines, ate healthier and became the fastest dog in town.", -// ))).child( -// InteractiveText::new( -// "interactive", -// StyledText::new("Hello world, how is it going?").with_highlights(&cx.text_style(), [ -// (6..11, HighlightStyle { -// background_color: Some(green()), -// ..Default::default() -// }), -// ]), -// ) -// .on_click(vec![2..4, 1..3, 7..9], |range_ix, _cx| { -// println!("Clicked range {range_ix}"); -// }) -// ) -// } -// } diff --git a/crates/storybook/src/stories/z_index.rs b/crates/storybook/src/stories/z_index.rs index b6e49bfae32b046242e96a5de34d30c3be806b94..63ee1af7591ee8a62f07f052b320db8a70077b9d 100644 --- a/crates/storybook/src/stories/z_index.rs +++ b/crates/storybook/src/stories/z_index.rs @@ -76,7 +76,7 @@ impl Styles for Div {} #[derive(IntoElement)] struct ZIndexExample { - z_index: u8, + z_index: u16, } impl RenderOnce for ZIndexExample { @@ -166,7 +166,7 @@ impl RenderOnce for ZIndexExample { } impl ZIndexExample { - pub fn new(z_index: u8) -> Self { + pub fn new(z_index: u16) -> Self { Self { z_index } } } diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 0b87ed1d976daf247b7b84efe3280290602911f3..ee45bc8f7ce85c04526e9ae55d08059d27dd67ce 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -52,9 +52,9 @@ use std::{ use thiserror::Error; use gpui::{ - actions, black, px, red, AnyWindowHandle, AppContext, Bounds, ClipboardItem, EventEmitter, - Hsla, Keystroke, ModelContext, Modifiers, MouseButton, MouseDownEvent, MouseMoveEvent, - MouseUpEvent, Pixels, Point, Rgba, ScrollWheelEvent, Size, Task, TouchPhase, + actions, black, px, AnyWindowHandle, AppContext, Bounds, ClipboardItem, EventEmitter, Hsla, + Keystroke, ModelContext, Modifiers, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, + Pixels, Point, Rgba, ScrollWheelEvent, Size, Task, TouchPhase, }; use crate::mappings::{colors::to_alac_rgb, keys::to_esc_str}; @@ -599,7 +599,11 @@ impl Terminal { } } - /// Update the cached process info, returns whether the Zed-relevant info has changed + pub fn selection_started(&self) -> bool { + self.selection_phase == SelectionPhase::Selecting + } + + /// Updates the cached process info, returns whether the Zed-relevant info has changed fn update_process_info(&mut self) -> bool { let mut pid = unsafe { libc::tcgetpgrp(self.shell_fd as i32) }; if pid < 0 { @@ -1206,7 +1210,7 @@ impl Terminal { pub fn scroll_wheel(&mut self, e: &ScrollWheelEvent, origin: Point) { let mouse_mode = self.mouse_mode(e.shift); - if let Some(scroll_lines) = self.determine_scroll_lines(&e, mouse_mode) { + if let Some(scroll_lines) = self.determine_scroll_lines(e, mouse_mode) { if mouse_mode { let point = grid_point( e.position - origin, @@ -1215,7 +1219,7 @@ impl Terminal { ); if let Some(scrolls) = - scroll_report(point, scroll_lines as i32, &e, self.last_content.mode) + scroll_report(point, scroll_lines as i32, e, self.last_content.mode) { for scroll in scrolls { self.pty_tx.notify(scroll); @@ -1295,7 +1299,7 @@ impl Terminal { "{}{}", fpi.name, if fpi.argv.len() >= 1 { - format!(" {}", (&fpi.argv[1..]).join(" ")) + format!(" {}", (fpi.argv[1..]).join(" ")) } else { "".to_string() } @@ -1380,7 +1384,7 @@ pub fn get_color_at_index(index: usize, theme: &Theme) -> Hsla { let colors = theme.colors(); match index { - //0-15 are the same as the named colors above + // 0-15 are the same as the named colors above 0 => colors.terminal_ansi_black, 1 => colors.terminal_ansi_red, 2 => colors.terminal_ansi_green, @@ -1397,34 +1401,32 @@ pub fn get_color_at_index(index: usize, theme: &Theme) -> Hsla { 13 => colors.terminal_ansi_bright_magenta, 14 => colors.terminal_ansi_bright_cyan, 15 => colors.terminal_ansi_bright_white, - //16-231 are mapped to their RGB colors on a 0-5 range per channel + // 16-231 are mapped to their RGB colors on a 0-5 range per channel 16..=231 => { - let (r, g, b) = rgb_for_index(&(index as u8)); //Split the index into it's ANSI-RGB components - let step = (u8::MAX as f32 / 5.).floor() as u8; //Split the RGB range into 5 chunks, with floor so no overflow - rgba_color(r * step, g * step, b * step) //Map the ANSI-RGB components to an RGB color + let (r, g, b) = rgb_for_index(&(index as u8)); // Split the index into it's ANSI-RGB components + let step = (u8::MAX as f32 / 5.).floor() as u8; // Split the RGB range into 5 chunks, with floor so no overflow + rgba_color(r * step, g * step, b * step) // Map the ANSI-RGB components to an RGB color } - //232-255 are a 24 step grayscale from black to white + // 232-255 are a 24 step grayscale from black to white 232..=255 => { - let i = index as u8 - 232; //Align index to 0..24 - let step = (u8::MAX as f32 / 24.).floor() as u8; //Split the RGB grayscale values into 24 chunks - rgba_color(i * step, i * step, i * step) //Map the ANSI-grayscale components to the RGB-grayscale + let i = index as u8 - 232; // Align index to 0..24 + let step = (u8::MAX as f32 / 24.).floor() as u8; // Split the RGB grayscale values into 24 chunks + rgba_color(i * step, i * step, i * step) // Map the ANSI-grayscale components to the RGB-grayscale } - //For compatibility with the alacritty::Colors interface + // For compatibility with the alacritty::Colors interface 256 => colors.text, 257 => colors.background, 258 => theme.players().local().cursor, - - // todo!(more colors) - 259 => red(), //style.dim_black, - 260 => red(), //style.dim_red, - 261 => red(), //style.dim_green, - 262 => red(), //style.dim_yellow, - 263 => red(), //style.dim_blue, - 264 => red(), //style.dim_magenta, - 265 => red(), //style.dim_cyan, - 266 => red(), //style.dim_white, - 267 => red(), //style.bright_foreground, - 268 => colors.terminal_ansi_black, //'Dim Background', non-standard color + 259 => colors.terminal_ansi_dim_black, + 260 => colors.terminal_ansi_dim_red, + 261 => colors.terminal_ansi_dim_green, + 262 => colors.terminal_ansi_dim_yellow, + 263 => colors.terminal_ansi_dim_blue, + 264 => colors.terminal_ansi_dim_magenta, + 265 => colors.terminal_ansi_dim_cyan, + 266 => colors.terminal_ansi_dim_white, + 267 => colors.terminal_bright_foreground, + 268 => colors.terminal_ansi_black, // 'Dim Background', non-standard color _ => black(), } diff --git a/crates/terminal/src/terminal_settings.rs b/crates/terminal/src/terminal_settings.rs index b9b79c9c6b6996e674f1af25485343a093b3fe85..7fcbcf954180fd32eb674339a439f5f433087585 100644 --- a/crates/terminal/src/terminal_settings.rs +++ b/crates/terminal/src/terminal_settings.rs @@ -90,17 +90,17 @@ pub struct TerminalSettingsContent { /// /// Default: current_project_directory pub working_directory: Option, - /// Set the terminal's font size. + /// Sets the terminal's font size. /// /// If this option is not included, /// the terminal will default to matching the buffer's font size. pub font_size: Option, - /// Set the terminal's font family. + /// Sets the terminal's font family. /// /// If this option is not included, /// the terminal will default to matching the buffer's font family. pub font_family: Option, - /// Set the terminal's line height. + /// Sets the terminal's line height. /// /// Default: comfortable pub line_height: Option, @@ -110,18 +110,18 @@ pub struct TerminalSettingsContent { /// /// Default: {} pub env: Option>, - /// Set the cursor blinking behavior in the terminal. + /// Sets the cursor blinking behavior in the terminal. /// /// Default: terminal_controlled pub blinking: Option, - /// Set whether Alternate Scroll mode (code: ?1007) is active by default. + /// Sets whether Alternate Scroll mode (code: ?1007) is active by default. /// Alternate Scroll mode converts mouse scroll events into up / down key /// presses when in the alternate screen (e.g. when running applications /// like vim or less). The terminal can still set and unset this mode. /// /// Default: off pub alternate_scroll: Option, - /// Set whether the option key behaves as the meta key. + /// Sets whether the option key behaves as the meta key. /// /// Default: false pub option_as_meta: Option, @@ -139,7 +139,7 @@ pub struct TerminalSettingsContent { /// /// Default: 320 pub default_height: Option, - /// Activate the python virtual environment, if one is found, in the + /// Activates the python virtual environment, if one is found, in the /// terminal's working directory (as resolved by the working_directory /// setting). Set this to "off" to disable this behavior. /// diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index 746a3716b82adbc7cbd0addba7c4d1d024eabc6c..c67fbfc4d0fa4d26e671d24eb6d7b4a2f76a6344 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -1,12 +1,11 @@ use editor::{Cursor, HighlightedRange, HighlightedRangeLine}; use gpui::{ - div, fill, point, px, red, relative, AnyElement, AsyncWindowContext, AvailableSpace, - BorrowWindow, Bounds, DispatchPhase, Element, ElementId, FocusHandle, Font, FontStyle, - FontWeight, HighlightStyle, Hsla, InteractiveBounds, InteractiveElement, - InteractiveElementState, Interactivity, IntoElement, LayoutId, Model, ModelContext, - ModifiersChangedEvent, MouseButton, MouseMoveEvent, Pixels, PlatformInputHandler, Point, - ShapedLine, StatefulInteractiveElement, Styled, TextRun, TextStyle, TextSystem, UnderlineStyle, - WeakView, WhiteSpace, WindowContext, + div, fill, point, px, relative, AnyElement, AvailableSpace, Bounds, DispatchPhase, Element, + ElementContext, ElementId, FocusHandle, Font, FontStyle, FontWeight, HighlightStyle, Hsla, + InputHandler, InteractiveBounds, InteractiveElement, InteractiveElementState, Interactivity, + IntoElement, LayoutId, Model, ModelContext, ModifiersChangedEvent, MouseButton, MouseMoveEvent, + Pixels, Point, ShapedLine, StatefulInteractiveElement, Styled, TextRun, TextStyle, TextSystem, + UnderlineStyle, WeakView, WhiteSpace, WindowContext, }; use itertools::Itertools; use language::CursorShape; @@ -29,7 +28,7 @@ use workspace::Workspace; use std::mem; use std::{fmt::Debug, ops::RangeInclusive}; -///The information generated during layout that is necessary for painting +/// The information generated during layout that is necessary for painting. pub struct LayoutState { cells: Vec, rects: Vec, @@ -43,7 +42,7 @@ pub struct LayoutState { gutter: Pixels, } -///Helper struct for converting data between alacritty's cursor points, and displayed cursor points +/// Helper struct for converting data between Alacritty's cursor points, and displayed cursor points. struct DisplayCursor { line: i32, col: usize, @@ -82,7 +81,7 @@ impl LayoutCell { origin: Point, layout: &LayoutState, _visible_bounds: Bounds, - cx: &mut WindowContext, + cx: &mut ElementContext, ) { let pos = { let point = self.point; @@ -121,7 +120,7 @@ impl LayoutRect { } } - fn paint(&self, origin: Point, layout: &LayoutState, cx: &mut WindowContext) { + fn paint(&self, origin: Point, layout: &LayoutState, cx: &mut ElementContext) { let position = { let alac_point = self.point; point( @@ -139,8 +138,8 @@ impl LayoutRect { } } -///The GPUI element that paints the terminal. -///We need to keep a reference to the view for mouse events, do we need it for any other terminal stuff, or can we move that to connection? +/// The GPUI element that paints the terminal. +/// We need to keep a reference to the view for mouse events, do we need it for any other terminal stuff, or can we move that to connection? pub struct TerminalElement { terminal: Model, workspace: WeakView, @@ -278,8 +277,8 @@ impl TerminalElement { (cells, rects) } - // Compute the cursor position and expected block width, may return a zero width if x_for_index returns - // the same position for sequential indexes. Use em_width instead + /// Computes the cursor position and expected block width, may return a zero width if x_for_index returns + /// the same position for sequential indexes. Use em_width instead fn shape_cursor( cursor_point: DisplayCursor, size: TerminalSize, @@ -306,7 +305,7 @@ impl TerminalElement { } } - /// Convert the Alacritty cell styles to GPUI text styles and background color + /// Converts the Alacritty cell styles to GPUI text styles and background color. fn cell_style( indexed: &IndexedCell, fg: terminal::alacritty_terminal::ansi::Color, @@ -366,7 +365,7 @@ impl TerminalElement { result } - fn compute_layout(&self, bounds: Bounds, cx: &mut WindowContext) -> LayoutState { + fn compute_layout(&self, bounds: Bounds, cx: &mut ElementContext) -> LayoutState { let settings = ThemeSettings::get_global(cx).clone(); let buffer_font_size = settings.buffer_font_size(cx); @@ -506,8 +505,8 @@ impl TerminalElement { cx, ); - //Layout cursor. Rectangle is used for IME, so we should lay it out even - //if we don't end up showing it. + // Layout cursor. Rectangle is used for IME, so we should lay it out even + // if we don't end up showing it. let cursor = if let AlacCursorShape::Hidden = cursor.shape { None } else { @@ -550,12 +549,12 @@ impl TerminalElement { theme.players().local().cursor, shape, text, + None, ) }, ) }; - //Done! LayoutState { cells, cursor, @@ -591,7 +590,7 @@ impl TerminalElement { origin: Point, mode: TermMode, bounds: Bounds, - cx: &mut WindowContext, + cx: &mut ElementContext, ) { let focus = self.focus.clone(); let terminal = self.terminal.clone(); @@ -622,9 +621,17 @@ impl TerminalElement { } if e.pressed_button.is_some() && !cx.has_active_drag() { + let visibly_contains = interactive_bounds.visibly_contains(&e.position, cx); terminal.update(cx, |terminal, cx| { - terminal.mouse_drag(e, origin, bounds); - cx.notify(); + if !terminal.selection_started() { + if visibly_contains { + terminal.mouse_drag(e, origin, bounds); + cx.notify(); + } + } else { + terminal.mouse_drag(e, origin, bounds); + cx.notify(); + } }) } @@ -715,7 +722,7 @@ impl Element for TerminalElement { fn request_layout( &mut self, element_state: Option, - cx: &mut WindowContext<'_>, + cx: &mut ElementContext<'_>, ) -> (LayoutId, Self::State) { let (layout_id, interactive_state) = self.interactivity @@ -734,7 +741,7 @@ impl Element for TerminalElement { &mut self, bounds: Bounds, state: &mut Self::State, - cx: &mut WindowContext<'_>, + cx: &mut ElementContext<'_>, ) { let mut layout = self.compute_layout(bounds, cx); @@ -742,7 +749,6 @@ impl Element for TerminalElement { let origin = bounds.origin + Point::new(layout.gutter, px(0.)); let terminal_input_handler = TerminalInputHandler { - cx: cx.to_async(), terminal: self.terminal.clone(), cursor_bounds: layout .cursor @@ -831,37 +837,35 @@ impl IntoElement for TerminalElement { } struct TerminalInputHandler { - cx: AsyncWindowContext, terminal: Model, workspace: WeakView, cursor_bounds: Option>, } -impl PlatformInputHandler for TerminalInputHandler { - fn selected_text_range(&mut self) -> Option> { - self.cx - .update(|_, cx| { - if self - .terminal - .read(cx) - .last_content - .mode - .contains(TermMode::ALT_SCREEN) - { - None - } else { - Some(0..0) - } - }) - .ok() - .flatten() +impl InputHandler for TerminalInputHandler { + fn selected_text_range(&mut self, cx: &mut WindowContext) -> Option> { + if self + .terminal + .read(cx) + .last_content + .mode + .contains(TermMode::ALT_SCREEN) + { + None + } else { + Some(0..0) + } } - fn marked_text_range(&mut self) -> Option> { + fn marked_text_range(&mut self, _: &mut WindowContext) -> Option> { None } - fn text_for_range(&mut self, _: std::ops::Range) -> Option { + fn text_for_range( + &mut self, + _: std::ops::Range, + _: &mut WindowContext, + ) -> Option { None } @@ -869,19 +873,16 @@ impl PlatformInputHandler for TerminalInputHandler { &mut self, _replacement_range: Option>, text: &str, + cx: &mut WindowContext, ) { - self.cx - .update(|_, cx| { - self.terminal.update(cx, |terminal, _| { - terminal.input(text.into()); - }); + self.terminal.update(cx, |terminal, _| { + terminal.input(text.into()); + }); - self.workspace - .update(cx, |this, cx| { - let telemetry = this.project().read(cx).client().telemetry().clone(); - telemetry.log_edit_event("terminal"); - }) - .ok(); + self.workspace + .update(cx, |this, cx| { + let telemetry = this.project().read(cx).client().telemetry().clone(); + telemetry.log_edit_event("terminal"); }) .ok(); } @@ -891,12 +892,17 @@ impl PlatformInputHandler for TerminalInputHandler { _range_utf16: Option>, _new_text: &str, _new_selected_range: Option>, + _: &mut WindowContext, ) { } - fn unmark_text(&mut self) {} + fn unmark_text(&mut self, _: &mut WindowContext) {} - fn bounds_for_range(&mut self, _range_utf16: std::ops::Range) -> Option> { + fn bounds_for_range( + &mut self, + _range_utf16: std::ops::Range, + _: &mut WindowContext, + ) -> Option> { self.cursor_bounds } } @@ -990,11 +996,11 @@ fn to_highlighted_range_lines( Some((start_y, highlighted_range_lines)) } -///Converts a 2, 8, or 24 bit color ANSI color to the GPUI equivalent +/// Converts a 2, 8, or 24 bit color ANSI color to the GPUI equivalent. fn convert_color(fg: &terminal::alacritty_terminal::ansi::Color, theme: &Theme) -> Hsla { let colors = theme.colors(); match fg { - //Named and theme defined colors + // Named and theme defined colors terminal::alacritty_terminal::ansi::Color::Named(n) => match n { NamedColor::Black => colors.terminal_ansi_black, NamedColor::Red => colors.terminal_ansi_red, @@ -1015,24 +1021,22 @@ fn convert_color(fg: &terminal::alacritty_terminal::ansi::Color, theme: &Theme) NamedColor::Foreground => colors.text, NamedColor::Background => colors.background, NamedColor::Cursor => theme.players().local().cursor, - - // todo!(more colors) - NamedColor::DimBlack => red(), - NamedColor::DimRed => red(), - NamedColor::DimGreen => red(), - NamedColor::DimYellow => red(), - NamedColor::DimBlue => red(), - NamedColor::DimMagenta => red(), - NamedColor::DimCyan => red(), - NamedColor::DimWhite => red(), - NamedColor::BrightForeground => red(), - NamedColor::DimForeground => red(), + NamedColor::DimBlack => colors.terminal_ansi_dim_black, + NamedColor::DimRed => colors.terminal_ansi_dim_red, + NamedColor::DimGreen => colors.terminal_ansi_dim_green, + NamedColor::DimYellow => colors.terminal_ansi_dim_yellow, + NamedColor::DimBlue => colors.terminal_ansi_dim_blue, + NamedColor::DimMagenta => colors.terminal_ansi_dim_magenta, + NamedColor::DimCyan => colors.terminal_ansi_dim_cyan, + NamedColor::DimWhite => colors.terminal_ansi_dim_white, + NamedColor::BrightForeground => colors.terminal_bright_foreground, + NamedColor::DimForeground => colors.terminal_dim_foreground, }, - //'True' colors + // 'True' colors terminal::alacritty_terminal::ansi::Color::Spec(rgb) => { terminal::rgba_color(rgb.r, rgb.g, rgb.b) } - //8 bit, indexed colors + // 8 bit, indexed colors terminal::alacritty_terminal::ansi::Color::Indexed(i) => { terminal::get_color_at_index(*i as usize, theme) } diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index fc5829ba00058d686bd67676e85755fbbb955197..180667b113816f0928a23882fb40222824136bbc 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -772,7 +772,7 @@ impl Item for TerminalView { .log_err() .flatten() .or_else(|| { - cx.update(|_, cx| { + cx.update(|cx| { let strategy = TerminalSettings::get_global(cx).working_directory.clone(); workspace .upgrade() @@ -832,7 +832,7 @@ impl SearchableItem for TerminalView { self.terminal().update(cx, |term, _| term.matches = matches) } - /// Return the selection content to pre-load into this search + /// Returns the selection content to pre-load into this search fn query_suggestion(&mut self, cx: &mut ViewContext) -> String { self.terminal() .read(cx) diff --git a/crates/theme/src/default_colors.rs b/crates/theme/src/default_colors.rs index fb88afb7dacf79d947a170e26136b293d093542b..755ece62f98a28238b4d1da97d755e5effa716f2 100644 --- a/crates/theme/src/default_colors.rs +++ b/crates/theme/src/default_colors.rs @@ -77,6 +77,9 @@ impl ThemeColors { editor_document_highlight_read_background: neutral().light_alpha().step_3(), editor_document_highlight_write_background: neutral().light_alpha().step_4(), terminal_background: neutral().light().step_1(), + terminal_foreground: black().light().step_12(), + terminal_bright_foreground: black().light().step_11(), + terminal_dim_foreground: black().light().step_10(), terminal_ansi_bright_black: black().light().step_11(), terminal_ansi_bright_red: red().light().step_10(), terminal_ansi_bright_green: green().light().step_10(), @@ -93,6 +96,14 @@ impl ThemeColors { terminal_ansi_magenta: violet().light().step_11(), terminal_ansi_cyan: cyan().light().step_11(), terminal_ansi_white: neutral().light().step_12(), + terminal_ansi_dim_black: black().light().step_11(), + terminal_ansi_dim_red: red().light().step_10(), + terminal_ansi_dim_green: green().light().step_10(), + terminal_ansi_dim_yellow: yellow().light().step_10(), + terminal_ansi_dim_blue: blue().light().step_10(), + terminal_ansi_dim_magenta: violet().light().step_10(), + terminal_ansi_dim_cyan: cyan().light().step_10(), + terminal_ansi_dim_white: neutral().light().step_11(), link_text_hover: orange().light().step_10(), } } @@ -160,22 +171,33 @@ impl ThemeColors { editor_document_highlight_read_background: neutral().dark_alpha().step_4(), editor_document_highlight_write_background: neutral().dark_alpha().step_4(), terminal_background: neutral().dark().step_1(), - terminal_ansi_bright_black: black().dark().step_11(), - terminal_ansi_bright_red: red().dark().step_10(), - terminal_ansi_bright_green: green().dark().step_10(), - terminal_ansi_bright_yellow: yellow().dark().step_10(), - terminal_ansi_bright_blue: blue().dark().step_10(), - terminal_ansi_bright_magenta: violet().dark().step_10(), - terminal_ansi_bright_cyan: cyan().dark().step_10(), - terminal_ansi_bright_white: neutral().dark().step_11(), + terminal_foreground: white().dark().step_12(), + terminal_bright_foreground: white().dark().step_11(), + terminal_dim_foreground: white().dark().step_10(), terminal_ansi_black: black().dark().step_12(), + terminal_ansi_bright_black: black().dark().step_11(), + terminal_ansi_dim_black: black().dark().step_10(), terminal_ansi_red: red().dark().step_11(), + terminal_ansi_bright_red: red().dark().step_10(), + terminal_ansi_dim_red: red().dark().step_9(), terminal_ansi_green: green().dark().step_11(), + terminal_ansi_bright_green: green().dark().step_10(), + terminal_ansi_dim_green: green().dark().step_9(), terminal_ansi_yellow: yellow().dark().step_11(), + terminal_ansi_bright_yellow: yellow().dark().step_10(), + terminal_ansi_dim_yellow: yellow().dark().step_9(), terminal_ansi_blue: blue().dark().step_11(), + terminal_ansi_bright_blue: blue().dark().step_10(), + terminal_ansi_dim_blue: blue().dark().step_9(), terminal_ansi_magenta: violet().dark().step_11(), + terminal_ansi_bright_magenta: violet().dark().step_10(), + terminal_ansi_dim_magenta: violet().dark().step_9(), terminal_ansi_cyan: cyan().dark().step_11(), + terminal_ansi_bright_cyan: cyan().dark().step_10(), + terminal_ansi_dim_cyan: cyan().dark().step_9(), terminal_ansi_white: neutral().dark().step_12(), + terminal_ansi_bright_white: neutral().dark().step_11(), + terminal_ansi_dim_white: neutral().dark().step_10(), link_text_hover: orange().dark().step_10(), } } diff --git a/crates/theme/src/one_themes.rs b/crates/theme/src/one_themes.rs index fab3631d13ca890ca3ccb9fa2e06605534602abd..c461779bd1b7fd332537590f7dc0acde8e03338f 100644 --- a/crates/theme/src/one_themes.rs +++ b/crates/theme/src/one_themes.rs @@ -101,6 +101,9 @@ pub(crate) fn one_dark() -> Theme { terminal_background: bg, // todo!("Use one colors for terminal") + terminal_foreground: crate::white().dark().step_12(), + terminal_bright_foreground: crate::white().dark().step_11(), + terminal_dim_foreground: crate::white().dark().step_10(), terminal_ansi_black: crate::black().dark().step_12(), terminal_ansi_red: crate::red().dark().step_11(), terminal_ansi_green: crate::green().dark().step_11(), @@ -117,6 +120,14 @@ pub(crate) fn one_dark() -> Theme { terminal_ansi_bright_magenta: crate::violet().dark().step_10(), terminal_ansi_bright_cyan: crate::cyan().dark().step_10(), terminal_ansi_bright_white: crate::neutral().dark().step_11(), + terminal_ansi_dim_black: crate::black().dark().step_10(), + terminal_ansi_dim_red: crate::red().dark().step_9(), + terminal_ansi_dim_green: crate::green().dark().step_9(), + terminal_ansi_dim_yellow: crate::yellow().dark().step_9(), + terminal_ansi_dim_blue: crate::blue().dark().step_9(), + terminal_ansi_dim_magenta: crate::violet().dark().step_9(), + terminal_ansi_dim_cyan: crate::cyan().dark().step_9(), + terminal_ansi_dim_white: crate::neutral().dark().step_10(), panel_background: bg, panel_focused_border: blue, pane_focused_border: blue, diff --git a/crates/theme/src/styles/colors.rs b/crates/theme/src/styles/colors.rs index 51f3949897c2195ac5620886cf6b58dedebea9f2..a01e73f5ef3e47820c2aa58ae07db842584e3767 100644 --- a/crates/theme/src/styles/colors.rs +++ b/crates/theme/src/styles/colors.rs @@ -169,40 +169,63 @@ pub struct ThemeColors { // === // Terminal // === - /// Terminal Background Color + /// Terminal background color. pub terminal_background: Hsla, - /// Bright Black Color for ANSI Terminal - pub terminal_ansi_bright_black: Hsla, - /// Bright Red Color for ANSI Terminal - pub terminal_ansi_bright_red: Hsla, - /// Bright Green Color for ANSI Terminal - pub terminal_ansi_bright_green: Hsla, - /// Bright Yellow Color for ANSI Terminal - pub terminal_ansi_bright_yellow: Hsla, - /// Bright Blue Color for ANSI Terminal - pub terminal_ansi_bright_blue: Hsla, - /// Bright Magenta Color for ANSI Terminal - pub terminal_ansi_bright_magenta: Hsla, - /// Bright Cyan Color for ANSI Terminal - pub terminal_ansi_bright_cyan: Hsla, - /// Bright White Color for ANSI Terminal - pub terminal_ansi_bright_white: Hsla, - /// Black Color for ANSI Terminal + /// Terminal foreground color. + pub terminal_foreground: Hsla, + /// Bright terminal foreground color. + pub terminal_bright_foreground: Hsla, + /// Dim terminal foreground color. + pub terminal_dim_foreground: Hsla, + + /// Black ANSI terminal color. pub terminal_ansi_black: Hsla, - /// Red Color for ANSI Terminal + /// Bright black ANSI terminal color. + pub terminal_ansi_bright_black: Hsla, + /// Dim black ANSI terminal color. + pub terminal_ansi_dim_black: Hsla, + /// Red ANSI terminal color. pub terminal_ansi_red: Hsla, - /// Green Color for ANSI Terminal + /// Bright red ANSI terminal color. + pub terminal_ansi_bright_red: Hsla, + /// Dim red ANSI terminal color. + pub terminal_ansi_dim_red: Hsla, + /// Green ANSI terminal color. pub terminal_ansi_green: Hsla, - /// Yellow Color for ANSI Terminal + /// Bright green ANSI terminal color. + pub terminal_ansi_bright_green: Hsla, + /// Dim green ANSI terminal color. + pub terminal_ansi_dim_green: Hsla, + /// Yellow ANSI terminal color. pub terminal_ansi_yellow: Hsla, - /// Blue Color for ANSI Terminal + /// Bright yellow ANSI terminal color. + pub terminal_ansi_bright_yellow: Hsla, + /// Dim yellow ANSI terminal color. + pub terminal_ansi_dim_yellow: Hsla, + /// Blue ANSI terminal color. pub terminal_ansi_blue: Hsla, - /// Magenta Color for ANSI Terminal + /// Bright blue ANSI terminal color. + pub terminal_ansi_bright_blue: Hsla, + /// Dim blue ANSI terminal color. + pub terminal_ansi_dim_blue: Hsla, + /// Magenta ANSI terminal color. pub terminal_ansi_magenta: Hsla, - /// Cyan Color for ANSI Terminal + /// Bright magenta ANSI terminal color. + pub terminal_ansi_bright_magenta: Hsla, + /// Dim magenta ANSI terminal color. + pub terminal_ansi_dim_magenta: Hsla, + /// Cyan ANSI terminal color. pub terminal_ansi_cyan: Hsla, - /// White Color for ANSI Terminal + /// Bright cyan ANSI terminal color. + pub terminal_ansi_bright_cyan: Hsla, + /// Dim cyan ANSI terminal color. + pub terminal_ansi_dim_cyan: Hsla, + /// White ANSI terminal color. pub terminal_ansi_white: Hsla, + /// Bright white ANSI terminal color. + pub terminal_ansi_bright_white: Hsla, + /// Dim white ANSI terminal color. + pub terminal_ansi_dim_white: Hsla, // === // UI/Rich Text diff --git a/crates/theme/src/themes/andromeda.rs b/crates/theme/src/themes/andromeda.rs index 45dc66094518c5ab25fe2ad12e616213e4520994..184e27833d58fc5e203afb79fbb6a359a30b9c76 100644 --- a/crates/theme/src/themes/andromeda.rs +++ b/crates/theme/src/themes/andromeda.rs @@ -75,22 +75,33 @@ pub fn andromeda() -> UserThemeFamily { editor_document_highlight_read_background: Some(rgba(0x11a7931a).into()), editor_document_highlight_write_background: Some(rgba(0x64646d66).into()), terminal_background: Some(rgba(0x1e2025ff).into()), - terminal_ansi_bright_black: Some(rgba(0x40434cff).into()), - terminal_ansi_bright_red: Some(rgba(0x8e103aff).into()), - terminal_ansi_bright_green: Some(rgba(0x457c38ff).into()), - terminal_ansi_bright_yellow: Some(rgba(0x958435ff).into()), - terminal_ansi_bright_blue: Some(rgba(0x1b5148ff).into()), - terminal_ansi_bright_magenta: Some(rgba(0x682781ff).into()), - terminal_ansi_bright_cyan: Some(rgba(0x018169ff).into()), - terminal_ansi_bright_white: Some(rgba(0xf7f7f8ff).into()), + terminal_foreground: Some(rgba(0xf7f7f8ff).into()), + terminal_bright_foreground: Some(rgba(0xf7f7f8ff).into()), + terminal_dim_foreground: Some(rgba(0x1e2025ff).into()), terminal_ansi_black: Some(rgba(0x1e2025ff).into()), + terminal_ansi_bright_black: Some(rgba(0x40434cff).into()), + terminal_ansi_dim_black: Some(rgba(0xf7f7f8ff).into()), terminal_ansi_red: Some(rgba(0xf82872ff).into()), + terminal_ansi_bright_red: Some(rgba(0x8e103aff).into()), + terminal_ansi_dim_red: Some(rgba(0xffa3b6ff).into()), terminal_ansi_green: Some(rgba(0x96df72ff).into()), + terminal_ansi_bright_green: Some(rgba(0x457c38ff).into()), + terminal_ansi_dim_green: Some(rgba(0xcef0b9ff).into()), terminal_ansi_yellow: Some(rgba(0xfee56dff).into()), + terminal_ansi_bright_yellow: Some(rgba(0x958435ff).into()), + terminal_ansi_dim_yellow: Some(rgba(0xfff2b8ff).into()), terminal_ansi_blue: Some(rgba(0x11a793ff).into()), + terminal_ansi_bright_blue: Some(rgba(0x1b5148ff).into()), + terminal_ansi_dim_blue: Some(rgba(0x9cd4c8ff).into()), terminal_ansi_magenta: Some(rgba(0xc74decff).into()), + terminal_ansi_bright_magenta: Some(rgba(0x682781ff).into()), + terminal_ansi_dim_magenta: Some(rgba(0xe8abf7ff).into()), terminal_ansi_cyan: Some(rgba(0x09e7c6ff).into()), + terminal_ansi_bright_cyan: Some(rgba(0x018169ff).into()), + terminal_ansi_dim_cyan: Some(rgba(0xaaf5e2ff).into()), terminal_ansi_white: Some(rgba(0xf7f7f8ff).into()), + terminal_ansi_bright_white: Some(rgba(0xf7f7f8ff).into()), + terminal_ansi_dim_white: Some(rgba(0x88868dff).into()), link_text_hover: Some(rgba(0x11a793ff).into()), ..Default::default() }, diff --git a/crates/theme/src/themes/atelier.rs b/crates/theme/src/themes/atelier.rs index e0682b217e0442a53d7c25e6ac8e1339fc55524b..300c16f94c84bdbb49bae0549b74574d98dd1d7d 100644 --- a/crates/theme/src/themes/atelier.rs +++ b/crates/theme/src/themes/atelier.rs @@ -76,22 +76,33 @@ pub fn atelier() -> UserThemeFamily { editor_document_highlight_read_background: Some(rgba(0x576dda1a).into()), editor_document_highlight_write_background: Some(rgba(0x726c7a66).into()), terminal_background: Some(rgba(0x19171cff).into()), - terminal_ansi_bright_black: Some(rgba(0x635d6bff).into()), - terminal_ansi_bright_red: Some(rgba(0x5c283cff).into()), - terminal_ansi_bright_green: Some(rgba(0x1f4747ff).into()), - terminal_ansi_bright_yellow: Some(rgba(0x4e3821ff).into()), - terminal_ansi_bright_blue: Some(rgba(0x2d376fff).into()), - terminal_ansi_bright_magenta: Some(rgba(0x60255bff).into()), - terminal_ansi_bright_cyan: Some(rgba(0x26445eff).into()), - terminal_ansi_bright_white: Some(rgba(0xefecf4ff).into()), + terminal_foreground: Some(rgba(0xefecf4ff).into()), + terminal_bright_foreground: Some(rgba(0xefecf4ff).into()), + terminal_dim_foreground: Some(rgba(0x19171cff).into()), terminal_ansi_black: Some(rgba(0x19171cff).into()), + terminal_ansi_bright_black: Some(rgba(0x635d6bff).into()), + terminal_ansi_dim_black: Some(rgba(0xefecf4ff).into()), terminal_ansi_red: Some(rgba(0xbe4678ff).into()), + terminal_ansi_bright_red: Some(rgba(0x5c283cff).into()), + terminal_ansi_dim_red: Some(rgba(0xe3a4b9ff).into()), terminal_ansi_green: Some(rgba(0x2c9292ff).into()), + terminal_ansi_bright_green: Some(rgba(0x1f4747ff).into()), + terminal_ansi_dim_green: Some(rgba(0x9dc8c8ff).into()), terminal_ansi_yellow: Some(rgba(0xa06e3bff).into()), + terminal_ansi_bright_yellow: Some(rgba(0x4e3821ff).into()), + terminal_ansi_dim_yellow: Some(rgba(0xd4b499ff).into()), terminal_ansi_blue: Some(rgba(0x576ddaff).into()), + terminal_ansi_bright_blue: Some(rgba(0x2d376fff).into()), + terminal_ansi_dim_blue: Some(rgba(0xb3b3eeff).into()), terminal_ansi_magenta: Some(rgba(0xbf41bfff).into()), + terminal_ansi_bright_magenta: Some(rgba(0x60255bff).into()), + terminal_ansi_dim_magenta: Some(rgba(0xe3a4dfff).into()), terminal_ansi_cyan: Some(rgba(0x3a8bc6ff).into()), + terminal_ansi_bright_cyan: Some(rgba(0x26445eff).into()), + terminal_ansi_dim_cyan: Some(rgba(0xa6c4e3ff).into()), terminal_ansi_white: Some(rgba(0xefecf4ff).into()), + terminal_ansi_bright_white: Some(rgba(0xefecf4ff).into()), + terminal_ansi_dim_white: Some(rgba(0x807b89ff).into()), link_text_hover: Some(rgba(0x576ddaff).into()), ..Default::default() }, @@ -541,22 +552,33 @@ pub fn atelier() -> UserThemeFamily { editor_document_highlight_read_background: Some(rgba(0x586dda1a).into()), editor_document_highlight_write_background: Some(rgba(0x726c7a66).into()), terminal_background: Some(rgba(0xefecf4ff).into()), - terminal_ansi_bright_black: Some(rgba(0x807b89ff).into()), - terminal_ansi_bright_red: Some(rgba(0xe3a4b9ff).into()), - terminal_ansi_bright_green: Some(rgba(0x9dc8c8ff).into()), - terminal_ansi_bright_yellow: Some(rgba(0xd4b499ff).into()), - terminal_ansi_bright_blue: Some(rgba(0xb3b3eeff).into()), - terminal_ansi_bright_magenta: Some(rgba(0xe3a4dfff).into()), - terminal_ansi_bright_cyan: Some(rgba(0xa6c4e3ff).into()), - terminal_ansi_bright_white: Some(rgba(0x19171cff).into()), + terminal_foreground: Some(rgba(0x19171cff).into()), + terminal_bright_foreground: Some(rgba(0x19171cff).into()), + terminal_dim_foreground: Some(rgba(0xefecf4ff).into()), terminal_ansi_black: Some(rgba(0xefecf4ff).into()), + terminal_ansi_bright_black: Some(rgba(0x807b89ff).into()), + terminal_ansi_dim_black: Some(rgba(0x19171cff).into()), terminal_ansi_red: Some(rgba(0xbe4778ff).into()), + terminal_ansi_bright_red: Some(rgba(0xe3a4b9ff).into()), + terminal_ansi_dim_red: Some(rgba(0x5c283cff).into()), terminal_ansi_green: Some(rgba(0x2c9292ff).into()), + terminal_ansi_bright_green: Some(rgba(0x9dc8c8ff).into()), + terminal_ansi_dim_green: Some(rgba(0x1f4747ff).into()), terminal_ansi_yellow: Some(rgba(0xa06e3cff).into()), + terminal_ansi_bright_yellow: Some(rgba(0xd4b499ff).into()), + terminal_ansi_dim_yellow: Some(rgba(0x4e3821ff).into()), terminal_ansi_blue: Some(rgba(0x586ddaff).into()), + terminal_ansi_bright_blue: Some(rgba(0xb3b3eeff).into()), + terminal_ansi_dim_blue: Some(rgba(0x2d376fff).into()), terminal_ansi_magenta: Some(rgba(0xbf41bfff).into()), + terminal_ansi_bright_magenta: Some(rgba(0xe3a4dfff).into()), + terminal_ansi_dim_magenta: Some(rgba(0x60255bff).into()), terminal_ansi_cyan: Some(rgba(0x3b8bc6ff).into()), + terminal_ansi_bright_cyan: Some(rgba(0xa6c4e3ff).into()), + terminal_ansi_dim_cyan: Some(rgba(0x26445eff).into()), terminal_ansi_white: Some(rgba(0x19171cff).into()), + terminal_ansi_bright_white: Some(rgba(0x19171cff).into()), + terminal_ansi_dim_white: Some(rgba(0x635d6bff).into()), link_text_hover: Some(rgba(0x586ddaff).into()), ..Default::default() }, @@ -1006,22 +1028,33 @@ pub fn atelier() -> UserThemeFamily { editor_document_highlight_read_background: Some(rgba(0x6684e01a).into()), editor_document_highlight_write_background: Some(rgba(0x8b887466).into()), terminal_background: Some(rgba(0x20201dff).into()), - terminal_ansi_bright_black: Some(rgba(0x7a7766ff).into()), - terminal_ansi_bright_red: Some(rgba(0x781c1fff).into()), - terminal_ansi_bright_green: Some(rgba(0x335322ff).into()), - terminal_ansi_bright_yellow: Some(rgba(0x574815ff).into()), - terminal_ansi_bright_blue: Some(rgba(0x334173ff).into()), - terminal_ansi_bright_magenta: Some(rgba(0x721d2bff).into()), - terminal_ansi_bright_cyan: Some(rgba(0x1e5341ff).into()), - terminal_ansi_bright_white: Some(rgba(0xfefbecff).into()), + terminal_foreground: Some(rgba(0xfefbecff).into()), + terminal_bright_foreground: Some(rgba(0xfefbecff).into()), + terminal_dim_foreground: Some(rgba(0x20201dff).into()), terminal_ansi_black: Some(rgba(0x20201dff).into()), + terminal_ansi_bright_black: Some(rgba(0x7a7766ff).into()), + terminal_ansi_dim_black: Some(rgba(0xfefbecff).into()), terminal_ansi_red: Some(rgba(0xd73837ff).into()), + terminal_ansi_bright_red: Some(rgba(0x781c1fff).into()), + terminal_ansi_dim_red: Some(rgba(0xf7a195ff).into()), terminal_ansi_green: Some(rgba(0x60ac3aff).into()), + terminal_ansi_bright_green: Some(rgba(0x335322ff).into()), + terminal_ansi_dim_green: Some(rgba(0xb3d69cff).into()), terminal_ansi_yellow: Some(rgba(0xae9515ff).into()), + terminal_ansi_bright_yellow: Some(rgba(0x574815ff).into()), + terminal_ansi_dim_yellow: Some(rgba(0xdcc98eff).into()), terminal_ansi_blue: Some(rgba(0x6684e0ff).into()), + terminal_ansi_bright_blue: Some(rgba(0x334173ff).into()), + terminal_ansi_dim_blue: Some(rgba(0xb8c0f1ff).into()), terminal_ansi_magenta: Some(rgba(0xd43652ff).into()), + terminal_ansi_bright_magenta: Some(rgba(0x721d2bff).into()), + terminal_ansi_dim_magenta: Some(rgba(0xf3a0a4ff).into()), terminal_ansi_cyan: Some(rgba(0x21ad83ff).into()), + terminal_ansi_bright_cyan: Some(rgba(0x1e5341ff).into()), + terminal_ansi_dim_cyan: Some(rgba(0x9ed7c0ff).into()), terminal_ansi_white: Some(rgba(0xfefbecff).into()), + terminal_ansi_bright_white: Some(rgba(0xfefbecff).into()), + terminal_ansi_dim_white: Some(rgba(0x9b9782ff).into()), link_text_hover: Some(rgba(0x6684e0ff).into()), ..Default::default() }, @@ -1471,22 +1504,33 @@ pub fn atelier() -> UserThemeFamily { editor_document_highlight_read_background: Some(rgba(0x6784e01a).into()), editor_document_highlight_write_background: Some(rgba(0x8b887466).into()), terminal_background: Some(rgba(0xfefbecff).into()), - terminal_ansi_bright_black: Some(rgba(0x9b9782ff).into()), - terminal_ansi_bright_red: Some(rgba(0xf7a195ff).into()), - terminal_ansi_bright_green: Some(rgba(0xb3d69cff).into()), - terminal_ansi_bright_yellow: Some(rgba(0xdcc98eff).into()), - terminal_ansi_bright_blue: Some(rgba(0xb8c0f1ff).into()), - terminal_ansi_bright_magenta: Some(rgba(0xf3a0a4ff).into()), - terminal_ansi_bright_cyan: Some(rgba(0x9ed7c0ff).into()), - terminal_ansi_bright_white: Some(rgba(0x20201dff).into()), + terminal_foreground: Some(rgba(0x20201dff).into()), + terminal_bright_foreground: Some(rgba(0x20201dff).into()), + terminal_dim_foreground: Some(rgba(0xfefbecff).into()), terminal_ansi_black: Some(rgba(0xfefbecff).into()), + terminal_ansi_bright_black: Some(rgba(0x9b9782ff).into()), + terminal_ansi_dim_black: Some(rgba(0x20201dff).into()), terminal_ansi_red: Some(rgba(0xd73838ff).into()), + terminal_ansi_bright_red: Some(rgba(0xf7a195ff).into()), + terminal_ansi_dim_red: Some(rgba(0x781c1fff).into()), terminal_ansi_green: Some(rgba(0x61ac3aff).into()), + terminal_ansi_bright_green: Some(rgba(0xb3d69cff).into()), + terminal_ansi_dim_green: Some(rgba(0x335322ff).into()), terminal_ansi_yellow: Some(rgba(0xae9515ff).into()), + terminal_ansi_bright_yellow: Some(rgba(0xdcc98eff).into()), + terminal_ansi_dim_yellow: Some(rgba(0x574815ff).into()), terminal_ansi_blue: Some(rgba(0x6784e0ff).into()), + terminal_ansi_bright_blue: Some(rgba(0xb8c0f1ff).into()), + terminal_ansi_dim_blue: Some(rgba(0x334173ff).into()), terminal_ansi_magenta: Some(rgba(0xd43753ff).into()), + terminal_ansi_bright_magenta: Some(rgba(0xf3a0a4ff).into()), + terminal_ansi_dim_magenta: Some(rgba(0x721d2bff).into()), terminal_ansi_cyan: Some(rgba(0x22ad83ff).into()), + terminal_ansi_bright_cyan: Some(rgba(0x9ed7c0ff).into()), + terminal_ansi_dim_cyan: Some(rgba(0x1e5341ff).into()), terminal_ansi_white: Some(rgba(0x20201dff).into()), + terminal_ansi_bright_white: Some(rgba(0x20201dff).into()), + terminal_ansi_dim_white: Some(rgba(0x7a7766ff).into()), link_text_hover: Some(rgba(0x6784e0ff).into()), ..Default::default() }, @@ -1936,22 +1980,33 @@ pub fn atelier() -> UserThemeFamily { editor_document_highlight_read_background: Some(rgba(0x37a1661a).into()), editor_document_highlight_write_background: Some(rgba(0x7a786766).into()), terminal_background: Some(rgba(0x22221bff).into()), - terminal_ansi_bright_black: Some(rgba(0x6a6958ff).into()), - terminal_ansi_bright_red: Some(rgba(0x5c331fff).into()), - terminal_ansi_bright_green: Some(rgba(0x3f491aff).into()), - terminal_ansi_bright_yellow: Some(rgba(0x514a14ff).into()), - terminal_ansi_bright_blue: Some(rgba(0x234e34ff).into()), - terminal_ansi_bright_magenta: Some(rgba(0x4c373eff).into()), - terminal_ansi_bright_cyan: Some(rgba(0x314c27ff).into()), - terminal_ansi_bright_white: Some(rgba(0xf4f3ecff).into()), + terminal_foreground: Some(rgba(0xf4f3ecff).into()), + terminal_bright_foreground: Some(rgba(0xf4f3ecff).into()), + terminal_dim_foreground: Some(rgba(0x22221bff).into()), terminal_ansi_black: Some(rgba(0x22221bff).into()), + terminal_ansi_bright_black: Some(rgba(0x6a6958ff).into()), + terminal_ansi_dim_black: Some(rgba(0xf4f3ecff).into()), terminal_ansi_red: Some(rgba(0xba6237ff).into()), + terminal_ansi_bright_red: Some(rgba(0x5c331fff).into()), + terminal_ansi_dim_red: Some(rgba(0xe4af96ff).into()), terminal_ansi_green: Some(rgba(0x7d9727ff).into()), + terminal_ansi_bright_green: Some(rgba(0x3f491aff).into()), + terminal_ansi_dim_green: Some(rgba(0xc0ca93ff).into()), terminal_ansi_yellow: Some(rgba(0xa59810ff).into()), + terminal_ansi_bright_yellow: Some(rgba(0x514a14ff).into()), + terminal_ansi_dim_yellow: Some(rgba(0xd7ca8dff).into()), terminal_ansi_blue: Some(rgba(0x37a166ff).into()), + terminal_ansi_bright_blue: Some(rgba(0x234e34ff).into()), + terminal_ansi_dim_blue: Some(rgba(0xa0d1b0ff).into()), terminal_ansi_magenta: Some(rgba(0x9d6c7cff).into()), + terminal_ansi_bright_magenta: Some(rgba(0x4c373eff).into()), + terminal_ansi_dim_magenta: Some(rgba(0xcfb4bcff).into()), terminal_ansi_cyan: Some(rgba(0x5b9d48ff).into()), + terminal_ansi_bright_cyan: Some(rgba(0x314c27ff).into()), + terminal_ansi_dim_cyan: Some(rgba(0xaecea1ff).into()), terminal_ansi_white: Some(rgba(0xf4f3ecff).into()), + terminal_ansi_bright_white: Some(rgba(0xf4f3ecff).into()), + terminal_ansi_dim_white: Some(rgba(0x898775ff).into()), link_text_hover: Some(rgba(0x37a166ff).into()), ..Default::default() }, @@ -2401,22 +2456,33 @@ pub fn atelier() -> UserThemeFamily { editor_document_highlight_read_background: Some(rgba(0x38a1661a).into()), editor_document_highlight_write_background: Some(rgba(0x7a786766).into()), terminal_background: Some(rgba(0xf4f3ecff).into()), - terminal_ansi_bright_black: Some(rgba(0x898775ff).into()), - terminal_ansi_bright_red: Some(rgba(0xe4af96ff).into()), - terminal_ansi_bright_green: Some(rgba(0xc0ca93ff).into()), - terminal_ansi_bright_yellow: Some(rgba(0xd7ca8dff).into()), - terminal_ansi_bright_blue: Some(rgba(0xa0d1b0ff).into()), - terminal_ansi_bright_magenta: Some(rgba(0xcfb4bcff).into()), - terminal_ansi_bright_cyan: Some(rgba(0xaecea1ff).into()), - terminal_ansi_bright_white: Some(rgba(0x22221bff).into()), + terminal_foreground: Some(rgba(0x22221bff).into()), + terminal_bright_foreground: Some(rgba(0x22221bff).into()), + terminal_dim_foreground: Some(rgba(0xf4f3ecff).into()), terminal_ansi_black: Some(rgba(0xf4f3ecff).into()), + terminal_ansi_bright_black: Some(rgba(0x898775ff).into()), + terminal_ansi_dim_black: Some(rgba(0x22221bff).into()), terminal_ansi_red: Some(rgba(0xba6337ff).into()), + terminal_ansi_bright_red: Some(rgba(0xe4af96ff).into()), + terminal_ansi_dim_red: Some(rgba(0x5c331fff).into()), terminal_ansi_green: Some(rgba(0x7d9728ff).into()), + terminal_ansi_bright_green: Some(rgba(0xc0ca93ff).into()), + terminal_ansi_dim_green: Some(rgba(0x3f491aff).into()), terminal_ansi_yellow: Some(rgba(0xa59810ff).into()), + terminal_ansi_bright_yellow: Some(rgba(0xd7ca8dff).into()), + terminal_ansi_dim_yellow: Some(rgba(0x514a14ff).into()), terminal_ansi_blue: Some(rgba(0x38a166ff).into()), + terminal_ansi_bright_blue: Some(rgba(0xa0d1b0ff).into()), + terminal_ansi_dim_blue: Some(rgba(0x234e34ff).into()), terminal_ansi_magenta: Some(rgba(0x9d6c7cff).into()), + terminal_ansi_bright_magenta: Some(rgba(0xcfb4bcff).into()), + terminal_ansi_dim_magenta: Some(rgba(0x4c373eff).into()), terminal_ansi_cyan: Some(rgba(0x5c9d49ff).into()), + terminal_ansi_bright_cyan: Some(rgba(0xaecea1ff).into()), + terminal_ansi_dim_cyan: Some(rgba(0x314c27ff).into()), terminal_ansi_white: Some(rgba(0x22221bff).into()), + terminal_ansi_bright_white: Some(rgba(0x22221bff).into()), + terminal_ansi_dim_white: Some(rgba(0x6a6958ff).into()), link_text_hover: Some(rgba(0x38a166ff).into()), ..Default::default() }, @@ -2866,22 +2932,33 @@ pub fn atelier() -> UserThemeFamily { editor_document_highlight_read_background: Some(rgba(0x417ee61a).into()), editor_document_highlight_write_background: Some(rgba(0x89817e66).into()), terminal_background: Some(rgba(0x1b1918ff).into()), - terminal_ansi_bright_black: Some(rgba(0x746c69ff).into()), - terminal_ansi_bright_red: Some(rgba(0x8c1223ff).into()), - terminal_ansi_bright_green: Some(rgba(0x3e491aff).into()), - terminal_ansi_bright_yellow: Some(rgba(0x674115ff).into()), - terminal_ansi_bright_blue: Some(rgba(0x213f78ff).into()), - terminal_ansi_bright_magenta: Some(rgba(0x662186ff).into()), - terminal_ansi_bright_cyan: Some(rgba(0x264958ff).into()), - terminal_ansi_bright_white: Some(rgba(0xf1efeeff).into()), + terminal_foreground: Some(rgba(0xf1efeeff).into()), + terminal_bright_foreground: Some(rgba(0xf1efeeff).into()), + terminal_dim_foreground: Some(rgba(0x1b1918ff).into()), terminal_ansi_black: Some(rgba(0x1b1918ff).into()), + terminal_ansi_bright_black: Some(rgba(0x746c69ff).into()), + terminal_ansi_dim_black: Some(rgba(0xf1efeeff).into()), terminal_ansi_red: Some(rgba(0xf22d40ff).into()), + terminal_ansi_bright_red: Some(rgba(0x8c1223ff).into()), + terminal_ansi_dim_red: Some(rgba(0xffa29aff).into()), terminal_ansi_green: Some(rgba(0x7b9727ff).into()), + terminal_ansi_bright_green: Some(rgba(0x3e491aff).into()), + terminal_ansi_dim_green: Some(rgba(0xbfca93ff).into()), terminal_ansi_yellow: Some(rgba(0xc38419ff).into()), + terminal_ansi_bright_yellow: Some(rgba(0x674115ff).into()), + terminal_ansi_dim_yellow: Some(rgba(0xe9c08eff).into()), terminal_ansi_blue: Some(rgba(0x417ee6ff).into()), + terminal_ansi_bright_blue: Some(rgba(0x213f78ff).into()), + terminal_ansi_dim_blue: Some(rgba(0xaebcf4ff).into()), terminal_ansi_magenta: Some(rgba(0xc340f2ff).into()), + terminal_ansi_bright_magenta: Some(rgba(0x662186ff).into()), + terminal_ansi_dim_magenta: Some(rgba(0xe7a6fbff).into()), terminal_ansi_cyan: Some(rgba(0x3e97b8ff).into()), + terminal_ansi_bright_cyan: Some(rgba(0x264958ff).into()), + terminal_ansi_dim_cyan: Some(rgba(0xa6cadbff).into()), terminal_ansi_white: Some(rgba(0xf1efeeff).into()), + terminal_ansi_bright_white: Some(rgba(0xf1efeeff).into()), + terminal_ansi_dim_white: Some(rgba(0x9e9693ff).into()), link_text_hover: Some(rgba(0x417ee6ff).into()), ..Default::default() }, @@ -3331,22 +3408,33 @@ pub fn atelier() -> UserThemeFamily { editor_document_highlight_read_background: Some(rgba(0x417ee61a).into()), editor_document_highlight_write_background: Some(rgba(0x89817e66).into()), terminal_background: Some(rgba(0xf1efeeff).into()), - terminal_ansi_bright_black: Some(rgba(0x9e9693ff).into()), - terminal_ansi_bright_red: Some(rgba(0xffa29aff).into()), - terminal_ansi_bright_green: Some(rgba(0xbfca93ff).into()), - terminal_ansi_bright_yellow: Some(rgba(0xe9c08eff).into()), - terminal_ansi_bright_blue: Some(rgba(0xaebcf4ff).into()), - terminal_ansi_bright_magenta: Some(rgba(0xe7a6fbff).into()), - terminal_ansi_bright_cyan: Some(rgba(0xa6cadbff).into()), - terminal_ansi_bright_white: Some(rgba(0x1b1918ff).into()), + terminal_foreground: Some(rgba(0x1b1918ff).into()), + terminal_bright_foreground: Some(rgba(0x1b1918ff).into()), + terminal_dim_foreground: Some(rgba(0xf1efeeff).into()), terminal_ansi_black: Some(rgba(0xf1efeeff).into()), + terminal_ansi_bright_black: Some(rgba(0x9e9693ff).into()), + terminal_ansi_dim_black: Some(rgba(0x1b1918ff).into()), terminal_ansi_red: Some(rgba(0xf22e41ff).into()), + terminal_ansi_bright_red: Some(rgba(0xffa29aff).into()), + terminal_ansi_dim_red: Some(rgba(0x8c1223ff).into()), terminal_ansi_green: Some(rgba(0x7b9728ff).into()), + terminal_ansi_bright_green: Some(rgba(0xbfca93ff).into()), + terminal_ansi_dim_green: Some(rgba(0x3e491aff).into()), terminal_ansi_yellow: Some(rgba(0xc3841aff).into()), + terminal_ansi_bright_yellow: Some(rgba(0xe9c08eff).into()), + terminal_ansi_dim_yellow: Some(rgba(0x674115ff).into()), terminal_ansi_blue: Some(rgba(0x417ee6ff).into()), + terminal_ansi_bright_blue: Some(rgba(0xaebcf4ff).into()), + terminal_ansi_dim_blue: Some(rgba(0x213f78ff).into()), terminal_ansi_magenta: Some(rgba(0xc340f2ff).into()), + terminal_ansi_bright_magenta: Some(rgba(0xe7a6fbff).into()), + terminal_ansi_dim_magenta: Some(rgba(0x662186ff).into()), terminal_ansi_cyan: Some(rgba(0x3f97b8ff).into()), + terminal_ansi_bright_cyan: Some(rgba(0xa6cadbff).into()), + terminal_ansi_dim_cyan: Some(rgba(0x264958ff).into()), terminal_ansi_white: Some(rgba(0x1b1918ff).into()), + terminal_ansi_bright_white: Some(rgba(0x1b1918ff).into()), + terminal_ansi_dim_white: Some(rgba(0x746c69ff).into()), link_text_hover: Some(rgba(0x417ee6ff).into()), ..Default::default() }, @@ -3796,22 +3884,33 @@ pub fn atelier() -> UserThemeFamily { editor_document_highlight_read_background: Some(rgba(0x526aeb1a).into()), editor_document_highlight_write_background: Some(rgba(0x8b7c8b66).into()), terminal_background: Some(rgba(0x1b181bff).into()), - terminal_ansi_bright_black: Some(rgba(0x756775ff).into()), - terminal_ansi_bright_red: Some(rgba(0x6d221aff).into()), - terminal_ansi_bright_green: Some(rgba(0x474422ff).into()), - terminal_ansi_bright_yellow: Some(rgba(0x5e441fff).into()), - terminal_ansi_bright_blue: Some(rgba(0x26367eff).into()), - terminal_ansi_bright_magenta: Some(rgba(0x6c1e67ff).into()), - terminal_ansi_bright_cyan: Some(rgba(0x1a4848ff).into()), - terminal_ansi_bright_white: Some(rgba(0xf7f3f7ff).into()), + terminal_foreground: Some(rgba(0xf7f3f7ff).into()), + terminal_bright_foreground: Some(rgba(0xf7f3f7ff).into()), + terminal_dim_foreground: Some(rgba(0x1b181bff).into()), terminal_ansi_black: Some(rgba(0x1b181bff).into()), + terminal_ansi_bright_black: Some(rgba(0x756775ff).into()), + terminal_ansi_dim_black: Some(rgba(0xf7f3f7ff).into()), terminal_ansi_red: Some(rgba(0xca402cff).into()), + terminal_ansi_bright_red: Some(rgba(0x6d221aff).into()), + terminal_ansi_dim_red: Some(rgba(0xf0a28fff).into()), terminal_ansi_green: Some(rgba(0x918b3bff).into()), + terminal_ansi_bright_green: Some(rgba(0x474422ff).into()), + terminal_ansi_dim_green: Some(rgba(0xcac49aff).into()), terminal_ansi_yellow: Some(rgba(0xbb8a36ff).into()), + terminal_ansi_bright_yellow: Some(rgba(0x5e441fff).into()), + terminal_ansi_dim_yellow: Some(rgba(0xe2c398ff).into()), terminal_ansi_blue: Some(rgba(0x526aebff).into()), + terminal_ansi_bright_blue: Some(rgba(0x26367eff).into()), + terminal_ansi_dim_blue: Some(rgba(0xb4b2f7ff).into()), terminal_ansi_magenta: Some(rgba(0xcc34ccff).into()), + terminal_ansi_bright_magenta: Some(rgba(0x6c1e67ff).into()), + terminal_ansi_dim_magenta: Some(rgba(0xeba2e6ff).into()), terminal_ansi_cyan: Some(rgba(0x189393ff).into()), + terminal_ansi_bright_cyan: Some(rgba(0x1a4848ff).into()), + terminal_ansi_dim_cyan: Some(rgba(0x9ac9c8ff).into()), terminal_ansi_white: Some(rgba(0xf7f3f7ff).into()), + terminal_ansi_bright_white: Some(rgba(0xf7f3f7ff).into()), + terminal_ansi_dim_white: Some(rgba(0xa091a0ff).into()), link_text_hover: Some(rgba(0x526aebff).into()), ..Default::default() }, @@ -4261,22 +4360,33 @@ pub fn atelier() -> UserThemeFamily { editor_document_highlight_read_background: Some(rgba(0x526aeb1a).into()), editor_document_highlight_write_background: Some(rgba(0x8b7c8b66).into()), terminal_background: Some(rgba(0xf7f3f7ff).into()), - terminal_ansi_bright_black: Some(rgba(0xa091a0ff).into()), - terminal_ansi_bright_red: Some(rgba(0xf0a28fff).into()), - terminal_ansi_bright_green: Some(rgba(0xcac49aff).into()), - terminal_ansi_bright_yellow: Some(rgba(0xe2c398ff).into()), - terminal_ansi_bright_blue: Some(rgba(0xb4b2f7ff).into()), - terminal_ansi_bright_magenta: Some(rgba(0xeba2e6ff).into()), - terminal_ansi_bright_cyan: Some(rgba(0x9ac9c8ff).into()), - terminal_ansi_bright_white: Some(rgba(0x1b181bff).into()), + terminal_foreground: Some(rgba(0x1b181bff).into()), + terminal_bright_foreground: Some(rgba(0x1b181bff).into()), + terminal_dim_foreground: Some(rgba(0xf7f3f7ff).into()), terminal_ansi_black: Some(rgba(0xf7f3f7ff).into()), + terminal_ansi_bright_black: Some(rgba(0xa091a0ff).into()), + terminal_ansi_dim_black: Some(rgba(0x1b181bff).into()), terminal_ansi_red: Some(rgba(0xca412cff).into()), + terminal_ansi_bright_red: Some(rgba(0xf0a28fff).into()), + terminal_ansi_dim_red: Some(rgba(0x6d221aff).into()), terminal_ansi_green: Some(rgba(0x918b3cff).into()), + terminal_ansi_bright_green: Some(rgba(0xcac49aff).into()), + terminal_ansi_dim_green: Some(rgba(0x474422ff).into()), terminal_ansi_yellow: Some(rgba(0xbb8a36ff).into()), + terminal_ansi_bright_yellow: Some(rgba(0xe2c398ff).into()), + terminal_ansi_dim_yellow: Some(rgba(0x5e441fff).into()), terminal_ansi_blue: Some(rgba(0x526aebff).into()), + terminal_ansi_bright_blue: Some(rgba(0xb4b2f7ff).into()), + terminal_ansi_dim_blue: Some(rgba(0x26367eff).into()), terminal_ansi_magenta: Some(rgba(0xcc35ccff).into()), + terminal_ansi_bright_magenta: Some(rgba(0xeba2e6ff).into()), + terminal_ansi_dim_magenta: Some(rgba(0x6c1e67ff).into()), terminal_ansi_cyan: Some(rgba(0x199393ff).into()), + terminal_ansi_bright_cyan: Some(rgba(0x9ac9c8ff).into()), + terminal_ansi_dim_cyan: Some(rgba(0x1a4848ff).into()), terminal_ansi_white: Some(rgba(0x1b181bff).into()), + terminal_ansi_bright_white: Some(rgba(0x1b181bff).into()), + terminal_ansi_dim_white: Some(rgba(0x756775ff).into()), link_text_hover: Some(rgba(0x526aebff).into()), ..Default::default() }, @@ -4726,22 +4836,33 @@ pub fn atelier() -> UserThemeFamily { editor_document_highlight_read_background: Some(rgba(0x277fad1a).into()), editor_document_highlight_write_background: Some(rgba(0x66889a66).into()), terminal_background: Some(rgba(0x161b1dff).into()), - terminal_ansi_bright_black: Some(rgba(0x587989ff).into()), - terminal_ansi_bright_red: Some(rgba(0x6f1c3aff).into()), - terminal_ansi_bright_green: Some(rgba(0x2e4522ff).into()), - terminal_ansi_bright_yellow: Some(rgba(0x454413ff).into()), - terminal_ansi_bright_blue: Some(rgba(0x1e3f53ff).into()), - terminal_ansi_bright_magenta: Some(rgba(0x5c1e6bff).into()), - terminal_ansi_bright_cyan: Some(rgba(0x1f4638ff).into()), - terminal_ansi_bright_white: Some(rgba(0xebf8ffff).into()), + terminal_foreground: Some(rgba(0xebf8ffff).into()), + terminal_bright_foreground: Some(rgba(0xebf8ffff).into()), + terminal_dim_foreground: Some(rgba(0x161b1dff).into()), terminal_ansi_black: Some(rgba(0x161b1dff).into()), + terminal_ansi_bright_black: Some(rgba(0x587989ff).into()), + terminal_ansi_dim_black: Some(rgba(0xebf8ffff).into()), terminal_ansi_red: Some(rgba(0xd22e72ff).into()), + terminal_ansi_bright_red: Some(rgba(0x6f1c3aff).into()), + terminal_ansi_dim_red: Some(rgba(0xf09fb6ff).into()), terminal_ansi_green: Some(rgba(0x568c3bff).into()), + terminal_ansi_bright_green: Some(rgba(0x2e4522ff).into()), + terminal_ansi_dim_green: Some(rgba(0xabc59aff).into()), terminal_ansi_yellow: Some(rgba(0x8a8a11ff).into()), + terminal_ansi_bright_yellow: Some(rgba(0x454413ff).into()), + terminal_ansi_dim_yellow: Some(rgba(0xc8c38bff).into()), terminal_ansi_blue: Some(rgba(0x277fadff).into()), + terminal_ansi_bright_blue: Some(rgba(0x1e3f53ff).into()), + terminal_ansi_dim_blue: Some(rgba(0x9ebdd6ff).into()), terminal_ansi_magenta: Some(rgba(0xb72ed2ff).into()), + terminal_ansi_bright_magenta: Some(rgba(0x5c1e6bff).into()), + terminal_ansi_dim_magenta: Some(rgba(0xe09fe9ff).into()), terminal_ansi_cyan: Some(rgba(0x2e8f6fff).into()), + terminal_ansi_bright_cyan: Some(rgba(0x1f4638ff).into()), + terminal_ansi_dim_cyan: Some(rgba(0x9bc7b5ff).into()), terminal_ansi_white: Some(rgba(0xebf8ffff).into()), + terminal_ansi_bright_white: Some(rgba(0xebf8ffff).into()), + terminal_ansi_dim_white: Some(rgba(0x7397aaff).into()), link_text_hover: Some(rgba(0x277fadff).into()), ..Default::default() }, @@ -5191,22 +5312,33 @@ pub fn atelier() -> UserThemeFamily { editor_document_highlight_read_background: Some(rgba(0x277fad1a).into()), editor_document_highlight_write_background: Some(rgba(0x66889a66).into()), terminal_background: Some(rgba(0xebf8ffff).into()), - terminal_ansi_bright_black: Some(rgba(0x7397aaff).into()), - terminal_ansi_bright_red: Some(rgba(0xf09fb6ff).into()), - terminal_ansi_bright_green: Some(rgba(0xabc59aff).into()), - terminal_ansi_bright_yellow: Some(rgba(0xc8c38bff).into()), - terminal_ansi_bright_blue: Some(rgba(0x9ebdd6ff).into()), - terminal_ansi_bright_magenta: Some(rgba(0xe09fe9ff).into()), - terminal_ansi_bright_cyan: Some(rgba(0x9bc7b5ff).into()), - terminal_ansi_bright_white: Some(rgba(0x161b1dff).into()), + terminal_foreground: Some(rgba(0x161b1dff).into()), + terminal_bright_foreground: Some(rgba(0x161b1dff).into()), + terminal_dim_foreground: Some(rgba(0xebf8ffff).into()), terminal_ansi_black: Some(rgba(0xebf8ffff).into()), + terminal_ansi_bright_black: Some(rgba(0x7397aaff).into()), + terminal_ansi_dim_black: Some(rgba(0x161b1dff).into()), terminal_ansi_red: Some(rgba(0xd22f72ff).into()), + terminal_ansi_bright_red: Some(rgba(0xf09fb6ff).into()), + terminal_ansi_dim_red: Some(rgba(0x6f1c3aff).into()), terminal_ansi_green: Some(rgba(0x578c3cff).into()), + terminal_ansi_bright_green: Some(rgba(0xabc59aff).into()), + terminal_ansi_dim_green: Some(rgba(0x2e4522ff).into()), terminal_ansi_yellow: Some(rgba(0x8a8a11ff).into()), + terminal_ansi_bright_yellow: Some(rgba(0xc8c38bff).into()), + terminal_ansi_dim_yellow: Some(rgba(0x454413ff).into()), terminal_ansi_blue: Some(rgba(0x277fadff).into()), + terminal_ansi_bright_blue: Some(rgba(0x9ebdd6ff).into()), + terminal_ansi_dim_blue: Some(rgba(0x1e3f53ff).into()), terminal_ansi_magenta: Some(rgba(0xb72fd2ff).into()), + terminal_ansi_bright_magenta: Some(rgba(0xe09fe9ff).into()), + terminal_ansi_dim_magenta: Some(rgba(0x5c1e6bff).into()), terminal_ansi_cyan: Some(rgba(0x2f8f6fff).into()), + terminal_ansi_bright_cyan: Some(rgba(0x9bc7b5ff).into()), + terminal_ansi_dim_cyan: Some(rgba(0x1f4638ff).into()), terminal_ansi_white: Some(rgba(0x161b1dff).into()), + terminal_ansi_bright_white: Some(rgba(0x161b1dff).into()), + terminal_ansi_dim_white: Some(rgba(0x587989ff).into()), link_text_hover: Some(rgba(0x277fadff).into()), ..Default::default() }, @@ -5656,22 +5788,33 @@ pub fn atelier() -> UserThemeFamily { editor_document_highlight_read_background: Some(rgba(0x7272ca1a).into()), editor_document_highlight_write_background: Some(rgba(0x726a6a66).into()), terminal_background: Some(rgba(0x1b1818ff).into()), - terminal_ansi_bright_black: Some(rgba(0x635b5bff).into()), - terminal_ansi_bright_red: Some(rgba(0x692727ff).into()), - terminal_ansi_bright_green: Some(rgba(0x2a4444ff).into()), - terminal_ansi_bright_yellow: Some(rgba(0x4e3821ff).into()), - terminal_ansi_bright_blue: Some(rgba(0x3b3960ff).into()), - terminal_ansi_bright_magenta: Some(rgba(0x5b2c42ff).into()), - terminal_ansi_bright_cyan: Some(rgba(0x2e4257ff).into()), - terminal_ansi_bright_white: Some(rgba(0xf4ececff).into()), + terminal_foreground: Some(rgba(0xf4ececff).into()), + terminal_bright_foreground: Some(rgba(0xf4ececff).into()), + terminal_dim_foreground: Some(rgba(0x1b1818ff).into()), terminal_ansi_black: Some(rgba(0x1b1818ff).into()), + terminal_ansi_bright_black: Some(rgba(0x635b5bff).into()), + terminal_ansi_dim_black: Some(rgba(0xf4ececff).into()), terminal_ansi_red: Some(rgba(0xca4949ff).into()), + terminal_ansi_bright_red: Some(rgba(0x692727ff).into()), + terminal_ansi_dim_red: Some(rgba(0xeda69fff).into()), terminal_ansi_green: Some(rgba(0x4b8b8bff).into()), + terminal_ansi_bright_green: Some(rgba(0x2a4444ff).into()), + terminal_ansi_dim_green: Some(rgba(0xa6c4c4ff).into()), terminal_ansi_yellow: Some(rgba(0xa06e3bff).into()), + terminal_ansi_bright_yellow: Some(rgba(0x4e3821ff).into()), + terminal_ansi_dim_yellow: Some(rgba(0xd4b499ff).into()), terminal_ansi_blue: Some(rgba(0x7272caff).into()), + terminal_ansi_bright_blue: Some(rgba(0x3b3960ff).into()), + terminal_ansi_dim_blue: Some(rgba(0xbbb6e5ff).into()), terminal_ansi_magenta: Some(rgba(0xbd5187ff).into()), + terminal_ansi_bright_magenta: Some(rgba(0x5b2c42ff).into()), + terminal_ansi_dim_magenta: Some(rgba(0xe2a9c2ff).into()), terminal_ansi_cyan: Some(rgba(0x5485b6ff).into()), + terminal_ansi_bright_cyan: Some(rgba(0x2e4257ff).into()), + terminal_ansi_dim_cyan: Some(rgba(0xacc0daff).into()), terminal_ansi_white: Some(rgba(0xf4ececff).into()), + terminal_ansi_bright_white: Some(rgba(0xf4ececff).into()), + terminal_ansi_dim_white: Some(rgba(0x807979ff).into()), link_text_hover: Some(rgba(0x7272caff).into()), ..Default::default() }, @@ -6121,22 +6264,33 @@ pub fn atelier() -> UserThemeFamily { editor_document_highlight_read_background: Some(rgba(0x7372ca1a).into()), editor_document_highlight_write_background: Some(rgba(0x726a6a66).into()), terminal_background: Some(rgba(0xf4ececff).into()), - terminal_ansi_bright_black: Some(rgba(0x807979ff).into()), - terminal_ansi_bright_red: Some(rgba(0xeda69fff).into()), - terminal_ansi_bright_green: Some(rgba(0xa6c4c4ff).into()), - terminal_ansi_bright_yellow: Some(rgba(0xd4b499ff).into()), - terminal_ansi_bright_blue: Some(rgba(0xbbb6e5ff).into()), - terminal_ansi_bright_magenta: Some(rgba(0xe2a9c2ff).into()), - terminal_ansi_bright_cyan: Some(rgba(0xacc0daff).into()), - terminal_ansi_bright_white: Some(rgba(0x1b1818ff).into()), + terminal_foreground: Some(rgba(0x1b1818ff).into()), + terminal_bright_foreground: Some(rgba(0x1b1818ff).into()), + terminal_dim_foreground: Some(rgba(0xf4ececff).into()), terminal_ansi_black: Some(rgba(0xf4ececff).into()), + terminal_ansi_bright_black: Some(rgba(0x807979ff).into()), + terminal_ansi_dim_black: Some(rgba(0x1b1818ff).into()), terminal_ansi_red: Some(rgba(0xca4a4aff).into()), + terminal_ansi_bright_red: Some(rgba(0xeda69fff).into()), + terminal_ansi_dim_red: Some(rgba(0x692727ff).into()), terminal_ansi_green: Some(rgba(0x4c8b8bff).into()), + terminal_ansi_bright_green: Some(rgba(0xa6c4c4ff).into()), + terminal_ansi_dim_green: Some(rgba(0x2a4444ff).into()), terminal_ansi_yellow: Some(rgba(0xa06e3cff).into()), + terminal_ansi_bright_yellow: Some(rgba(0xd4b499ff).into()), + terminal_ansi_dim_yellow: Some(rgba(0x4e3821ff).into()), terminal_ansi_blue: Some(rgba(0x7372caff).into()), + terminal_ansi_bright_blue: Some(rgba(0xbbb6e5ff).into()), + terminal_ansi_dim_blue: Some(rgba(0x3b3960ff).into()), terminal_ansi_magenta: Some(rgba(0xbd5287ff).into()), + terminal_ansi_bright_magenta: Some(rgba(0xe2a9c2ff).into()), + terminal_ansi_dim_magenta: Some(rgba(0x5b2c42ff).into()), terminal_ansi_cyan: Some(rgba(0x5585b6ff).into()), + terminal_ansi_bright_cyan: Some(rgba(0xacc0daff).into()), + terminal_ansi_dim_cyan: Some(rgba(0x2e4257ff).into()), terminal_ansi_white: Some(rgba(0x1b1818ff).into()), + terminal_ansi_bright_white: Some(rgba(0x1b1818ff).into()), + terminal_ansi_dim_white: Some(rgba(0x635b5bff).into()), link_text_hover: Some(rgba(0x7372caff).into()), ..Default::default() }, @@ -6586,22 +6740,33 @@ pub fn atelier() -> UserThemeFamily { editor_document_highlight_read_background: Some(rgba(0x478c901a).into()), editor_document_highlight_write_background: Some(rgba(0x6c7a7166).into()), terminal_background: Some(rgba(0x171c19ff).into()), - terminal_ansi_bright_black: Some(rgba(0x5d6b62ff).into()), - terminal_ansi_bright_red: Some(rgba(0x563220ff).into()), - terminal_ansi_bright_green: Some(rgba(0x294a33ff).into()), - terminal_ansi_bright_yellow: Some(rgba(0x4e3f22ff).into()), - terminal_ansi_bright_blue: Some(rgba(0x284546ff).into()), - terminal_ansi_bright_magenta: Some(rgba(0x423a36ff).into()), - terminal_ansi_bright_cyan: Some(rgba(0x1d4b4dff).into()), - terminal_ansi_bright_white: Some(rgba(0xecf4eeff).into()), + terminal_foreground: Some(rgba(0xecf4eeff).into()), + terminal_bright_foreground: Some(rgba(0xecf4eeff).into()), + terminal_dim_foreground: Some(rgba(0x171c19ff).into()), terminal_ansi_black: Some(rgba(0x171c19ff).into()), + terminal_ansi_bright_black: Some(rgba(0x5d6b62ff).into()), + terminal_ansi_dim_black: Some(rgba(0xecf4eeff).into()), terminal_ansi_red: Some(rgba(0xb16139ff).into()), + terminal_ansi_bright_red: Some(rgba(0x563220ff).into()), + terminal_ansi_dim_red: Some(rgba(0xdeae97ff).into()), terminal_ansi_green: Some(rgba(0x489963ff).into()), + terminal_ansi_bright_green: Some(rgba(0x294a33ff).into()), + terminal_ansi_dim_green: Some(rgba(0xa5ccafff).into()), terminal_ansi_yellow: Some(rgba(0xa07e3bff).into()), + terminal_ansi_bright_yellow: Some(rgba(0x4e3f22ff).into()), + terminal_ansi_dim_yellow: Some(rgba(0xd3bd9aff).into()), terminal_ansi_blue: Some(rgba(0x478c90ff).into()), + terminal_ansi_bright_blue: Some(rgba(0x284546ff).into()), + terminal_ansi_dim_blue: Some(rgba(0xa5c5c6ff).into()), terminal_ansi_magenta: Some(rgba(0x867469ff).into()), + terminal_ansi_bright_magenta: Some(rgba(0x423a36ff).into()), + terminal_ansi_dim_magenta: Some(rgba(0xc2b7b1ff).into()), terminal_ansi_cyan: Some(rgba(0x1e9aa0ff).into()), + terminal_ansi_bright_cyan: Some(rgba(0x1d4b4dff).into()), + terminal_ansi_dim_cyan: Some(rgba(0x9dcdcfff).into()), terminal_ansi_white: Some(rgba(0xecf4eeff).into()), + terminal_ansi_bright_white: Some(rgba(0xecf4eeff).into()), + terminal_ansi_dim_white: Some(rgba(0x7b897fff).into()), link_text_hover: Some(rgba(0x478c90ff).into()), ..Default::default() }, @@ -7051,22 +7216,33 @@ pub fn atelier() -> UserThemeFamily { editor_document_highlight_read_background: Some(rgba(0x488c901a).into()), editor_document_highlight_write_background: Some(rgba(0x6c7a7166).into()), terminal_background: Some(rgba(0xecf4eeff).into()), - terminal_ansi_bright_black: Some(rgba(0x7b897fff).into()), - terminal_ansi_bright_red: Some(rgba(0xdeae97ff).into()), - terminal_ansi_bright_green: Some(rgba(0xa5ccafff).into()), - terminal_ansi_bright_yellow: Some(rgba(0xd3bd9aff).into()), - terminal_ansi_bright_blue: Some(rgba(0xa5c5c6ff).into()), - terminal_ansi_bright_magenta: Some(rgba(0xc2b7b1ff).into()), - terminal_ansi_bright_cyan: Some(rgba(0x9dcdcfff).into()), - terminal_ansi_bright_white: Some(rgba(0x171c19ff).into()), + terminal_foreground: Some(rgba(0x171c19ff).into()), + terminal_bright_foreground: Some(rgba(0x171c19ff).into()), + terminal_dim_foreground: Some(rgba(0xecf4eeff).into()), terminal_ansi_black: Some(rgba(0xecf4eeff).into()), + terminal_ansi_bright_black: Some(rgba(0x7b897fff).into()), + terminal_ansi_dim_black: Some(rgba(0x171c19ff).into()), terminal_ansi_red: Some(rgba(0xb1623aff).into()), + terminal_ansi_bright_red: Some(rgba(0xdeae97ff).into()), + terminal_ansi_dim_red: Some(rgba(0x563220ff).into()), terminal_ansi_green: Some(rgba(0x499963ff).into()), + terminal_ansi_bright_green: Some(rgba(0xa5ccafff).into()), + terminal_ansi_dim_green: Some(rgba(0x294a33ff).into()), terminal_ansi_yellow: Some(rgba(0xa07e3cff).into()), + terminal_ansi_bright_yellow: Some(rgba(0xd3bd9aff).into()), + terminal_ansi_dim_yellow: Some(rgba(0x4e3f22ff).into()), terminal_ansi_blue: Some(rgba(0x488c90ff).into()), + terminal_ansi_bright_blue: Some(rgba(0xa5c5c6ff).into()), + terminal_ansi_dim_blue: Some(rgba(0x284546ff).into()), terminal_ansi_magenta: Some(rgba(0x867469ff).into()), + terminal_ansi_bright_magenta: Some(rgba(0xc2b7b1ff).into()), + terminal_ansi_dim_magenta: Some(rgba(0x423a36ff).into()), terminal_ansi_cyan: Some(rgba(0x1f9aa0ff).into()), + terminal_ansi_bright_cyan: Some(rgba(0x9dcdcfff).into()), + terminal_ansi_dim_cyan: Some(rgba(0x1d4b4dff).into()), terminal_ansi_white: Some(rgba(0x171c19ff).into()), + terminal_ansi_bright_white: Some(rgba(0x171c19ff).into()), + terminal_ansi_dim_white: Some(rgba(0x5d6b62ff).into()), link_text_hover: Some(rgba(0x488c90ff).into()), ..Default::default() }, @@ -7516,22 +7692,33 @@ pub fn atelier() -> UserThemeFamily { editor_document_highlight_read_background: Some(rgba(0x3e62f41a).into()), editor_document_highlight_write_background: Some(rgba(0x748b7466).into()), terminal_background: Some(rgba(0x131513ff).into()), - terminal_ansi_bright_black: Some(rgba(0x667a66ff).into()), - terminal_ansi_bright_red: Some(rgba(0x840b21ff).into()), - terminal_ansi_bright_green: Some(rgba(0x204f1bff).into()), - terminal_ansi_bright_yellow: Some(rgba(0x4b4a17ff).into()), - terminal_ansi_bright_blue: Some(rgba(0x193385ff).into()), - terminal_ansi_bright_magenta: Some(rgba(0x810e60ff).into()), - terminal_ansi_bright_cyan: Some(rgba(0x1d4a56ff).into()), - terminal_ansi_bright_white: Some(rgba(0xf4fbf4ff).into()), + terminal_foreground: Some(rgba(0xf4fbf4ff).into()), + terminal_bright_foreground: Some(rgba(0xf4fbf4ff).into()), + terminal_dim_foreground: Some(rgba(0x131513ff).into()), terminal_ansi_black: Some(rgba(0x131513ff).into()), + terminal_ansi_bright_black: Some(rgba(0x667a66ff).into()), + terminal_ansi_dim_black: Some(rgba(0xf4fbf4ff).into()), terminal_ansi_red: Some(rgba(0xe61c3cff).into()), + terminal_ansi_bright_red: Some(rgba(0x840b21ff).into()), + terminal_ansi_dim_red: Some(rgba(0xff9d98ff).into()), terminal_ansi_green: Some(rgba(0x2ba32aff).into()), + terminal_ansi_bright_green: Some(rgba(0x204f1bff).into()), + terminal_ansi_dim_green: Some(rgba(0xa0d294ff).into()), terminal_ansi_yellow: Some(rgba(0x98981cff).into()), + terminal_ansi_bright_yellow: Some(rgba(0x4b4a17ff).into()), + terminal_ansi_dim_yellow: Some(rgba(0xd0ca90ff).into()), terminal_ansi_blue: Some(rgba(0x3e62f4ff).into()), + terminal_ansi_bright_blue: Some(rgba(0x193385ff).into()), + terminal_ansi_dim_blue: Some(rgba(0xb1adfcff).into()), terminal_ansi_magenta: Some(rgba(0xe61cc3ff).into()), + terminal_ansi_bright_magenta: Some(rgba(0x810e60ff).into()), + terminal_ansi_dim_magenta: Some(rgba(0xf9a1e1ff).into()), terminal_ansi_cyan: Some(rgba(0x1c99b3ff).into()), + terminal_ansi_bright_cyan: Some(rgba(0x1d4a56ff).into()), + terminal_ansi_dim_cyan: Some(rgba(0x9fccd9ff).into()), terminal_ansi_white: Some(rgba(0xf4fbf4ff).into()), + terminal_ansi_bright_white: Some(rgba(0xf4fbf4ff).into()), + terminal_ansi_dim_white: Some(rgba(0x829b82ff).into()), link_text_hover: Some(rgba(0x3e62f4ff).into()), ..Default::default() }, @@ -7981,22 +8168,33 @@ pub fn atelier() -> UserThemeFamily { editor_document_highlight_read_background: Some(rgba(0x3f62f41a).into()), editor_document_highlight_write_background: Some(rgba(0x748b7466).into()), terminal_background: Some(rgba(0xf4fbf4ff).into()), - terminal_ansi_bright_black: Some(rgba(0x829b82ff).into()), - terminal_ansi_bright_red: Some(rgba(0xff9d98ff).into()), - terminal_ansi_bright_green: Some(rgba(0xa0d294ff).into()), - terminal_ansi_bright_yellow: Some(rgba(0xd0ca90ff).into()), - terminal_ansi_bright_blue: Some(rgba(0xb1adfcff).into()), - terminal_ansi_bright_magenta: Some(rgba(0xf9a1e1ff).into()), - terminal_ansi_bright_cyan: Some(rgba(0x9fccd9ff).into()), - terminal_ansi_bright_white: Some(rgba(0x131513ff).into()), + terminal_foreground: Some(rgba(0x131513ff).into()), + terminal_bright_foreground: Some(rgba(0x131513ff).into()), + terminal_dim_foreground: Some(rgba(0xf4fbf4ff).into()), terminal_ansi_black: Some(rgba(0xf4fbf4ff).into()), + terminal_ansi_bright_black: Some(rgba(0x829b82ff).into()), + terminal_ansi_dim_black: Some(rgba(0x131513ff).into()), terminal_ansi_red: Some(rgba(0xe61c3dff).into()), + terminal_ansi_bright_red: Some(rgba(0xff9d98ff).into()), + terminal_ansi_dim_red: Some(rgba(0x840b21ff).into()), terminal_ansi_green: Some(rgba(0x2ba32bff).into()), + terminal_ansi_bright_green: Some(rgba(0xa0d294ff).into()), + terminal_ansi_dim_green: Some(rgba(0x204f1bff).into()), terminal_ansi_yellow: Some(rgba(0x98981dff).into()), + terminal_ansi_bright_yellow: Some(rgba(0xd0ca90ff).into()), + terminal_ansi_dim_yellow: Some(rgba(0x4b4a17ff).into()), terminal_ansi_blue: Some(rgba(0x3f62f4ff).into()), + terminal_ansi_bright_blue: Some(rgba(0xb1adfcff).into()), + terminal_ansi_dim_blue: Some(rgba(0x193385ff).into()), terminal_ansi_magenta: Some(rgba(0xe61dc3ff).into()), + terminal_ansi_bright_magenta: Some(rgba(0xf9a1e1ff).into()), + terminal_ansi_dim_magenta: Some(rgba(0x810e60ff).into()), terminal_ansi_cyan: Some(rgba(0x1d99b3ff).into()), + terminal_ansi_bright_cyan: Some(rgba(0x9fccd9ff).into()), + terminal_ansi_dim_cyan: Some(rgba(0x1d4a56ff).into()), terminal_ansi_white: Some(rgba(0x131513ff).into()), + terminal_ansi_bright_white: Some(rgba(0x131513ff).into()), + terminal_ansi_dim_white: Some(rgba(0x667a66ff).into()), link_text_hover: Some(rgba(0x3f62f4ff).into()), ..Default::default() }, @@ -8446,22 +8644,33 @@ pub fn atelier() -> UserThemeFamily { editor_document_highlight_read_background: Some(rgba(0x3e8fd01a).into()), editor_document_highlight_write_background: Some(rgba(0x7a819c66).into()), terminal_background: Some(rgba(0x202746ff).into()), - terminal_ansi_bright_black: Some(rgba(0x697192ff).into()), - terminal_ansi_bright_red: Some(rgba(0x6d2616ff).into()), - terminal_ansi_bright_green: Some(rgba(0x534921ff).into()), - terminal_ansi_bright_yellow: Some(rgba(0x63441eff).into()), - terminal_ansi_bright_blue: Some(rgba(0x274664ff).into()), - terminal_ansi_bright_magenta: Some(rgba(0x4c333dff).into()), - terminal_ansi_bright_cyan: Some(rgba(0x214e5fff).into()), - terminal_ansi_bright_white: Some(rgba(0xf5f7ffff).into()), + terminal_foreground: Some(rgba(0xf5f7ffff).into()), + terminal_bright_foreground: Some(rgba(0xf5f7ffff).into()), + terminal_dim_foreground: Some(rgba(0x202746ff).into()), terminal_ansi_black: Some(rgba(0x202746ff).into()), + terminal_ansi_bright_black: Some(rgba(0x697192ff).into()), + terminal_ansi_dim_black: Some(rgba(0xf5f7ffff).into()), terminal_ansi_red: Some(rgba(0xc94923ff).into()), + terminal_ansi_bright_red: Some(rgba(0x6d2616ff).into()), + terminal_ansi_dim_red: Some(rgba(0xefa58cff).into()), terminal_ansi_green: Some(rgba(0xac973aff).into()), + terminal_ansi_bright_green: Some(rgba(0x534921ff).into()), + terminal_ansi_dim_green: Some(rgba(0xd9ca9bff).into()), terminal_ansi_yellow: Some(rgba(0xc08b31ff).into()), + terminal_ansi_bright_yellow: Some(rgba(0x63441eff).into()), + terminal_ansi_dim_yellow: Some(rgba(0xe5c497ff).into()), terminal_ansi_blue: Some(rgba(0x3e8fd0ff).into()), + terminal_ansi_bright_blue: Some(rgba(0x274664ff).into()), + terminal_ansi_dim_blue: Some(rgba(0xa9c6e8ff).into()), terminal_ansi_magenta: Some(rgba(0x9c637aff).into()), + terminal_ansi_bright_magenta: Some(rgba(0x4c333dff).into()), + terminal_ansi_dim_magenta: Some(rgba(0xcfafbbff).into()), terminal_ansi_cyan: Some(rgba(0x25a2c9ff).into()), + terminal_ansi_bright_cyan: Some(rgba(0x214e5fff).into()), + terminal_ansi_dim_cyan: Some(rgba(0xa4d0e4ff).into()), terminal_ansi_white: Some(rgba(0xf5f7ffff).into()), + terminal_ansi_bright_white: Some(rgba(0xf5f7ffff).into()), + terminal_ansi_dim_white: Some(rgba(0x8b91a7ff).into()), link_text_hover: Some(rgba(0x3e8fd0ff).into()), ..Default::default() }, @@ -8911,22 +9120,33 @@ pub fn atelier() -> UserThemeFamily { editor_document_highlight_read_background: Some(rgba(0x3f8fd01a).into()), editor_document_highlight_write_background: Some(rgba(0x7a819c66).into()), terminal_background: Some(rgba(0xf5f7ffff).into()), - terminal_ansi_bright_black: Some(rgba(0x8b91a7ff).into()), - terminal_ansi_bright_red: Some(rgba(0xefa58cff).into()), - terminal_ansi_bright_green: Some(rgba(0xd9ca9bff).into()), - terminal_ansi_bright_yellow: Some(rgba(0xe5c497ff).into()), - terminal_ansi_bright_blue: Some(rgba(0xa9c6e8ff).into()), - terminal_ansi_bright_magenta: Some(rgba(0xcfafbbff).into()), - terminal_ansi_bright_cyan: Some(rgba(0xa4d0e4ff).into()), - terminal_ansi_bright_white: Some(rgba(0x202746ff).into()), + terminal_foreground: Some(rgba(0x202746ff).into()), + terminal_bright_foreground: Some(rgba(0x202746ff).into()), + terminal_dim_foreground: Some(rgba(0xf5f7ffff).into()), terminal_ansi_black: Some(rgba(0xf5f7ffff).into()), + terminal_ansi_bright_black: Some(rgba(0x8b91a7ff).into()), + terminal_ansi_dim_black: Some(rgba(0x202746ff).into()), terminal_ansi_red: Some(rgba(0xc94a23ff).into()), + terminal_ansi_bright_red: Some(rgba(0xefa58cff).into()), + terminal_ansi_dim_red: Some(rgba(0x6d2616ff).into()), terminal_ansi_green: Some(rgba(0xac973aff).into()), + terminal_ansi_bright_green: Some(rgba(0xd9ca9bff).into()), + terminal_ansi_dim_green: Some(rgba(0x534921ff).into()), terminal_ansi_yellow: Some(rgba(0xc08b31ff).into()), + terminal_ansi_bright_yellow: Some(rgba(0xe5c497ff).into()), + terminal_ansi_dim_yellow: Some(rgba(0x63441eff).into()), terminal_ansi_blue: Some(rgba(0x3f8fd0ff).into()), + terminal_ansi_bright_blue: Some(rgba(0xa9c6e8ff).into()), + terminal_ansi_dim_blue: Some(rgba(0x274664ff).into()), terminal_ansi_magenta: Some(rgba(0x9c637aff).into()), + terminal_ansi_bright_magenta: Some(rgba(0xcfafbbff).into()), + terminal_ansi_dim_magenta: Some(rgba(0x4c333dff).into()), terminal_ansi_cyan: Some(rgba(0x25a2c9ff).into()), + terminal_ansi_bright_cyan: Some(rgba(0xa4d0e4ff).into()), + terminal_ansi_dim_cyan: Some(rgba(0x214e5fff).into()), terminal_ansi_white: Some(rgba(0x202746ff).into()), + terminal_ansi_bright_white: Some(rgba(0x202746ff).into()), + terminal_ansi_dim_white: Some(rgba(0x697192ff).into()), link_text_hover: Some(rgba(0x3f8fd0ff).into()), ..Default::default() }, diff --git a/crates/theme/src/themes/ayu.rs b/crates/theme/src/themes/ayu.rs index 9c9030b2f27928c5e9fb16a6e536de84036c4f8e..6bde2410ae94f90fc9709c2027db3330c3e186af 100644 --- a/crates/theme/src/themes/ayu.rs +++ b/crates/theme/src/themes/ayu.rs @@ -76,22 +76,33 @@ pub fn ayu() -> UserThemeFamily { editor_document_highlight_read_background: Some(rgba(0x5ac2fe1a).into()), editor_document_highlight_write_background: Some(rgba(0x66676766).into()), terminal_background: Some(rgba(0x0d1017ff).into()), - terminal_ansi_bright_black: Some(rgba(0x545557ff).into()), - terminal_ansi_bright_red: Some(rgba(0x83363cff).into()), - terminal_ansi_bright_green: Some(rgba(0x567627ff).into()), - terminal_ansi_bright_yellow: Some(rgba(0x92592cff).into()), - terminal_ansi_bright_blue: Some(rgba(0x28628cff).into()), - terminal_ansi_bright_magenta: Some(rgba(0x205b78ff).into()), - terminal_ansi_bright_cyan: Some(rgba(0x4c806fff).into()), - terminal_ansi_bright_white: Some(rgba(0xbfbdb6ff).into()), + terminal_foreground: Some(rgba(0xbfbdb6ff).into()), + terminal_bright_foreground: Some(rgba(0xbfbdb6ff).into()), + terminal_dim_foreground: Some(rgba(0x0d1017ff).into()), terminal_ansi_black: Some(rgba(0x0d1017ff).into()), + terminal_ansi_bright_black: Some(rgba(0x545557ff).into()), + terminal_ansi_dim_black: Some(rgba(0xbfbdb6ff).into()), terminal_ansi_red: Some(rgba(0xef7178ff).into()), + terminal_ansi_bright_red: Some(rgba(0x83363cff).into()), + terminal_ansi_dim_red: Some(rgba(0xfebab9ff).into()), terminal_ansi_green: Some(rgba(0xaad84cff).into()), + terminal_ansi_bright_green: Some(rgba(0x567627ff).into()), + terminal_ansi_dim_green: Some(rgba(0xd8eca8ff).into()), terminal_ansi_yellow: Some(rgba(0xfeb454ff).into()), + terminal_ansi_bright_yellow: Some(rgba(0x92592cff).into()), + terminal_ansi_dim_yellow: Some(rgba(0xffd9aaff).into()), terminal_ansi_blue: Some(rgba(0x5ac2feff).into()), + terminal_ansi_bright_blue: Some(rgba(0x28628cff).into()), + terminal_ansi_dim_blue: Some(rgba(0xb8e0ffff).into()), terminal_ansi_magenta: Some(rgba(0x3abae5ff).into()), + terminal_ansi_bright_magenta: Some(rgba(0x205b78ff).into()), + terminal_ansi_dim_magenta: Some(rgba(0xaddcf3ff).into()), terminal_ansi_cyan: Some(rgba(0x95e5cbff).into()), + terminal_ansi_bright_cyan: Some(rgba(0x4c806fff).into()), + terminal_ansi_dim_cyan: Some(rgba(0xccf3e5ff).into()), terminal_ansi_white: Some(rgba(0xbfbdb6ff).into()), + terminal_ansi_bright_white: Some(rgba(0xbfbdb6ff).into()), + terminal_ansi_dim_white: Some(rgba(0x787876ff).into()), link_text_hover: Some(rgba(0x5ac2feff).into()), ..Default::default() }, @@ -520,22 +531,33 @@ pub fn ayu() -> UserThemeFamily { editor_document_highlight_read_background: Some(rgba(0x3b9ee51a).into()), editor_document_highlight_write_background: Some(rgba(0xacafb166).into()), terminal_background: Some(rgba(0xfcfcfcff).into()), - terminal_ansi_bright_black: Some(rgba(0xbcbec0ff).into()), - terminal_ansi_bright_red: Some(rgba(0xfebab6ff).into()), - terminal_ansi_bright_green: Some(rgba(0xc7d98fff).into()), - terminal_ansi_bright_yellow: Some(rgba(0xffd6a4ff).into()), - terminal_ansi_bright_blue: Some(rgba(0xaccef3ff).into()), - terminal_ansi_bright_magenta: Some(rgba(0xb2d9e9ff).into()), - terminal_ansi_bright_cyan: Some(rgba(0xace0cbff).into()), - terminal_ansi_bright_white: Some(rgba(0x5c6166ff).into()), + terminal_foreground: Some(rgba(0x5c6166ff).into()), + terminal_bright_foreground: Some(rgba(0x5c6166ff).into()), + terminal_dim_foreground: Some(rgba(0xfcfcfcff).into()), terminal_ansi_black: Some(rgba(0xfcfcfcff).into()), + terminal_ansi_bright_black: Some(rgba(0xbcbec0ff).into()), + terminal_ansi_dim_black: Some(rgba(0x5c6166ff).into()), terminal_ansi_red: Some(rgba(0xef7271ff).into()), + terminal_ansi_bright_red: Some(rgba(0xfebab6ff).into()), + terminal_ansi_dim_red: Some(rgba(0x833639ff).into()), terminal_ansi_green: Some(rgba(0x86b305ff).into()), + terminal_ansi_bright_green: Some(rgba(0xc7d98fff).into()), + terminal_ansi_dim_green: Some(rgba(0x445614ff).into()), terminal_ansi_yellow: Some(rgba(0xf1ae4aff).into()), + terminal_ansi_bright_yellow: Some(rgba(0xffd6a4ff).into()), + terminal_ansi_dim_yellow: Some(rgba(0x8a5328ff).into()), terminal_ansi_blue: Some(rgba(0x3b9ee5ff).into()), + terminal_ansi_bright_blue: Some(rgba(0xaccef3ff).into()), + terminal_ansi_dim_blue: Some(rgba(0x214d76ff).into()), terminal_ansi_magenta: Some(rgba(0x56b4d3ff).into()), + terminal_ansi_bright_magenta: Some(rgba(0xb2d9e9ff).into()), + terminal_ansi_dim_magenta: Some(rgba(0x2f5669ff).into()), terminal_ansi_cyan: Some(rgba(0x4dbf99ff).into()), + terminal_ansi_bright_cyan: Some(rgba(0xace0cbff).into()), + terminal_ansi_dim_cyan: Some(rgba(0x2a5f4aff).into()), terminal_ansi_white: Some(rgba(0x5c6166ff).into()), + terminal_ansi_bright_white: Some(rgba(0x5c6166ff).into()), + terminal_ansi_dim_white: Some(rgba(0x9c9fa2ff).into()), link_text_hover: Some(rgba(0x3b9ee5ff).into()), ..Default::default() }, @@ -964,22 +986,33 @@ pub fn ayu() -> UserThemeFamily { editor_document_highlight_read_background: Some(rgba(0x73cffe1a).into()), editor_document_highlight_write_background: Some(rgba(0x787a7c66).into()), terminal_background: Some(rgba(0x242936ff).into()), - terminal_ansi_bright_black: Some(rgba(0x67696eff).into()), - terminal_ansi_bright_red: Some(rgba(0x83403dff).into()), - terminal_ansi_bright_green: Some(rgba(0x76993dff).into()), - terminal_ansi_bright_yellow: Some(rgba(0x937238ff).into()), - terminal_ansi_bright_blue: Some(rgba(0x346e8dff).into()), - terminal_ansi_bright_magenta: Some(rgba(0x2b6c7bff).into()), - terminal_ansi_bright_cyan: Some(rgba(0x4c806fff).into()), - terminal_ansi_bright_white: Some(rgba(0xcccac2ff).into()), + terminal_foreground: Some(rgba(0xcccac2ff).into()), + terminal_bright_foreground: Some(rgba(0xcccac2ff).into()), + terminal_dim_foreground: Some(rgba(0x242936ff).into()), terminal_ansi_black: Some(rgba(0x242936ff).into()), + terminal_ansi_bright_black: Some(rgba(0x67696eff).into()), + terminal_ansi_dim_black: Some(rgba(0xcccac2ff).into()), terminal_ansi_red: Some(rgba(0xf18779ff).into()), + terminal_ansi_bright_red: Some(rgba(0x83403dff).into()), + terminal_ansi_dim_red: Some(rgba(0xfec4baff).into()), terminal_ansi_green: Some(rgba(0xd5fe80ff).into()), + terminal_ansi_bright_green: Some(rgba(0x76993dff).into()), + terminal_ansi_dim_green: Some(rgba(0xecffc1ff).into()), terminal_ansi_yellow: Some(rgba(0xfed073ff).into()), + terminal_ansi_bright_yellow: Some(rgba(0x937238ff).into()), + terminal_ansi_dim_yellow: Some(rgba(0xffe7b9ff).into()), terminal_ansi_blue: Some(rgba(0x73cffeff).into()), + terminal_ansi_bright_blue: Some(rgba(0x346e8dff).into()), + terminal_ansi_dim_blue: Some(rgba(0xc1e7ffff).into()), terminal_ansi_magenta: Some(rgba(0x5ccee5ff).into()), + terminal_ansi_bright_magenta: Some(rgba(0x2b6c7bff).into()), + terminal_ansi_dim_magenta: Some(rgba(0xb7e7f2ff).into()), terminal_ansi_cyan: Some(rgba(0x95e5cbff).into()), + terminal_ansi_bright_cyan: Some(rgba(0x4c806fff).into()), + terminal_ansi_dim_cyan: Some(rgba(0xccf3e5ff).into()), terminal_ansi_white: Some(rgba(0xcccac2ff).into()), + terminal_ansi_bright_white: Some(rgba(0xcccac2ff).into()), + terminal_ansi_dim_white: Some(rgba(0x898a8aff).into()), link_text_hover: Some(rgba(0x73cffeff).into()), ..Default::default() }, diff --git a/crates/theme/src/themes/gruvbox.rs b/crates/theme/src/themes/gruvbox.rs index 34ccefee1172cb9aee9d85496f2fdce40ab4abd2..83cab3a89dafbce187daa5325cef09319310cde3 100644 --- a/crates/theme/src/themes/gruvbox.rs +++ b/crates/theme/src/themes/gruvbox.rs @@ -76,22 +76,33 @@ pub fn gruvbox() -> UserThemeFamily { editor_document_highlight_read_background: Some(rgba(0x83a5981a).into()), editor_document_highlight_write_background: Some(rgba(0x92847466).into()), terminal_background: Some(rgba(0x282828ff).into()), - terminal_ansi_bright_black: Some(rgba(0x73675eff).into()), - terminal_ansi_bright_red: Some(rgba(0x93211eff).into()), - terminal_ansi_bright_green: Some(rgba(0x615d1bff).into()), - terminal_ansi_bright_yellow: Some(rgba(0x91611cff).into()), - terminal_ansi_bright_blue: Some(rgba(0x414f4aff).into()), - terminal_ansi_bright_magenta: Some(rgba(0x514a41ff).into()), - terminal_ansi_bright_cyan: Some(rgba(0x45603eff).into()), - terminal_ansi_bright_white: Some(rgba(0xfbf1c7ff).into()), + terminal_foreground: Some(rgba(0xfbf1c7ff).into()), + terminal_bright_foreground: Some(rgba(0xfbf1c7ff).into()), + terminal_dim_foreground: Some(rgba(0x282828ff).into()), terminal_ansi_black: Some(rgba(0x282828ff).into()), + terminal_ansi_bright_black: Some(rgba(0x73675eff).into()), + terminal_ansi_dim_black: Some(rgba(0xfbf1c7ff).into()), terminal_ansi_red: Some(rgba(0xfb4a35ff).into()), + terminal_ansi_bright_red: Some(rgba(0x93211eff).into()), + terminal_ansi_dim_red: Some(rgba(0xffaa95ff).into()), terminal_ansi_green: Some(rgba(0xb8bb27ff).into()), + terminal_ansi_bright_green: Some(rgba(0x615d1bff).into()), + terminal_ansi_dim_green: Some(rgba(0xe0dc98ff).into()), terminal_ansi_yellow: Some(rgba(0xf9bd30ff).into()), + terminal_ansi_bright_yellow: Some(rgba(0x91611cff).into()), + terminal_ansi_dim_yellow: Some(rgba(0xffdd9cff).into()), terminal_ansi_blue: Some(rgba(0x83a598ff).into()), + terminal_ansi_bright_blue: Some(rgba(0x414f4aff).into()), + terminal_ansi_dim_blue: Some(rgba(0xc0d2cbff).into()), terminal_ansi_magenta: Some(rgba(0xa89984ff).into()), + terminal_ansi_bright_magenta: Some(rgba(0x514a41ff).into()), + terminal_ansi_dim_magenta: Some(rgba(0xd3cbc0ff).into()), terminal_ansi_cyan: Some(rgba(0x8ec07cff).into()), + terminal_ansi_bright_cyan: Some(rgba(0x45603eff).into()), + terminal_ansi_dim_cyan: Some(rgba(0xc7dfbdff).into()), terminal_ansi_white: Some(rgba(0xfbf1c7ff).into()), + terminal_ansi_bright_white: Some(rgba(0xfbf1c7ff).into()), + terminal_ansi_dim_white: Some(rgba(0xb1a28aff).into()), link_text_hover: Some(rgba(0x83a598ff).into()), ..Default::default() }, @@ -527,22 +538,33 @@ pub fn gruvbox() -> UserThemeFamily { editor_document_highlight_read_background: Some(rgba(0x83a5981a).into()), editor_document_highlight_write_background: Some(rgba(0x92847466).into()), terminal_background: Some(rgba(0x1d2021ff).into()), - terminal_ansi_bright_black: Some(rgba(0x73675eff).into()), - terminal_ansi_bright_red: Some(rgba(0x93211eff).into()), - terminal_ansi_bright_green: Some(rgba(0x615d1bff).into()), - terminal_ansi_bright_yellow: Some(rgba(0x91611cff).into()), - terminal_ansi_bright_blue: Some(rgba(0x414f4aff).into()), - terminal_ansi_bright_magenta: Some(rgba(0x514a41ff).into()), - terminal_ansi_bright_cyan: Some(rgba(0x45603eff).into()), - terminal_ansi_bright_white: Some(rgba(0xfbf1c7ff).into()), + terminal_foreground: Some(rgba(0xfbf1c7ff).into()), + terminal_bright_foreground: Some(rgba(0xfbf1c7ff).into()), + terminal_dim_foreground: Some(rgba(0x1d2021ff).into()), terminal_ansi_black: Some(rgba(0x1d2021ff).into()), + terminal_ansi_bright_black: Some(rgba(0x73675eff).into()), + terminal_ansi_dim_black: Some(rgba(0xfbf1c7ff).into()), terminal_ansi_red: Some(rgba(0xfb4a35ff).into()), + terminal_ansi_bright_red: Some(rgba(0x93211eff).into()), + terminal_ansi_dim_red: Some(rgba(0xffaa95ff).into()), terminal_ansi_green: Some(rgba(0xb8bb27ff).into()), + terminal_ansi_bright_green: Some(rgba(0x615d1bff).into()), + terminal_ansi_dim_green: Some(rgba(0xe0dc98ff).into()), terminal_ansi_yellow: Some(rgba(0xf9bd30ff).into()), + terminal_ansi_bright_yellow: Some(rgba(0x91611cff).into()), + terminal_ansi_dim_yellow: Some(rgba(0xffdd9cff).into()), terminal_ansi_blue: Some(rgba(0x83a598ff).into()), + terminal_ansi_bright_blue: Some(rgba(0x414f4aff).into()), + terminal_ansi_dim_blue: Some(rgba(0xc0d2cbff).into()), terminal_ansi_magenta: Some(rgba(0xa89984ff).into()), + terminal_ansi_bright_magenta: Some(rgba(0x514a41ff).into()), + terminal_ansi_dim_magenta: Some(rgba(0xd3cbc0ff).into()), terminal_ansi_cyan: Some(rgba(0x8ec07cff).into()), + terminal_ansi_bright_cyan: Some(rgba(0x45603eff).into()), + terminal_ansi_dim_cyan: Some(rgba(0xc7dfbdff).into()), terminal_ansi_white: Some(rgba(0xfbf1c7ff).into()), + terminal_ansi_bright_white: Some(rgba(0xfbf1c7ff).into()), + terminal_ansi_dim_white: Some(rgba(0xb1a28aff).into()), link_text_hover: Some(rgba(0x83a598ff).into()), ..Default::default() }, @@ -978,22 +1000,33 @@ pub fn gruvbox() -> UserThemeFamily { editor_document_highlight_read_background: Some(rgba(0x83a5981a).into()), editor_document_highlight_write_background: Some(rgba(0x92847466).into()), terminal_background: Some(rgba(0x32302fff).into()), - terminal_ansi_bright_black: Some(rgba(0x73675eff).into()), - terminal_ansi_bright_red: Some(rgba(0x93211eff).into()), - terminal_ansi_bright_green: Some(rgba(0x615d1bff).into()), - terminal_ansi_bright_yellow: Some(rgba(0x91611cff).into()), - terminal_ansi_bright_blue: Some(rgba(0x414f4aff).into()), - terminal_ansi_bright_magenta: Some(rgba(0x514a41ff).into()), - terminal_ansi_bright_cyan: Some(rgba(0x45603eff).into()), - terminal_ansi_bright_white: Some(rgba(0xfbf1c7ff).into()), + terminal_foreground: Some(rgba(0xfbf1c7ff).into()), + terminal_bright_foreground: Some(rgba(0xfbf1c7ff).into()), + terminal_dim_foreground: Some(rgba(0x32302fff).into()), terminal_ansi_black: Some(rgba(0x32302fff).into()), + terminal_ansi_bright_black: Some(rgba(0x73675eff).into()), + terminal_ansi_dim_black: Some(rgba(0xfbf1c7ff).into()), terminal_ansi_red: Some(rgba(0xfb4a35ff).into()), + terminal_ansi_bright_red: Some(rgba(0x93211eff).into()), + terminal_ansi_dim_red: Some(rgba(0xffaa95ff).into()), terminal_ansi_green: Some(rgba(0xb8bb27ff).into()), + terminal_ansi_bright_green: Some(rgba(0x615d1bff).into()), + terminal_ansi_dim_green: Some(rgba(0xe0dc98ff).into()), terminal_ansi_yellow: Some(rgba(0xf9bd30ff).into()), + terminal_ansi_bright_yellow: Some(rgba(0x91611cff).into()), + terminal_ansi_dim_yellow: Some(rgba(0xffdd9cff).into()), terminal_ansi_blue: Some(rgba(0x83a598ff).into()), + terminal_ansi_bright_blue: Some(rgba(0x414f4aff).into()), + terminal_ansi_dim_blue: Some(rgba(0xc0d2cbff).into()), terminal_ansi_magenta: Some(rgba(0xa89984ff).into()), + terminal_ansi_bright_magenta: Some(rgba(0x514a41ff).into()), + terminal_ansi_dim_magenta: Some(rgba(0xd3cbc0ff).into()), terminal_ansi_cyan: Some(rgba(0x8ec07cff).into()), + terminal_ansi_bright_cyan: Some(rgba(0x45603eff).into()), + terminal_ansi_dim_cyan: Some(rgba(0xc7dfbdff).into()), terminal_ansi_white: Some(rgba(0xfbf1c7ff).into()), + terminal_ansi_bright_white: Some(rgba(0xfbf1c7ff).into()), + terminal_ansi_dim_white: Some(rgba(0xb1a28aff).into()), link_text_hover: Some(rgba(0x83a598ff).into()), ..Default::default() }, @@ -1429,22 +1462,33 @@ pub fn gruvbox() -> UserThemeFamily { editor_document_highlight_read_background: Some(rgba(0x0b66781a).into()), editor_document_highlight_write_background: Some(rgba(0x92847466).into()), terminal_background: Some(rgba(0xfbf1c7ff).into()), - terminal_ansi_bright_black: Some(rgba(0xb1a28aff).into()), - terminal_ansi_bright_red: Some(rgba(0xdc8c7bff).into()), - terminal_ansi_bright_green: Some(rgba(0xbfb787ff).into()), - terminal_ansi_bright_yellow: Some(rgba(0xe2b88bff).into()), - terminal_ansi_bright_blue: Some(rgba(0x8fb0baff).into()), - terminal_ansi_bright_magenta: Some(rgba(0xbcb5afff).into()), - terminal_ansi_bright_cyan: Some(rgba(0x9fbca8ff).into()), - terminal_ansi_bright_white: Some(rgba(0x282828ff).into()), + terminal_foreground: Some(rgba(0x282828ff).into()), + terminal_bright_foreground: Some(rgba(0x282828ff).into()), + terminal_dim_foreground: Some(rgba(0xfbf1c7ff).into()), terminal_ansi_black: Some(rgba(0xfbf1c7ff).into()), + terminal_ansi_bright_black: Some(rgba(0xb1a28aff).into()), + terminal_ansi_dim_black: Some(rgba(0x282828ff).into()), terminal_ansi_red: Some(rgba(0x9d0408ff).into()), + terminal_ansi_bright_red: Some(rgba(0xdc8c7bff).into()), + terminal_ansi_dim_red: Some(rgba(0x4f1207ff).into()), terminal_ansi_green: Some(rgba(0x797410ff).into()), + terminal_ansi_bright_green: Some(rgba(0xbfb787ff).into()), + terminal_ansi_dim_green: Some(rgba(0x3e3a11ff).into()), terminal_ansi_yellow: Some(rgba(0xb57616ff).into()), + terminal_ansi_bright_yellow: Some(rgba(0xe2b88bff).into()), + terminal_ansi_dim_yellow: Some(rgba(0x5c3b13ff).into()), terminal_ansi_blue: Some(rgba(0x0b6678ff).into()), + terminal_ansi_bright_blue: Some(rgba(0x8fb0baff).into()), + terminal_ansi_dim_blue: Some(rgba(0x14343cff).into()), terminal_ansi_magenta: Some(rgba(0x7c6f64ff).into()), + terminal_ansi_bright_magenta: Some(rgba(0xbcb5afff).into()), + terminal_ansi_dim_magenta: Some(rgba(0x3e3833ff).into()), terminal_ansi_cyan: Some(rgba(0x437b59ff).into()), + terminal_ansi_bright_cyan: Some(rgba(0x9fbca8ff).into()), + terminal_ansi_dim_cyan: Some(rgba(0x253e2eff).into()), terminal_ansi_white: Some(rgba(0x282828ff).into()), + terminal_ansi_bright_white: Some(rgba(0x282828ff).into()), + terminal_ansi_dim_white: Some(rgba(0x73675eff).into()), link_text_hover: Some(rgba(0x0b6678ff).into()), ..Default::default() }, @@ -1880,22 +1924,33 @@ pub fn gruvbox() -> UserThemeFamily { editor_document_highlight_read_background: Some(rgba(0x0b66781a).into()), editor_document_highlight_write_background: Some(rgba(0x92847466).into()), terminal_background: Some(rgba(0xf9f5d7ff).into()), - terminal_ansi_bright_black: Some(rgba(0xb1a28aff).into()), - terminal_ansi_bright_red: Some(rgba(0xdc8c7bff).into()), - terminal_ansi_bright_green: Some(rgba(0xbfb787ff).into()), - terminal_ansi_bright_yellow: Some(rgba(0xe2b88bff).into()), - terminal_ansi_bright_blue: Some(rgba(0x8fb0baff).into()), - terminal_ansi_bright_magenta: Some(rgba(0xbcb5afff).into()), - terminal_ansi_bright_cyan: Some(rgba(0x9fbca8ff).into()), - terminal_ansi_bright_white: Some(rgba(0x282828ff).into()), + terminal_foreground: Some(rgba(0x282828ff).into()), + terminal_bright_foreground: Some(rgba(0x282828ff).into()), + terminal_dim_foreground: Some(rgba(0xf9f5d7ff).into()), terminal_ansi_black: Some(rgba(0xf9f5d7ff).into()), + terminal_ansi_bright_black: Some(rgba(0xb1a28aff).into()), + terminal_ansi_dim_black: Some(rgba(0x282828ff).into()), terminal_ansi_red: Some(rgba(0x9d0408ff).into()), + terminal_ansi_bright_red: Some(rgba(0xdc8c7bff).into()), + terminal_ansi_dim_red: Some(rgba(0x4f1207ff).into()), terminal_ansi_green: Some(rgba(0x797410ff).into()), + terminal_ansi_bright_green: Some(rgba(0xbfb787ff).into()), + terminal_ansi_dim_green: Some(rgba(0x3e3a11ff).into()), terminal_ansi_yellow: Some(rgba(0xb57616ff).into()), + terminal_ansi_bright_yellow: Some(rgba(0xe2b88bff).into()), + terminal_ansi_dim_yellow: Some(rgba(0x5c3b13ff).into()), terminal_ansi_blue: Some(rgba(0x0b6678ff).into()), + terminal_ansi_bright_blue: Some(rgba(0x8fb0baff).into()), + terminal_ansi_dim_blue: Some(rgba(0x14343cff).into()), terminal_ansi_magenta: Some(rgba(0x7c6f64ff).into()), + terminal_ansi_bright_magenta: Some(rgba(0xbcb5afff).into()), + terminal_ansi_dim_magenta: Some(rgba(0x3e3833ff).into()), terminal_ansi_cyan: Some(rgba(0x437b59ff).into()), + terminal_ansi_bright_cyan: Some(rgba(0x9fbca8ff).into()), + terminal_ansi_dim_cyan: Some(rgba(0x253e2eff).into()), terminal_ansi_white: Some(rgba(0x282828ff).into()), + terminal_ansi_bright_white: Some(rgba(0x282828ff).into()), + terminal_ansi_dim_white: Some(rgba(0x73675eff).into()), link_text_hover: Some(rgba(0x0b6678ff).into()), ..Default::default() }, @@ -2331,22 +2386,33 @@ pub fn gruvbox() -> UserThemeFamily { editor_document_highlight_read_background: Some(rgba(0x0b66781a).into()), editor_document_highlight_write_background: Some(rgba(0x92847466).into()), terminal_background: Some(rgba(0xf2e5bcff).into()), - terminal_ansi_bright_black: Some(rgba(0xb1a28aff).into()), - terminal_ansi_bright_red: Some(rgba(0xdc8c7bff).into()), - terminal_ansi_bright_green: Some(rgba(0xbfb787ff).into()), - terminal_ansi_bright_yellow: Some(rgba(0xe2b88bff).into()), - terminal_ansi_bright_blue: Some(rgba(0x8fb0baff).into()), - terminal_ansi_bright_magenta: Some(rgba(0xbcb5afff).into()), - terminal_ansi_bright_cyan: Some(rgba(0x9fbca8ff).into()), - terminal_ansi_bright_white: Some(rgba(0x282828ff).into()), + terminal_foreground: Some(rgba(0x282828ff).into()), + terminal_bright_foreground: Some(rgba(0x282828ff).into()), + terminal_dim_foreground: Some(rgba(0xf2e5bcff).into()), terminal_ansi_black: Some(rgba(0xf2e5bcff).into()), + terminal_ansi_bright_black: Some(rgba(0xb1a28aff).into()), + terminal_ansi_dim_black: Some(rgba(0x282828ff).into()), terminal_ansi_red: Some(rgba(0x9d0408ff).into()), + terminal_ansi_bright_red: Some(rgba(0xdc8c7bff).into()), + terminal_ansi_dim_red: Some(rgba(0x4f1207ff).into()), terminal_ansi_green: Some(rgba(0x797410ff).into()), + terminal_ansi_bright_green: Some(rgba(0xbfb787ff).into()), + terminal_ansi_dim_green: Some(rgba(0x3e3a11ff).into()), terminal_ansi_yellow: Some(rgba(0xb57616ff).into()), + terminal_ansi_bright_yellow: Some(rgba(0xe2b88bff).into()), + terminal_ansi_dim_yellow: Some(rgba(0x5c3b13ff).into()), terminal_ansi_blue: Some(rgba(0x0b6678ff).into()), + terminal_ansi_bright_blue: Some(rgba(0x8fb0baff).into()), + terminal_ansi_dim_blue: Some(rgba(0x14343cff).into()), terminal_ansi_magenta: Some(rgba(0x7c6f64ff).into()), + terminal_ansi_bright_magenta: Some(rgba(0xbcb5afff).into()), + terminal_ansi_dim_magenta: Some(rgba(0x3e3833ff).into()), terminal_ansi_cyan: Some(rgba(0x437b59ff).into()), + terminal_ansi_bright_cyan: Some(rgba(0x9fbca8ff).into()), + terminal_ansi_dim_cyan: Some(rgba(0x253e2eff).into()), terminal_ansi_white: Some(rgba(0x282828ff).into()), + terminal_ansi_bright_white: Some(rgba(0x282828ff).into()), + terminal_ansi_dim_white: Some(rgba(0x73675eff).into()), link_text_hover: Some(rgba(0x0b6678ff).into()), ..Default::default() }, diff --git a/crates/theme/src/themes/one.rs b/crates/theme/src/themes/one.rs index 5928939f7a5a51607f20cb7779ce35ad93a1278c..b2227b18a3729cd9ede63df8cc5e81c835bf2ce0 100644 --- a/crates/theme/src/themes/one.rs +++ b/crates/theme/src/themes/one.rs @@ -76,22 +76,33 @@ pub fn one() -> UserThemeFamily { editor_document_highlight_read_background: Some(rgba(0x74ade81a).into()), editor_document_highlight_write_background: Some(rgba(0x555a6366).into()), terminal_background: Some(rgba(0x282c34ff).into()), - terminal_ansi_bright_black: Some(rgba(0x525661ff).into()), - terminal_ansi_bright_red: Some(rgba(0x673a3cff).into()), - terminal_ansi_bright_green: Some(rgba(0x4d6140ff).into()), - terminal_ansi_bright_yellow: Some(rgba(0x786441ff).into()), - terminal_ansi_bright_blue: Some(rgba(0x385378ff).into()), - terminal_ansi_bright_magenta: Some(rgba(0x5e2b26ff).into()), - terminal_ansi_bright_cyan: Some(rgba(0x3a565bff).into()), - terminal_ansi_bright_white: Some(rgba(0xc8ccd4ff).into()), + terminal_foreground: Some(rgba(0xc8ccd4ff).into()), + terminal_bright_foreground: Some(rgba(0xc8ccd4ff).into()), + terminal_dim_foreground: Some(rgba(0x282c34ff).into()), terminal_ansi_black: Some(rgba(0x282c34ff).into()), + terminal_ansi_bright_black: Some(rgba(0x525661ff).into()), + terminal_ansi_dim_black: Some(rgba(0xc8ccd4ff).into()), terminal_ansi_red: Some(rgba(0xd07277ff).into()), + terminal_ansi_bright_red: Some(rgba(0x673a3cff).into()), + terminal_ansi_dim_red: Some(rgba(0xebb8b9ff).into()), terminal_ansi_green: Some(rgba(0xa1c181ff).into()), + terminal_ansi_bright_green: Some(rgba(0x4d6140ff).into()), + terminal_ansi_dim_green: Some(rgba(0xd1e0bfff).into()), terminal_ansi_yellow: Some(rgba(0xdec184ff).into()), + terminal_ansi_bright_yellow: Some(rgba(0x786441ff).into()), + terminal_ansi_dim_yellow: Some(rgba(0xf1dfc1ff).into()), terminal_ansi_blue: Some(rgba(0x74ade8ff).into()), + terminal_ansi_bright_blue: Some(rgba(0x385378ff).into()), + terminal_ansi_dim_blue: Some(rgba(0xbed5f4ff).into()), terminal_ansi_magenta: Some(rgba(0xbe5046ff).into()), + terminal_ansi_bright_magenta: Some(rgba(0x5e2b26ff).into()), + terminal_ansi_dim_magenta: Some(rgba(0xe6a79eff).into()), terminal_ansi_cyan: Some(rgba(0x6fb4c0ff).into()), + terminal_ansi_bright_cyan: Some(rgba(0x3a565bff).into()), + terminal_ansi_dim_cyan: Some(rgba(0xb9d9dfff).into()), terminal_ansi_white: Some(rgba(0xc8ccd4ff).into()), + terminal_ansi_bright_white: Some(rgba(0xc8ccd4ff).into()), + terminal_ansi_dim_white: Some(rgba(0x575d65ff).into()), link_text_hover: Some(rgba(0x74ade8ff).into()), ..Default::default() }, @@ -527,22 +538,33 @@ pub fn one() -> UserThemeFamily { editor_document_highlight_read_background: Some(rgba(0x5c79e21a).into()), editor_document_highlight_write_background: Some(rgba(0xa3a3a466).into()), terminal_background: Some(rgba(0xfafafaff).into()), - terminal_ansi_bright_black: Some(rgba(0xaaaaaaff).into()), - terminal_ansi_bright_red: Some(rgba(0xf0b0a4ff).into()), - terminal_ansi_bright_green: Some(rgba(0xb2cfa9ff).into()), - terminal_ansi_bright_yellow: Some(rgba(0xf1dfc1ff).into()), - terminal_ansi_bright_blue: Some(rgba(0xb5baf2ff).into()), - terminal_ansi_bright_magenta: Some(rgba(0xcea6d3ff).into()), - terminal_ansi_bright_cyan: Some(rgba(0xa4bfdbff).into()), - terminal_ansi_bright_white: Some(rgba(0x383a41ff).into()), + terminal_foreground: Some(rgba(0x383a41ff).into()), + terminal_bright_foreground: Some(rgba(0x383a41ff).into()), + terminal_dim_foreground: Some(rgba(0xfafafaff).into()), terminal_ansi_black: Some(rgba(0xfafafaff).into()), + terminal_ansi_bright_black: Some(rgba(0xaaaaaaff).into()), + terminal_ansi_dim_black: Some(rgba(0x383a41ff).into()), terminal_ansi_red: Some(rgba(0xd36151ff).into()), + terminal_ansi_bright_red: Some(rgba(0xf0b0a4ff).into()), + terminal_ansi_dim_red: Some(rgba(0x6f312aff).into()), terminal_ansi_green: Some(rgba(0x669f59ff).into()), + terminal_ansi_bright_green: Some(rgba(0xb2cfa9ff).into()), + terminal_ansi_dim_green: Some(rgba(0x354d2eff).into()), terminal_ansi_yellow: Some(rgba(0xdec184ff).into()), + terminal_ansi_bright_yellow: Some(rgba(0xf1dfc1ff).into()), + terminal_ansi_dim_yellow: Some(rgba(0x786441ff).into()), terminal_ansi_blue: Some(rgba(0x5c79e2ff).into()), + terminal_ansi_bright_blue: Some(rgba(0xb5baf2ff).into()), + terminal_ansi_dim_blue: Some(rgba(0x2d3d75ff).into()), terminal_ansi_magenta: Some(rgba(0x994fa6ff).into()), + terminal_ansi_bright_magenta: Some(rgba(0xcea6d3ff).into()), + terminal_ansi_dim_magenta: Some(rgba(0x4b2a50ff).into()), terminal_ansi_cyan: Some(rgba(0x3b82b7ff).into()), + terminal_ansi_bright_cyan: Some(rgba(0xa4bfdbff).into()), + terminal_ansi_dim_cyan: Some(rgba(0x254058ff).into()), terminal_ansi_white: Some(rgba(0x383a41ff).into()), + terminal_ansi_bright_white: Some(rgba(0x383a41ff).into()), + terminal_ansi_dim_white: Some(rgba(0x98989bff).into()), link_text_hover: Some(rgba(0x5c79e2ff).into()), ..Default::default() }, diff --git a/crates/theme/src/themes/rose_pine.rs b/crates/theme/src/themes/rose_pine.rs index f654e4d9953cb5418eb3838b3ce27b8691a92913..4f68d606478afc9c6c3be7333780183ff4c1946b 100644 --- a/crates/theme/src/themes/rose_pine.rs +++ b/crates/theme/src/themes/rose_pine.rs @@ -76,22 +76,33 @@ pub fn rose_pine() -> UserThemeFamily { editor_document_highlight_read_background: Some(rgba(0x9cced71a).into()), editor_document_highlight_write_background: Some(rgba(0x28253c66).into()), terminal_background: Some(rgba(0x191724ff).into()), - terminal_ansi_bright_black: Some(rgba(0x403d55ff).into()), - terminal_ansi_bright_red: Some(rgba(0x7e3647ff).into()), - terminal_ansi_bright_green: Some(rgba(0x31614fff).into()), - terminal_ansi_bright_yellow: Some(rgba(0x8a653bff).into()), - terminal_ansi_bright_blue: Some(rgba(0x566c70ff).into()), - terminal_ansi_bright_magenta: Some(rgba(0x4c3b47ff).into()), - terminal_ansi_bright_cyan: Some(rgba(0x203a46ff).into()), - terminal_ansi_bright_white: Some(rgba(0xe0def4ff).into()), + terminal_foreground: Some(rgba(0xe0def4ff).into()), + terminal_bright_foreground: Some(rgba(0xe0def4ff).into()), + terminal_dim_foreground: Some(rgba(0x191724ff).into()), terminal_ansi_black: Some(rgba(0x191724ff).into()), + terminal_ansi_bright_black: Some(rgba(0x403d55ff).into()), + terminal_ansi_dim_black: Some(rgba(0xe0def4ff).into()), terminal_ansi_red: Some(rgba(0xea6f92ff).into()), + terminal_ansi_bright_red: Some(rgba(0x7e3647ff).into()), + terminal_ansi_dim_red: Some(rgba(0xfab9c7ff).into()), terminal_ansi_green: Some(rgba(0x5dc2a3ff).into()), + terminal_ansi_bright_green: Some(rgba(0x31614fff).into()), + terminal_ansi_dim_green: Some(rgba(0xb3e1d1ff).into()), terminal_ansi_yellow: Some(rgba(0xf5c177ff).into()), + terminal_ansi_bright_yellow: Some(rgba(0x8a653bff).into()), + terminal_ansi_dim_yellow: Some(rgba(0xfedfbbff).into()), terminal_ansi_blue: Some(rgba(0x9cced7ff).into()), + terminal_ansi_bright_blue: Some(rgba(0x566c70ff).into()), + terminal_ansi_dim_blue: Some(rgba(0xcfe7ebff).into()), terminal_ansi_magenta: Some(rgba(0x9d7691ff).into()), + terminal_ansi_bright_magenta: Some(rgba(0x4c3b47ff).into()), + terminal_ansi_dim_magenta: Some(rgba(0xceb9c7ff).into()), terminal_ansi_cyan: Some(rgba(0x32748fff).into()), + terminal_ansi_bright_cyan: Some(rgba(0x203a46ff).into()), + terminal_ansi_dim_cyan: Some(rgba(0x9cb7c6ff).into()), terminal_ansi_white: Some(rgba(0xe0def4ff).into()), + terminal_ansi_bright_white: Some(rgba(0xe0def4ff).into()), + terminal_ansi_dim_white: Some(rgba(0x514e68ff).into()), link_text_hover: Some(rgba(0x9cced7ff).into()), ..Default::default() }, @@ -534,22 +545,33 @@ pub fn rose_pine() -> UserThemeFamily { editor_document_highlight_read_background: Some(rgba(0x57949f1a).into()), editor_document_highlight_write_background: Some(rgba(0x9691a466).into()), terminal_background: Some(rgba(0xfaf4edff).into()), - terminal_ansi_bright_black: Some(rgba(0xb8b2baff).into()), - terminal_ansi_bright_red: Some(rgba(0xdcb0bbff).into()), - terminal_ansi_bright_green: Some(rgba(0xa5d5c5ff).into()), - terminal_ansi_bright_yellow: Some(rgba(0xfccd9bff).into()), - terminal_ansi_bright_blue: Some(rgba(0xacc9ceff).into()), - terminal_ansi_bright_magenta: Some(rgba(0xbcb1bdff).into()), - terminal_ansi_bright_cyan: Some(rgba(0x97b1c0ff).into()), - terminal_ansi_bright_white: Some(rgba(0x575279ff).into()), + terminal_foreground: Some(rgba(0x575279ff).into()), + terminal_bright_foreground: Some(rgba(0x575279ff).into()), + terminal_dim_foreground: Some(rgba(0xfaf4edff).into()), terminal_ansi_black: Some(rgba(0xfaf4edff).into()), + terminal_ansi_bright_black: Some(rgba(0xb8b2baff).into()), + terminal_ansi_dim_black: Some(rgba(0x575279ff).into()), terminal_ansi_red: Some(rgba(0xb4647aff).into()), + terminal_ansi_bright_red: Some(rgba(0xdcb0bbff).into()), + terminal_ansi_dim_red: Some(rgba(0x57333dff).into()), terminal_ansi_green: Some(rgba(0x3eaa8eff).into()), + terminal_ansi_bright_green: Some(rgba(0xa5d5c5ff).into()), + terminal_ansi_dim_green: Some(rgba(0x265245ff).into()), terminal_ansi_yellow: Some(rgba(0xe99d35ff).into()), + terminal_ansi_bright_yellow: Some(rgba(0xfccd9bff).into()), + terminal_ansi_dim_yellow: Some(rgba(0x854a1fff).into()), terminal_ansi_blue: Some(rgba(0x57949fff).into()), + terminal_ansi_bright_blue: Some(rgba(0xacc9ceff).into()), + terminal_ansi_dim_blue: Some(rgba(0x2f484dff).into()), terminal_ansi_magenta: Some(rgba(0x7c697fff).into()), + terminal_ansi_bright_magenta: Some(rgba(0xbcb1bdff).into()), + terminal_ansi_dim_magenta: Some(rgba(0x3e353fff).into()), terminal_ansi_cyan: Some(rgba(0x2a6983ff).into()), + terminal_ansi_bright_cyan: Some(rgba(0x97b1c0ff).into()), + terminal_ansi_dim_cyan: Some(rgba(0x1c3641ff).into()), terminal_ansi_white: Some(rgba(0x575279ff).into()), + terminal_ansi_bright_white: Some(rgba(0x575279ff).into()), + terminal_ansi_dim_white: Some(rgba(0x827e98ff).into()), link_text_hover: Some(rgba(0x57949fff).into()), ..Default::default() }, @@ -992,22 +1014,33 @@ pub fn rose_pine() -> UserThemeFamily { editor_document_highlight_read_background: Some(rgba(0x9cced71a).into()), editor_document_highlight_write_background: Some(rgba(0x59557166).into()), terminal_background: Some(rgba(0x232136ff).into()), - terminal_ansi_bright_black: Some(rgba(0x3f3b58ff).into()), - terminal_ansi_bright_red: Some(rgba(0x7e3647ff).into()), - terminal_ansi_bright_green: Some(rgba(0x31614fff).into()), - terminal_ansi_bright_yellow: Some(rgba(0x8a653bff).into()), - terminal_ansi_bright_blue: Some(rgba(0x566c70ff).into()), - terminal_ansi_bright_magenta: Some(rgba(0x51414eff).into()), - terminal_ansi_bright_cyan: Some(rgba(0x264654ff).into()), - terminal_ansi_bright_white: Some(rgba(0xe0def4ff).into()), + terminal_foreground: Some(rgba(0xe0def4ff).into()), + terminal_bright_foreground: Some(rgba(0xe0def4ff).into()), + terminal_dim_foreground: Some(rgba(0x232136ff).into()), terminal_ansi_black: Some(rgba(0x232136ff).into()), + terminal_ansi_bright_black: Some(rgba(0x3f3b58ff).into()), + terminal_ansi_dim_black: Some(rgba(0xe0def4ff).into()), terminal_ansi_red: Some(rgba(0xea6f92ff).into()), + terminal_ansi_bright_red: Some(rgba(0x7e3647ff).into()), + terminal_ansi_dim_red: Some(rgba(0xfab9c7ff).into()), terminal_ansi_green: Some(rgba(0x5dc2a3ff).into()), + terminal_ansi_bright_green: Some(rgba(0x31614fff).into()), + terminal_ansi_dim_green: Some(rgba(0xb3e1d1ff).into()), terminal_ansi_yellow: Some(rgba(0xf5c177ff).into()), + terminal_ansi_bright_yellow: Some(rgba(0x8a653bff).into()), + terminal_ansi_dim_yellow: Some(rgba(0xfedfbbff).into()), terminal_ansi_blue: Some(rgba(0x9cced7ff).into()), + terminal_ansi_bright_blue: Some(rgba(0x566c70ff).into()), + terminal_ansi_dim_blue: Some(rgba(0xcfe7ebff).into()), terminal_ansi_magenta: Some(rgba(0xa784a1ff).into()), + terminal_ansi_bright_magenta: Some(rgba(0x51414eff).into()), + terminal_ansi_dim_magenta: Some(rgba(0xd3c0cfff).into()), terminal_ansi_cyan: Some(rgba(0x3f8fb0ff).into()), + terminal_ansi_bright_cyan: Some(rgba(0x264654ff).into()), + terminal_ansi_dim_cyan: Some(rgba(0xa5c6d7ff).into()), terminal_ansi_white: Some(rgba(0xe0def4ff).into()), + terminal_ansi_bright_white: Some(rgba(0xe0def4ff).into()), + terminal_ansi_dim_white: Some(rgba(0x75718eff).into()), link_text_hover: Some(rgba(0x9cced7ff).into()), ..Default::default() }, diff --git a/crates/theme/src/themes/sandcastle.rs b/crates/theme/src/themes/sandcastle.rs index ccf49061014ccf4dbfe4c71393aa9d433e1f63f3..a672f702e13f4f1316c29d1dae32ee4c3f5c6edf 100644 --- a/crates/theme/src/themes/sandcastle.rs +++ b/crates/theme/src/themes/sandcastle.rs @@ -75,22 +75,33 @@ pub fn sandcastle() -> UserThemeFamily { editor_document_highlight_read_background: Some(rgba(0x528b8b1a).into()), editor_document_highlight_write_background: Some(rgba(0x7c6f6466).into()), terminal_background: Some(rgba(0x282c34ff).into()), - terminal_ansi_bright_black: Some(rgba(0x5e5753ff).into()), - terminal_ansi_bright_red: Some(rgba(0x57333dff).into()), - terminal_ansi_bright_green: Some(rgba(0x414f4aff).into()), - terminal_ansi_bright_yellow: Some(rgba(0x4e3f22ff).into()), - terminal_ansi_bright_blue: Some(rgba(0x2c4444ff).into()), - terminal_ansi_bright_magenta: Some(rgba(0x523a18ff).into()), - terminal_ansi_bright_cyan: Some(rgba(0x414f4aff).into()), - terminal_ansi_bright_white: Some(rgba(0xfdf4c1ff).into()), + terminal_foreground: Some(rgba(0xfdf4c1ff).into()), + terminal_bright_foreground: Some(rgba(0xfdf4c1ff).into()), + terminal_dim_foreground: Some(rgba(0x282c34ff).into()), terminal_ansi_black: Some(rgba(0x282c34ff).into()), + terminal_ansi_bright_black: Some(rgba(0x5e5753ff).into()), + terminal_ansi_dim_black: Some(rgba(0xfdf4c1ff).into()), terminal_ansi_red: Some(rgba(0xb4637aff).into()), + terminal_ansi_bright_red: Some(rgba(0x57333dff).into()), + terminal_ansi_dim_red: Some(rgba(0xdcb0bbff).into()), terminal_ansi_green: Some(rgba(0x83a598ff).into()), + terminal_ansi_bright_green: Some(rgba(0x414f4aff).into()), + terminal_ansi_dim_green: Some(rgba(0xc0d2cbff).into()), terminal_ansi_yellow: Some(rgba(0xa07e3bff).into()), + terminal_ansi_bright_yellow: Some(rgba(0x4e3f22ff).into()), + terminal_ansi_dim_yellow: Some(rgba(0xd3bd9aff).into()), terminal_ansi_blue: Some(rgba(0x528b8bff).into()), + terminal_ansi_bright_blue: Some(rgba(0x2c4444ff).into()), + terminal_ansi_dim_blue: Some(rgba(0xa8c4c4ff).into()), terminal_ansi_magenta: Some(rgba(0xa87323ff).into()), + terminal_ansi_bright_magenta: Some(rgba(0x523a18ff).into()), + terminal_ansi_dim_magenta: Some(rgba(0xdab78eff).into()), terminal_ansi_cyan: Some(rgba(0x83a598ff).into()), + terminal_ansi_bright_cyan: Some(rgba(0x414f4aff).into()), + terminal_ansi_dim_cyan: Some(rgba(0xc0d2cbff).into()), terminal_ansi_white: Some(rgba(0xfdf4c1ff).into()), + terminal_ansi_bright_white: Some(rgba(0xfdf4c1ff).into()), + terminal_ansi_dim_white: Some(rgba(0x968777ff).into()), link_text_hover: Some(rgba(0x528b8bff).into()), ..Default::default() }, diff --git a/crates/theme/src/themes/solarized.rs b/crates/theme/src/themes/solarized.rs index b903d645396e78c9a2a3a1ebfe0c0f714b59e7d5..ddf6ae8e081faae5d43a2b6750cda2e7198f9293 100644 --- a/crates/theme/src/themes/solarized.rs +++ b/crates/theme/src/themes/solarized.rs @@ -76,22 +76,33 @@ pub fn solarized() -> UserThemeFamily { editor_document_highlight_read_background: Some(rgba(0x288bd11a).into()), editor_document_highlight_write_background: Some(rgba(0x6d828866).into()), terminal_background: Some(rgba(0x002b36ff).into()), - terminal_ansi_bright_black: Some(rgba(0x5c7279ff).into()), - terminal_ansi_bright_red: Some(rgba(0x7d181cff).into()), - terminal_ansi_bright_green: Some(rgba(0x434a11ff).into()), - terminal_ansi_bright_yellow: Some(rgba(0x5d4310ff).into()), - terminal_ansi_bright_blue: Some(rgba(0x214465ff).into()), - terminal_ansi_bright_magenta: Some(rgba(0x6f1f40ff).into()), - terminal_ansi_bright_cyan: Some(rgba(0x204e4aff).into()), - terminal_ansi_bright_white: Some(rgba(0xfdf6e3ff).into()), + terminal_foreground: Some(rgba(0xfdf6e3ff).into()), + terminal_bright_foreground: Some(rgba(0xfdf6e3ff).into()), + terminal_dim_foreground: Some(rgba(0x002b36ff).into()), terminal_ansi_black: Some(rgba(0x002b36ff).into()), + terminal_ansi_bright_black: Some(rgba(0x5c7279ff).into()), + terminal_ansi_dim_black: Some(rgba(0xfdf6e3ff).into()), terminal_ansi_red: Some(rgba(0xdc3330ff).into()), + terminal_ansi_bright_red: Some(rgba(0x7d181cff).into()), + terminal_ansi_dim_red: Some(rgba(0xfaa091ff).into()), terminal_ansi_green: Some(rgba(0x859904ff).into()), + terminal_ansi_bright_green: Some(rgba(0x434a11ff).into()), + terminal_ansi_dim_green: Some(rgba(0xc6cb8bff).into()), terminal_ansi_yellow: Some(rgba(0xb58903ff).into()), + terminal_ansi_bright_yellow: Some(rgba(0x5d4310ff).into()), + terminal_ansi_dim_yellow: Some(rgba(0xe1c28aff).into()), terminal_ansi_blue: Some(rgba(0x288bd1ff).into()), + terminal_ansi_bright_blue: Some(rgba(0x214465ff).into()), + terminal_ansi_dim_blue: Some(rgba(0xa5c3e9ff).into()), terminal_ansi_magenta: Some(rgba(0xd33782ff).into()), + terminal_ansi_bright_magenta: Some(rgba(0x6f1f40ff).into()), + terminal_ansi_dim_magenta: Some(rgba(0xf0a2bfff).into()), terminal_ansi_cyan: Some(rgba(0x2ca198ff).into()), + terminal_ansi_bright_cyan: Some(rgba(0x204e4aff).into()), + terminal_ansi_dim_cyan: Some(rgba(0x9fd0cbff).into()), terminal_ansi_white: Some(rgba(0xfdf6e3ff).into()), + terminal_ansi_bright_white: Some(rgba(0xfdf6e3ff).into()), + terminal_ansi_dim_white: Some(rgba(0x7b8e91ff).into()), link_text_hover: Some(rgba(0x288bd1ff).into()), ..Default::default() }, @@ -520,22 +531,33 @@ pub fn solarized() -> UserThemeFamily { editor_document_highlight_read_background: Some(rgba(0x298bd11a).into()), editor_document_highlight_write_background: Some(rgba(0x6d828866).into()), terminal_background: Some(rgba(0xfdf6e3ff).into()), - terminal_ansi_bright_black: Some(rgba(0x7b8e91ff).into()), - terminal_ansi_bright_red: Some(rgba(0xfaa091ff).into()), - terminal_ansi_bright_green: Some(rgba(0xc6cb8bff).into()), - terminal_ansi_bright_yellow: Some(rgba(0xe1c28aff).into()), - terminal_ansi_bright_blue: Some(rgba(0xa5c3e9ff).into()), - terminal_ansi_bright_magenta: Some(rgba(0xf0a2bfff).into()), - terminal_ansi_bright_cyan: Some(rgba(0x9fd0cbff).into()), - terminal_ansi_bright_white: Some(rgba(0x002b36ff).into()), + terminal_foreground: Some(rgba(0x002b36ff).into()), + terminal_bright_foreground: Some(rgba(0x002b36ff).into()), + terminal_dim_foreground: Some(rgba(0xfdf6e3ff).into()), terminal_ansi_black: Some(rgba(0xfdf6e3ff).into()), + terminal_ansi_bright_black: Some(rgba(0x7b8e91ff).into()), + terminal_ansi_dim_black: Some(rgba(0x002b36ff).into()), terminal_ansi_red: Some(rgba(0xdc3330ff).into()), + terminal_ansi_bright_red: Some(rgba(0xfaa091ff).into()), + terminal_ansi_dim_red: Some(rgba(0x7d181cff).into()), terminal_ansi_green: Some(rgba(0x859904ff).into()), + terminal_ansi_bright_green: Some(rgba(0xc6cb8bff).into()), + terminal_ansi_dim_green: Some(rgba(0x434a11ff).into()), terminal_ansi_yellow: Some(rgba(0xb58904ff).into()), + terminal_ansi_bright_yellow: Some(rgba(0xe1c28aff).into()), + terminal_ansi_dim_yellow: Some(rgba(0x5d4310ff).into()), terminal_ansi_blue: Some(rgba(0x298bd1ff).into()), + terminal_ansi_bright_blue: Some(rgba(0xa5c3e9ff).into()), + terminal_ansi_dim_blue: Some(rgba(0x214465ff).into()), terminal_ansi_magenta: Some(rgba(0xd33882ff).into()), + terminal_ansi_bright_magenta: Some(rgba(0xf0a2bfff).into()), + terminal_ansi_dim_magenta: Some(rgba(0x6f1f40ff).into()), terminal_ansi_cyan: Some(rgba(0x2ca198ff).into()), + terminal_ansi_bright_cyan: Some(rgba(0x9fd0cbff).into()), + terminal_ansi_dim_cyan: Some(rgba(0x204e4aff).into()), terminal_ansi_white: Some(rgba(0x002b36ff).into()), + terminal_ansi_bright_white: Some(rgba(0x002b36ff).into()), + terminal_ansi_dim_white: Some(rgba(0x5c7279ff).into()), link_text_hover: Some(rgba(0x298bd1ff).into()), ..Default::default() }, diff --git a/crates/theme/src/themes/summercamp.rs b/crates/theme/src/themes/summercamp.rs index 0a580e6893578d846b475493897e294aafaf74eb..5f5a922b0b2cf47ed10570a5748c765c79a2631a 100644 --- a/crates/theme/src/themes/summercamp.rs +++ b/crates/theme/src/themes/summercamp.rs @@ -75,22 +75,33 @@ pub fn summercamp() -> UserThemeFamily { editor_document_highlight_read_background: Some(rgba(0x499bef1a).into()), editor_document_highlight_write_background: Some(rgba(0x49443366).into()), terminal_background: Some(rgba(0x1c1810ff).into()), - terminal_ansi_bright_black: Some(rgba(0x3b3627ff).into()), - terminal_ansi_bright_red: Some(rgba(0x7f2724ff).into()), - terminal_ansi_bright_green: Some(rgba(0x28842cff).into()), - terminal_ansi_bright_yellow: Some(rgba(0x8c9a10ff).into()), - terminal_ansi_bright_blue: Some(rgba(0x234b7fff).into()), - terminal_ansi_bright_magenta: Some(rgba(0x88487eff).into()), - terminal_ansi_bright_cyan: Some(rgba(0x298462ff).into()), - terminal_ansi_bright_white: Some(rgba(0xf8f5deff).into()), + terminal_foreground: Some(rgba(0xf8f5deff).into()), + terminal_bright_foreground: Some(rgba(0xf8f5deff).into()), + terminal_dim_foreground: Some(rgba(0x1c1810ff).into()), terminal_ansi_black: Some(rgba(0x1c1810ff).into()), + terminal_ansi_bright_black: Some(rgba(0x3b3627ff).into()), + terminal_ansi_dim_black: Some(rgba(0xf8f5deff).into()), terminal_ansi_red: Some(rgba(0xe35142ff).into()), + terminal_ansi_bright_red: Some(rgba(0x7f2724ff).into()), + terminal_ansi_dim_red: Some(rgba(0xfbab9cff).into()), terminal_ansi_green: Some(rgba(0x5dea5aff).into()), + terminal_ansi_bright_green: Some(rgba(0x28842cff).into()), + terminal_ansi_dim_green: Some(rgba(0xb9f7aeff).into()), terminal_ansi_yellow: Some(rgba(0xf1fe29ff).into()), + terminal_ansi_bright_yellow: Some(rgba(0x8c9a10ff).into()), + terminal_ansi_dim_yellow: Some(rgba(0xffffa2ff).into()), terminal_ansi_blue: Some(rgba(0x499befff).into()), + terminal_ansi_bright_blue: Some(rgba(0x234b7fff).into()), + terminal_ansi_dim_blue: Some(rgba(0xb1ccf8ff).into()), terminal_ansi_magenta: Some(rgba(0xf59be6ff).into()), + terminal_ansi_bright_magenta: Some(rgba(0x88487eff).into()), + terminal_ansi_dim_magenta: Some(rgba(0xfccef3ff).into()), terminal_ansi_cyan: Some(rgba(0x5beabcff).into()), + terminal_ansi_bright_cyan: Some(rgba(0x298462ff).into()), + terminal_ansi_dim_cyan: Some(rgba(0xb7f6ddff).into()), terminal_ansi_white: Some(rgba(0xf8f5deff).into()), + terminal_ansi_bright_white: Some(rgba(0xf8f5deff).into()), + terminal_ansi_dim_white: Some(rgba(0x57533fff).into()), link_text_hover: Some(rgba(0x499befff).into()), ..Default::default() }, diff --git a/crates/theme_importer/src/theme_printer.rs b/crates/theme_importer/src/theme_printer.rs index f708e4305edf71eb914bf445370758bf0d624e42..88a860034d6cd6c6bf3f124f7788cade0ed78953 100644 --- a/crates/theme_importer/src/theme_printer.rs +++ b/crates/theme_importer/src/theme_printer.rs @@ -244,43 +244,60 @@ impl<'a> Debug for ThemeColorsRefinementPrinter<'a> { self.0.editor_document_highlight_write_background, ), ("terminal_background", self.0.terminal_background), + ("terminal_foreground", self.0.terminal_foreground), + ( + "terminal_bright_foreground", + self.0.terminal_bright_foreground, + ), + ("terminal_dim_foreground", self.0.terminal_dim_foreground), + ("terminal_ansi_black", self.0.terminal_ansi_black), ( "terminal_ansi_bright_black", self.0.terminal_ansi_bright_black, ), + ("terminal_ansi_dim_black", self.0.terminal_ansi_dim_black), + ("terminal_ansi_red", self.0.terminal_ansi_red), ("terminal_ansi_bright_red", self.0.terminal_ansi_bright_red), + ("terminal_ansi_dim_red", self.0.terminal_ansi_dim_red), + ("terminal_ansi_green", self.0.terminal_ansi_green), ( "terminal_ansi_bright_green", self.0.terminal_ansi_bright_green, ), + ("terminal_ansi_dim_green", self.0.terminal_ansi_dim_green), + ("terminal_ansi_yellow", self.0.terminal_ansi_yellow), ( "terminal_ansi_bright_yellow", self.0.terminal_ansi_bright_yellow, ), + ("terminal_ansi_dim_yellow", self.0.terminal_ansi_dim_yellow), + ("terminal_ansi_blue", self.0.terminal_ansi_blue), ( "terminal_ansi_bright_blue", self.0.terminal_ansi_bright_blue, ), + ("terminal_ansi_dim_blue", self.0.terminal_ansi_dim_blue), + ("terminal_ansi_magenta", self.0.terminal_ansi_magenta), ( "terminal_ansi_bright_magenta", self.0.terminal_ansi_bright_magenta, ), + ( + "terminal_ansi_dim_magenta", + self.0.terminal_ansi_dim_magenta, + ), + ("terminal_ansi_cyan", self.0.terminal_ansi_cyan), ( "terminal_ansi_bright_cyan", self.0.terminal_ansi_bright_cyan, ), + ("terminal_ansi_dim_cyan", self.0.terminal_ansi_dim_cyan), + ("terminal_ansi_white", self.0.terminal_ansi_white), ( "terminal_ansi_bright_white", self.0.terminal_ansi_bright_white, ), - ("terminal_ansi_black", self.0.terminal_ansi_black), - ("terminal_ansi_red", self.0.terminal_ansi_red), - ("terminal_ansi_green", self.0.terminal_ansi_green), - ("terminal_ansi_yellow", self.0.terminal_ansi_yellow), - ("terminal_ansi_blue", self.0.terminal_ansi_blue), - ("terminal_ansi_magenta", self.0.terminal_ansi_magenta), - ("terminal_ansi_cyan", self.0.terminal_ansi_cyan), - ("terminal_ansi_white", self.0.terminal_ansi_white), + ("terminal_ansi_dim_white", self.0.terminal_ansi_dim_white), ("link_text_hover", self.0.link_text_hover), ]; diff --git a/crates/theme_importer/src/zed1/converter.rs b/crates/theme_importer/src/zed1/converter.rs index 2f640c799f99ddf21989890c2e7a37a2a332b791..3c5f2498404242505ea10a2da505e23ac06cb82a 100644 --- a/crates/theme_importer/src/zed1/converter.rs +++ b/crates/theme_importer/src/zed1/converter.rs @@ -250,22 +250,33 @@ impl Zed1ThemeConverter { editor.document_highlight_write_background, ), terminal_background: convert(terminal.background), - terminal_ansi_bright_black: convert(terminal.bright_black), - terminal_ansi_bright_red: convert(terminal.bright_red), - terminal_ansi_bright_green: convert(terminal.bright_green), - terminal_ansi_bright_yellow: convert(terminal.bright_yellow), - terminal_ansi_bright_blue: convert(terminal.bright_blue), - terminal_ansi_bright_magenta: convert(terminal.bright_magenta), - terminal_ansi_bright_cyan: convert(terminal.bright_cyan), - terminal_ansi_bright_white: convert(terminal.bright_white), + terminal_foreground: convert(terminal.foreground), + terminal_bright_foreground: convert(terminal.bright_foreground), + terminal_dim_foreground: convert(terminal.dim_foreground), terminal_ansi_black: convert(terminal.black), + terminal_ansi_bright_black: convert(terminal.bright_black), + terminal_ansi_dim_black: convert(terminal.dim_black), terminal_ansi_red: convert(terminal.red), + terminal_ansi_bright_red: convert(terminal.bright_red), + terminal_ansi_dim_red: convert(terminal.dim_red), terminal_ansi_green: convert(terminal.green), + terminal_ansi_bright_green: convert(terminal.bright_green), + terminal_ansi_dim_green: convert(terminal.dim_green), terminal_ansi_yellow: convert(terminal.yellow), + terminal_ansi_bright_yellow: convert(terminal.bright_yellow), + terminal_ansi_dim_yellow: convert(terminal.dim_yellow), terminal_ansi_blue: convert(terminal.blue), + terminal_ansi_bright_blue: convert(terminal.bright_blue), + terminal_ansi_dim_blue: convert(terminal.dim_blue), terminal_ansi_magenta: convert(terminal.magenta), + terminal_ansi_bright_magenta: convert(terminal.bright_magenta), + terminal_ansi_dim_magenta: convert(terminal.dim_magenta), terminal_ansi_cyan: convert(terminal.cyan), + terminal_ansi_bright_cyan: convert(terminal.bright_cyan), + terminal_ansi_dim_cyan: convert(terminal.dim_cyan), terminal_ansi_white: convert(terminal.white), + terminal_ansi_bright_white: convert(terminal.bright_white), + terminal_ansi_dim_white: convert(terminal.dim_white), link_text_hover: convert(highest.accent.default.foreground), }) } diff --git a/crates/ui/src/components/avatar/avatar.rs b/crates/ui/src/components/avatar/avatar.rs index 932cc9e243558fb78cbf6e15340974a0551699c9..54f5557e358e4b695db556fe308c5a0f94666d34 100644 --- a/crates/ui/src/components/avatar/avatar.rs +++ b/crates/ui/src/components/avatar/avatar.rs @@ -44,7 +44,7 @@ impl Avatar { /// Sets the shape of the avatar image. /// - /// This method allows the shape of the avatar to be specified using a [`Shape`]. + /// This method allows the shape of the avatar to be specified using an [`AvatarShape`]. /// It modifies the corner radius of the image to match the specified shape. /// /// # Examples diff --git a/crates/ui/src/components/avatar/avatar_availability_indicator.rs b/crates/ui/src/components/avatar/avatar_availability_indicator.rs index 3a033cd3959c5d63af5960ba839fb37950fbec57..3e4f9b2d1bc7d00a3383b6b1ebe1bf11ec2195a1 100644 --- a/crates/ui/src/components/avatar/avatar_availability_indicator.rs +++ b/crates/ui/src/components/avatar/avatar_availability_indicator.rs @@ -20,7 +20,7 @@ impl AvatarAvailabilityIndicator { } } - /// Sets the size of the [`Avatar`] this indicator appears on. + /// Sets the size of the [`Avatar`](crate::Avatar) this indicator appears on. pub fn avatar_size(mut self, size: impl Into>) -> Self { self.avatar_size = size.into(); self diff --git a/crates/ui/src/components/button/button_like.rs b/crates/ui/src/components/button/button_like.rs index aafb33cd6f541a7573f7910fc559a8d170416689..80dd7b5ce71a8e2632afc6a2f58b33d26ed59ca6 100644 --- a/crates/ui/src/components/button/button_like.rs +++ b/crates/ui/src/components/button/button_like.rs @@ -407,8 +407,8 @@ impl VisibleOnHover for ButtonLike { } impl ParentElement for ButtonLike { - fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { - &mut self.children + fn extend(&mut self, elements: impl Iterator) { + self.children.extend(elements) } } diff --git a/crates/ui/src/components/context_menu.rs b/crates/ui/src/components/context_menu.rs index 470483cc0a713cbfc53d1a9e7db343a604918a2e..e3a7aeefda4ab8ab8ea6c583ba59b2549c98a5f6 100644 --- a/crates/ui/src/components/context_menu.rs +++ b/crates/ui/src/components/context_menu.rs @@ -27,6 +27,7 @@ enum ContextMenuItem { pub struct ContextMenu { items: Vec, focus_handle: FocusHandle, + action_context: Option, selected_index: Option, delayed: bool, clicked: bool, @@ -41,6 +42,8 @@ impl FocusableView for ContextMenu { impl EventEmitter for ContextMenu {} +impl FluentBuilder for ContextMenu {} + impl ContextMenu { pub fn build( cx: &mut WindowContext, @@ -56,6 +59,7 @@ impl ContextMenu { Self { items: Default::default(), focus_handle, + action_context: None, selected_index: None, delayed: false, clicked: false, @@ -66,6 +70,11 @@ impl ContextMenu { }) } + pub fn context(mut self, focus: FocusHandle) -> Self { + self.action_context = Some(focus); + self + } + pub fn header(mut self, title: impl Into) -> Self { self.items.push(ContextMenuItem::Header(title.into())); self @@ -305,7 +314,14 @@ impl Render for ContextMenu { .child(label_element) .debug_selector(|| format!("MENU_ITEM-{}", label)) .children(action.as_ref().and_then(|action| { - KeyBinding::for_action(&**action, cx) + self.action_context + .as_ref() + .map(|focus| { + KeyBinding::for_action_in(&**action, focus, cx) + }) + .unwrap_or_else(|| { + KeyBinding::for_action(&**action, cx) + }) .map(|binding| div().ml_1().child(binding)) })), ) diff --git a/crates/ui/src/components/keybinding.rs b/crates/ui/src/components/keybinding.rs index d8077f0ffca2d27cf9b8b9660fcef8bfda62173d..65d145ebd05ee41a5ede674ce39598feff780d61 100644 --- a/crates/ui/src/components/keybinding.rs +++ b/crates/ui/src/components/keybinding.rs @@ -16,7 +16,7 @@ impl RenderOnce for KeyBinding { .flex_none() .gap_2() .children(self.key_binding.keystrokes().iter().map(|keystroke| { - let key_icon = Self::icon_for_key(&keystroke); + let key_icon = Self::icon_for_key(keystroke); h_flex() .flex_none() diff --git a/crates/ui/src/components/label/label.rs b/crates/ui/src/components/label/label.rs index 0ba67286a22ccaeec1505612ec34cbc3b1b5883e..13ee4161451444282d951b3b8f80daf3fb7b6568 100644 --- a/crates/ui/src/components/label/label.rs +++ b/crates/ui/src/components/label/label.rs @@ -37,7 +37,7 @@ pub struct Label { } impl Label { - /// Create a new [`Label`] with the given text. + /// Creates a new [`Label`] with the given text. /// /// # Examples /// diff --git a/crates/ui/src/components/label/label_like.rs b/crates/ui/src/components/label/label_like.rs index 6da07d81a39c8a51355b6d922e19c302c1c683ad..cddd849b89d7d15e6ebfa6367ad5829bfe4a709a 100644 --- a/crates/ui/src/components/label/label_like.rs +++ b/crates/ui/src/components/label/label_like.rs @@ -78,8 +78,8 @@ impl LabelCommon for LabelLike { } impl ParentElement for LabelLike { - fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { - &mut self.children + fn extend(&mut self, elements: impl Iterator) { + self.children.extend(elements) } } diff --git a/crates/ui/src/components/list/list.rs b/crates/ui/src/components/list/list.rs index 436f3e034d17a757e9598fff0584d7239d6f4d65..9d102d709138b6271988663acc08a9a1a86dc392 100644 --- a/crates/ui/src/components/list/list.rs +++ b/crates/ui/src/components/list/list.rs @@ -40,8 +40,8 @@ impl List { } impl ParentElement for List { - fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { - &mut self.children + fn extend(&mut self, elements: impl Iterator) { + self.children.extend(elements) } } diff --git a/crates/ui/src/components/list/list_item.rs b/crates/ui/src/components/list/list_item.rs index ed1edc0864c7f6ee7435779dd7daeff9f2d329f9..f23de39253e510c9b064af9bc081d7f0fd463c67 100644 --- a/crates/ui/src/components/list/list_item.rs +++ b/crates/ui/src/components/list/list_item.rs @@ -141,8 +141,8 @@ impl Selectable for ListItem { } impl ParentElement for ListItem { - fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { - &mut self.children + fn extend(&mut self, elements: impl Iterator) { + self.children.extend(elements) } } @@ -222,17 +222,19 @@ impl RenderOnce for ListItem { })) .child( h_flex() - // HACK: We need to set *any* width value here in order for this container to size correctly. - // Without this the `h_flex` will overflow the parent `inner_list_item`. - .w_px() - .flex_1() + .flex_grow() + .flex_shrink_0() + .flex_basis(relative(0.25)) .gap_1() + .overflow_hidden() .children(self.start_slot) .children(self.children), ) .when_some(self.end_slot, |this, end_slot| { this.justify_between().child( h_flex() + .flex_shrink() + .overflow_hidden() .when(self.end_hover_slot.is_some(), |this| { this.visible() .group_hover("list_item", |this| this.invisible()) diff --git a/crates/ui/src/components/popover.rs b/crates/ui/src/components/popover.rs index ad72a1d9b6efd45cf16270ac0cb552b46732957a..fea2a550fa92acd3d6a0cd5908a54de63066eed9 100644 --- a/crates/ui/src/components/popover.rs +++ b/crates/ui/src/components/popover.rs @@ -16,7 +16,7 @@ use smallvec::SmallVec; /// /// Related elements: /// -/// `ContextMenu`: +/// [`ContextMenu`](crate::ContextMenu): /// /// Used to display a popover menu that only contains a list of items. Context menus are always /// launched by secondary clicking on an element. The menu is positioned relative to the user's cursor. @@ -74,7 +74,7 @@ impl Popover { } impl ParentElement for Popover { - fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { - &mut self.children + fn extend(&mut self, elements: impl Iterator) { + self.children.extend(elements) } } diff --git a/crates/ui/src/components/popover_menu.rs b/crates/ui/src/components/popover_menu.rs index 39202bf7ef5c68e5722c5b6d55a6cab3676c8ef7..0bbbee2900ba8005b4f133c3e9ef7748d6e158b2 100644 --- a/crates/ui/src/components/popover_menu.rs +++ b/crates/ui/src/components/popover_menu.rs @@ -1,9 +1,10 @@ use std::{cell::RefCell, rc::Rc}; use gpui::{ - overlay, point, px, rems, AnchorCorner, AnyElement, Bounds, DismissEvent, DispatchPhase, - Element, ElementId, InteractiveBounds, IntoElement, LayoutId, ManagedView, MouseDownEvent, - ParentElement, Pixels, Point, View, VisualContext, WindowContext, + overlay, point, prelude::FluentBuilder, px, rems, AnchorCorner, AnyElement, Bounds, + DismissEvent, DispatchPhase, Element, ElementContext, ElementId, InteractiveBounds, + IntoElement, LayoutId, ManagedView, MouseDownEvent, ParentElement, Pixels, Point, View, + VisualContext, WindowContext, }; use crate::{Clickable, Selectable}; @@ -51,7 +52,7 @@ impl PopoverMenu { cx.subscribe(&new_menu, move |modal, _: &DismissEvent, cx| { if modal.focus_handle(cx).contains_focused(cx) { if previous_focus_handle.is_some() { - cx.focus(&previous_focus_handle.as_ref().unwrap()) + cx.focus(previous_focus_handle.as_ref().unwrap()) } } *menu2.borrow_mut() = None; @@ -134,7 +135,7 @@ impl Element for PopoverMenu { fn request_layout( &mut self, element_state: Option, - cx: &mut WindowContext, + cx: &mut ElementContext, ) -> (gpui::LayoutId, Self::State) { let mut menu_layout_id = None; @@ -188,7 +189,7 @@ impl Element for PopoverMenu { &mut self, _: Bounds, element_state: &mut Self::State, - cx: &mut WindowContext, + cx: &mut ElementContext, ) { if let Some(mut child) = element_state.child_element.take() { child.paint(cx); diff --git a/crates/ui/src/components/right_click_menu.rs b/crates/ui/src/components/right_click_menu.rs index 9d32073dbdb077614f1c23dda58ce0d4bae35ce8..b08f3911cbf7f39ac040b9da936088f580bcbff6 100644 --- a/crates/ui/src/components/right_click_menu.rs +++ b/crates/ui/src/components/right_click_menu.rs @@ -1,9 +1,9 @@ use std::{cell::RefCell, rc::Rc}; use gpui::{ - overlay, AnchorCorner, AnyElement, BorrowWindow, Bounds, DismissEvent, DispatchPhase, Element, - ElementId, InteractiveBounds, IntoElement, LayoutId, ManagedView, MouseButton, MouseDownEvent, - ParentElement, Pixels, Point, View, VisualContext, WindowContext, + overlay, AnchorCorner, AnyElement, Bounds, DismissEvent, DispatchPhase, Element, + ElementContext, ElementId, InteractiveBounds, IntoElement, LayoutId, ManagedView, MouseButton, + MouseDownEvent, ParentElement, Pixels, Point, View, VisualContext, WindowContext, }; pub struct RightClickMenu { @@ -64,7 +64,7 @@ impl Element for RightClickMenu { fn request_layout( &mut self, element_state: Option, - cx: &mut WindowContext, + cx: &mut ElementContext, ) -> (gpui::LayoutId, Self::State) { let (menu, position) = if let Some(element_state) = element_state { (element_state.menu, element_state.position) @@ -116,7 +116,7 @@ impl Element for RightClickMenu { &mut self, bounds: Bounds, element_state: &mut Self::State, - cx: &mut WindowContext, + cx: &mut ElementContext, ) { if let Some(mut child) = element_state.child_element.take() { child.paint(cx); @@ -155,7 +155,7 @@ impl Element for RightClickMenu { cx.subscribe(&new_menu, move |modal, _: &DismissEvent, cx| { if modal.focus_handle(cx).contains_focused(cx) { if previous_focus_handle.is_some() { - cx.focus(&previous_focus_handle.as_ref().unwrap()) + cx.focus(previous_focus_handle.as_ref().unwrap()) } } *menu2.borrow_mut() = None; diff --git a/crates/ui/src/components/stories/list_item.rs b/crates/ui/src/components/stories/list_item.rs index f2af011db8e8554b07434e699b61c8492040ede8..ae7be9b9c74c6ce2084ae84615ae7468ec64dbe0 100644 --- a/crates/ui/src/components/stories/list_item.rs +++ b/crates/ui/src/components/stories/list_item.rs @@ -4,6 +4,8 @@ use story::Story; use crate::{prelude::*, Avatar}; use crate::{IconName, ListItem}; +const OVERFLOWING_TEXT: &'static str = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean mauris ligula, luctus vel dignissim eu, vestibulum sed libero. Sed at convallis velit."; + pub struct ListItemStory; impl Render for ListItemStory { @@ -98,5 +100,29 @@ impl Render for ListItemStory { println!("Right mouse down!"); }), ) + .child(Story::label("With overflowing content in the `end_slot`")) + .child( + ListItem::new("with_overflowing_content_in_end_slot") + .child("An excerpt") + .end_slot(Label::new(OVERFLOWING_TEXT).color(Color::Muted)), + ) + .child(Story::label( + "`inset` with overflowing content in the `end_slot`", + )) + .child( + ListItem::new("inset_with_overflowing_content_in_end_slot") + .inset(true) + .child("An excerpt") + .end_slot(Label::new(OVERFLOWING_TEXT).color(Color::Muted)), + ) + .child(Story::label( + "`inset` with overflowing content in `children` and `end_slot`", + )) + .child( + ListItem::new("inset_with_overflowing_content_in_children_and_end_slot") + .inset(true) + .child(Label::new(OVERFLOWING_TEXT)) + .end_slot(Label::new(OVERFLOWING_TEXT).color(Color::Muted)), + ) } } diff --git a/crates/ui/src/components/tab.rs b/crates/ui/src/components/tab.rs index 7f1fcca721b9524b8879f2c6051e7481dc8db6ab..db81bb5801609d46fc8cb8e0af2d5404acce54a3 100644 --- a/crates/ui/src/components/tab.rs +++ b/crates/ui/src/components/tab.rs @@ -92,8 +92,8 @@ impl Selectable for Tab { } impl ParentElement for Tab { - fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { - &mut self.children + fn extend(&mut self, elements: impl Iterator) { + self.children.extend(elements) } } diff --git a/crates/ui/src/components/tab_bar.rs b/crates/ui/src/components/tab_bar.rs index adee8389e47a8db3f343e01cc9d81a9c1d08ead4..abfd7284f2b4987c1d38ef235a338fa22a059cf0 100644 --- a/crates/ui/src/components/tab_bar.rs +++ b/crates/ui/src/components/tab_bar.rs @@ -83,8 +83,8 @@ impl TabBar { } impl ParentElement for TabBar { - fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { - &mut self.children + fn extend(&mut self, elements: impl Iterator) { + self.children.extend(elements) } } @@ -117,7 +117,7 @@ impl RenderOnce for TabBar { .relative() .flex_1() .h_full() - .overflow_hidden_x() + .overflow_x_hidden() .child( div() .absolute() diff --git a/crates/ui/src/components/tooltip.rs b/crates/ui/src/components/tooltip.rs index f76085daa35ccfa37131b44bca2d83b9532cffb5..6db1804740c265dbcdb1162663137b8c683db8a8 100644 --- a/crates/ui/src/components/tooltip.rs +++ b/crates/ui/src/components/tooltip.rs @@ -69,29 +69,74 @@ impl Tooltip { impl Render for Tooltip { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone(); - overlay().child( - // padding to avoid mouse cursor - div().pl_2().pt_2p5().child( - v_flex() - .elevation_2(cx) - .font(ui_font) - .text_ui() - .text_color(cx.theme().colors().text) - .py_1() - .px_2() - .child( - h_flex() - .gap_4() - .child(self.title.clone()) - .when_some(self.key_binding.clone(), |this, key_binding| { - this.justify_between().child(key_binding) - }), - ) - .when_some(self.meta.clone(), |this, meta| { - this.child(Label::new(meta).size(LabelSize::Small).color(Color::Muted)) + tooltip_container(cx, |el, _| { + el.child( + h_flex() + .gap_4() + .child(self.title.clone()) + .when_some(self.key_binding.clone(), |this, key_binding| { + this.justify_between().child(key_binding) }), - ), - ) + ) + .when_some(self.meta.clone(), |this, meta| { + this.child(Label::new(meta).size(LabelSize::Small).color(Color::Muted)) + }) + }) + } +} + +fn tooltip_container( + cx: &mut ViewContext, + f: impl FnOnce(Div, &mut ViewContext) -> Div, +) -> impl IntoElement { + let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone(); + overlay().child( + // padding to avoid mouse cursor + div().pl_2().pt_2p5().child( + v_flex() + .elevation_2(cx) + .font(ui_font) + .text_ui() + .text_color(cx.theme().colors().text) + .py_1() + .px_2() + .map(|el| f(el, cx)), + ), + ) +} + +pub struct LinkPreview { + link: SharedString, +} + +impl LinkPreview { + pub fn new(url: &str, cx: &mut WindowContext) -> AnyView { + let mut wrapped_url = String::new(); + for (i, ch) in url.chars().enumerate() { + if i == 500 { + wrapped_url.push('…'); + break; + } + if i % 100 == 0 && i != 0 { + wrapped_url.push('\n'); + } + wrapped_url.push(ch); + } + cx.new_view(|_cx| LinkPreview { + link: wrapped_url.into(), + }) + .into() + } +} + +impl Render for LinkPreview { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + tooltip_container(cx, |el, _| { + el.child( + Label::new(self.link.clone()) + .size(LabelSize::XSmall) + .color(Color::Muted), + ) + }) } } diff --git a/crates/ui/src/prelude.rs b/crates/ui/src/prelude.rs index 837d93db2d20017742a618202cd41eb89075ce6d..91e6ed24503659ca9ccfdc11f3a95261ea5a57ee 100644 --- a/crates/ui/src/prelude.rs +++ b/crates/ui/src/prelude.rs @@ -2,9 +2,9 @@ pub use gpui::prelude::*; pub use gpui::{ - div, px, relative, rems, AbsoluteLength, DefiniteLength, Div, Element, ElementId, - InteractiveElement, ParentElement, Pixels, Rems, RenderOnce, SharedString, Styled, ViewContext, - WindowContext, + div, px, relative, rems, AbsoluteLength, DefiniteLength, Div, Element, ElementContext, + ElementId, InteractiveElement, ParentElement, Pixels, Rems, RenderOnce, SharedString, Styled, + ViewContext, WindowContext, }; pub use crate::clickable::*; diff --git a/crates/ui/src/styles/elevation.rs b/crates/ui/src/styles/elevation.rs index 0aa3786a279242c8a3a301b497a60ab0c2c537ec..c2605fd152df49d04db57d6784bc3a54aaefd80d 100644 --- a/crates/ui/src/styles/elevation.rs +++ b/crates/ui/src/styles/elevation.rs @@ -20,7 +20,7 @@ pub enum ElevationIndex { } impl ElevationIndex { - pub fn z_index(self) -> u8 { + pub fn z_index(self) -> u16 { match self { ElevationIndex::Background => 0, ElevationIndex::Surface => 42, diff --git a/crates/util/src/http.rs b/crates/util/src/http.rs index 329d84996d908259ea6fe2a40d0ec536d9292f97..f6487c04f2dae3adc98a1eb70ffca6da97f14346 100644 --- a/crates/util/src/http.rs +++ b/crates/util/src/http.rs @@ -99,6 +99,15 @@ impl FakeHttpClient { .unwrap()) }) } + + pub fn with_200_response() -> Arc { + Self::create(|_| async move { + Ok(Response::builder() + .status(200) + .body(Default::default()) + .unwrap()) + }) + } } #[cfg(feature = "test-support")] diff --git a/crates/util/src/util.rs b/crates/util/src/util.rs index a2f8b87feea91e5ccb09aea4c752cd1c5a583fda..ed03eb25ba534d6a123810b89667d0e577396d38 100644 --- a/crates/util/src/util.rs +++ b/crates/util/src/util.rs @@ -7,19 +7,21 @@ pub mod paths; #[cfg(any(test, feature = "test-support"))] pub mod test; +pub use backtrace::Backtrace; +use futures::Future; +use lazy_static::lazy_static; +use rand::{seq::SliceRandom, Rng}; use std::{ borrow::Cow, cmp::{self, Ordering}, + env, ops::{AddAssign, Range, RangeInclusive}, panic::Location, pin::Pin, task::{Context, Poll}, + time::Instant, }; -pub use backtrace::Backtrace; -use futures::Future; -use rand::{seq::SliceRandom, Rng}; - pub use take_until::*; #[macro_export] @@ -133,6 +135,24 @@ pub fn merge_non_null_json_value_into(source: serde_json::Value, target: &mut se } } +pub fn measure(label: &str, f: impl FnOnce() -> R) -> R { + lazy_static! { + pub static ref ZED_MEASUREMENTS: bool = env::var("ZED_MEASUREMENTS") + .map(|measurements| measurements == "1" || measurements == "true") + .unwrap_or(false); + } + + if *ZED_MEASUREMENTS { + let start = Instant::now(); + let result = f(); + let elapsed = start.elapsed(); + eprintln!("{}: {:?}", label, elapsed); + result + } else { + f() + } +} + pub trait ResultExt { type Ok; @@ -299,7 +319,7 @@ pub struct Deferred(Option); impl Deferred { /// Drop without running the deferred function. - pub fn cancel(mut self) { + pub fn abort(mut self) { self.0.take(); } } diff --git a/crates/vim/Cargo.toml b/crates/vim/Cargo.toml index 95702585292d82fe586d72cc2e0bfb54f4979a1b..ef3fd2a4c790ee928ecc7ea05882fd7cf6d3a738 100644 --- a/crates/vim/Cargo.toml +++ b/crates/vim/Cargo.toml @@ -23,6 +23,7 @@ async-trait = { workspace = true, "optional" = true } nvim-rs = { git = "https://github.com/KillTheMule/nvim-rs", branch = "master", features = ["use_tokio"], optional = true } tokio = { version = "1.15", "optional" = true } serde_json.workspace = true +regex.workspace = true collections = { path = "../collections" } command_palette = { path = "../command_palette" } diff --git a/crates/vim/README.md b/crates/vim/README.md new file mode 100644 index 0000000000000000000000000000000000000000..547ca686fb9c5b5146d254db808a9b61aa630804 --- /dev/null +++ b/crates/vim/README.md @@ -0,0 +1,36 @@ +This contains the code for Zed's Vim emulation mode. + +Vim mode in Zed is supposed to primarily "do what you expect": it mostly tries to copy vim exactly, but will use Zed-specific functionality when available to make things smoother. This means Zed will never be 100% vim compatible, but should be 100% vim familiar! + +The backlog is maintained in the `#vim` channel notes. + +## Testing against Neovim + +If you are making a change to make Zed's behaviour more closely match vim/nvim, you can create a test using the `NeovimBackedTestContext`. + +For example, the following test checks that Zed and Neovim have the same behaviour when running `*` in visual mode: + +```rust +#[gpui::test] +async fn test_visual_star_hash(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state("ˇa.c. abcd a.c. abcd").await; + cx.simulate_shared_keystrokes(["v", "3", "l", "*"]).await; + cx.assert_shared_state("a.c. abcd ˇa.c. abcd").await; +} +``` + +To keep CI runs fast, by default the neovim tests use a cached JSON file that records what neovim did (see crates/vim/test_data), +but while developing this test you'll need to run it with the neovim flag enabled: + +``` +cargo test -p vim --features neovim test_visual_star_hash +``` + +This will run your keystrokes against a headless neovim and cache the results in the test_data directory. + + +## Testing zed-only behaviour + +Zed does more than vim/neovim in their default modes. The `VimTestContext` can be used instead. This lets you test integration with the language server and other parts of zed's UI that don't have a NeoVim equivalent. diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index 6215e4c16c0d1ebb10eb59358a87d1978bc45c5d..01d8bec569ec851c91597ea47146db05accd15f4 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -11,6 +11,7 @@ use workspace::Workspace; use crate::{ normal::normal_motion, state::{Mode, Operator}, + utils::coerce_punctuation, visual::visual_motion, Vim, }; @@ -72,9 +73,9 @@ pub(crate) struct Up { #[derive(Clone, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] -struct Down { +pub(crate) struct Down { #[serde(default)] - display_lines: bool, + pub(crate) display_lines: bool, } #[derive(Clone, Deserialize, PartialEq)] @@ -680,8 +681,8 @@ pub(crate) fn next_word_start( for _ in 0..times { let mut crossed_newline = false; point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| { - let left_kind = char_kind(&scope, left).coerce_punctuation(ignore_punctuation); - let right_kind = char_kind(&scope, right).coerce_punctuation(ignore_punctuation); + let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation); + let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation); let at_newline = right == '\n'; let found = (left_kind != right_kind && right_kind != CharKind::Whitespace) @@ -710,8 +711,8 @@ fn next_word_end( *point.column_mut() = 0; } point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| { - let left_kind = char_kind(&scope, left).coerce_punctuation(ignore_punctuation); - let right_kind = char_kind(&scope, right).coerce_punctuation(ignore_punctuation); + let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation); + let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation); left_kind != right_kind && left_kind != CharKind::Whitespace }); @@ -743,8 +744,8 @@ fn previous_word_start( // cursor because the newline is checked only once. point = movement::find_preceding_boundary(map, point, FindRange::MultiLine, |left, right| { - let left_kind = char_kind(&scope, left).coerce_punctuation(ignore_punctuation); - let right_kind = char_kind(&scope, right).coerce_punctuation(ignore_punctuation); + let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation); + let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation); (left_kind != right_kind && !right.is_whitespace()) || left == '\n' }); diff --git a/crates/vim/src/normal/change.rs b/crates/vim/src/normal/change.rs index 86b77038461577909ffe49445fe442cbb0e54f69..52de1f7e0a4a59859caafb2e1300b910248168c3 100644 --- a/crates/vim/src/normal/change.rs +++ b/crates/vim/src/normal/change.rs @@ -1,4 +1,10 @@ -use crate::{motion::Motion, object::Object, state::Mode, utils::copy_selections_content, Vim}; +use crate::{ + motion::Motion, + object::Object, + state::Mode, + utils::{coerce_punctuation, copy_selections_content}, + Vim, +}; use editor::{ display_map::DisplaySnapshot, movement::{self, FindRange, TextLayoutDetails}, @@ -102,9 +108,9 @@ fn expand_changed_word_selection( if in_word { selection.end = movement::find_boundary(map, selection.end, FindRange::MultiLine, |left, right| { - let left_kind = char_kind(&scope, left).coerce_punctuation(ignore_punctuation); + let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation); let right_kind = - char_kind(&scope, right).coerce_punctuation(ignore_punctuation); + coerce_punctuation(char_kind(&scope, right), ignore_punctuation); left_kind != right_kind && left_kind != CharKind::Whitespace }); diff --git a/crates/vim/src/normal/repeat.rs b/crates/vim/src/normal/repeat.rs index 796cfce7a3c5bb3091996d0a8c72d76c4379abfa..4850151d94d09996474ae000ed4d5255141ac33f 100644 --- a/crates/vim/src/normal/repeat.rs +++ b/crates/vim/src/normal/repeat.rs @@ -202,7 +202,7 @@ mod test { use futures::StreamExt; use indoc::indoc; - use gpui::InputHandler; + use gpui::ViewInputHandler; use crate::{ state::Mode, diff --git a/crates/vim/src/normal/search.rs b/crates/vim/src/normal/search.rs index f85e3d9ba92415040114fbcfd61c55c5066dbf79..68a8ada984301aad8e9211672de86738ba01fb60 100644 --- a/crates/vim/src/normal/search.rs +++ b/crates/vim/src/normal/search.rs @@ -3,7 +3,12 @@ use search::{buffer_search, BufferSearchBar, SearchMode, SearchOptions}; use serde_derive::Deserialize; use workspace::{searchable::Direction, Workspace}; -use crate::{motion::Motion, normal::move_cursor, state::SearchState, Vim}; +use crate::{ + motion::Motion, + normal::move_cursor, + state::{Mode, SearchState}, + Vim, +}; #[derive(Clone, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] @@ -91,7 +96,6 @@ fn search(workspace: &mut Workspace, action: &Search, cx: &mut ViewContext() { let search = search_bar.update(cx, |search_bar, cx| { - let mut options = SearchOptions::CASE_SENSITIVE; - options.set(SearchOptions::WHOLE_WORD, whole_word); - if search_bar.show(cx) { - search_bar - .query_suggestion(cx) - .map(|query| search_bar.search(&query, Some(options), cx)) - } else { - None + let options = SearchOptions::CASE_SENSITIVE; + if !search_bar.show(cx) { + return None; + } + let Some(query) = search_bar.query_suggestion(cx) else { + return None; + }; + let mut query = regex::escape(&query); + if whole_word { + query = format!(r"\b{}\b", query); } + search_bar.activate_search_mode(SearchMode::Regex, cx); + Some(search_bar.search(&query, Some(options), cx)) }); if let Some(search) = search { @@ -173,6 +182,11 @@ pub fn move_to_internal( } } }); + + if vim.state().mode.is_visual() { + vim.switch_mode(Mode::Normal, false, cx) + } + vim.clear_operator(cx); }); } @@ -350,7 +364,10 @@ mod test { use editor::DisplayPoint; use search::BufferSearchBar; - use crate::{state::Mode, test::VimTestContext}; + use crate::{ + state::Mode, + test::{NeovimBackedTestContext, VimTestContext}, + }; #[gpui::test] async fn test_move_to_next(cx: &mut gpui::TestAppContext) { @@ -459,6 +476,13 @@ mod test { cx.simulate_keystrokes(["/", "b"]); cx.simulate_keystrokes(["enter"]); cx.assert_state("aa\nˇbb\ndd\ncc\nbb\n", Mode::Normal); + + // check that searching switches to normal mode if in visual mode + cx.set_state("ˇone two one", Mode::Normal); + cx.simulate_keystrokes(["v", "l", "l"]); + cx.assert_editor_state("«oneˇ» two one"); + cx.simulate_keystrokes(["*"]); + cx.assert_state("one two ˇone", Mode::Normal); } #[gpui::test] @@ -474,4 +498,14 @@ mod test { cx.simulate_keystrokes(["shift-enter"]); cx.assert_editor_state("«oneˇ» one one one"); } + + #[gpui::test] + async fn test_visual_star_hash(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state("ˇa.c. abcd a.c. abcd").await; + cx.simulate_shared_keystrokes(["v", "3", "l", "*"]).await; + cx.assert_shared_state("a.c. abcd ˇa.c. abcd").await; + cx.assert_shared_mode(Mode::Normal).await; + } } diff --git a/crates/vim/src/object.rs b/crates/vim/src/object.rs index 1e9361618c5cd6874a3d869dabe9ba01e411599c..49406cf992e2a44a17960ca3c84459b2772cb5e4 100644 --- a/crates/vim/src/object.rs +++ b/crates/vim/src/object.rs @@ -10,7 +10,10 @@ use language::{char_kind, CharKind, Selection}; use serde::Deserialize; use workspace::Workspace; -use crate::{motion::right, normal::normal_object, state::Mode, visual::visual_object, Vim}; +use crate::{ + motion::right, normal::normal_object, state::Mode, utils::coerce_punctuation, + visual::visual_object, Vim, +}; #[derive(Copy, Clone, Debug, PartialEq)] pub enum Object { @@ -195,9 +198,10 @@ impl Object { } } -/// Return a range that surrounds the word relative_to is in -/// If relative_to is at the start of a word, return the word. -/// If relative_to is between words, return the space between +/// Returns a range that surrounds the word `relative_to` is in. +/// +/// If `relative_to` is at the start of a word, return the word. +/// If `relative_to` is between words, return the space between. fn in_word( map: &DisplaySnapshot, relative_to: DisplayPoint, @@ -212,24 +216,25 @@ fn in_word( right(map, relative_to, 1), movement::FindRange::SingleLine, |left, right| { - char_kind(&scope, left).coerce_punctuation(ignore_punctuation) - != char_kind(&scope, right).coerce_punctuation(ignore_punctuation) + coerce_punctuation(char_kind(&scope, left), ignore_punctuation) + != coerce_punctuation(char_kind(&scope, right), ignore_punctuation) }, ); let end = movement::find_boundary(map, relative_to, FindRange::SingleLine, |left, right| { - char_kind(&scope, left).coerce_punctuation(ignore_punctuation) - != char_kind(&scope, right).coerce_punctuation(ignore_punctuation) + coerce_punctuation(char_kind(&scope, left), ignore_punctuation) + != coerce_punctuation(char_kind(&scope, right), ignore_punctuation) }); Some(start..end) } -/// Return a range that surrounds the word and following whitespace +/// Returns a range that surrounds the word and following whitespace /// relative_to is in. -/// If relative_to is at the start of a word, return the word and following whitespace. -/// If relative_to is between words, return the whitespace back and the following word - +/// +/// If `relative_to` is at the start of a word, return the word and following whitespace. +/// If `relative_to` is between words, return the whitespace back and the following word. +/// /// if in word /// delete that word /// if there is whitespace following the word, delete that as well @@ -281,15 +286,15 @@ fn around_next_word( right(map, relative_to, 1), FindRange::SingleLine, |left, right| { - char_kind(&scope, left).coerce_punctuation(ignore_punctuation) - != char_kind(&scope, right).coerce_punctuation(ignore_punctuation) + coerce_punctuation(char_kind(&scope, left), ignore_punctuation) + != coerce_punctuation(char_kind(&scope, right), ignore_punctuation) }, ); let mut word_found = false; let end = movement::find_boundary(map, relative_to, FindRange::MultiLine, |left, right| { - let left_kind = char_kind(&scope, left).coerce_punctuation(ignore_punctuation); - let right_kind = char_kind(&scope, right).coerce_punctuation(ignore_punctuation); + let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation); + let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation); let found = (word_found && left_kind != right_kind) || right == '\n' && left == '\n'; diff --git a/crates/vim/src/test.rs b/crates/vim/src/test.rs index fa2dcb45cda61f4637b5bcb6ac0cd1d43236def3..1819be7878b63c8db61bd97b0a42b0e72e234f73 100644 --- a/crates/vim/src/test.rs +++ b/crates/vim/src/test.rs @@ -3,8 +3,11 @@ mod neovim_backed_test_context; mod neovim_connection; mod vim_test_context; +use std::time::Duration; + use command_palette::CommandPalette; use editor::DisplayPoint; +use gpui::KeyBinding; pub use neovim_backed_binding_test_context::*; pub use neovim_backed_test_context::*; pub use vim_test_context::*; @@ -12,7 +15,7 @@ pub use vim_test_context::*; use indoc::indoc; use search::BufferSearchBar; -use crate::{state::Mode, ModeIndicator}; +use crate::{insert::NormalBefore, motion, state::Mode, ModeIndicator}; #[gpui::test] async fn test_initially_disabled(cx: &mut gpui::TestAppContext) { @@ -774,3 +777,73 @@ async fn test_select_all_issue_2170(cx: &mut gpui::TestAppContext) { Mode::Visual, ); } + +#[gpui::test] +async fn test_jk(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.update(|cx| { + cx.bind_keys([KeyBinding::new( + "j k", + NormalBefore, + Some("vim_mode == insert"), + )]) + }); + cx.neovim.exec("imap jk ").await; + + cx.set_shared_state("ˇhello").await; + cx.simulate_shared_keystrokes(["i", "j", "o", "j", "k"]) + .await; + cx.assert_shared_state("jˇohello").await; +} + +#[gpui::test] +async fn test_jk_delay(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + + cx.update(|cx| { + cx.bind_keys([KeyBinding::new( + "j k", + NormalBefore, + Some("vim_mode == insert"), + )]) + }); + + cx.set_state("ˇhello", Mode::Normal); + cx.simulate_keystrokes(["i", "j"]); + cx.executor().advance_clock(Duration::from_millis(500)); + cx.run_until_parked(); + cx.assert_state("ˇhello", Mode::Insert); + cx.executor().advance_clock(Duration::from_millis(500)); + cx.run_until_parked(); + cx.assert_state("jˇhello", Mode::Insert); + cx.simulate_keystrokes(["k", "j", "k"]); + cx.assert_state("jˇkhello", Mode::Normal); +} + +#[gpui::test] +async fn test_comma_w(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.update(|cx| { + cx.bind_keys([KeyBinding::new( + ", w", + motion::Down { + display_lines: false, + }, + Some("vim_mode == normal"), + )]) + }); + cx.neovim.exec("map ,w j").await; + + cx.set_shared_state("ˇhello hello\nhello hello").await; + cx.simulate_shared_keystrokes(["f", "o", ";", ",", "w"]) + .await; + cx.assert_shared_state("hello hello\nhello hellˇo").await; + + cx.set_shared_state("ˇhello hello\nhello hello").await; + cx.simulate_shared_keystrokes(["f", "o", ";", ",", "i"]) + .await; + cx.assert_shared_state("hellˇo hello\nhello hello").await; + cx.assert_shared_mode(Mode::Insert).await; +} diff --git a/crates/vim/src/test/neovim_backed_test_context.rs b/crates/vim/src/test/neovim_backed_test_context.rs index 384722286cdfa3768320c0810655c4ca06c9b012..977d6aa7c6b1b3c5c2f65e8e4e9b9ded17eb3a6b 100644 --- a/crates/vim/src/test/neovim_backed_test_context.rs +++ b/crates/vim/src/test/neovim_backed_test_context.rs @@ -52,7 +52,7 @@ pub struct NeovimBackedTestContext { // Lookup for exempted assertions. Keyed by the insertion text, and with a value indicating which // bindings are exempted. If None, all bindings are ignored for that insertion text. exemptions: HashMap>>, - neovim: NeovimConnection, + pub(crate) neovim: NeovimConnection, last_set_state: Option, recent_keystrokes: Vec, @@ -277,6 +277,24 @@ impl NeovimBackedTestContext { self.neovim.mode().await.unwrap() } + pub async fn assert_shared_mode(&mut self, mode: Mode) { + let neovim = self.neovim_mode().await; + let editor = self.cx.mode(); + + if neovim != mode || editor != mode { + panic!( + indoc! {"Test failed (zed does not match nvim behaviour) + # desired mode: + {:?} + # neovim mode: + {:?} + # zed mode: + {:?}"}, + mode, neovim, editor, + ) + } + } + pub async fn assert_state_matches(&mut self) { self.is_dirty = false; let neovim = self.neovim_state().await; diff --git a/crates/vim/src/test/neovim_connection.rs b/crates/vim/src/test/neovim_connection.rs index a2daf7499d887eee0d7cd6de363f305f7740387c..4de0943321fa4657956c022d5ad0fe8fff388898 100644 --- a/crates/vim/src/test/neovim_connection.rs +++ b/crates/vim/src/test/neovim_connection.rs @@ -42,6 +42,7 @@ pub enum NeovimData { Key(String), Get { state: String, mode: Option }, ReadRegister { name: char, value: String }, + Exec { command: String }, SetOption { value: String }, } @@ -269,6 +270,32 @@ impl NeovimConnection { ); } + #[cfg(feature = "neovim")] + pub async fn exec(&mut self, value: &str) { + self.nvim + .command_output(format!("{}", value).as_str()) + .await + .unwrap(); + + self.data.push_back(NeovimData::Exec { + command: value.to_string(), + }) + } + + #[cfg(not(feature = "neovim"))] + pub async fn exec(&mut self, value: &str) { + if let Some(NeovimData::Get { .. }) = self.data.front() { + self.data.pop_front(); + }; + assert_eq!( + self.data.pop_front(), + Some(NeovimData::Exec { + command: value.to_string(), + }), + "operation does not match recorded script. re-record with --features=neovim" + ); + } + #[cfg(not(feature = "neovim"))] pub async fn read_register(&mut self, register: char) -> String { if let Some(NeovimData::Get { .. }) = self.data.front() { diff --git a/crates/vim/src/utils.rs b/crates/vim/src/utils.rs index 797e94b0fa01af189464d7719345d0733df09ad6..0ff857af9cdfcee4609c67a3a5f4b07d59fe8bf9 100644 --- a/crates/vim/src/utils.rs +++ b/crates/vim/src/utils.rs @@ -1,6 +1,6 @@ use editor::{ClipboardSelection, Editor}; use gpui::{AppContext, ClipboardItem}; -use language::Point; +use language::{CharKind, Point}; pub fn copy_selections_content(editor: &mut Editor, linewise: bool, cx: &mut AppContext) { let selections = editor.selections.all_adjusted(cx); @@ -48,3 +48,11 @@ pub fn copy_selections_content(editor: &mut Editor, linewise: bool, cx: &mut App cx.write_to_clipboard(ClipboardItem::new(text).with_metadata(clipboard_selections)); } + +pub fn coerce_punctuation(kind: CharKind, treat_punctuation_as_word: bool) -> CharKind { + if treat_punctuation_as_word && kind == CharKind::Punctuation { + CharKind::Word + } else { + kind + } +} diff --git a/crates/vim/test_data/test_comma_w.json b/crates/vim/test_data/test_comma_w.json new file mode 100644 index 0000000000000000000000000000000000000000..ac7a91c80c5b759f7baed7ff64131b98e6d0fcce --- /dev/null +++ b/crates/vim/test_data/test_comma_w.json @@ -0,0 +1,15 @@ +{"Exec":{"command":"map ,w j"}} +{"Put":{"state":"ˇhello hello\nhello hello"}} +{"Key":"f"} +{"Key":"o"} +{"Key":";"} +{"Key":","} +{"Key":"w"} +{"Get":{"state":"hello hello\nhello hellˇo","mode":"Normal"}} +{"Put":{"state":"ˇhello hello\nhello hello"}} +{"Key":"f"} +{"Key":"o"} +{"Key":";"} +{"Key":","} +{"Key":"i"} +{"Get":{"state":"hellˇo hello\nhello hello","mode":"Insert"}} diff --git a/crates/vim/test_data/test_jk.json b/crates/vim/test_data/test_jk.json new file mode 100644 index 0000000000000000000000000000000000000000..bc1a6a4ba5b3d23d8f5296fd73e4e225c940482d --- /dev/null +++ b/crates/vim/test_data/test_jk.json @@ -0,0 +1,8 @@ +{"Exec":{"command":"imap jk "}} +{"Put":{"state":"ˇhello"}} +{"Key":"i"} +{"Key":"j"} +{"Key":"o"} +{"Key":"j"} +{"Key":"k"} +{"Get":{"state":"jˇohello","mode":"Normal"}} diff --git a/crates/vim/test_data/test_visual_star_hash.json b/crates/vim/test_data/test_visual_star_hash.json new file mode 100644 index 0000000000000000000000000000000000000000..d6523c4a45145cbbf1d053488227149c4fc99c3e --- /dev/null +++ b/crates/vim/test_data/test_visual_star_hash.json @@ -0,0 +1,6 @@ +{"Put":{"state":"ˇa.c. abcd a.c. abcd"}} +{"Key":"v"} +{"Key":"3"} +{"Key":"l"} +{"Key":"*"} +{"Get":{"state":"a.c. abcd ˇa.c. abcd","mode":"Normal"}} diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index 4ae408993572e06e97635f8fb579fbee1395238b..a649b441a60ccdce6470eb449fb704ef3708d376 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -1,3 +1,4 @@ +use crate::persistence::model::DockData; use crate::DraggedDock; use crate::{status_bar::StatusItemView, Workspace}; use gpui::{ @@ -141,6 +142,7 @@ pub struct Dock { is_open: bool, active_panel_index: usize, focus_handle: FocusHandle, + pub(crate) serialized_dock: Option, _focus_subscription: Subscription, } @@ -201,6 +203,7 @@ impl Dock { is_open: false, focus_handle: focus_handle.clone(), _focus_subscription: focus_subscription, + serialized_dock: None, } }); @@ -408,10 +411,26 @@ impl Dock { }), ]; + let name = panel.persistent_name().to_string(); + self.panel_entries.push(PanelEntry { panel: Arc::new(panel), _subscriptions: subscriptions, }); + if let Some(serialized) = self.serialized_dock.clone() { + if serialized.active_panel == Some(name) { + self.activate_panel(self.panel_entries.len() - 1, cx); + if serialized.visible { + self.set_open(true, cx); + } + if serialized.zoom { + if let Some(panel) = self.active_panel() { + panel.set_zoomed(true, cx) + }; + } + } + } + cx.notify() } diff --git a/crates/workspace/src/notifications.rs b/crates/workspace/src/notifications.rs index 6e7590c7d3886493f00d2b43728326549e5665f8..1b41b7040ceb00eacbcc38890018e852f446d95a 100644 --- a/crates/workspace/src/notifications.rs +++ b/crates/workspace/src/notifications.rs @@ -281,7 +281,7 @@ where Ok(value) => Some(value), Err(err) => { log::error!("TODO {err:?}"); - cx.update(|view, cx| { + cx.update_root(|view, cx| { if let Ok(workspace) = view.downcast::() { workspace.update(cx, |workspace, cx| workspace.show_error(&err, cx)) } diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 17001ed76f084275ac2b93304e8e5bcad152b650..10209f6e880b3ac99e7a6e88855713f25ba7ab09 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -1092,7 +1092,7 @@ impl Pane { return Ok(true); } - let (mut has_conflict, mut is_dirty, mut can_save, can_save_as) = cx.update(|_, cx| { + let (mut has_conflict, mut is_dirty, mut can_save, can_save_as) = cx.update(|cx| { ( item.has_conflict(cx), item.is_dirty(cx), @@ -1132,7 +1132,7 @@ impl Pane { } } else if is_dirty && (can_save || can_save_as) { if save_intent == SaveIntent::Close { - let will_autosave = cx.update(|_, cx| { + let will_autosave = cx.update(|cx| { matches!( WorkspaceSettings::get_global(cx).autosave, AutosaveSetting::OnFocusChange | AutosaveSetting::OnWindowChange @@ -1166,7 +1166,7 @@ impl Pane { })? .unwrap_or_else(|| Path::new("").into()); - let abs_path = cx.update(|_, cx| cx.prompt_for_new_path(&start_abs_path))?; + let abs_path = cx.update(|cx| cx.prompt_for_new_path(&start_abs_path))?; if let Some(abs_path) = abs_path.await.ok().flatten() { pane.update(cx, |_, cx| item.save_as(project, abs_path, cx))? .await?; @@ -2054,7 +2054,7 @@ fn dirty_message_for(buffer_path: Option) -> String { let path = buffer_path .as_ref() .and_then(|p| p.path.to_str()) - .unwrap_or(&"This buffer"); + .unwrap_or("This buffer"); let path = truncate_and_remove_front(path, 80); format!("{path} contains unsaved edits. Do you want to save it?") } diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index 476592f3745568e67fcbea9b44060cceea90370c..6963ed3cae1d51c15af8cc0113f510ce808671dc 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -710,7 +710,7 @@ mod element { pane_bounds: Bounds, axis_bounds: Bounds, workspace: WeakView, - cx: &mut WindowContext, + cx: &mut ElementContext, ) { let handle_bounds = Bounds { origin: pane_bounds.origin.apply_along(axis, |origin| { @@ -803,7 +803,7 @@ mod element { fn request_layout( &mut self, state: Option, - cx: &mut ui::prelude::WindowContext, + cx: &mut ui::prelude::ElementContext, ) -> (gpui::LayoutId, Self::State) { let mut style = Style::default(); style.flex_grow = 1.; @@ -820,7 +820,7 @@ mod element { &mut self, bounds: gpui::Bounds, state: &mut Self::State, - cx: &mut ui::prelude::WindowContext, + cx: &mut ui::prelude::ElementContext, ) { let flexes = self.flexes.lock().clone(); let len = self.children.len(); @@ -904,8 +904,8 @@ mod element { } impl ParentElement for PaneAxisElement { - fn children_mut(&mut self) -> &mut smallvec::SmallVec<[AnyElement; 2]> { - &mut self.children + fn extend(&mut self, elements: impl Iterator) { + self.children.extend(elements) } } diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index 56aa6e4322bce652aacda0ddb1f0c77cef61b7ee..2c6bf95c607aa5bf01d94b16251edf949e22f8fd 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -591,7 +591,7 @@ impl WorkspaceDb { Ok(()) } SerializedPaneGroup::Pane(pane) => { - Self::save_pane(conn, workspace_id, &pane, parent)?; + Self::save_pane(conn, workspace_id, pane, parent)?; Ok(()) } } diff --git a/crates/workspace/src/searchable.rs b/crates/workspace/src/searchable.rs index e1f93f31cb22cc7fe7c7b23158de6bb034424526..8e1095b038af89149cb180632f8d36bf532234ff 100644 --- a/crates/workspace/src/searchable.rs +++ b/crates/workspace/src/searchable.rs @@ -11,7 +11,7 @@ use crate::{ ItemHandle, }; -#[derive(Debug)] +#[derive(Clone, Debug)] pub enum SearchEvent { MatchesInvalidated, ActiveMatchChanged, diff --git a/crates/workspace/src/status_bar.rs b/crates/workspace/src/status_bar.rs index 34a1412533bf14829e193df680085bc50c1166c2..f575feef7ed8046bd661c0f33d8d4e107a6dd058 100644 --- a/crates/workspace/src/status_bar.rs +++ b/crates/workspace/src/status_bar.rs @@ -1,6 +1,6 @@ use crate::{ItemHandle, Pane}; use gpui::{ - div, AnyView, IntoElement, ParentElement, Render, Styled, Subscription, View, ViewContext, + AnyView, IntoElement, ParentElement, Render, Styled, Subscription, View, ViewContext, WindowContext, }; use std::any::TypeId; @@ -34,13 +34,12 @@ pub struct StatusBar { impl Render for StatusBar { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - div() + h_flex() + .w_full() + .justify_between() + .gap_2() .py_0p5() .px_1() - .flex() - .items_center() - .justify_between() - .w_full() .h_8() .bg(cx.theme().colors().status_bar_background) .child(self.render_left_tools(cx)) @@ -51,14 +50,13 @@ impl Render for StatusBar { impl StatusBar { fn render_left_tools(&self, _: &mut ViewContext) -> impl IntoElement { h_flex() - .items_center() .gap_2() + .overflow_x_hidden() .children(self.left_items.iter().map(|item| item.to_any())) } fn render_right_tools(&self, _: &mut ViewContext) -> impl IntoElement { h_flex() - .items_center() .gap_2() .children(self.right_items.iter().rev().map(|item| item.to_any())) } diff --git a/crates/workspace/src/toolbar.rs b/crates/workspace/src/toolbar.rs index 3d5df3294e1fc84faab10c69c46c7ce424682aad..b127de8de5bea535658750e95bc181d5883cc1e2 100644 --- a/crates/workspace/src/toolbar.rs +++ b/crates/workspace/src/toolbar.rs @@ -112,18 +112,22 @@ impl Render for Toolbar { .child( h_flex() .justify_between() + .gap_2() .when(has_left_items, |this| { this.child( h_flex() - .flex_1() + .flex_auto() .justify_start() + .overflow_x_hidden() .children(self.left_items().map(|item| item.to_any())), ) }) .when(has_right_items, |this| { this.child( h_flex() - .flex_1() + // We're using `flex_none` here to prevent some flickering that can occur when the + // size of the left items container changes. + .flex_none() .justify_end() .children(self.right_items().map(|item| item.to_any())), ) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 20c8bfc94a8adffb1eeb680d95c4f6dc602b34f5..a552d9c5af645d5a76ba8010b52dae79139b022a 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -26,12 +26,12 @@ use futures::{ }; use gpui::{ actions, canvas, div, impl_actions, point, px, size, Action, AnyElement, AnyModel, AnyView, - AnyWeakView, AppContext, AsyncAppContext, AsyncWindowContext, BorrowWindow, Bounds, Context, - Div, DragMoveEvent, Element, Entity, EntityId, EventEmitter, FocusHandle, FocusableView, - GlobalPixels, InteractiveElement, IntoElement, KeyContext, LayoutId, ManagedView, Model, - ModelContext, ParentElement, PathPromptOptions, Pixels, Point, PromptLevel, Render, Size, - Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowBounds, - WindowContext, WindowHandle, WindowOptions, + AnyWeakView, AppContext, AsyncAppContext, AsyncWindowContext, Bounds, Context, Div, + DragMoveEvent, Element, ElementContext, Entity, EntityId, EventEmitter, FocusHandle, + FocusableView, GlobalPixels, InteractiveElement, IntoElement, KeyContext, LayoutId, + ManagedView, Model, ModelContext, ParentElement, PathPromptOptions, Pixels, Point, PromptLevel, + Render, Size, Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView, + WindowBounds, WindowContext, WindowHandle, WindowOptions, }; use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem}; use itertools::Itertools; @@ -669,18 +669,6 @@ impl Workspace { cx.defer(|this, cx| { this.update_window_title(cx); - // todo! @nate - these are useful for testing notifications - // this.show_error( - // &anyhow::anyhow!("what happens if this message is very very very very very long"), - // cx, - // ); - - // this.show_notification(1, cx, |cx| { - // cx.new_view(|_cx| { - // simple_message_notification::MessageNotification::new(format!("Error:")) - // .with_click_message("click here because!") - // }) - // }); }); Workspace { weak_self: weak_handle.clone(), @@ -740,7 +728,7 @@ impl Workspace { cx.spawn(|mut cx| async move { let serialized_workspace: Option = - persistence::DB.workspace_for_roots(&abs_paths.as_slice()); + persistence::DB.workspace_for_roots(abs_paths.as_slice()); let paths_to_open = Arc::new(abs_paths); @@ -860,7 +848,7 @@ impl Workspace { self.window_edited } - pub fn add_panel(&mut self, panel: View, cx: &mut ViewContext) { + pub fn add_panel(&mut self, panel: View, cx: &mut WindowContext) { let dock = match panel.position(cx) { DockPosition::Left => &self.left_dock, DockPosition::Bottom => &self.bottom_dock, @@ -1245,7 +1233,7 @@ impl Workspace { } for (pane, item) in dirty_items { let (singleton, project_entry_ids) = - cx.update(|_, cx| (item.is_singleton(cx), item.project_entry_ids(cx)))?; + cx.update(|cx| (item.is_singleton(cx), item.project_entry_ids(cx)))?; if singleton || !project_entry_ids.is_empty() { if let Some(ix) = pane.update(&mut cx, |pane, _| pane.index_for_item(item.as_ref()))? @@ -1319,7 +1307,7 @@ impl Workspace { } else { None }; - cx.update(|_, cx| open_paths(&paths, &app_state, window_to_replace, cx))? + cx.update(|cx| open_paths(&paths, &app_state, window_to_replace, cx))? .await?; Ok(()) }) @@ -1500,14 +1488,14 @@ impl Workspace { item.to_any().downcast::().ok() } - fn active_project_path(&self, cx: &ViewContext) -> Option { + fn active_project_path(&self, cx: &AppContext) -> Option { self.active_item(cx).and_then(|item| item.project_path(cx)) } pub fn save_active_item( &mut self, save_intent: SaveIntent, - cx: &mut ViewContext, + cx: &mut WindowContext, ) -> Task> { let project = self.project.clone(); let pane = self.active_pane(); @@ -1515,7 +1503,7 @@ impl Workspace { let item = pane.read(cx).active_item(); let pane = pane.downgrade(); - cx.spawn(|_, mut cx| async move { + cx.spawn(|mut cx| async move { if let Some(item) = item { Pane::save_item(project, &pane, item_ix, item.as_ref(), save_intent, &mut cx) .await @@ -1693,6 +1681,18 @@ impl Workspace { None } + /// Open the panel of the given type + pub fn open_panel(&mut self, cx: &mut ViewContext) { + for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] { + if let Some(panel_index) = dock.read(cx).panel_index_for_type::() { + dock.update(cx, |dock, cx| { + dock.activate_panel(panel_index, cx); + dock.set_open(true, cx); + }); + } + } + } + pub fn panel(&self, cx: &WindowContext) -> Option> { for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] { let dock = dock.read(cx); @@ -1776,7 +1776,7 @@ impl Workspace { } } - pub fn add_item(&mut self, item: Box, cx: &mut ViewContext) { + pub fn add_item(&mut self, item: Box, cx: &mut WindowContext) { if let Some(text) = item.telemetry_event_text(cx) { self.client() .telemetry() @@ -1858,7 +1858,7 @@ impl Workspace { path: impl Into, pane: Option>, focus_item: bool, - cx: &mut ViewContext, + cx: &mut WindowContext, ) -> Task, anyhow::Error>> { let pane = pane.unwrap_or_else(|| { self.last_active_center_pane.clone().unwrap_or_else(|| { @@ -1870,7 +1870,7 @@ impl Workspace { }); let task = self.load_path(path.into(), cx); - cx.spawn(move |_, mut cx| async move { + cx.spawn(move |mut cx| async move { let (project_entry_id, build_item) = task.await?; pane.update(&mut cx, |pane, cx| { pane.open_item(project_entry_id, focus_item, cx, build_item) @@ -1913,7 +1913,7 @@ impl Workspace { fn load_path( &mut self, path: ProjectPath, - cx: &mut ViewContext, + cx: &mut WindowContext, ) -> Task< Result<( Option, @@ -1922,9 +1922,9 @@ impl Workspace { > { let project = self.project().clone(); let project_item = project.update(cx, |project, cx| project.open_path(path, cx)); - cx.spawn(|_, mut cx| async move { + cx.spawn(|mut cx| async move { let (project_entry_id, project_item) = project_item.await?; - let build_item = cx.update(|_, cx| { + let build_item = cx.update(|cx| { cx.default_global::() .get(&project_item.entity_type()) .ok_or_else(|| anyhow!("no item builder for project item")) @@ -1992,7 +1992,7 @@ impl Workspace { } } - pub fn activate_item(&mut self, item: &dyn ItemHandle, cx: &mut ViewContext) -> bool { + pub fn activate_item(&mut self, item: &dyn ItemHandle, cx: &mut WindowContext) -> bool { let result = self.panes.iter().find_map(|pane| { pane.read(cx) .index_for_item(item) @@ -2015,7 +2015,7 @@ impl Workspace { } } - pub fn activate_next_pane(&mut self, cx: &mut ViewContext) { + pub fn activate_next_pane(&mut self, cx: &mut WindowContext) { let panes = self.center.panes(); if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) { let next_ix = (ix + 1) % panes.len(); @@ -2024,7 +2024,7 @@ impl Workspace { } } - pub fn activate_previous_pane(&mut self, cx: &mut ViewContext) { + pub fn activate_previous_pane(&mut self, cx: &mut WindowContext) { let panes = self.center.panes(); if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) { let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1); @@ -2036,7 +2036,7 @@ impl Workspace { pub fn activate_pane_in_direction( &mut self, direction: SplitDirection, - cx: &mut ViewContext, + cx: &mut WindowContext, ) { if let Some(pane) = self.find_pane_in_direction(direction, cx) { cx.focus_view(pane); @@ -2060,7 +2060,7 @@ impl Workspace { fn find_pane_in_direction( &mut self, direction: SplitDirection, - cx: &mut ViewContext, + cx: &AppContext, ) -> Option<&View> { let Some(bounding_box) = self.center.bounding_box_for_pane(&self.active_pane) else { return None; @@ -2495,14 +2495,14 @@ impl Workspace { .any(|state| state.leader_id == peer_id) } - fn active_item_path_changed(&mut self, cx: &mut ViewContext) { + fn active_item_path_changed(&mut self, cx: &mut WindowContext) { let active_entry = self.active_project_path(cx); self.project .update(cx, |project, cx| project.set_active_path(active_entry, cx)); self.update_window_title(cx); } - fn update_window_title(&mut self, cx: &mut ViewContext) { + fn update_window_title(&mut self, cx: &mut WindowContext) { let project = self.project().read(cx); let mut title = String::new(); @@ -2546,7 +2546,7 @@ impl Workspace { cx.set_window_title(&title); } - fn update_window_edited(&mut self, cx: &mut ViewContext) { + fn update_window_edited(&mut self, cx: &mut WindowContext) { let is_edited = !self.project.read(cx).is_disconnected() && self .items(cx) @@ -2721,7 +2721,7 @@ impl Workspace { ) -> Result<()> { let this = this.upgrade().context("workspace dropped")?; - let item_builders = cx.update(|_, cx| { + let item_builders = cx.update(|cx| { cx.default_global::() .values() .map(|b| b.0) @@ -2740,7 +2740,7 @@ impl Workspace { Err(anyhow!("missing view variant"))?; } for build_item in &item_builders { - let task = cx.update(|_, cx| { + let task = cx.update(|cx| { build_item(pane.clone(), this.clone(), id, &mut variant, cx) })?; if let Some(task) = task { @@ -2771,7 +2771,7 @@ impl Workspace { Ok(()) } - fn update_active_view_for_followers(&mut self, cx: &mut ViewContext) { + fn update_active_view_for_followers(&mut self, cx: &mut WindowContext) { let mut is_project_item = true; let mut update = proto::UpdateActiveView::default(); @@ -2888,7 +2888,7 @@ impl Workspace { &self, peer_id: PeerId, pane: &View, - cx: &mut ViewContext, + cx: &mut WindowContext, ) -> Option> { let call = self.active_call()?; let room = call.read(cx).room()?.read(cx); @@ -3005,7 +3005,7 @@ impl Workspace { })); } - fn serialize_workspace(&self, cx: &mut ViewContext) { + fn serialize_workspace(&self, cx: &mut WindowContext) { fn serialize_pane_handle(pane_handle: &View, cx: &WindowContext) -> SerializedPane { let (items, active) = { let pane = pane_handle.read(cx); @@ -3046,15 +3046,12 @@ impl Workspace { flexes: Some(flexes.lock().clone()), }, Member::Pane(pane_handle) => { - SerializedPaneGroup::Pane(serialize_pane_handle(&pane_handle, cx)) + SerializedPaneGroup::Pane(serialize_pane_handle(pane_handle, cx)) } } } - fn build_serialized_docks( - this: &Workspace, - cx: &mut ViewContext, - ) -> DockStructure { + fn build_serialized_docks(this: &Workspace, cx: &mut WindowContext) -> DockStructure { let left_dock = this.left_dock.read(cx); let left_visible = left_dock.is_open(); let left_active_panel = left_dock @@ -3121,7 +3118,7 @@ impl Workspace { docks, }; - cx.spawn(|_, _| persistence::DB.save_workspace(serialized_workspace)) + cx.spawn(|_| persistence::DB.save_workspace(serialized_workspace)) .detach(); } } @@ -3153,7 +3150,7 @@ impl Workspace { center_group = Some((group, active_pane)) } - let mut items_by_project_path = cx.update(|_, cx| { + let mut items_by_project_path = cx.update(|cx| { center_items .unwrap_or_default() .into_iter() @@ -3190,49 +3187,19 @@ impl Workspace { } let docks = serialized_workspace.docks; - workspace.left_dock.update(cx, |dock, cx| { - dock.set_open(docks.left.visible, cx); - if let Some(active_panel) = docks.left.active_panel { - if let Some(ix) = dock.panel_index_for_persistent_name(&active_panel, cx) { - dock.activate_panel(ix, cx); - } - } - dock.active_panel() - .map(|panel| panel.set_zoomed(docks.left.zoom, cx)); - if docks.left.visible && docks.left.zoom { - cx.focus_self() - } - }); - // TODO: I think the bug is that setting zoom or active undoes the bottom zoom or something - workspace.right_dock.update(cx, |dock, cx| { - dock.set_open(docks.right.visible, cx); - if let Some(active_panel) = docks.right.active_panel { - if let Some(ix) = dock.panel_index_for_persistent_name(&active_panel, cx) { - dock.activate_panel(ix, cx); - } - } - dock.active_panel() - .map(|panel| panel.set_zoomed(docks.right.zoom, cx)); - - if docks.right.visible && docks.right.zoom { - cx.focus_self() - } - }); - workspace.bottom_dock.update(cx, |dock, cx| { - dock.set_open(docks.bottom.visible, cx); - if let Some(active_panel) = docks.bottom.active_panel { - if let Some(ix) = dock.panel_index_for_persistent_name(&active_panel, cx) { - dock.activate_panel(ix, cx); - } - } - - dock.active_panel() - .map(|panel| panel.set_zoomed(docks.bottom.zoom, cx)); - if docks.bottom.visible && docks.bottom.zoom { - cx.focus_self() - } - }); + let right = docks.right.clone(); + workspace + .right_dock + .update(cx, |dock, _| dock.serialized_dock = Some(right)); + let left = docks.left.clone(); + workspace + .left_dock + .update(cx, |dock, _| dock.serialized_dock = Some(left)); + let bottom = docks.bottom.clone(); + workspace + .bottom_dock + .update(cx, |dock, _| dock.serialized_dock = Some(bottom)); cx.notify(); })?; @@ -3362,14 +3329,11 @@ impl Workspace { self.modal_layer.read(cx).has_active_modal() } - pub fn active_modal( - &mut self, - cx: &ViewContext, - ) -> Option> { + pub fn active_modal(&mut self, cx: &AppContext) -> Option> { self.modal_layer.read(cx).active_modal() } - pub fn toggle_modal(&mut self, cx: &mut ViewContext, build: B) + pub fn toggle_modal(&mut self, cx: &mut WindowContext, build: B) where B: FnOnce(&mut ViewContext) -> V, { @@ -3419,7 +3383,7 @@ fn open_items( let restored_project_paths = restored_items .iter() .filter_map(|item| { - cx.update(|_, cx| item.as_ref()?.project_path(cx)) + cx.update(|cx| item.as_ref()?.project_path(cx)) .ok() .flatten() }) @@ -3557,9 +3521,14 @@ impl Render for Workspace { .border_b() .border_color(colors.border) .child( - canvas(cx.listener(|workspace, bounds, _| { - workspace.bounds = *bounds; - })) + canvas({ + let this = cx.view().clone(); + move |bounds, cx| { + this.update(cx, |this, _cx| { + this.bounds = *bounds; + }) + } + }) .absolute() .size_full(), ) @@ -4246,7 +4215,7 @@ pub fn restart(_: &Restart, cx: &mut AppContext) { // If multiple windows have unsaved changes, and need a save prompt, // prompt in the active window before switching to a different window. - workspace_windows.sort_by_key(|window| window.is_active(&cx) == Some(false)); + workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false)); let mut prompt = None; if let (true, Some(window)) = (should_confirm, workspace_windows.first()) { @@ -4311,7 +4280,7 @@ impl Element for DisconnectedOverlay { fn request_layout( &mut self, _: Option, - cx: &mut WindowContext, + cx: &mut ElementContext, ) -> (LayoutId, Self::State) { let mut background = cx.theme().colors().elevated_surface_background; background.fade_out(0.2); @@ -4333,8 +4302,13 @@ impl Element for DisconnectedOverlay { (overlay.request_layout(cx), overlay) } - fn paint(&mut self, bounds: Bounds, overlay: &mut Self::State, cx: &mut WindowContext) { - cx.with_z_index(u8::MAX, |cx| { + fn paint( + &mut self, + bounds: Bounds, + overlay: &mut Self::State, + cx: &mut ElementContext, + ) { + cx.with_z_index(u16::MAX, |cx| { cx.add_opaque_layer(bounds); overlay.paint(cx); }) diff --git a/crates/zed/resources/app-icon-nightly.png b/crates/zed/resources/app-icon-nightly.png index 35b3173478a9fbb58d380dd7bc70f1a16b99ce01..5f1304a6af8d57bbf7414d175611829989d122da 100644 Binary files a/crates/zed/resources/app-icon-nightly.png and b/crates/zed/resources/app-icon-nightly.png differ diff --git a/crates/zed/resources/app-icon-nightly@2x.png b/crates/zed/resources/app-icon-nightly@2x.png index 8766bc08ae547a6748d22f4f1baae9fdecbd71b4..edb416ede489be0733bdbb00c467a1d828173e56 100644 Binary files a/crates/zed/resources/app-icon-nightly@2x.png and b/crates/zed/resources/app-icon-nightly@2x.png differ diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index be07ba6a5faed48a1a43940aef2e55198f2e9332..b25145367070a23e127c10284c67704a7cfebd4f 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -544,22 +544,7 @@ fn init_panic_hook(app: &App, installation_id: Option, session_id: Strin let mut backtrace = backtrace .frames() .iter() - .flat_map(|frame| { - frame.symbols().iter().filter_map(|symbol| { - let name = symbol.name()?; - let addr = symbol.addr()? as usize; - let position = if let (Some(path), Some(lineno)) = ( - symbol.filename().and_then(|path| path.file_name()), - symbol.lineno(), - ) { - format!("{}:{}", path.to_string_lossy(), lineno) - } else { - "?".to_string() - }; - - Some(format!("{:} ({:#x}) at {}", name, addr, position)) - }) - }) + .filter_map(|frame| Some(format!("{:#}", frame.symbols().first()?.name()?))) .collect::>(); // Strip out leading stack frames for rust panic-handling. diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 112c219d2d728b9a21b36d05554358d47a2b0aa2..e2b64a1c9362a672175577de6568990e1064ab7f 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -32,11 +32,11 @@ use util::{ }; use uuid::Uuid; use welcome::BaseKeymap; +use workspace::Pane; use workspace::{ create_and_open_local_file, notifications::simple_message_notification::MessageNotification, open_new, AppState, NewFile, NewWindow, Workspace, WorkspaceSettings, }; -use workspace::{dock::Panel, Pane}; use zed_actions::{OpenBrowser, OpenSettings, OpenZedURL, Quit}; actions!( @@ -178,10 +178,7 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { )?; workspace_handle.update(&mut cx, |workspace, cx| { - let (position, was_deserialized) = { - let project_panel = project_panel.read(cx); - (project_panel.position(cx), project_panel.was_deserialized()) - }; + let was_deserialized = project_panel.read(cx).was_deserialized(); workspace.add_panel(project_panel, cx); workspace.add_panel(terminal_panel, cx); workspace.add_panel(assistant_panel, cx); @@ -200,7 +197,7 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { .map_or(false, |entry| entry.is_dir()) }) { - workspace.toggle_dock(position, cx); + workspace.open_panel::(cx); } cx.focus_self(); }) @@ -875,7 +872,7 @@ mod tests { let window = cx.update(|cx| cx.windows()[0].downcast::().unwrap()); let window_is_edited = |window: WindowHandle, cx: &mut TestAppContext| { - cx.test_window(window.into()).edited() + cx.update(|cx| window.read(cx).unwrap().is_edited()) }; let pane = window .read_with(cx, |workspace, _| workspace.active_pane().clone()) diff --git a/script/deploy-collab b/script/deploy-collab index 8dbee18e3b78cfb1a26a74338509a69db54b0307..54442d5ddfc89fe74475914e5c99e4a63c8b059b 100755 --- a/script/deploy-collab +++ b/script/deploy-collab @@ -19,5 +19,6 @@ export ZED_IMAGE_ID=${image_id} target_zed_kube_cluster envsubst < crates/collab/k8s/collab.template.yml | kubectl apply -f - +kubectl -n "$environment" rollout status deployment/collab --watch echo "deployed collab v${version} to ${environment}" diff --git a/script/histogram b/script/histogram new file mode 100755 index 0000000000000000000000000000000000000000..b9885fbc006d9cbd284211a705d261041dae1630 --- /dev/null +++ b/script/histogram @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 + +# Required dependencies for this script: +# +# pandas: For data manipulation and analysis. +# matplotlib: For creating static, interactive, and animated visualizations in Python. +# seaborn: For making statistical graphics in Python, based on matplotlib. + +# To install these dependencies, use the following pip command: +# pip install pandas matplotlib seaborn + +# This script is designed to parse log files for performance measurements and create histograms of these measurements. +# It expects log files to contain lines with measurements in the format "measurement: timeunit" where timeunit can be in milliseconds (ms) or microseconds (µs). +# Lines that do not contain a colon ':' are skipped. +# The script takes one or more file paths as command-line arguments, parses each log file, and then combines the data into a single DataFrame. +# It then converts all time measurements into milliseconds, discards the original time and unit columns, and creates histograms for each unique measurement type. +# The histograms display the distribution of times for each measurement, separated by log file, and normalized to show density rather than count. +# To use this script, run it from the command line with the log file paths as arguments, like so: +# python this_script.py log1.txt log2.txt ... +# The script will then parse the provided log files and display the histograms for each type of measurement found. + +import pandas as pd +import matplotlib.pyplot as plt +import seaborn as sns +import sys + +def parse_log_file(file_path): + data = {'measurement': [], 'time': [], 'unit': [], 'log_file': []} + with open(file_path, 'r') as file: + for line in file: + if ':' not in line: + continue + + parts = line.strip().split(': ') + if len(parts) != 2: + continue + + measurement, time_with_unit = parts[0], parts[1] + if 'ms' in time_with_unit: + time, unit = time_with_unit[:-2], 'ms' + elif 'µs' in time_with_unit: + time, unit = time_with_unit[:-2], 'µs' + else: + raise ValueError(f"Invalid time unit in line: {line.strip()}") + continue + + data['measurement'].append(measurement) + data['time'].append(float(time)) + data['unit'].append(unit) + data['log_file'].append(file_path.split('/')[-1]) + return pd.DataFrame(data) + +def create_histograms(df, measurement): + filtered_df = df[df['measurement'] == measurement] + plt.figure(figsize=(12, 6)) + sns.histplot(data=filtered_df, x='time_ms', hue='log_file', element='step', stat='density', common_norm=False, palette='bright') + plt.title(f'Histogram of {measurement}') + plt.xlabel('Time (ms)') + plt.ylabel('Density') + plt.grid(True) + plt.xlim(filtered_df['time_ms'].quantile(0.01), filtered_df['time_ms'].quantile(0.99)) + plt.show() + + +file_paths = sys.argv[1:] +dfs = [parse_log_file(path) for path in file_paths] +combined_df = pd.concat(dfs, ignore_index=True) +combined_df['time_ms'] = combined_df.apply(lambda row: row['time'] if row['unit'] == 'ms' else row['time'] / 1000, axis=1) +combined_df.drop(['time', 'unit'], axis=1, inplace=True) + +measurement_types = combined_df['measurement'].unique() +for measurement in measurement_types: + create_histograms(combined_df, measurement)