feat: add dummy search bar in diff view
This commit is contained in:
@@ -25,6 +25,7 @@ memchr = "2.8.0"
|
|||||||
thiserror = "2.0.18"
|
thiserror = "2.0.18"
|
||||||
tree-sitter-highlight = "0.26.9"
|
tree-sitter-highlight = "0.26.9"
|
||||||
tree-sitter-rust = "0.24.2"
|
tree-sitter-rust = "0.24.2"
|
||||||
|
unicode-segmentation = "1.13.2"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
serde_json = "1.0.149"
|
serde_json = "1.0.149"
|
||||||
|
|||||||
@@ -318,8 +318,8 @@ impl query::QueryFn for ListPullRequests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let query_string = match self.filter {
|
let query_string = match self.filter {
|
||||||
| Some(filter) => format!("is:pr archived:false sort:updated-desc {}", filter),
|
| Some(filter) => format!("is:pr archived:false sort:updated-desc {}", filter),
|
||||||
| None => "is:pr archived:false sort:updated-desc".into(),
|
| None => "is:pr archived:false sort:updated-desc".into(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let gql =
|
let gql =
|
||||||
@@ -341,20 +341,20 @@ impl query::QueryFn for ListPullRequests {
|
|||||||
.flatten()
|
.flatten()
|
||||||
.filter_map(|edge| {
|
.filter_map(|edge| {
|
||||||
edge.node.and_then(|n| match n {
|
edge.node.and_then(|n| match n {
|
||||||
| PullRequestPaginationQuerySearchEdgesNode::PullRequest(p) => {
|
| PullRequestPaginationQuerySearchEdgesNode::PullRequest(p) => {
|
||||||
Some(PullRequest {
|
Some(PullRequest {
|
||||||
id: p.id.into(),
|
id: p.id.into(),
|
||||||
title: p.title.into(),
|
title: p.title.into(),
|
||||||
state: p.state,
|
state: p.state,
|
||||||
is_draft: p.is_draft,
|
is_draft: p.is_draft,
|
||||||
repo_slug: format!(
|
repo_slug: format!(
|
||||||
"{}/{}",
|
"{}/{}",
|
||||||
p.repository.owner.login, p.repository.name
|
p.repository.owner.login, p.repository.name
|
||||||
)
|
)
|
||||||
.into(),
|
.into(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
| _ => None,
|
| _ => None,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
@@ -399,43 +399,43 @@ impl query::QueryFn for FetchPullRequest {
|
|||||||
"missing 'node' field on PullRequestQuery response".into(),
|
"missing 'node' field on PullRequestQuery response".into(),
|
||||||
))
|
))
|
||||||
.and_then(|n| match n {
|
.and_then(|n| match n {
|
||||||
| PullRequestQueryNode::PullRequest(p) => {
|
| PullRequestQueryNode::PullRequest(p) => {
|
||||||
let created_at =
|
let created_at =
|
||||||
chrono::DateTime::parse_from_rfc3339(&p.created_at).map_err(|err| {
|
chrono::DateTime::parse_from_rfc3339(&p.created_at).map_err(|err| {
|
||||||
api::Error::MalformedResponse(format!(
|
api::Error::MalformedResponse(format!(
|
||||||
"invalid pull request createdAt {:?}: {err}",
|
"invalid pull request createdAt {:?}: {err}",
|
||||||
p.created_at
|
p.created_at
|
||||||
))
|
))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(DetailedPullRequest {
|
Ok(DetailedPullRequest {
|
||||||
id: Id(p.id.into()),
|
id: Id(p.id.into()),
|
||||||
title: p.title.into(),
|
title: p.title.into(),
|
||||||
state: p.state,
|
state: p.state,
|
||||||
is_draft: p.is_draft,
|
is_draft: p.is_draft,
|
||||||
body: p.body.into(),
|
body: p.body.into(),
|
||||||
author: p.author.map(|it| api::user::Actor {
|
author: p.author.map(|it| api::user::Actor {
|
||||||
login: it.login.into(),
|
login: it.login.into(),
|
||||||
avatar_url: it.avatar_url.into(),
|
avatar_url: it.avatar_url.into(),
|
||||||
}),
|
}),
|
||||||
base_repo_slug: p
|
base_repo_slug: p
|
||||||
.base_repository
|
.base_repository
|
||||||
.map(|it| it.name_with_owner.into())
|
.map(|it| it.name_with_owner.into())
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
base_branch_name: p.base_ref_name.into(),
|
base_branch_name: p.base_ref_name.into(),
|
||||||
base_ref: p.base_ref_oid.into(),
|
base_ref: p.base_ref_oid.into(),
|
||||||
head_repo_slug: p
|
head_repo_slug: p
|
||||||
.head_repository
|
.head_repository
|
||||||
.map(|it| it.name_with_owner.into())
|
.map(|it| it.name_with_owner.into())
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
head_branch_name: p.head_ref_name.into(),
|
head_branch_name: p.head_ref_name.into(),
|
||||||
head_ref: p.head_ref_oid.into(),
|
head_ref: p.head_ref_oid.into(),
|
||||||
created_at: Some(created_at),
|
created_at: Some(created_at),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
| _ => Err(api::Error::MalformedResponse(
|
| _ => Err(api::Error::MalformedResponse(
|
||||||
"unexpected node type on PullRequestQuery".into(),
|
"unexpected node type on PullRequestQuery".into(),
|
||||||
)),
|
)),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -494,12 +494,12 @@ impl query::QueryFn for FetchPullRequestFileTree {
|
|||||||
"missing 'node' field on PullRequestFileTreeQuery response".into(),
|
"missing 'node' field on PullRequestFileTreeQuery response".into(),
|
||||||
))
|
))
|
||||||
.and_then(|node| match node {
|
.and_then(|node| match node {
|
||||||
| pull_request_file_tree_query::PullRequestFileTreeQueryNode::PullRequest(
|
| pull_request_file_tree_query::PullRequestFileTreeQueryNode::PullRequest(
|
||||||
pull_request,
|
pull_request,
|
||||||
) => Ok(pull_request),
|
) => Ok(pull_request),
|
||||||
| _ => Err(api::Error::MalformedResponse(
|
| _ => Err(api::Error::MalformedResponse(
|
||||||
"unexpected node type on PullRequestFileTreeQuery".into(),
|
"unexpected node type on PullRequestFileTreeQuery".into(),
|
||||||
)),
|
)),
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(pull_request
|
Ok(pull_request
|
||||||
@@ -513,23 +513,25 @@ impl query::QueryFn for FetchPullRequestFileTree {
|
|||||||
edge.node.map(|node| ChangedFile {
|
edge.node.map(|node| ChangedFile {
|
||||||
cursor,
|
cursor,
|
||||||
change_type: match node.change_type {
|
change_type: match node.change_type {
|
||||||
| pull_request_file_tree_query::PatchStatus::ADDED => ChangeType::Added,
|
| pull_request_file_tree_query::PatchStatus::ADDED => {
|
||||||
| pull_request_file_tree_query::PatchStatus::MODIFIED => {
|
ChangeType::Added
|
||||||
ChangeType::Modified
|
}
|
||||||
}
|
| pull_request_file_tree_query::PatchStatus::MODIFIED => {
|
||||||
| pull_request_file_tree_query::PatchStatus::DELETED => {
|
ChangeType::Modified
|
||||||
ChangeType::Deleted
|
}
|
||||||
}
|
| pull_request_file_tree_query::PatchStatus::DELETED => {
|
||||||
| pull_request_file_tree_query::PatchStatus::RENAMED => {
|
ChangeType::Deleted
|
||||||
ChangeType::Renamed
|
}
|
||||||
}
|
| pull_request_file_tree_query::PatchStatus::RENAMED => {
|
||||||
| pull_request_file_tree_query::PatchStatus::COPIED => {
|
ChangeType::Renamed
|
||||||
ChangeType::Copied
|
}
|
||||||
}
|
| pull_request_file_tree_query::PatchStatus::COPIED => {
|
||||||
| pull_request_file_tree_query::PatchStatus::CHANGED => {
|
ChangeType::Copied
|
||||||
ChangeType::Changed
|
}
|
||||||
}
|
| pull_request_file_tree_query::PatchStatus::CHANGED => {
|
||||||
| _ => ChangeType::Changed,
|
ChangeType::Changed
|
||||||
|
}
|
||||||
|
| _ => ChangeType::Changed,
|
||||||
},
|
},
|
||||||
additions: node.additions,
|
additions: node.additions,
|
||||||
deletions: node.deletions,
|
deletions: node.deletions,
|
||||||
@@ -576,11 +578,11 @@ impl query::QueryFn for FetchPullRequestTimeline {
|
|||||||
|
|
||||||
TimelineActor {
|
TimelineActor {
|
||||||
kind: match on {
|
kind: match on {
|
||||||
| actorFieldsOn::Bot => "Bot",
|
| actorFieldsOn::Bot => "Bot",
|
||||||
| actorFieldsOn::EnterpriseUserAccount => "EnterpriseUserAccount",
|
| actorFieldsOn::EnterpriseUserAccount => "EnterpriseUserAccount",
|
||||||
| actorFieldsOn::Mannequin => "Mannequin",
|
| actorFieldsOn::Mannequin => "Mannequin",
|
||||||
| actorFieldsOn::Organization => "Organization",
|
| actorFieldsOn::Organization => "Organization",
|
||||||
| actorFieldsOn::User => "User",
|
| actorFieldsOn::User => "User",
|
||||||
}
|
}
|
||||||
.into(),
|
.into(),
|
||||||
name: login,
|
name: login,
|
||||||
@@ -590,62 +592,62 @@ impl query::QueryFn for FetchPullRequestTimeline {
|
|||||||
|
|
||||||
fn normalize_assignee(actor: assigneeFields) -> TimelineActor {
|
fn normalize_assignee(actor: assigneeFields) -> TimelineActor {
|
||||||
match actor {
|
match actor {
|
||||||
| assigneeFields::Bot(actor) => TimelineActor {
|
| assigneeFields::Bot(actor) => TimelineActor {
|
||||||
kind: "Bot".into(),
|
kind: "Bot".into(),
|
||||||
name: actor.login,
|
name: actor.login,
|
||||||
avatar_url: Some(actor.avatar_url),
|
avatar_url: Some(actor.avatar_url),
|
||||||
},
|
},
|
||||||
| assigneeFields::Mannequin(actor) => TimelineActor {
|
| assigneeFields::Mannequin(actor) => TimelineActor {
|
||||||
kind: "Mannequin".into(),
|
kind: "Mannequin".into(),
|
||||||
name: actor.login,
|
name: actor.login,
|
||||||
avatar_url: Some(actor.avatar_url),
|
avatar_url: Some(actor.avatar_url),
|
||||||
},
|
},
|
||||||
| assigneeFields::Organization(actor) => TimelineActor {
|
| assigneeFields::Organization(actor) => TimelineActor {
|
||||||
kind: "Organization".into(),
|
kind: "Organization".into(),
|
||||||
name: actor.login,
|
name: actor.login,
|
||||||
avatar_url: Some(actor.avatar_url),
|
avatar_url: Some(actor.avatar_url),
|
||||||
},
|
},
|
||||||
| assigneeFields::User(actor) => TimelineActor {
|
| assigneeFields::User(actor) => TimelineActor {
|
||||||
kind: "User".into(),
|
kind: "User".into(),
|
||||||
name: actor.login,
|
name: actor.login,
|
||||||
avatar_url: Some(actor.avatar_url),
|
avatar_url: Some(actor.avatar_url),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn normalize_requested_reviewer(actor: requestedReviewerFields) -> TimelineActor {
|
fn normalize_requested_reviewer(actor: requestedReviewerFields) -> TimelineActor {
|
||||||
match actor {
|
match actor {
|
||||||
| requestedReviewerFields::Bot(actor) => TimelineActor {
|
| requestedReviewerFields::Bot(actor) => TimelineActor {
|
||||||
kind: "Bot".into(),
|
kind: "Bot".into(),
|
||||||
name: actor.login,
|
name: actor.login,
|
||||||
avatar_url: Some(actor.avatar_url),
|
avatar_url: Some(actor.avatar_url),
|
||||||
},
|
},
|
||||||
| requestedReviewerFields::Mannequin(actor) => TimelineActor {
|
| requestedReviewerFields::Mannequin(actor) => TimelineActor {
|
||||||
kind: "Mannequin".into(),
|
kind: "Mannequin".into(),
|
||||||
name: actor.login,
|
name: actor.login,
|
||||||
avatar_url: Some(actor.avatar_url),
|
avatar_url: Some(actor.avatar_url),
|
||||||
},
|
},
|
||||||
| requestedReviewerFields::Team(actor) => TimelineActor {
|
| requestedReviewerFields::Team(actor) => TimelineActor {
|
||||||
kind: "Team".into(),
|
kind: "Team".into(),
|
||||||
name: actor.name,
|
name: actor.name,
|
||||||
avatar_url: None,
|
avatar_url: None,
|
||||||
},
|
},
|
||||||
| requestedReviewerFields::User(actor) => TimelineActor {
|
| requestedReviewerFields::User(actor) => TimelineActor {
|
||||||
kind: "User".into(),
|
kind: "User".into(),
|
||||||
name: actor.login,
|
name: actor.login,
|
||||||
avatar_url: Some(actor.avatar_url),
|
avatar_url: Some(actor.avatar_url),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn normalize_review_state(state: PullRequestReviewState) -> String {
|
fn normalize_review_state(state: PullRequestReviewState) -> String {
|
||||||
match state {
|
match state {
|
||||||
| PullRequestReviewState::PENDING => "PENDING",
|
| PullRequestReviewState::PENDING => "PENDING",
|
||||||
| PullRequestReviewState::COMMENTED => "COMMENTED",
|
| PullRequestReviewState::COMMENTED => "COMMENTED",
|
||||||
| PullRequestReviewState::APPROVED => "APPROVED",
|
| PullRequestReviewState::APPROVED => "APPROVED",
|
||||||
| PullRequestReviewState::CHANGES_REQUESTED => "CHANGES_REQUESTED",
|
| PullRequestReviewState::CHANGES_REQUESTED => "CHANGES_REQUESTED",
|
||||||
| PullRequestReviewState::DISMISSED => "DISMISSED",
|
| PullRequestReviewState::DISMISSED => "DISMISSED",
|
||||||
| _ => "OTHER",
|
| _ => "OTHER",
|
||||||
}
|
}
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
@@ -865,10 +867,10 @@ impl query::QueryFn for FetchPullRequestTimeline {
|
|||||||
"missing 'node' field on PullRequestTimelineQuery response".into(),
|
"missing 'node' field on PullRequestTimelineQuery response".into(),
|
||||||
))
|
))
|
||||||
.and_then(|node| match node {
|
.and_then(|node| match node {
|
||||||
| PullRequestTimelineQueryNode::PullRequest(pull_request) => Ok(pull_request),
|
| PullRequestTimelineQueryNode::PullRequest(pull_request) => Ok(pull_request),
|
||||||
| _ => Err(api::Error::MalformedResponse(
|
| _ => Err(api::Error::MalformedResponse(
|
||||||
"unexpected node type on PullRequestTimelineQuery".into(),
|
"unexpected node type on PullRequestTimelineQuery".into(),
|
||||||
)),
|
)),
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let timeline = pull_request.timeline_items;
|
let timeline = pull_request.timeline_items;
|
||||||
|
|||||||
@@ -117,11 +117,13 @@ impl DiffRow {
|
|||||||
.map(|it| it.to_shared_string());
|
.map(|it| it.to_shared_string());
|
||||||
|
|
||||||
let marker = match self.line.op {
|
let marker = match self.line.op {
|
||||||
| util::diff::Op::Equal => code_view::CodeLineMarker::Unchanged,
|
| util::diff::Op::Equal => code_view::CodeLineMarker::Unchanged,
|
||||||
// inserting on new side, so placeholder on old side
|
// inserting on new side, so placeholder on old side
|
||||||
| util::diff::Op::Insert => code_view::CodeLineMarker::Placeholder,
|
| util::diff::Op::Insert => code_view::CodeLineMarker::Placeholder,
|
||||||
// old side replaced, so delete
|
// old side replaced, so delete
|
||||||
| util::diff::Op::Replace | util::diff::Op::Delete => code_view::CodeLineMarker::Deleted,
|
| util::diff::Op::Replace | util::diff::Op::Delete => {
|
||||||
|
code_view::CodeLineMarker::Deleted
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
match self.line.old_line.and_then(|line| {
|
match self.line.old_line.and_then(|line| {
|
||||||
@@ -130,14 +132,14 @@ impl DiffRow {
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|it| it.highlights_at_line(line))
|
.map(|it| it.highlights_at_line(line))
|
||||||
}) {
|
}) {
|
||||||
| Some(highlights) => code_line_with_highlights(
|
| Some(highlights) => code_line_with_highlights(
|
||||||
self.line.old_line,
|
self.line.old_line,
|
||||||
content,
|
content,
|
||||||
highlights.iter().cloned(),
|
highlights.iter().cloned(),
|
||||||
marker,
|
marker,
|
||||||
),
|
),
|
||||||
|
|
||||||
| None => code_line(self.line.old_line, content, marker),
|
| None => code_line(self.line.old_line, content, marker),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,9 +153,9 @@ impl DiffRow {
|
|||||||
.map(|it| it.to_shared_string());
|
.map(|it| it.to_shared_string());
|
||||||
|
|
||||||
let marker = match self.line.op {
|
let marker = match self.line.op {
|
||||||
| util::diff::Op::Equal => code_view::CodeLineMarker::Unchanged,
|
| util::diff::Op::Equal => code_view::CodeLineMarker::Unchanged,
|
||||||
| util::diff::Op::Insert | util::diff::Op::Replace => code_view::CodeLineMarker::Added,
|
| util::diff::Op::Insert | util::diff::Op::Replace => code_view::CodeLineMarker::Added,
|
||||||
| util::diff::Op::Delete => code_view::CodeLineMarker::Deleted,
|
| util::diff::Op::Delete => code_view::CodeLineMarker::Deleted,
|
||||||
};
|
};
|
||||||
|
|
||||||
match self.line.new_line.and_then(|line| {
|
match self.line.new_line.and_then(|line| {
|
||||||
@@ -162,14 +164,14 @@ impl DiffRow {
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|it| it.highlights_at_line(line))
|
.map(|it| it.highlights_at_line(line))
|
||||||
}) {
|
}) {
|
||||||
| Some(highlights) => code_line_with_highlights(
|
| Some(highlights) => code_line_with_highlights(
|
||||||
self.line.new_line,
|
self.line.new_line,
|
||||||
content,
|
content,
|
||||||
highlights.iter().cloned(),
|
highlights.iter().cloned(),
|
||||||
marker,
|
marker,
|
||||||
),
|
),
|
||||||
|
|
||||||
| None => code_line(self.line.new_line, content, marker),
|
| None => code_line(self.line.new_line, content, marker),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,3 +6,4 @@ pub(crate) mod font_icon;
|
|||||||
pub(crate) mod markdown;
|
pub(crate) mod markdown;
|
||||||
pub(crate) mod segmented_control;
|
pub(crate) mod segmented_control;
|
||||||
pub(crate) mod text;
|
pub(crate) mod text;
|
||||||
|
pub(crate) mod text_input;
|
||||||
|
|||||||
911
src/component/text_input.rs
Normal file
911
src/component/text_input.rs
Normal file
@@ -0,0 +1,911 @@
|
|||||||
|
use std::ops::Range;
|
||||||
|
|
||||||
|
use gpui::{
|
||||||
|
App, Bounds, Context, CursorStyle, Element, ElementId, ElementInputHandler, Entity,
|
||||||
|
EntityInputHandler, FocusHandle, Focusable, GlobalElementId, InteractiveElement, IntoElement,
|
||||||
|
LayoutId, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement,
|
||||||
|
Pixels, Point, Refineable, ShapedLine, SharedString, Style, Styled, TextRun, UTF16Selection,
|
||||||
|
UnderlineStyle, Window, div, fill, point, px, relative, size,
|
||||||
|
};
|
||||||
|
use unicode_segmentation::UnicodeSegmentation;
|
||||||
|
|
||||||
|
use crate::app;
|
||||||
|
|
||||||
|
gpui::actions!(
|
||||||
|
text_input,
|
||||||
|
[
|
||||||
|
Backspace,
|
||||||
|
BackspaceWord,
|
||||||
|
Delete,
|
||||||
|
Left,
|
||||||
|
WordLeft,
|
||||||
|
SelectLeft,
|
||||||
|
SelectWordLeft,
|
||||||
|
Right,
|
||||||
|
SelectRight,
|
||||||
|
WordRight,
|
||||||
|
SelectWordRight,
|
||||||
|
LineStart,
|
||||||
|
SelectToLineStart,
|
||||||
|
LineEnd,
|
||||||
|
SelectToLineEnd,
|
||||||
|
SelectAll,
|
||||||
|
Home,
|
||||||
|
End,
|
||||||
|
Paste,
|
||||||
|
Cut,
|
||||||
|
Copy,
|
||||||
|
Submit,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
const KEY_CONTEXT: &str = "TextInput";
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub(crate) enum Event {
|
||||||
|
Changed(SharedString),
|
||||||
|
Submitted(SharedString),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct TextInput {
|
||||||
|
focus_handle: FocusHandle,
|
||||||
|
content: SharedString,
|
||||||
|
placeholder: SharedString,
|
||||||
|
selected_range: Range<usize>,
|
||||||
|
selection_reversed: bool,
|
||||||
|
marked_range: Option<Range<usize>>,
|
||||||
|
last_layout: Option<ShapedLine>,
|
||||||
|
last_bounds: Option<Bounds<Pixels>>,
|
||||||
|
is_selecting: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(gpui::IntoElement)]
|
||||||
|
pub(crate) struct TextInputView {
|
||||||
|
input: Entity<TextInput>,
|
||||||
|
style: gpui::StyleRefinement,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub(crate) fn text_input(input: Entity<TextInput>) -> TextInputView {
|
||||||
|
TextInputView {
|
||||||
|
input,
|
||||||
|
style: gpui::StyleRefinement::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
impl TextInput {
|
||||||
|
pub(crate) fn new(cx: &mut Context<Self>) -> Self {
|
||||||
|
Self::with_placeholder("", cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn with_placeholder(
|
||||||
|
placeholder: impl Into<SharedString>,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
focus_handle: cx.focus_handle(),
|
||||||
|
content: "".into(),
|
||||||
|
placeholder: placeholder.into(),
|
||||||
|
selected_range: 0..0,
|
||||||
|
selection_reversed: false,
|
||||||
|
marked_range: None,
|
||||||
|
last_layout: None,
|
||||||
|
last_bounds: None,
|
||||||
|
is_selecting: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn key_bindings() -> [gpui::KeyBinding; 26] {
|
||||||
|
[
|
||||||
|
gpui::KeyBinding::new("backspace", Backspace, Some(KEY_CONTEXT)),
|
||||||
|
gpui::KeyBinding::new("alt-backspace", BackspaceWord, Some(KEY_CONTEXT)),
|
||||||
|
gpui::KeyBinding::new("delete", Delete, Some(KEY_CONTEXT)),
|
||||||
|
//
|
||||||
|
gpui::KeyBinding::new("left", Left, Some(KEY_CONTEXT)),
|
||||||
|
gpui::KeyBinding::new("shift-left", SelectLeft, Some(KEY_CONTEXT)),
|
||||||
|
gpui::KeyBinding::new("alt-left", WordLeft, Some(KEY_CONTEXT)),
|
||||||
|
gpui::KeyBinding::new("alt-shift-left", SelectWordLeft, Some(KEY_CONTEXT)),
|
||||||
|
gpui::KeyBinding::new("cmd-left", LineStart, Some(KEY_CONTEXT)),
|
||||||
|
gpui::KeyBinding::new("shift-cmd-left", SelectToLineStart, Some(KEY_CONTEXT)),
|
||||||
|
//
|
||||||
|
gpui::KeyBinding::new("right", Right, Some(KEY_CONTEXT)),
|
||||||
|
gpui::KeyBinding::new("shift-right", SelectRight, Some(KEY_CONTEXT)),
|
||||||
|
gpui::KeyBinding::new("alt-right", WordRight, Some(KEY_CONTEXT)),
|
||||||
|
gpui::KeyBinding::new("alt-shift-right", SelectWordRight, Some(KEY_CONTEXT)),
|
||||||
|
gpui::KeyBinding::new("cmd-right", LineEnd, Some(KEY_CONTEXT)),
|
||||||
|
gpui::KeyBinding::new("shift-cmd-right", SelectToLineEnd, Some(KEY_CONTEXT)),
|
||||||
|
//
|
||||||
|
gpui::KeyBinding::new("cmd-a", SelectAll, Some(KEY_CONTEXT)),
|
||||||
|
gpui::KeyBinding::new("ctrl-a", SelectAll, Some(KEY_CONTEXT)),
|
||||||
|
gpui::KeyBinding::new("cmd-v", Paste, Some(KEY_CONTEXT)),
|
||||||
|
gpui::KeyBinding::new("ctrl-v", Paste, Some(KEY_CONTEXT)),
|
||||||
|
gpui::KeyBinding::new("cmd-c", Copy, Some(KEY_CONTEXT)),
|
||||||
|
gpui::KeyBinding::new("ctrl-c", Copy, Some(KEY_CONTEXT)),
|
||||||
|
gpui::KeyBinding::new("cmd-x", Cut, Some(KEY_CONTEXT)),
|
||||||
|
gpui::KeyBinding::new("ctrl-x", Cut, Some(KEY_CONTEXT)),
|
||||||
|
gpui::KeyBinding::new("home", Home, Some(KEY_CONTEXT)),
|
||||||
|
gpui::KeyBinding::new("end", End, Some(KEY_CONTEXT)),
|
||||||
|
gpui::KeyBinding::new("enter", Submit, Some(KEY_CONTEXT)),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn content(&self) -> &str {
|
||||||
|
&self.content
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_content(&mut self, content: impl Into<SharedString>, cx: &mut Context<Self>) {
|
||||||
|
let content = content.into();
|
||||||
|
let changed = self.content != content;
|
||||||
|
self.content = content;
|
||||||
|
self.selected_range = self.content.len()..self.content.len();
|
||||||
|
self.selection_reversed = false;
|
||||||
|
self.marked_range = None;
|
||||||
|
cx.notify();
|
||||||
|
|
||||||
|
if changed {
|
||||||
|
cx.emit(Event::Changed(self.content.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn clear(&mut self, cx: &mut Context<Self>) {
|
||||||
|
self.set_content("", cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn left(&mut self, _: &Left, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if self.selected_range.is_empty() {
|
||||||
|
self.move_to(self.previous_boundary(self.cursor_offset()), cx);
|
||||||
|
} else {
|
||||||
|
self.move_to(self.selected_range.start, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn word_left(&mut self, _: &WordLeft, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if self.selected_range.is_empty() {
|
||||||
|
self.move_to(self.previous_word_boundary(self.cursor_offset()), cx);
|
||||||
|
} else {
|
||||||
|
self.move_to(self.selected_range.start, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn right(&mut self, _: &Right, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if self.selected_range.is_empty() {
|
||||||
|
self.move_to(self.next_boundary(self.cursor_offset()), cx);
|
||||||
|
} else {
|
||||||
|
self.move_to(self.selected_range.end, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn word_right(&mut self, _: &WordRight, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if self.selected_range.is_empty() {
|
||||||
|
self.move_to(self.next_word_boundary(self.cursor_offset()), cx);
|
||||||
|
} else {
|
||||||
|
self.move_to(self.selected_range.end, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn select_left(&mut self, _: &SelectLeft, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.select_to(self.previous_boundary(self.cursor_offset()), cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn select_word_left(&mut self, _: &SelectWordLeft, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.select_to(self.previous_word_boundary(self.cursor_offset()), cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn select_right(&mut self, _: &SelectRight, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.select_to(self.next_boundary(self.cursor_offset()), cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn select_word_right(&mut self, _: &SelectWordRight, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.select_to(self.next_word_boundary(self.cursor_offset()), cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn line_start(&mut self, _: &LineStart, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.move_to(0, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn select_to_line_start(
|
||||||
|
&mut self,
|
||||||
|
_: &SelectToLineStart,
|
||||||
|
_: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.select_to(0, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn select_to_line_end(&mut self, _: &SelectToLineEnd, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.select_to(self.content.len(), cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn line_end(&mut self, _: &LineEnd, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.move_to(self.content().len(), cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn select_all(&mut self, _: &SelectAll, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.move_to(0, cx);
|
||||||
|
self.select_to(self.content.len(), cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn home(&mut self, _: &Home, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.move_to(0, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn end(&mut self, _: &End, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.move_to(self.content.len(), cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn backspace(&mut self, _: &Backspace, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if self.selected_range.is_empty() {
|
||||||
|
self.select_to(self.previous_boundary(self.cursor_offset()), cx);
|
||||||
|
}
|
||||||
|
self.replace_text_in_range(None, "", window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn backspace_word(&mut self, _: &BackspaceWord, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if self.selected_range.is_empty() {
|
||||||
|
self.select_to(self.previous_word_boundary(self.cursor_offset()), cx);
|
||||||
|
}
|
||||||
|
self.replace_text_in_range(None, "", window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete(&mut self, _: &Delete, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if self.selected_range.is_empty() {
|
||||||
|
self.select_to(self.next_boundary(self.cursor_offset()), cx);
|
||||||
|
}
|
||||||
|
self.replace_text_in_range(None, "", window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if let Some(text) = cx.read_from_clipboard().and_then(|item| item.text()) {
|
||||||
|
self.replace_text_in_range(None, &text.replace('\n', " "), window, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn copy(&mut self, _: &Copy, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if !self.selected_range.is_empty() {
|
||||||
|
cx.write_to_clipboard(gpui::ClipboardItem::new_string(
|
||||||
|
self.content[self.selected_range.clone()].to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cut(&mut self, _: &Cut, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if !self.selected_range.is_empty() {
|
||||||
|
cx.write_to_clipboard(gpui::ClipboardItem::new_string(
|
||||||
|
self.content[self.selected_range.clone()].to_string(),
|
||||||
|
));
|
||||||
|
self.replace_text_in_range(None, "", window, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn submit(&mut self, _: &Submit, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
cx.emit(Event::Submitted(self.content.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_mouse_down(
|
||||||
|
&mut self,
|
||||||
|
event: &MouseDownEvent,
|
||||||
|
_window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.is_selecting = true;
|
||||||
|
|
||||||
|
if event.modifiers.shift {
|
||||||
|
self.select_to(self.index_for_mouse_position(event.position), cx);
|
||||||
|
} else {
|
||||||
|
self.move_to(self.index_for_mouse_position(event.position), cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_mouse_up(&mut self, _: &MouseUpEvent, _window: &mut Window, _: &mut Context<Self>) {
|
||||||
|
self.is_selecting = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_mouse_move(&mut self, event: &MouseMoveEvent, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if self.is_selecting {
|
||||||
|
self.select_to(self.index_for_mouse_position(event.position), cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn move_to(&mut self, offset: usize, cx: &mut Context<Self>) {
|
||||||
|
let offset = offset.min(self.content.len());
|
||||||
|
self.selected_range = offset..offset;
|
||||||
|
self.selection_reversed = false;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cursor_offset(&self) -> usize {
|
||||||
|
if self.selection_reversed {
|
||||||
|
self.selected_range.start
|
||||||
|
} else {
|
||||||
|
self.selected_range.end
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn index_for_mouse_position(&self, position: Point<Pixels>) -> usize {
|
||||||
|
if self.content.is_empty() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (Some(bounds), Some(line)) = (self.last_bounds.as_ref(), self.last_layout.as_ref())
|
||||||
|
else {
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
if position.y < bounds.top() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if position.y > bounds.bottom() {
|
||||||
|
return self.content.len();
|
||||||
|
}
|
||||||
|
|
||||||
|
line.closest_index_for_x(position.x - bounds.left())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn select_to(&mut self, offset: usize, cx: &mut Context<Self>) {
|
||||||
|
let offset = offset.min(self.content.len());
|
||||||
|
if self.selection_reversed {
|
||||||
|
self.selected_range.start = offset;
|
||||||
|
} else {
|
||||||
|
self.selected_range.end = offset;
|
||||||
|
}
|
||||||
|
if self.selected_range.end < self.selected_range.start {
|
||||||
|
self.selection_reversed = !self.selection_reversed;
|
||||||
|
self.selected_range = self.selected_range.end..self.selected_range.start;
|
||||||
|
}
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn range_to_utf16(&self, range: &Range<usize>) -> Range<usize> {
|
||||||
|
offset_to_utf16(&self.content, range.start)..offset_to_utf16(&self.content, range.end)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn range_from_utf16(&self, range_utf16: &Range<usize>) -> Range<usize> {
|
||||||
|
range_from_utf16(&self.content, range_utf16)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn previous_boundary(&self, offset: usize) -> usize {
|
||||||
|
self.content
|
||||||
|
.grapheme_indices(true)
|
||||||
|
.rev()
|
||||||
|
.find_map(|(idx, _)| (idx < offset).then_some(idx))
|
||||||
|
.unwrap_or(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn previous_word_boundary(&self, offset: usize) -> usize {
|
||||||
|
self.content
|
||||||
|
.split_word_bound_indices()
|
||||||
|
.rev()
|
||||||
|
.find_map(|(idx, word)| (idx < offset && !word.trim().is_empty()).then_some(idx))
|
||||||
|
.unwrap_or(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_boundary(&self, offset: usize) -> usize {
|
||||||
|
self.content
|
||||||
|
.grapheme_indices(true)
|
||||||
|
.find_map(|(idx, _)| (idx > offset).then_some(idx))
|
||||||
|
.unwrap_or(self.content.len())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_word_boundary(&self, offset: usize) -> usize {
|
||||||
|
self.content
|
||||||
|
.split_word_bound_indices()
|
||||||
|
.find_map(|(idx, word)| {
|
||||||
|
let word_end = idx + word.len();
|
||||||
|
(word_end > offset && !word.trim().is_empty()).then_some(word_end)
|
||||||
|
})
|
||||||
|
.unwrap_or(offset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EntityInputHandler for TextInput {
|
||||||
|
fn text_for_range(
|
||||||
|
&mut self,
|
||||||
|
range_utf16: Range<usize>,
|
||||||
|
actual_range: &mut Option<Range<usize>>,
|
||||||
|
_window: &mut Window,
|
||||||
|
_cx: &mut Context<Self>,
|
||||||
|
) -> Option<String> {
|
||||||
|
let range = self.range_from_utf16(&range_utf16);
|
||||||
|
actual_range.replace(self.range_to_utf16(&range));
|
||||||
|
Some(self.content[range].to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn selected_text_range(
|
||||||
|
&mut self,
|
||||||
|
_ignore_disabled_input: bool,
|
||||||
|
_window: &mut Window,
|
||||||
|
_cx: &mut Context<Self>,
|
||||||
|
) -> Option<UTF16Selection> {
|
||||||
|
Some(UTF16Selection {
|
||||||
|
range: self.range_to_utf16(&self.selected_range),
|
||||||
|
reversed: self.selection_reversed,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn marked_text_range(
|
||||||
|
&self,
|
||||||
|
_window: &mut Window,
|
||||||
|
_cx: &mut Context<Self>,
|
||||||
|
) -> Option<Range<usize>> {
|
||||||
|
self.marked_range
|
||||||
|
.as_ref()
|
||||||
|
.map(|range| self.range_to_utf16(range))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unmark_text(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {
|
||||||
|
self.marked_range = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn replace_text_in_range(
|
||||||
|
&mut self,
|
||||||
|
range_utf16: Option<Range<usize>>,
|
||||||
|
new_text: &str,
|
||||||
|
_window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
let range = range_utf16
|
||||||
|
.as_ref()
|
||||||
|
.map(|range_utf16| self.range_from_utf16(range_utf16))
|
||||||
|
.or(self.marked_range.clone())
|
||||||
|
.unwrap_or(self.selected_range.clone());
|
||||||
|
|
||||||
|
let old_content = self.content.clone();
|
||||||
|
self.content =
|
||||||
|
(self.content[0..range.start].to_owned() + new_text + &self.content[range.end..])
|
||||||
|
.into();
|
||||||
|
let cursor = range.start + new_text.len();
|
||||||
|
self.selected_range = cursor..cursor;
|
||||||
|
self.selection_reversed = false;
|
||||||
|
self.marked_range.take();
|
||||||
|
cx.notify();
|
||||||
|
|
||||||
|
if self.content != old_content {
|
||||||
|
cx.emit(Event::Changed(self.content.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn replace_and_mark_text_in_range(
|
||||||
|
&mut self,
|
||||||
|
range_utf16: Option<Range<usize>>,
|
||||||
|
new_text: &str,
|
||||||
|
new_selected_range_utf16: Option<Range<usize>>,
|
||||||
|
_window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
let range = range_utf16
|
||||||
|
.as_ref()
|
||||||
|
.map(|range_utf16| self.range_from_utf16(range_utf16))
|
||||||
|
.or(self.marked_range.clone())
|
||||||
|
.unwrap_or(self.selected_range.clone());
|
||||||
|
|
||||||
|
let old_content = self.content.clone();
|
||||||
|
self.content =
|
||||||
|
(self.content[0..range.start].to_owned() + new_text + &self.content[range.end..])
|
||||||
|
.into();
|
||||||
|
self.marked_range =
|
||||||
|
(!new_text.is_empty()).then_some(range.start..range.start + new_text.len());
|
||||||
|
self.selected_range = new_selected_range_utf16
|
||||||
|
.as_ref()
|
||||||
|
.map(|range_utf16| range_from_utf16(new_text, range_utf16))
|
||||||
|
.map(|new_range| range.start + new_range.start..range.start + new_range.end)
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
let cursor = range.start + new_text.len();
|
||||||
|
cursor..cursor
|
||||||
|
});
|
||||||
|
self.selection_reversed = false;
|
||||||
|
cx.notify();
|
||||||
|
|
||||||
|
if self.content != old_content {
|
||||||
|
cx.emit(Event::Changed(self.content.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bounds_for_range(
|
||||||
|
&mut self,
|
||||||
|
range_utf16: Range<usize>,
|
||||||
|
bounds: Bounds<Pixels>,
|
||||||
|
_window: &mut Window,
|
||||||
|
_cx: &mut Context<Self>,
|
||||||
|
) -> Option<Bounds<Pixels>> {
|
||||||
|
let last_layout = self.last_layout.as_ref()?;
|
||||||
|
let range = self.range_from_utf16(&range_utf16);
|
||||||
|
Some(Bounds::from_corners(
|
||||||
|
point(
|
||||||
|
bounds.left() + last_layout.x_for_index(range.start),
|
||||||
|
bounds.top(),
|
||||||
|
),
|
||||||
|
point(
|
||||||
|
bounds.left() + last_layout.x_for_index(range.end),
|
||||||
|
bounds.bottom(),
|
||||||
|
),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn character_index_for_point(
|
||||||
|
&mut self,
|
||||||
|
point: Point<Pixels>,
|
||||||
|
_window: &mut Window,
|
||||||
|
_cx: &mut Context<Self>,
|
||||||
|
) -> Option<usize> {
|
||||||
|
let bounds = self.last_bounds?;
|
||||||
|
let last_layout = self.last_layout.as_ref()?;
|
||||||
|
let local_point = bounds.localize(&point)?;
|
||||||
|
let utf8_index = last_layout.index_for_x(local_point.x)?;
|
||||||
|
Some(offset_to_utf16(&self.content, utf8_index))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Focusable for TextInput {
|
||||||
|
fn focus_handle(&self, _: &App) -> FocusHandle {
|
||||||
|
self.focus_handle.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl gpui::EventEmitter<Event> for TextInput {}
|
||||||
|
|
||||||
|
impl Styled for TextInputView {
|
||||||
|
fn style(&mut self) -> &mut gpui::StyleRefinement {
|
||||||
|
&mut self.style
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl gpui::Render for TextInput {
|
||||||
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
TextInputElement { input: cx.entity() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl gpui::RenderOnce for TextInputView {
|
||||||
|
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
|
let theme = app::current_theme(cx);
|
||||||
|
let focus_handle = self.input.read(cx).focus_handle.clone();
|
||||||
|
|
||||||
|
let on_backspace = self.input.clone();
|
||||||
|
let on_backspace_word = self.input.clone();
|
||||||
|
let on_delete = self.input.clone();
|
||||||
|
|
||||||
|
let on_left = self.input.clone();
|
||||||
|
let on_select_left = self.input.clone();
|
||||||
|
let on_word_left = self.input.clone();
|
||||||
|
let on_select_word_left = self.input.clone();
|
||||||
|
|
||||||
|
let on_right = self.input.clone();
|
||||||
|
let on_select_right = self.input.clone();
|
||||||
|
let on_word_right = self.input.clone();
|
||||||
|
let on_select_word_right = self.input.clone();
|
||||||
|
|
||||||
|
let on_line_start = self.input.clone();
|
||||||
|
let on_select_to_line_start = self.input.clone();
|
||||||
|
let on_line_end = self.input.clone();
|
||||||
|
let on_select_to_line_end = self.input.clone();
|
||||||
|
|
||||||
|
let on_select_all = self.input.clone();
|
||||||
|
let on_home = self.input.clone();
|
||||||
|
let on_end = self.input.clone();
|
||||||
|
let on_paste = self.input.clone();
|
||||||
|
let on_cut = self.input.clone();
|
||||||
|
let on_copy = self.input.clone();
|
||||||
|
let on_submit = self.input.clone();
|
||||||
|
let on_mouse_down = self.input.clone();
|
||||||
|
let on_mouse_up = self.input.clone();
|
||||||
|
let on_mouse_up_out = self.input.clone();
|
||||||
|
let on_mouse_move = self.input.clone();
|
||||||
|
|
||||||
|
let mut input = div()
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.w_full()
|
||||||
|
.px_2()
|
||||||
|
.py_1()
|
||||||
|
.text_color(theme.colors.text)
|
||||||
|
.line_height(px(20.))
|
||||||
|
.key_context(KEY_CONTEXT)
|
||||||
|
.track_focus(&focus_handle)
|
||||||
|
.cursor(CursorStyle::IBeam)
|
||||||
|
.on_action(move |action: &Backspace, window, cx| {
|
||||||
|
_ = on_backspace.update(cx, |this, cx| this.backspace(action, window, cx));
|
||||||
|
})
|
||||||
|
.on_action(move |action: &BackspaceWord, window, cx| {
|
||||||
|
_ = on_backspace_word.update(cx, |this, cx| this.backspace_word(action, window, cx))
|
||||||
|
})
|
||||||
|
.on_action(move |action: &Delete, window, cx| {
|
||||||
|
_ = on_delete.update(cx, |this, cx| this.delete(action, window, cx));
|
||||||
|
})
|
||||||
|
.on_action(move |action: &Left, window, cx| {
|
||||||
|
_ = on_left.update(cx, |this, cx| this.left(action, window, cx));
|
||||||
|
})
|
||||||
|
.on_action(move |action: &SelectLeft, window, cx| {
|
||||||
|
_ = on_select_left.update(cx, |this, cx| this.select_left(action, window, cx));
|
||||||
|
})
|
||||||
|
.on_action(move |action: &WordLeft, window, cx| {
|
||||||
|
_ = on_word_left.update(cx, |this, cx| this.word_left(action, window, cx));
|
||||||
|
})
|
||||||
|
.on_action(move |action: &SelectWordLeft, window, cx| {
|
||||||
|
_ = on_select_word_left
|
||||||
|
.update(cx, |this, cx| this.select_word_left(action, window, cx));
|
||||||
|
})
|
||||||
|
.on_action(move |action: &Right, window, cx| {
|
||||||
|
_ = on_right.update(cx, |this, cx| this.right(action, window, cx));
|
||||||
|
})
|
||||||
|
.on_action(move |action: &WordRight, window, cx| {
|
||||||
|
_ = on_word_right.update(cx, |this, cx| this.word_right(action, window, cx));
|
||||||
|
})
|
||||||
|
.on_action(move |action: &SelectRight, window, cx| {
|
||||||
|
_ = on_select_right.update(cx, |this, cx| this.select_right(action, window, cx));
|
||||||
|
})
|
||||||
|
.on_action(move |action: &SelectWordRight, window, cx| {
|
||||||
|
_ = on_select_word_right
|
||||||
|
.update(cx, |this, cx| this.select_word_right(action, window, cx));
|
||||||
|
})
|
||||||
|
.on_action(move |action: &LineStart, window, cx| {
|
||||||
|
_ = on_line_start.update(cx, |this, cx| this.line_start(action, window, cx));
|
||||||
|
})
|
||||||
|
.on_action(move |action: &SelectToLineStart, window, cx| {
|
||||||
|
_ = on_select_to_line_start
|
||||||
|
.update(cx, |this, cx| this.select_to_line_start(action, window, cx));
|
||||||
|
})
|
||||||
|
.on_action(move |action: &LineEnd, window, cx| {
|
||||||
|
_ = on_line_end.update(cx, |this, cx| this.line_end(action, window, cx));
|
||||||
|
})
|
||||||
|
.on_action(move |action: &SelectToLineEnd, window, cx| {
|
||||||
|
_ = on_select_to_line_end
|
||||||
|
.update(cx, |this, cx| this.select_to_line_end(action, window, cx));
|
||||||
|
})
|
||||||
|
.on_action(move |action: &SelectAll, window, cx| {
|
||||||
|
_ = on_select_all.update(cx, |this, cx| this.select_all(action, window, cx));
|
||||||
|
})
|
||||||
|
.on_action(move |action: &Home, window, cx| {
|
||||||
|
_ = on_home.update(cx, |this, cx| this.home(action, window, cx));
|
||||||
|
})
|
||||||
|
.on_action(move |action: &End, window, cx| {
|
||||||
|
_ = on_end.update(cx, |this, cx| this.end(action, window, cx));
|
||||||
|
})
|
||||||
|
.on_action(move |action: &Paste, window, cx| {
|
||||||
|
_ = on_paste.update(cx, |this, cx| this.paste(action, window, cx));
|
||||||
|
})
|
||||||
|
.on_action(move |action: &Cut, window, cx| {
|
||||||
|
_ = on_cut.update(cx, |this, cx| this.cut(action, window, cx));
|
||||||
|
})
|
||||||
|
.on_action(move |action: &Copy, window, cx| {
|
||||||
|
_ = on_copy.update(cx, |this, cx| this.copy(action, window, cx));
|
||||||
|
})
|
||||||
|
.on_action(move |action: &Submit, window, cx| {
|
||||||
|
_ = on_submit.update(cx, |this, cx| this.submit(action, window, cx));
|
||||||
|
})
|
||||||
|
.on_mouse_down(MouseButton::Left, move |event, window, cx| {
|
||||||
|
_ = on_mouse_down.update(cx, |this, cx| this.on_mouse_down(event, window, cx));
|
||||||
|
})
|
||||||
|
.on_mouse_up(MouseButton::Left, move |event, window, cx| {
|
||||||
|
_ = on_mouse_up.update(cx, |this, cx| this.on_mouse_up(event, window, cx));
|
||||||
|
})
|
||||||
|
.on_mouse_up_out(MouseButton::Left, move |event, window, cx| {
|
||||||
|
_ = on_mouse_up_out.update(cx, |this, cx| this.on_mouse_up(event, window, cx));
|
||||||
|
})
|
||||||
|
.on_mouse_move(move |event, window, cx| {
|
||||||
|
_ = on_mouse_move.update(cx, |this, cx| this.on_mouse_move(event, window, cx));
|
||||||
|
})
|
||||||
|
.child(self.input.clone());
|
||||||
|
|
||||||
|
input.style().refine(&self.style);
|
||||||
|
|
||||||
|
input
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TextInputElement {
|
||||||
|
input: Entity<TextInput>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PrepaintState {
|
||||||
|
line: Option<ShapedLine>,
|
||||||
|
cursor: Option<PaintQuad>,
|
||||||
|
selection: Option<PaintQuad>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoElement for TextInputElement {
|
||||||
|
type Element = Self;
|
||||||
|
|
||||||
|
fn into_element(self) -> Self::Element {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Element for TextInputElement {
|
||||||
|
type RequestLayoutState = ();
|
||||||
|
type PrepaintState = PrepaintState;
|
||||||
|
|
||||||
|
fn id(&self) -> Option<ElementId> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request_layout(
|
||||||
|
&mut self,
|
||||||
|
_id: Option<&GlobalElementId>,
|
||||||
|
_inspector_id: Option<&gpui::InspectorElementId>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> (LayoutId, Self::RequestLayoutState) {
|
||||||
|
let mut style = Style::default();
|
||||||
|
style.size.width = relative(1.).into();
|
||||||
|
style.size.height = window.line_height().into();
|
||||||
|
(window.request_layout(style, [], cx), ())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepaint(
|
||||||
|
&mut self,
|
||||||
|
_id: Option<&GlobalElementId>,
|
||||||
|
_inspector_id: Option<&gpui::InspectorElementId>,
|
||||||
|
bounds: Bounds<Pixels>,
|
||||||
|
_request_layout: &mut Self::RequestLayoutState,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> Self::PrepaintState {
|
||||||
|
let theme = app::current_theme(cx);
|
||||||
|
let input = self.input.read(cx);
|
||||||
|
let content = input.content.clone();
|
||||||
|
let selected_range = input.selected_range.clone();
|
||||||
|
let cursor = input.cursor_offset();
|
||||||
|
let style = window.text_style();
|
||||||
|
|
||||||
|
let (display_text, text_color) = if content.is_empty() {
|
||||||
|
(input.placeholder.clone(), theme.colors.text_subtle.into())
|
||||||
|
} else {
|
||||||
|
(content, style.color)
|
||||||
|
};
|
||||||
|
|
||||||
|
let run = TextRun {
|
||||||
|
len: display_text.len(),
|
||||||
|
font: style.font(),
|
||||||
|
color: text_color,
|
||||||
|
background_color: None,
|
||||||
|
underline: None,
|
||||||
|
strikethrough: None,
|
||||||
|
};
|
||||||
|
let runs = if let Some(marked_range) = input.marked_range.as_ref() {
|
||||||
|
vec![
|
||||||
|
TextRun {
|
||||||
|
len: marked_range.start,
|
||||||
|
..run.clone()
|
||||||
|
},
|
||||||
|
TextRun {
|
||||||
|
len: marked_range.end - marked_range.start,
|
||||||
|
underline: Some(UnderlineStyle {
|
||||||
|
color: Some(run.color),
|
||||||
|
thickness: px(1.0),
|
||||||
|
wavy: false,
|
||||||
|
}),
|
||||||
|
..run.clone()
|
||||||
|
},
|
||||||
|
TextRun {
|
||||||
|
len: display_text.len() - marked_range.end,
|
||||||
|
..run
|
||||||
|
},
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.filter(|run| run.len > 0)
|
||||||
|
.collect()
|
||||||
|
} else {
|
||||||
|
vec![run]
|
||||||
|
};
|
||||||
|
|
||||||
|
let font_size = style.font_size.to_pixels(window.rem_size());
|
||||||
|
let line = window
|
||||||
|
.text_system()
|
||||||
|
.shape_line(display_text, font_size, &runs, None);
|
||||||
|
|
||||||
|
let cursor_pos = line.x_for_index(cursor);
|
||||||
|
let (selection, cursor) = if selected_range.is_empty() {
|
||||||
|
(
|
||||||
|
None,
|
||||||
|
Some(fill(
|
||||||
|
Bounds::new(
|
||||||
|
point(bounds.left() + cursor_pos, bounds.top()),
|
||||||
|
size(px(1.), bounds.bottom() - bounds.top()),
|
||||||
|
),
|
||||||
|
theme.colors.accent_solid,
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
Some(fill(
|
||||||
|
Bounds::from_corners(
|
||||||
|
point(
|
||||||
|
bounds.left() + line.x_for_index(selected_range.start),
|
||||||
|
bounds.top(),
|
||||||
|
),
|
||||||
|
point(
|
||||||
|
bounds.left() + line.x_for_index(selected_range.end),
|
||||||
|
bounds.bottom(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
theme.colors.selection_bg,
|
||||||
|
)),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
PrepaintState {
|
||||||
|
line: Some(line),
|
||||||
|
cursor,
|
||||||
|
selection,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paint(
|
||||||
|
&mut self,
|
||||||
|
_id: Option<&GlobalElementId>,
|
||||||
|
_inspector_id: Option<&gpui::InspectorElementId>,
|
||||||
|
bounds: Bounds<Pixels>,
|
||||||
|
_request_layout: &mut Self::RequestLayoutState,
|
||||||
|
prepaint: &mut Self::PrepaintState,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut App,
|
||||||
|
) {
|
||||||
|
let focus_handle = self.input.read(cx).focus_handle.clone();
|
||||||
|
window.handle_input(
|
||||||
|
&focus_handle,
|
||||||
|
ElementInputHandler::new(bounds, self.input.clone()),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Some(selection) = prepaint.selection.take() {
|
||||||
|
window.paint_quad(selection);
|
||||||
|
}
|
||||||
|
|
||||||
|
let line = prepaint.line.take().unwrap();
|
||||||
|
line.paint(bounds.origin, window.line_height(), window, cx)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
if focus_handle.is_focused(window)
|
||||||
|
&& let Some(cursor) = prepaint.cursor.take()
|
||||||
|
{
|
||||||
|
window.paint_quad(cursor);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.input.update(cx, |input, _cx| {
|
||||||
|
input.last_layout = Some(line);
|
||||||
|
input.last_bounds = Some(bounds);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn offset_to_utf16(text: &str, offset: usize) -> usize {
|
||||||
|
let mut utf16_offset = 0;
|
||||||
|
let mut utf8_count = 0;
|
||||||
|
|
||||||
|
for ch in text.chars() {
|
||||||
|
if utf8_count >= offset {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
utf8_count += ch.len_utf8();
|
||||||
|
utf16_offset += ch.len_utf16();
|
||||||
|
}
|
||||||
|
|
||||||
|
utf16_offset
|
||||||
|
}
|
||||||
|
|
||||||
|
fn offset_from_utf16(text: &str, offset: usize) -> usize {
|
||||||
|
let mut utf8_offset = 0;
|
||||||
|
let mut utf16_count = 0;
|
||||||
|
|
||||||
|
for ch in text.chars() {
|
||||||
|
if utf16_count >= offset {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
utf16_count += ch.len_utf16();
|
||||||
|
utf8_offset += ch.len_utf8();
|
||||||
|
}
|
||||||
|
|
||||||
|
utf8_offset
|
||||||
|
}
|
||||||
|
|
||||||
|
fn range_from_utf16(text: &str, range_utf16: &Range<usize>) -> Range<usize> {
|
||||||
|
offset_from_utf16(text, range_utf16.start)..offset_from_utf16(text, range_utf16.end)
|
||||||
|
}
|
||||||
9
src/keyboard.rs
Normal file
9
src/keyboard.rs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
use crate::{
|
||||||
|
component::text_input::TextInput,
|
||||||
|
screen::dashboard::pull_request_diff_view::PullRequestDiffView,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub(crate) fn attach_key_binds(cx: &mut gpui::App) {
|
||||||
|
cx.bind_keys(PullRequestDiffView::key_bindings());
|
||||||
|
cx.bind_keys(TextInput::key_bindings());
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ mod asset;
|
|||||||
mod colors;
|
mod colors;
|
||||||
mod component;
|
mod component;
|
||||||
mod http;
|
mod http;
|
||||||
|
mod keyboard;
|
||||||
mod query;
|
mod query;
|
||||||
mod screen;
|
mod screen;
|
||||||
mod storage;
|
mod storage;
|
||||||
@@ -62,6 +63,8 @@ fn setup_application(cx: &mut gpui::App) {
|
|||||||
cx.set_global(global);
|
cx.set_global(global);
|
||||||
cx.set_global(query_store);
|
cx.set_global(query_store);
|
||||||
|
|
||||||
|
keyboard::attach_key_binds(cx);
|
||||||
|
|
||||||
if diffops_playground::is_enabled() {
|
if diffops_playground::is_enabled() {
|
||||||
_ = diffops_playground::open_window(cx);
|
_ = diffops_playground::open_window(cx);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
mod issue_list;
|
mod issue_list;
|
||||||
mod pull_request_change_view;
|
mod pull_request_change_view;
|
||||||
mod pull_request_diff_view;
|
pub(crate) mod pull_request_diff_view;
|
||||||
mod pull_request_file_tree;
|
mod pull_request_file_tree;
|
||||||
mod pull_request_view;
|
mod pull_request_view;
|
||||||
mod screen;
|
mod screen;
|
||||||
|
|||||||
@@ -60,22 +60,28 @@ impl PullRequestChangeView {
|
|||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_file_tree_item_click(&mut self, i: usize, cx: &mut gpui::Context<Self>) {
|
fn handle_file_tree_item_click(
|
||||||
|
&mut self,
|
||||||
|
i: usize,
|
||||||
|
window: &mut gpui::Window,
|
||||||
|
cx: &mut gpui::Context<Self>,
|
||||||
|
) {
|
||||||
let item = &self.file_tree_items[i];
|
let item = &self.file_tree_items[i];
|
||||||
match item.kind {
|
match item.kind {
|
||||||
| FileTreeItemKind::Directory => {
|
| FileTreeItemKind::Directory => {
|
||||||
self.file_tree_state
|
self.file_tree_state
|
||||||
.toggle_directory(&item.full_path, &self.file_tree_items);
|
.toggle_directory(&item.full_path, &self.file_tree_items);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
| FileTreeItemKind::File => {
|
| FileTreeItemKind::File => {
|
||||||
self.selected_file_path = Some(Arc::clone(&item.full_path));
|
self.selected_file_path = Some(Arc::clone(&item.full_path));
|
||||||
self.file_tree_state.highlight_item(i);
|
self.file_tree_state.highlight_item(i);
|
||||||
self.diff_view.update(cx, |diff_view, cx| {
|
self.diff_view.update(cx, |diff_view, cx| {
|
||||||
diff_view.show_diff_for_file(&item.full_path, cx);
|
diff_view.show_diff_for_file(&item.full_path, cx);
|
||||||
});
|
});
|
||||||
cx.notify();
|
cx.focus_view(&self.diff_view, window);
|
||||||
}
|
cx.notify();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -106,9 +112,11 @@ impl gpui::Render for PullRequestChangeView {
|
|||||||
file_tree(self.file_tree_state.clone(), move |i, _, cx| {
|
file_tree(self.file_tree_state.clone(), move |i, _, cx| {
|
||||||
weak.read(cx).file_tree_items[i].clone()
|
weak.read(cx).file_tree_items[i].clone()
|
||||||
})
|
})
|
||||||
.on_item_click(cx.listener(|this, i, _, cx| {
|
.on_item_click(cx.listener(
|
||||||
this.handle_file_tree_item_click(*i, cx);
|
|this, i, window, cx| {
|
||||||
})),
|
this.handle_file_tree_item_click(*i, window, cx);
|
||||||
|
},
|
||||||
|
)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use gpui::{AppContext, IntoElement, div};
|
use gpui::{
|
||||||
|
AppContext, FocusHandle, InteractiveElement, IntoElement, ParentElement, Styled, div,
|
||||||
|
prelude::FluentBuilder,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{self},
|
api::{self},
|
||||||
app,
|
app,
|
||||||
component::diff_view::{DiffViewContent, DiffViewState, diff_view},
|
component::{
|
||||||
|
diff_view::{DiffViewContent, DiffViewState, diff_view},
|
||||||
|
text_input::{TextInput, text_input},
|
||||||
|
},
|
||||||
query::{self, QueryStatus, read_query, use_query, watch_query},
|
query::{self, QueryStatus, read_query, use_query, watch_query},
|
||||||
util,
|
util,
|
||||||
};
|
};
|
||||||
@@ -16,8 +22,16 @@ pub(crate) struct PullRequestDiffView {
|
|||||||
diff_view_state: DiffViewState,
|
diff_view_state: DiffViewState,
|
||||||
diff_view_content: Option<DiffViewContent>,
|
diff_view_content: Option<DiffViewContent>,
|
||||||
current_file_path: Option<Arc<str>>,
|
current_file_path: Option<Arc<str>>,
|
||||||
|
|
||||||
|
focus_handle: gpui::FocusHandle,
|
||||||
|
is_search_input_visible: bool,
|
||||||
|
search_input: gpui::Entity<TextInput>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gpui::actions!([Search, Escape]);
|
||||||
|
|
||||||
|
const KEY_CONTEXT: &'static str = "PullRequestDiffView";
|
||||||
|
|
||||||
impl PullRequestDiffView {
|
impl PullRequestDiffView {
|
||||||
pub(crate) fn new(pr_id: api::issues::Id, cx: &mut gpui::Context<Self>) -> Self {
|
pub(crate) fn new(pr_id: api::issues::Id, cx: &mut gpui::Context<Self>) -> Self {
|
||||||
let mut s = Self {
|
let mut s = Self {
|
||||||
@@ -26,11 +40,22 @@ impl PullRequestDiffView {
|
|||||||
diff_view_state: DiffViewState::new(),
|
diff_view_state: DiffViewState::new(),
|
||||||
diff_view_content: None,
|
diff_view_content: None,
|
||||||
current_file_path: None,
|
current_file_path: None,
|
||||||
|
|
||||||
|
focus_handle: cx.focus_handle(),
|
||||||
|
is_search_input_visible: false,
|
||||||
|
search_input: cx.new(|cx| TextInput::with_placeholder("Search", cx)),
|
||||||
};
|
};
|
||||||
s.on_create(cx);
|
s.on_create(cx);
|
||||||
s
|
s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn key_bindings() -> [gpui::KeyBinding; 2] {
|
||||||
|
[
|
||||||
|
gpui::KeyBinding::new("cmd-f", Search, Some(KEY_CONTEXT)),
|
||||||
|
gpui::KeyBinding::new("escape", Escape, Some(KEY_CONTEXT)),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn show_diff_for_file(
|
pub(crate) fn show_diff_for_file(
|
||||||
&mut self,
|
&mut self,
|
||||||
file_path: &Arc<str>,
|
file_path: &Arc<str>,
|
||||||
@@ -146,6 +171,24 @@ impl PullRequestDiffView {
|
|||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn open_search_box(&mut self, window: &mut gpui::Window, cx: &mut gpui::Context<Self>) {
|
||||||
|
self.is_search_input_visible = true;
|
||||||
|
cx.focus_view(&self.search_input, window);
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn close_search_box(&mut self, window: &mut gpui::Window, cx: &mut gpui::Context<Self>) {
|
||||||
|
self.is_search_input_visible = false;
|
||||||
|
self.focus_handle.focus(window);
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl gpui::Focusable for PullRequestDiffView {
|
||||||
|
fn focus_handle(&self, _: &gpui::App) -> FocusHandle {
|
||||||
|
self.focus_handle.clone()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl gpui::Render for PullRequestDiffView {
|
impl gpui::Render for PullRequestDiffView {
|
||||||
@@ -154,18 +197,49 @@ impl gpui::Render for PullRequestDiffView {
|
|||||||
_window: &mut gpui::Window,
|
_window: &mut gpui::Window,
|
||||||
cx: &mut gpui::prelude::Context<Self>,
|
cx: &mut gpui::prelude::Context<Self>,
|
||||||
) -> impl gpui::IntoElement {
|
) -> impl gpui::IntoElement {
|
||||||
|
let theme = app::current_theme(cx);
|
||||||
let content_diff = self
|
let content_diff = self
|
||||||
.content_diff_query
|
.content_diff_query
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|q| read_query(q, cx))
|
.map(|q| read_query(q, cx))
|
||||||
.unwrap_or(QueryStatus::Loading);
|
.unwrap_or(QueryStatus::Loading);
|
||||||
|
|
||||||
match (content_diff, &self.diff_view_content) {
|
div()
|
||||||
| (QueryStatus::Loaded(_), Some(content)) => {
|
.id(KEY_CONTEXT)
|
||||||
diff_view(self.diff_view_state.clone(), content.clone()).into_any_element()
|
.key_context(KEY_CONTEXT)
|
||||||
}
|
.track_focus(&self.focus_handle)
|
||||||
|
.flex()
|
||||||
| (_, _) => div().into_any_element(),
|
.flex_col()
|
||||||
}
|
.size_full()
|
||||||
|
.when(self.is_search_input_visible, |it| {
|
||||||
|
it.child(
|
||||||
|
text_input(self.search_input.clone())
|
||||||
|
.w_full()
|
||||||
|
.text_color(theme.colors.text)
|
||||||
|
.text_xs()
|
||||||
|
.px_3()
|
||||||
|
.bg(theme.colors.surface_chrome)
|
||||||
|
.border_0()
|
||||||
|
.border_b_1()
|
||||||
|
.border_color(theme.colors.border_muted),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.when_some(
|
||||||
|
match (content_diff, &self.diff_view_content) {
|
||||||
|
| (QueryStatus::Loaded(_), Some(content)) => Some(content.clone()),
|
||||||
|
| (_, _) => None,
|
||||||
|
},
|
||||||
|
|it, content| {
|
||||||
|
it.child(
|
||||||
|
diff_view(self.diff_view_state.clone(), content.clone()).into_any_element(),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.on_action(cx.listener(|this, _: &Search, window, cx| {
|
||||||
|
this.open_search_box(window, cx);
|
||||||
|
}))
|
||||||
|
.on_action(cx.listener(|this, _: &Escape, window, cx| {
|
||||||
|
this.close_search_box(window, cx);
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
166
src/util/file.rs
166
src/util/file.rs
@@ -43,17 +43,17 @@ pub(crate) fn classify_content(content: &[u8]) -> ContentType {
|
|||||||
ContentType::Text
|
ContentType::Text
|
||||||
} else {
|
} else {
|
||||||
match memchr(0, &content[..content.len().min(8192)]) {
|
match memchr(0, &content[..content.len().min(8192)]) {
|
||||||
| None => ContentType::Text,
|
| None => ContentType::Text,
|
||||||
| Some(_) => ContentType::Binary,
|
| Some(_) => ContentType::Binary,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn file_type_from_path(path: &str) -> FileType {
|
pub(crate) fn file_type_from_path(path: &str) -> FileType {
|
||||||
match Path::new(path).extension().map(|it| it.to_str()).flatten() {
|
match Path::new(path).extension().map(|it| it.to_str()).flatten() {
|
||||||
| Some("rs") => FileType::Rust,
|
| Some("rs") => FileType::Rust,
|
||||||
| Some("js") | Some("jsx") => FileType::JavaScript,
|
| Some("js") | Some("jsx") => FileType::JavaScript,
|
||||||
| _ => FileType::Unknown,
|
| _ => FileType::Unknown,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,18 +71,18 @@ pub(crate) fn line_ranges(content: &[u8]) -> Vec<std::ops::Range<usize>> {
|
|||||||
let c = content[i];
|
let c = content[i];
|
||||||
|
|
||||||
match (c, content.get(i + 1)) {
|
match (c, content.get(i + 1)) {
|
||||||
| (b'\r', Some(b'\n')) => {
|
| (b'\r', Some(b'\n')) => {
|
||||||
// if \r found, check if its \r\n or if its a lone \r
|
// if \r found, check if its \r\n or if its a lone \r
|
||||||
// if \r\n, then treat as one line break
|
// if \r\n, then treat as one line break
|
||||||
ranges.push(line_start..i + 1);
|
ranges.push(line_start..i + 1);
|
||||||
// because we already counted the \n byte, the next iter into it needs to be skipped
|
// because we already counted the \n byte, the next iter into it needs to be skipped
|
||||||
skip_next = true;
|
skip_next = true;
|
||||||
line_start = i + 2;
|
line_start = i + 2;
|
||||||
}
|
}
|
||||||
| _ => {
|
| _ => {
|
||||||
ranges.push(line_start..i);
|
ranges.push(line_start..i);
|
||||||
line_start = i + 1;
|
line_start = i + 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,28 +101,28 @@ pub(crate) fn sort_by_path<T>(mut items: Vec<T>, key: impl Fn(&T) -> &str) -> So
|
|||||||
let b_is_root_file = !b_path.contains('/');
|
let b_is_root_file = !b_path.contains('/');
|
||||||
|
|
||||||
match (a_is_root_file, b_is_root_file) {
|
match (a_is_root_file, b_is_root_file) {
|
||||||
| (true, false) => return std::cmp::Ordering::Greater,
|
| (true, false) => return std::cmp::Ordering::Greater,
|
||||||
| (false, true) => return std::cmp::Ordering::Less,
|
| (false, true) => return std::cmp::Ordering::Less,
|
||||||
| _ => {}
|
| _ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut a_parts = a_path.split('/').peekable();
|
let mut a_parts = a_path.split('/').peekable();
|
||||||
let mut b_parts = b_path.split('/').peekable();
|
let mut b_parts = b_path.split('/').peekable();
|
||||||
loop {
|
loop {
|
||||||
match (a_parts.next(), b_parts.next()) {
|
match (a_parts.next(), b_parts.next()) {
|
||||||
| (Some(a), Some(b)) => {
|
| (Some(a), Some(b)) => {
|
||||||
if a != b {
|
if a != b {
|
||||||
match (a_parts.peek().is_some(), b_parts.peek().is_some()) {
|
match (a_parts.peek().is_some(), b_parts.peek().is_some()) {
|
||||||
| (true, false) => return std::cmp::Ordering::Less,
|
| (true, false) => return std::cmp::Ordering::Less,
|
||||||
| (false, true) => return std::cmp::Ordering::Greater,
|
| (false, true) => return std::cmp::Ordering::Greater,
|
||||||
| _ => {}
|
| _ => {}
|
||||||
|
}
|
||||||
|
return a.cmp(b);
|
||||||
}
|
}
|
||||||
return a.cmp(b);
|
|
||||||
}
|
}
|
||||||
}
|
| (Some(_), None) => return std::cmp::Ordering::Greater,
|
||||||
| (Some(_), None) => return std::cmp::Ordering::Greater,
|
| (None, Some(_)) => return std::cmp::Ordering::Less,
|
||||||
| (None, Some(_)) => return std::cmp::Ordering::Less,
|
| (None, None) => return std::cmp::Ordering::Equal,
|
||||||
| (None, None) => return std::cmp::Ordering::Equal,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -222,67 +222,67 @@ pub(crate) fn build_file_tree<T>(
|
|||||||
for path in paths.0.iter() {
|
for path in paths.0.iter() {
|
||||||
let path = key(path);
|
let path = key(path);
|
||||||
match path.rsplit_once('/') {
|
match path.rsplit_once('/') {
|
||||||
| None => {
|
| None => {
|
||||||
flush_leafs(&mut leafs, &stack, &mut items, emitted_depth, base_depth);
|
flush_leafs(&mut leafs, &stack, &mut items, emitted_depth, base_depth);
|
||||||
stack.clear();
|
stack.clear();
|
||||||
// top level file
|
// top level file
|
||||||
items.push(FileTreeItem {
|
items.push(FileTreeItem {
|
||||||
kind: FileTreeItemKind::File,
|
kind: FileTreeItemKind::File,
|
||||||
full_path: path.into(),
|
full_path: path.into(),
|
||||||
name: path.into(),
|
name: path.into(),
|
||||||
level: 0,
|
level: 0,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
| Some((parent, _)) => {
|
|
||||||
let mut common_depth = 0;
|
|
||||||
|
|
||||||
for (i, seg) in parent.split('/').enumerate() {
|
|
||||||
let stack_item = stack.get(i);
|
|
||||||
if stack_item.is_none() {
|
|
||||||
// segment is unseen, push to stack
|
|
||||||
stack.push(seg);
|
|
||||||
common_depth += 1;
|
|
||||||
} else if Some(&seg) == stack.get(i) {
|
|
||||||
// segment matches stack, continue comparison
|
|
||||||
common_depth += 1;
|
|
||||||
} else {
|
|
||||||
// segment differs from stack, stop comparison
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if common_depth == stack.len() {
|
| Some((parent, _)) => {
|
||||||
// current path is in same directory as stack, add to leafs
|
let mut common_depth = 0;
|
||||||
leafs.push(path);
|
|
||||||
base_depth = common_depth;
|
|
||||||
} else {
|
|
||||||
// e.g. stack = ["a", "b", "c"], path = ["a", "c"]
|
|
||||||
// common dir path = "a/", stack dir path = "a/b/c", common count = 1
|
|
||||||
// push common dir a to items
|
|
||||||
// also push stack dir a/b/c to items but strip a from name so that it becomes "b/c" with level equal to common_count
|
|
||||||
// finally push any leaf under a/b/c
|
|
||||||
|
|
||||||
let base_dir_created =
|
for (i, seg) in parent.split('/').enumerate() {
|
||||||
flush_leafs(&mut leafs, &stack, &mut items, emitted_depth, common_depth);
|
let stack_item = stack.get(i);
|
||||||
|
if stack_item.is_none() {
|
||||||
|
// segment is unseen, push to stack
|
||||||
|
stack.push(seg);
|
||||||
|
common_depth += 1;
|
||||||
|
} else if Some(&seg) == stack.get(i) {
|
||||||
|
// segment matches stack, continue comparison
|
||||||
|
common_depth += 1;
|
||||||
|
} else {
|
||||||
|
// segment differs from stack, stop comparison
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// pop top of stack minus common dir
|
if common_depth == stack.len() {
|
||||||
stack.truncate(common_depth);
|
// current path is in same directory as stack, add to leafs
|
||||||
|
leafs.push(path);
|
||||||
if base_dir_created {
|
base_depth = common_depth;
|
||||||
emitted_depth = common_depth;
|
|
||||||
} else {
|
} else {
|
||||||
emitted_depth = 0;
|
// e.g. stack = ["a", "b", "c"], path = ["a", "c"]
|
||||||
}
|
// common dir path = "a/", stack dir path = "a/b/c", common count = 1
|
||||||
|
// push common dir a to items
|
||||||
|
// also push stack dir a/b/c to items but strip a from name so that it becomes "b/c" with level equal to common_count
|
||||||
|
// finally push any leaf under a/b/c
|
||||||
|
|
||||||
for seg in parent.split('/').skip(common_depth) {
|
let base_dir_created =
|
||||||
stack.push(seg);
|
flush_leafs(&mut leafs, &stack, &mut items, emitted_depth, common_depth);
|
||||||
}
|
|
||||||
|
|
||||||
leafs.push(path);
|
// pop top of stack minus common dir
|
||||||
|
stack.truncate(common_depth);
|
||||||
|
|
||||||
|
if base_dir_created {
|
||||||
|
emitted_depth = common_depth;
|
||||||
|
} else {
|
||||||
|
emitted_depth = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
for seg in parent.split('/').skip(common_depth) {
|
||||||
|
stack.push(seg);
|
||||||
|
}
|
||||||
|
|
||||||
|
leafs.push(path);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
flush_leafs(&mut leafs, &stack, &mut items, emitted_depth, base_depth);
|
flush_leafs(&mut leafs, &stack, &mut items, emitted_depth, base_depth);
|
||||||
|
|||||||
Reference in New Issue
Block a user