feat: add dummy search bar in diff view
This commit is contained in:
@@ -25,6 +25,7 @@ memchr = "2.8.0"
|
|||||||
thiserror = "2.0.18"
|
thiserror = "2.0.18"
|
||||||
tree-sitter-highlight = "0.26.9"
|
tree-sitter-highlight = "0.26.9"
|
||||||
tree-sitter-rust = "0.24.2"
|
tree-sitter-rust = "0.24.2"
|
||||||
|
unicode-segmentation = "1.13.2"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
serde_json = "1.0.149"
|
serde_json = "1.0.149"
|
||||||
|
|||||||
@@ -513,7 +513,9 @@ impl query::QueryFn for FetchPullRequestFileTree {
|
|||||||
edge.node.map(|node| ChangedFile {
|
edge.node.map(|node| ChangedFile {
|
||||||
cursor,
|
cursor,
|
||||||
change_type: match node.change_type {
|
change_type: match node.change_type {
|
||||||
| pull_request_file_tree_query::PatchStatus::ADDED => ChangeType::Added,
|
| pull_request_file_tree_query::PatchStatus::ADDED => {
|
||||||
|
ChangeType::Added
|
||||||
|
}
|
||||||
| pull_request_file_tree_query::PatchStatus::MODIFIED => {
|
| pull_request_file_tree_query::PatchStatus::MODIFIED => {
|
||||||
ChangeType::Modified
|
ChangeType::Modified
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,7 +121,9 @@ impl DiffRow {
|
|||||||
// inserting on new side, so placeholder on old side
|
// inserting on new side, so placeholder on old side
|
||||||
| util::diff::Op::Insert => code_view::CodeLineMarker::Placeholder,
|
| util::diff::Op::Insert => code_view::CodeLineMarker::Placeholder,
|
||||||
// old side replaced, so delete
|
// old side replaced, so delete
|
||||||
| util::diff::Op::Replace | util::diff::Op::Delete => code_view::CodeLineMarker::Deleted,
|
| util::diff::Op::Replace | util::diff::Op::Delete => {
|
||||||
|
code_view::CodeLineMarker::Deleted
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
match self.line.old_line.and_then(|line| {
|
match self.line.old_line.and_then(|line| {
|
||||||
|
|||||||
@@ -6,3 +6,4 @@ pub(crate) mod font_icon;
|
|||||||
pub(crate) mod markdown;
|
pub(crate) mod markdown;
|
||||||
pub(crate) mod segmented_control;
|
pub(crate) mod segmented_control;
|
||||||
pub(crate) mod text;
|
pub(crate) mod text;
|
||||||
|
pub(crate) mod text_input;
|
||||||
|
|||||||
911
src/component/text_input.rs
Normal file
911
src/component/text_input.rs
Normal file
@@ -0,0 +1,911 @@
|
|||||||
|
use std::ops::Range;
|
||||||
|
|
||||||
|
use gpui::{
|
||||||
|
App, Bounds, Context, CursorStyle, Element, ElementId, ElementInputHandler, Entity,
|
||||||
|
EntityInputHandler, FocusHandle, Focusable, GlobalElementId, InteractiveElement, IntoElement,
|
||||||
|
LayoutId, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement,
|
||||||
|
Pixels, Point, Refineable, ShapedLine, SharedString, Style, Styled, TextRun, UTF16Selection,
|
||||||
|
UnderlineStyle, Window, div, fill, point, px, relative, size,
|
||||||
|
};
|
||||||
|
use unicode_segmentation::UnicodeSegmentation;
|
||||||
|
|
||||||
|
use crate::app;
|
||||||
|
|
||||||
|
gpui::actions!(
|
||||||
|
text_input,
|
||||||
|
[
|
||||||
|
Backspace,
|
||||||
|
BackspaceWord,
|
||||||
|
Delete,
|
||||||
|
Left,
|
||||||
|
WordLeft,
|
||||||
|
SelectLeft,
|
||||||
|
SelectWordLeft,
|
||||||
|
Right,
|
||||||
|
SelectRight,
|
||||||
|
WordRight,
|
||||||
|
SelectWordRight,
|
||||||
|
LineStart,
|
||||||
|
SelectToLineStart,
|
||||||
|
LineEnd,
|
||||||
|
SelectToLineEnd,
|
||||||
|
SelectAll,
|
||||||
|
Home,
|
||||||
|
End,
|
||||||
|
Paste,
|
||||||
|
Cut,
|
||||||
|
Copy,
|
||||||
|
Submit,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
const KEY_CONTEXT: &str = "TextInput";
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub(crate) enum Event {
|
||||||
|
Changed(SharedString),
|
||||||
|
Submitted(SharedString),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct TextInput {
|
||||||
|
focus_handle: FocusHandle,
|
||||||
|
content: SharedString,
|
||||||
|
placeholder: SharedString,
|
||||||
|
selected_range: Range<usize>,
|
||||||
|
selection_reversed: bool,
|
||||||
|
marked_range: Option<Range<usize>>,
|
||||||
|
last_layout: Option<ShapedLine>,
|
||||||
|
last_bounds: Option<Bounds<Pixels>>,
|
||||||
|
is_selecting: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(gpui::IntoElement)]
|
||||||
|
pub(crate) struct TextInputView {
|
||||||
|
input: Entity<TextInput>,
|
||||||
|
style: gpui::StyleRefinement,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub(crate) fn text_input(input: Entity<TextInput>) -> TextInputView {
|
||||||
|
TextInputView {
|
||||||
|
input,
|
||||||
|
style: gpui::StyleRefinement::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
impl TextInput {
|
||||||
|
pub(crate) fn new(cx: &mut Context<Self>) -> Self {
|
||||||
|
Self::with_placeholder("", cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn with_placeholder(
|
||||||
|
placeholder: impl Into<SharedString>,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
focus_handle: cx.focus_handle(),
|
||||||
|
content: "".into(),
|
||||||
|
placeholder: placeholder.into(),
|
||||||
|
selected_range: 0..0,
|
||||||
|
selection_reversed: false,
|
||||||
|
marked_range: None,
|
||||||
|
last_layout: None,
|
||||||
|
last_bounds: None,
|
||||||
|
is_selecting: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn key_bindings() -> [gpui::KeyBinding; 26] {
|
||||||
|
[
|
||||||
|
gpui::KeyBinding::new("backspace", Backspace, Some(KEY_CONTEXT)),
|
||||||
|
gpui::KeyBinding::new("alt-backspace", BackspaceWord, Some(KEY_CONTEXT)),
|
||||||
|
gpui::KeyBinding::new("delete", Delete, Some(KEY_CONTEXT)),
|
||||||
|
//
|
||||||
|
gpui::KeyBinding::new("left", Left, Some(KEY_CONTEXT)),
|
||||||
|
gpui::KeyBinding::new("shift-left", SelectLeft, Some(KEY_CONTEXT)),
|
||||||
|
gpui::KeyBinding::new("alt-left", WordLeft, Some(KEY_CONTEXT)),
|
||||||
|
gpui::KeyBinding::new("alt-shift-left", SelectWordLeft, Some(KEY_CONTEXT)),
|
||||||
|
gpui::KeyBinding::new("cmd-left", LineStart, Some(KEY_CONTEXT)),
|
||||||
|
gpui::KeyBinding::new("shift-cmd-left", SelectToLineStart, Some(KEY_CONTEXT)),
|
||||||
|
//
|
||||||
|
gpui::KeyBinding::new("right", Right, Some(KEY_CONTEXT)),
|
||||||
|
gpui::KeyBinding::new("shift-right", SelectRight, Some(KEY_CONTEXT)),
|
||||||
|
gpui::KeyBinding::new("alt-right", WordRight, Some(KEY_CONTEXT)),
|
||||||
|
gpui::KeyBinding::new("alt-shift-right", SelectWordRight, Some(KEY_CONTEXT)),
|
||||||
|
gpui::KeyBinding::new("cmd-right", LineEnd, Some(KEY_CONTEXT)),
|
||||||
|
gpui::KeyBinding::new("shift-cmd-right", SelectToLineEnd, Some(KEY_CONTEXT)),
|
||||||
|
//
|
||||||
|
gpui::KeyBinding::new("cmd-a", SelectAll, Some(KEY_CONTEXT)),
|
||||||
|
gpui::KeyBinding::new("ctrl-a", SelectAll, Some(KEY_CONTEXT)),
|
||||||
|
gpui::KeyBinding::new("cmd-v", Paste, Some(KEY_CONTEXT)),
|
||||||
|
gpui::KeyBinding::new("ctrl-v", Paste, Some(KEY_CONTEXT)),
|
||||||
|
gpui::KeyBinding::new("cmd-c", Copy, Some(KEY_CONTEXT)),
|
||||||
|
gpui::KeyBinding::new("ctrl-c", Copy, Some(KEY_CONTEXT)),
|
||||||
|
gpui::KeyBinding::new("cmd-x", Cut, Some(KEY_CONTEXT)),
|
||||||
|
gpui::KeyBinding::new("ctrl-x", Cut, Some(KEY_CONTEXT)),
|
||||||
|
gpui::KeyBinding::new("home", Home, Some(KEY_CONTEXT)),
|
||||||
|
gpui::KeyBinding::new("end", End, Some(KEY_CONTEXT)),
|
||||||
|
gpui::KeyBinding::new("enter", Submit, Some(KEY_CONTEXT)),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn content(&self) -> &str {
|
||||||
|
&self.content
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_content(&mut self, content: impl Into<SharedString>, cx: &mut Context<Self>) {
|
||||||
|
let content = content.into();
|
||||||
|
let changed = self.content != content;
|
||||||
|
self.content = content;
|
||||||
|
self.selected_range = self.content.len()..self.content.len();
|
||||||
|
self.selection_reversed = false;
|
||||||
|
self.marked_range = None;
|
||||||
|
cx.notify();
|
||||||
|
|
||||||
|
if changed {
|
||||||
|
cx.emit(Event::Changed(self.content.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn clear(&mut self, cx: &mut Context<Self>) {
|
||||||
|
self.set_content("", cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn left(&mut self, _: &Left, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if self.selected_range.is_empty() {
|
||||||
|
self.move_to(self.previous_boundary(self.cursor_offset()), cx);
|
||||||
|
} else {
|
||||||
|
self.move_to(self.selected_range.start, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn word_left(&mut self, _: &WordLeft, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if self.selected_range.is_empty() {
|
||||||
|
self.move_to(self.previous_word_boundary(self.cursor_offset()), cx);
|
||||||
|
} else {
|
||||||
|
self.move_to(self.selected_range.start, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn right(&mut self, _: &Right, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if self.selected_range.is_empty() {
|
||||||
|
self.move_to(self.next_boundary(self.cursor_offset()), cx);
|
||||||
|
} else {
|
||||||
|
self.move_to(self.selected_range.end, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn word_right(&mut self, _: &WordRight, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if self.selected_range.is_empty() {
|
||||||
|
self.move_to(self.next_word_boundary(self.cursor_offset()), cx);
|
||||||
|
} else {
|
||||||
|
self.move_to(self.selected_range.end, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn select_left(&mut self, _: &SelectLeft, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.select_to(self.previous_boundary(self.cursor_offset()), cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn select_word_left(&mut self, _: &SelectWordLeft, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.select_to(self.previous_word_boundary(self.cursor_offset()), cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn select_right(&mut self, _: &SelectRight, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.select_to(self.next_boundary(self.cursor_offset()), cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn select_word_right(&mut self, _: &SelectWordRight, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.select_to(self.next_word_boundary(self.cursor_offset()), cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn line_start(&mut self, _: &LineStart, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.move_to(0, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn select_to_line_start(
|
||||||
|
&mut self,
|
||||||
|
_: &SelectToLineStart,
|
||||||
|
_: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.select_to(0, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn select_to_line_end(&mut self, _: &SelectToLineEnd, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.select_to(self.content.len(), cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn line_end(&mut self, _: &LineEnd, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.move_to(self.content().len(), cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn select_all(&mut self, _: &SelectAll, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.move_to(0, cx);
|
||||||
|
self.select_to(self.content.len(), cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn home(&mut self, _: &Home, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.move_to(0, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn end(&mut self, _: &End, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.move_to(self.content.len(), cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn backspace(&mut self, _: &Backspace, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if self.selected_range.is_empty() {
|
||||||
|
self.select_to(self.previous_boundary(self.cursor_offset()), cx);
|
||||||
|
}
|
||||||
|
self.replace_text_in_range(None, "", window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn backspace_word(&mut self, _: &BackspaceWord, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if self.selected_range.is_empty() {
|
||||||
|
self.select_to(self.previous_word_boundary(self.cursor_offset()), cx);
|
||||||
|
}
|
||||||
|
self.replace_text_in_range(None, "", window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete(&mut self, _: &Delete, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if self.selected_range.is_empty() {
|
||||||
|
self.select_to(self.next_boundary(self.cursor_offset()), cx);
|
||||||
|
}
|
||||||
|
self.replace_text_in_range(None, "", window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if let Some(text) = cx.read_from_clipboard().and_then(|item| item.text()) {
|
||||||
|
self.replace_text_in_range(None, &text.replace('\n', " "), window, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn copy(&mut self, _: &Copy, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if !self.selected_range.is_empty() {
|
||||||
|
cx.write_to_clipboard(gpui::ClipboardItem::new_string(
|
||||||
|
self.content[self.selected_range.clone()].to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cut(&mut self, _: &Cut, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if !self.selected_range.is_empty() {
|
||||||
|
cx.write_to_clipboard(gpui::ClipboardItem::new_string(
|
||||||
|
self.content[self.selected_range.clone()].to_string(),
|
||||||
|
));
|
||||||
|
self.replace_text_in_range(None, "", window, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn submit(&mut self, _: &Submit, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
cx.emit(Event::Submitted(self.content.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_mouse_down(
|
||||||
|
&mut self,
|
||||||
|
event: &MouseDownEvent,
|
||||||
|
_window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.is_selecting = true;
|
||||||
|
|
||||||
|
if event.modifiers.shift {
|
||||||
|
self.select_to(self.index_for_mouse_position(event.position), cx);
|
||||||
|
} else {
|
||||||
|
self.move_to(self.index_for_mouse_position(event.position), cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_mouse_up(&mut self, _: &MouseUpEvent, _window: &mut Window, _: &mut Context<Self>) {
|
||||||
|
self.is_selecting = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_mouse_move(&mut self, event: &MouseMoveEvent, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if self.is_selecting {
|
||||||
|
self.select_to(self.index_for_mouse_position(event.position), cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn move_to(&mut self, offset: usize, cx: &mut Context<Self>) {
|
||||||
|
let offset = offset.min(self.content.len());
|
||||||
|
self.selected_range = offset..offset;
|
||||||
|
self.selection_reversed = false;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cursor_offset(&self) -> usize {
|
||||||
|
if self.selection_reversed {
|
||||||
|
self.selected_range.start
|
||||||
|
} else {
|
||||||
|
self.selected_range.end
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn index_for_mouse_position(&self, position: Point<Pixels>) -> usize {
|
||||||
|
if self.content.is_empty() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (Some(bounds), Some(line)) = (self.last_bounds.as_ref(), self.last_layout.as_ref())
|
||||||
|
else {
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
if position.y < bounds.top() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if position.y > bounds.bottom() {
|
||||||
|
return self.content.len();
|
||||||
|
}
|
||||||
|
|
||||||
|
line.closest_index_for_x(position.x - bounds.left())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn select_to(&mut self, offset: usize, cx: &mut Context<Self>) {
|
||||||
|
let offset = offset.min(self.content.len());
|
||||||
|
if self.selection_reversed {
|
||||||
|
self.selected_range.start = offset;
|
||||||
|
} else {
|
||||||
|
self.selected_range.end = offset;
|
||||||
|
}
|
||||||
|
if self.selected_range.end < self.selected_range.start {
|
||||||
|
self.selection_reversed = !self.selection_reversed;
|
||||||
|
self.selected_range = self.selected_range.end..self.selected_range.start;
|
||||||
|
}
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn range_to_utf16(&self, range: &Range<usize>) -> Range<usize> {
|
||||||
|
offset_to_utf16(&self.content, range.start)..offset_to_utf16(&self.content, range.end)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn range_from_utf16(&self, range_utf16: &Range<usize>) -> Range<usize> {
|
||||||
|
range_from_utf16(&self.content, range_utf16)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn previous_boundary(&self, offset: usize) -> usize {
|
||||||
|
self.content
|
||||||
|
.grapheme_indices(true)
|
||||||
|
.rev()
|
||||||
|
.find_map(|(idx, _)| (idx < offset).then_some(idx))
|
||||||
|
.unwrap_or(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn previous_word_boundary(&self, offset: usize) -> usize {
|
||||||
|
self.content
|
||||||
|
.split_word_bound_indices()
|
||||||
|
.rev()
|
||||||
|
.find_map(|(idx, word)| (idx < offset && !word.trim().is_empty()).then_some(idx))
|
||||||
|
.unwrap_or(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_boundary(&self, offset: usize) -> usize {
|
||||||
|
self.content
|
||||||
|
.grapheme_indices(true)
|
||||||
|
.find_map(|(idx, _)| (idx > offset).then_some(idx))
|
||||||
|
.unwrap_or(self.content.len())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_word_boundary(&self, offset: usize) -> usize {
|
||||||
|
self.content
|
||||||
|
.split_word_bound_indices()
|
||||||
|
.find_map(|(idx, word)| {
|
||||||
|
let word_end = idx + word.len();
|
||||||
|
(word_end > offset && !word.trim().is_empty()).then_some(word_end)
|
||||||
|
})
|
||||||
|
.unwrap_or(offset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EntityInputHandler for TextInput {
|
||||||
|
fn text_for_range(
|
||||||
|
&mut self,
|
||||||
|
range_utf16: Range<usize>,
|
||||||
|
actual_range: &mut Option<Range<usize>>,
|
||||||
|
_window: &mut Window,
|
||||||
|
_cx: &mut Context<Self>,
|
||||||
|
) -> Option<String> {
|
||||||
|
let range = self.range_from_utf16(&range_utf16);
|
||||||
|
actual_range.replace(self.range_to_utf16(&range));
|
||||||
|
Some(self.content[range].to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn selected_text_range(
|
||||||
|
&mut self,
|
||||||
|
_ignore_disabled_input: bool,
|
||||||
|
_window: &mut Window,
|
||||||
|
_cx: &mut Context<Self>,
|
||||||
|
) -> Option<UTF16Selection> {
|
||||||
|
Some(UTF16Selection {
|
||||||
|
range: self.range_to_utf16(&self.selected_range),
|
||||||
|
reversed: self.selection_reversed,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn marked_text_range(
|
||||||
|
&self,
|
||||||
|
_window: &mut Window,
|
||||||
|
_cx: &mut Context<Self>,
|
||||||
|
) -> Option<Range<usize>> {
|
||||||
|
self.marked_range
|
||||||
|
.as_ref()
|
||||||
|
.map(|range| self.range_to_utf16(range))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unmark_text(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {
|
||||||
|
self.marked_range = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn replace_text_in_range(
|
||||||
|
&mut self,
|
||||||
|
range_utf16: Option<Range<usize>>,
|
||||||
|
new_text: &str,
|
||||||
|
_window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
let range = range_utf16
|
||||||
|
.as_ref()
|
||||||
|
.map(|range_utf16| self.range_from_utf16(range_utf16))
|
||||||
|
.or(self.marked_range.clone())
|
||||||
|
.unwrap_or(self.selected_range.clone());
|
||||||
|
|
||||||
|
let old_content = self.content.clone();
|
||||||
|
self.content =
|
||||||
|
(self.content[0..range.start].to_owned() + new_text + &self.content[range.end..])
|
||||||
|
.into();
|
||||||
|
let cursor = range.start + new_text.len();
|
||||||
|
self.selected_range = cursor..cursor;
|
||||||
|
self.selection_reversed = false;
|
||||||
|
self.marked_range.take();
|
||||||
|
cx.notify();
|
||||||
|
|
||||||
|
if self.content != old_content {
|
||||||
|
cx.emit(Event::Changed(self.content.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn replace_and_mark_text_in_range(
|
||||||
|
&mut self,
|
||||||
|
range_utf16: Option<Range<usize>>,
|
||||||
|
new_text: &str,
|
||||||
|
new_selected_range_utf16: Option<Range<usize>>,
|
||||||
|
_window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
let range = range_utf16
|
||||||
|
.as_ref()
|
||||||
|
.map(|range_utf16| self.range_from_utf16(range_utf16))
|
||||||
|
.or(self.marked_range.clone())
|
||||||
|
.unwrap_or(self.selected_range.clone());
|
||||||
|
|
||||||
|
let old_content = self.content.clone();
|
||||||
|
self.content =
|
||||||
|
(self.content[0..range.start].to_owned() + new_text + &self.content[range.end..])
|
||||||
|
.into();
|
||||||
|
self.marked_range =
|
||||||
|
(!new_text.is_empty()).then_some(range.start..range.start + new_text.len());
|
||||||
|
self.selected_range = new_selected_range_utf16
|
||||||
|
.as_ref()
|
||||||
|
.map(|range_utf16| range_from_utf16(new_text, range_utf16))
|
||||||
|
.map(|new_range| range.start + new_range.start..range.start + new_range.end)
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
let cursor = range.start + new_text.len();
|
||||||
|
cursor..cursor
|
||||||
|
});
|
||||||
|
self.selection_reversed = false;
|
||||||
|
cx.notify();
|
||||||
|
|
||||||
|
if self.content != old_content {
|
||||||
|
cx.emit(Event::Changed(self.content.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bounds_for_range(
|
||||||
|
&mut self,
|
||||||
|
range_utf16: Range<usize>,
|
||||||
|
bounds: Bounds<Pixels>,
|
||||||
|
_window: &mut Window,
|
||||||
|
_cx: &mut Context<Self>,
|
||||||
|
) -> Option<Bounds<Pixels>> {
|
||||||
|
let last_layout = self.last_layout.as_ref()?;
|
||||||
|
let range = self.range_from_utf16(&range_utf16);
|
||||||
|
Some(Bounds::from_corners(
|
||||||
|
point(
|
||||||
|
bounds.left() + last_layout.x_for_index(range.start),
|
||||||
|
bounds.top(),
|
||||||
|
),
|
||||||
|
point(
|
||||||
|
bounds.left() + last_layout.x_for_index(range.end),
|
||||||
|
bounds.bottom(),
|
||||||
|
),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn character_index_for_point(
|
||||||
|
&mut self,
|
||||||
|
point: Point<Pixels>,
|
||||||
|
_window: &mut Window,
|
||||||
|
_cx: &mut Context<Self>,
|
||||||
|
) -> Option<usize> {
|
||||||
|
let bounds = self.last_bounds?;
|
||||||
|
let last_layout = self.last_layout.as_ref()?;
|
||||||
|
let local_point = bounds.localize(&point)?;
|
||||||
|
let utf8_index = last_layout.index_for_x(local_point.x)?;
|
||||||
|
Some(offset_to_utf16(&self.content, utf8_index))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Focusable for TextInput {
|
||||||
|
fn focus_handle(&self, _: &App) -> FocusHandle {
|
||||||
|
self.focus_handle.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl gpui::EventEmitter<Event> for TextInput {}
|
||||||
|
|
||||||
|
impl Styled for TextInputView {
|
||||||
|
fn style(&mut self) -> &mut gpui::StyleRefinement {
|
||||||
|
&mut self.style
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl gpui::Render for TextInput {
|
||||||
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
TextInputElement { input: cx.entity() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl gpui::RenderOnce for TextInputView {
|
||||||
|
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
|
let theme = app::current_theme(cx);
|
||||||
|
let focus_handle = self.input.read(cx).focus_handle.clone();
|
||||||
|
|
||||||
|
let on_backspace = self.input.clone();
|
||||||
|
let on_backspace_word = self.input.clone();
|
||||||
|
let on_delete = self.input.clone();
|
||||||
|
|
||||||
|
let on_left = self.input.clone();
|
||||||
|
let on_select_left = self.input.clone();
|
||||||
|
let on_word_left = self.input.clone();
|
||||||
|
let on_select_word_left = self.input.clone();
|
||||||
|
|
||||||
|
let on_right = self.input.clone();
|
||||||
|
let on_select_right = self.input.clone();
|
||||||
|
let on_word_right = self.input.clone();
|
||||||
|
let on_select_word_right = self.input.clone();
|
||||||
|
|
||||||
|
let on_line_start = self.input.clone();
|
||||||
|
let on_select_to_line_start = self.input.clone();
|
||||||
|
let on_line_end = self.input.clone();
|
||||||
|
let on_select_to_line_end = self.input.clone();
|
||||||
|
|
||||||
|
let on_select_all = self.input.clone();
|
||||||
|
let on_home = self.input.clone();
|
||||||
|
let on_end = self.input.clone();
|
||||||
|
let on_paste = self.input.clone();
|
||||||
|
let on_cut = self.input.clone();
|
||||||
|
let on_copy = self.input.clone();
|
||||||
|
let on_submit = self.input.clone();
|
||||||
|
let on_mouse_down = self.input.clone();
|
||||||
|
let on_mouse_up = self.input.clone();
|
||||||
|
let on_mouse_up_out = self.input.clone();
|
||||||
|
let on_mouse_move = self.input.clone();
|
||||||
|
|
||||||
|
let mut input = div()
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.w_full()
|
||||||
|
.px_2()
|
||||||
|
.py_1()
|
||||||
|
.text_color(theme.colors.text)
|
||||||
|
.line_height(px(20.))
|
||||||
|
.key_context(KEY_CONTEXT)
|
||||||
|
.track_focus(&focus_handle)
|
||||||
|
.cursor(CursorStyle::IBeam)
|
||||||
|
.on_action(move |action: &Backspace, window, cx| {
|
||||||
|
_ = on_backspace.update(cx, |this, cx| this.backspace(action, window, cx));
|
||||||
|
})
|
||||||
|
.on_action(move |action: &BackspaceWord, window, cx| {
|
||||||
|
_ = on_backspace_word.update(cx, |this, cx| this.backspace_word(action, window, cx))
|
||||||
|
})
|
||||||
|
.on_action(move |action: &Delete, window, cx| {
|
||||||
|
_ = on_delete.update(cx, |this, cx| this.delete(action, window, cx));
|
||||||
|
})
|
||||||
|
.on_action(move |action: &Left, window, cx| {
|
||||||
|
_ = on_left.update(cx, |this, cx| this.left(action, window, cx));
|
||||||
|
})
|
||||||
|
.on_action(move |action: &SelectLeft, window, cx| {
|
||||||
|
_ = on_select_left.update(cx, |this, cx| this.select_left(action, window, cx));
|
||||||
|
})
|
||||||
|
.on_action(move |action: &WordLeft, window, cx| {
|
||||||
|
_ = on_word_left.update(cx, |this, cx| this.word_left(action, window, cx));
|
||||||
|
})
|
||||||
|
.on_action(move |action: &SelectWordLeft, window, cx| {
|
||||||
|
_ = on_select_word_left
|
||||||
|
.update(cx, |this, cx| this.select_word_left(action, window, cx));
|
||||||
|
})
|
||||||
|
.on_action(move |action: &Right, window, cx| {
|
||||||
|
_ = on_right.update(cx, |this, cx| this.right(action, window, cx));
|
||||||
|
})
|
||||||
|
.on_action(move |action: &WordRight, window, cx| {
|
||||||
|
_ = on_word_right.update(cx, |this, cx| this.word_right(action, window, cx));
|
||||||
|
})
|
||||||
|
.on_action(move |action: &SelectRight, window, cx| {
|
||||||
|
_ = on_select_right.update(cx, |this, cx| this.select_right(action, window, cx));
|
||||||
|
})
|
||||||
|
.on_action(move |action: &SelectWordRight, window, cx| {
|
||||||
|
_ = on_select_word_right
|
||||||
|
.update(cx, |this, cx| this.select_word_right(action, window, cx));
|
||||||
|
})
|
||||||
|
.on_action(move |action: &LineStart, window, cx| {
|
||||||
|
_ = on_line_start.update(cx, |this, cx| this.line_start(action, window, cx));
|
||||||
|
})
|
||||||
|
.on_action(move |action: &SelectToLineStart, window, cx| {
|
||||||
|
_ = on_select_to_line_start
|
||||||
|
.update(cx, |this, cx| this.select_to_line_start(action, window, cx));
|
||||||
|
})
|
||||||
|
.on_action(move |action: &LineEnd, window, cx| {
|
||||||
|
_ = on_line_end.update(cx, |this, cx| this.line_end(action, window, cx));
|
||||||
|
})
|
||||||
|
.on_action(move |action: &SelectToLineEnd, window, cx| {
|
||||||
|
_ = on_select_to_line_end
|
||||||
|
.update(cx, |this, cx| this.select_to_line_end(action, window, cx));
|
||||||
|
})
|
||||||
|
.on_action(move |action: &SelectAll, window, cx| {
|
||||||
|
_ = on_select_all.update(cx, |this, cx| this.select_all(action, window, cx));
|
||||||
|
})
|
||||||
|
.on_action(move |action: &Home, window, cx| {
|
||||||
|
_ = on_home.update(cx, |this, cx| this.home(action, window, cx));
|
||||||
|
})
|
||||||
|
.on_action(move |action: &End, window, cx| {
|
||||||
|
_ = on_end.update(cx, |this, cx| this.end(action, window, cx));
|
||||||
|
})
|
||||||
|
.on_action(move |action: &Paste, window, cx| {
|
||||||
|
_ = on_paste.update(cx, |this, cx| this.paste(action, window, cx));
|
||||||
|
})
|
||||||
|
.on_action(move |action: &Cut, window, cx| {
|
||||||
|
_ = on_cut.update(cx, |this, cx| this.cut(action, window, cx));
|
||||||
|
})
|
||||||
|
.on_action(move |action: &Copy, window, cx| {
|
||||||
|
_ = on_copy.update(cx, |this, cx| this.copy(action, window, cx));
|
||||||
|
})
|
||||||
|
.on_action(move |action: &Submit, window, cx| {
|
||||||
|
_ = on_submit.update(cx, |this, cx| this.submit(action, window, cx));
|
||||||
|
})
|
||||||
|
.on_mouse_down(MouseButton::Left, move |event, window, cx| {
|
||||||
|
_ = on_mouse_down.update(cx, |this, cx| this.on_mouse_down(event, window, cx));
|
||||||
|
})
|
||||||
|
.on_mouse_up(MouseButton::Left, move |event, window, cx| {
|
||||||
|
_ = on_mouse_up.update(cx, |this, cx| this.on_mouse_up(event, window, cx));
|
||||||
|
})
|
||||||
|
.on_mouse_up_out(MouseButton::Left, move |event, window, cx| {
|
||||||
|
_ = on_mouse_up_out.update(cx, |this, cx| this.on_mouse_up(event, window, cx));
|
||||||
|
})
|
||||||
|
.on_mouse_move(move |event, window, cx| {
|
||||||
|
_ = on_mouse_move.update(cx, |this, cx| this.on_mouse_move(event, window, cx));
|
||||||
|
})
|
||||||
|
.child(self.input.clone());
|
||||||
|
|
||||||
|
input.style().refine(&self.style);
|
||||||
|
|
||||||
|
input
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TextInputElement {
|
||||||
|
input: Entity<TextInput>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PrepaintState {
|
||||||
|
line: Option<ShapedLine>,
|
||||||
|
cursor: Option<PaintQuad>,
|
||||||
|
selection: Option<PaintQuad>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoElement for TextInputElement {
|
||||||
|
type Element = Self;
|
||||||
|
|
||||||
|
fn into_element(self) -> Self::Element {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Element for TextInputElement {
|
||||||
|
type RequestLayoutState = ();
|
||||||
|
type PrepaintState = PrepaintState;
|
||||||
|
|
||||||
|
fn id(&self) -> Option<ElementId> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request_layout(
|
||||||
|
&mut self,
|
||||||
|
_id: Option<&GlobalElementId>,
|
||||||
|
_inspector_id: Option<&gpui::InspectorElementId>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> (LayoutId, Self::RequestLayoutState) {
|
||||||
|
let mut style = Style::default();
|
||||||
|
style.size.width = relative(1.).into();
|
||||||
|
style.size.height = window.line_height().into();
|
||||||
|
(window.request_layout(style, [], cx), ())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepaint(
|
||||||
|
&mut self,
|
||||||
|
_id: Option<&GlobalElementId>,
|
||||||
|
_inspector_id: Option<&gpui::InspectorElementId>,
|
||||||
|
bounds: Bounds<Pixels>,
|
||||||
|
_request_layout: &mut Self::RequestLayoutState,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> Self::PrepaintState {
|
||||||
|
let theme = app::current_theme(cx);
|
||||||
|
let input = self.input.read(cx);
|
||||||
|
let content = input.content.clone();
|
||||||
|
let selected_range = input.selected_range.clone();
|
||||||
|
let cursor = input.cursor_offset();
|
||||||
|
let style = window.text_style();
|
||||||
|
|
||||||
|
let (display_text, text_color) = if content.is_empty() {
|
||||||
|
(input.placeholder.clone(), theme.colors.text_subtle.into())
|
||||||
|
} else {
|
||||||
|
(content, style.color)
|
||||||
|
};
|
||||||
|
|
||||||
|
let run = TextRun {
|
||||||
|
len: display_text.len(),
|
||||||
|
font: style.font(),
|
||||||
|
color: text_color,
|
||||||
|
background_color: None,
|
||||||
|
underline: None,
|
||||||
|
strikethrough: None,
|
||||||
|
};
|
||||||
|
let runs = if let Some(marked_range) = input.marked_range.as_ref() {
|
||||||
|
vec![
|
||||||
|
TextRun {
|
||||||
|
len: marked_range.start,
|
||||||
|
..run.clone()
|
||||||
|
},
|
||||||
|
TextRun {
|
||||||
|
len: marked_range.end - marked_range.start,
|
||||||
|
underline: Some(UnderlineStyle {
|
||||||
|
color: Some(run.color),
|
||||||
|
thickness: px(1.0),
|
||||||
|
wavy: false,
|
||||||
|
}),
|
||||||
|
..run.clone()
|
||||||
|
},
|
||||||
|
TextRun {
|
||||||
|
len: display_text.len() - marked_range.end,
|
||||||
|
..run
|
||||||
|
},
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.filter(|run| run.len > 0)
|
||||||
|
.collect()
|
||||||
|
} else {
|
||||||
|
vec![run]
|
||||||
|
};
|
||||||
|
|
||||||
|
let font_size = style.font_size.to_pixels(window.rem_size());
|
||||||
|
let line = window
|
||||||
|
.text_system()
|
||||||
|
.shape_line(display_text, font_size, &runs, None);
|
||||||
|
|
||||||
|
let cursor_pos = line.x_for_index(cursor);
|
||||||
|
let (selection, cursor) = if selected_range.is_empty() {
|
||||||
|
(
|
||||||
|
None,
|
||||||
|
Some(fill(
|
||||||
|
Bounds::new(
|
||||||
|
point(bounds.left() + cursor_pos, bounds.top()),
|
||||||
|
size(px(1.), bounds.bottom() - bounds.top()),
|
||||||
|
),
|
||||||
|
theme.colors.accent_solid,
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
Some(fill(
|
||||||
|
Bounds::from_corners(
|
||||||
|
point(
|
||||||
|
bounds.left() + line.x_for_index(selected_range.start),
|
||||||
|
bounds.top(),
|
||||||
|
),
|
||||||
|
point(
|
||||||
|
bounds.left() + line.x_for_index(selected_range.end),
|
||||||
|
bounds.bottom(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
theme.colors.selection_bg,
|
||||||
|
)),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
PrepaintState {
|
||||||
|
line: Some(line),
|
||||||
|
cursor,
|
||||||
|
selection,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paint(
|
||||||
|
&mut self,
|
||||||
|
_id: Option<&GlobalElementId>,
|
||||||
|
_inspector_id: Option<&gpui::InspectorElementId>,
|
||||||
|
bounds: Bounds<Pixels>,
|
||||||
|
_request_layout: &mut Self::RequestLayoutState,
|
||||||
|
prepaint: &mut Self::PrepaintState,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut App,
|
||||||
|
) {
|
||||||
|
let focus_handle = self.input.read(cx).focus_handle.clone();
|
||||||
|
window.handle_input(
|
||||||
|
&focus_handle,
|
||||||
|
ElementInputHandler::new(bounds, self.input.clone()),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Some(selection) = prepaint.selection.take() {
|
||||||
|
window.paint_quad(selection);
|
||||||
|
}
|
||||||
|
|
||||||
|
let line = prepaint.line.take().unwrap();
|
||||||
|
line.paint(bounds.origin, window.line_height(), window, cx)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
if focus_handle.is_focused(window)
|
||||||
|
&& let Some(cursor) = prepaint.cursor.take()
|
||||||
|
{
|
||||||
|
window.paint_quad(cursor);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.input.update(cx, |input, _cx| {
|
||||||
|
input.last_layout = Some(line);
|
||||||
|
input.last_bounds = Some(bounds);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn offset_to_utf16(text: &str, offset: usize) -> usize {
|
||||||
|
let mut utf16_offset = 0;
|
||||||
|
let mut utf8_count = 0;
|
||||||
|
|
||||||
|
for ch in text.chars() {
|
||||||
|
if utf8_count >= offset {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
utf8_count += ch.len_utf8();
|
||||||
|
utf16_offset += ch.len_utf16();
|
||||||
|
}
|
||||||
|
|
||||||
|
utf16_offset
|
||||||
|
}
|
||||||
|
|
||||||
|
fn offset_from_utf16(text: &str, offset: usize) -> usize {
|
||||||
|
let mut utf8_offset = 0;
|
||||||
|
let mut utf16_count = 0;
|
||||||
|
|
||||||
|
for ch in text.chars() {
|
||||||
|
if utf16_count >= offset {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
utf16_count += ch.len_utf16();
|
||||||
|
utf8_offset += ch.len_utf8();
|
||||||
|
}
|
||||||
|
|
||||||
|
utf8_offset
|
||||||
|
}
|
||||||
|
|
||||||
|
fn range_from_utf16(text: &str, range_utf16: &Range<usize>) -> Range<usize> {
|
||||||
|
offset_from_utf16(text, range_utf16.start)..offset_from_utf16(text, range_utf16.end)
|
||||||
|
}
|
||||||
9
src/keyboard.rs
Normal file
9
src/keyboard.rs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
use crate::{
|
||||||
|
component::text_input::TextInput,
|
||||||
|
screen::dashboard::pull_request_diff_view::PullRequestDiffView,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub(crate) fn attach_key_binds(cx: &mut gpui::App) {
|
||||||
|
cx.bind_keys(PullRequestDiffView::key_bindings());
|
||||||
|
cx.bind_keys(TextInput::key_bindings());
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ mod asset;
|
|||||||
mod colors;
|
mod colors;
|
||||||
mod component;
|
mod component;
|
||||||
mod http;
|
mod http;
|
||||||
|
mod keyboard;
|
||||||
mod query;
|
mod query;
|
||||||
mod screen;
|
mod screen;
|
||||||
mod storage;
|
mod storage;
|
||||||
@@ -62,6 +63,8 @@ fn setup_application(cx: &mut gpui::App) {
|
|||||||
cx.set_global(global);
|
cx.set_global(global);
|
||||||
cx.set_global(query_store);
|
cx.set_global(query_store);
|
||||||
|
|
||||||
|
keyboard::attach_key_binds(cx);
|
||||||
|
|
||||||
if diffops_playground::is_enabled() {
|
if diffops_playground::is_enabled() {
|
||||||
_ = diffops_playground::open_window(cx);
|
_ = diffops_playground::open_window(cx);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
mod issue_list;
|
mod issue_list;
|
||||||
mod pull_request_change_view;
|
mod pull_request_change_view;
|
||||||
mod pull_request_diff_view;
|
pub(crate) mod pull_request_diff_view;
|
||||||
mod pull_request_file_tree;
|
mod pull_request_file_tree;
|
||||||
mod pull_request_view;
|
mod pull_request_view;
|
||||||
mod screen;
|
mod screen;
|
||||||
|
|||||||
@@ -60,7 +60,12 @@ impl PullRequestChangeView {
|
|||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_file_tree_item_click(&mut self, i: usize, cx: &mut gpui::Context<Self>) {
|
fn handle_file_tree_item_click(
|
||||||
|
&mut self,
|
||||||
|
i: usize,
|
||||||
|
window: &mut gpui::Window,
|
||||||
|
cx: &mut gpui::Context<Self>,
|
||||||
|
) {
|
||||||
let item = &self.file_tree_items[i];
|
let item = &self.file_tree_items[i];
|
||||||
match item.kind {
|
match item.kind {
|
||||||
| FileTreeItemKind::Directory => {
|
| FileTreeItemKind::Directory => {
|
||||||
@@ -74,6 +79,7 @@ impl PullRequestChangeView {
|
|||||||
self.diff_view.update(cx, |diff_view, cx| {
|
self.diff_view.update(cx, |diff_view, cx| {
|
||||||
diff_view.show_diff_for_file(&item.full_path, cx);
|
diff_view.show_diff_for_file(&item.full_path, cx);
|
||||||
});
|
});
|
||||||
|
cx.focus_view(&self.diff_view, window);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -106,9 +112,11 @@ impl gpui::Render for PullRequestChangeView {
|
|||||||
file_tree(self.file_tree_state.clone(), move |i, _, cx| {
|
file_tree(self.file_tree_state.clone(), move |i, _, cx| {
|
||||||
weak.read(cx).file_tree_items[i].clone()
|
weak.read(cx).file_tree_items[i].clone()
|
||||||
})
|
})
|
||||||
.on_item_click(cx.listener(|this, i, _, cx| {
|
.on_item_click(cx.listener(
|
||||||
this.handle_file_tree_item_click(*i, cx);
|
|this, i, window, cx| {
|
||||||
})),
|
this.handle_file_tree_item_click(*i, window, cx);
|
||||||
|
},
|
||||||
|
)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use gpui::{AppContext, IntoElement, div};
|
use gpui::{
|
||||||
|
AppContext, FocusHandle, InteractiveElement, IntoElement, ParentElement, Styled, div,
|
||||||
|
prelude::FluentBuilder,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{self},
|
api::{self},
|
||||||
app,
|
app,
|
||||||
component::diff_view::{DiffViewContent, DiffViewState, diff_view},
|
component::{
|
||||||
|
diff_view::{DiffViewContent, DiffViewState, diff_view},
|
||||||
|
text_input::{TextInput, text_input},
|
||||||
|
},
|
||||||
query::{self, QueryStatus, read_query, use_query, watch_query},
|
query::{self, QueryStatus, read_query, use_query, watch_query},
|
||||||
util,
|
util,
|
||||||
};
|
};
|
||||||
@@ -16,8 +22,16 @@ pub(crate) struct PullRequestDiffView {
|
|||||||
diff_view_state: DiffViewState,
|
diff_view_state: DiffViewState,
|
||||||
diff_view_content: Option<DiffViewContent>,
|
diff_view_content: Option<DiffViewContent>,
|
||||||
current_file_path: Option<Arc<str>>,
|
current_file_path: Option<Arc<str>>,
|
||||||
|
|
||||||
|
focus_handle: gpui::FocusHandle,
|
||||||
|
is_search_input_visible: bool,
|
||||||
|
search_input: gpui::Entity<TextInput>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gpui::actions!([Search, Escape]);
|
||||||
|
|
||||||
|
const KEY_CONTEXT: &'static str = "PullRequestDiffView";
|
||||||
|
|
||||||
impl PullRequestDiffView {
|
impl PullRequestDiffView {
|
||||||
pub(crate) fn new(pr_id: api::issues::Id, cx: &mut gpui::Context<Self>) -> Self {
|
pub(crate) fn new(pr_id: api::issues::Id, cx: &mut gpui::Context<Self>) -> Self {
|
||||||
let mut s = Self {
|
let mut s = Self {
|
||||||
@@ -26,11 +40,22 @@ impl PullRequestDiffView {
|
|||||||
diff_view_state: DiffViewState::new(),
|
diff_view_state: DiffViewState::new(),
|
||||||
diff_view_content: None,
|
diff_view_content: None,
|
||||||
current_file_path: None,
|
current_file_path: None,
|
||||||
|
|
||||||
|
focus_handle: cx.focus_handle(),
|
||||||
|
is_search_input_visible: false,
|
||||||
|
search_input: cx.new(|cx| TextInput::with_placeholder("Search", cx)),
|
||||||
};
|
};
|
||||||
s.on_create(cx);
|
s.on_create(cx);
|
||||||
s
|
s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn key_bindings() -> [gpui::KeyBinding; 2] {
|
||||||
|
[
|
||||||
|
gpui::KeyBinding::new("cmd-f", Search, Some(KEY_CONTEXT)),
|
||||||
|
gpui::KeyBinding::new("escape", Escape, Some(KEY_CONTEXT)),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn show_diff_for_file(
|
pub(crate) fn show_diff_for_file(
|
||||||
&mut self,
|
&mut self,
|
||||||
file_path: &Arc<str>,
|
file_path: &Arc<str>,
|
||||||
@@ -146,6 +171,24 @@ impl PullRequestDiffView {
|
|||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn open_search_box(&mut self, window: &mut gpui::Window, cx: &mut gpui::Context<Self>) {
|
||||||
|
self.is_search_input_visible = true;
|
||||||
|
cx.focus_view(&self.search_input, window);
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn close_search_box(&mut self, window: &mut gpui::Window, cx: &mut gpui::Context<Self>) {
|
||||||
|
self.is_search_input_visible = false;
|
||||||
|
self.focus_handle.focus(window);
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl gpui::Focusable for PullRequestDiffView {
|
||||||
|
fn focus_handle(&self, _: &gpui::App) -> FocusHandle {
|
||||||
|
self.focus_handle.clone()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl gpui::Render for PullRequestDiffView {
|
impl gpui::Render for PullRequestDiffView {
|
||||||
@@ -154,18 +197,49 @@ impl gpui::Render for PullRequestDiffView {
|
|||||||
_window: &mut gpui::Window,
|
_window: &mut gpui::Window,
|
||||||
cx: &mut gpui::prelude::Context<Self>,
|
cx: &mut gpui::prelude::Context<Self>,
|
||||||
) -> impl gpui::IntoElement {
|
) -> impl gpui::IntoElement {
|
||||||
|
let theme = app::current_theme(cx);
|
||||||
let content_diff = self
|
let content_diff = self
|
||||||
.content_diff_query
|
.content_diff_query
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|q| read_query(q, cx))
|
.map(|q| read_query(q, cx))
|
||||||
.unwrap_or(QueryStatus::Loading);
|
.unwrap_or(QueryStatus::Loading);
|
||||||
|
|
||||||
|
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) {
|
match (content_diff, &self.diff_view_content) {
|
||||||
| (QueryStatus::Loaded(_), Some(content)) => {
|
| (QueryStatus::Loaded(_), Some(content)) => Some(content.clone()),
|
||||||
diff_view(self.diff_view_state.clone(), content.clone()).into_any_element()
|
| (_, _) => None,
|
||||||
}
|
},
|
||||||
|
|it, content| {
|
||||||
| (_, _) => div().into_any_element(),
|
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);
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user