3use crate::{graphics::Rect, Document, DocumentId, ViewId};
6 graphemes::{grapheme_width, RopeGraphemes},
7 line_ending::line_end_char_index,
8 Position, RopeSlice, Selection,
11type Jump = (DocumentId, Selection);
13#[derive(Debug, Clone)]
20 pub fn new(initial: Jump) -> Self {
27 pub fn push(&mut self, jump: Jump) {
28 self.jumps.truncate(self.current);
29 // don't push duplicates
30 if self.jumps.last() != Some(&jump) {
31 self.jumps.push(jump);
32 self.current = self.jumps.len();
36 pub fn forward(&mut self, count: usize) -> Option<&Jump> {
37 if self.current + count < self.jumps.len() {
38 self.current += count;
39 self.jumps.get(self.current)
45 // Taking view and doc to prevent unnecessary cloning when jump is not required.
46 pub fn backward(&mut self, view_id: ViewId, doc: &mut Document, count: usize) -> Option<&Jump> {
47 if let Some(current) = self.current.checked_sub(count) {
48 if self.current == self.jumps.len() {
49 let jump = (doc.id(), doc.selection(view_id).clone());
52 self.current = current;
53 self.jumps.get(self.current)
67 /// the last accessed file before the current one
68 pub last_accessed_doc: Option<DocumentId>,
72 pub fn new(doc: DocumentId) -> Self {
74 id: ViewId::default(),
76 offset: Position::new(0, 0),
77 area: Rect::default(), // will get calculated upon inserting into tree
78 jumps: JumpList::new((doc, Selection::point(0))), // TODO: use actual sel
79 last_accessed_doc: None,
83 pub fn inner_area(&self) -> Rect {
85 const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter
89 self.area.width - OFFSET,
90 self.area.height.saturating_sub(1), // -1 for statusline
94 pub fn ensure_cursor_in_view(&mut self, doc: &Document, scrolloff: usize) {
98 .cursor(doc.text().slice(..));
99 let Position { col, row: line } = coords_at_pos(doc.text().slice(..), cursor);
100 let inner_area = self.inner_area();
101 let last_line = (self.offset.row + inner_area.height as usize).saturating_sub(1);
103 // - 1 so we have at least one gap in the middle.
104 // a height of 6 with padding of 3 on each side will keep shifting the view back and forth
106 let scrolloff = scrolloff.min(inner_area.height.saturating_sub(1) as usize / 2);
108 let last_col = self.offset.col + inner_area.width.saturating_sub(1) as usize;
110 if line > last_line.saturating_sub(scrolloff) {
112 self.offset.row += line - (last_line.saturating_sub(scrolloff));
113 } else if line < self.offset.row + scrolloff {
115 self.offset.row = line.saturating_sub(scrolloff);
118 if col > last_col.saturating_sub(scrolloff) {
120 self.offset.col += col - (last_col.saturating_sub(scrolloff));
121 } else if col < self.offset.col + scrolloff {
123 self.offset.col = col.saturating_sub(scrolloff);
127 /// Calculates the last visible line on screen
129 pub fn last_line(&self, doc: &Document) -> usize {
130 let height = self.inner_area().height;
132 // Saturating subs to make it inclusive zero indexing.
133 (self.offset.row + height as usize).saturating_sub(1),
134 doc.text().len_lines().saturating_sub(1),
138 /// Translates a document position to an absolute position in the terminal.
139 /// Returns a (line, col) position if the position is visible on screen.
140 // TODO: Could return width as well for the character width at cursor.
141 pub fn screen_coords_at_pos(
146 ) -> Option<Position> {
147 let line = text.char_to_line(pos);
149 if line < self.offset.row || line > self.last_line(doc) {
150 // Line is not visible on screen
154 let line_start = text.line_to_char(line);
155 let line_slice = text.slice(line_start..pos);
157 let tab_width = doc.tab_width();
159 for grapheme in RopeGraphemes::new(line_slice) {
160 if grapheme == "\t" {
163 let grapheme = Cow::from(grapheme);
164 col += grapheme_width(&grapheme);
168 // It is possible for underflow to occur if the buffer length is larger than the terminal width.
169 let row = line.saturating_sub(self.offset.row);
170 let col = col.saturating_sub(self.offset.col);
172 Some(Position::new(row, col))
175 pub fn text_pos_at_screen_coords(
182 let inner = self.inner_area();
184 if row < inner.top() || row >= inner.bottom() {
188 if column < inner.left() || column > inner.right() {
192 let line_number = (row - inner.y) as usize + self.offset.row;
194 if line_number > text.len_lines() - 1 {
195 return Some(text.len_chars());
198 let mut pos = text.line_to_char(line_number);
200 let current_line = text.line(line_number);
202 let target = (column - inner.x) as usize + self.offset.col;
203 let mut selected = 0;
205 for grapheme in RopeGraphemes::new(current_line) {
206 if selected >= target {
209 if grapheme == "\t" {
210 selected += tab_width;
212 let width = grapheme_width(&Cow::from(grapheme));
215 pos += grapheme.chars().count();
218 Some(pos.min(line_end_char_index(&text.slice(..), line_number)))
221 /// Translates a screen position to position in the text document.
222 /// Returns a usize typed position in bounds of the text if found in this view, None if out of view.
223 pub fn pos_at_screen_coords(&self, doc: &Document, row: u16, column: u16) -> Option<usize> {
224 self.text_pos_at_screen_coords(&doc.text().slice(..), row, column, doc.tab_width())
226 // pub fn traverse<F>(&self, text: RopeSlice, start: usize, end: usize, fun: F)
228 // F: Fn(usize, usize),
230 // let start = self.screen_coords_at_pos(text, start);
231 // let end = self.screen_coords_at_pos(text, end);
233 // match (start, end) {
234 // // fully on screen
235 // (Some(start), Some(end)) => {
236 // // we want to calculate ends of lines for each char..
238 // // from start to end of screen
239 // (Some(start), None) => {}
240 // // from start of screen to end
241 // (None, Some(end)) => {}
243 // (None, None) => return,
251 use helix_core::Rope;
252 const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter
255 fn test_text_pos_at_screen_coords() {
256 let mut view = View::new(DocumentId::default());
257 view.area = Rect::new(40, 40, 40, 40);
258 let rope = Rope::from_str("abc\n\tdef");
259 let text = rope.slice(..);
261 assert_eq!(view.text_pos_at_screen_coords(&text, 40, 2, 4), None);
263 assert_eq!(view.text_pos_at_screen_coords(&text, 40, 41, 4), None);
265 assert_eq!(view.text_pos_at_screen_coords(&text, 0, 2, 4), None);
267 assert_eq!(view.text_pos_at_screen_coords(&text, 0, 49, 4), None);
269 assert_eq!(view.text_pos_at_screen_coords(&text, 0, 41, 4), None);
271 assert_eq!(view.text_pos_at_screen_coords(&text, 40, 81, 4), None);
273 assert_eq!(view.text_pos_at_screen_coords(&text, 78, 41, 4), None);
276 view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET + 3, 4),
280 assert_eq!(view.text_pos_at_screen_coords(&text, 40, 80, 4), Some(3));
283 view.text_pos_at_screen_coords(&text, 41, 40 + OFFSET + 1, 4),
288 view.text_pos_at_screen_coords(&text, 41, 40 + OFFSET + 4, 4),
293 view.text_pos_at_screen_coords(&text, 41, 40 + OFFSET + 7, 4),
297 assert_eq!(view.text_pos_at_screen_coords(&text, 41, 80, 4), Some(8));
301 fn test_text_pos_at_screen_coords_cjk() {
302 let mut view = View::new(DocumentId::default());
303 view.area = Rect::new(40, 40, 40, 40);
304 let rope = Rope::from_str("Hi! こんにちは皆さん");
305 let text = rope.slice(..);
308 view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET + 0, 4),
313 view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET + 5, 4),
318 view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET + 6, 4),
323 view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET + 7, 4),
328 view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET + 8, 4),
334 fn test_text_pos_at_screen_coords_graphemes() {
335 let mut view = View::new(DocumentId::default());
336 view.area = Rect::new(40, 40, 40, 40);
337 let rope = Rope::from_str("Hèl̀l̀ò world!");
338 let text = rope.slice(..);
341 view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET + 0, 4),
346 view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET + 1, 4),
351 view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET + 2, 4),
356 view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET + 3, 4),
361 view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET + 4, 4),
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list