aboutsummaryrefslogtreecommitdiff
path: root/helix-view/src/view.rs
blob: 1d806da907bac0636d0adfb28e1445d3591077e5 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
use anyhow::Error;

use std::borrow::Cow;

use crate::Document;
use helix_core::{
    graphemes::{grapheme_width, RopeGraphemes},
    indent::TAB_WIDTH,
    Position, RopeSlice,
};
use slotmap::DefaultKey as Key;
use tui::layout::Rect;

pub const PADDING: usize = 5;

// TODO: view should be View { doc: Document(state, history,..) }
// since we can have multiple views into the same file
pub struct View {
    pub id: Key,
    pub doc: Document,
    pub first_line: usize,
    pub area: Rect,
}
// TODO: popups should be a thing on the view with a rect + text

impl View {
    pub fn new(doc: Document) -> Result<Self, Error> {
        let view = Self {
            id: Key::default(),
            doc,
            first_line: 0,
            area: Rect::default(), // will get calculated upon inserting into tree
        };

        Ok(view)
    }

    pub fn check_cursor_in_view(&self) -> bool {
        let cursor = self.doc.selection().cursor();
        let line = self.doc.text().char_to_line(cursor);
        let document_end = self.first_line + self.area.height.saturating_sub(1) as usize;

        if (line > document_end.saturating_sub(PADDING)) || (line < self.first_line + PADDING) {
            return false;
        }
        true
    }

    pub fn ensure_cursor_in_view(&mut self) {
        let cursor = self.doc.state.selection().cursor();
        let line = self.doc.text().char_to_line(cursor);
        let document_end = self.first_line + (self.area.height as usize).saturating_sub(2);

        // TODO: side scroll

        if line > document_end.saturating_sub(PADDING) {
            // scroll down
            self.first_line += line - (document_end.saturating_sub(PADDING));
        } else if line < self.first_line + PADDING {
            // scroll up
            self.first_line = line.saturating_sub(PADDING);
        }
    }

    /// Calculates the last visible line on screen
    #[inline]
    pub fn last_line(&self) -> usize {
        let viewport = Rect::new(6, 0, self.area.width, self.area.height.saturating_sub(1)); // - 1 for statusline
        std::cmp::min(
            self.first_line + (viewport.height as usize),
            self.doc.text().len_lines() - 1,
        )
    }

    /// Translates a document position to an absolute position in the terminal.
    /// Returns a (line, col) position if the position is visible on screen.
    // TODO: Could return width as well for the character width at cursor.
    pub fn screen_coords_at_pos(&self, text: RopeSlice, pos: usize) -> Option<Position> {
        let line = text.char_to_line(pos);

        if line < self.first_line as usize || line > self.last_line() {
            // Line is not visible on screen
            return None;
        }

        let line_start = text.line_to_char(line);
        let line_slice = text.slice(line_start..pos);
        let mut col = 0;

        for grapheme in RopeGraphemes::new(line_slice) {
            if grapheme == "\t" {
                col += TAB_WIDTH;
            } else {
                let grapheme = Cow::from(grapheme);
                col += grapheme_width(&grapheme);
            }
        }

        let row = line - self.first_line as usize;

        Some(Position::new(row, col))
    }

    // pub fn traverse<F>(&self, text: RopeSlice, start: usize, end: usize, fun: F)
    // where
    //     F: Fn(usize, usize),
    // {
    //     let start = self.screen_coords_at_pos(text, start);
    //     let end = self.screen_coords_at_pos(text, end);

    //     match (start, end) {
    //         // fully on screen
    //         (Some(start), Some(end)) => {
    //             // we want to calculate ends of lines for each char..
    //         }
    //         // from start to end of screen
    //         (Some(start), None) => {}
    //         // from start of screen to end
    //         (None, Some(end)) => {}
    //         // not on screen
    //         (None, None) => return,
    //     }
    // }
}