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"
|
||||
tree-sitter-highlight = "0.26.9"
|
||||
tree-sitter-rust = "0.24.2"
|
||||
unicode-segmentation = "1.13.2"
|
||||
|
||||
[build-dependencies]
|
||||
serde_json = "1.0.149"
|
||||
|
||||
@@ -318,8 +318,8 @@ impl query::QueryFn for ListPullRequests {
|
||||
}
|
||||
|
||||
let query_string = match self.filter {
|
||||
| Some(filter) => format!("is:pr archived:false sort:updated-desc {}", filter),
|
||||
| None => "is:pr archived:false sort:updated-desc".into(),
|
||||
| Some(filter) => format!("is:pr archived:false sort:updated-desc {}", filter),
|
||||
| None => "is:pr archived:false sort:updated-desc".into(),
|
||||
};
|
||||
|
||||
let gql =
|
||||
@@ -341,20 +341,20 @@ impl query::QueryFn for ListPullRequests {
|
||||
.flatten()
|
||||
.filter_map(|edge| {
|
||||
edge.node.and_then(|n| match n {
|
||||
| PullRequestPaginationQuerySearchEdgesNode::PullRequest(p) => {
|
||||
Some(PullRequest {
|
||||
id: p.id.into(),
|
||||
title: p.title.into(),
|
||||
state: p.state,
|
||||
is_draft: p.is_draft,
|
||||
repo_slug: format!(
|
||||
"{}/{}",
|
||||
p.repository.owner.login, p.repository.name
|
||||
)
|
||||
.into(),
|
||||
})
|
||||
}
|
||||
| _ => None,
|
||||
| PullRequestPaginationQuerySearchEdgesNode::PullRequest(p) => {
|
||||
Some(PullRequest {
|
||||
id: p.id.into(),
|
||||
title: p.title.into(),
|
||||
state: p.state,
|
||||
is_draft: p.is_draft,
|
||||
repo_slug: format!(
|
||||
"{}/{}",
|
||||
p.repository.owner.login, p.repository.name
|
||||
)
|
||||
.into(),
|
||||
})
|
||||
}
|
||||
| _ => None,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
@@ -399,43 +399,43 @@ impl query::QueryFn for FetchPullRequest {
|
||||
"missing 'node' field on PullRequestQuery response".into(),
|
||||
))
|
||||
.and_then(|n| match n {
|
||||
| PullRequestQueryNode::PullRequest(p) => {
|
||||
let created_at =
|
||||
chrono::DateTime::parse_from_rfc3339(&p.created_at).map_err(|err| {
|
||||
api::Error::MalformedResponse(format!(
|
||||
"invalid pull request createdAt {:?}: {err}",
|
||||
p.created_at
|
||||
))
|
||||
})?;
|
||||
| PullRequestQueryNode::PullRequest(p) => {
|
||||
let created_at =
|
||||
chrono::DateTime::parse_from_rfc3339(&p.created_at).map_err(|err| {
|
||||
api::Error::MalformedResponse(format!(
|
||||
"invalid pull request createdAt {:?}: {err}",
|
||||
p.created_at
|
||||
))
|
||||
})?;
|
||||
|
||||
Ok(DetailedPullRequest {
|
||||
id: Id(p.id.into()),
|
||||
title: p.title.into(),
|
||||
state: p.state,
|
||||
is_draft: p.is_draft,
|
||||
body: p.body.into(),
|
||||
author: p.author.map(|it| api::user::Actor {
|
||||
login: it.login.into(),
|
||||
avatar_url: it.avatar_url.into(),
|
||||
}),
|
||||
base_repo_slug: p
|
||||
.base_repository
|
||||
.map(|it| it.name_with_owner.into())
|
||||
.unwrap_or_default(),
|
||||
base_branch_name: p.base_ref_name.into(),
|
||||
base_ref: p.base_ref_oid.into(),
|
||||
head_repo_slug: p
|
||||
.head_repository
|
||||
.map(|it| it.name_with_owner.into())
|
||||
.unwrap_or_default(),
|
||||
head_branch_name: p.head_ref_name.into(),
|
||||
head_ref: p.head_ref_oid.into(),
|
||||
created_at: Some(created_at),
|
||||
})
|
||||
}
|
||||
| _ => Err(api::Error::MalformedResponse(
|
||||
"unexpected node type on PullRequestQuery".into(),
|
||||
)),
|
||||
Ok(DetailedPullRequest {
|
||||
id: Id(p.id.into()),
|
||||
title: p.title.into(),
|
||||
state: p.state,
|
||||
is_draft: p.is_draft,
|
||||
body: p.body.into(),
|
||||
author: p.author.map(|it| api::user::Actor {
|
||||
login: it.login.into(),
|
||||
avatar_url: it.avatar_url.into(),
|
||||
}),
|
||||
base_repo_slug: p
|
||||
.base_repository
|
||||
.map(|it| it.name_with_owner.into())
|
||||
.unwrap_or_default(),
|
||||
base_branch_name: p.base_ref_name.into(),
|
||||
base_ref: p.base_ref_oid.into(),
|
||||
head_repo_slug: p
|
||||
.head_repository
|
||||
.map(|it| it.name_with_owner.into())
|
||||
.unwrap_or_default(),
|
||||
head_branch_name: p.head_ref_name.into(),
|
||||
head_ref: p.head_ref_oid.into(),
|
||||
created_at: Some(created_at),
|
||||
})
|
||||
}
|
||||
| _ => Err(api::Error::MalformedResponse(
|
||||
"unexpected node type on PullRequestQuery".into(),
|
||||
)),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -494,12 +494,12 @@ impl query::QueryFn for FetchPullRequestFileTree {
|
||||
"missing 'node' field on PullRequestFileTreeQuery response".into(),
|
||||
))
|
||||
.and_then(|node| match node {
|
||||
| pull_request_file_tree_query::PullRequestFileTreeQueryNode::PullRequest(
|
||||
pull_request,
|
||||
) => Ok(pull_request),
|
||||
| _ => Err(api::Error::MalformedResponse(
|
||||
"unexpected node type on PullRequestFileTreeQuery".into(),
|
||||
)),
|
||||
| pull_request_file_tree_query::PullRequestFileTreeQueryNode::PullRequest(
|
||||
pull_request,
|
||||
) => Ok(pull_request),
|
||||
| _ => Err(api::Error::MalformedResponse(
|
||||
"unexpected node type on PullRequestFileTreeQuery".into(),
|
||||
)),
|
||||
})?;
|
||||
|
||||
Ok(pull_request
|
||||
@@ -513,23 +513,25 @@ impl query::QueryFn for FetchPullRequestFileTree {
|
||||
edge.node.map(|node| ChangedFile {
|
||||
cursor,
|
||||
change_type: match node.change_type {
|
||||
| pull_request_file_tree_query::PatchStatus::ADDED => ChangeType::Added,
|
||||
| pull_request_file_tree_query::PatchStatus::MODIFIED => {
|
||||
ChangeType::Modified
|
||||
}
|
||||
| pull_request_file_tree_query::PatchStatus::DELETED => {
|
||||
ChangeType::Deleted
|
||||
}
|
||||
| pull_request_file_tree_query::PatchStatus::RENAMED => {
|
||||
ChangeType::Renamed
|
||||
}
|
||||
| pull_request_file_tree_query::PatchStatus::COPIED => {
|
||||
ChangeType::Copied
|
||||
}
|
||||
| pull_request_file_tree_query::PatchStatus::CHANGED => {
|
||||
ChangeType::Changed
|
||||
}
|
||||
| _ => ChangeType::Changed,
|
||||
| pull_request_file_tree_query::PatchStatus::ADDED => {
|
||||
ChangeType::Added
|
||||
}
|
||||
| pull_request_file_tree_query::PatchStatus::MODIFIED => {
|
||||
ChangeType::Modified
|
||||
}
|
||||
| pull_request_file_tree_query::PatchStatus::DELETED => {
|
||||
ChangeType::Deleted
|
||||
}
|
||||
| pull_request_file_tree_query::PatchStatus::RENAMED => {
|
||||
ChangeType::Renamed
|
||||
}
|
||||
| pull_request_file_tree_query::PatchStatus::COPIED => {
|
||||
ChangeType::Copied
|
||||
}
|
||||
| pull_request_file_tree_query::PatchStatus::CHANGED => {
|
||||
ChangeType::Changed
|
||||
}
|
||||
| _ => ChangeType::Changed,
|
||||
},
|
||||
additions: node.additions,
|
||||
deletions: node.deletions,
|
||||
@@ -576,11 +578,11 @@ impl query::QueryFn for FetchPullRequestTimeline {
|
||||
|
||||
TimelineActor {
|
||||
kind: match on {
|
||||
| actorFieldsOn::Bot => "Bot",
|
||||
| actorFieldsOn::EnterpriseUserAccount => "EnterpriseUserAccount",
|
||||
| actorFieldsOn::Mannequin => "Mannequin",
|
||||
| actorFieldsOn::Organization => "Organization",
|
||||
| actorFieldsOn::User => "User",
|
||||
| actorFieldsOn::Bot => "Bot",
|
||||
| actorFieldsOn::EnterpriseUserAccount => "EnterpriseUserAccount",
|
||||
| actorFieldsOn::Mannequin => "Mannequin",
|
||||
| actorFieldsOn::Organization => "Organization",
|
||||
| actorFieldsOn::User => "User",
|
||||
}
|
||||
.into(),
|
||||
name: login,
|
||||
@@ -590,62 +592,62 @@ impl query::QueryFn for FetchPullRequestTimeline {
|
||||
|
||||
fn normalize_assignee(actor: assigneeFields) -> TimelineActor {
|
||||
match actor {
|
||||
| assigneeFields::Bot(actor) => TimelineActor {
|
||||
kind: "Bot".into(),
|
||||
name: actor.login,
|
||||
avatar_url: Some(actor.avatar_url),
|
||||
},
|
||||
| assigneeFields::Mannequin(actor) => TimelineActor {
|
||||
kind: "Mannequin".into(),
|
||||
name: actor.login,
|
||||
avatar_url: Some(actor.avatar_url),
|
||||
},
|
||||
| assigneeFields::Organization(actor) => TimelineActor {
|
||||
kind: "Organization".into(),
|
||||
name: actor.login,
|
||||
avatar_url: Some(actor.avatar_url),
|
||||
},
|
||||
| assigneeFields::User(actor) => TimelineActor {
|
||||
kind: "User".into(),
|
||||
name: actor.login,
|
||||
avatar_url: Some(actor.avatar_url),
|
||||
},
|
||||
| assigneeFields::Bot(actor) => TimelineActor {
|
||||
kind: "Bot".into(),
|
||||
name: actor.login,
|
||||
avatar_url: Some(actor.avatar_url),
|
||||
},
|
||||
| assigneeFields::Mannequin(actor) => TimelineActor {
|
||||
kind: "Mannequin".into(),
|
||||
name: actor.login,
|
||||
avatar_url: Some(actor.avatar_url),
|
||||
},
|
||||
| assigneeFields::Organization(actor) => TimelineActor {
|
||||
kind: "Organization".into(),
|
||||
name: actor.login,
|
||||
avatar_url: Some(actor.avatar_url),
|
||||
},
|
||||
| assigneeFields::User(actor) => TimelineActor {
|
||||
kind: "User".into(),
|
||||
name: actor.login,
|
||||
avatar_url: Some(actor.avatar_url),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_requested_reviewer(actor: requestedReviewerFields) -> TimelineActor {
|
||||
match actor {
|
||||
| requestedReviewerFields::Bot(actor) => TimelineActor {
|
||||
kind: "Bot".into(),
|
||||
name: actor.login,
|
||||
avatar_url: Some(actor.avatar_url),
|
||||
},
|
||||
| requestedReviewerFields::Mannequin(actor) => TimelineActor {
|
||||
kind: "Mannequin".into(),
|
||||
name: actor.login,
|
||||
avatar_url: Some(actor.avatar_url),
|
||||
},
|
||||
| requestedReviewerFields::Team(actor) => TimelineActor {
|
||||
kind: "Team".into(),
|
||||
name: actor.name,
|
||||
avatar_url: None,
|
||||
},
|
||||
| requestedReviewerFields::User(actor) => TimelineActor {
|
||||
kind: "User".into(),
|
||||
name: actor.login,
|
||||
avatar_url: Some(actor.avatar_url),
|
||||
},
|
||||
| requestedReviewerFields::Bot(actor) => TimelineActor {
|
||||
kind: "Bot".into(),
|
||||
name: actor.login,
|
||||
avatar_url: Some(actor.avatar_url),
|
||||
},
|
||||
| requestedReviewerFields::Mannequin(actor) => TimelineActor {
|
||||
kind: "Mannequin".into(),
|
||||
name: actor.login,
|
||||
avatar_url: Some(actor.avatar_url),
|
||||
},
|
||||
| requestedReviewerFields::Team(actor) => TimelineActor {
|
||||
kind: "Team".into(),
|
||||
name: actor.name,
|
||||
avatar_url: None,
|
||||
},
|
||||
| requestedReviewerFields::User(actor) => TimelineActor {
|
||||
kind: "User".into(),
|
||||
name: actor.login,
|
||||
avatar_url: Some(actor.avatar_url),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_review_state(state: PullRequestReviewState) -> String {
|
||||
match state {
|
||||
| PullRequestReviewState::PENDING => "PENDING",
|
||||
| PullRequestReviewState::COMMENTED => "COMMENTED",
|
||||
| PullRequestReviewState::APPROVED => "APPROVED",
|
||||
| PullRequestReviewState::CHANGES_REQUESTED => "CHANGES_REQUESTED",
|
||||
| PullRequestReviewState::DISMISSED => "DISMISSED",
|
||||
| _ => "OTHER",
|
||||
| PullRequestReviewState::PENDING => "PENDING",
|
||||
| PullRequestReviewState::COMMENTED => "COMMENTED",
|
||||
| PullRequestReviewState::APPROVED => "APPROVED",
|
||||
| PullRequestReviewState::CHANGES_REQUESTED => "CHANGES_REQUESTED",
|
||||
| PullRequestReviewState::DISMISSED => "DISMISSED",
|
||||
| _ => "OTHER",
|
||||
}
|
||||
.into()
|
||||
}
|
||||
@@ -865,10 +867,10 @@ impl query::QueryFn for FetchPullRequestTimeline {
|
||||
"missing 'node' field on PullRequestTimelineQuery response".into(),
|
||||
))
|
||||
.and_then(|node| match node {
|
||||
| PullRequestTimelineQueryNode::PullRequest(pull_request) => Ok(pull_request),
|
||||
| _ => Err(api::Error::MalformedResponse(
|
||||
"unexpected node type on PullRequestTimelineQuery".into(),
|
||||
)),
|
||||
| PullRequestTimelineQueryNode::PullRequest(pull_request) => Ok(pull_request),
|
||||
| _ => Err(api::Error::MalformedResponse(
|
||||
"unexpected node type on PullRequestTimelineQuery".into(),
|
||||
)),
|
||||
})?;
|
||||
|
||||
let timeline = pull_request.timeline_items;
|
||||
|
||||
@@ -117,11 +117,13 @@ impl DiffRow {
|
||||
.map(|it| it.to_shared_string());
|
||||
|
||||
let marker = match self.line.op {
|
||||
| util::diff::Op::Equal => code_view::CodeLineMarker::Unchanged,
|
||||
// inserting on new side, so placeholder on old side
|
||||
| util::diff::Op::Insert => code_view::CodeLineMarker::Placeholder,
|
||||
// old side replaced, so delete
|
||||
| util::diff::Op::Replace | util::diff::Op::Delete => code_view::CodeLineMarker::Deleted,
|
||||
| util::diff::Op::Equal => code_view::CodeLineMarker::Unchanged,
|
||||
// inserting on new side, so placeholder on old side
|
||||
| util::diff::Op::Insert => code_view::CodeLineMarker::Placeholder,
|
||||
// old side replaced, so delete
|
||||
| util::diff::Op::Replace | util::diff::Op::Delete => {
|
||||
code_view::CodeLineMarker::Deleted
|
||||
}
|
||||
};
|
||||
|
||||
match self.line.old_line.and_then(|line| {
|
||||
@@ -130,14 +132,14 @@ impl DiffRow {
|
||||
.as_ref()
|
||||
.map(|it| it.highlights_at_line(line))
|
||||
}) {
|
||||
| Some(highlights) => code_line_with_highlights(
|
||||
self.line.old_line,
|
||||
content,
|
||||
highlights.iter().cloned(),
|
||||
marker,
|
||||
),
|
||||
| Some(highlights) => code_line_with_highlights(
|
||||
self.line.old_line,
|
||||
content,
|
||||
highlights.iter().cloned(),
|
||||
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());
|
||||
|
||||
let marker = match self.line.op {
|
||||
| util::diff::Op::Equal => code_view::CodeLineMarker::Unchanged,
|
||||
| util::diff::Op::Insert | util::diff::Op::Replace => code_view::CodeLineMarker::Added,
|
||||
| util::diff::Op::Delete => code_view::CodeLineMarker::Deleted,
|
||||
| util::diff::Op::Equal => code_view::CodeLineMarker::Unchanged,
|
||||
| util::diff::Op::Insert | util::diff::Op::Replace => code_view::CodeLineMarker::Added,
|
||||
| util::diff::Op::Delete => code_view::CodeLineMarker::Deleted,
|
||||
};
|
||||
|
||||
match self.line.new_line.and_then(|line| {
|
||||
@@ -162,14 +164,14 @@ impl DiffRow {
|
||||
.as_ref()
|
||||
.map(|it| it.highlights_at_line(line))
|
||||
}) {
|
||||
| Some(highlights) => code_line_with_highlights(
|
||||
self.line.new_line,
|
||||
content,
|
||||
highlights.iter().cloned(),
|
||||
marker,
|
||||
),
|
||||
| Some(highlights) => code_line_with_highlights(
|
||||
self.line.new_line,
|
||||
content,
|
||||
highlights.iter().cloned(),
|
||||
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 segmented_control;
|
||||
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 component;
|
||||
mod http;
|
||||
mod keyboard;
|
||||
mod query;
|
||||
mod screen;
|
||||
mod storage;
|
||||
@@ -62,6 +63,8 @@ fn setup_application(cx: &mut gpui::App) {
|
||||
cx.set_global(global);
|
||||
cx.set_global(query_store);
|
||||
|
||||
keyboard::attach_key_binds(cx);
|
||||
|
||||
if diffops_playground::is_enabled() {
|
||||
_ = diffops_playground::open_window(cx);
|
||||
return;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
mod issue_list;
|
||||
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_view;
|
||||
mod screen;
|
||||
|
||||
@@ -60,22 +60,28 @@ impl PullRequestChangeView {
|
||||
.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];
|
||||
match item.kind {
|
||||
| FileTreeItemKind::Directory => {
|
||||
self.file_tree_state
|
||||
.toggle_directory(&item.full_path, &self.file_tree_items);
|
||||
cx.notify();
|
||||
}
|
||||
| FileTreeItemKind::File => {
|
||||
self.selected_file_path = Some(Arc::clone(&item.full_path));
|
||||
self.file_tree_state.highlight_item(i);
|
||||
self.diff_view.update(cx, |diff_view, cx| {
|
||||
diff_view.show_diff_for_file(&item.full_path, cx);
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
| FileTreeItemKind::Directory => {
|
||||
self.file_tree_state
|
||||
.toggle_directory(&item.full_path, &self.file_tree_items);
|
||||
cx.notify();
|
||||
}
|
||||
| FileTreeItemKind::File => {
|
||||
self.selected_file_path = Some(Arc::clone(&item.full_path));
|
||||
self.file_tree_state.highlight_item(i);
|
||||
self.diff_view.update(cx, |diff_view, cx| {
|
||||
diff_view.show_diff_for_file(&item.full_path, cx);
|
||||
});
|
||||
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| {
|
||||
weak.read(cx).file_tree_items[i].clone()
|
||||
})
|
||||
.on_item_click(cx.listener(|this, i, _, cx| {
|
||||
this.handle_file_tree_item_click(*i, cx);
|
||||
})),
|
||||
.on_item_click(cx.listener(
|
||||
|this, i, window, cx| {
|
||||
this.handle_file_tree_item_click(*i, window, cx);
|
||||
},
|
||||
)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use gpui::{AppContext, IntoElement, div};
|
||||
use gpui::{
|
||||
AppContext, FocusHandle, InteractiveElement, IntoElement, ParentElement, Styled, div,
|
||||
prelude::FluentBuilder,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
api::{self},
|
||||
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},
|
||||
util,
|
||||
};
|
||||
@@ -16,8 +22,16 @@ pub(crate) struct PullRequestDiffView {
|
||||
diff_view_state: DiffViewState,
|
||||
diff_view_content: Option<DiffViewContent>,
|
||||
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 {
|
||||
pub(crate) fn new(pr_id: api::issues::Id, cx: &mut gpui::Context<Self>) -> Self {
|
||||
let mut s = Self {
|
||||
@@ -26,11 +40,22 @@ impl PullRequestDiffView {
|
||||
diff_view_state: DiffViewState::new(),
|
||||
diff_view_content: 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
|
||||
}
|
||||
|
||||
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(
|
||||
&mut self,
|
||||
file_path: &Arc<str>,
|
||||
@@ -146,6 +171,24 @@ impl PullRequestDiffView {
|
||||
.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 {
|
||||
@@ -154,18 +197,49 @@ impl gpui::Render for PullRequestDiffView {
|
||||
_window: &mut gpui::Window,
|
||||
cx: &mut gpui::prelude::Context<Self>,
|
||||
) -> impl gpui::IntoElement {
|
||||
let theme = app::current_theme(cx);
|
||||
let content_diff = self
|
||||
.content_diff_query
|
||||
.as_ref()
|
||||
.map(|q| read_query(q, cx))
|
||||
.unwrap_or(QueryStatus::Loading);
|
||||
|
||||
match (content_diff, &self.diff_view_content) {
|
||||
| (QueryStatus::Loaded(_), Some(content)) => {
|
||||
diff_view(self.diff_view_state.clone(), content.clone()).into_any_element()
|
||||
}
|
||||
|
||||
| (_, _) => div().into_any_element(),
|
||||
}
|
||||
div()
|
||||
.id(KEY_CONTEXT)
|
||||
.key_context(KEY_CONTEXT)
|
||||
.track_focus(&self.focus_handle)
|
||||
.flex()
|
||||
.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
|
||||
} else {
|
||||
match memchr(0, &content[..content.len().min(8192)]) {
|
||||
| None => ContentType::Text,
|
||||
| Some(_) => ContentType::Binary,
|
||||
| None => ContentType::Text,
|
||||
| Some(_) => ContentType::Binary,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn file_type_from_path(path: &str) -> FileType {
|
||||
match Path::new(path).extension().map(|it| it.to_str()).flatten() {
|
||||
| Some("rs") => FileType::Rust,
|
||||
| Some("js") | Some("jsx") => FileType::JavaScript,
|
||||
| _ => FileType::Unknown,
|
||||
| Some("rs") => FileType::Rust,
|
||||
| Some("js") | Some("jsx") => FileType::JavaScript,
|
||||
| _ => FileType::Unknown,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,18 +71,18 @@ pub(crate) fn line_ranges(content: &[u8]) -> Vec<std::ops::Range<usize>> {
|
||||
let c = content[i];
|
||||
|
||||
match (c, content.get(i + 1)) {
|
||||
| (b'\r', Some(b'\n')) => {
|
||||
// if \r found, check if its \r\n or if its a lone \r
|
||||
// if \r\n, then treat as one line break
|
||||
ranges.push(line_start..i + 1);
|
||||
// because we already counted the \n byte, the next iter into it needs to be skipped
|
||||
skip_next = true;
|
||||
line_start = i + 2;
|
||||
}
|
||||
| _ => {
|
||||
ranges.push(line_start..i);
|
||||
line_start = i + 1;
|
||||
}
|
||||
| (b'\r', Some(b'\n')) => {
|
||||
// if \r found, check if its \r\n or if its a lone \r
|
||||
// if \r\n, then treat as one line break
|
||||
ranges.push(line_start..i + 1);
|
||||
// because we already counted the \n byte, the next iter into it needs to be skipped
|
||||
skip_next = true;
|
||||
line_start = i + 2;
|
||||
}
|
||||
| _ => {
|
||||
ranges.push(line_start..i);
|
||||
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('/');
|
||||
|
||||
match (a_is_root_file, b_is_root_file) {
|
||||
| (true, false) => return std::cmp::Ordering::Greater,
|
||||
| (false, true) => return std::cmp::Ordering::Less,
|
||||
| _ => {}
|
||||
| (true, false) => return std::cmp::Ordering::Greater,
|
||||
| (false, true) => return std::cmp::Ordering::Less,
|
||||
| _ => {}
|
||||
}
|
||||
|
||||
let mut a_parts = a_path.split('/').peekable();
|
||||
let mut b_parts = b_path.split('/').peekable();
|
||||
loop {
|
||||
match (a_parts.next(), b_parts.next()) {
|
||||
| (Some(a), Some(b)) => {
|
||||
if a != b {
|
||||
match (a_parts.peek().is_some(), b_parts.peek().is_some()) {
|
||||
| (true, false) => return std::cmp::Ordering::Less,
|
||||
| (false, true) => return std::cmp::Ordering::Greater,
|
||||
| _ => {}
|
||||
| (Some(a), Some(b)) => {
|
||||
if a != b {
|
||||
match (a_parts.peek().is_some(), b_parts.peek().is_some()) {
|
||||
| (true, false) => return std::cmp::Ordering::Less,
|
||||
| (false, true) => return std::cmp::Ordering::Greater,
|
||||
| _ => {}
|
||||
}
|
||||
return a.cmp(b);
|
||||
}
|
||||
return a.cmp(b);
|
||||
}
|
||||
}
|
||||
| (Some(_), None) => return std::cmp::Ordering::Greater,
|
||||
| (None, Some(_)) => return std::cmp::Ordering::Less,
|
||||
| (None, None) => return std::cmp::Ordering::Equal,
|
||||
| (Some(_), None) => return std::cmp::Ordering::Greater,
|
||||
| (None, Some(_)) => return std::cmp::Ordering::Less,
|
||||
| (None, None) => return std::cmp::Ordering::Equal,
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -222,67 +222,67 @@ pub(crate) fn build_file_tree<T>(
|
||||
for path in paths.0.iter() {
|
||||
let path = key(path);
|
||||
match path.rsplit_once('/') {
|
||||
| None => {
|
||||
flush_leafs(&mut leafs, &stack, &mut items, emitted_depth, base_depth);
|
||||
stack.clear();
|
||||
// top level file
|
||||
items.push(FileTreeItem {
|
||||
kind: FileTreeItemKind::File,
|
||||
full_path: path.into(),
|
||||
name: path.into(),
|
||||
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;
|
||||
}
|
||||
| None => {
|
||||
flush_leafs(&mut leafs, &stack, &mut items, emitted_depth, base_depth);
|
||||
stack.clear();
|
||||
// top level file
|
||||
items.push(FileTreeItem {
|
||||
kind: FileTreeItemKind::File,
|
||||
full_path: path.into(),
|
||||
name: path.into(),
|
||||
level: 0,
|
||||
});
|
||||
}
|
||||
|
||||
if common_depth == stack.len() {
|
||||
// current path is in same directory as stack, add to leafs
|
||||
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
|
||||
| Some((parent, _)) => {
|
||||
let mut common_depth = 0;
|
||||
|
||||
let base_dir_created =
|
||||
flush_leafs(&mut leafs, &stack, &mut items, emitted_depth, common_depth);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// pop top of stack minus common dir
|
||||
stack.truncate(common_depth);
|
||||
|
||||
if base_dir_created {
|
||||
emitted_depth = common_depth;
|
||||
if common_depth == stack.len() {
|
||||
// current path is in same directory as stack, add to leafs
|
||||
leafs.push(path);
|
||||
base_depth = common_depth;
|
||||
} 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) {
|
||||
stack.push(seg);
|
||||
}
|
||||
let base_dir_created =
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user