feat: add dummy search bar in diff view

This commit is contained in:
2026-05-31 00:45:25 +01:00
parent 4a7ebf660e
commit f5ebb210ac
11 changed files with 1273 additions and 262 deletions

View File

@@ -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;

View File

@@ -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),
}
}
}

View File

@@ -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
View 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
View 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());
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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(

View File

@@ -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);
}))
}
}

View File

@@ -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);