/* * Copyright (c) 2023 Marceline Cramer * Copyright (c) 2023 Emma Tebibyte * SPDX-License-Identifier: AGPL-3.0-or-later * * This program is free software: you can redistribute it and/or modify it under * the terms of the GNU Affero General Public License as published by the Free * Software Foundation, either version 3 of the License, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more * details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see https://www.gnu.org/licenses/. */ use std::io::Write; use std::ops::Range; use std::sync::Arc; use crossterm::{cursor, ExecutableCommand}; use parking_lot::Mutex; use ropey::Rope; use syntect::easy::ScopeRangeIterator; use syntect::parsing::{ParseState, Scope, ScopeStack, SyntaxSet}; use crate::theme::{Style, StyleStore}; use crate::{Cursor, Direction}; #[derive(Clone, Debug)] pub struct Buffer { styles: Arc>, text: Rope, syntax_set: Arc, parser: ParseState, styled: Vec)>>, } impl Buffer { pub fn from_str(styles: Arc>, text: &str) -> Self { let syntax_set = SyntaxSet::load_defaults_nonewlines(); // TODO load non-Rust syntax highlighting let syntax_ref = syntax_set.find_syntax_by_extension("rs").unwrap(); let parser = ParseState::new(syntax_ref); let mut buf = Self { styles, text: Rope::from_str(text), syntax_set: Arc::new(SyntaxSet::load_defaults_newlines()), parser, styled: Vec::new(), }; buf.parse(); buf } pub fn draw( &self, styles: &mut StyleStore, cols: u16, rows: u16, scroll: Cursor, out: &mut (impl ExecutableCommand + Write), ) -> crossterm::Result { let linenr_style = styles.get_scope("ui.linenr"); let linenr_width = self.text.len_lines().ilog10() + 1; let gutter_width = linenr_width + 1; let text_width = cols as usize - gutter_width as usize; out.execute(cursor::MoveTo(0, 0))?; for (row, line) in (0..rows).zip(self.text.lines_at(scroll.line)) { // only the last line is empty and should be skipped if line.len_chars() == 0 { break; } let row = row as usize + scroll.line; let linenr = format!("{:width$} ", row, width = linenr_width as usize); linenr_style.print_styled(out, &linenr)?; let lhs = scroll.column; let width = line.len_chars(); if lhs < width { let window = text_width.min(width - lhs); let rhs = lhs + window; let styled = &self.styled[row]; for (style, range) in styled.iter() { let range = range.start.max(lhs)..range.end.min(rhs); if range.start < range.end { // this range is in view so display let content = line.slice(range.clone()); style.print_styled(out, content)?; } if range.end >= rhs { // past the end of the window so we're done break; } } } out.execute(cursor::MoveToNextLine(1))?; } Ok(gutter_width) } pub fn remove(&mut self, cursor: Cursor) { let index = self.cursor_to_char(cursor); self.text.remove(index..=index); self.parse(); } pub fn insert_char(&mut self, cursor: Cursor, c: char) { let index = self.cursor_to_char(cursor); self.text.insert_char(index, c); self.parse(); } /// Parses the whole file from scratch. fn parse(&mut self) { use std::fmt::Write; let mut styles = self.styles.lock(); self.styled.clear(); let mut parser = self.parser.clone(); let mut line_buf = String::new(); let mut stack = ScopeStack::new(); for line in self.text.lines() { // display line to a pre-allocated buffer for fast parsing line_buf.clear(); write!(line_buf, "{}", line).unwrap(); let end = line_buf.trim_end().len(); line_buf.truncate(end); // parse the line into a sequence of stack operations let stack_ops = parser.parse_line(&line_buf, &self.syntax_set).unwrap(); // execute the operations on the stack let mut line = Vec::new(); let range_iter = ScopeRangeIterator::new(&stack_ops, &line_buf); for (range, op) in range_iter { stack.apply(&op).unwrap(); if range.start == range.end { continue; } let mut style = Style::default(); for scope in stack.scopes.clone() { // TODO docs say that build_string "shouldn't be done frequently" let scope = scope.build_string(); style.apply(&styles.get_scope(&scope)); } line.push((style, range)); } self.styled.push(line); } } pub fn clamped_cursor(&self, cursor: Cursor) -> Cursor { Cursor { line: cursor.line, column: cursor .column .min(self.text.line(cursor.line).len_chars() - 1), } } pub fn cursor_to_char(&self, cursor: Cursor) -> usize { let cursor = self.clamped_cursor(cursor); self.text.line_to_char(cursor.line) + cursor.column } pub fn move_cursor(&self, cursor: &mut Cursor, direction: Direction, enable_linewrap: bool) { *cursor = self.clamped_cursor(*cursor); match direction { Direction::Left => { if cursor.column > 0 { cursor.column -= 1; } else if enable_linewrap && cursor.line > 0 { cursor.line -= 1; let line = self.text.line(cursor.line); cursor.column = line.len_chars() - 1; } } Direction::Down => { if cursor.line + 2 < self.text.len_lines() { cursor.line += 1; } } Direction::Up => { if cursor.line > 0 { cursor.line -= 1; } } Direction::Right => { let line = self.text.line(cursor.line); if cursor.column + 2 > line.len_chars() { if enable_linewrap && cursor.line + 2 < self.text.len_lines() { cursor.line += 1; cursor.column = 0; } } else { cursor.column += 1; } } } } } #[cfg(test)] mod tests { use super::*; #[test] fn style_scopes() { let text = include_str!("buffer.rs"); let mut buffer = Buffer::from_str(Default::default(), text); buffer.parse(); println!("{:#?}", buffer.styled); } }