Compare commits
4 Commits
v1.0.0-bet
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 1f04a824d7 | |||
| c78e0a5163 | |||
| 5d4c8634ef | |||
| 4efeec08c4 |
1000
Cargo.lock
generated
1000
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
14
Cargo.toml
14
Cargo.toml
@@ -4,7 +4,7 @@ members = ["crates/*", "desktop", "web"]
|
|||||||
default-members = ["desktop"]
|
default-members = ["desktop"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "1.0.0-beta4"
|
version = "1.0.0-beta5"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
@@ -19,12 +19,12 @@ gpui_tokio = { git = "https://github.com/zed-industries/zed" }
|
|||||||
reqwest_client = { git = "https://github.com/zed-industries/zed" }
|
reqwest_client = { git = "https://github.com/zed-industries/zed" }
|
||||||
|
|
||||||
# Nostr
|
# Nostr
|
||||||
nostr-lmdb = { git = "https://github.com/rust-nostr/nostr" }
|
nostr-lmdb = { git = "https://github.com/rust-nostr/nostr", rev = "46d66396467e07c3dfb71e5102104ecb7e9c6b64" }
|
||||||
nostr-connect = { git = "https://github.com/rust-nostr/nostr" }
|
nostr-connect = { git = "https://github.com/rust-nostr/nostr", rev = "46d66396467e07c3dfb71e5102104ecb7e9c6b64" }
|
||||||
nostr-blossom = { git = "https://github.com/rust-nostr/nostr" }
|
nostr-blossom = { git = "https://github.com/rust-nostr/nostr", rev = "46d66396467e07c3dfb71e5102104ecb7e9c6b64" }
|
||||||
nostr-gossip-memory = { git = "https://github.com/rust-nostr/nostr" }
|
nostr-gossip-memory = { git = "https://github.com/rust-nostr/nostr", rev = "46d66396467e07c3dfb71e5102104ecb7e9c6b64" }
|
||||||
nostr-sdk = { git = "https://github.com/rust-nostr/nostr" }
|
nostr-sdk = { git = "https://github.com/rust-nostr/nostr", rev = "46d66396467e07c3dfb71e5102104ecb7e9c6b64" }
|
||||||
nostr = { git = "https://github.com/rust-nostr/nostr", features = [ "nip96", "nip59", "nip49", "nip44" ] }
|
nostr = { git = "https://github.com/rust-nostr/nostr", features = [ "nip96", "nip59", "nip49", "nip44" ], rev = "46d66396467e07c3dfb71e5102104ecb7e9c6b64" }
|
||||||
|
|
||||||
# Others
|
# Others
|
||||||
anyhow = "1.0.44"
|
anyhow = "1.0.44"
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ use theme::ActiveTheme;
|
|||||||
use ui::avatar::Avatar;
|
use ui::avatar::Avatar;
|
||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
use ui::dock::{Panel, PanelEvent};
|
use ui::dock::{Panel, PanelEvent};
|
||||||
use ui::input::{InputEvent, InputState, TextInput};
|
use ui::input::{Input, InputEvent, InputState};
|
||||||
use ui::menu::DropdownMenu;
|
use ui::menu::DropdownMenu;
|
||||||
use ui::notification::Notification;
|
use ui::notification::Notification;
|
||||||
use ui::scroll::Scrollbar;
|
use ui::scroll::Scrollbar;
|
||||||
@@ -119,7 +119,6 @@ impl ChatPanel {
|
|||||||
InputState::new(window, cx)
|
InputState::new(window, cx)
|
||||||
.placeholder(format!("Message {}", name))
|
.placeholder(format!("Message {}", name))
|
||||||
.auto_grow(1, 20)
|
.auto_grow(1, 20)
|
||||||
.prevent_new_line_on_enter()
|
|
||||||
.clean_on_escape()
|
.clean_on_escape()
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1498,7 +1497,7 @@ impl Render for ChatPanel {
|
|||||||
.border_b_1()
|
.border_b_1()
|
||||||
.border_color(cx.theme().border)
|
.border_color(cx.theme().border)
|
||||||
.child(
|
.child(
|
||||||
TextInput::new(&self.subject_input)
|
Input::new(&self.subject_input)
|
||||||
.text_sm()
|
.text_sm()
|
||||||
.small()
|
.small()
|
||||||
.bordered(false),
|
.bordered(false),
|
||||||
@@ -1553,12 +1552,7 @@ impl Render for ChatPanel {
|
|||||||
this.upload(window, cx);
|
this.upload(window, cx);
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
.child(
|
.child(Input::new(&self.input).appearance(false).text_sm().flex_1())
|
||||||
TextInput::new(&self.input)
|
|
||||||
.appearance(false)
|
|
||||||
.text_sm()
|
|
||||||
.flex_1(),
|
|
||||||
)
|
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.pl_1()
|
.pl_1()
|
||||||
|
|||||||
@@ -220,14 +220,6 @@ impl NostrRegistry {
|
|||||||
let client = self.client();
|
let client = self.client();
|
||||||
|
|
||||||
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
||||||
// Add search relay to the relay pool
|
|
||||||
for url in SEARCH_RELAYS.into_iter() {
|
|
||||||
client
|
|
||||||
.add_relay(url)
|
|
||||||
.capabilities(RelayCapabilities::READ)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add indexer relay to the relay pool
|
// Add indexer relay to the relay pool
|
||||||
for url in INDEXER_RELAYS.into_iter() {
|
for url in INDEXER_RELAYS.into_iter() {
|
||||||
client
|
client
|
||||||
@@ -633,6 +625,18 @@ impl NostrRegistry {
|
|||||||
return Ok(results);
|
return Ok(results);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add search relay to the relay pool
|
||||||
|
for url in SEARCH_RELAYS.into_iter() {
|
||||||
|
if client.relay(url).await.is_ok() {
|
||||||
|
client
|
||||||
|
.add_relay(url)
|
||||||
|
.capabilities(RelayCapabilities::READ)
|
||||||
|
.await?;
|
||||||
|
} else {
|
||||||
|
return Err(anyhow!("Failed to add search relay: {}", url));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Return early if the query is a valid public key
|
// Return early if the query is a valid public key
|
||||||
if let Ok(public_key) = PublicKey::parse(&query) {
|
if let Ok(public_key) = PublicKey::parse(&query) {
|
||||||
results.push(public_key);
|
results.push(public_key);
|
||||||
|
|||||||
@@ -22,5 +22,6 @@ uuid = "1.10"
|
|||||||
regex = "1"
|
regex = "1"
|
||||||
image = "0.25.1"
|
image = "0.25.1"
|
||||||
lsp-types = "0.97.0"
|
lsp-types = "0.97.0"
|
||||||
rope = { git = "https://github.com/zed-industries/zed" }
|
ropey = { version = "=2.0.0-beta.1", features = ["metric_lines_lf", "metric_utf16"] }
|
||||||
sum_tree = { git = "https://github.com/zed-industries/zed" }
|
sum_tree = { git = "https://github.com/zed-industries/zed" }
|
||||||
|
tree-sitter = "0.26"
|
||||||
|
|||||||
@@ -1,191 +0,0 @@
|
|||||||
Copyright 2024 Longbridge <https://longbridge.com>
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
|
|
||||||
|
|
||||||
Apache License
|
|
||||||
Version 2.0, January 2004
|
|
||||||
http://www.apache.org/licenses/
|
|
||||||
|
|
||||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
||||||
|
|
||||||
1. Definitions.
|
|
||||||
|
|
||||||
"License" shall mean the terms and conditions for use, reproduction,
|
|
||||||
and distribution as defined by Sections 1 through 9 of this document.
|
|
||||||
|
|
||||||
"Licensor" shall mean the copyright owner or entity authorized by
|
|
||||||
the copyright owner that is granting the License.
|
|
||||||
|
|
||||||
"Legal Entity" shall mean the union of the acting entity and all
|
|
||||||
other entities that control, are controlled by, or are under common
|
|
||||||
control with that entity. For the purposes of this definition,
|
|
||||||
"control" means (i) the power, direct or indirect, to cause the
|
|
||||||
direction or management of such entity, whether by contract or
|
|
||||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
||||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
||||||
|
|
||||||
"You" (or "Your") shall mean an individual or Legal Entity
|
|
||||||
exercising permissions granted by this License.
|
|
||||||
|
|
||||||
"Source" form shall mean the preferred form for making modifications,
|
|
||||||
including but not limited to software source code, documentation
|
|
||||||
source, and configuration files.
|
|
||||||
|
|
||||||
"Object" form shall mean any form resulting from mechanical
|
|
||||||
transformation or translation of a Source form, including but
|
|
||||||
not limited to compiled object code, generated documentation,
|
|
||||||
and conversions to other media types.
|
|
||||||
|
|
||||||
"Work" shall mean the work of authorship, whether in Source or
|
|
||||||
Object form, made available under the License, as indicated by a
|
|
||||||
copyright notice that is included in or attached to the work
|
|
||||||
(an example is provided in the Appendix below).
|
|
||||||
|
|
||||||
"Derivative Works" shall mean any work, whether in Source or Object
|
|
||||||
form, that is based on (or derived from) the Work and for which the
|
|
||||||
editorial revisions, annotations, elaborations, or other modifications
|
|
||||||
represent, as a whole, an original work of authorship. For the purposes
|
|
||||||
of this License, Derivative Works shall not include works that remain
|
|
||||||
separable from, or merely link (or bind by name) to the interfaces of,
|
|
||||||
the Work and Derivative Works thereof.
|
|
||||||
|
|
||||||
"Contribution" shall mean any work of authorship, including
|
|
||||||
the original version of the Work and any modifications or additions
|
|
||||||
to that Work or Derivative Works thereof, that is intentionally
|
|
||||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
||||||
or by an individual or Legal Entity authorized to submit on behalf of
|
|
||||||
the copyright owner. For the purposes of this definition, "submitted"
|
|
||||||
means any form of electronic, verbal, or written communication sent
|
|
||||||
to the Licensor or its representatives, including but not limited to
|
|
||||||
communication on electronic mailing lists, source code control systems,
|
|
||||||
and issue tracking systems that are managed by, or on behalf of, the
|
|
||||||
Licensor for the purpose of discussing and improving the Work, but
|
|
||||||
excluding communication that is conspicuously marked or otherwise
|
|
||||||
designated in writing by the copyright owner as "Not a Contribution."
|
|
||||||
|
|
||||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
||||||
on behalf of whom a Contribution has been received by Licensor and
|
|
||||||
subsequently incorporated within the Work.
|
|
||||||
|
|
||||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
copyright license to reproduce, prepare Derivative Works of,
|
|
||||||
publicly display, publicly perform, sublicense, and distribute the
|
|
||||||
Work and such Derivative Works in Source or Object form.
|
|
||||||
|
|
||||||
3. Grant of Patent License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
(except as stated in this section) patent license to make, have made,
|
|
||||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
||||||
where such license applies only to those patent claims licensable
|
|
||||||
by such Contributor that are necessarily infringed by their
|
|
||||||
Contribution(s) alone or by combination of their Contribution(s)
|
|
||||||
with the Work to which such Contribution(s) was submitted. If You
|
|
||||||
institute patent litigation against any entity (including a
|
|
||||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
||||||
or a Contribution incorporated within the Work constitutes direct
|
|
||||||
or contributory patent infringement, then any patent licenses
|
|
||||||
granted to You under this License for that Work shall terminate
|
|
||||||
as of the date such litigation is filed.
|
|
||||||
|
|
||||||
4. Redistribution. You may reproduce and distribute copies of the
|
|
||||||
Work or Derivative Works thereof in any medium, with or without
|
|
||||||
modifications, and in Source or Object form, provided that You
|
|
||||||
meet the following conditions:
|
|
||||||
|
|
||||||
(a) You must give any other recipients of the Work or
|
|
||||||
Derivative Works a copy of this License; and
|
|
||||||
|
|
||||||
(b) You must cause any modified files to carry prominent notices
|
|
||||||
stating that You changed the files; and
|
|
||||||
|
|
||||||
(c) You must retain, in the Source form of any Derivative Works
|
|
||||||
that You distribute, all copyright, patent, trademark, and
|
|
||||||
attribution notices from the Source form of the Work,
|
|
||||||
excluding those notices that do not pertain to any part of
|
|
||||||
the Derivative Works; and
|
|
||||||
|
|
||||||
(d) If the Work includes a "NOTICE" text file as part of its
|
|
||||||
distribution, then any Derivative Works that You distribute must
|
|
||||||
include a readable copy of the attribution notices contained
|
|
||||||
within such NOTICE file, excluding those notices that do not
|
|
||||||
pertain to any part of the Derivative Works, in at least one
|
|
||||||
of the following places: within a NOTICE text file distributed
|
|
||||||
as part of the Derivative Works; within the Source form or
|
|
||||||
documentation, if provided along with the Derivative Works; or,
|
|
||||||
within a display generated by the Derivative Works, if and
|
|
||||||
wherever such third-party notices normally appear. The contents
|
|
||||||
of the NOTICE file are for informational purposes only and
|
|
||||||
do not modify the License. You may add Your own attribution
|
|
||||||
notices within Derivative Works that You distribute, alongside
|
|
||||||
or as an addendum to the NOTICE text from the Work, provided
|
|
||||||
that such additional attribution notices cannot be construed
|
|
||||||
as modifying the License.
|
|
||||||
|
|
||||||
You may add Your own copyright statement to Your modifications and
|
|
||||||
may provide additional or different license terms and conditions
|
|
||||||
for use, reproduction, or distribution of Your modifications, or
|
|
||||||
for any such Derivative Works as a whole, provided Your use,
|
|
||||||
reproduction, and distribution of the Work otherwise complies with
|
|
||||||
the conditions stated in this License.
|
|
||||||
|
|
||||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
||||||
any Contribution intentionally submitted for inclusion in the Work
|
|
||||||
by You to the Licensor shall be under the terms and conditions of
|
|
||||||
this License, without any additional terms or conditions.
|
|
||||||
Notwithstanding the above, nothing herein shall supersede or modify
|
|
||||||
the terms of any separate license agreement you may have executed
|
|
||||||
with Licensor regarding such Contributions.
|
|
||||||
|
|
||||||
6. Trademarks. This License does not grant permission to use the trade
|
|
||||||
names, trademarks, service marks, or product names of the Licensor,
|
|
||||||
except as required for reasonable and customary use in describing the
|
|
||||||
origin of the Work and reproducing the content of the NOTICE file.
|
|
||||||
|
|
||||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
||||||
agreed to in writing, Licensor provides the Work (and each
|
|
||||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
||||||
implied, including, without limitation, any warranties or conditions
|
|
||||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
||||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
||||||
appropriateness of using or redistributing the Work and assume any
|
|
||||||
risks associated with Your exercise of permissions under this License.
|
|
||||||
|
|
||||||
8. Limitation of Liability. In no event and under no legal theory,
|
|
||||||
whether in tort (including negligence), contract, or otherwise,
|
|
||||||
unless required by applicable law (such as deliberate and grossly
|
|
||||||
negligent acts) or agreed to in writing, shall any Contributor be
|
|
||||||
liable to You for damages, including any direct, indirect, special,
|
|
||||||
incidental, or consequential damages of any character arising as a
|
|
||||||
result of this License or out of the use or inability to use the
|
|
||||||
Work (including but not limited to damages for loss of goodwill,
|
|
||||||
work stoppage, computer failure or malfunction, or any and all
|
|
||||||
other commercial damages or losses), even if such Contributor
|
|
||||||
has been advised of the possibility of such damages.
|
|
||||||
|
|
||||||
9. Accepting Warranty or Additional Liability. While redistributing
|
|
||||||
the Work or Derivative Works thereof, You may choose to offer,
|
|
||||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
||||||
or other liability obligations and/or rights consistent with this
|
|
||||||
License. However, in accepting such obligations, You may act only
|
|
||||||
on Your own behalf and on Your sole responsibility, not on behalf
|
|
||||||
of any other Contributor, and only if You agree to indemnify,
|
|
||||||
defend, and hold each Contributor harmless for any liability
|
|
||||||
incurred by, or claims asserted against, such Contributor by reason
|
|
||||||
of your accepting any such warranty or additional liability.
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
|
||||||
@@ -750,7 +750,7 @@ impl TabPanel {
|
|||||||
div()
|
div()
|
||||||
.id("tab-bar-empty-space")
|
.id("tab-bar-empty-space")
|
||||||
.h_full()
|
.h_full()
|
||||||
.flex_grow()
|
.flex_grow_1()
|
||||||
.min_w_16()
|
.min_w_16()
|
||||||
.when(state.droppable, |this| {
|
.when(state.droppable, |this| {
|
||||||
let view = cx.entity();
|
let view = cx.entity();
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
/// A HistoryItem represents a single change in the history.
|
||||||
|
/// It must implement Clone and PartialEq to be used in the History.
|
||||||
pub trait HistoryItem: Clone + PartialEq {
|
pub trait HistoryItem: Clone + PartialEq {
|
||||||
fn version(&self) -> usize;
|
fn version(&self) -> usize;
|
||||||
fn set_version(&mut self, version: usize);
|
fn set_version(&mut self, version: usize);
|
||||||
@@ -22,10 +24,11 @@ pub struct History<I: HistoryItem> {
|
|||||||
redos: Vec<I>,
|
redos: Vec<I>,
|
||||||
last_changed_at: Instant,
|
last_changed_at: Instant,
|
||||||
version: usize,
|
version: usize,
|
||||||
max_undo: usize,
|
pub(crate) ignore: bool,
|
||||||
|
max_undos: usize,
|
||||||
group_interval: Option<Duration>,
|
group_interval: Option<Duration>,
|
||||||
|
grouping: bool,
|
||||||
unique: bool,
|
unique: bool,
|
||||||
pub ignore: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<I> History<I>
|
impl<I> History<I>
|
||||||
@@ -39,15 +42,16 @@ where
|
|||||||
ignore: false,
|
ignore: false,
|
||||||
last_changed_at: Instant::now(),
|
last_changed_at: Instant::now(),
|
||||||
version: 0,
|
version: 0,
|
||||||
max_undo: 1000,
|
max_undos: 1000,
|
||||||
group_interval: None,
|
group_interval: None,
|
||||||
|
grouping: false,
|
||||||
unique: false,
|
unique: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the maximum number of undo steps to keep, defaults to 1000.
|
/// Set the maximum number of undo steps to keep, defaults to 1000.
|
||||||
pub fn max_undo(mut self, max_undo: usize) -> Self {
|
pub fn max_undos(mut self, max_undos: usize) -> Self {
|
||||||
self.max_undo = max_undo;
|
self.max_undos = max_undos;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,10 +68,20 @@ where
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Start grouping changes, this will prevent the version from being incremented until `end_grouping` is called.
|
||||||
|
pub fn start_grouping(&mut self) {
|
||||||
|
self.grouping = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// End grouping changes, this will allow the version to be incremented again.
|
||||||
|
pub fn end_grouping(&mut self) {
|
||||||
|
self.grouping = false;
|
||||||
|
}
|
||||||
|
|
||||||
/// Increment the version number if the last change was made more than `GROUP_INTERVAL` milliseconds ago.
|
/// Increment the version number if the last change was made more than `GROUP_INTERVAL` milliseconds ago.
|
||||||
fn inc_version(&mut self) -> usize {
|
fn inc_version(&mut self) -> usize {
|
||||||
let t = Instant::now();
|
let t = Instant::now();
|
||||||
if Some(self.last_changed_at.elapsed()) > self.group_interval {
|
if !self.grouping && Some(self.last_changed_at.elapsed()) > self.group_interval {
|
||||||
self.version += 1;
|
self.version += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,10 +94,11 @@ where
|
|||||||
self.version
|
self.version
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Push a new change to the history.
|
||||||
pub fn push(&mut self, item: I) {
|
pub fn push(&mut self, item: I) {
|
||||||
let version = self.inc_version();
|
let version = self.inc_version();
|
||||||
|
|
||||||
if self.undos.len() >= self.max_undo {
|
if self.undos.len() >= self.max_undos {
|
||||||
self.undos.remove(0);
|
self.undos.remove(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,6 +128,7 @@ where
|
|||||||
self.redos.clear();
|
self.redos.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Undo the last change and return the changes that were undone.
|
||||||
pub fn undo(&mut self) -> Option<Vec<I>> {
|
pub fn undo(&mut self) -> Option<Vec<I>> {
|
||||||
if let Some(first_change) = self.undos.pop() {
|
if let Some(first_change) = self.undos.pop() {
|
||||||
let mut changes = vec![first_change.clone()];
|
let mut changes = vec![first_change.clone()];
|
||||||
@@ -135,6 +151,7 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Redo the last undone change and return the changes that were redone.
|
||||||
pub fn redo(&mut self) -> Option<Vec<I>> {
|
pub fn redo(&mut self) -> Option<Vec<I>> {
|
||||||
if let Some(first_change) = self.redos.pop() {
|
if let Some(first_change) = self.redos.pop() {
|
||||||
let mut changes = vec![first_change.clone()];
|
let mut changes = vec![first_change.clone()];
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use gpui::{px, Context, Pixels};
|
use gpui::{Context, Pixels, Task, px};
|
||||||
|
|
||||||
static INTERVAL: Duration = Duration::from_millis(500);
|
static INTERVAL: Duration = Duration::from_millis(500);
|
||||||
static PAUSE_DELAY: Duration = Duration::from_millis(300);
|
static PAUSE_DELAY: Duration = Duration::from_millis(300);
|
||||||
|
|
||||||
|
// On Windows, Linux, we should use integer to avoid blurry cursor.
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
pub(super) const CURSOR_WIDTH: Pixels = px(2.);
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
pub(super) const CURSOR_WIDTH: Pixels = px(1.5);
|
pub(super) const CURSOR_WIDTH: Pixels = px(1.5);
|
||||||
|
|
||||||
/// To manage the Input cursor blinking.
|
/// To manage the Input cursor blinking.
|
||||||
@@ -12,10 +17,12 @@ pub(super) const CURSOR_WIDTH: Pixels = px(1.5);
|
|||||||
/// Every loop will notify the view to update the `visible`, and Input will observe this update to touch repaint.
|
/// Every loop will notify the view to update the `visible`, and Input will observe this update to touch repaint.
|
||||||
///
|
///
|
||||||
/// The input painter will check if this in visible state, then it will draw the cursor.
|
/// The input painter will check if this in visible state, then it will draw the cursor.
|
||||||
pub struct BlinkCursor {
|
pub(crate) struct BlinkCursor {
|
||||||
visible: bool,
|
visible: bool,
|
||||||
paused: bool,
|
paused: bool,
|
||||||
epoch: usize,
|
epoch: usize,
|
||||||
|
|
||||||
|
_task: Task<()>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BlinkCursor {
|
impl BlinkCursor {
|
||||||
@@ -24,6 +31,7 @@ impl BlinkCursor {
|
|||||||
visible: false,
|
visible: false,
|
||||||
paused: false,
|
paused: false,
|
||||||
epoch: 0,
|
epoch: 0,
|
||||||
|
_task: Task::ready(()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,14 +61,12 @@ impl BlinkCursor {
|
|||||||
|
|
||||||
// Schedule the next blink
|
// Schedule the next blink
|
||||||
let epoch = self.next_epoch();
|
let epoch = self.next_epoch();
|
||||||
cx.spawn(async move |this, cx| {
|
self._task = cx.spawn(async move |this, cx| {
|
||||||
cx.background_executor().timer(INTERVAL).await;
|
cx.background_executor().timer(INTERVAL).await;
|
||||||
|
|
||||||
if let Some(this) = this.upgrade() {
|
if let Some(this) = this.upgrade() {
|
||||||
this.update(cx, |this, cx| this.blink(epoch, cx));
|
this.update(cx, |this, cx| this.blink(epoch, cx));
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
.detach();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn visible(&self) -> bool {
|
pub fn visible(&self) -> bool {
|
||||||
@@ -76,7 +82,7 @@ impl BlinkCursor {
|
|||||||
|
|
||||||
// delay 500ms to start the blinking
|
// delay 500ms to start the blinking
|
||||||
let epoch = self.next_epoch();
|
let epoch = self.next_epoch();
|
||||||
cx.spawn(async move |this, cx| {
|
self._task = cx.spawn(async move |this, cx| {
|
||||||
cx.background_executor().timer(PAUSE_DELAY).await;
|
cx.background_executor().timer(PAUSE_DELAY).await;
|
||||||
|
|
||||||
if let Some(this) = this.upgrade() {
|
if let Some(this) = this.upgrade() {
|
||||||
@@ -85,13 +91,6 @@ impl BlinkCursor {
|
|||||||
this.blink(epoch, cx);
|
this.blink(epoch, cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
.detach();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for BlinkCursor {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
|
|
||||||
use crate::history::HistoryItem;
|
use crate::{history::HistoryItem, input::Selection};
|
||||||
use crate::input::cursor::Selection;
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Clone)]
|
#[derive(Debug, PartialEq, Clone)]
|
||||||
pub struct Change {
|
pub struct Change {
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
use gpui::{App, Styled};
|
use gpui::{App, Styled};
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
|
|
||||||
use crate::button::{Button, ButtonVariants};
|
use crate::button::{Button, ButtonVariants as _};
|
||||||
use crate::{Icon, IconName, Sizable};
|
use crate::{Icon, IconName, Sizable as _};
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn clear_button(cx: &App) -> Button {
|
pub(crate) fn clear_button(cx: &App) -> Button {
|
||||||
Button::new("clean")
|
Button::new("clean")
|
||||||
.icon(Icon::new(IconName::CloseCircle))
|
.icon(Icon::new(IconName::CloseCircle))
|
||||||
.tooltip("Clear")
|
.ghost()
|
||||||
.small()
|
.xsmall()
|
||||||
.transparent()
|
.tab_stop(false)
|
||||||
.text_color(cx.theme().text_muted)
|
.text_color(cx.theme().icon_muted)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use std::ops::Range;
|
use std::ops::{Range, RangeBounds};
|
||||||
|
|
||||||
/// A selection in the text, represented by start and end byte indices.
|
/// A selection in the text, represented by start and end byte indices.
|
||||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
|
||||||
@@ -42,5 +42,12 @@ impl From<Selection> for Range<usize> {
|
|||||||
value.start..value.end
|
value.start..value.end
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
impl RangeBounds<usize> for Selection {
|
||||||
|
fn start_bound(&self) -> std::ops::Bound<&usize> {
|
||||||
|
std::ops::Bound::Included(&self.start)
|
||||||
|
}
|
||||||
|
|
||||||
pub type Position = lsp_types::Position;
|
fn end_bound(&self) -> std::ops::Bound<&usize> {
|
||||||
|
std::ops::Bound::Excluded(&self.end)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
336
crates/ui/src/input/display_map/display_map.rs
Normal file
336
crates/ui/src/input/display_map/display_map.rs
Normal file
@@ -0,0 +1,336 @@
|
|||||||
|
/// DisplayMap: Public facade for Editor/Input display mapping.
|
||||||
|
///
|
||||||
|
/// This combines WrapMap and FoldMap to provide a unified API:
|
||||||
|
/// - BufferPoint ↔ DisplayPoint conversion
|
||||||
|
/// - Fold management (candidates, toggle, query)
|
||||||
|
/// - Automatic projection updates on text/layout changes
|
||||||
|
use std::ops::Range;
|
||||||
|
|
||||||
|
use gpui::{App, Font, Pixels};
|
||||||
|
use ropey::Rope;
|
||||||
|
|
||||||
|
use super::fold_map::FoldMap;
|
||||||
|
use super::folding::FoldRange;
|
||||||
|
use super::text_wrapper::{LineItem, WrapDisplayPoint};
|
||||||
|
use super::wrap_map::WrapMap;
|
||||||
|
use super::{BufferPoint, DisplayPoint};
|
||||||
|
use crate::input::display_map::WrapPoint;
|
||||||
|
use crate::input::rope_ext::RopeExt as _;
|
||||||
|
use crate::input::Point as TreeSitterPoint;
|
||||||
|
|
||||||
|
/// DisplayMap is the main interface for Editor/Input coordinate mapping.
|
||||||
|
///
|
||||||
|
/// It manages the two-layer projection:
|
||||||
|
/// 1. Buffer → Wrap (soft-wrapping)
|
||||||
|
/// 2. Wrap → Display (folding)
|
||||||
|
///
|
||||||
|
/// Editor/Input only needs to work with BufferPoint and DisplayPoint.
|
||||||
|
pub struct DisplayMap {
|
||||||
|
wrap_map: WrapMap,
|
||||||
|
fold_map: FoldMap,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DisplayMap {
|
||||||
|
pub fn new(font: Font, font_size: Pixels, wrap_width: Option<Pixels>) -> Self {
|
||||||
|
Self {
|
||||||
|
wrap_map: WrapMap::new(font, font_size, wrap_width),
|
||||||
|
fold_map: FoldMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Core Coordinate Mapping ====================
|
||||||
|
|
||||||
|
/// Convert buffer position to display position
|
||||||
|
pub fn buffer_pos_to_display_pos(&self, pos: BufferPoint) -> DisplayPoint {
|
||||||
|
// Buffer → Wrap
|
||||||
|
let wrap_pos = self.wrap_map.buffer_pos_to_wrap_pos(pos);
|
||||||
|
|
||||||
|
// Wrap → Display
|
||||||
|
if let Some(display_row) = self.fold_map.wrap_row_to_display_row(wrap_pos.row) {
|
||||||
|
DisplayPoint::new(display_row, wrap_pos.col)
|
||||||
|
} else {
|
||||||
|
// Cursor is in a folded region, find nearest visible row
|
||||||
|
let display_row = self.fold_map.nearest_visible_display_row(wrap_pos.row);
|
||||||
|
DisplayPoint::new(display_row, 0) // Column 0 at fold boundary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert display position to buffer position
|
||||||
|
pub fn display_pos_to_buffer_pos(&self, pos: DisplayPoint) -> BufferPoint {
|
||||||
|
// Display → Wrap
|
||||||
|
let wrap_row = self.fold_map.display_row_to_wrap_row(pos.row).unwrap_or(0);
|
||||||
|
|
||||||
|
// Wrap → Buffer
|
||||||
|
let wrap_pos = WrapPoint::new(wrap_row, pos.col);
|
||||||
|
self.wrap_map.wrap_pos_to_buffer_pos(wrap_pos)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get total number of visible display rows
|
||||||
|
#[inline]
|
||||||
|
pub fn display_row_count(&self) -> usize {
|
||||||
|
self.fold_map.display_row_count()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the buffer line for a given display row
|
||||||
|
pub fn display_row_to_buffer_line(&self, display_row: usize) -> usize {
|
||||||
|
// Display → Wrap
|
||||||
|
let wrap_row = self
|
||||||
|
.fold_map
|
||||||
|
.display_row_to_wrap_row(display_row)
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
// Wrap → Buffer line
|
||||||
|
self.wrap_map.wrap_row_to_buffer_line(wrap_row)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the display row range for a buffer line: [start, end)
|
||||||
|
/// Returns None if the buffer line is completely hidden
|
||||||
|
pub fn buffer_line_to_display_row_range(&self, line: usize) -> Option<Range<usize>> {
|
||||||
|
// Buffer line → Wrap row range
|
||||||
|
let wrap_row_range = self.wrap_map.buffer_line_to_wrap_row_range(line);
|
||||||
|
|
||||||
|
// Find first and last visible display rows in this range
|
||||||
|
let mut first_display_row = None;
|
||||||
|
let mut last_display_row = None;
|
||||||
|
|
||||||
|
for wrap_row in wrap_row_range {
|
||||||
|
if let Some(display_row) = self.fold_map.wrap_row_to_display_row(wrap_row) {
|
||||||
|
if first_display_row.is_none() {
|
||||||
|
first_display_row = Some(display_row);
|
||||||
|
}
|
||||||
|
last_display_row = Some(display_row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let (Some(start), Some(end)) = (first_display_row, last_display_row) {
|
||||||
|
Some(start..end + 1)
|
||||||
|
} else {
|
||||||
|
None // Completely folded
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a buffer line is completely hidden
|
||||||
|
#[inline]
|
||||||
|
pub fn is_buffer_line_hidden(&self, line: usize) -> bool {
|
||||||
|
self.buffer_line_to_display_row_range(line).is_none()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set fold candidates (from tree-sitter/LSP)
|
||||||
|
pub fn set_fold_candidates(&mut self, candidates: Vec<FoldRange>) {
|
||||||
|
self.fold_map.set_candidates(candidates);
|
||||||
|
self.rebuild_fold_projection();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set a fold at the given start_line (must be in candidates)
|
||||||
|
pub fn set_folded(&mut self, start_line: usize, folded: bool) {
|
||||||
|
self.fold_map.set_folded(start_line, folded);
|
||||||
|
self.rebuild_fold_projection();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Toggle fold at the given start_line
|
||||||
|
pub fn toggle_fold(&mut self, start_line: usize) {
|
||||||
|
self.fold_map.toggle_fold(start_line);
|
||||||
|
self.rebuild_fold_projection();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a line is currently folded
|
||||||
|
#[inline]
|
||||||
|
pub fn is_folded_at(&self, start_line: usize) -> bool {
|
||||||
|
self.fold_map.is_folded_at(start_line)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a line is a fold candidate
|
||||||
|
#[inline]
|
||||||
|
pub fn is_fold_candidate(&self, start_line: usize) -> bool {
|
||||||
|
self.fold_map.is_fold_candidate(start_line)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all currently folded ranges
|
||||||
|
#[inline]
|
||||||
|
pub fn folded_ranges(&self) -> &[FoldRange] {
|
||||||
|
self.fold_map.folded_ranges()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear all folds
|
||||||
|
pub fn clear_folds(&mut self) {
|
||||||
|
self.fold_map.clear_folds();
|
||||||
|
self.rebuild_fold_projection();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Text and Layout Updates ====================
|
||||||
|
|
||||||
|
/// Adjust folds and candidates for a text edit before updating the wrap map.
|
||||||
|
///
|
||||||
|
/// Must be called with the OLD text (before replacement) and the edit range/new_text
|
||||||
|
/// so we can compute which old lines were affected.
|
||||||
|
pub fn adjust_folds_for_edit(&mut self, old_text: &Rope, range: &Range<usize>, new_text: &str) {
|
||||||
|
if self.fold_map.folded_ranges().is_empty() && self.fold_map.fold_candidates().is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let edit_start_line = old_text.offset_to_point(range.start).row;
|
||||||
|
let edit_end_line = old_text.offset_to_point(range.end.min(old_text.len())).row;
|
||||||
|
|
||||||
|
let old_lines_in_range = edit_end_line.saturating_sub(edit_start_line);
|
||||||
|
let new_lines_in_range = new_text.chars().filter(|c| *c == '\n').count();
|
||||||
|
let line_delta = new_lines_in_range as isize - old_lines_in_range as isize;
|
||||||
|
|
||||||
|
self.fold_map
|
||||||
|
.adjust_folds_for_edit(edit_start_line, edit_end_line, line_delta);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Incrementally update fold candidates after a text edit.
|
||||||
|
///
|
||||||
|
/// Extracts new fold candidates only within the edited byte range
|
||||||
|
/// and merges them with existing (already adjusted) candidates.
|
||||||
|
pub fn update_fold_candidates_for_edit(
|
||||||
|
&mut self,
|
||||||
|
tree: &super::folding::Tree,
|
||||||
|
edit_byte_range: Range<usize>,
|
||||||
|
new_text: &Rope,
|
||||||
|
) {
|
||||||
|
let new_start_line = new_text.offset_to_point(edit_byte_range.start).row;
|
||||||
|
let new_end_line = new_text
|
||||||
|
.offset_to_point(edit_byte_range.end.min(new_text.len()))
|
||||||
|
.row;
|
||||||
|
|
||||||
|
let new_candidates = super::folding::extract_fold_ranges_in_range(tree, edit_byte_range);
|
||||||
|
self.fold_map
|
||||||
|
.merge_candidates_for_edit(new_start_line, new_end_line, new_candidates);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update text (incremental or full)
|
||||||
|
pub fn on_text_changed(
|
||||||
|
&mut self,
|
||||||
|
changed_text: &Rope,
|
||||||
|
range: &Range<usize>,
|
||||||
|
new_text: &Rope,
|
||||||
|
cx: &mut App,
|
||||||
|
) {
|
||||||
|
self.wrap_map
|
||||||
|
.on_text_changed(changed_text, range, new_text, cx);
|
||||||
|
self.rebuild_fold_projection();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update layout parameters (wrap width or font)
|
||||||
|
pub fn on_layout_changed(&mut self, wrap_width: Option<Pixels>, cx: &mut App) {
|
||||||
|
self.wrap_map.on_layout_changed(wrap_width, cx);
|
||||||
|
self.rebuild_fold_projection();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set font parameters
|
||||||
|
pub fn set_font(&mut self, font: Font, font_size: Pixels, cx: &mut App) {
|
||||||
|
self.wrap_map.set_font(font, font_size, cx);
|
||||||
|
self.rebuild_fold_projection();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ensure text is prepared (initializes wrapper if needed)
|
||||||
|
pub fn ensure_text_prepared(&mut self, text: &Rope, cx: &mut App) {
|
||||||
|
let did_initialize = self.wrap_map.ensure_text_prepared(text, cx);
|
||||||
|
if did_initialize {
|
||||||
|
self.rebuild_fold_projection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initialize with text
|
||||||
|
pub fn set_text(&mut self, text: &Rope, cx: &mut App) {
|
||||||
|
self.wrap_map.set_text(text, cx);
|
||||||
|
self.rebuild_fold_projection();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Internal Helpers ====================
|
||||||
|
|
||||||
|
/// Rebuild fold projection after wrap_map or fold state changes
|
||||||
|
/// Only rebuilds if there are actually folded ranges
|
||||||
|
fn rebuild_fold_projection(&mut self) {
|
||||||
|
if !self.fold_map.folded_ranges().is_empty() {
|
||||||
|
self.fold_map.rebuild(&self.wrap_map);
|
||||||
|
} else {
|
||||||
|
// No active folds: identity mapping (wrap_row == display_row).
|
||||||
|
// Just update cached count so query methods work without Vec allocation.
|
||||||
|
self.fold_map
|
||||||
|
.mark_dirty_with_wrap_count(self.wrap_map.wrap_row_count());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Wrap Display Point Operations ====================
|
||||||
|
|
||||||
|
/// Convert byte offset to wrap display point (with soft wrap info).
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn offset_to_wrap_display_point(&self, offset: usize) -> WrapDisplayPoint {
|
||||||
|
self.wrap_map.wrapper().offset_to_display_point(offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert wrap display point to byte offset.
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn wrap_display_point_to_offset(&self, point: WrapDisplayPoint) -> usize {
|
||||||
|
self.wrap_map.wrapper().display_point_to_offset(point)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert wrap display point to TreeSitterPoint (buffer line/col).
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn wrap_display_point_to_point(
|
||||||
|
&self,
|
||||||
|
point: WrapDisplayPoint,
|
||||||
|
) -> TreeSitterPoint {
|
||||||
|
self.wrap_map.wrapper().display_point_to_point(point)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert a wrap row to a display row (skipping folded rows).
|
||||||
|
/// Returns None if the wrap row is folded.
|
||||||
|
#[inline]
|
||||||
|
pub fn wrap_row_to_display_row(&self, wrap_row: usize) -> Option<usize> {
|
||||||
|
self.fold_map.wrap_row_to_display_row(wrap_row)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find the nearest visible display row for a given wrap row.
|
||||||
|
#[inline]
|
||||||
|
pub fn nearest_visible_display_row(&self, wrap_row: usize) -> usize {
|
||||||
|
self.fold_map.nearest_visible_display_row(wrap_row)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert a display row to a wrap row.
|
||||||
|
#[inline]
|
||||||
|
pub fn display_row_to_wrap_row(&self, display_row: usize) -> Option<usize> {
|
||||||
|
self.fold_map.display_row_to_wrap_row(display_row)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the longest row index (by byte length).
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn longest_row(&self) -> usize {
|
||||||
|
self.wrap_map.wrapper().longest_row.row
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Access Methods ====================
|
||||||
|
|
||||||
|
/// Get access to line items (for rendering)
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn lines(&self) -> &[LineItem] {
|
||||||
|
self.wrap_map.lines()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the rope text
|
||||||
|
#[inline]
|
||||||
|
pub fn text(&self) -> &Rope {
|
||||||
|
self.wrap_map.text()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculate how many wrap rows of a buffer line are visible (not folded)
|
||||||
|
#[inline]
|
||||||
|
pub fn visible_wrap_row_count_for_buffer_line(&self, line: usize) -> usize {
|
||||||
|
self.wrap_map
|
||||||
|
.visible_wrap_row_count_for_line(line, &self.fold_map)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the wrap row count (before folding)
|
||||||
|
#[inline]
|
||||||
|
pub fn wrap_row_count(&self) -> usize {
|
||||||
|
self.wrap_map.wrap_row_count()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the buffer line count (logical lines)
|
||||||
|
#[inline]
|
||||||
|
pub fn buffer_line_count(&self) -> usize {
|
||||||
|
self.wrap_map.buffer_line_count()
|
||||||
|
}
|
||||||
|
}
|
||||||
343
crates/ui/src/input/display_map/fold_map.rs
Normal file
343
crates/ui/src/input/display_map/fold_map.rs
Normal file
@@ -0,0 +1,343 @@
|
|||||||
|
/// FoldMap: Folding projection layer (Wrap rows → Display rows).
|
||||||
|
///
|
||||||
|
/// This module manages code folding by:
|
||||||
|
/// - Filtering out wrap rows that belong to folded regions
|
||||||
|
/// - Maintaining bidirectional mapping: wrap_row ↔ display_row
|
||||||
|
/// - Handling fold state changes and rebuilding the projection
|
||||||
|
use super::folding::FoldRange;
|
||||||
|
use super::wrap_map::WrapMap;
|
||||||
|
|
||||||
|
/// FoldMap projects wrap rows to display rows by hiding folded regions.
|
||||||
|
pub struct FoldMap {
|
||||||
|
/// Mapping: display_row → wrap_row
|
||||||
|
/// index = display_row, value = actual wrap_row
|
||||||
|
visible_wrap_rows: Vec<usize>,
|
||||||
|
|
||||||
|
/// Reverse mapping: wrap_row → display_row
|
||||||
|
/// index = wrap_row, value = Some(display_row) if visible, None if folded
|
||||||
|
wrap_row_to_display_row: Vec<Option<usize>>,
|
||||||
|
|
||||||
|
/// Candidate fold ranges (from tree-sitter/LSP)
|
||||||
|
/// Sorted by start_line, unique start_line
|
||||||
|
candidates: Vec<FoldRange>,
|
||||||
|
|
||||||
|
/// Currently folded ranges
|
||||||
|
/// Subset of candidates, sorted by start_line
|
||||||
|
folded: Vec<FoldRange>,
|
||||||
|
|
||||||
|
/// Flag indicating if the fold projection needs rebuilding
|
||||||
|
/// Used for lazy evaluation to avoid expensive rebuilds on every text change
|
||||||
|
needs_rebuild: bool,
|
||||||
|
|
||||||
|
/// Cached wrap_row_count from last rebuild
|
||||||
|
/// Used to detect if WrapMap changed and rebuild is needed
|
||||||
|
cached_wrap_row_count: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FoldMap {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
visible_wrap_rows: Vec::new(),
|
||||||
|
wrap_row_to_display_row: Vec::new(),
|
||||||
|
candidates: Vec::new(),
|
||||||
|
folded: Vec::new(),
|
||||||
|
needs_rebuild: true,
|
||||||
|
cached_wrap_row_count: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update cached wrap_row_count without full rebuild.
|
||||||
|
/// Used when no folds are active (identity mapping assumed).
|
||||||
|
pub(super) fn mark_dirty_with_wrap_count(&mut self, wrap_row_count: usize) {
|
||||||
|
self.needs_rebuild = true;
|
||||||
|
self.cached_wrap_row_count = wrap_row_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get total number of visible display rows
|
||||||
|
pub fn display_row_count(&self) -> usize {
|
||||||
|
if self.folded.is_empty() {
|
||||||
|
return self.cached_wrap_row_count;
|
||||||
|
}
|
||||||
|
self.visible_wrap_rows.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert wrap_row to display_row
|
||||||
|
/// Returns None if the wrap_row is hidden by folding
|
||||||
|
pub fn wrap_row_to_display_row(&self, wrap_row: usize) -> Option<usize> {
|
||||||
|
if self.folded.is_empty() {
|
||||||
|
return if wrap_row < self.cached_wrap_row_count {
|
||||||
|
Some(wrap_row)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
}
|
||||||
|
self.wrap_row_to_display_row
|
||||||
|
.get(wrap_row)
|
||||||
|
.copied()
|
||||||
|
.flatten()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert display_row to wrap_row
|
||||||
|
pub fn display_row_to_wrap_row(&self, display_row: usize) -> Option<usize> {
|
||||||
|
if self.folded.is_empty() {
|
||||||
|
return if display_row < self.cached_wrap_row_count {
|
||||||
|
Some(display_row)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
}
|
||||||
|
self.visible_wrap_rows.get(display_row).copied()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find the nearest visible display_row for a given wrap_row
|
||||||
|
pub fn nearest_visible_display_row(&self, wrap_row: usize) -> usize {
|
||||||
|
if self.folded.is_empty() {
|
||||||
|
return wrap_row.min(self.cached_wrap_row_count.saturating_sub(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(dr) = self.wrap_row_to_display_row(wrap_row) {
|
||||||
|
return dr;
|
||||||
|
}
|
||||||
|
|
||||||
|
match self.visible_wrap_rows.binary_search(&wrap_row) {
|
||||||
|
Ok(idx) => idx,
|
||||||
|
Err(insert_pos) => insert_pos.saturating_sub(1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set fold candidates (from tree-sitter/LSP), full replacement.
|
||||||
|
pub fn set_candidates(&mut self, mut candidates: Vec<FoldRange>) {
|
||||||
|
// Sort and deduplicate by start_line
|
||||||
|
candidates.sort_by_key(|r| r.start_line);
|
||||||
|
candidates.dedup_by_key(|r| r.start_line);
|
||||||
|
self.candidates = candidates;
|
||||||
|
|
||||||
|
// Remove any folded ranges that are no longer in candidates
|
||||||
|
self.folded.retain(|fold| {
|
||||||
|
self.candidates
|
||||||
|
.iter()
|
||||||
|
.any(|c| c.start_line == fold.start_line)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Merge new candidates extracted from an edited region into existing candidates.
|
||||||
|
///
|
||||||
|
/// Replaces candidates within [edit_start_line, edit_end_line] with `new_candidates`,
|
||||||
|
/// keeping candidates outside the edit range intact.
|
||||||
|
pub fn merge_candidates_for_edit(
|
||||||
|
&mut self,
|
||||||
|
edit_start_line: usize,
|
||||||
|
edit_end_line: usize,
|
||||||
|
new_candidates: Vec<FoldRange>,
|
||||||
|
) {
|
||||||
|
// Remove old candidates within the edit range (already done by adjust_folds_for_edit)
|
||||||
|
// But do it again in case adjust wasn't called or range differs
|
||||||
|
self.candidates
|
||||||
|
.retain(|c| c.start_line < edit_start_line || c.start_line > edit_end_line);
|
||||||
|
|
||||||
|
// Add new candidates
|
||||||
|
self.candidates.extend(new_candidates);
|
||||||
|
self.candidates.sort_by_key(|r| r.start_line);
|
||||||
|
self.candidates.dedup_by_key(|r| r.start_line);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set a fold at the given start_line (must be in candidates)
|
||||||
|
pub fn set_folded(&mut self, start_line: usize, folded: bool) {
|
||||||
|
if folded {
|
||||||
|
// Find the candidate range for this start_line
|
||||||
|
if let Some(candidate) = self.candidates.iter().find(|c| c.start_line == start_line) {
|
||||||
|
// Add to folded if not already present
|
||||||
|
if !self.folded.iter().any(|f| f.start_line == start_line) {
|
||||||
|
self.folded.push(*candidate);
|
||||||
|
self.folded.sort_by_key(|r| r.start_line);
|
||||||
|
self.needs_rebuild = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Remove from folded
|
||||||
|
self.folded.retain(|f| f.start_line != start_line);
|
||||||
|
self.needs_rebuild = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Toggle fold at the given start_line
|
||||||
|
pub fn toggle_fold(&mut self, start_line: usize) {
|
||||||
|
let is_folded = self.is_folded_at(start_line);
|
||||||
|
self.set_folded(start_line, !is_folded);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a line is currently folded
|
||||||
|
pub fn is_folded_at(&self, start_line: usize) -> bool {
|
||||||
|
self.folded.iter().any(|f| f.start_line == start_line)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a line is a fold candidate
|
||||||
|
pub fn is_fold_candidate(&self, start_line: usize) -> bool {
|
||||||
|
self.candidates.iter().any(|c| c.start_line == start_line)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all fold candidates
|
||||||
|
#[inline]
|
||||||
|
pub fn fold_candidates(&self) -> &[FoldRange] {
|
||||||
|
&self.candidates
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all currently folded ranges
|
||||||
|
#[inline]
|
||||||
|
pub fn folded_ranges(&self) -> &[FoldRange] {
|
||||||
|
&self.folded
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear all folds
|
||||||
|
#[inline]
|
||||||
|
pub fn clear_folds(&mut self) {
|
||||||
|
self.folded.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adjust folds and candidates after a text edit.
|
||||||
|
///
|
||||||
|
/// - Folds/candidates overlapping the edited line range are removed
|
||||||
|
/// - Folds/candidates after the edit are shifted by line_delta
|
||||||
|
///
|
||||||
|
/// This avoids expensive full tree traversal on every keystroke.
|
||||||
|
pub fn adjust_folds_for_edit(
|
||||||
|
&mut self,
|
||||||
|
edit_start_line: usize,
|
||||||
|
edit_end_line: usize,
|
||||||
|
line_delta: isize,
|
||||||
|
) {
|
||||||
|
// Adjust folded ranges
|
||||||
|
if !self.folded.is_empty() {
|
||||||
|
self.folded.retain(|fold| {
|
||||||
|
!(fold.start_line <= edit_end_line && fold.end_line >= edit_start_line)
|
||||||
|
});
|
||||||
|
|
||||||
|
if line_delta != 0 {
|
||||||
|
for fold in &mut self.folded {
|
||||||
|
if fold.start_line > edit_end_line {
|
||||||
|
fold.start_line = (fold.start_line as isize + line_delta).max(0) as usize;
|
||||||
|
fold.end_line = (fold.end_line as isize + line_delta).max(0) as usize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjust candidates the same way
|
||||||
|
if !self.candidates.is_empty() {
|
||||||
|
self.candidates
|
||||||
|
.retain(|c| !(c.start_line <= edit_end_line && c.end_line >= edit_start_line));
|
||||||
|
|
||||||
|
if line_delta != 0 {
|
||||||
|
for c in &mut self.candidates {
|
||||||
|
if c.start_line > edit_end_line {
|
||||||
|
c.start_line = (c.start_line as isize + line_delta).max(0) as usize;
|
||||||
|
c.end_line = (c.end_line as isize + line_delta).max(0) as usize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.needs_rebuild = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rebuild the fold mapping after wrap_map or fold state changes
|
||||||
|
///
|
||||||
|
/// This is the core algorithm that projects wrap rows to display rows.
|
||||||
|
pub fn rebuild(&mut self, wrap_map: &WrapMap) {
|
||||||
|
let wrap_row_count = wrap_map.wrap_row_count();
|
||||||
|
|
||||||
|
// Performance optimization: skip rebuild if nothing changed
|
||||||
|
if !self.needs_rebuild && wrap_row_count == self.cached_wrap_row_count {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.cached_wrap_row_count = wrap_row_count;
|
||||||
|
|
||||||
|
self.visible_wrap_rows.clear();
|
||||||
|
self.wrap_row_to_display_row = vec![None; wrap_row_count];
|
||||||
|
|
||||||
|
if self.folded.is_empty() {
|
||||||
|
// Fast path: no folds, all wrap rows are visible
|
||||||
|
self.visible_wrap_rows = (0..wrap_row_count).collect();
|
||||||
|
for (display_row, &wrap_row) in self.visible_wrap_rows.iter().enumerate() {
|
||||||
|
self.wrap_row_to_display_row[wrap_row] = Some(display_row);
|
||||||
|
}
|
||||||
|
self.needs_rebuild = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build set of hidden wrap_row ranges from folded buffer lines
|
||||||
|
let mut hidden_ranges = Vec::new();
|
||||||
|
for fold in &self.folded {
|
||||||
|
// Hide wrap rows from (start_line + 1) to (end_line - 1) (inclusive)
|
||||||
|
// Both the first line and last line of the fold remain visible
|
||||||
|
let hide_start_line = fold.start_line + 1;
|
||||||
|
let hide_end_line = fold.end_line.saturating_sub(1);
|
||||||
|
|
||||||
|
if hide_start_line > hide_end_line {
|
||||||
|
continue; // No middle lines to hide (0 or 1 lines between start and end)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get wrap_row ranges for the hidden buffer lines
|
||||||
|
let start_wrap_row = wrap_map.buffer_line_to_first_wrap_row(hide_start_line);
|
||||||
|
let end_wrap_row = if hide_end_line + 1 < wrap_map.buffer_line_count() {
|
||||||
|
wrap_map.buffer_line_to_first_wrap_row(hide_end_line + 1)
|
||||||
|
} else {
|
||||||
|
wrap_row_count
|
||||||
|
};
|
||||||
|
|
||||||
|
if start_wrap_row < end_wrap_row {
|
||||||
|
hidden_ranges.push(start_wrap_row..end_wrap_row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge overlapping hidden ranges
|
||||||
|
hidden_ranges.sort_by_key(|r| r.start);
|
||||||
|
let mut merged_hidden = Vec::new();
|
||||||
|
for range in hidden_ranges {
|
||||||
|
if let Some(last) = merged_hidden.last_mut() {
|
||||||
|
if range.start <= *last {
|
||||||
|
// Overlapping or adjacent, merge
|
||||||
|
*last = (*last).max(range.end);
|
||||||
|
} else {
|
||||||
|
merged_hidden.push(range.start);
|
||||||
|
merged_hidden.push(range.end);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
merged_hidden.push(range.start);
|
||||||
|
merged_hidden.push(range.end);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan all wrap rows and filter out hidden ones
|
||||||
|
let mut display_row = 0;
|
||||||
|
let mut hidden_iter = merged_hidden.chunks_exact(2);
|
||||||
|
let mut current_hidden = hidden_iter.next();
|
||||||
|
|
||||||
|
for wrap_row in 0..wrap_row_count {
|
||||||
|
// Check if wrap_row is in current hidden range
|
||||||
|
let is_hidden = if let Some(&[start, end]) = current_hidden {
|
||||||
|
if wrap_row >= end {
|
||||||
|
current_hidden = hidden_iter.next();
|
||||||
|
if let Some(&[new_start, new_end]) = current_hidden {
|
||||||
|
wrap_row >= new_start && wrap_row < new_end
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
wrap_row >= start && wrap_row < end
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
|
if !is_hidden {
|
||||||
|
self.visible_wrap_rows.push(wrap_row);
|
||||||
|
self.wrap_row_to_display_row[wrap_row] = Some(display_row);
|
||||||
|
display_row += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.needs_rebuild = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
96
crates/ui/src/input/display_map/folding.rs
Normal file
96
crates/ui/src/input/display_map/folding.rs
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
use std::ops::Range;
|
||||||
|
|
||||||
|
#[cfg(not(target_family = "wasm"))]
|
||||||
|
use tree_sitter::Node;
|
||||||
|
#[cfg(not(target_family = "wasm"))]
|
||||||
|
pub use tree_sitter::Tree;
|
||||||
|
|
||||||
|
#[cfg(target_family = "wasm")]
|
||||||
|
/// Stub type for tree-sitter Tree on WASM (tree-sitter not available).
|
||||||
|
pub struct Tree;
|
||||||
|
|
||||||
|
#[cfg(not(target_family = "wasm"))]
|
||||||
|
/// Minimum line span for a node to be considered foldable.
|
||||||
|
const MIN_FOLD_LINES: usize = 2;
|
||||||
|
|
||||||
|
/// A fold range representing a foldable code region.
|
||||||
|
///
|
||||||
|
/// The fold range spans from start_line to end_line (inclusive).
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub struct FoldRange {
|
||||||
|
/// Start line (inclusive)
|
||||||
|
pub start_line: usize,
|
||||||
|
/// End line (inclusive)
|
||||||
|
pub end_line: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FoldRange {
|
||||||
|
pub fn new(start_line: usize, end_line: usize) -> Self {
|
||||||
|
assert!(
|
||||||
|
start_line <= end_line,
|
||||||
|
"fold start_line must be <= end_line"
|
||||||
|
);
|
||||||
|
Self {
|
||||||
|
start_line,
|
||||||
|
end_line,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_family = "wasm"))]
|
||||||
|
/// Check if a named node qualifies as a fold candidate.
|
||||||
|
///
|
||||||
|
/// Uses a structural heuristic: any **named** node spanning ≥ MIN_FOLD_LINES
|
||||||
|
/// is foldable. tree-sitter already parses code into semantic units (functions,
|
||||||
|
/// classes, blocks, etc.), so named nodes naturally correspond to meaningful
|
||||||
|
/// foldable regions across all languages without a per-language node-type list.
|
||||||
|
fn is_foldable_node(node: &Node) -> bool {
|
||||||
|
let start = node.start_position().row;
|
||||||
|
let end = node.end_position().row;
|
||||||
|
end.saturating_sub(start) >= MIN_FOLD_LINES
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_family = "wasm"))]
|
||||||
|
/// Extract fold ranges only within a byte range (for incremental updates after edits).
|
||||||
|
///
|
||||||
|
/// Skips subtrees entirely outside the range, making it O(nodes in range)
|
||||||
|
/// instead of O(all nodes in tree).
|
||||||
|
pub fn extract_fold_ranges_in_range(tree: &Tree, byte_range: Range<usize>) -> Vec<FoldRange> {
|
||||||
|
let mut ranges = Vec::new();
|
||||||
|
let root = tree.root_node();
|
||||||
|
let mut cursor = root.walk();
|
||||||
|
// Skip the root, it's not foldable. Use named_children to skip literal tokens.
|
||||||
|
for child in root.named_children(&mut cursor) {
|
||||||
|
collect_foldable_nodes_in_range(child, &byte_range, &mut ranges);
|
||||||
|
}
|
||||||
|
|
||||||
|
ranges.sort_by_key(|r| r.start_line);
|
||||||
|
ranges.dedup_by_key(|r| r.start_line);
|
||||||
|
ranges
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_family = "wasm"))]
|
||||||
|
/// Recursively collect foldable nodes, skipping subtrees outside byte_range.
|
||||||
|
fn collect_foldable_nodes_in_range(
|
||||||
|
node: Node,
|
||||||
|
byte_range: &Range<usize>,
|
||||||
|
ranges: &mut Vec<FoldRange>,
|
||||||
|
) {
|
||||||
|
if node.end_byte() <= byte_range.start || node.start_byte() >= byte_range.end {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !is_foldable_node(&node) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ranges.push(FoldRange {
|
||||||
|
start_line: node.start_position().row,
|
||||||
|
end_line: node.end_position().row,
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut cursor = node.walk();
|
||||||
|
for child in node.named_children(&mut cursor) {
|
||||||
|
collect_foldable_nodes_in_range(child, byte_range, ranges);
|
||||||
|
}
|
||||||
|
}
|
||||||
69
crates/ui/src/input/display_map/mod.rs
Normal file
69
crates/ui/src/input/display_map/mod.rs
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
/// Display mapping system for Editor/Input.
|
||||||
|
///
|
||||||
|
/// This module implements a layered display mapping architecture:
|
||||||
|
/// - **WrapMap**: Handles soft-wrapping (buffer → wrap rows)
|
||||||
|
/// - **FoldMap**: Handles folding (wrap rows → display rows)
|
||||||
|
/// - **DisplayMap**: Public facade for Editor/Input
|
||||||
|
///
|
||||||
|
/// The goal is to provide a clean, unified API where Editor only needs to know
|
||||||
|
/// about `BufferPoint ↔ DisplayPoint` mapping, without worrying about internal wrap/fold complexity.
|
||||||
|
mod display_map;
|
||||||
|
mod fold_map;
|
||||||
|
#[cfg(not(target_family = "wasm"))]
|
||||||
|
mod folding;
|
||||||
|
#[cfg(target_family = "wasm")]
|
||||||
|
pub mod folding;
|
||||||
|
mod text_wrapper;
|
||||||
|
mod wrap_map;
|
||||||
|
|
||||||
|
// Re-export public API
|
||||||
|
// Re-export FoldRange and extract_fold_ranges
|
||||||
|
pub use folding::FoldRange;
|
||||||
|
|
||||||
|
pub use self::display_map::DisplayMap;
|
||||||
|
pub(crate) use self::text_wrapper::LineLayout;
|
||||||
|
|
||||||
|
/// Position in the buffer (logical text).
|
||||||
|
///
|
||||||
|
/// - `line`: 0-based logical line number (split by `\n`)
|
||||||
|
/// - `col`: 0-based column offset (byte offset)
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub struct BufferPoint {
|
||||||
|
pub line: usize,
|
||||||
|
pub col: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BufferPoint {
|
||||||
|
pub fn new(line: usize, col: usize) -> Self {
|
||||||
|
Self { line, col }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Position after soft-wrapping but before folding (internal).
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub(super) struct WrapPoint {
|
||||||
|
pub row: usize,
|
||||||
|
pub col: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WrapPoint {
|
||||||
|
pub fn new(row: usize, col: usize) -> Self {
|
||||||
|
Self { row, col }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Final display position (after soft-wrapping and folding).
|
||||||
|
///
|
||||||
|
/// - `row`: 0-based display row (final visible row)
|
||||||
|
/// - `col`: 0-based display column
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub struct DisplayPoint {
|
||||||
|
pub row: usize,
|
||||||
|
pub col: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DisplayPoint {
|
||||||
|
pub fn new(row: usize, col: usize) -> Self {
|
||||||
|
Self { row, col }
|
||||||
|
}
|
||||||
|
}
|
||||||
930
crates/ui/src/input/display_map/text_wrapper.rs
Normal file
930
crates/ui/src/input/display_map/text_wrapper.rs
Normal file
@@ -0,0 +1,930 @@
|
|||||||
|
use std::ops::Range;
|
||||||
|
use gpui::Half;
|
||||||
|
|
||||||
|
use gpui::{
|
||||||
|
App, Font, LineFragment, Pixels, Point, ShapedLine, Size, TextAlign, Window, point, px,
|
||||||
|
size,
|
||||||
|
};
|
||||||
|
use ropey::Rope;
|
||||||
|
use smallvec::SmallVec;
|
||||||
|
|
||||||
|
use crate::input::{LastLayout, Point as TreeSitterPoint, RopeExt, WhitespaceIndicators};
|
||||||
|
|
||||||
|
/// A line with soft wrapped lines info.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub(crate) struct LineItem {
|
||||||
|
/// The original line text, without end `\n`.
|
||||||
|
line: Rope,
|
||||||
|
/// The soft wrapped lines relative byte range (0..line.len) of this line (Include first line).
|
||||||
|
///
|
||||||
|
/// Not contains the line end `\n`.
|
||||||
|
pub(crate) wrapped_lines: Vec<Range<usize>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LineItem {
|
||||||
|
/// Get the bytes length of this line.
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn len(&self) -> usize {
|
||||||
|
self.line.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get number of soft wrapped lines of this line (include the first line).
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn lines_len(&self) -> usize {
|
||||||
|
self.wrapped_lines.len()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub(crate) struct LongestRow {
|
||||||
|
/// The 0-based row index.
|
||||||
|
pub row: usize,
|
||||||
|
/// The bytes length of the longest line.
|
||||||
|
pub len: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Used to prepare the text with soft wrap to be get lines to displayed in the Editor.
|
||||||
|
///
|
||||||
|
/// After use lines to calculate the scroll size of the Editor.
|
||||||
|
pub(crate) struct TextWrapper {
|
||||||
|
text: Rope,
|
||||||
|
/// Total wrapped lines (Inlucde the first line), value is start and end index of the line.
|
||||||
|
soft_lines: usize,
|
||||||
|
font: Font,
|
||||||
|
font_size: Pixels,
|
||||||
|
/// If is none, it means the text is not wrapped
|
||||||
|
wrap_width: Option<Pixels>,
|
||||||
|
/// The longest (row, bytes len) in characters, used to calculate the horizontal scroll width.
|
||||||
|
pub(crate) longest_row: LongestRow,
|
||||||
|
/// The lines by split \n
|
||||||
|
pub(crate) lines: Vec<LineItem>,
|
||||||
|
|
||||||
|
_initialized: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
impl TextWrapper {
|
||||||
|
pub(crate) fn new(font: Font, font_size: Pixels, wrap_width: Option<Pixels>) -> Self {
|
||||||
|
Self {
|
||||||
|
text: Rope::new(),
|
||||||
|
font,
|
||||||
|
font_size,
|
||||||
|
wrap_width,
|
||||||
|
soft_lines: 0,
|
||||||
|
longest_row: LongestRow::default(),
|
||||||
|
lines: Vec::new(),
|
||||||
|
_initialized: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn set_default_text(&mut self, text: &Rope) {
|
||||||
|
self.text = text.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get reference to the rope text.
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn text(&self) -> &Rope {
|
||||||
|
&self.text
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the total number of lines including wrapped lines.
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn len(&self) -> usize {
|
||||||
|
self.soft_lines
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the line item by row index.
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn line(&self, row: usize) -> Option<&LineItem> {
|
||||||
|
self.lines.iter().skip(row).next()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_wrap_width(&mut self, wrap_width: Option<Pixels>, cx: &mut App) {
|
||||||
|
if wrap_width == self.wrap_width {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.wrap_width = wrap_width;
|
||||||
|
self.update_all(&self.text.clone(), cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_font(&mut self, font: Font, font_size: Pixels, cx: &mut App) {
|
||||||
|
if self.font.eq(&font) && self.font_size == font_size {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.font = font;
|
||||||
|
self.font_size = font_size;
|
||||||
|
self.update_all(&self.text.clone(), cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn prepare_if_need(&mut self, text: &Rope, cx: &mut App) -> bool {
|
||||||
|
if self._initialized {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
self._initialized = true;
|
||||||
|
self.update_all(text, cx);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the text wrapper and recalculate the wrapped lines.
|
||||||
|
///
|
||||||
|
/// If the `text` is the same as the current text, do nothing.
|
||||||
|
///
|
||||||
|
/// - `changed_text`: The text [`Rope`] that has changed.
|
||||||
|
/// - `range`: The `selected_range` before change.
|
||||||
|
/// - `new_text`: The inserted text.
|
||||||
|
/// - `force`: Whether to force the update, if false, the update will be skipped if the text is the same.
|
||||||
|
/// - `cx`: The application context.
|
||||||
|
pub(crate) fn update(
|
||||||
|
&mut self,
|
||||||
|
changed_text: &Rope,
|
||||||
|
range: &Range<usize>,
|
||||||
|
new_text: &Rope,
|
||||||
|
cx: &mut App,
|
||||||
|
) {
|
||||||
|
let mut line_wrapper = cx
|
||||||
|
.text_system()
|
||||||
|
.line_wrapper(self.font.clone(), self.font_size);
|
||||||
|
self._update(
|
||||||
|
changed_text,
|
||||||
|
range,
|
||||||
|
new_text,
|
||||||
|
&mut |line_str, wrap_width| {
|
||||||
|
line_wrapper
|
||||||
|
.wrap_line(&[LineFragment::text(line_str)], wrap_width)
|
||||||
|
.collect()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _update<F>(
|
||||||
|
&mut self,
|
||||||
|
changed_text: &Rope,
|
||||||
|
range: &Range<usize>,
|
||||||
|
new_text: &Rope,
|
||||||
|
wrap_line: &mut F,
|
||||||
|
) where
|
||||||
|
F: FnMut(&str, Pixels) -> Vec<gpui::Boundary>,
|
||||||
|
{
|
||||||
|
// Remove the old changed lines.
|
||||||
|
let start_row = self.text.offset_to_point(range.start).row;
|
||||||
|
let start_row = start_row.min(self.lines.len().saturating_sub(1));
|
||||||
|
let end_row = self.text.offset_to_point(range.end).row;
|
||||||
|
let end_row = end_row.min(self.lines.len().saturating_sub(1));
|
||||||
|
let rows_range = start_row..=end_row;
|
||||||
|
|
||||||
|
if rows_range.contains(&self.longest_row.row) {
|
||||||
|
self.longest_row = LongestRow::default();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut longest_row_ix = self.longest_row.row;
|
||||||
|
let mut longest_row_len = self.longest_row.len;
|
||||||
|
|
||||||
|
// To add the new lines.
|
||||||
|
let new_start_row = changed_text.offset_to_point(range.start).row;
|
||||||
|
let new_start_offset = changed_text.line_start_offset(new_start_row);
|
||||||
|
let new_end_row = changed_text
|
||||||
|
.offset_to_point(range.start + new_text.len())
|
||||||
|
.row;
|
||||||
|
let new_end_offset = changed_text.line_end_offset(new_end_row);
|
||||||
|
let new_range = new_start_offset..new_end_offset;
|
||||||
|
|
||||||
|
let mut new_lines = vec![];
|
||||||
|
let wrap_width = self.wrap_width;
|
||||||
|
|
||||||
|
// line not contains `\n`.
|
||||||
|
for (ix, line) in Rope::from(changed_text.slice(new_range))
|
||||||
|
.iter_lines()
|
||||||
|
.enumerate()
|
||||||
|
{
|
||||||
|
let line_str = line.to_string();
|
||||||
|
let mut wrapped_lines = vec![];
|
||||||
|
let mut prev_boundary_ix = 0;
|
||||||
|
|
||||||
|
if line_str.len() > longest_row_len {
|
||||||
|
longest_row_ix = new_start_row + ix;
|
||||||
|
longest_row_len = line_str.len();
|
||||||
|
}
|
||||||
|
|
||||||
|
// If wrap_width is Pixels::MAX, skip wrapping to disable word wrap
|
||||||
|
if let Some(wrap_width) = wrap_width {
|
||||||
|
// Here only have wrapped line, if there is no wrap meet, the `line_wraps` result will empty.
|
||||||
|
for boundary in wrap_line(&line_str, wrap_width) {
|
||||||
|
wrapped_lines.push(prev_boundary_ix..boundary.ix);
|
||||||
|
prev_boundary_ix = boundary.ix;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset of the line
|
||||||
|
if !line_str[prev_boundary_ix..].is_empty() || prev_boundary_ix == 0 {
|
||||||
|
wrapped_lines.push(prev_boundary_ix..line.len());
|
||||||
|
}
|
||||||
|
|
||||||
|
new_lines.push(LineItem {
|
||||||
|
line: Rope::from(line),
|
||||||
|
wrapped_lines,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.lines.len() == 0 {
|
||||||
|
self.lines = new_lines;
|
||||||
|
} else {
|
||||||
|
self.lines.splice(rows_range, new_lines);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.text = changed_text.clone();
|
||||||
|
self.soft_lines = self.lines.iter().map(|l| l.lines_len()).sum();
|
||||||
|
self.longest_row = LongestRow {
|
||||||
|
row: longest_row_ix,
|
||||||
|
len: longest_row_len,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the text wrapper and recalculate the wrapped lines.
|
||||||
|
///
|
||||||
|
/// If the `text` is the same as the current text, do nothing.
|
||||||
|
fn update_all(&mut self, text: &Rope, cx: &mut App) {
|
||||||
|
self.update(text, &(0..text.len()), &text, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return display point (with soft wrap) from the given byte offset in the text.
|
||||||
|
///
|
||||||
|
/// Panics if the `offset` is out of bounds.
|
||||||
|
pub(crate) fn offset_to_display_point(&self, offset: usize) -> WrapDisplayPoint {
|
||||||
|
let row = self.text.offset_to_point(offset).row;
|
||||||
|
let start = self.text.line_start_offset(row);
|
||||||
|
let line = &self.lines[row];
|
||||||
|
|
||||||
|
let mut wrapped_row = self
|
||||||
|
.lines
|
||||||
|
.iter()
|
||||||
|
.take(row)
|
||||||
|
.map(|l| l.lines_len())
|
||||||
|
.sum::<usize>();
|
||||||
|
|
||||||
|
let local_offset = offset.saturating_sub(start);
|
||||||
|
for (ix, range) in line.wrapped_lines.iter().enumerate() {
|
||||||
|
if range.contains(&local_offset) {
|
||||||
|
return WrapDisplayPoint::new(
|
||||||
|
wrapped_row + ix,
|
||||||
|
ix,
|
||||||
|
local_offset.saturating_sub(range.start),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise return the eof of the line.
|
||||||
|
let last_range = line.wrapped_lines.last().unwrap_or(&(0..0));
|
||||||
|
let ix = line.lines_len().saturating_sub(1);
|
||||||
|
return WrapDisplayPoint::new(wrapped_row + ix, ix, last_range.len());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return byte offset in the text from the given display point (with soft wrap).
|
||||||
|
///
|
||||||
|
/// Panics if the `point.row` is out of bounds.
|
||||||
|
pub(crate) fn display_point_to_offset(&self, point: WrapDisplayPoint) -> usize {
|
||||||
|
let mut wrapped_row = 0;
|
||||||
|
for (row, line) in self.lines.iter().enumerate() {
|
||||||
|
if wrapped_row + line.lines_len() > point.row {
|
||||||
|
let line_start = self.text.line_start_offset(row);
|
||||||
|
let local_row = point.row.saturating_sub(wrapped_row);
|
||||||
|
if let Some(range) = line.wrapped_lines.get(local_row) {
|
||||||
|
return line_start + (range.start + point.column).min(range.end);
|
||||||
|
} else {
|
||||||
|
// If not found, return the end of the line.
|
||||||
|
return line_start + line.len();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapped_row += line.lines_len();
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.text.len();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn display_point_to_point(&self, point: WrapDisplayPoint) -> TreeSitterPoint {
|
||||||
|
let offset = self.display_point_to_offset(point);
|
||||||
|
self.text.offset_to_point(offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn point_to_display_point(&self, point: TreeSitterPoint) -> WrapDisplayPoint {
|
||||||
|
let offset = self.text.point_to_offset(point);
|
||||||
|
self.offset_to_display_point(offset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A display point within the soft-wrapped text.
|
||||||
|
///
|
||||||
|
/// This represents a position in the text after soft-wrapping,
|
||||||
|
/// with an additional `local_row` field tracking the wrap line
|
||||||
|
/// within the original buffer line.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub(crate) struct WrapDisplayPoint {
|
||||||
|
/// The 0-based soft wrapped row index in the text.
|
||||||
|
pub row: usize,
|
||||||
|
/// The 0-based row index in local line (include first line).
|
||||||
|
///
|
||||||
|
/// This value only valid when return from [`TextWrapper::offset_to_display_point`], otherwise it will be ignored.
|
||||||
|
pub local_row: usize,
|
||||||
|
/// The 0-based column byte index in the display line (with soft wrap).
|
||||||
|
pub column: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WrapDisplayPoint {
|
||||||
|
pub fn new(row: usize, local_row: usize, column: usize) -> Self {
|
||||||
|
Self {
|
||||||
|
row,
|
||||||
|
local_row,
|
||||||
|
column,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The layout info of a line with soft wrapped lines.
|
||||||
|
pub(crate) struct LineLayout {
|
||||||
|
/// Total bytes length of this line.
|
||||||
|
len: usize,
|
||||||
|
/// The soft wrapped lines of this line (Include the first line).
|
||||||
|
pub(crate) wrapped_lines: SmallVec<[ShapedLine; 1]>,
|
||||||
|
pub(crate) longest_width: Pixels,
|
||||||
|
pub(crate) whitespace_indicators: Option<WhitespaceIndicators>,
|
||||||
|
/// Whitespace indicators: (line_index, x_position, is_tab)
|
||||||
|
pub(crate) whitespace_chars: Vec<(usize, Pixels, bool)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LineLayout {
|
||||||
|
pub(crate) fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
len: 0,
|
||||||
|
longest_width: px(0.),
|
||||||
|
wrapped_lines: SmallVec::new(),
|
||||||
|
whitespace_chars: Vec::new(),
|
||||||
|
whitespace_indicators: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn lines(mut self, wrapped_lines: SmallVec<[ShapedLine; 1]>) -> Self {
|
||||||
|
self.set_wrapped_lines(wrapped_lines);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_wrapped_lines(&mut self, wrapped_lines: SmallVec<[ShapedLine; 1]>) {
|
||||||
|
self.len = wrapped_lines.iter().map(|l| l.len).sum();
|
||||||
|
let width = wrapped_lines
|
||||||
|
.iter()
|
||||||
|
.map(|l| l.width)
|
||||||
|
.max()
|
||||||
|
.unwrap_or_default();
|
||||||
|
self.longest_width = width;
|
||||||
|
self.wrapped_lines = wrapped_lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn with_whitespaces(mut self, indicators: Option<WhitespaceIndicators>) -> Self {
|
||||||
|
self.whitespace_indicators = indicators;
|
||||||
|
let Some(indicators) = self.whitespace_indicators.as_ref() else {
|
||||||
|
return self;
|
||||||
|
};
|
||||||
|
|
||||||
|
let space_indicator_offset = indicators.space.width.half();
|
||||||
|
|
||||||
|
for (line_index, wrapped_line) in self.wrapped_lines.iter().enumerate() {
|
||||||
|
for (relative_offset, c) in wrapped_line.text.char_indices() {
|
||||||
|
if matches!(c, ' ' | '\t') {
|
||||||
|
let is_tab = c == '\t';
|
||||||
|
let start_x = wrapped_line.x_for_index(relative_offset);
|
||||||
|
let end_x = wrapped_line.x_for_index(relative_offset + c.len_utf8());
|
||||||
|
// Center the indicator in the actual character's space
|
||||||
|
let x_position = if c == ' ' {
|
||||||
|
(start_x + end_x).half() - space_indicator_offset
|
||||||
|
} else {
|
||||||
|
start_x
|
||||||
|
};
|
||||||
|
|
||||||
|
self.whitespace_chars.push((line_index, x_position, is_tab));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn len(&self) -> usize {
|
||||||
|
self.len
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the position (x, y) for the given index in this line layout.
|
||||||
|
///
|
||||||
|
/// - The `offset` is a local byte index in this line layout.
|
||||||
|
/// - When `line_end_affinity` is true, an offset at a soft wrap boundary is placed at
|
||||||
|
/// the end of the current visual line rather than the start of the next one.
|
||||||
|
/// - The return value is relative to the top-left corner of this line layout, start from (0, 0)
|
||||||
|
pub(crate) fn position_for_index(
|
||||||
|
&self,
|
||||||
|
offset: usize,
|
||||||
|
last_layout: &LastLayout,
|
||||||
|
line_end_affinity: bool,
|
||||||
|
) -> Option<Point<Pixels>> {
|
||||||
|
let mut acc_len = 0;
|
||||||
|
let mut offset_y = px(0.);
|
||||||
|
|
||||||
|
let x_offset = last_layout.alignment_offset(self.longest_width);
|
||||||
|
|
||||||
|
for (i, line) in self.wrapped_lines.iter().enumerate() {
|
||||||
|
let is_last = i + 1 == self.wrapped_lines.len();
|
||||||
|
|
||||||
|
let matches = if line.len == 0 {
|
||||||
|
// Empty visual lines still own their boundary offset.
|
||||||
|
offset == acc_len
|
||||||
|
} else if is_last || line_end_affinity {
|
||||||
|
// Inclusive: cursor can sit at end of this visual line.
|
||||||
|
offset >= acc_len && offset <= acc_len + line.len
|
||||||
|
} else {
|
||||||
|
// Exclusive: boundary offset belongs to the next visual line.
|
||||||
|
offset >= acc_len && offset < acc_len + line.len
|
||||||
|
};
|
||||||
|
|
||||||
|
if matches {
|
||||||
|
let x = line.x_for_index(offset.saturating_sub(acc_len)) + x_offset;
|
||||||
|
return Some(point(x, offset_y));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always advance by actual line length. The last line gets +1 so the
|
||||||
|
// cursor can be placed after the final character.
|
||||||
|
acc_len += if is_last { line.len + 1 } else { line.len };
|
||||||
|
offset_y += last_layout.line_height;
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the closest index for the given x in this line layout.
|
||||||
|
pub(crate) fn closest_index_for_x(&self, x: Pixels, last_layout: &LastLayout) -> usize {
|
||||||
|
let mut acc_len = 0;
|
||||||
|
let x_offset = last_layout.alignment_offset(self.longest_width);
|
||||||
|
let x = x - x_offset;
|
||||||
|
|
||||||
|
for (i, line) in self.wrapped_lines.iter().enumerate() {
|
||||||
|
let is_last = i + 1 == self.wrapped_lines.len();
|
||||||
|
if x <= line.width {
|
||||||
|
let mut ix = line.closest_index_for_x(x);
|
||||||
|
if !is_last && ix == line.text.len() {
|
||||||
|
// For soft wrap line, we can't put the cursor at the end of the line.
|
||||||
|
let c_len = line.text.chars().last().map(|c| c.len_utf8()).unwrap_or(0);
|
||||||
|
ix = ix.saturating_sub(c_len);
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc_len + ix;
|
||||||
|
}
|
||||||
|
acc_len += line.text.len();
|
||||||
|
}
|
||||||
|
|
||||||
|
acc_len
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the index for the given position (x, y) in this line layout.
|
||||||
|
///
|
||||||
|
/// The `pos` is relative to the top-left corner of this line layout, start from (0, 0)
|
||||||
|
/// The return value is a local byte index in this line layout, start from 0.
|
||||||
|
pub(crate) fn closest_index_for_position(
|
||||||
|
&self,
|
||||||
|
pos: Point<Pixels>,
|
||||||
|
last_layout: &LastLayout,
|
||||||
|
) -> Option<usize> {
|
||||||
|
let mut offset = 0;
|
||||||
|
let mut line_top = px(0.);
|
||||||
|
let x_offset = last_layout.alignment_offset(self.longest_width);
|
||||||
|
for (i, line) in self.wrapped_lines.iter().enumerate() {
|
||||||
|
let is_last = i + 1 == self.wrapped_lines.len();
|
||||||
|
let line_bottom = line_top + last_layout.line_height;
|
||||||
|
if pos.y >= line_top && pos.y < line_bottom {
|
||||||
|
let mut ix = line.closest_index_for_x(pos.x - x_offset);
|
||||||
|
if !is_last && ix == line.text.len() {
|
||||||
|
// For soft wrap line, we can't put the cursor at the end of the line.
|
||||||
|
let c_len = line.text.chars().last().map(|c| c.len_utf8()).unwrap_or(0);
|
||||||
|
ix = ix.saturating_sub(c_len);
|
||||||
|
}
|
||||||
|
return Some(offset + ix);
|
||||||
|
}
|
||||||
|
|
||||||
|
offset += line.text.len();
|
||||||
|
line_top = line_bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn index_for_position(
|
||||||
|
&self,
|
||||||
|
pos: Point<Pixels>,
|
||||||
|
last_layout: &LastLayout,
|
||||||
|
) -> Option<usize> {
|
||||||
|
let mut offset = 0;
|
||||||
|
let mut line_top = px(0.);
|
||||||
|
let x_offset = last_layout.alignment_offset(self.longest_width);
|
||||||
|
for line in self.wrapped_lines.iter() {
|
||||||
|
let line_bottom = line_top + last_layout.line_height;
|
||||||
|
if pos.y >= line_top && pos.y < line_bottom {
|
||||||
|
let ix = line.index_for_x(pos.x - x_offset)?;
|
||||||
|
return Some(offset + ix);
|
||||||
|
}
|
||||||
|
|
||||||
|
offset += line.text.len();
|
||||||
|
line_top = line_bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn size(&self, line_height: Pixels) -> Size<Pixels> {
|
||||||
|
size(self.longest_width, self.wrapped_lines.len() * line_height)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn paint(
|
||||||
|
&self,
|
||||||
|
pos: Point<Pixels>,
|
||||||
|
line_height: Pixels,
|
||||||
|
text_align: TextAlign,
|
||||||
|
align_width: Option<Pixels>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut App,
|
||||||
|
) {
|
||||||
|
for (ix, line) in self.wrapped_lines.iter().enumerate() {
|
||||||
|
_ = line.paint(
|
||||||
|
pos + point(px(0.), ix * line_height),
|
||||||
|
line_height,
|
||||||
|
text_align,
|
||||||
|
align_width,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Paint whitespace indicators
|
||||||
|
if let Some(indicators) = self.whitespace_indicators.as_ref() {
|
||||||
|
for (line_index, x_position, is_tab) in &self.whitespace_chars {
|
||||||
|
let invisible = if *is_tab {
|
||||||
|
indicators.tab.clone()
|
||||||
|
} else {
|
||||||
|
indicators.space.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
let origin = point(
|
||||||
|
pos.x + *x_position,
|
||||||
|
pos.y + *line_index as f32 * line_height,
|
||||||
|
);
|
||||||
|
|
||||||
|
_ = invisible.paint(origin, line_height, text_align, align_width, window, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use gpui::{Boundary, FontFeatures, FontStyle, FontWeight, px};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_update() {
|
||||||
|
let font = gpui::Font {
|
||||||
|
family: "Arial".into(),
|
||||||
|
weight: FontWeight::default(),
|
||||||
|
style: FontStyle::Normal,
|
||||||
|
features: FontFeatures::default(),
|
||||||
|
fallbacks: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut wrapper = TextWrapper::new(font, px(14.), None);
|
||||||
|
let mut text = Rope::from(
|
||||||
|
"Hello, 世界!\r\nThis is second line.\nThis is third line.\n这里是第 4 行。",
|
||||||
|
);
|
||||||
|
|
||||||
|
fn fake_wrap_line(_line: &str, _wrap_width: Pixels) -> Vec<Boundary> {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
|
fn assert_wrapper_lines(text: &Rope, wrapper: &TextWrapper, expected_lines: &[&[&str]]) {
|
||||||
|
let mut actual_lines = vec![];
|
||||||
|
let mut offset = 0;
|
||||||
|
for line in wrapper.lines.iter() {
|
||||||
|
actual_lines.push(
|
||||||
|
line.wrapped_lines
|
||||||
|
.iter()
|
||||||
|
.map(|range| text.slice(offset + range.start..offset + range.end))
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
);
|
||||||
|
// +1 \n
|
||||||
|
offset += line.len() + 1;
|
||||||
|
}
|
||||||
|
assert_eq!(actual_lines, expected_lines);
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapper._update(&text, &(0..text.len()), &text, &mut fake_wrap_line);
|
||||||
|
assert_eq!(wrapper.lines.len(), 4);
|
||||||
|
assert_wrapper_lines(
|
||||||
|
&text,
|
||||||
|
&wrapper,
|
||||||
|
&[
|
||||||
|
&["Hello, 世界!\r"],
|
||||||
|
&["This is second line."],
|
||||||
|
&["This is third line."],
|
||||||
|
&["这里是第 4 行。"],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add a new text to end
|
||||||
|
let range = text.len()..text.len();
|
||||||
|
let new_text = "New text";
|
||||||
|
text.replace(range.clone(), new_text);
|
||||||
|
wrapper._update(&text, &range, &Rope::from(new_text), &mut fake_wrap_line);
|
||||||
|
assert_eq!(
|
||||||
|
text.to_string(),
|
||||||
|
"Hello, 世界!\r\nThis is second line.\nThis is third line.\n这里是第 4 行。New text"
|
||||||
|
);
|
||||||
|
assert_eq!(wrapper.lines.len(), 4);
|
||||||
|
assert_eq!(wrapper.lines.len(), 4);
|
||||||
|
assert_wrapper_lines(
|
||||||
|
&text,
|
||||||
|
&wrapper,
|
||||||
|
&[
|
||||||
|
&["Hello, 世界!\r"],
|
||||||
|
&["This is second line."],
|
||||||
|
&["This is third line."],
|
||||||
|
&["这里是第 4 行。New text"],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Replace first line `Hello` to `AAA`
|
||||||
|
let range = 0..5;
|
||||||
|
let new_text = "AAA";
|
||||||
|
text.replace(range.clone(), new_text);
|
||||||
|
wrapper._update(&text, &range, &Rope::from(new_text), &mut fake_wrap_line);
|
||||||
|
assert_eq!(
|
||||||
|
text.to_string(),
|
||||||
|
"AAA, 世界!\r\nThis is second line.\nThis is third line.\n这里是第 4 行。New text"
|
||||||
|
);
|
||||||
|
assert_eq!(wrapper.lines.len(), 4);
|
||||||
|
assert_wrapper_lines(
|
||||||
|
&text,
|
||||||
|
&wrapper,
|
||||||
|
&[
|
||||||
|
&["AAA, 世界!\r"],
|
||||||
|
&["This is second line."],
|
||||||
|
&["This is third line."],
|
||||||
|
&["这里是第 4 行。New text"],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Remove the second line
|
||||||
|
let start_offset = text.line_start_offset(1);
|
||||||
|
let end_offset = text.line_end_offset(1);
|
||||||
|
let range = start_offset..end_offset + 1;
|
||||||
|
text.replace(range.clone(), "");
|
||||||
|
wrapper._update(&text, &range, &Rope::from(""), &mut fake_wrap_line);
|
||||||
|
assert_eq!(
|
||||||
|
text.to_string(),
|
||||||
|
"AAA, 世界!\r\nThis is third line.\n这里是第 4 行。New text"
|
||||||
|
);
|
||||||
|
assert_eq!(wrapper.lines.len(), 3);
|
||||||
|
assert_wrapper_lines(
|
||||||
|
&text,
|
||||||
|
&wrapper,
|
||||||
|
&[
|
||||||
|
&["AAA, 世界!\r"],
|
||||||
|
&["This is third line."],
|
||||||
|
&["这里是第 4 行。New text"],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Replace the first 2 lines to "This is a new line."
|
||||||
|
let range = text.line_start_offset(0)..text.line_end_offset(1) + 1;
|
||||||
|
let new_text = "This is a new line.\nThis is new line 2.\n";
|
||||||
|
text.replace(range.clone(), new_text);
|
||||||
|
wrapper._update(&text, &range, &Rope::from(new_text), &mut fake_wrap_line);
|
||||||
|
assert_eq!(
|
||||||
|
text.to_string(),
|
||||||
|
"This is a new line.\nThis is new line 2.\n这里是第 4 行。New text"
|
||||||
|
);
|
||||||
|
assert_eq!(wrapper.lines.len(), 3);
|
||||||
|
assert_wrapper_lines(
|
||||||
|
&text,
|
||||||
|
&wrapper,
|
||||||
|
&[
|
||||||
|
&["This is a new line."],
|
||||||
|
&["This is new line 2."],
|
||||||
|
&["这里是第 4 行。New text"],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add a new line at the end
|
||||||
|
let range = text.len()..text.len();
|
||||||
|
let new_text = "\nThis is a new line at the end.";
|
||||||
|
text.replace(range.clone(), new_text);
|
||||||
|
wrapper._update(&text, &range, &Rope::from(new_text), &mut fake_wrap_line);
|
||||||
|
assert_eq!(
|
||||||
|
text.to_string(),
|
||||||
|
"This is a new line.\nThis is new line 2.\n这里是第 4 行。New text\nThis is a new line at the end."
|
||||||
|
);
|
||||||
|
assert_eq!(wrapper.lines.len(), 4);
|
||||||
|
assert_wrapper_lines(
|
||||||
|
&text,
|
||||||
|
&wrapper,
|
||||||
|
&[
|
||||||
|
&["This is a new line."],
|
||||||
|
&["This is new line 2."],
|
||||||
|
&["这里是第 4 行。New text"],
|
||||||
|
&["This is a new line at the end."],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add a new line at the beginning
|
||||||
|
let range = 0..0;
|
||||||
|
let new_text = "This is a new line at the beginning.\n";
|
||||||
|
text.replace(range.clone(), new_text);
|
||||||
|
wrapper._update(&text, &range, &Rope::from(new_text), &mut fake_wrap_line);
|
||||||
|
assert_eq!(
|
||||||
|
text.to_string(),
|
||||||
|
"This is a new line at the beginning.\nThis is a new line.\nThis is new line 2.\n这里是第 4 行。New text\nThis is a new line at the end."
|
||||||
|
);
|
||||||
|
assert_eq!(wrapper.lines.len(), 5);
|
||||||
|
assert_wrapper_lines(
|
||||||
|
&text,
|
||||||
|
&wrapper,
|
||||||
|
&[
|
||||||
|
&["This is a new line at the beginning."],
|
||||||
|
&["This is a new line."],
|
||||||
|
&["This is new line 2."],
|
||||||
|
&["这里是第 4 行。New text"],
|
||||||
|
&["This is a new line at the end."],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Remove all to at least one line in `lines`.
|
||||||
|
let range = 0..text.len();
|
||||||
|
let new_text = "";
|
||||||
|
text.replace(range.clone(), new_text);
|
||||||
|
wrapper._update(&text, &range, &Rope::from(new_text), &mut fake_wrap_line);
|
||||||
|
assert_eq!(text.to_string(), "");
|
||||||
|
assert_eq!(wrapper.lines.len(), 1);
|
||||||
|
assert_eq!(wrapper.lines[0].wrapped_lines, vec![0..0]);
|
||||||
|
|
||||||
|
// Test update_all
|
||||||
|
let range = 0..text.len();
|
||||||
|
let new_text = "This is a full text.\nThis is a second line.";
|
||||||
|
text.replace(range.clone(), new_text);
|
||||||
|
wrapper._update(&text, &range, &text, &mut fake_wrap_line);
|
||||||
|
assert_eq!(
|
||||||
|
text.to_string(),
|
||||||
|
"This is a full text.\nThis is a second line."
|
||||||
|
);
|
||||||
|
assert_eq!(wrapper.lines.len(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_line_layout() {
|
||||||
|
let mut line_layout = LineLayout::new();
|
||||||
|
|
||||||
|
let line1 = ShapedLine::default().with_len(100);
|
||||||
|
let line2 = ShapedLine::default().with_len(50);
|
||||||
|
let wrapped_lines = smallvec::smallvec![line1, line2];
|
||||||
|
line_layout.set_wrapped_lines(wrapped_lines);
|
||||||
|
assert_eq!(line_layout.len(), 150);
|
||||||
|
assert_eq!(line_layout.wrapped_lines.len(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_position_for_index_prefers_first_leading_empty_visual_line() {
|
||||||
|
let mut line_layout = LineLayout::new();
|
||||||
|
line_layout.set_wrapped_lines(smallvec::smallvec![
|
||||||
|
ShapedLine::default(),
|
||||||
|
ShapedLine::default(),
|
||||||
|
ShapedLine::default().with_len(3),
|
||||||
|
]);
|
||||||
|
|
||||||
|
let last_layout = LastLayout {
|
||||||
|
visible_range: 0..1,
|
||||||
|
visible_buffer_lines: vec![0],
|
||||||
|
visible_line_byte_offsets: vec![0],
|
||||||
|
visible_top: px(0.),
|
||||||
|
visible_range_offset: 0..0,
|
||||||
|
lines: Rc::new(vec![]),
|
||||||
|
line_height: px(20.),
|
||||||
|
wrap_width: None,
|
||||||
|
line_number_width: px(0.),
|
||||||
|
cursor_bounds: None,
|
||||||
|
text_align: TextAlign::Left,
|
||||||
|
content_width: px(0.),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
line_layout.position_for_index(0, &last_layout, false),
|
||||||
|
Some(point(px(0.), px(0.)))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_offset_to_display_point() {
|
||||||
|
let font = gpui::Font {
|
||||||
|
family: "Arial".into(),
|
||||||
|
weight: FontWeight::default(),
|
||||||
|
style: FontStyle::Normal,
|
||||||
|
features: FontFeatures::default(),
|
||||||
|
fallbacks: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut wrapper = TextWrapper::new(font, px(14.), None);
|
||||||
|
wrapper.text = Rope::from(
|
||||||
|
"Hello, 世界!\r\nThis is second line.\nThis is third line.\n这里是第 4 行。",
|
||||||
|
);
|
||||||
|
wrapper.lines = vec![
|
||||||
|
// range: 0..15
|
||||||
|
LineItem {
|
||||||
|
line: Rope::from("Hello, 世界!\r"),
|
||||||
|
wrapped_lines: vec![0..15],
|
||||||
|
},
|
||||||
|
// range: 16..36
|
||||||
|
LineItem {
|
||||||
|
line: Rope::from("This is second line."),
|
||||||
|
wrapped_lines: vec![0..10, 10..20],
|
||||||
|
},
|
||||||
|
// range: 37..56
|
||||||
|
LineItem {
|
||||||
|
line: Rope::from("This is third line."),
|
||||||
|
wrapped_lines: vec![0..9, 9..15, 15..20],
|
||||||
|
},
|
||||||
|
// range: 57..79
|
||||||
|
LineItem {
|
||||||
|
line: Rope::from("这里是第 4 行。"),
|
||||||
|
wrapped_lines: vec![0..22],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
wrapper.offset_to_display_point(12),
|
||||||
|
WrapDisplayPoint::new(0, 0, 12)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
wrapper.offset_to_display_point(15),
|
||||||
|
WrapDisplayPoint::new(0, 0, 15)
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
wrapper.offset_to_display_point(16),
|
||||||
|
WrapDisplayPoint::new(1, 0, 0)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
wrapper.offset_to_display_point(21),
|
||||||
|
WrapDisplayPoint::new(1, 0, 5)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
wrapper.offset_to_display_point(27),
|
||||||
|
WrapDisplayPoint::new(2, 1, 1)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
wrapper.offset_to_display_point(37),
|
||||||
|
WrapDisplayPoint::new(3, 0, 0)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
wrapper.offset_to_display_point(54),
|
||||||
|
WrapDisplayPoint::new(5, 2, 2)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
wrapper.offset_to_display_point(59),
|
||||||
|
WrapDisplayPoint::new(6, 0, 2)
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
wrapper.display_point_to_offset(WrapDisplayPoint::new(6, 0, 2)),
|
||||||
|
59
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
wrapper.display_point_to_offset(WrapDisplayPoint::new(5, 2, 2)),
|
||||||
|
54
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
wrapper.display_point_to_offset(WrapDisplayPoint::new(3, 0, 0)),
|
||||||
|
37
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
wrapper.display_point_to_offset(WrapDisplayPoint::new(2, 1, 1)),
|
||||||
|
27
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
wrapper.display_point_to_offset(WrapDisplayPoint::new(1, 0, 5)),
|
||||||
|
21
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
wrapper.display_point_to_offset(WrapDisplayPoint::new(1, 0, 0)),
|
||||||
|
16
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
wrapper.display_point_to_offset(WrapDisplayPoint::new(0, 0, 15)),
|
||||||
|
15
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
222
crates/ui/src/input/display_map/wrap_map.rs
Normal file
222
crates/ui/src/input/display_map/wrap_map.rs
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
/// WrapMap: Soft-wrapping layer (Buffer → Wrap rows).
|
||||||
|
///
|
||||||
|
/// This module wraps the existing TextWrapper and provides:
|
||||||
|
/// - BufferPoint ↔ WrapPoint mapping
|
||||||
|
/// - Efficient buffer_line → wrap_row queries via prefix sum cache
|
||||||
|
/// - Incremental updates when text or layout changes
|
||||||
|
use std::ops::Range;
|
||||||
|
|
||||||
|
use gpui::{App, Font, Pixels};
|
||||||
|
use ropey::Rope;
|
||||||
|
|
||||||
|
use super::fold_map::FoldMap;
|
||||||
|
use super::text_wrapper::{LineItem, TextWrapper, WrapDisplayPoint};
|
||||||
|
use super::{BufferPoint, WrapPoint};
|
||||||
|
use crate::input::rope_ext::RopeExt;
|
||||||
|
|
||||||
|
/// WrapMap manages soft-wrapping and provides buffer ↔ wrap coordinate mapping.
|
||||||
|
pub struct WrapMap {
|
||||||
|
/// The underlying text wrapper (reuses existing implementation)
|
||||||
|
wrapper: TextWrapper,
|
||||||
|
|
||||||
|
/// Prefix sum cache: buffer_line_starts[line] = first wrap_row for buffer line `line`
|
||||||
|
/// This allows O(1) lookup of buffer_line → wrap_row
|
||||||
|
buffer_line_starts: Vec<usize>,
|
||||||
|
|
||||||
|
/// Cached line count from last rebuild
|
||||||
|
cached_line_count: usize,
|
||||||
|
|
||||||
|
/// Cached total wrap row count from last rebuild.
|
||||||
|
/// Used together with `cached_line_count` to detect if the cache is stale.
|
||||||
|
/// When soft wrap changes a line's wrap count without changing buffer line count,
|
||||||
|
/// this catches the staleness.
|
||||||
|
cached_wrap_row_count: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WrapMap {
|
||||||
|
pub fn new(font: Font, font_size: Pixels, wrap_width: Option<Pixels>) -> Self {
|
||||||
|
Self {
|
||||||
|
wrapper: TextWrapper::new(font, font_size, wrap_width),
|
||||||
|
buffer_line_starts: Vec::new(),
|
||||||
|
cached_line_count: 0,
|
||||||
|
cached_wrap_row_count: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get total number of wrap rows (visual rows after soft-wrapping)
|
||||||
|
#[inline]
|
||||||
|
pub fn wrap_row_count(&self) -> usize {
|
||||||
|
self.wrapper.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get total number of buffer lines (logical lines)
|
||||||
|
#[inline]
|
||||||
|
pub fn buffer_line_count(&self) -> usize {
|
||||||
|
self.wrapper.lines.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert buffer position to wrap position
|
||||||
|
pub(super) fn buffer_pos_to_wrap_pos(&self, pos: BufferPoint) -> WrapPoint {
|
||||||
|
let BufferPoint { line, col } = pos;
|
||||||
|
|
||||||
|
// Clamp to valid range
|
||||||
|
let line = line.min(self.buffer_line_count().saturating_sub(1));
|
||||||
|
let line_item = self.wrapper.lines.get(line);
|
||||||
|
|
||||||
|
let col = if let Some(line_item) = line_item {
|
||||||
|
col.min(line_item.len())
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
|
||||||
|
// Calculate offset in rope
|
||||||
|
let line_start_offset = self.wrapper.text().line_start_offset(line);
|
||||||
|
let offset = line_start_offset + col;
|
||||||
|
|
||||||
|
// Use TextWrapper's existing conversion
|
||||||
|
let display_point = self.wrapper.offset_to_display_point(offset);
|
||||||
|
|
||||||
|
WrapPoint::new(display_point.row, display_point.column)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert wrap position to buffer position
|
||||||
|
pub(super) fn wrap_pos_to_buffer_pos(&self, pos: WrapPoint) -> BufferPoint {
|
||||||
|
let WrapPoint { row, col } = pos;
|
||||||
|
|
||||||
|
// Clamp wrap_row to valid range
|
||||||
|
let row = row.min(self.wrap_row_count().saturating_sub(1));
|
||||||
|
|
||||||
|
// Use TextWrapper's existing conversion
|
||||||
|
let display_point = WrapDisplayPoint::new(row, 0, col);
|
||||||
|
let offset = self.wrapper.display_point_to_offset(display_point);
|
||||||
|
|
||||||
|
// Convert offset to buffer position
|
||||||
|
let point = self.wrapper.text().offset_to_point(offset);
|
||||||
|
let line_start = self.wrapper.text().line_start_offset(point.row);
|
||||||
|
let col = offset.saturating_sub(line_start);
|
||||||
|
|
||||||
|
BufferPoint::new(point.row, col)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the buffer line for a given wrap row
|
||||||
|
pub fn wrap_row_to_buffer_line(&self, wrap_row: usize) -> usize {
|
||||||
|
if wrap_row >= self.wrap_row_count() {
|
||||||
|
return self.buffer_line_count().saturating_sub(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Binary search in prefix sum cache
|
||||||
|
match self.buffer_line_starts.binary_search(&wrap_row) {
|
||||||
|
Ok(line) => line,
|
||||||
|
Err(insert_pos) => insert_pos.saturating_sub(1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the first wrap row for a given buffer line
|
||||||
|
pub fn buffer_line_to_first_wrap_row(&self, line: usize) -> usize {
|
||||||
|
if line >= self.buffer_line_starts.len() {
|
||||||
|
return self.wrap_row_count();
|
||||||
|
}
|
||||||
|
self.buffer_line_starts[line]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the wrap row range for a buffer line: [start, end)
|
||||||
|
pub fn buffer_line_to_wrap_row_range(&self, line: usize) -> Range<usize> {
|
||||||
|
let start = self.buffer_line_to_first_wrap_row(line);
|
||||||
|
let end = if line + 1 < self.buffer_line_starts.len() {
|
||||||
|
self.buffer_line_starts[line + 1]
|
||||||
|
} else {
|
||||||
|
self.wrap_row_count()
|
||||||
|
};
|
||||||
|
start..end
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update text (incremental or full)
|
||||||
|
pub fn on_text_changed(
|
||||||
|
&mut self,
|
||||||
|
changed_text: &Rope,
|
||||||
|
range: &Range<usize>,
|
||||||
|
new_text: &Rope,
|
||||||
|
cx: &mut App,
|
||||||
|
) {
|
||||||
|
self.wrapper.update(changed_text, range, new_text, cx);
|
||||||
|
self.rebuild_cache();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update layout parameters (wrap width or font)
|
||||||
|
pub fn on_layout_changed(&mut self, wrap_width: Option<Pixels>, cx: &mut App) {
|
||||||
|
self.wrapper.set_wrap_width(wrap_width, cx);
|
||||||
|
self.rebuild_cache();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set font parameters
|
||||||
|
pub fn set_font(&mut self, font: Font, font_size: Pixels, cx: &mut App) {
|
||||||
|
self.wrapper.set_font(font, font_size, cx);
|
||||||
|
self.rebuild_cache();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ensure text is prepared (initializes wrapper if needed)
|
||||||
|
pub fn ensure_text_prepared(&mut self, text: &Rope, cx: &mut App) -> bool {
|
||||||
|
let did_initialize = self.wrapper.prepare_if_need(text, cx);
|
||||||
|
if did_initialize {
|
||||||
|
self.rebuild_cache();
|
||||||
|
}
|
||||||
|
did_initialize
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initialize with text
|
||||||
|
pub fn set_text(&mut self, text: &Rope, cx: &mut App) {
|
||||||
|
self.wrapper.set_default_text(text);
|
||||||
|
self.wrapper.prepare_if_need(text, cx);
|
||||||
|
self.rebuild_cache();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rebuild the prefix sum cache: buffer_line_starts
|
||||||
|
fn rebuild_cache(&mut self) {
|
||||||
|
let line_count = self.wrapper.lines.len();
|
||||||
|
let wrap_row_count = self.wrapper.len();
|
||||||
|
|
||||||
|
// Skip if nothing changed: both buffer line count and total wrap row count must match.
|
||||||
|
// Checking wrap_row_count is essential because soft-wrap can change the number of
|
||||||
|
// wrap rows per line without changing the buffer line count.
|
||||||
|
if line_count == self.cached_line_count
|
||||||
|
&& wrap_row_count == self.cached_wrap_row_count
|
||||||
|
&& !self.buffer_line_starts.is_empty()
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.buffer_line_starts.clear();
|
||||||
|
|
||||||
|
let mut wrap_row = 0;
|
||||||
|
for line_item in &self.wrapper.lines {
|
||||||
|
self.buffer_line_starts.push(wrap_row);
|
||||||
|
wrap_row += line_item.lines_len();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.cached_line_count = line_count;
|
||||||
|
self.cached_wrap_row_count = wrap_row_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get access to the underlying wrapper (for rendering/hit-testing)
|
||||||
|
pub(crate) fn wrapper(&self) -> &TextWrapper {
|
||||||
|
&self.wrapper
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get access to line items (for rendering)
|
||||||
|
pub(crate) fn lines(&self) -> &[LineItem] {
|
||||||
|
&self.wrapper.lines
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the rope text
|
||||||
|
pub fn text(&self) -> &Rope {
|
||||||
|
self.wrapper.text()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculate how many wrap rows of a buffer line are visible (not folded)
|
||||||
|
pub fn visible_wrap_row_count_for_line(&self, line: usize, fold_map: &FoldMap) -> usize {
|
||||||
|
let wrap_range = self.buffer_line_to_wrap_row_range(line);
|
||||||
|
wrap_range
|
||||||
|
.filter(|&wr| fold_map.wrap_row_to_display_row(wr).is_some())
|
||||||
|
.count()
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
424
crates/ui/src/input/indent.rs
Normal file
424
crates/ui/src/input/indent.rs
Normal file
@@ -0,0 +1,424 @@
|
|||||||
|
use gpui::{
|
||||||
|
Bounds, Context, EntityInputHandler as _, Hsla, Path, PathBuilder, Pixels, SharedString,
|
||||||
|
TextRun, TextStyle, Window, point, px,
|
||||||
|
};
|
||||||
|
use ropey::RopeSlice;
|
||||||
|
|
||||||
|
use crate::input::element::TextElement;
|
||||||
|
use crate::input::mode::InputMode;
|
||||||
|
use crate::input::{Indent, IndentInline, InputState, LastLayout, Outdent, OutdentInline, RopeExt};
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
pub struct TabSize {
|
||||||
|
/// Default is 2
|
||||||
|
pub tab_size: usize,
|
||||||
|
/// Set true to use `\t` as tab indent, default is false
|
||||||
|
pub hard_tabs: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for TabSize {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
tab_size: 2,
|
||||||
|
hard_tabs: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TabSize {
|
||||||
|
pub(super) fn to_string(self) -> SharedString {
|
||||||
|
if self.hard_tabs {
|
||||||
|
"\t".into()
|
||||||
|
} else {
|
||||||
|
" ".repeat(self.tab_size).into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Count the indent size of the line in spaces.
|
||||||
|
pub fn indent_count(&self, line: &RopeSlice) -> usize {
|
||||||
|
let mut count = 0;
|
||||||
|
for ch in line.chars() {
|
||||||
|
match ch {
|
||||||
|
'\t' => count += self.tab_size,
|
||||||
|
' ' => count += 1,
|
||||||
|
_ => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputMode {
|
||||||
|
#[inline]
|
||||||
|
pub(super) fn is_indentable(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
InputMode::PlainText { multi_line, .. } | InputMode::CodeEditor { multi_line, .. } => {
|
||||||
|
*multi_line
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub(super) fn has_indent_guides(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
InputMode::CodeEditor {
|
||||||
|
indent_guides,
|
||||||
|
multi_line,
|
||||||
|
..
|
||||||
|
} => *indent_guides && *multi_line,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub(super) fn tab_size(&self) -> TabSize {
|
||||||
|
match self {
|
||||||
|
InputMode::PlainText { tab, .. } => *tab,
|
||||||
|
InputMode::CodeEditor { tab, .. } => *tab,
|
||||||
|
_ => TabSize::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TextElement {
|
||||||
|
/// Measure the indent width in pixels for given column count.
|
||||||
|
fn measure_indent_width(&self, style: &TextStyle, column: usize, window: &Window) -> Pixels {
|
||||||
|
let font_size = style.font_size.to_pixels(window.rem_size());
|
||||||
|
let layout = window.text_system().shape_line(
|
||||||
|
SharedString::from(" ".repeat(column)),
|
||||||
|
font_size,
|
||||||
|
&[TextRun {
|
||||||
|
len: column,
|
||||||
|
font: style.font(),
|
||||||
|
color: Hsla::default(),
|
||||||
|
background_color: None,
|
||||||
|
strikethrough: None,
|
||||||
|
underline: None,
|
||||||
|
}],
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
layout.width
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn layout_indent_guides(
|
||||||
|
&self,
|
||||||
|
state: &InputState,
|
||||||
|
bounds: &Bounds<Pixels>,
|
||||||
|
last_layout: &LastLayout,
|
||||||
|
text_style: &TextStyle,
|
||||||
|
window: &mut Window,
|
||||||
|
) -> Option<Path<Pixels>> {
|
||||||
|
if !state.mode.has_indent_guides() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let indent_width =
|
||||||
|
self.measure_indent_width(text_style, state.mode.tab_size().tab_size, window);
|
||||||
|
|
||||||
|
let tab_size = state.mode.tab_size();
|
||||||
|
let line_height = last_layout.line_height;
|
||||||
|
let mut builder = PathBuilder::stroke(px(1.));
|
||||||
|
let mut offset_y = last_layout.visible_top;
|
||||||
|
let mut last_indents = vec![];
|
||||||
|
|
||||||
|
for (&buffer_line, line_layout) in last_layout
|
||||||
|
.visible_buffer_lines
|
||||||
|
.iter()
|
||||||
|
.zip(last_layout.lines.iter())
|
||||||
|
{
|
||||||
|
let line = state.text.slice_line(buffer_line);
|
||||||
|
let mut current_indents = vec![];
|
||||||
|
if line.len() > 0 {
|
||||||
|
let indent_count = tab_size.indent_count(&line);
|
||||||
|
for offset in (0..indent_count).step_by(tab_size.tab_size) {
|
||||||
|
let x = if indent_count > 0 {
|
||||||
|
indent_width * offset as f32 / tab_size.tab_size as f32
|
||||||
|
} else {
|
||||||
|
px(0.)
|
||||||
|
};
|
||||||
|
|
||||||
|
let pos = point(x + last_layout.line_number_width, offset_y);
|
||||||
|
|
||||||
|
builder.move_to(pos);
|
||||||
|
builder.line_to(point(pos.x, pos.y + line_height));
|
||||||
|
current_indents.push(pos.x);
|
||||||
|
}
|
||||||
|
} else if !last_indents.is_empty() {
|
||||||
|
for x in &last_indents {
|
||||||
|
let pos = point(*x, offset_y);
|
||||||
|
builder.move_to(pos);
|
||||||
|
builder.line_to(point(pos.x, pos.y + line_height));
|
||||||
|
}
|
||||||
|
current_indents = last_indents.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
offset_y += line_layout.wrapped_lines.len() * line_height;
|
||||||
|
last_indents = current_indents;
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.translate(bounds.origin);
|
||||||
|
let path = builder.build().unwrap();
|
||||||
|
Some(path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputState {
|
||||||
|
/// Set whether to show indent guides in code editor mode, default is true.
|
||||||
|
///
|
||||||
|
/// Only for [`InputMode::CodeEditor`] mode.
|
||||||
|
pub fn indent_guides(mut self, indent_guides: bool) -> Self {
|
||||||
|
debug_assert!(self.mode.is_code_editor() && self.mode.is_multi_line());
|
||||||
|
if let InputMode::CodeEditor {
|
||||||
|
indent_guides: l, ..
|
||||||
|
} = &mut self.mode
|
||||||
|
{
|
||||||
|
*l = indent_guides;
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set indent guides in code editor mode.
|
||||||
|
///
|
||||||
|
/// Only for [`InputMode::CodeEditor`] mode.
|
||||||
|
pub fn set_indent_guides(
|
||||||
|
&mut self,
|
||||||
|
indent_guides: bool,
|
||||||
|
_: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
debug_assert!(self.mode.is_code_editor());
|
||||||
|
if let InputMode::CodeEditor {
|
||||||
|
indent_guides: l, ..
|
||||||
|
} = &mut self.mode
|
||||||
|
{
|
||||||
|
*l = indent_guides;
|
||||||
|
}
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the tab size for the input.
|
||||||
|
///
|
||||||
|
/// Only for [`InputMode::PlainText`] and [`InputMode::CodeEditor`] mode with multi_line.
|
||||||
|
pub fn tab_size(mut self, tab: TabSize) -> Self {
|
||||||
|
debug_assert!(self.mode.is_multi_line() || self.mode.is_code_editor());
|
||||||
|
match &mut self.mode {
|
||||||
|
InputMode::PlainText { tab: t, .. } => *t = tab,
|
||||||
|
InputMode::CodeEditor { tab: t, .. } => *t = tab,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn indent_inline(
|
||||||
|
&mut self,
|
||||||
|
_: &IndentInline,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.indent(false, window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn indent_block(&mut self, _: &Indent, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.indent(true, window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn outdent_inline(
|
||||||
|
&mut self,
|
||||||
|
_: &OutdentInline,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.outdent(false, window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn outdent_block(
|
||||||
|
&mut self,
|
||||||
|
_: &Outdent,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.outdent(true, window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn indent(&mut self, block: bool, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if !self.mode.is_indentable() {
|
||||||
|
cx.propagate();
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let tab_indent = self.mode.tab_size().to_string();
|
||||||
|
let selected_range = self.selected_range;
|
||||||
|
let mut added_len = 0;
|
||||||
|
let is_selected = !self.selected_range.is_empty();
|
||||||
|
|
||||||
|
if is_selected || block {
|
||||||
|
let start_offset = self.start_of_line_of_selection(window, cx);
|
||||||
|
let mut offset = start_offset;
|
||||||
|
|
||||||
|
let selected_text = self
|
||||||
|
.text_for_range(
|
||||||
|
self.range_to_utf16(&(offset..selected_range.end)),
|
||||||
|
&mut None,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.unwrap_or("".into());
|
||||||
|
|
||||||
|
for line in selected_text.split('\n') {
|
||||||
|
self.replace_text_in_range_silent(
|
||||||
|
Some(self.range_to_utf16(&(offset..offset))),
|
||||||
|
&tab_indent,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
added_len += tab_indent.len();
|
||||||
|
// +1 for "\n", the `\r` is included in the `line`.
|
||||||
|
offset += line.len() + tab_indent.len() + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_selected {
|
||||||
|
self.selected_range = (start_offset..selected_range.end + added_len).into();
|
||||||
|
} else {
|
||||||
|
self.selected_range =
|
||||||
|
(selected_range.start + added_len..selected_range.end + added_len).into();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Selected none
|
||||||
|
let offset = self.selected_range.start;
|
||||||
|
self.replace_text_in_range_silent(
|
||||||
|
Some(self.range_to_utf16(&(offset..offset))),
|
||||||
|
&tab_indent,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
added_len = tab_indent.len();
|
||||||
|
|
||||||
|
self.selected_range =
|
||||||
|
(selected_range.start + added_len..selected_range.end + added_len).into();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn outdent(&mut self, block: bool, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if !self.mode.is_indentable() {
|
||||||
|
cx.propagate();
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let tab_indent = self.mode.tab_size().to_string();
|
||||||
|
let selected_range = self.selected_range;
|
||||||
|
let mut removed_len = 0;
|
||||||
|
let is_selected = !self.selected_range.is_empty();
|
||||||
|
|
||||||
|
if is_selected || block {
|
||||||
|
let start_offset = self.start_of_line_of_selection(window, cx);
|
||||||
|
let mut offset = start_offset;
|
||||||
|
|
||||||
|
let selected_text = self
|
||||||
|
.text_for_range(
|
||||||
|
self.range_to_utf16(&(offset..selected_range.end)),
|
||||||
|
&mut None,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.unwrap_or("".into());
|
||||||
|
|
||||||
|
for line in selected_text.split('\n') {
|
||||||
|
if line.starts_with(tab_indent.as_ref()) {
|
||||||
|
self.replace_text_in_range_silent(
|
||||||
|
Some(self.range_to_utf16(&(offset..offset + tab_indent.len()))),
|
||||||
|
"",
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
removed_len += tab_indent.len();
|
||||||
|
|
||||||
|
// +1 for "\n"
|
||||||
|
offset += line.len().saturating_sub(tab_indent.len()) + 1;
|
||||||
|
} else {
|
||||||
|
offset += line.len() + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_selected {
|
||||||
|
self.selected_range =
|
||||||
|
(start_offset..selected_range.end.saturating_sub(removed_len)).into();
|
||||||
|
} else {
|
||||||
|
self.selected_range = (selected_range.start.saturating_sub(removed_len)
|
||||||
|
..selected_range.end.saturating_sub(removed_len))
|
||||||
|
.into();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Selected none
|
||||||
|
let start_offset = self.selected_range.start;
|
||||||
|
let offset = self.start_of_line_of_selection(window, cx);
|
||||||
|
let offset = self.offset_from_utf16(self.offset_to_utf16(offset));
|
||||||
|
// FIXME: To improve performance
|
||||||
|
if self
|
||||||
|
.text
|
||||||
|
.slice(offset..self.text.len())
|
||||||
|
.to_string()
|
||||||
|
.starts_with(tab_indent.as_ref())
|
||||||
|
{
|
||||||
|
self.replace_text_in_range_silent(
|
||||||
|
Some(self.range_to_utf16(&(offset..offset + tab_indent.len()))),
|
||||||
|
"",
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
removed_len = tab_indent.len();
|
||||||
|
let new_offset = start_offset.saturating_sub(removed_len);
|
||||||
|
self.selected_range = (new_offset..new_offset).into();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use ropey::RopeSlice;
|
||||||
|
|
||||||
|
use super::TabSize;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_tab_size() {
|
||||||
|
let tab = TabSize {
|
||||||
|
tab_size: 2,
|
||||||
|
hard_tabs: false,
|
||||||
|
};
|
||||||
|
assert_eq!(tab.to_string(), " ");
|
||||||
|
let tab = TabSize {
|
||||||
|
tab_size: 4,
|
||||||
|
hard_tabs: false,
|
||||||
|
};
|
||||||
|
assert_eq!(tab.to_string(), " ");
|
||||||
|
|
||||||
|
let tab = TabSize {
|
||||||
|
tab_size: 2,
|
||||||
|
hard_tabs: true,
|
||||||
|
};
|
||||||
|
assert_eq!(tab.to_string(), "\t");
|
||||||
|
let tab = TabSize {
|
||||||
|
tab_size: 4,
|
||||||
|
hard_tabs: true,
|
||||||
|
};
|
||||||
|
assert_eq!(tab.to_string(), "\t");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_tab_size_indent_count() {
|
||||||
|
let tab = TabSize {
|
||||||
|
tab_size: 4,
|
||||||
|
hard_tabs: false,
|
||||||
|
};
|
||||||
|
assert_eq!(tab.indent_count(&RopeSlice::from("abc")), 0);
|
||||||
|
assert_eq!(tab.indent_count(&RopeSlice::from(" abc")), 2);
|
||||||
|
assert_eq!(tab.indent_count(&RopeSlice::from(" abc")), 4);
|
||||||
|
assert_eq!(tab.indent_count(&RopeSlice::from("\tabc")), 4);
|
||||||
|
assert_eq!(tab.indent_count(&RopeSlice::from(" \tabc")), 6);
|
||||||
|
assert_eq!(tab.indent_count(&RopeSlice::from(" \t abc ")), 6);
|
||||||
|
assert_eq!(tab.indent_count(&RopeSlice::from("abc")), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,19 +1,30 @@
|
|||||||
use gpui::prelude::FluentBuilder as _;
|
use gpui::prelude::FluentBuilder as _;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, px, relative, AnyElement, App, DefiniteLength, Entity, InteractiveElement as _,
|
AnyElement, App, DefiniteLength, Edges, EdgesRefinement, Entity, Hsla, InteractiveElement as _,
|
||||||
IntoElement, MouseButton, ParentElement as _, Rems, RenderOnce, StyleRefinement, Styled,
|
IntoElement, MouseButton, ParentElement as _, Rems, RenderOnce, StyleRefinement, Styled,
|
||||||
Window,
|
TextAlign, Window, div, px, relative,
|
||||||
};
|
};
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
|
|
||||||
use super::clear_button::clear_button;
|
use super::InputState;
|
||||||
use super::state::{InputState, CONTEXT};
|
use super::element::EditorScrollbar;
|
||||||
use crate::button::{Button, ButtonVariants};
|
use crate::button::{Button, ButtonVariants as _};
|
||||||
use crate::indicator::Indicator;
|
use crate::indicator::Indicator;
|
||||||
use crate::{h_flex, IconName, Sizable, Size, StyleSized, StyledExt};
|
use crate::input::clear_button;
|
||||||
|
use crate::{IconName, Selectable, Sizable, Size, StyleSized, StyledExt, h_flex, v_flex};
|
||||||
|
|
||||||
|
/// Returns `(background, foreground)` colors for input-like components.
|
||||||
|
pub(crate) fn input_style(disabled: bool, cx: &App) -> (Hsla, Hsla) {
|
||||||
|
if disabled {
|
||||||
|
(cx.theme().surface_background, cx.theme().text_muted)
|
||||||
|
} else {
|
||||||
|
(cx.theme().surface_background, cx.theme().text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A text input element bind to an [`InputState`].
|
||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
pub struct TextInput {
|
pub struct Input {
|
||||||
state: Entity<InputState>,
|
state: Entity<InputState>,
|
||||||
style: StyleRefinement,
|
style: StyleRefinement,
|
||||||
size: Size,
|
size: Size,
|
||||||
@@ -26,17 +37,30 @@ pub struct TextInput {
|
|||||||
disabled: bool,
|
disabled: bool,
|
||||||
bordered: bool,
|
bordered: bool,
|
||||||
focus_bordered: bool,
|
focus_bordered: bool,
|
||||||
|
tab_index: isize,
|
||||||
|
selected: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Sizable for TextInput {
|
impl Sizable for Input {
|
||||||
fn with_size(mut self, size: impl Into<Size>) -> Self {
|
fn with_size(mut self, size: impl Into<Size>) -> Self {
|
||||||
self.size = size.into();
|
self.size = size.into();
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TextInput {
|
impl Selectable for Input {
|
||||||
/// Create a new [`TextInput`] element bind to the [`InputState`].
|
fn selected(mut self, selected: bool) -> Self {
|
||||||
|
self.selected = selected;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_selected(&self) -> bool {
|
||||||
|
self.selected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Input {
|
||||||
|
/// Create a new [`Input`] element bind to the [`InputState`].
|
||||||
pub fn new(state: &Entity<InputState>) -> Self {
|
pub fn new(state: &Entity<InputState>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
state: state.clone(),
|
state: state.clone(),
|
||||||
@@ -51,6 +75,8 @@ impl TextInput {
|
|||||||
disabled: false,
|
disabled: false,
|
||||||
bordered: true,
|
bordered: true,
|
||||||
focus_bordered: true,
|
focus_bordered: true,
|
||||||
|
tab_index: 0,
|
||||||
|
selected: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,9 +120,9 @@ impl TextInput {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set true to show the clear button when the input field is not empty.
|
/// Set whether to show the clear button when the input field is not empty, default is false.
|
||||||
pub fn cleanable(mut self) -> Self {
|
pub fn cleanable(mut self, cleanable: bool) -> Self {
|
||||||
self.cleanable = true;
|
self.cleanable = cleanable;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,79 +138,123 @@ impl TextInput {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_toggle_mask_button(state: Entity<InputState>) -> impl IntoElement {
|
/// Set the tab index for the input, default is 0.
|
||||||
|
pub fn tab_index(mut self, index: isize) -> Self {
|
||||||
|
self.tab_index = index;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_toggle_mask_button(state: &Entity<InputState>, cx: &App) -> impl IntoElement {
|
||||||
|
let _masked = state.read(cx).masked;
|
||||||
Button::new("toggle-mask")
|
Button::new("toggle-mask")
|
||||||
.icon(IconName::Eye)
|
.icon(IconName::Eye)
|
||||||
.xsmall()
|
.xsmall()
|
||||||
.ghost()
|
.ghost()
|
||||||
.on_mouse_down(MouseButton::Left, {
|
.tab_stop(false)
|
||||||
|
.on_click({
|
||||||
let state = state.clone();
|
let state = state.clone();
|
||||||
move |_, window, cx| {
|
move |_, window, cx| {
|
||||||
state.update(cx, |state, cx| {
|
state.update(cx, |state, cx| {
|
||||||
state.set_masked(false, window, cx);
|
state.set_masked(!state.masked, window, cx);
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.on_mouse_up(MouseButton::Left, {
|
|
||||||
let state = state.clone();
|
|
||||||
move |_, window, cx| {
|
|
||||||
state.update(cx, |state, cx| {
|
|
||||||
state.set_masked(true, window, cx);
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// This method must after the refine_style.
|
||||||
|
fn render_editor(
|
||||||
|
paddings: EdgesRefinement<DefiniteLength>,
|
||||||
|
input_state: &Entity<InputState>,
|
||||||
|
state: &InputState,
|
||||||
|
window: &Window,
|
||||||
|
) -> impl IntoElement {
|
||||||
|
let base_size = window.text_style().font_size;
|
||||||
|
let rem_size = window.rem_size();
|
||||||
|
|
||||||
|
let paddings = Edges {
|
||||||
|
left: paddings
|
||||||
|
.left
|
||||||
|
.map(|v| v.to_pixels(base_size, rem_size))
|
||||||
|
.unwrap_or(px(0.)),
|
||||||
|
right: paddings
|
||||||
|
.right
|
||||||
|
.map(|v| v.to_pixels(base_size, rem_size))
|
||||||
|
.unwrap_or(px(0.)),
|
||||||
|
top: paddings
|
||||||
|
.top
|
||||||
|
.map(|v| v.to_pixels(base_size, rem_size))
|
||||||
|
.unwrap_or(px(0.)),
|
||||||
|
bottom: paddings
|
||||||
|
.bottom
|
||||||
|
.map(|v| v.to_pixels(base_size, rem_size))
|
||||||
|
.unwrap_or(px(0.)),
|
||||||
|
};
|
||||||
|
|
||||||
|
state.editor_scrollbar_paddings.set(paddings);
|
||||||
|
state.editor_scrollbar_snapshot.set(None);
|
||||||
|
|
||||||
|
v_flex().size_full().child(
|
||||||
|
div()
|
||||||
|
.relative()
|
||||||
|
.flex_1()
|
||||||
|
.child(input_state.clone())
|
||||||
|
.child(EditorScrollbar::new(input_state.clone())),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Styled for TextInput {
|
impl Styled for Input {
|
||||||
fn style(&mut self) -> &mut StyleRefinement {
|
fn style(&mut self) -> &mut StyleRefinement {
|
||||||
&mut self.style
|
&mut self.style
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderOnce for TextInput {
|
impl RenderOnce for Input {
|
||||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
const LINE_HEIGHT: Rems = Rems(1.25);
|
const LINE_HEIGHT: Rems = Rems(1.25);
|
||||||
|
let text_align = self.style.text.text_align.unwrap_or(TextAlign::Left);
|
||||||
|
|
||||||
let font = window.text_style().font();
|
self.state.update(cx, |state, _| {
|
||||||
let font_size = window.text_style().font_size.to_pixels(window.rem_size());
|
|
||||||
|
|
||||||
self.state.update(cx, |state, cx| {
|
|
||||||
state.text_wrapper.set_font(font, font_size, cx);
|
|
||||||
state.text_wrapper.prepare_if_need(&state.text, cx);
|
|
||||||
state.disabled = self.disabled;
|
state.disabled = self.disabled;
|
||||||
|
state.size = self.size;
|
||||||
|
// Only for single line mode
|
||||||
|
if state.mode.is_single_line() {
|
||||||
|
state.text_align = text_align;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let state = self.state.read(cx);
|
let state = self.state.read(cx);
|
||||||
let focused = state.focus_handle.is_focused(window) && !state.disabled;
|
let _focused = state.focus_handle.is_focused(window) && !state.disabled;
|
||||||
|
|
||||||
let gap_x = match self.size {
|
let gap_x = match self.size {
|
||||||
Size::Small => px(4.),
|
Size::Small => px(4.),
|
||||||
Size::Large => px(8.),
|
Size::Large => px(8.),
|
||||||
_ => px(4.),
|
_ => px(6.),
|
||||||
};
|
};
|
||||||
|
|
||||||
let bg = if state.disabled {
|
let (bg, _) = input_style(state.disabled, cx);
|
||||||
|
|
||||||
|
let bg = if state.mode.is_code_editor() {
|
||||||
cx.theme().surface_background
|
cx.theme().surface_background
|
||||||
} else {
|
} else {
|
||||||
cx.theme().elevated_surface_background
|
bg
|
||||||
};
|
};
|
||||||
|
|
||||||
let prefix = self.prefix;
|
let prefix = self.prefix;
|
||||||
let suffix = self.suffix;
|
let suffix = self.suffix;
|
||||||
|
|
||||||
let show_clear_button = self.cleanable
|
let show_clear_button = self.cleanable
|
||||||
|
&& !state.disabled
|
||||||
&& !state.loading
|
&& !state.loading
|
||||||
&& !state.text.is_empty()
|
&& state.text.len() > 0
|
||||||
&& state.mode.is_single_line();
|
&& state.mode.is_single_line();
|
||||||
|
|
||||||
let has_suffix = suffix.is_some() || state.loading || self.mask_toggle || show_clear_button;
|
let has_suffix = suffix.is_some() || state.loading || self.mask_toggle || show_clear_button;
|
||||||
|
|
||||||
div()
|
div()
|
||||||
.id(("input", self.state.entity_id()))
|
.id(("input", self.state.entity_id()))
|
||||||
.flex()
|
.flex()
|
||||||
.key_context(CONTEXT)
|
.key_context(crate::input::CONTEXT)
|
||||||
.track_focus(&state.focus_handle)
|
.track_focus(&state.focus_handle.clone())
|
||||||
|
.tab_index(self.tab_index)
|
||||||
.when(!state.disabled, |this| {
|
.when(!state.disabled, |this| {
|
||||||
this.on_action(window.listener_for(&self.state, InputState::backspace))
|
this.on_action(window.listener_for(&self.state, InputState::backspace))
|
||||||
.on_action(window.listener_for(&self.state, InputState::delete))
|
.on_action(window.listener_for(&self.state, InputState::delete))
|
||||||
@@ -205,9 +275,6 @@ impl RenderOnce for TextInput {
|
|||||||
.on_action(window.listener_for(&self.state, InputState::outdent_inline))
|
.on_action(window.listener_for(&self.state, InputState::outdent_inline))
|
||||||
.on_action(window.listener_for(&self.state, InputState::indent_block))
|
.on_action(window.listener_for(&self.state, InputState::indent_block))
|
||||||
.on_action(window.listener_for(&self.state, InputState::outdent_block))
|
.on_action(window.listener_for(&self.state, InputState::outdent_block))
|
||||||
.on_action(
|
|
||||||
window.listener_for(&self.state, InputState::shift_to_new_line),
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.on_action(window.listener_for(&self.state, InputState::left))
|
.on_action(window.listener_for(&self.state, InputState::left))
|
||||||
@@ -260,8 +327,8 @@ impl RenderOnce for TextInput {
|
|||||||
.input_px(self.size)
|
.input_px(self.size)
|
||||||
.input_py(self.size)
|
.input_py(self.size)
|
||||||
.input_h(self.size)
|
.input_h(self.size)
|
||||||
.cursor_text()
|
.input_font_size(self.size)
|
||||||
.text_size(font_size)
|
.when(!self.disabled, |this| this.cursor_text())
|
||||||
.items_center()
|
.items_center()
|
||||||
.when(state.mode.is_multi_line(), |this| {
|
.when(state.mode.is_multi_line(), |this| {
|
||||||
this.h_auto()
|
this.h_auto()
|
||||||
@@ -269,33 +336,34 @@ impl RenderOnce for TextInput {
|
|||||||
})
|
})
|
||||||
.when(self.appearance, |this| {
|
.when(self.appearance, |this| {
|
||||||
this.bg(bg)
|
this.bg(bg)
|
||||||
|
.when(self.disabled, |this| this.opacity(0.5))
|
||||||
.rounded(cx.theme().radius)
|
.rounded(cx.theme().radius)
|
||||||
.when(self.bordered, |this| {
|
.when(self.bordered, |this| {
|
||||||
this.border_color(cx.theme().border)
|
this.border_color(cx.theme().border)
|
||||||
.border_1()
|
.border_1()
|
||||||
.when(cx.theme().shadow, |this| this.shadow_xs())
|
.when(cx.theme().shadow, |this| this.shadow_xs())
|
||||||
.when(focused && self.focus_bordered, |this| {
|
|
||||||
this.border_color(cx.theme().border_focused)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.items_center()
|
.items_center()
|
||||||
.gap(gap_x)
|
.gap(gap_x)
|
||||||
.refine_style(&self.style)
|
.refine_style(&self.style)
|
||||||
.children(prefix)
|
.children(prefix)
|
||||||
.child(self.state.clone())
|
.when(state.mode.is_multi_line(), |mut this| {
|
||||||
|
let paddings = this.style().padding.clone();
|
||||||
|
this.child(Self::render_editor(paddings, &self.state, state, window))
|
||||||
|
})
|
||||||
|
.when(!state.mode.is_multi_line(), |this| {
|
||||||
|
this.child(self.state.clone())
|
||||||
|
})
|
||||||
.when(has_suffix, |this| {
|
.when(has_suffix, |this| {
|
||||||
this.pr_2().child(
|
this.pr_2().child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.id("suffix")
|
.id("suffix")
|
||||||
.gap(gap_x)
|
.gap(gap_x)
|
||||||
.when(self.appearance, |this| this.bg(bg))
|
|
||||||
.items_center()
|
.items_center()
|
||||||
.when(state.loading, |this| {
|
.when(state.loading, |this| this.child(Indicator::new()))
|
||||||
this.child(Indicator::new().color(cx.theme().text_muted))
|
|
||||||
})
|
|
||||||
.when(self.mask_toggle, |this| {
|
.when(self.mask_toggle, |this| {
|
||||||
this.child(Self::render_toggle_mask_button(self.state.clone()))
|
this.child(Self::render_toggle_mask_button(&self.state, cx))
|
||||||
})
|
})
|
||||||
.when(show_clear_button, |this| {
|
.when(show_clear_button, |this| {
|
||||||
this.child(clear_button(cx).on_click({
|
this.child(clear_button(cx).on_click({
|
||||||
@@ -303,6 +371,7 @@ impl RenderOnce for TextInput {
|
|||||||
move |_, window, cx| {
|
move |_, window, cx| {
|
||||||
state.update(cx, |state, cx| {
|
state.update(cx, |state, cx| {
|
||||||
state.clean(window, cx);
|
state.clean(window, cx);
|
||||||
|
state.focus(window, cx);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
@@ -319,14 +319,14 @@ impl MaskPattern {
|
|||||||
if fraction == &Some(0) {
|
if fraction == &Some(0) {
|
||||||
int_with_sep
|
int_with_sep
|
||||||
} else {
|
} else {
|
||||||
format!("{int_with_sep}.{frac}")
|
format!("{}.{}", int_with_sep, frac)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
int_with_sep
|
int_with_sep
|
||||||
};
|
};
|
||||||
|
|
||||||
let final_str = if let Some(sign) = maybe_signed {
|
let final_str = if let Some(sign) = maybe_signed {
|
||||||
format!("{sign}{final_str}")
|
format!("{}{}", sign, final_str)
|
||||||
} else {
|
} else {
|
||||||
final_str
|
final_str
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,15 +1,29 @@
|
|||||||
|
pub(super) const MASK_CHAR: char = '*';
|
||||||
|
|
||||||
mod blink_cursor;
|
mod blink_cursor;
|
||||||
mod change;
|
mod change;
|
||||||
|
mod clear_button;
|
||||||
mod cursor;
|
mod cursor;
|
||||||
|
mod display_map;
|
||||||
mod element;
|
mod element;
|
||||||
|
mod indent;
|
||||||
|
#[allow(clippy::module_inception)]
|
||||||
|
mod input;
|
||||||
mod mask_pattern;
|
mod mask_pattern;
|
||||||
mod mode;
|
mod mode;
|
||||||
|
mod movement;
|
||||||
mod rope_ext;
|
mod rope_ext;
|
||||||
|
mod selection;
|
||||||
mod state;
|
mod state;
|
||||||
mod text_input;
|
|
||||||
mod text_wrapper;
|
|
||||||
|
|
||||||
pub(crate) mod clear_button;
|
|
||||||
|
|
||||||
|
pub(crate) use clear_button::*;
|
||||||
|
pub use cursor::*;
|
||||||
|
#[cfg(target_family = "wasm")]
|
||||||
|
pub use display_map::folding::Tree;
|
||||||
|
pub use display_map::{BufferPoint, DisplayMap, DisplayPoint, FoldRange};
|
||||||
|
pub use indent::TabSize;
|
||||||
|
pub use input::*;
|
||||||
|
pub use mask_pattern::MaskPattern;
|
||||||
|
pub use rope_ext::{InputEdit, Point, RopeExt, RopeLines};
|
||||||
|
pub use ropey::Rope;
|
||||||
pub use state::*;
|
pub use state::*;
|
||||||
pub use text_input::*;
|
|
||||||
|
|||||||
@@ -1,54 +1,122 @@
|
|||||||
use gpui::SharedString;
|
use std::cell::RefCell;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
use super::text_wrapper::TextWrapper;
|
use gpui::{SharedString, Task};
|
||||||
|
use ropey::Rope;
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone)]
|
use super::display_map::DisplayMap;
|
||||||
pub struct TabSize {
|
use crate::input::TabSize;
|
||||||
/// Default is 2
|
|
||||||
pub tab_size: usize,
|
#[allow(dead_code)]
|
||||||
/// Set true to use `\t` as tab indent, default is false
|
pub(super) struct PendingBackgroundParse {
|
||||||
pub hard_tabs: bool,
|
pub parse_task: Rc<RefCell<Option<Task<()>>>>,
|
||||||
|
pub language: SharedString,
|
||||||
|
pub text: Rope,
|
||||||
|
pub is_folding: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for TabSize {
|
#[derive(Clone)]
|
||||||
fn default() -> Self {
|
pub(crate) enum InputMode {
|
||||||
Self {
|
/// A plain text input mode.
|
||||||
tab_size: 2,
|
PlainText {
|
||||||
hard_tabs: false,
|
multi_line: bool,
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TabSize {
|
|
||||||
pub(super) fn to_string(self) -> SharedString {
|
|
||||||
if self.hard_tabs {
|
|
||||||
"\t".into()
|
|
||||||
} else {
|
|
||||||
" ".repeat(self.tab_size).into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Clone)]
|
|
||||||
pub enum InputMode {
|
|
||||||
#[default]
|
|
||||||
SingleLine,
|
|
||||||
MultiLine {
|
|
||||||
tab: TabSize,
|
tab: TabSize,
|
||||||
rows: usize,
|
rows: usize,
|
||||||
},
|
},
|
||||||
|
/// An auto grow input mode.
|
||||||
AutoGrow {
|
AutoGrow {
|
||||||
rows: usize,
|
rows: usize,
|
||||||
min_rows: usize,
|
min_rows: usize,
|
||||||
max_rows: usize,
|
max_rows: usize,
|
||||||
},
|
},
|
||||||
|
/// A code editor input mode.
|
||||||
|
CodeEditor {
|
||||||
|
multi_line: bool,
|
||||||
|
tab: TabSize,
|
||||||
|
rows: usize,
|
||||||
|
/// Show line number
|
||||||
|
line_number: bool,
|
||||||
|
language: SharedString,
|
||||||
|
indent_guides: bool,
|
||||||
|
folding: bool,
|
||||||
|
parse_task: Rc<RefCell<Option<Task<()>>>>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for InputMode {
|
||||||
|
fn default() -> Self {
|
||||||
|
InputMode::plain_text()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
impl InputMode {
|
impl InputMode {
|
||||||
|
/// Create a plain input mode with default settings.
|
||||||
|
pub(super) fn plain_text() -> Self {
|
||||||
|
InputMode::PlainText {
|
||||||
|
multi_line: false,
|
||||||
|
tab: TabSize::default(),
|
||||||
|
rows: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a code editor input mode with default settings.
|
||||||
|
pub(super) fn code_editor(language: impl Into<SharedString>) -> Self {
|
||||||
|
InputMode::CodeEditor {
|
||||||
|
rows: 2,
|
||||||
|
multi_line: true,
|
||||||
|
tab: TabSize::default(),
|
||||||
|
language: language.into(),
|
||||||
|
line_number: true,
|
||||||
|
indent_guides: true,
|
||||||
|
folding: true,
|
||||||
|
parse_task: Rc::new(RefCell::new(None)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create an auto grow input mode with given min and max rows.
|
||||||
|
pub(super) fn auto_grow(min_rows: usize, max_rows: usize) -> Self {
|
||||||
|
InputMode::AutoGrow {
|
||||||
|
rows: min_rows,
|
||||||
|
min_rows,
|
||||||
|
max_rows,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn multi_line(mut self, multi_line: bool) -> Self {
|
||||||
|
match &mut self {
|
||||||
|
InputMode::PlainText { multi_line: ml, .. } => *ml = multi_line,
|
||||||
|
InputMode::CodeEditor { multi_line: ml, .. } => *ml = multi_line,
|
||||||
|
InputMode::AutoGrow { .. } => {}
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(super) fn is_single_line(&self) -> bool {
|
pub(super) fn is_single_line(&self) -> bool {
|
||||||
matches!(self, InputMode::SingleLine)
|
!self.is_multi_line()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub(super) fn is_code_editor(&self) -> bool {
|
||||||
|
matches!(self, InputMode::CodeEditor { .. })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return true if the mode is code editor and `folding: true`, `multi_line: true`.
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn is_folding(&self) -> bool {
|
||||||
|
if cfg!(target_family = "wasm") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
matches!(
|
||||||
|
self,
|
||||||
|
InputMode::CodeEditor {
|
||||||
|
folding: true,
|
||||||
|
multi_line: true,
|
||||||
|
..
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
@@ -58,15 +126,19 @@ impl InputMode {
|
|||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(super) fn is_multi_line(&self) -> bool {
|
pub(super) fn is_multi_line(&self) -> bool {
|
||||||
matches!(
|
match self {
|
||||||
self,
|
InputMode::PlainText { multi_line, .. } => *multi_line,
|
||||||
InputMode::MultiLine { .. } | InputMode::AutoGrow { .. }
|
InputMode::CodeEditor { multi_line, .. } => *multi_line,
|
||||||
)
|
InputMode::AutoGrow { max_rows, .. } => *max_rows > 1,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn set_rows(&mut self, new_rows: usize) {
|
pub(super) fn set_rows(&mut self, new_rows: usize) {
|
||||||
match self {
|
match self {
|
||||||
InputMode::MultiLine { rows, .. } => {
|
InputMode::PlainText { rows, .. } => {
|
||||||
|
*rows = new_rows;
|
||||||
|
}
|
||||||
|
InputMode::CodeEditor { rows, .. } => {
|
||||||
*rows = new_rows;
|
*rows = new_rows;
|
||||||
}
|
}
|
||||||
InputMode::AutoGrow {
|
InputMode::AutoGrow {
|
||||||
@@ -76,25 +148,28 @@ impl InputMode {
|
|||||||
} => {
|
} => {
|
||||||
*rows = new_rows.clamp(*min_rows, *max_rows);
|
*rows = new_rows.clamp(*min_rows, *max_rows);
|
||||||
}
|
}
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn update_auto_grow(&mut self, text_wrapper: &TextWrapper) {
|
pub(super) fn update_auto_grow(&mut self, display_map: &DisplayMap) {
|
||||||
if self.is_single_line() {
|
if self.is_single_line() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let wrapped_lines = text_wrapper.len();
|
let wrapped_lines = display_map.wrap_row_count();
|
||||||
self.set_rows(wrapped_lines);
|
self.set_rows(wrapped_lines);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// At least 1 row be return.
|
/// At least 1 row be return.
|
||||||
pub(super) fn rows(&self) -> usize {
|
pub(super) fn rows(&self) -> usize {
|
||||||
|
if !self.is_multi_line() {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
match self {
|
match self {
|
||||||
InputMode::MultiLine { rows, .. } => *rows,
|
InputMode::PlainText { rows, .. } => *rows,
|
||||||
|
InputMode::CodeEditor { rows, .. } => *rows,
|
||||||
InputMode::AutoGrow { rows, .. } => *rows,
|
InputMode::AutoGrow { rows, .. } => *rows,
|
||||||
_ => 1,
|
|
||||||
}
|
}
|
||||||
.max(1)
|
.max(1)
|
||||||
}
|
}
|
||||||
@@ -103,7 +178,6 @@ impl InputMode {
|
|||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
pub(super) fn min_rows(&self) -> usize {
|
pub(super) fn min_rows(&self) -> usize {
|
||||||
match self {
|
match self {
|
||||||
InputMode::MultiLine { .. } => 1,
|
|
||||||
InputMode::AutoGrow { min_rows, .. } => *min_rows,
|
InputMode::AutoGrow { min_rows, .. } => *min_rows,
|
||||||
_ => 1,
|
_ => 1,
|
||||||
}
|
}
|
||||||
@@ -112,18 +186,26 @@ impl InputMode {
|
|||||||
|
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
pub(super) fn max_rows(&self) -> usize {
|
pub(super) fn max_rows(&self) -> usize {
|
||||||
|
if !self.is_multi_line() {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
match self {
|
match self {
|
||||||
InputMode::MultiLine { .. } => usize::MAX,
|
|
||||||
InputMode::AutoGrow { max_rows, .. } => *max_rows,
|
InputMode::AutoGrow { max_rows, .. } => *max_rows,
|
||||||
_ => 1,
|
_ => usize::MAX,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return false if the mode is not [`InputMode::CodeEditor`].
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(super) fn tab_size(&self) -> Option<&TabSize> {
|
pub(super) fn line_number(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
InputMode::MultiLine { tab, .. } => Some(tab),
|
InputMode::CodeEditor {
|
||||||
_ => None,
|
line_number,
|
||||||
|
multi_line,
|
||||||
|
..
|
||||||
|
} => *line_number && *multi_line,
|
||||||
|
_ => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
264
crates/ui/src/input/movement.rs
Normal file
264
crates/ui/src/input/movement.rs
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
use gpui::{Context, Point, Window};
|
||||||
|
|
||||||
|
use crate::input::{
|
||||||
|
InputState, MoveDown, MoveEnd, MoveHome, MoveLeft, MovePageDown, MovePageUp, MoveRight,
|
||||||
|
MoveToEnd, MoveToNextWord, MoveToPreviousWord, MoveToStart, MoveUp, RopeExt as _,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub(crate) enum MoveDirection {
|
||||||
|
Up,
|
||||||
|
Down,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputState {
|
||||||
|
/// Called after moving the cursor. Updates preferred_column if we know where the cursor now is.
|
||||||
|
pub(super) fn update_preferred_column(&mut self) {
|
||||||
|
let Some(last_layout) = &self.last_layout else {
|
||||||
|
self.preferred_column = None;
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let point = self.text.offset_to_point(self.cursor());
|
||||||
|
let Some(line) = last_layout.line(point.row) else {
|
||||||
|
self.preferred_column = None;
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(pos) = line.position_for_index(point.column, last_layout, false) else {
|
||||||
|
self.preferred_column = None;
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
self.preferred_column = Some((pos.x, point.column));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Move the cursor to the given offset.
|
||||||
|
///
|
||||||
|
/// The offset is the UTF-8 offset.
|
||||||
|
///
|
||||||
|
/// Ensure the offset use self.next_boundary or self.previous_boundary to get the correct offset.
|
||||||
|
pub(crate) fn move_to(
|
||||||
|
&mut self,
|
||||||
|
offset: usize,
|
||||||
|
direction: Option<MoveDirection>,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
let offset = offset.clamp(0, self.text.len());
|
||||||
|
self.cursor_line_end_affinity = false;
|
||||||
|
self.selected_range = (offset..offset).into();
|
||||||
|
self.scroll_to(offset, direction, cx);
|
||||||
|
self.pause_blink_cursor(cx);
|
||||||
|
self.update_preferred_column();
|
||||||
|
cx.notify()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Move the cursor vertically by one line (up or down) while preserving the column if possible.
|
||||||
|
///
|
||||||
|
/// move_lines: Number of lines to move vertically (positive for down, negative for up).
|
||||||
|
pub(super) fn move_vertical(
|
||||||
|
&mut self,
|
||||||
|
move_lines: isize,
|
||||||
|
_: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
if self.mode.is_single_line() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let Some(last_layout) = &self.last_layout else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let offset = self.cursor();
|
||||||
|
let was_preferred_column = self.preferred_column;
|
||||||
|
|
||||||
|
let mut display_point = self.display_map.offset_to_wrap_display_point(offset);
|
||||||
|
|
||||||
|
// Convert wrap row → display row (skips folded rows), move, then convert back
|
||||||
|
let current_display_row = self
|
||||||
|
.display_map
|
||||||
|
.wrap_row_to_display_row(display_point.row)
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
self.display_map
|
||||||
|
.nearest_visible_display_row(display_point.row)
|
||||||
|
});
|
||||||
|
let max_display_row = self.display_map.display_row_count().saturating_sub(1);
|
||||||
|
let target_display_row = current_display_row
|
||||||
|
.saturating_add_signed(move_lines)
|
||||||
|
.min(max_display_row);
|
||||||
|
let target_wrap_row = self
|
||||||
|
.display_map
|
||||||
|
.display_row_to_wrap_row(target_display_row)
|
||||||
|
.unwrap_or(display_point.row);
|
||||||
|
|
||||||
|
display_point.row = target_wrap_row;
|
||||||
|
display_point.column = 0;
|
||||||
|
let mut new_offset = self.display_map.wrap_display_point_to_offset(display_point);
|
||||||
|
|
||||||
|
if let Some((preferred_x, column)) = was_preferred_column {
|
||||||
|
// Get display point again to update local_row.
|
||||||
|
let mut next_display_point = self.display_map.offset_to_wrap_display_point(new_offset);
|
||||||
|
next_display_point.column = 0;
|
||||||
|
let next_point = self
|
||||||
|
.display_map
|
||||||
|
.wrap_display_point_to_point(next_display_point);
|
||||||
|
let line_start_offset = self.text.line_start_offset(next_point.row);
|
||||||
|
|
||||||
|
// If in visible range, prefer to use position to get column.
|
||||||
|
if let Some(line) = last_layout.line(next_point.row) {
|
||||||
|
if let Some(x) = line.closest_index_for_position(
|
||||||
|
Point {
|
||||||
|
x: preferred_x,
|
||||||
|
y: next_display_point.local_row * last_layout.line_height,
|
||||||
|
},
|
||||||
|
last_layout,
|
||||||
|
) {
|
||||||
|
new_offset = line_start_offset + x;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Not in visible range, use column directly.
|
||||||
|
let max_line_len = self.text.slice_line(next_point.row).len();
|
||||||
|
new_offset = line_start_offset + column.min(max_line_len);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.pause_blink_cursor(cx);
|
||||||
|
let direction = if move_lines < 0 {
|
||||||
|
MoveDirection::Up
|
||||||
|
} else {
|
||||||
|
MoveDirection::Down
|
||||||
|
};
|
||||||
|
self.move_to(new_offset, Some(direction), cx);
|
||||||
|
// Set back the preferred_column
|
||||||
|
self.preferred_column = was_preferred_column;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn left(&mut self, _: &MoveLeft, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.pause_blink_cursor(cx);
|
||||||
|
if self.selected_range.is_empty() {
|
||||||
|
self.move_to(self.previous_boundary(self.cursor()), None, cx);
|
||||||
|
} else {
|
||||||
|
self.move_to(self.selected_range.start, None, cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn right(&mut self, _: &MoveRight, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.pause_blink_cursor(cx);
|
||||||
|
if self.selected_range.is_empty() {
|
||||||
|
self.move_to(self.next_boundary(self.selected_range.end), None, cx);
|
||||||
|
} else {
|
||||||
|
self.move_to(self.selected_range.end, None, cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn up(&mut self, _action: &MoveUp, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if self.mode.is_single_line() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.selected_range.is_empty() {
|
||||||
|
self.move_to(
|
||||||
|
self.previous_boundary(self.selected_range.start.saturating_sub(1)),
|
||||||
|
Some(MoveDirection::Up),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
self.pause_blink_cursor(cx);
|
||||||
|
self.move_vertical(-1, window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn down(&mut self, _action: &MoveDown, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if self.mode.is_single_line() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.selected_range.is_empty() {
|
||||||
|
self.move_to(
|
||||||
|
self.next_boundary(self.selected_range.end.saturating_sub(1)),
|
||||||
|
Some(MoveDirection::Down),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.pause_blink_cursor(cx);
|
||||||
|
self.move_vertical(1, window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn page_up(&mut self, _: &MovePageUp, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if self.mode.is_single_line() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(last_layout) = &self.last_layout else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let display_lines = (self.input_bounds.size.height / last_layout.line_height) as isize;
|
||||||
|
self.move_vertical(-display_lines, window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn page_down(
|
||||||
|
&mut self,
|
||||||
|
_: &MovePageDown,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
if self.mode.is_single_line() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(last_layout) = &self.last_layout else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let display_lines = (self.input_bounds.size.height / last_layout.line_height) as isize;
|
||||||
|
self.move_vertical(display_lines, window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn home(&mut self, _: &MoveHome, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.pause_blink_cursor(cx);
|
||||||
|
let offset = self.start_of_line();
|
||||||
|
self.move_to(offset, Some(MoveDirection::Up), cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn end(&mut self, _: &MoveEnd, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.pause_blink_cursor(cx);
|
||||||
|
let offset = self.end_of_line();
|
||||||
|
self.move_to(offset, Some(MoveDirection::Down), cx);
|
||||||
|
self.cursor_line_end_affinity = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn move_to_start(
|
||||||
|
&mut self,
|
||||||
|
_: &MoveToStart,
|
||||||
|
_: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.move_to(0, None, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn move_to_end(&mut self, _: &MoveToEnd, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.move_to(self.text.len(), None, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn move_to_previous_word(
|
||||||
|
&mut self,
|
||||||
|
_: &MoveToPreviousWord,
|
||||||
|
_: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
let offset = self.previous_start_of_word();
|
||||||
|
self.move_to(offset, None, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn move_to_next_word(
|
||||||
|
&mut self,
|
||||||
|
_: &MoveToNextWord,
|
||||||
|
_: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
let offset = self.next_end_of_word();
|
||||||
|
self.move_to(offset, None, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
337
crates/ui/src/input/popovers/code_action_menu.rs
Normal file
337
crates/ui/src/input/popovers/code_action_menu.rs
Normal file
@@ -0,0 +1,337 @@
|
|||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use gpui::prelude::FluentBuilder;
|
||||||
|
use gpui::{
|
||||||
|
Action, AnyElement, App, AppContext, Context, DismissEvent, Empty, Entity, EventEmitter,
|
||||||
|
InteractiveElement as _, IntoElement, ParentElement, Pixels, Point, Render, RenderOnce,
|
||||||
|
SharedString, Styled, StyledText, Subscription, Window, deferred, div, px, relative,
|
||||||
|
};
|
||||||
|
use lsp_types::CodeAction;
|
||||||
|
use theme::ActiveTheme;
|
||||||
|
|
||||||
|
const MAX_MENU_WIDTH: Pixels = px(320.);
|
||||||
|
const MAX_MENU_HEIGHT: Pixels = px(480.);
|
||||||
|
|
||||||
|
use crate::input::popovers::editor_popover;
|
||||||
|
use crate::input::{self, InputState};
|
||||||
|
use crate::list::{List, ListDelegate, ListEvent, ListState};
|
||||||
|
use crate::{IndexPath, Selectable, actions, h_flex};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub(crate) struct CodeActionItem {
|
||||||
|
/// The `id` of the `CodeActionProvider` that provided this item.
|
||||||
|
pub(crate) provider_id: SharedString,
|
||||||
|
pub(crate) action: CodeAction,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MenuDelegate {
|
||||||
|
menu: Entity<CodeActionMenu>,
|
||||||
|
items: Vec<Rc<CodeActionItem>>,
|
||||||
|
selected_ix: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MenuDelegate {
|
||||||
|
fn set_items(&mut self, items: Vec<CodeActionItem>) {
|
||||||
|
self.items = items.into_iter().map(Rc::new).collect();
|
||||||
|
self.selected_ix = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn selected_item(&self) -> Option<&Rc<CodeActionItem>> {
|
||||||
|
self.items.get(self.selected_ix)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(IntoElement)]
|
||||||
|
struct MenuItem {
|
||||||
|
ix: usize,
|
||||||
|
item: Rc<CodeActionItem>,
|
||||||
|
children: Vec<AnyElement>,
|
||||||
|
selected: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MenuItem {
|
||||||
|
fn new(ix: usize, item: Rc<CodeActionItem>) -> Self {
|
||||||
|
Self {
|
||||||
|
ix,
|
||||||
|
item,
|
||||||
|
children: vec![],
|
||||||
|
selected: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Selectable for MenuItem {
|
||||||
|
fn selected(mut self, selected: bool) -> Self {
|
||||||
|
self.selected = selected;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_selected(&self) -> bool {
|
||||||
|
self.selected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ParentElement for MenuItem {
|
||||||
|
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||||
|
self.children.extend(elements);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl RenderOnce for MenuItem {
|
||||||
|
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
|
let item = self.item;
|
||||||
|
|
||||||
|
let highlights = vec![];
|
||||||
|
|
||||||
|
h_flex()
|
||||||
|
.id(self.ix)
|
||||||
|
.gap_2()
|
||||||
|
.p_1()
|
||||||
|
.text_xs()
|
||||||
|
.line_height(relative(1.))
|
||||||
|
.rounded(cx.theme().radius)
|
||||||
|
.hover(|this| this.bg(cx.theme().secondary_hover))
|
||||||
|
.when(self.selected, |this| {
|
||||||
|
this.bg(cx.theme().secondary_background)
|
||||||
|
.text_color(cx.theme().secondary_foreground)
|
||||||
|
})
|
||||||
|
.child(
|
||||||
|
div().child(StyledText::new(item.action.title.clone()).with_highlights(highlights)),
|
||||||
|
)
|
||||||
|
.children(self.children)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventEmitter<DismissEvent> for MenuDelegate {}
|
||||||
|
|
||||||
|
impl ListDelegate for MenuDelegate {
|
||||||
|
type Item = MenuItem;
|
||||||
|
|
||||||
|
fn items_count(&self, _: usize, _: &gpui::App) -> usize {
|
||||||
|
self.items.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_item(
|
||||||
|
&mut self,
|
||||||
|
ix: crate::IndexPath,
|
||||||
|
_: &mut Window,
|
||||||
|
_: &mut Context<ListState<Self>>,
|
||||||
|
) -> Option<Self::Item> {
|
||||||
|
let item = self.items.get(ix.row)?;
|
||||||
|
Some(MenuItem::new(ix.row, item.clone()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_selected_index(
|
||||||
|
&mut self,
|
||||||
|
ix: Option<crate::IndexPath>,
|
||||||
|
_: &mut Window,
|
||||||
|
cx: &mut Context<ListState<Self>>,
|
||||||
|
) {
|
||||||
|
self.selected_ix = ix.map(|i| i.row).unwrap_or(0);
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<ListState<Self>>) {
|
||||||
|
let Some(item) = self.selected_item() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
self.menu.update(cx, |this, cx| {
|
||||||
|
this.select_item(&item, window, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A context menu for code completions and code actions.
|
||||||
|
pub struct CodeActionMenu {
|
||||||
|
offset: usize,
|
||||||
|
state: Entity<InputState>,
|
||||||
|
list: Entity<ListState<MenuDelegate>>,
|
||||||
|
open: bool,
|
||||||
|
|
||||||
|
_subscriptions: Vec<Subscription>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CodeActionMenu {
|
||||||
|
/// Creates a new `CompletionMenu` with the given offset and completion items.
|
||||||
|
///
|
||||||
|
/// NOTE: This element should not call from InputState::new, unless that will stack overflow.
|
||||||
|
pub(crate) fn new(
|
||||||
|
state: Entity<InputState>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> Entity<Self> {
|
||||||
|
cx.new(|cx| {
|
||||||
|
let view = cx.entity();
|
||||||
|
let menu = MenuDelegate {
|
||||||
|
menu: view,
|
||||||
|
items: vec![],
|
||||||
|
selected_ix: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
let list = cx.new(|cx| ListState::new(menu, window, cx));
|
||||||
|
|
||||||
|
let _subscriptions =
|
||||||
|
vec![
|
||||||
|
cx.subscribe(&list, |this: &mut Self, _, ev: &ListEvent, cx| {
|
||||||
|
match ev {
|
||||||
|
ListEvent::Confirm(_) => {
|
||||||
|
this.hide(cx);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
cx.notify();
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
Self {
|
||||||
|
offset: 0,
|
||||||
|
state,
|
||||||
|
list,
|
||||||
|
open: false,
|
||||||
|
_subscriptions,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn select_item(&mut self, item: &CodeActionItem, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let state = self.state.clone();
|
||||||
|
let item = item.clone();
|
||||||
|
|
||||||
|
cx.spawn_in(window, {
|
||||||
|
async move |_, cx| {
|
||||||
|
state.update_in(cx, |state, window, cx| {
|
||||||
|
state.perform_code_action(&item, window, cx);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
self.hide(cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn handle_action(
|
||||||
|
&mut self,
|
||||||
|
action: Box<dyn Action>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> bool {
|
||||||
|
if !self.open {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
cx.propagate();
|
||||||
|
if input::Enter::is_primary(&*action) {
|
||||||
|
self.on_action_enter(window, cx);
|
||||||
|
} else if action.partial_eq(&input::Escape) {
|
||||||
|
self.on_action_escape(window, cx);
|
||||||
|
} else if action.partial_eq(&input::MoveUp) {
|
||||||
|
self.on_action_up(window, cx);
|
||||||
|
} else if action.partial_eq(&input::MoveDown) {
|
||||||
|
self.on_action_down(window, cx);
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_action_enter(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let Some(item) = self.list.read(cx).delegate().selected_item().cloned() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
self.select_item(&item, window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_action_escape(&mut self, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.hide(cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_action_up(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.list.update(cx, |this, cx| {
|
||||||
|
this.on_action_select_prev(&actions::SelectUp, window, cx)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_action_down(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.list.update(cx, |this, cx| {
|
||||||
|
this.on_action_select_next(&actions::SelectDown, window, cx)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn is_open(&self) -> bool {
|
||||||
|
self.open
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Hide the completion menu and reset the trigger start offset.
|
||||||
|
pub(crate) fn hide(&mut self, cx: &mut Context<Self>) {
|
||||||
|
self.open = false;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn show(
|
||||||
|
&mut self,
|
||||||
|
offset: usize,
|
||||||
|
items: impl Into<Vec<CodeActionItem>>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
let items = items.into();
|
||||||
|
self.offset = offset;
|
||||||
|
self.open = true;
|
||||||
|
self.list.update(cx, |this, cx| {
|
||||||
|
this.delegate_mut().set_items(items);
|
||||||
|
this.set_selected_index(Some(IndexPath::new(0)), window, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn origin(&self, cx: &App) -> Option<Point<Pixels>> {
|
||||||
|
let state = self.state.read(cx);
|
||||||
|
let Some(last_layout) = state.last_layout.as_ref() else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
let Some(cursor_origin) = last_layout.cursor_bounds.map(|b| b.origin) else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
let scroll_origin = self.state.read(cx).scroll_handle.offset();
|
||||||
|
|
||||||
|
Some(
|
||||||
|
scroll_origin + cursor_origin - state.input_bounds.origin
|
||||||
|
+ Point::new(-px(4.), last_layout.line_height + px(4.)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for CodeActionMenu {
|
||||||
|
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
if !self.open {
|
||||||
|
return Empty.into_any_element();
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.list.read(cx).delegate().items.is_empty() {
|
||||||
|
self.open = false;
|
||||||
|
return Empty.into_any_element();
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(pos) = self.origin(cx) else {
|
||||||
|
return Empty.into_any_element();
|
||||||
|
};
|
||||||
|
|
||||||
|
let max_width = MAX_MENU_WIDTH.min(window.bounds().size.width - pos.x);
|
||||||
|
|
||||||
|
deferred(
|
||||||
|
editor_popover("code-action-menu", cx)
|
||||||
|
.absolute()
|
||||||
|
.left(pos.x)
|
||||||
|
.top(pos.y)
|
||||||
|
.max_w(max_width)
|
||||||
|
.min_w(px(120.))
|
||||||
|
.child(List::new(&self.list).max_h(MAX_MENU_HEIGHT))
|
||||||
|
.on_mouse_down_out(cx.listener(|this, _, _, cx| {
|
||||||
|
this.hide(cx);
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
}
|
||||||
446
crates/ui/src/input/popovers/completion_menu.rs
Normal file
446
crates/ui/src/input/popovers/completion_menu.rs
Normal file
@@ -0,0 +1,446 @@
|
|||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use gpui::prelude::FluentBuilder;
|
||||||
|
use gpui::{
|
||||||
|
Action, AnyElement, App, AppContext, Context, DismissEvent, Empty, Entity, EventEmitter,
|
||||||
|
Half as _, HighlightStyle, InteractiveElement as _, IntoElement, ParentElement, Pixels, Point,
|
||||||
|
Render, RenderOnce, SharedString, Styled, StyledText, Subscription, Window, deferred, div, px,
|
||||||
|
relative,
|
||||||
|
};
|
||||||
|
use lsp_types::{CompletionItem, CompletionTextEdit};
|
||||||
|
use theme::ActiveTheme;
|
||||||
|
|
||||||
|
const MAX_MENU_WIDTH: Pixels = px(320.);
|
||||||
|
const MAX_MENU_HEIGHT: Pixels = px(240.);
|
||||||
|
const POPOVER_GAP: Pixels = px(4.);
|
||||||
|
|
||||||
|
use crate::input::popovers::{editor_popover, render_markdown};
|
||||||
|
use crate::input::{self, InputState, RopeExt};
|
||||||
|
use crate::list::{List, ListDelegate, ListEvent, ListState};
|
||||||
|
use crate::{IndexPath, Selectable, actions, h_flex};
|
||||||
|
|
||||||
|
struct ContextMenuDelegate {
|
||||||
|
query: SharedString,
|
||||||
|
menu: Entity<CompletionMenu>,
|
||||||
|
items: Vec<Rc<CompletionItem>>,
|
||||||
|
selected_ix: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ContextMenuDelegate {
|
||||||
|
fn set_items(&mut self, items: Vec<CompletionItem>) {
|
||||||
|
self.items = items.into_iter().map(Rc::new).collect();
|
||||||
|
self.selected_ix = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn selected_item(&self) -> Option<&Rc<CompletionItem>> {
|
||||||
|
self.items.get(self.selected_ix)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(IntoElement)]
|
||||||
|
struct CompletionMenuItem {
|
||||||
|
ix: usize,
|
||||||
|
item: Rc<CompletionItem>,
|
||||||
|
children: Vec<AnyElement>,
|
||||||
|
selected: bool,
|
||||||
|
highlight_prefix: SharedString,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CompletionMenuItem {
|
||||||
|
fn new(ix: usize, item: Rc<CompletionItem>) -> Self {
|
||||||
|
Self {
|
||||||
|
ix,
|
||||||
|
item,
|
||||||
|
children: vec![],
|
||||||
|
selected: false,
|
||||||
|
highlight_prefix: "".into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn highlight_prefix(mut self, s: impl Into<SharedString>) -> Self {
|
||||||
|
self.highlight_prefix = s.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Selectable for CompletionMenuItem {
|
||||||
|
fn selected(mut self, selected: bool) -> Self {
|
||||||
|
self.selected = selected;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_selected(&self) -> bool {
|
||||||
|
self.selected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ParentElement for CompletionMenuItem {
|
||||||
|
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||||
|
self.children.extend(elements);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderOnce for CompletionMenuItem {
|
||||||
|
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
|
let item = self.item;
|
||||||
|
|
||||||
|
let matched_len = item
|
||||||
|
.filter_text
|
||||||
|
.as_ref()
|
||||||
|
.map(|s| s.len())
|
||||||
|
.unwrap_or(self.highlight_prefix.len())
|
||||||
|
.min(item.label.len());
|
||||||
|
|
||||||
|
let highlights = vec![(
|
||||||
|
0..matched_len,
|
||||||
|
HighlightStyle {
|
||||||
|
color: Some(cx.theme().selection),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)];
|
||||||
|
|
||||||
|
h_flex()
|
||||||
|
.id(self.ix)
|
||||||
|
.gap_2()
|
||||||
|
.p_1()
|
||||||
|
.text_xs()
|
||||||
|
.line_height(relative(1.))
|
||||||
|
.rounded(cx.theme().radius.half())
|
||||||
|
.when(item.deprecated.unwrap_or(false), |this| this.line_through())
|
||||||
|
.hover(|this| this.bg(cx.theme().secondary_hover))
|
||||||
|
.when(self.selected, |this| {
|
||||||
|
this.bg(cx.theme().secondary_background)
|
||||||
|
.text_color(cx.theme().secondary_foreground)
|
||||||
|
})
|
||||||
|
.child(div().child(StyledText::new(item.label.clone()).with_highlights(highlights)))
|
||||||
|
.children(self.children)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventEmitter<DismissEvent> for ContextMenuDelegate {}
|
||||||
|
|
||||||
|
impl ListDelegate for ContextMenuDelegate {
|
||||||
|
type Item = CompletionMenuItem;
|
||||||
|
|
||||||
|
fn items_count(&self, _: usize, _: &gpui::App) -> usize {
|
||||||
|
self.items.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_item(
|
||||||
|
&mut self,
|
||||||
|
ix: crate::IndexPath,
|
||||||
|
_: &mut Window,
|
||||||
|
_: &mut Context<ListState<Self>>,
|
||||||
|
) -> Option<Self::Item> {
|
||||||
|
let item = self.items.get(ix.row)?;
|
||||||
|
Some(CompletionMenuItem::new(ix.row, item.clone()).highlight_prefix(self.query.clone()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_selected_index(
|
||||||
|
&mut self,
|
||||||
|
ix: Option<crate::IndexPath>,
|
||||||
|
_: &mut Window,
|
||||||
|
cx: &mut Context<ListState<Self>>,
|
||||||
|
) {
|
||||||
|
self.selected_ix = ix.map(|i| i.row).unwrap_or(0);
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<ListState<Self>>) {
|
||||||
|
let Some(item) = self.selected_item() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
self.menu.update(cx, |this, cx| {
|
||||||
|
this.select_item(&item, window, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A context menu for code completions and code actions.
|
||||||
|
pub struct CompletionMenu {
|
||||||
|
offset: usize,
|
||||||
|
editor: Entity<InputState>,
|
||||||
|
list: Entity<ListState<ContextMenuDelegate>>,
|
||||||
|
open: bool,
|
||||||
|
|
||||||
|
/// The offset of the first character that triggered the completion.
|
||||||
|
pub(crate) trigger_start_offset: Option<usize>,
|
||||||
|
query: SharedString,
|
||||||
|
_subscriptions: Vec<Subscription>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CompletionMenu {
|
||||||
|
/// Creates a new `CompletionMenu` with the given offset and completion items.
|
||||||
|
///
|
||||||
|
/// NOTE: This element should not call from InputState::new, unless that will stack overflow.
|
||||||
|
pub(crate) fn new(
|
||||||
|
editor: Entity<InputState>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> Entity<Self> {
|
||||||
|
cx.new(|cx| {
|
||||||
|
let view = cx.entity();
|
||||||
|
let menu = ContextMenuDelegate {
|
||||||
|
query: SharedString::default(),
|
||||||
|
menu: view,
|
||||||
|
items: vec![],
|
||||||
|
selected_ix: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
let list = cx.new(|cx| ListState::new(menu, window, cx));
|
||||||
|
|
||||||
|
let _subscriptions =
|
||||||
|
vec![
|
||||||
|
cx.subscribe(&list, |this: &mut Self, _, ev: &ListEvent, cx| {
|
||||||
|
match ev {
|
||||||
|
ListEvent::Confirm(_) => {
|
||||||
|
this.hide(cx);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
cx.notify();
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
Self {
|
||||||
|
offset: 0,
|
||||||
|
editor,
|
||||||
|
list,
|
||||||
|
open: false,
|
||||||
|
trigger_start_offset: None,
|
||||||
|
query: SharedString::default(),
|
||||||
|
_subscriptions,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn select_item(&mut self, item: &CompletionItem, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let offset = self.offset;
|
||||||
|
let item = item.clone();
|
||||||
|
let mut range = self.trigger_start_offset.unwrap_or(self.offset)..self.offset;
|
||||||
|
|
||||||
|
let editor = self.editor.clone();
|
||||||
|
|
||||||
|
cx.spawn_in(window, async move |_, cx| {
|
||||||
|
editor.update_in(cx, |editor, window, cx| {
|
||||||
|
editor.completion_inserting = true;
|
||||||
|
|
||||||
|
let mut new_text = item.label.clone();
|
||||||
|
if let Some(text_edit) = item.text_edit.as_ref() {
|
||||||
|
match text_edit {
|
||||||
|
CompletionTextEdit::Edit(edit) => {
|
||||||
|
new_text = edit.new_text.clone();
|
||||||
|
range.start = editor.text.position_to_offset(&edit.range.start);
|
||||||
|
range.end = editor.text.position_to_offset(&edit.range.end);
|
||||||
|
}
|
||||||
|
CompletionTextEdit::InsertAndReplace(edit) => {
|
||||||
|
new_text = edit.new_text.clone();
|
||||||
|
range.start = editor.text.position_to_offset(&edit.replace.start);
|
||||||
|
range.end = editor.text.position_to_offset(&edit.replace.end);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if let Some(insert_text) = item.insert_text.clone() {
|
||||||
|
new_text = insert_text;
|
||||||
|
range = offset..offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
editor.replace_text_in_range_silent(
|
||||||
|
Some(editor.range_to_utf16(&range)),
|
||||||
|
&new_text,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
editor.completion_inserting = false;
|
||||||
|
// FIXME: Input not get the focus
|
||||||
|
editor.focus(window, cx);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
self.hide(cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn handle_action(
|
||||||
|
&mut self,
|
||||||
|
action: Box<dyn Action>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> bool {
|
||||||
|
if !self.open {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
cx.propagate();
|
||||||
|
if input::Enter::is_primary(&*action) {
|
||||||
|
self.on_action_enter(window, cx);
|
||||||
|
} else if action.partial_eq(&input::Escape) {
|
||||||
|
self.on_action_escape(window, cx);
|
||||||
|
} else if action.partial_eq(&input::MoveUp) {
|
||||||
|
self.on_action_up(window, cx);
|
||||||
|
} else if action.partial_eq(&input::MoveDown) {
|
||||||
|
self.on_action_down(window, cx);
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_action_enter(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let Some(item) = self.list.read(cx).delegate().selected_item().cloned() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
self.select_item(&item, window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_action_escape(&mut self, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.hide(cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_action_up(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.list.update(cx, |this, cx| {
|
||||||
|
this.on_action_select_prev(&actions::SelectUp, window, cx)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_action_down(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.list.update(cx, |this, cx| {
|
||||||
|
this.on_action_select_next(&actions::SelectDown, window, cx)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn is_open(&self) -> bool {
|
||||||
|
self.open
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Hide the completion menu and reset the trigger start offset.
|
||||||
|
pub(crate) fn hide(&mut self, cx: &mut Context<Self>) {
|
||||||
|
self.open = false;
|
||||||
|
self.trigger_start_offset = None;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the trigger start offset if it is not already set.
|
||||||
|
pub(crate) fn update_query(&mut self, start_offset: usize, query: impl Into<SharedString>) {
|
||||||
|
if self.trigger_start_offset.is_none() {
|
||||||
|
self.trigger_start_offset = Some(start_offset);
|
||||||
|
}
|
||||||
|
self.query = query.into();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn show(
|
||||||
|
&mut self,
|
||||||
|
offset: usize,
|
||||||
|
items: impl Into<Vec<CompletionItem>>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
let items = items.into();
|
||||||
|
self.offset = offset;
|
||||||
|
self.open = true;
|
||||||
|
self.list.update(cx, |this, cx| {
|
||||||
|
let longest_ix = items
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.max_by_key(|(_, item)| {
|
||||||
|
item.label.len() + item.detail.as_ref().map(|d| d.len()).unwrap_or(0)
|
||||||
|
})
|
||||||
|
.map(|(ix, _)| ix)
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
this.delegate_mut().query = self.query.clone();
|
||||||
|
this.delegate_mut().set_items(items);
|
||||||
|
this.set_selected_index(Some(IndexPath::new(0)), window, cx);
|
||||||
|
this.set_item_to_measure_index(IndexPath::new(longest_ix), window, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn origin(&self, cx: &App) -> Option<Point<Pixels>> {
|
||||||
|
let editor = self.editor.read(cx);
|
||||||
|
let Some(last_layout) = editor.last_layout.as_ref() else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
let Some(cursor_origin) = last_layout.cursor_bounds.map(|b| b.origin) else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
let scroll_origin = self.editor.read(cx).scroll_handle.offset();
|
||||||
|
|
||||||
|
Some(
|
||||||
|
scroll_origin + cursor_origin - editor.input_bounds.origin
|
||||||
|
+ Point::new(-px(4.), last_layout.line_height + px(4.)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for CompletionMenu {
|
||||||
|
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
if !self.open {
|
||||||
|
return Empty.into_any_element();
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.list.read(cx).delegate().items.is_empty() {
|
||||||
|
self.open = false;
|
||||||
|
return Empty.into_any_element();
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(pos) = self.origin(cx) else {
|
||||||
|
return Empty.into_any_element();
|
||||||
|
};
|
||||||
|
|
||||||
|
let selected_documentation = self
|
||||||
|
.list
|
||||||
|
.read(cx)
|
||||||
|
.delegate()
|
||||||
|
.selected_item()
|
||||||
|
.and_then(|item| item.documentation.clone());
|
||||||
|
|
||||||
|
let max_width = MAX_MENU_WIDTH.min(window.bounds().size.width - pos.x);
|
||||||
|
let abs_pos = self.editor.read(cx).input_bounds.origin + pos;
|
||||||
|
let vertical_layout =
|
||||||
|
abs_pos.x + MAX_MENU_WIDTH + POPOVER_GAP + MAX_MENU_WIDTH + POPOVER_GAP
|
||||||
|
> window.bounds().size.width;
|
||||||
|
|
||||||
|
deferred(
|
||||||
|
div()
|
||||||
|
.absolute()
|
||||||
|
.left(pos.x)
|
||||||
|
.top(pos.y)
|
||||||
|
.flex()
|
||||||
|
.flex_row()
|
||||||
|
.gap(POPOVER_GAP)
|
||||||
|
.items_start()
|
||||||
|
.when(vertical_layout, |this| this.flex_col())
|
||||||
|
.child(
|
||||||
|
editor_popover("completion-menu", cx)
|
||||||
|
.max_w(max_width)
|
||||||
|
.min_w(px(120.))
|
||||||
|
.child(List::new(&self.list).max_h(MAX_MENU_HEIGHT)),
|
||||||
|
)
|
||||||
|
.when_some(selected_documentation, |this, documentation| {
|
||||||
|
let mut doc = match documentation {
|
||||||
|
lsp_types::Documentation::String(s) => s.clone(),
|
||||||
|
lsp_types::Documentation::MarkupContent(mc) => mc.value.clone(),
|
||||||
|
};
|
||||||
|
if vertical_layout {
|
||||||
|
doc = doc.split("\n").next().unwrap_or_default().to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.child(
|
||||||
|
div().child(
|
||||||
|
editor_popover("completion-menu", cx)
|
||||||
|
.w(MAX_MENU_WIDTH)
|
||||||
|
.px_2()
|
||||||
|
.child(render_markdown("doc", doc, window, cx)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.on_mouse_down_out(cx.listener(|this, _, _, cx| {
|
||||||
|
this.hide(cx);
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
}
|
||||||
142
crates/ui/src/input/popovers/context_menu.rs
Normal file
142
crates/ui/src/input/popovers/context_menu.rs
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
use gpui::prelude::FluentBuilder as _;
|
||||||
|
use gpui::{
|
||||||
|
Anchor, App, AppContext as _, Context, DismissEvent, Entity, IntoElement, MouseDownEvent,
|
||||||
|
ParentElement as _, Pixels, Point, Render, Styled, Subscription, Window, anchored, deferred,
|
||||||
|
div, px,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::input::popovers::ContextMenu;
|
||||||
|
use crate::input::{self, InputState};
|
||||||
|
use crate::menu::PopupMenu;
|
||||||
|
|
||||||
|
/// Context menu for mouse right clicks.
|
||||||
|
pub(crate) struct InputContextMenu {
|
||||||
|
editor: Entity<InputState>,
|
||||||
|
menu: Entity<PopupMenu>,
|
||||||
|
mouse_position: Point<Pixels>,
|
||||||
|
open: bool,
|
||||||
|
|
||||||
|
_subscriptions: Vec<Subscription>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputState {
|
||||||
|
pub(crate) fn handle_right_click_menu(
|
||||||
|
&mut self,
|
||||||
|
event: &MouseDownEvent,
|
||||||
|
offset: usize,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
// Show Mouse context menu
|
||||||
|
if !self.selected_range.contains(offset) {
|
||||||
|
self.move_to(offset, None, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.context_menu_content = Some(ContextMenu::RightClick(self.context_menu.clone()));
|
||||||
|
|
||||||
|
let is_code_editor = self.mode.is_code_editor();
|
||||||
|
if is_code_editor {
|
||||||
|
self.handle_hover_definition(offset, window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
let is_enable = !self.disabled;
|
||||||
|
let has_goto_definition = is_enable && self.lsp.definition_provider.is_some();
|
||||||
|
let has_code_action = is_enable && !self.lsp.code_action_providers.is_empty();
|
||||||
|
let is_selected = !self.selected_range.is_empty();
|
||||||
|
let has_paste = is_enable && cx.read_from_clipboard().is_some();
|
||||||
|
|
||||||
|
let action_context = self.focus_handle.clone();
|
||||||
|
self.context_menu.update(cx, |this, cx| {
|
||||||
|
this.mouse_position = event.position;
|
||||||
|
this.menu.update(cx, |menu, cx| {
|
||||||
|
let new_menu = if let Some(builder) = &self.context_menu_builder {
|
||||||
|
builder(PopupMenu::new(cx), window, cx)
|
||||||
|
} else {
|
||||||
|
PopupMenu::new(cx)
|
||||||
|
.when(is_code_editor, |m| {
|
||||||
|
m.menu_with_enable(
|
||||||
|
"Go to Definition",
|
||||||
|
Box::new(input::GoToDefinition),
|
||||||
|
has_goto_definition,
|
||||||
|
)
|
||||||
|
.menu_with_enable(
|
||||||
|
"Show Code Actions",
|
||||||
|
Box::new(input::ToggleCodeActions),
|
||||||
|
has_code_action,
|
||||||
|
)
|
||||||
|
.separator()
|
||||||
|
})
|
||||||
|
.menu_with_enable("Cut", Box::new(input::Cut), is_enable && is_selected)
|
||||||
|
.menu_with_enable("Copy", Box::new(input::Copy), is_selected)
|
||||||
|
.menu_with_enable("Paste", Box::new(input::Paste), has_paste)
|
||||||
|
.separator()
|
||||||
|
.menu("Select All", Box::new(input::SelectAll))
|
||||||
|
};
|
||||||
|
|
||||||
|
menu.menu_items = new_menu.menu_items;
|
||||||
|
menu.action_context = Some(action_context);
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
cx.defer_in(window, |this, _, cx| {
|
||||||
|
this.open = true;
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputContextMenu {
|
||||||
|
pub(crate) fn new(
|
||||||
|
editor: Entity<InputState>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> Entity<Self> {
|
||||||
|
cx.new(|cx| {
|
||||||
|
let menu = cx.new(|cx| PopupMenu::new(cx).small());
|
||||||
|
|
||||||
|
let _subscriptions = vec![cx.subscribe_in(&menu, window, {
|
||||||
|
move |this: &mut Self, _, _: &DismissEvent, window, cx| {
|
||||||
|
this.close(window, cx);
|
||||||
|
}
|
||||||
|
})];
|
||||||
|
|
||||||
|
Self {
|
||||||
|
editor,
|
||||||
|
menu,
|
||||||
|
mouse_position: Point::default(),
|
||||||
|
open: false,
|
||||||
|
_subscriptions,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn is_open(&self) -> bool {
|
||||||
|
self.open
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn close(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.open = false;
|
||||||
|
self.editor.update(cx, |this, cx| {
|
||||||
|
this.focus(window, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for InputContextMenu {
|
||||||
|
fn render(&mut self, _: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
if !self.open {
|
||||||
|
return div().into_any_element();
|
||||||
|
}
|
||||||
|
|
||||||
|
deferred(
|
||||||
|
anchored()
|
||||||
|
.snap_to_window_with_margin(px(8.))
|
||||||
|
.anchor(Anchor::TopLeft)
|
||||||
|
.position(self.mouse_position)
|
||||||
|
.child(div().cursor_default().child(self.menu.clone())),
|
||||||
|
)
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
}
|
||||||
95
crates/ui/src/input/popovers/diagnostic_popover.rs
Normal file
95
crates/ui/src/input/popovers/diagnostic_popover.rs
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use gpui::{
|
||||||
|
prelude::FluentBuilder as _, px, App, AppContext as _, Bounds, Context, Empty, Entity,
|
||||||
|
IntoElement, Pixels, Point, Render, Styled, Window,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
highlighter::DiagnosticEntry,
|
||||||
|
input::{
|
||||||
|
popovers::{render_markdown, Popover},
|
||||||
|
InputState,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct DiagnosticPopover {
|
||||||
|
state: Entity<InputState>,
|
||||||
|
pub(crate) diagnostic: Rc<DiagnosticEntry>,
|
||||||
|
bounds: Bounds<Pixels>,
|
||||||
|
open: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DiagnosticPopover {
|
||||||
|
pub fn new(
|
||||||
|
diagnostic: &DiagnosticEntry,
|
||||||
|
state: Entity<InputState>,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> Entity<Self> {
|
||||||
|
let diagnostic = Rc::new(diagnostic.clone());
|
||||||
|
|
||||||
|
cx.new(|_| Self {
|
||||||
|
diagnostic,
|
||||||
|
state,
|
||||||
|
bounds: Bounds::default(),
|
||||||
|
open: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn show(&mut self, cx: &mut Context<Self>) {
|
||||||
|
self.open = true;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn hide(&mut self, cx: &mut Context<Self>) {
|
||||||
|
self.open = false;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn check_to_hide(&mut self, mouse_position: Point<Pixels>, cx: &mut Context<Self>) {
|
||||||
|
if !self.open {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let padding = px(5.);
|
||||||
|
let bounds = Bounds {
|
||||||
|
origin: self.bounds.origin.map(|v| v - padding),
|
||||||
|
size: self.bounds.size.map(|v| v + padding * 2.),
|
||||||
|
};
|
||||||
|
|
||||||
|
if !bounds.contains(&mouse_position) {
|
||||||
|
self.hide(cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for DiagnosticPopover {
|
||||||
|
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
if !self.open {
|
||||||
|
return Empty.into_any_element();
|
||||||
|
}
|
||||||
|
|
||||||
|
let message = self.diagnostic.message.clone();
|
||||||
|
|
||||||
|
let (border, bg, fg) = (
|
||||||
|
self.diagnostic.severity.border(cx),
|
||||||
|
self.diagnostic.severity.bg(cx),
|
||||||
|
self.diagnostic.severity.fg(cx),
|
||||||
|
);
|
||||||
|
|
||||||
|
Popover::new(
|
||||||
|
"diagnostic-popover",
|
||||||
|
self.state.clone(),
|
||||||
|
self.diagnostic.range.clone(),
|
||||||
|
move |window, cx| render_markdown("message", message.clone(), window, cx),
|
||||||
|
)
|
||||||
|
.when(!self.open, |this| this.invisible())
|
||||||
|
.px_1()
|
||||||
|
.py_0p5()
|
||||||
|
.bg(bg)
|
||||||
|
.text_color(fg)
|
||||||
|
.border_1()
|
||||||
|
.border_color(border)
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
}
|
||||||
292
crates/ui/src/input/popovers/hover_popover.rs
Normal file
292
crates/ui/src/input/popovers/hover_popover.rs
Normal file
@@ -0,0 +1,292 @@
|
|||||||
|
use std::{ops::Range, rc::Rc};
|
||||||
|
|
||||||
|
use gpui::{
|
||||||
|
AnyElement, App, AppContext as _, AvailableSpace, Bounds, Element, ElementId, Entity,
|
||||||
|
InteractiveElement, IntoElement, MouseDownEvent, MouseMoveEvent, ParentElement as _, Pixels,
|
||||||
|
Render, StatefulInteractiveElement as _, StyleRefinement, Styled, Window, deferred, div, point,
|
||||||
|
px,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
StyledExt,
|
||||||
|
input::{InputState, popovers::render_markdown},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct HoverPopover {
|
||||||
|
editor: Entity<InputState>,
|
||||||
|
/// The symbol range byte of the hover trigger.
|
||||||
|
pub(crate) symbol_range: Range<usize>,
|
||||||
|
pub(crate) hover: Rc<lsp_types::Hover>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HoverPopover {
|
||||||
|
pub fn new(
|
||||||
|
editor: Entity<InputState>,
|
||||||
|
symbol_range: Range<usize>,
|
||||||
|
hover: &lsp_types::Hover,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> Entity<Self> {
|
||||||
|
let hover = Rc::new(hover.clone());
|
||||||
|
|
||||||
|
cx.new(|_| Self {
|
||||||
|
editor,
|
||||||
|
symbol_range,
|
||||||
|
hover,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn is_same(&self, offset: usize) -> bool {
|
||||||
|
self.symbol_range.contains(&offset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for HoverPopover {
|
||||||
|
fn render(&mut self, _: &mut Window, _: &mut gpui::Context<Self>) -> impl IntoElement {
|
||||||
|
let contents = match self.hover.contents.clone() {
|
||||||
|
lsp_types::HoverContents::Scalar(scalar) => match scalar {
|
||||||
|
lsp_types::MarkedString::String(s) => s,
|
||||||
|
lsp_types::MarkedString::LanguageString(ls) => ls.value,
|
||||||
|
},
|
||||||
|
lsp_types::HoverContents::Array(arr) => arr
|
||||||
|
.into_iter()
|
||||||
|
.map(|item| match item {
|
||||||
|
lsp_types::MarkedString::String(s) => s,
|
||||||
|
lsp_types::MarkedString::LanguageString(ls) => ls.value,
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\n\n"),
|
||||||
|
lsp_types::HoverContents::Markup(markup) => markup.value,
|
||||||
|
};
|
||||||
|
|
||||||
|
Popover::new(
|
||||||
|
"hover-popover",
|
||||||
|
self.editor.clone(),
|
||||||
|
self.symbol_range.clone(),
|
||||||
|
move |window, cx| render_markdown("message", contents.clone(), window, cx),
|
||||||
|
)
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct Popover {
|
||||||
|
id: ElementId,
|
||||||
|
style: StyleRefinement,
|
||||||
|
editor: Entity<InputState>,
|
||||||
|
range: Range<usize>,
|
||||||
|
width_limit: Range<Pixels>,
|
||||||
|
content_builder: Box<dyn Fn(&mut Window, &mut App) -> AnyElement>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Styled for Popover {
|
||||||
|
fn style(&mut self) -> &mut StyleRefinement {
|
||||||
|
&mut self.style
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Popover {
|
||||||
|
pub fn new<F, E>(
|
||||||
|
id: impl Into<ElementId>,
|
||||||
|
editor: Entity<InputState>,
|
||||||
|
range: Range<usize>,
|
||||||
|
f: F,
|
||||||
|
) -> Self
|
||||||
|
where
|
||||||
|
F: Fn(&mut Window, &mut App) -> E + 'static,
|
||||||
|
E: IntoElement,
|
||||||
|
{
|
||||||
|
Self {
|
||||||
|
id: id.into(),
|
||||||
|
editor,
|
||||||
|
range,
|
||||||
|
style: StyleRefinement::default(),
|
||||||
|
width_limit: px(200.)..px(500.),
|
||||||
|
content_builder: Box::new(move |window, cx| (f)(window, cx).into_any_element()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the bounds of the range in the editor, if it is visible.
|
||||||
|
fn trigger_bounds(&self, cx: &App) -> Option<Bounds<Pixels>> {
|
||||||
|
let editor = self.editor.read(cx);
|
||||||
|
let Some(last_layout) = editor.last_layout.as_ref() else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(last_bounds) = editor.last_bounds else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
let (_, _, start_pos) = editor.line_and_position_for_offset(self.range.start);
|
||||||
|
let (_, _, end_pos) = editor.line_and_position_for_offset(self.range.end);
|
||||||
|
|
||||||
|
let Some(start_pos) = start_pos else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
let Some(end_pos) = end_pos else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(Bounds::from_corners(
|
||||||
|
last_bounds.origin + start_pos,
|
||||||
|
last_bounds.origin + end_pos + point(px(0.), last_layout.line_height),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoElement for Popover {
|
||||||
|
type Element = Self;
|
||||||
|
|
||||||
|
fn into_element(self) -> Self::Element {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct PopoverLayoutState {
|
||||||
|
bounds: Bounds<Pixels>,
|
||||||
|
element: Option<AnyElement>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Element for Popover {
|
||||||
|
type RequestLayoutState = PopoverLayoutState;
|
||||||
|
type PrepaintState = ();
|
||||||
|
|
||||||
|
fn id(&self) -> Option<ElementId> {
|
||||||
|
Some(self.id.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request_layout(
|
||||||
|
&mut self,
|
||||||
|
_: Option<&gpui::GlobalElementId>,
|
||||||
|
_: Option<&gpui::InspectorElementId>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> (gpui::LayoutId, Self::RequestLayoutState) {
|
||||||
|
let trigger_bounds = match self.trigger_bounds(cx) {
|
||||||
|
Some(bounds) => bounds,
|
||||||
|
None => {
|
||||||
|
return (
|
||||||
|
div().into_any_element().request_layout(window, cx),
|
||||||
|
PopoverLayoutState {
|
||||||
|
bounds: Bounds::default(),
|
||||||
|
element: None,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let max_width = self
|
||||||
|
.width_limit
|
||||||
|
.end
|
||||||
|
.min(window.bounds().size.width - SNAP_TO_EDGE * 2)
|
||||||
|
.max(px(200.));
|
||||||
|
let max_height = (window.bounds().size.height - SNAP_TO_EDGE * 2).min(px(320.));
|
||||||
|
|
||||||
|
let mut popover = deferred(
|
||||||
|
div()
|
||||||
|
.id("hover-popover-content")
|
||||||
|
.flex_none()
|
||||||
|
.occlude()
|
||||||
|
.p_1()
|
||||||
|
.text_xs()
|
||||||
|
.popover_style(cx)
|
||||||
|
.shadow_md()
|
||||||
|
.max_w(max_width)
|
||||||
|
.max_h(max_height)
|
||||||
|
.overflow_y_scroll()
|
||||||
|
.refine_style(&self.style)
|
||||||
|
.child((self.content_builder)(window, cx)),
|
||||||
|
)
|
||||||
|
.into_any_element();
|
||||||
|
|
||||||
|
let popover_size = popover.layout_as_root(AvailableSpace::min_size(), window, cx);
|
||||||
|
const SNAP_TO_EDGE: Pixels = px(8.);
|
||||||
|
let top_space = trigger_bounds.top() - SNAP_TO_EDGE;
|
||||||
|
let right_space = window.bounds().size.width - trigger_bounds.left() - SNAP_TO_EDGE;
|
||||||
|
|
||||||
|
let mut pos = point(
|
||||||
|
trigger_bounds.left(),
|
||||||
|
trigger_bounds.top() - popover_size.height,
|
||||||
|
);
|
||||||
|
if popover_size.height > top_space {
|
||||||
|
pos.y = trigger_bounds.bottom();
|
||||||
|
}
|
||||||
|
if popover_size.width > right_space {
|
||||||
|
pos.x = trigger_bounds.right() - popover_size.width;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut empty = div().into_any_element();
|
||||||
|
let layout_id = empty.request_layout(window, cx);
|
||||||
|
(
|
||||||
|
layout_id,
|
||||||
|
PopoverLayoutState {
|
||||||
|
bounds: Bounds {
|
||||||
|
origin: pos,
|
||||||
|
size: popover_size,
|
||||||
|
},
|
||||||
|
element: Some(popover),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepaint(
|
||||||
|
&mut self,
|
||||||
|
_: Option<&gpui::GlobalElementId>,
|
||||||
|
_: Option<&gpui::InspectorElementId>,
|
||||||
|
_: Bounds<Pixels>,
|
||||||
|
request_layout: &mut Self::RequestLayoutState,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> Self::PrepaintState {
|
||||||
|
let bounds = request_layout.bounds;
|
||||||
|
let Some(popover) = request_layout.element.as_mut() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
window.with_absolute_element_offset(bounds.origin, |window| {
|
||||||
|
popover.prepaint(window, cx);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paint(
|
||||||
|
&mut self,
|
||||||
|
_: Option<&gpui::GlobalElementId>,
|
||||||
|
_: Option<&gpui::InspectorElementId>,
|
||||||
|
_: Bounds<Pixels>,
|
||||||
|
request_layout: &mut Self::RequestLayoutState,
|
||||||
|
_: &mut Self::PrepaintState,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut App,
|
||||||
|
) {
|
||||||
|
let bounds = request_layout.bounds;
|
||||||
|
let Some(popover) = request_layout.element.as_mut() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
popover.paint(window, cx);
|
||||||
|
|
||||||
|
let editor = self.editor.clone();
|
||||||
|
// Mouse down out to hide.
|
||||||
|
window.on_mouse_event(move |event: &MouseDownEvent, _, _, cx| {
|
||||||
|
if !bounds.contains(&event.position) {
|
||||||
|
let _ = editor.update(cx, |editor, cx| {
|
||||||
|
editor.clear_hover_state(cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mouse out of trigger + popover bounds
|
||||||
|
let editor = self.editor.clone();
|
||||||
|
let trigger_bounds = self.trigger_bounds(cx).unwrap_or(bounds);
|
||||||
|
let keep_open_region = trigger_bounds.union(&bounds);
|
||||||
|
window.on_mouse_event(move |event: &MouseMoveEvent, _, _, cx| {
|
||||||
|
if !keep_open_region.contains(&event.position) {
|
||||||
|
let _ = editor.update(cx, |editor, cx| {
|
||||||
|
editor.clear_hover_state(cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
41
crates/ui/src/input/popovers/mod.rs
Normal file
41
crates/ui/src/input/popovers/mod.rs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
mod code_action_menu;
|
||||||
|
mod completion_menu;
|
||||||
|
mod context_menu;
|
||||||
|
mod diagnostic_popover;
|
||||||
|
mod hover_popover;
|
||||||
|
|
||||||
|
pub(crate) use code_action_menu::*;
|
||||||
|
pub(crate) use completion_menu::*;
|
||||||
|
pub(crate) use context_menu::*;
|
||||||
|
pub(crate) use diagnostic_popover::*;
|
||||||
|
use gpui::{
|
||||||
|
App, Div, ElementId, Entity, InteractiveElement as _, IntoElement, SharedString, Stateful,
|
||||||
|
StyleRefinement, Styled as _, Window, div, px, rems,
|
||||||
|
};
|
||||||
|
pub(crate) use hover_popover::*;
|
||||||
|
|
||||||
|
use crate::StyledExt as _;
|
||||||
|
|
||||||
|
pub(crate) enum ContextMenu {
|
||||||
|
Completion(Entity<CompletionMenu>),
|
||||||
|
CodeAction(Entity<CodeActionMenu>),
|
||||||
|
RightClick(Entity<InputContextMenu>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ContextMenu {
|
||||||
|
pub(crate) fn is_open(&self, cx: &App) -> bool {
|
||||||
|
match self {
|
||||||
|
ContextMenu::Completion(menu) => menu.read(cx).is_open(),
|
||||||
|
ContextMenu::CodeAction(menu) => menu.read(cx).is_open(),
|
||||||
|
ContextMenu::RightClick(menu) => menu.read(cx).is_open(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn render(&self) -> impl IntoElement {
|
||||||
|
match self {
|
||||||
|
ContextMenu::Completion(menu) => menu.clone().into_any_element(),
|
||||||
|
ContextMenu::CodeAction(menu) => menu.clone().into_any_element(),
|
||||||
|
ContextMenu::RightClick(menu) => menu.clone().into_any_element(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,70 +1,49 @@
|
|||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
|
|
||||||
use rope::{Point, Rope};
|
use ropey::{LineType, Rope, RopeSlice};
|
||||||
|
use sum_tree::Bias;
|
||||||
|
#[cfg(not(target_family = "wasm"))]
|
||||||
|
pub use tree_sitter::{InputEdit, Point};
|
||||||
|
|
||||||
use super::cursor::Position;
|
#[cfg(target_family = "wasm")]
|
||||||
|
/// Stub type for tree-sitter Point on WASM (tree-sitter not available).
|
||||||
/// An extension trait for `Rope` to provide additional utility methods.
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
pub trait RopeExt {
|
pub struct Point {
|
||||||
/// Get the line at the given row (0-based) index, including the `\r` at the end, but not `\n`.
|
pub row: usize,
|
||||||
///
|
pub column: usize,
|
||||||
/// Return empty rope if the row (0-based) is out of bounds.
|
|
||||||
fn line(&self, row: usize) -> Rope;
|
|
||||||
|
|
||||||
/// Start offset of the line at the given row (0-based) index.
|
|
||||||
fn line_start_offset(&self, row: usize) -> usize;
|
|
||||||
|
|
||||||
/// Line the end offset (including `\n`) of the line at the given row (0-based) index.
|
|
||||||
///
|
|
||||||
/// Return the end of the rope if the row is out of bounds.
|
|
||||||
fn line_end_offset(&self, row: usize) -> usize;
|
|
||||||
|
|
||||||
/// Return the number of lines in the rope.
|
|
||||||
fn lines_len(&self) -> usize;
|
|
||||||
|
|
||||||
/// Return the lines iterator.
|
|
||||||
///
|
|
||||||
/// Each line is including the `\r` at the end, but not `\n`.
|
|
||||||
fn lines(&self) -> RopeLines;
|
|
||||||
|
|
||||||
/// Check is equal to another rope.
|
|
||||||
fn eq(&self, other: &Rope) -> bool;
|
|
||||||
|
|
||||||
/// Total number of characters in the rope.
|
|
||||||
fn chars_count(&self) -> usize;
|
|
||||||
|
|
||||||
/// Get char at the given offset (byte).
|
|
||||||
///
|
|
||||||
/// If the offset is in the middle of a multi-byte character will panic.
|
|
||||||
///
|
|
||||||
/// If the offset is out of bounds, return None.
|
|
||||||
fn char_at(&self, offset: usize) -> Option<char>;
|
|
||||||
|
|
||||||
/// Get the byte offset from the given line, column [`Position`] (0-based).
|
|
||||||
fn position_to_offset(&self, line_col: &Position) -> usize;
|
|
||||||
|
|
||||||
/// Get the line, column [`Position`] (0-based) from the given byte offset.
|
|
||||||
fn offset_to_position(&self, offset: usize) -> Position;
|
|
||||||
|
|
||||||
/// Get the word byte range at the given offset (byte).
|
|
||||||
#[allow(dead_code)]
|
|
||||||
fn word_range(&self, offset: usize) -> Option<Range<usize>>;
|
|
||||||
|
|
||||||
/// Get word at the given offset (byte).
|
|
||||||
#[allow(dead_code)]
|
|
||||||
fn word_at(&self, offset: usize) -> String;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_family = "wasm")]
|
||||||
|
impl Point {
|
||||||
|
pub fn new(row: usize, column: usize) -> Self {
|
||||||
|
Self { row, column }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_family = "wasm")]
|
||||||
|
/// Stub type for tree-sitter InputEdit on WASM (tree-sitter not available).
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct InputEdit {
|
||||||
|
pub start_byte: usize,
|
||||||
|
pub old_end_byte: usize,
|
||||||
|
pub new_end_byte: usize,
|
||||||
|
pub start_position: Point,
|
||||||
|
pub old_end_position: Point,
|
||||||
|
pub new_end_position: Point,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type Position = lsp_types::Position;
|
||||||
|
|
||||||
/// An iterator over the lines of a `Rope`.
|
/// An iterator over the lines of a `Rope`.
|
||||||
pub struct RopeLines {
|
pub struct RopeLines<'a> {
|
||||||
|
rope: &'a Rope,
|
||||||
row: usize,
|
row: usize,
|
||||||
end_row: usize,
|
end_row: usize,
|
||||||
rope: Rope,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RopeLines {
|
impl<'a> RopeLines<'a> {
|
||||||
/// Create a new `RopeLines` iterator.
|
/// Create a new `RopeLines` iterator.
|
||||||
pub fn new(rope: Rope) -> Self {
|
pub fn new(rope: &'a Rope) -> Self {
|
||||||
let end_row = rope.lines_len();
|
let end_row = rope.lines_len();
|
||||||
Self {
|
Self {
|
||||||
row: 0,
|
row: 0,
|
||||||
@@ -73,9 +52,8 @@ impl RopeLines {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
impl<'a> Iterator for RopeLines<'a> {
|
||||||
impl Iterator for RopeLines {
|
type Item = RopeSlice<'a>;
|
||||||
type Item = Rope;
|
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
@@ -83,7 +61,7 @@ impl Iterator for RopeLines {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let line = self.rope.line(self.row);
|
let line = self.rope.slice_line(self.row);
|
||||||
self.row += 1;
|
self.row += 1;
|
||||||
Some(line)
|
Some(line)
|
||||||
}
|
}
|
||||||
@@ -101,23 +79,261 @@ impl Iterator for RopeLines {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::iter::ExactSizeIterator for RopeLines {}
|
impl std::iter::ExactSizeIterator for RopeLines<'_> {}
|
||||||
impl std::iter::FusedIterator for RopeLines {}
|
impl std::iter::FusedIterator for RopeLines<'_> {}
|
||||||
|
|
||||||
|
/// An extension trait for [`Rope`] to provide additional utility methods.
|
||||||
|
pub trait RopeExt {
|
||||||
|
/// Start offset of the line at the given row (0-based) index.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use gpui_component::{Rope, RopeExt};
|
||||||
|
///
|
||||||
|
/// let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope");
|
||||||
|
/// assert_eq!(rope.line_start_offset(0), 0);
|
||||||
|
/// assert_eq!(rope.line_start_offset(1), 6);
|
||||||
|
/// ```
|
||||||
|
fn line_start_offset(&self, row: usize) -> usize;
|
||||||
|
|
||||||
|
/// Line the end offset (including `\n`) of the line at the given row (0-based) index.
|
||||||
|
///
|
||||||
|
/// Return the end of the rope if the row is out of bounds.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use gpui_component::{Rope, RopeExt};
|
||||||
|
/// let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope");
|
||||||
|
/// assert_eq!(rope.line_end_offset(0), 5); // "Hello\n"
|
||||||
|
/// assert_eq!(rope.line_end_offset(1), 12); // "World\r\n"
|
||||||
|
/// ```
|
||||||
|
fn line_end_offset(&self, row: usize) -> usize;
|
||||||
|
|
||||||
|
/// Return a line slice at the given row (0-based) index. including `\r` if present, but not `\n`.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use gpui_component::{Rope, RopeExt};
|
||||||
|
/// let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope");
|
||||||
|
/// assert_eq!(rope.slice_line(0).to_string(), "Hello");
|
||||||
|
/// assert_eq!(rope.slice_line(1).to_string(), "World\r");
|
||||||
|
/// assert_eq!(rope.slice_line(2).to_string(), "This is a test 中文");
|
||||||
|
/// assert_eq!(rope.slice_line(6).to_string(), ""); // out of bounds
|
||||||
|
/// ```
|
||||||
|
fn slice_line(&self, row: usize) -> RopeSlice<'_>;
|
||||||
|
|
||||||
|
/// Return a slice of rows in the given range (0-based, end exclusive).
|
||||||
|
///
|
||||||
|
/// If the range is out of bounds, it will be clamped to the valid range.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use gpui_component::{Rope, RopeExt};
|
||||||
|
/// let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope");
|
||||||
|
/// assert_eq!(rope.slice_lines(0..2).to_string(), "Hello\nWorld\r");
|
||||||
|
/// assert_eq!(rope.slice_lines(1..3).to_string(), "World\r\nThis is a test 中文");
|
||||||
|
/// assert_eq!(rope.slice_lines(2..5).to_string(), "This is a test 中文\nRope");
|
||||||
|
/// assert_eq!(rope.slice_lines(3..10).to_string(), "Rope");
|
||||||
|
/// assert_eq!(rope.slice_lines(5..10).to_string(), ""); // out of bounds
|
||||||
|
/// ```
|
||||||
|
fn slice_lines(&self, rows_range: Range<usize>) -> RopeSlice<'_>;
|
||||||
|
|
||||||
|
/// Return an iterator over all lines in the rope.
|
||||||
|
///
|
||||||
|
/// Each line slice includes `\r` if present, but not `\n`.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use gpui_component::{Rope, RopeExt};
|
||||||
|
/// let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope");
|
||||||
|
/// let lines: Vec<_> = rope.iter_lines().map(|r| r.to_string()).collect();
|
||||||
|
/// assert_eq!(lines, vec!["Hello", "World\r", "This is a test 中文", "Rope"]);
|
||||||
|
/// ```
|
||||||
|
fn iter_lines(&self) -> RopeLines<'_>;
|
||||||
|
|
||||||
|
/// Return the number of lines in the rope.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use gpui_component::{Rope, RopeExt};
|
||||||
|
/// let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope");
|
||||||
|
/// assert_eq!(rope.lines_len(), 4);
|
||||||
|
/// ```
|
||||||
|
fn lines_len(&self) -> usize;
|
||||||
|
|
||||||
|
/// Return the length of the row (0-based) in characters, including `\r` if present, but not `\n`.
|
||||||
|
///
|
||||||
|
/// If the row is out of bounds, return 0.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use gpui_component::{Rope, RopeExt};
|
||||||
|
/// let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope");
|
||||||
|
/// assert_eq!(rope.line_len(0), 5); // "Hello"
|
||||||
|
/// assert_eq!(rope.line_len(1), 6); // "World\r"
|
||||||
|
/// assert_eq!(rope.line_len(2), 21); // "This is a test 中文"
|
||||||
|
/// assert_eq!(rope.line_len(4), 0); // out of bounds
|
||||||
|
/// ```
|
||||||
|
fn line_len(&self, row: usize) -> usize;
|
||||||
|
|
||||||
|
/// Replace the text in the given byte range with new text.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// - If the range is not on char boundary.
|
||||||
|
/// - If the range is out of bounds.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use gpui_component::{Rope, RopeExt};
|
||||||
|
/// let mut rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope");
|
||||||
|
/// rope.replace(6..11, "Universe");
|
||||||
|
/// assert_eq!(rope.to_string(), "Hello\nUniverse\r\nThis is a test 中文\nRope");
|
||||||
|
/// ```
|
||||||
|
fn replace(&mut self, range: Range<usize>, new_text: &str);
|
||||||
|
|
||||||
|
/// Get char at the given offset (byte).
|
||||||
|
///
|
||||||
|
/// - If the offset is in the middle of a multi-byte character will panic.
|
||||||
|
/// - If the offset is out of bounds, return None.
|
||||||
|
fn char_at(&self, offset: usize) -> Option<char>;
|
||||||
|
|
||||||
|
/// Get the byte offset from the given line, column [`Position`] (0-based).
|
||||||
|
///
|
||||||
|
/// The column is in characters.
|
||||||
|
fn position_to_offset(&self, line_col: &Position) -> usize;
|
||||||
|
|
||||||
|
/// Get the line, column [`Position`] (0-based) from the given byte offset.
|
||||||
|
///
|
||||||
|
/// The column is in characters.
|
||||||
|
fn offset_to_position(&self, offset: usize) -> Position;
|
||||||
|
|
||||||
|
/// Get point (row, column) from the given byte offset.
|
||||||
|
///
|
||||||
|
/// The column is in bytes.
|
||||||
|
fn offset_to_point(&self, offset: usize) -> Point;
|
||||||
|
|
||||||
|
/// Get byte offset from the given point (row, column).
|
||||||
|
///
|
||||||
|
/// The column is 0-based in bytes.
|
||||||
|
fn point_to_offset(&self, point: Point) -> usize;
|
||||||
|
|
||||||
|
/// Get the word byte range at the given byte offset (0-based).
|
||||||
|
fn word_range(&self, offset: usize) -> Option<Range<usize>>;
|
||||||
|
|
||||||
|
/// Get word at the given byte offset (0-based).
|
||||||
|
fn word_at(&self, offset: usize) -> String;
|
||||||
|
|
||||||
|
/// Convert offset in UTF-16 to byte offset (0-based).
|
||||||
|
///
|
||||||
|
/// Runs in O(log N) time.
|
||||||
|
fn offset_utf16_to_offset(&self, offset_utf16: usize) -> usize;
|
||||||
|
|
||||||
|
/// Convert byte offset (0-based) to offset in UTF-16.
|
||||||
|
///
|
||||||
|
/// Runs in O(log N) time.
|
||||||
|
fn offset_to_offset_utf16(&self, offset: usize) -> usize;
|
||||||
|
|
||||||
|
/// Get a clipped offset (avoid in a char boundary).
|
||||||
|
///
|
||||||
|
/// - If Bias::Left and inside the char boundary, return the ix - 1;
|
||||||
|
/// - If Bias::Right and in inside char boundary, return the ix + 1;
|
||||||
|
/// - Otherwise return the ix.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use gpui_component::{Rope, RopeExt};
|
||||||
|
/// use sum_tree::Bias;
|
||||||
|
///
|
||||||
|
/// let rope = Rope::from("Hello 中文🎉 test\nRope");
|
||||||
|
/// assert_eq!(rope.clip_offset(5, Bias::Left), 5);
|
||||||
|
/// // Inside multi-byte character '中' (3 bytes)
|
||||||
|
/// assert_eq!(rope.clip_offset(7, Bias::Left), 6);
|
||||||
|
/// assert_eq!(rope.clip_offset(7, Bias::Right), 9);
|
||||||
|
/// ```
|
||||||
|
fn clip_offset(&self, offset: usize, bias: Bias) -> usize;
|
||||||
|
|
||||||
|
/// Convert offset in characters to byte offset (0-based).
|
||||||
|
///
|
||||||
|
/// Run in O(n) time.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use gpui_component::{Rope, RopeExt};
|
||||||
|
/// let rope = Rope::from("a 中文🎉 test\nRope");
|
||||||
|
/// assert_eq!(rope.char_index_to_offset(0), 0);
|
||||||
|
/// assert_eq!(rope.char_index_to_offset(1), 1);
|
||||||
|
/// assert_eq!(rope.char_index_to_offset(3), "a 中".len());
|
||||||
|
/// assert_eq!(rope.char_index_to_offset(5), "a 中文🎉".len());
|
||||||
|
/// ```
|
||||||
|
fn char_index_to_offset(&self, char_index: usize) -> usize;
|
||||||
|
|
||||||
|
/// Convert byte offset (0-based) to offset in characters.
|
||||||
|
///
|
||||||
|
/// Run in O(n) time.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use gpui_component::{Rope, RopeExt};
|
||||||
|
/// let rope = Rope::from("a 中文🎉 test\nRope");
|
||||||
|
/// assert_eq!(rope.offset_to_char_index(0), 0);
|
||||||
|
/// assert_eq!(rope.offset_to_char_index(1), 1);
|
||||||
|
/// assert_eq!(rope.offset_to_char_index(3), 3);
|
||||||
|
/// assert_eq!(rope.offset_to_char_index(4), 3);
|
||||||
|
/// ```
|
||||||
|
fn offset_to_char_index(&self, offset: usize) -> usize;
|
||||||
|
}
|
||||||
|
|
||||||
impl RopeExt for Rope {
|
impl RopeExt for Rope {
|
||||||
fn line(&self, row: usize) -> Rope {
|
fn slice_line(&self, row: usize) -> RopeSlice<'_> {
|
||||||
let start = self.line_start_offset(row);
|
let total_lines = self.lines_len();
|
||||||
let end = start + self.line_len(row as u32) as usize;
|
if row >= total_lines {
|
||||||
|
return self.slice(0..0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let line = self.line(row, LineType::LF);
|
||||||
|
if line.len() > 0 {
|
||||||
|
let line_end = line.len() - 1;
|
||||||
|
if line.is_char_boundary(line_end) && line.char(line_end) == '\n' {
|
||||||
|
return line.slice(..line_end);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
line
|
||||||
|
}
|
||||||
|
|
||||||
|
fn slice_lines(&self, rows_range: Range<usize>) -> RopeSlice<'_> {
|
||||||
|
let start = self.line_start_offset(rows_range.start);
|
||||||
|
let end = self.line_end_offset(rows_range.end.saturating_sub(1));
|
||||||
self.slice(start..end)
|
self.slice(start..end)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn iter_lines(&self) -> RopeLines<'_> {
|
||||||
|
RopeLines::new(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn line_len(&self, row: usize) -> usize {
|
||||||
|
self.slice_line(row).len()
|
||||||
|
}
|
||||||
|
|
||||||
fn line_start_offset(&self, row: usize) -> usize {
|
fn line_start_offset(&self, row: usize) -> usize {
|
||||||
let row = row as u32;
|
|
||||||
self.point_to_offset(Point::new(row, 0))
|
self.point_to_offset(Point::new(row, 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn offset_to_point(&self, offset: usize) -> Point {
|
||||||
|
let offset = self.clip_offset(offset, Bias::Left);
|
||||||
|
let row = self.byte_to_line_idx(offset, LineType::LF);
|
||||||
|
let line_start = self.line_to_byte_idx(row, LineType::LF);
|
||||||
|
let column = offset.saturating_sub(line_start);
|
||||||
|
Point::new(row, column)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn point_to_offset(&self, point: Point) -> usize {
|
||||||
|
if point.row >= self.lines_len() {
|
||||||
|
return self.len();
|
||||||
|
}
|
||||||
|
|
||||||
|
let line_start = self.line_to_byte_idx(point.row, LineType::LF);
|
||||||
|
line_start + point.column
|
||||||
|
}
|
||||||
|
|
||||||
fn position_to_offset(&self, pos: &Position) -> usize {
|
fn position_to_offset(&self, pos: &Position) -> usize {
|
||||||
let line = self.line(pos.line as usize);
|
let line = self.slice_line(pos.line as usize);
|
||||||
self.line_start_offset(pos.line as usize)
|
self.line_start_offset(pos.line as usize)
|
||||||
+ line
|
+ line
|
||||||
.chars()
|
.chars()
|
||||||
@@ -128,34 +344,22 @@ impl RopeExt for Rope {
|
|||||||
|
|
||||||
fn offset_to_position(&self, offset: usize) -> Position {
|
fn offset_to_position(&self, offset: usize) -> Position {
|
||||||
let point = self.offset_to_point(offset);
|
let point = self.offset_to_point(offset);
|
||||||
let line = self.line(point.row as usize);
|
let line = self.slice_line(point.row);
|
||||||
let column = line.clip_offset(point.column as usize, sum_tree::Bias::Left);
|
let offset = line.utf16_to_byte_idx(line.byte_to_utf16_idx(point.column));
|
||||||
let character = line.slice(0..column).chars().count();
|
let character = line.slice(..offset).chars().count();
|
||||||
Position::new(point.row, character as u32)
|
Position::new(point.row as u32, character as u32)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn line_end_offset(&self, row: usize) -> usize {
|
fn line_end_offset(&self, row: usize) -> usize {
|
||||||
if row > self.max_point().row as usize {
|
if row > self.lines_len() {
|
||||||
return self.len();
|
return self.len();
|
||||||
}
|
}
|
||||||
|
|
||||||
self.line_start_offset(row) + self.line_len(row as u32) as usize
|
self.line_start_offset(row) + self.line_len(row)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn lines_len(&self) -> usize {
|
fn lines_len(&self) -> usize {
|
||||||
self.max_point().row as usize + 1
|
self.len_lines(LineType::LF)
|
||||||
}
|
|
||||||
|
|
||||||
fn lines(&self) -> RopeLines {
|
|
||||||
RopeLines::new(self.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn eq(&self, other: &Rope) -> bool {
|
|
||||||
self.summary() == other.summary()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn chars_count(&self) -> usize {
|
|
||||||
self.chars().count()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn char_at(&self, offset: usize) -> Option<char> {
|
fn char_at(&self, offset: usize) -> Option<char> {
|
||||||
@@ -163,8 +367,7 @@ impl RopeExt for Rope {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let offset = self.clip_offset(offset, sum_tree::Bias::Left);
|
self.get_char(offset).ok()
|
||||||
self.slice(offset..self.len()).chars().next()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn word_range(&self, offset: usize) -> Option<Range<usize>> {
|
fn word_range(&self, offset: usize) -> Option<Range<usize>> {
|
||||||
@@ -172,10 +375,9 @@ impl RopeExt for Rope {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let offset = self.clip_offset(offset, sum_tree::Bias::Left);
|
|
||||||
|
|
||||||
let mut left = String::new();
|
let mut left = String::new();
|
||||||
for c in self.reversed_chars_at(offset) {
|
let offset = self.clip_offset(offset, Bias::Left);
|
||||||
|
for c in self.chars_at(offset).reversed() {
|
||||||
if c.is_alphanumeric() || c == '_' {
|
if c.is_alphanumeric() || c == '_' {
|
||||||
left.insert(0, c);
|
left.insert(0, c);
|
||||||
} else {
|
} else {
|
||||||
@@ -191,11 +393,7 @@ impl RopeExt for Rope {
|
|||||||
|
|
||||||
let end = offset + right.len();
|
let end = offset + right.len();
|
||||||
|
|
||||||
if start == end {
|
if start == end { None } else { Some(start..end) }
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(start..end)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn word_at(&self, offset: usize) -> String {
|
fn word_at(&self, offset: usize) -> String {
|
||||||
@@ -205,4 +403,54 @@ impl RopeExt for Rope {
|
|||||||
String::new()
|
String::new()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn offset_utf16_to_offset(&self, offset_utf16: usize) -> usize {
|
||||||
|
if offset_utf16 > self.len_utf16() {
|
||||||
|
return self.len();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.utf16_to_byte_idx(offset_utf16)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn offset_to_offset_utf16(&self, offset: usize) -> usize {
|
||||||
|
if offset > self.len() {
|
||||||
|
return self.len_utf16();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.byte_to_utf16_idx(offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn replace(&mut self, range: Range<usize>, new_text: &str) {
|
||||||
|
let range =
|
||||||
|
self.clip_offset(range.start, Bias::Left)..self.clip_offset(range.end, Bias::Right);
|
||||||
|
self.remove(range.clone());
|
||||||
|
self.insert(range.start, new_text);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clip_offset(&self, offset: usize, bias: Bias) -> usize {
|
||||||
|
if offset > self.len() {
|
||||||
|
return self.len();
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.is_char_boundary(offset) {
|
||||||
|
return offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
if bias == Bias::Left {
|
||||||
|
self.floor_char_boundary(offset)
|
||||||
|
} else {
|
||||||
|
self.ceil_char_boundary(offset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn char_index_to_offset(&self, char_offset: usize) -> usize {
|
||||||
|
self.chars().take(char_offset).map(|c| c.len_utf8()).sum()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn offset_to_char_index(&self, offset: usize) -> usize {
|
||||||
|
let offset = self.clip_offset(offset, Bias::Right);
|
||||||
|
self.slice(..offset).chars().count()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
140
crates/ui/src/input/selection.rs
Normal file
140
crates/ui/src/input/selection.rs
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
use std::ops::Range;
|
||||||
|
|
||||||
|
use gpui::{Context, Window};
|
||||||
|
use ropey::Rope;
|
||||||
|
use sum_tree::Bias;
|
||||||
|
|
||||||
|
use crate::input::{InputState, RopeExt};
|
||||||
|
|
||||||
|
impl InputState {
|
||||||
|
/// Select the word at the given offset on double-click.
|
||||||
|
///
|
||||||
|
/// The offset is the UTF-8 offset.
|
||||||
|
pub(super) fn select_word(&mut self, offset: usize, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let Some(range) = TextSelector::word_range(&self.text, offset) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
self.selected_range = (range.start..range.end).into();
|
||||||
|
self.selected_word_range = Some(self.selected_range);
|
||||||
|
cx.notify()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Select the line at the given offset on triple-click.
|
||||||
|
///
|
||||||
|
/// The offset is the UTF-8 offset.
|
||||||
|
pub(super) fn select_line(&mut self, offset: usize, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let range = TextSelector::line_range(&self.text, offset);
|
||||||
|
self.selected_range = (range.start..range.end).into();
|
||||||
|
self.selected_word_range = None;
|
||||||
|
cx.notify()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TextSelector;
|
||||||
|
impl TextSelector {
|
||||||
|
/// Select a line in the given text at the specified offset.
|
||||||
|
///
|
||||||
|
/// The offset is the UTF-8 offset.
|
||||||
|
///
|
||||||
|
/// Returns the start and end offsets of the selected line.
|
||||||
|
pub fn line_range(text: &Rope, offset: usize) -> Range<usize> {
|
||||||
|
let offset = text.clip_offset(offset, Bias::Left);
|
||||||
|
let row = text.offset_to_point(offset).row;
|
||||||
|
let start = text.line_start_offset(row);
|
||||||
|
let end = text.line_end_offset(row);
|
||||||
|
|
||||||
|
start..end
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Select a word in the given text at the specified offset.
|
||||||
|
///
|
||||||
|
/// The offset is the UTF-8 offset.
|
||||||
|
///
|
||||||
|
/// Returns the start and end offsets of the selected word.
|
||||||
|
pub fn word_range(text: &Rope, offset: usize) -> Option<Range<usize>> {
|
||||||
|
let offset = text.clip_offset(offset, Bias::Left);
|
||||||
|
let char = text.char_at(offset)?;
|
||||||
|
let end = offset + char.len_utf8();
|
||||||
|
let prev_chars = text.chars_at(offset).reversed().take(128);
|
||||||
|
let next_chars = text.chars_at(end).take(128);
|
||||||
|
|
||||||
|
Some(word_range_from_chars(offset, char, prev_chars, next_chars))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub(crate) enum CharType {
|
||||||
|
/// a-z, A-Z, 0-9, _
|
||||||
|
Word,
|
||||||
|
/// '\t', ' ', '\u{00A0}' etc.
|
||||||
|
Whitespace,
|
||||||
|
/// \n, \r
|
||||||
|
Newline,
|
||||||
|
/// . , ; : ( ) [ ] { } ... or CJK characters: `汉`, `🎉` etc.
|
||||||
|
Other,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<char> for CharType {
|
||||||
|
fn from(c: char) -> Self {
|
||||||
|
match c {
|
||||||
|
c if is_word_char(c) => CharType::Word,
|
||||||
|
c if c == '\n' || c == '\r' => CharType::Newline,
|
||||||
|
c if c.is_whitespace() => CharType::Whitespace,
|
||||||
|
_ => CharType::Other,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CharType {
|
||||||
|
fn is_connectable(self, c: char) -> bool {
|
||||||
|
matches!(
|
||||||
|
(self, CharType::from(c)),
|
||||||
|
(CharType::Word, CharType::Word) | (CharType::Whitespace, CharType::Whitespace)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_word_char(c: char) -> bool {
|
||||||
|
matches!(c, '_')
|
||||||
|
// ASCII alphanumeric characters, for English, numbers: `Hello123`, etc.
|
||||||
|
|| c.is_ascii_alphanumeric()
|
||||||
|
// Latin script in Unicode for French, German, Spanish, etc.
|
||||||
|
|| matches!(c, '\u{00C0}'..='\u{00FF}')
|
||||||
|
|| matches!(c, '\u{0100}'..='\u{017F}')
|
||||||
|
|| matches!(c, '\u{0180}'..='\u{024F}')
|
||||||
|
// Cyrillic for Russian, Ukrainian, etc.
|
||||||
|
|| matches!(c, '\u{0400}'..='\u{04FF}')
|
||||||
|
// Vietnamese
|
||||||
|
|| matches!(c, '\u{1E00}'..='\u{1EFF}')
|
||||||
|
|| matches!(c, '\u{0300}'..='\u{036F}')
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn word_range_from_chars(
|
||||||
|
offset: usize,
|
||||||
|
c: char,
|
||||||
|
prev_chars: impl Iterator<Item = char>,
|
||||||
|
next_chars: impl Iterator<Item = char>,
|
||||||
|
) -> Range<usize> {
|
||||||
|
let char_type = CharType::from(c);
|
||||||
|
let mut start = offset;
|
||||||
|
let mut end = offset + c.len_utf8();
|
||||||
|
|
||||||
|
for prev in prev_chars.take(128) {
|
||||||
|
if char_type.is_connectable(prev) {
|
||||||
|
start -= prev.len_utf8();
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for next in next_chars.take(128) {
|
||||||
|
if char_type.is_connectable(next) {
|
||||||
|
end += next.len_utf8();
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
start..end
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,227 +0,0 @@
|
|||||||
use std::ops::Range;
|
|
||||||
|
|
||||||
use gpui::{App, Font, LineFragment, Pixels};
|
|
||||||
use rope::Rope;
|
|
||||||
|
|
||||||
use super::rope_ext::RopeExt;
|
|
||||||
|
|
||||||
/// A line with soft wrapped lines info.
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub(super) struct LineItem {
|
|
||||||
/// The original line text.
|
|
||||||
line: Rope,
|
|
||||||
/// The soft wrapped lines relative byte range (0..line.len) of this line (Include first line).
|
|
||||||
///
|
|
||||||
/// FIXME: Here in somecase, the `line_wrapper.wrap_line` has returned different
|
|
||||||
/// like the `window.text_system().shape_text`. So, this value may not equal
|
|
||||||
/// the actual rendered lines.
|
|
||||||
wrapped_lines: Vec<Range<usize>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LineItem {
|
|
||||||
/// Get the bytes length of this line.
|
|
||||||
#[inline]
|
|
||||||
pub(super) fn len(&self) -> usize {
|
|
||||||
self.line.len()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get number of soft wrapped lines of this line (include the first line).
|
|
||||||
#[inline]
|
|
||||||
pub(super) fn lines_len(&self) -> usize {
|
|
||||||
self.wrapped_lines.len()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the height of this line item with given line height.
|
|
||||||
pub(super) fn height(&self, line_height: Pixels) -> Pixels {
|
|
||||||
self.lines_len() as f32 * line_height
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Used to prepare the text with soft wrap to be get lines to displayed in the Editor.
|
|
||||||
///
|
|
||||||
/// After use lines to calculate the scroll size of the Editor.
|
|
||||||
pub(super) struct TextWrapper {
|
|
||||||
text: Rope,
|
|
||||||
|
|
||||||
/// Total wrapped lines (Inlucde the first line), value is start and end index of the line.
|
|
||||||
soft_lines: usize,
|
|
||||||
font: Font,
|
|
||||||
font_size: Pixels,
|
|
||||||
|
|
||||||
/// If is none, it means the text is not wrapped
|
|
||||||
wrap_width: Option<Pixels>,
|
|
||||||
|
|
||||||
/// The lines by split \n
|
|
||||||
pub(super) lines: Vec<LineItem>,
|
|
||||||
|
|
||||||
_initialized: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(unused)]
|
|
||||||
impl TextWrapper {
|
|
||||||
pub(super) fn new(font: Font, font_size: Pixels, wrap_width: Option<Pixels>) -> Self {
|
|
||||||
Self {
|
|
||||||
text: Rope::new(),
|
|
||||||
font,
|
|
||||||
font_size,
|
|
||||||
wrap_width,
|
|
||||||
soft_lines: 0,
|
|
||||||
lines: Vec::new(),
|
|
||||||
_initialized: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub(super) fn set_default_text(&mut self, text: &Rope) {
|
|
||||||
self.text = text.clone();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the total number of lines including wrapped lines.
|
|
||||||
#[inline]
|
|
||||||
pub(super) fn len(&self) -> usize {
|
|
||||||
self.soft_lines
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the line item by row index.
|
|
||||||
#[inline]
|
|
||||||
pub(super) fn line(&self, row: usize) -> Option<&LineItem> {
|
|
||||||
self.lines.get(row)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn set_wrap_width(&mut self, wrap_width: Option<Pixels>, cx: &mut App) {
|
|
||||||
if wrap_width == self.wrap_width {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
self.wrap_width = wrap_width;
|
|
||||||
self.update_all(&self.text.clone(), true, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn set_font(&mut self, font: Font, font_size: Pixels, cx: &mut App) {
|
|
||||||
if self.font.eq(&font) && self.font_size == font_size {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
self.font = font;
|
|
||||||
self.font_size = font_size;
|
|
||||||
self.update_all(&self.text.clone(), true, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn prepare_if_need(&mut self, text: &Rope, cx: &mut App) {
|
|
||||||
if self._initialized {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
self._initialized = true;
|
|
||||||
self.update_all(text, true, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update the text wrapper and recalculate the wrapped lines.
|
|
||||||
///
|
|
||||||
/// If the `text` is the same as the current text, do nothing.
|
|
||||||
///
|
|
||||||
/// - `changed_text`: The text [`Rope`] that has changed.
|
|
||||||
/// - `range`: The `selected_range` before change.
|
|
||||||
/// - `new_text`: The inserted text.
|
|
||||||
/// - `force`: Whether to force the update, if false, the update will be skipped if the text is the same.
|
|
||||||
/// - `cx`: The application context.
|
|
||||||
pub(super) fn update(
|
|
||||||
&mut self,
|
|
||||||
changed_text: &Rope,
|
|
||||||
range: &Range<usize>,
|
|
||||||
new_text: &Rope,
|
|
||||||
force: bool,
|
|
||||||
cx: &mut App,
|
|
||||||
) {
|
|
||||||
let mut line_wrapper = cx
|
|
||||||
.text_system()
|
|
||||||
.line_wrapper(self.font.clone(), self.font_size);
|
|
||||||
self._update(
|
|
||||||
changed_text,
|
|
||||||
range,
|
|
||||||
new_text,
|
|
||||||
force,
|
|
||||||
&mut |line_str, wrap_width| {
|
|
||||||
line_wrapper
|
|
||||||
.wrap_line(&[LineFragment::text(line_str)], wrap_width)
|
|
||||||
.collect()
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn _update<F>(
|
|
||||||
&mut self,
|
|
||||||
changed_text: &Rope,
|
|
||||||
range: &Range<usize>,
|
|
||||||
new_text: &Rope,
|
|
||||||
force: bool,
|
|
||||||
wrap_line: &mut F,
|
|
||||||
) where
|
|
||||||
F: FnMut(&str, Pixels) -> Vec<gpui::Boundary>,
|
|
||||||
{
|
|
||||||
if self.text.eq(changed_text) && !force {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove the old changed lines.
|
|
||||||
let start_row = self.text.offset_to_point(range.start).row as usize;
|
|
||||||
let start_row = start_row.min(self.lines.len().saturating_sub(1));
|
|
||||||
let end_row = self.text.offset_to_point(range.end).row as usize;
|
|
||||||
let end_row = end_row.min(self.lines.len().saturating_sub(1));
|
|
||||||
let rows_range = start_row..=end_row;
|
|
||||||
|
|
||||||
// To add the new lines.
|
|
||||||
let new_start_row = changed_text.offset_to_point(range.start).row as usize;
|
|
||||||
let new_start_offset = changed_text.line_start_offset(new_start_row);
|
|
||||||
let new_end_row = changed_text
|
|
||||||
.offset_to_point(range.start + new_text.len())
|
|
||||||
.row as usize;
|
|
||||||
let new_end_offset = changed_text.line_end_offset(new_end_row);
|
|
||||||
let new_range = new_start_offset..new_end_offset;
|
|
||||||
|
|
||||||
let mut new_lines = vec![];
|
|
||||||
|
|
||||||
let wrap_width = self.wrap_width;
|
|
||||||
|
|
||||||
for line in changed_text.slice(new_range).lines() {
|
|
||||||
let line_str = line.to_string();
|
|
||||||
let mut wrapped_lines = vec![];
|
|
||||||
let mut prev_boundary_ix = 0;
|
|
||||||
|
|
||||||
// If wrap_width is Pixels::MAX, skip wrapping to disable word wrap
|
|
||||||
if let Some(wrap_width) = wrap_width {
|
|
||||||
// Here only have wrapped line, if there is no wrap meet, the `line_wraps` result will empty.
|
|
||||||
for boundary in wrap_line(&line_str, wrap_width) {
|
|
||||||
wrapped_lines.push(prev_boundary_ix..boundary.ix);
|
|
||||||
prev_boundary_ix = boundary.ix;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset of the line
|
|
||||||
if !line_str[prev_boundary_ix..].is_empty() || prev_boundary_ix == 0 {
|
|
||||||
wrapped_lines.push(prev_boundary_ix..line.len());
|
|
||||||
}
|
|
||||||
|
|
||||||
new_lines.push(LineItem {
|
|
||||||
line: line.clone(),
|
|
||||||
wrapped_lines,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// dbg!(&new_lines.len());
|
|
||||||
// dbg!(self.lines.len());
|
|
||||||
if self.lines.is_empty() {
|
|
||||||
self.lines = new_lines;
|
|
||||||
} else {
|
|
||||||
self.lines.splice(rows_range, new_lines);
|
|
||||||
}
|
|
||||||
|
|
||||||
// dbg!(self.lines.len());
|
|
||||||
self.text = changed_text.clone();
|
|
||||||
self.soft_lines = self.lines.iter().map(|l| l.lines_len()).sum();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update the text wrapper and recalculate the wrapped lines.
|
|
||||||
///
|
|
||||||
/// If the `text` is the same as the current text, do nothing.
|
|
||||||
pub(crate) fn update_all(&mut self, text: &Rope, force: bool, cx: &mut App) {
|
|
||||||
self.update(text, &(0..text.len()), text, force, cx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -13,7 +13,7 @@ use smol::Timer;
|
|||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
|
|
||||||
use crate::actions::{Cancel, Confirm, SelectDown, SelectUp};
|
use crate::actions::{Cancel, Confirm, SelectDown, SelectUp};
|
||||||
use crate::input::{InputEvent, InputState, TextInput};
|
use crate::input::{Input, InputEvent, InputState};
|
||||||
use crate::list::ListDelegate;
|
use crate::list::ListDelegate;
|
||||||
use crate::list::cache::{MeasuredEntrySize, RowEntry, RowsCache};
|
use crate::list::cache::{MeasuredEntrySize, RowEntry, RowsCache};
|
||||||
use crate::scroll::{Scrollbar, ScrollbarHandle};
|
use crate::scroll::{Scrollbar, ScrollbarHandle};
|
||||||
@@ -288,7 +288,7 @@ where
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
InputEvent::PressEnter { secondary } => self.on_action_confirm(
|
InputEvent::PressEnter { secondary, .. } => self.on_action_confirm(
|
||||||
&Confirm {
|
&Confirm {
|
||||||
secondary: *secondary,
|
secondary: *secondary,
|
||||||
},
|
},
|
||||||
@@ -498,7 +498,7 @@ where
|
|||||||
let scroll_handle = self.scroll_handle.clone();
|
let scroll_handle = self.scroll_handle.clone();
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.flex_grow()
|
.flex_grow_1()
|
||||||
.relative()
|
.relative()
|
||||||
.size_full()
|
.size_full()
|
||||||
.when_some(self.options.max_height, |this, h| this.max_h(h))
|
.when_some(self.options.max_height, |this, h| this.max_h(h))
|
||||||
@@ -632,10 +632,10 @@ where
|
|||||||
.border_b_1()
|
.border_b_1()
|
||||||
.border_color(cx.theme().border)
|
.border_color(cx.theme().border)
|
||||||
.child(
|
.child(
|
||||||
TextInput::new(&input)
|
Input::new(&input)
|
||||||
.with_size(self.options.size)
|
.with_size(self.options.size)
|
||||||
.appearance(false)
|
.appearance(false)
|
||||||
.cleanable()
|
.cleanable(true)
|
||||||
.p_0()
|
.p_0()
|
||||||
.prefix(
|
.prefix(
|
||||||
Icon::new(IconName::Search).text_color(cx.theme().text_muted),
|
Icon::new(IconName::Search).text_color(cx.theme().text_muted),
|
||||||
|
|||||||
@@ -521,12 +521,14 @@ impl RenderOnce for Modal {
|
|||||||
offset: point(px(0.), px(20.)),
|
offset: point(px(0.), px(20.)),
|
||||||
blur_radius: px(25.),
|
blur_radius: px(25.),
|
||||||
spread_radius: px(-5.),
|
spread_radius: px(-5.),
|
||||||
|
inset: false,
|
||||||
},
|
},
|
||||||
BoxShadow {
|
BoxShadow {
|
||||||
color: hsla(0., 0., 0., 0.1 * delta),
|
color: hsla(0., 0., 0., 0.1 * delta),
|
||||||
offset: point(px(0.), px(8.)),
|
offset: point(px(0.), px(8.)),
|
||||||
blur_radius: px(10.),
|
blur_radius: px(10.),
|
||||||
spread_radius: px(-6.),
|
spread_radius: px(-6.),
|
||||||
|
inset: false,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
this.top(y + y_offset).shadow(shadow)
|
this.top(y + y_offset).shadow(shadow)
|
||||||
|
|||||||
@@ -253,7 +253,7 @@ impl RenderOnce for ResizablePanel {
|
|||||||
div()
|
div()
|
||||||
.id(("resizable-panel", self.panel_ix))
|
.id(("resizable-panel", self.panel_ix))
|
||||||
.flex()
|
.flex()
|
||||||
.flex_grow()
|
.flex_grow_1()
|
||||||
.size_full()
|
.size_full()
|
||||||
.relative()
|
.relative()
|
||||||
.when(self.axis.is_vertical(), |this| {
|
.when(self.axis.is_vertical(), |this| {
|
||||||
@@ -265,7 +265,7 @@ impl RenderOnce for ResizablePanel {
|
|||||||
// 1. initial_size is None, to use auto size.
|
// 1. initial_size is None, to use auto size.
|
||||||
// 2. initial_size is Some and size is none, to use the initial size of the panel for first time render.
|
// 2. initial_size is Some and size is none, to use the initial size of the panel for first time render.
|
||||||
// 3. initial_size is Some and size is Some, use `size`.
|
// 3. initial_size is Some and size is Some, use `size`.
|
||||||
.when(self.initial_size.is_none(), |this| this.flex_shrink())
|
.when(self.initial_size.is_none(), |this| this.flex_shrink_1())
|
||||||
.when_some(self.initial_size, |this, initial_size| {
|
.when_some(self.initial_size, |this, initial_size| {
|
||||||
// The `self.size` is None, that mean the initial size for the panel,
|
// The `self.size` is None, that mean the initial size for the panel,
|
||||||
// so we need set `flex_shrink_0` To let it keep the initial size.
|
// so we need set `flex_shrink_0` To let it keep the initial size.
|
||||||
|
|||||||
@@ -385,6 +385,7 @@ impl Render for Root {
|
|||||||
blur_radius: CLIENT_SIDE_DECORATION_SHADOW / 2.,
|
blur_radius: CLIENT_SIDE_DECORATION_SHADOW / 2.,
|
||||||
spread_radius: px(0.),
|
spread_radius: px(0.),
|
||||||
offset: point(px(0.0), px(0.0)),
|
offset: point(px(0.0), px(0.0)),
|
||||||
|
inset: false,
|
||||||
}])
|
}])
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ product-name = "Coop"
|
|||||||
description = "Chat Freely, Stay Private on Nostr"
|
description = "Chat Freely, Stay Private on Nostr"
|
||||||
identifier = "su.reya.coop"
|
identifier = "su.reya.coop"
|
||||||
category = "SocialNetworking"
|
category = "SocialNetworking"
|
||||||
version = "1.0.0-beta4"
|
version = "1.0.0-beta5"
|
||||||
out-dir = "../dist"
|
out-dir = "../dist"
|
||||||
before-packaging-command = "cargo build --release"
|
before-packaging-command = "cargo build --release"
|
||||||
resources = ["Cargo.toml", "src"]
|
resources = ["Cargo.toml", "src"]
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ use smallvec::{SmallVec, smallvec};
|
|||||||
use state::{CoopAuthUrlHandler, NostrRegistry, StateEvent};
|
use state::{CoopAuthUrlHandler, NostrRegistry, StateEvent};
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
use ui::input::{InputEvent, InputState, TextInput};
|
use ui::input::{Input, InputEvent, InputState};
|
||||||
use ui::{Disableable, v_flex};
|
use ui::{Disableable, v_flex};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -250,7 +250,7 @@ impl Render for ImportKey {
|
|||||||
.text_sm()
|
.text_sm()
|
||||||
.text_color(cx.theme().text_muted)
|
.text_color(cx.theme().text_muted)
|
||||||
.child("nsec or bunker://")
|
.child("nsec or bunker://")
|
||||||
.child(TextInput::new(&self.key_input)),
|
.child(Input::new(&self.key_input)),
|
||||||
)
|
)
|
||||||
.when(
|
.when(
|
||||||
self.key_input.read(cx).value().starts_with("ncryptsec1"),
|
self.key_input.read(cx).value().starts_with("ncryptsec1"),
|
||||||
@@ -261,7 +261,7 @@ impl Render for ImportKey {
|
|||||||
.text_sm()
|
.text_sm()
|
||||||
.text_color(cx.theme().text_muted)
|
.text_color(cx.theme().text_muted)
|
||||||
.child("Password:")
|
.child("Password:")
|
||||||
.child(TextInput::new(&self.pass_input)),
|
.child(Input::new(&self.pass_input)),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ use gpui::{
|
|||||||
use nostr_connect::prelude::*;
|
use nostr_connect::prelude::*;
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
use ui::input::{InputEvent, InputState, TextInput};
|
use ui::input::{Input, InputEvent, InputState};
|
||||||
use ui::{WindowExtension, v_flex};
|
use ui::{WindowExtension, v_flex};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -107,7 +107,7 @@ impl Render for RestoreEncryption {
|
|||||||
.text_sm()
|
.text_sm()
|
||||||
.text_color(cx.theme().text_muted)
|
.text_color(cx.theme().text_muted)
|
||||||
.child("Secret Key")
|
.child("Secret Key")
|
||||||
.child(TextInput::new(&self.key_input)),
|
.child(Input::new(&self.key_input)),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
Button::new("restore")
|
Button::new("restore")
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use settings::{AppSettings, AuthMode};
|
|||||||
use theme::{ActiveTheme, Theme, ThemeMode};
|
use theme::{ActiveTheme, Theme, ThemeMode};
|
||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
use ui::group_box::{GroupBox, GroupBoxVariants};
|
use ui::group_box::{GroupBox, GroupBoxVariants};
|
||||||
use ui::input::{InputState, TextInput};
|
use ui::input::{Input, InputState};
|
||||||
use ui::menu::{DropdownMenu, PopupMenuItem};
|
use ui::menu::{DropdownMenu, PopupMenuItem};
|
||||||
use ui::notification::Notification;
|
use ui::notification::Notification;
|
||||||
use ui::switch::Switch;
|
use ui::switch::Switch;
|
||||||
@@ -218,7 +218,7 @@ impl Render for Preferences {
|
|||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.child(TextInput::new(&self.file_input).text_xs().small())
|
.child(Input::new(&self.file_input).text_xs().small())
|
||||||
.child(
|
.child(
|
||||||
Button::new("update-file-server")
|
Button::new("update-file-server")
|
||||||
.icon(IconName::Check)
|
.icon(IconName::Check)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ use state::KEYRING;
|
|||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
use ui::dock::{Panel, PanelEvent};
|
use ui::dock::{Panel, PanelEvent};
|
||||||
use ui::input::{InputState, TextInput};
|
use ui::input::{Input, InputState};
|
||||||
use ui::{IconName, Sizable, StyledExt, divider, v_flex};
|
use ui::{IconName, Sizable, StyledExt, divider, v_flex};
|
||||||
|
|
||||||
const MSG: &str = "Store your account keys in a safe location. \
|
const MSG: &str = "Store your account keys in a safe location. \
|
||||||
@@ -40,8 +40,8 @@ pub struct BackupPanel {
|
|||||||
|
|
||||||
impl BackupPanel {
|
impl BackupPanel {
|
||||||
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
let npub_input = cx.new(|cx| InputState::new(window, cx).disabled(true));
|
let npub_input = cx.new(|cx| InputState::new(window, cx));
|
||||||
let nsec_input = cx.new(|cx| InputState::new(window, cx).disabled(true).masked(true));
|
let nsec_input = cx.new(|cx| InputState::new(window, cx).masked(true));
|
||||||
|
|
||||||
// Run at the end of current cycle
|
// Run at the end of current cycle
|
||||||
cx.defer_in(window, |this, window, cx| {
|
cx.defer_in(window, |this, window, cx| {
|
||||||
@@ -156,7 +156,7 @@ impl Render for BackupPanel {
|
|||||||
.child(SharedString::from("Public Key:")),
|
.child(SharedString::from("Public Key:")),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
TextInput::new(&self.npub_input)
|
Input::new(&self.npub_input)
|
||||||
.small()
|
.small()
|
||||||
.bordered(false)
|
.bordered(false)
|
||||||
.disabled(true),
|
.disabled(true),
|
||||||
@@ -174,7 +174,7 @@ impl Render for BackupPanel {
|
|||||||
.child(SharedString::from("Secret Key:")),
|
.child(SharedString::from("Secret Key:")),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
TextInput::new(&self.nsec_input)
|
Input::new(&self.nsec_input)
|
||||||
.small()
|
.small()
|
||||||
.bordered(false)
|
.bordered(false)
|
||||||
.disabled(true),
|
.disabled(true),
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ use theme::ActiveTheme;
|
|||||||
use ui::avatar::Avatar;
|
use ui::avatar::Avatar;
|
||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
use ui::dock::{Panel, PanelEvent};
|
use ui::dock::{Panel, PanelEvent};
|
||||||
use ui::input::{InputEvent, InputState, TextInput};
|
use ui::input::{Input, InputEvent, InputState};
|
||||||
use ui::{Disableable, IconName, Sizable, StyledExt, WindowExtension, h_flex, v_flex};
|
use ui::{Disableable, IconName, Sizable, StyledExt, WindowExtension, h_flex, v_flex};
|
||||||
|
|
||||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<ContactListPanel> {
|
pub fn init(window: &mut Window, cx: &mut App) -> Entity<ContactListPanel> {
|
||||||
@@ -301,10 +301,10 @@ impl Render for ContactListPanel {
|
|||||||
.gap_1()
|
.gap_1()
|
||||||
.w_full()
|
.w_full()
|
||||||
.child(
|
.child(
|
||||||
TextInput::new(&self.input)
|
Input::new(&self.input)
|
||||||
.small()
|
.small()
|
||||||
.bordered(false)
|
.bordered(false)
|
||||||
.cleanable(),
|
.cleanable(true),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
Button::new("add")
|
Button::new("add")
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ use state::NostrRegistry;
|
|||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
use ui::dock::{Panel, PanelEvent};
|
use ui::dock::{Panel, PanelEvent};
|
||||||
use ui::input::{InputEvent, InputState, TextInput};
|
use ui::input::{Input, InputEvent, InputState};
|
||||||
use ui::{Disableable, IconName, Sizable, StyledExt, WindowExtension, divider, h_flex, v_flex};
|
use ui::{Disableable, IconName, Sizable, StyledExt, WindowExtension, divider, h_flex, v_flex};
|
||||||
|
|
||||||
const MSG: &str = "Messaging Relays are relays that hosted all your messages. \
|
const MSG: &str = "Messaging Relays are relays that hosted all your messages. \
|
||||||
@@ -317,10 +317,10 @@ impl Render for MessagingRelayPanel {
|
|||||||
.gap_1()
|
.gap_1()
|
||||||
.w_full()
|
.w_full()
|
||||||
.child(
|
.child(
|
||||||
TextInput::new(&self.input)
|
Input::new(&self.input)
|
||||||
.small()
|
.small()
|
||||||
.bordered(false)
|
.bordered(false)
|
||||||
.cleanable(),
|
.cleanable(true),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
Button::new("add")
|
Button::new("add")
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ use theme::ActiveTheme;
|
|||||||
use ui::avatar::Avatar;
|
use ui::avatar::Avatar;
|
||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
use ui::dock::{Panel, PanelEvent};
|
use ui::dock::{Panel, PanelEvent};
|
||||||
use ui::input::{InputState, TextInput};
|
use ui::input::{Input, InputState};
|
||||||
use ui::notification::Notification;
|
use ui::notification::Notification;
|
||||||
use ui::{Disableable, IconName, Sizable, StyledExt, WindowExtension, h_flex, v_flex};
|
use ui::{Disableable, IconName, Sizable, StyledExt, WindowExtension, h_flex, v_flex};
|
||||||
|
|
||||||
@@ -65,7 +65,7 @@ impl ProfilePanel {
|
|||||||
// Use multi-line input for bio
|
// Use multi-line input for bio
|
||||||
let bio_input = cx.new(|cx| {
|
let bio_input = cx.new(|cx| {
|
||||||
InputState::new(window, cx)
|
InputState::new(window, cx)
|
||||||
.multi_line()
|
.multi_line(true)
|
||||||
.auto_grow(3, 8)
|
.auto_grow(3, 8)
|
||||||
.placeholder("A short introduce about you.")
|
.placeholder("A short introduce about you.")
|
||||||
});
|
});
|
||||||
@@ -352,7 +352,7 @@ impl Render for ProfilePanel {
|
|||||||
.text_color(cx.theme().text_muted)
|
.text_color(cx.theme().text_muted)
|
||||||
.child(SharedString::from("What should people call you?")),
|
.child(SharedString::from("What should people call you?")),
|
||||||
)
|
)
|
||||||
.child(TextInput::new(&self.name_input).bordered(false).small()),
|
.child(Input::new(&self.name_input).bordered(false).small()),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
@@ -363,7 +363,7 @@ impl Render for ProfilePanel {
|
|||||||
.text_color(cx.theme().text_muted)
|
.text_color(cx.theme().text_muted)
|
||||||
.child(SharedString::from("A short introduction about you:")),
|
.child(SharedString::from("A short introduction about you:")),
|
||||||
)
|
)
|
||||||
.child(TextInput::new(&self.bio_input).bordered(false).small()),
|
.child(Input::new(&self.bio_input).bordered(false).small()),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
@@ -374,7 +374,7 @@ impl Render for ProfilePanel {
|
|||||||
.text_color(cx.theme().text_muted)
|
.text_color(cx.theme().text_muted)
|
||||||
.child(SharedString::from("Website:")),
|
.child(SharedString::from("Website:")),
|
||||||
)
|
)
|
||||||
.child(TextInput::new(&self.website_input).bordered(false).small()),
|
.child(Input::new(&self.website_input).bordered(false).small()),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ use state::NostrRegistry;
|
|||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
use ui::dock::{Panel, PanelEvent};
|
use ui::dock::{Panel, PanelEvent};
|
||||||
use ui::input::{InputEvent, InputState, TextInput};
|
use ui::input::{Input, InputEvent, InputState};
|
||||||
use ui::menu::DropdownMenu;
|
use ui::menu::DropdownMenu;
|
||||||
use ui::{Disableable, IconName, Sizable, StyledExt, WindowExtension, divider, h_flex, v_flex};
|
use ui::{Disableable, IconName, Sizable, StyledExt, WindowExtension, divider, h_flex, v_flex};
|
||||||
|
|
||||||
@@ -369,10 +369,10 @@ impl Render for RelayListPanel {
|
|||||||
.gap_1()
|
.gap_1()
|
||||||
.w_full()
|
.w_full()
|
||||||
.child(
|
.child(
|
||||||
TextInput::new(&self.input)
|
Input::new(&self.input)
|
||||||
.small()
|
.small()
|
||||||
.bordered(false)
|
.bordered(false)
|
||||||
.cleanable(),
|
.cleanable(true),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
Button::new("metadata")
|
Button::new("metadata")
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ use theme::{ActiveTheme, SIDEBAR_WIDTH, TABBAR_HEIGHT};
|
|||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
use ui::dock::{Panel, PanelEvent};
|
use ui::dock::{Panel, PanelEvent};
|
||||||
use ui::indicator::Indicator;
|
use ui::indicator::Indicator;
|
||||||
use ui::input::{InputEvent, InputState, TextInput};
|
use ui::input::{Input, InputEvent, InputState};
|
||||||
use ui::notification::Notification;
|
use ui::notification::Notification;
|
||||||
use ui::scroll::Scrollbar;
|
use ui::scroll::Scrollbar;
|
||||||
use ui::{Icon, IconName, Selectable, Sizable, StyledExt, WindowExtension, h_flex, v_flex};
|
use ui::{Icon, IconName, Selectable, Sizable, StyledExt, WindowExtension, h_flex, v_flex};
|
||||||
@@ -252,7 +252,6 @@ impl Sidebar {
|
|||||||
fn set_finding(&mut self, status: bool, _window: &mut Window, cx: &mut Context<Self>) {
|
fn set_finding(&mut self, status: bool, _window: &mut Window, cx: &mut Context<Self>) {
|
||||||
// Disable the input to prevent duplicate requests
|
// Disable the input to prevent duplicate requests
|
||||||
self.find_input.update(cx, |this, cx| {
|
self.find_input.update(cx, |this, cx| {
|
||||||
this.set_disabled(status, cx);
|
|
||||||
this.set_loading(status, cx);
|
this.set_loading(status, cx);
|
||||||
});
|
});
|
||||||
// Set the search status
|
// Set the search status
|
||||||
@@ -513,7 +512,7 @@ impl Render for Sidebar {
|
|||||||
.border_color(cx.theme().border)
|
.border_color(cx.theme().border)
|
||||||
.bg(cx.theme().tab_background)
|
.bg(cx.theme().tab_background)
|
||||||
.child(
|
.child(
|
||||||
TextInput::new(&self.find_input)
|
Input::new(&self.find_input)
|
||||||
.appearance(false)
|
.appearance(false)
|
||||||
.bordered(false)
|
.bordered(false)
|
||||||
.small()
|
.small()
|
||||||
|
|||||||
Reference in New Issue
Block a user