use std::error::Error;
use std::fmt::{Display, Formatter};
use std::ops::Range;
use std::sync::Arc;
use crate::cell_stream::{end_span, ends_with_cell_semicolon, frame_recovering};
use crate::native::{
NativeCellState, NativeParseError, incomplete_lex_error, incomplete_parse_error, parse_raw_cell,
};
use crate::{
Cell, CellStream, CellTokenStream, LexError, Source, Span, Spanned, TextEdit,
lex_str_recovering,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum NativeCellBoundary {
EOC,
EOF,
Unbounded,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct NativeRealtimeEditError;
impl Display for NativeRealtimeEditError {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
formatter.write_str("source edit is outside the current realtime buffer")
}
}
impl Error for NativeRealtimeEditError {}
#[derive(Debug)]
pub struct NativeRealtimeCell {
document_range: Range<usize>,
source: Span,
tokens: CellTokenStream,
syntax: Option<Cell>,
lexer_diagnostics: Vec<LexError>,
parse_error: Option<NativeParseError>,
state: NativeCellState,
boundary: NativeCellBoundary,
}
impl NativeRealtimeCell {
pub fn source(&self) -> &str {
self.source
.text()
.expect("realtime cell source spans must resolve to text")
}
pub fn document_range(&self) -> Range<usize> {
self.document_range.clone()
}
pub fn tokens(&self) -> &CellTokenStream {
&self.tokens
}
pub fn syntax(&self) -> Option<&Cell> {
self.syntax.as_ref()
}
pub fn lexer_diagnostics(&self) -> &[LexError] {
&self.lexer_diagnostics
}
pub fn parse_error(&self) -> Option<&NativeParseError> {
self.parse_error.as_ref()
}
pub fn state(&self) -> NativeCellState {
self.state
}
pub fn boundary(&self) -> NativeCellBoundary {
self.boundary
}
}
impl Spanned for NativeRealtimeCell {
fn try_span(&self) -> Option<Span> {
Some(self.source.clone())
}
}
#[derive(Debug)]
pub struct NativeRealtimeParser {
source: String,
cells: Vec<NativeRealtimeCell>,
reparsed_cells: Range<usize>,
}
impl Default for NativeRealtimeParser {
fn default() -> Self {
Self::new("")
}
}
impl NativeRealtimeParser {
pub fn new(source: impl Into<String>) -> Self {
let source = source.into();
let cells = analyze_region(&source, 0);
let reparsed_cells = 0..cells.len();
Self {
source,
cells,
reparsed_cells,
}
}
pub fn apply_edit(
&mut self,
edit: &TextEdit,
) -> Result<NativeCellState, NativeRealtimeEditError> {
let source = edit.apply(&self.source).ok_or(NativeRealtimeEditError)?;
let first_changed = self.first_affected_cell(edit);
let region_start = self
.cells
.get(first_changed)
.map_or(self.source.len(), |cell| cell.document_range.start);
let reusable_suffix = self.reusable_suffix(edit, &source, first_changed, region_start);
let rebuild_end = reusable_suffix
.map(|(_, start)| start)
.unwrap_or(source.len());
let rebuilt = analyze_region(&source[region_start..rebuild_end], region_start);
let mut previous = std::mem::take(&mut self.cells);
let previous_suffix = previous.split_off(first_changed);
let rebuilt_len = rebuilt.len();
previous.extend(rebuilt);
let reuse_from = reusable_suffix
.map(|(index, _)| index.saturating_sub(first_changed))
.unwrap_or(previous_suffix.len());
for mut retained in previous_suffix.into_iter().skip(reuse_from) {
retained.document_range = shifted_range(edit, retained.document_range);
previous.push(retained);
}
self.source = source;
self.cells = previous;
self.reparsed_cells = first_changed..first_changed + rebuilt_len;
Ok(self.state())
}
pub fn append(&mut self, source: impl Into<String>) -> NativeCellState {
let edit = TextEdit::insertion(self.source.len(), source);
self.apply_edit(&edit)
.expect("an insertion at the current EOF is always valid")
}
pub fn source(&self) -> &str {
&self.source
}
pub fn cells(&self) -> &[NativeRealtimeCell] {
&self.cells
}
pub fn reparsed_cells(&self) -> Range<usize> {
self.reparsed_cells.clone()
}
pub fn state(&self) -> NativeCellState {
if self
.cells
.iter()
.any(|cell| cell.state == NativeCellState::Invalid)
{
NativeCellState::Invalid
} else if self
.cells
.last()
.is_some_and(|cell| cell.state == NativeCellState::Incomplete)
{
NativeCellState::Incomplete
} else if self.cells.is_empty() {
NativeCellState::Empty
} else {
NativeCellState::Complete
}
}
pub fn cell_stream(&self) -> CellStream {
CellStream::from_cells(self.cells.iter().map(|cell| cell.tokens.clone()))
}
fn first_affected_cell(&self, edit: &TextEdit) -> usize {
let start = match edit {
TextEdit::Insertion { at, .. } => *at,
TextEdit::Deletion { start, .. } => *start,
};
self.cells
.iter()
.position(|cell| {
start < cell.document_range.end
|| (start == cell.document_range.end
&& cell.boundary != NativeCellBoundary::EOC)
})
.unwrap_or(self.cells.len())
}
fn reusable_suffix(
&self,
edit: &TextEdit,
source: &str,
first_changed: usize,
region_start: usize,
) -> Option<(usize, usize)> {
for index in first_changed.saturating_add(1)..self.cells.len() {
let old_start = self.cells[index].document_range.start;
let new_start = shifted_offset(edit, old_start);
if new_start < region_start
|| self.source.get(old_start..) != source.get(new_start..)
|| !ends_at_confirmed_boundary(&source[region_start..new_start])
{
continue;
}
return Some((index, new_start));
}
None
}
}
fn shifted_range(edit: &TextEdit, range: Range<usize>) -> Range<usize> {
shifted_offset(edit, range.start)..shifted_offset(edit, range.end)
}
fn shifted_offset(edit: &TextEdit, offset: usize) -> usize {
match edit {
TextEdit::Insertion { at, text } if offset >= *at => offset + text.len(),
TextEdit::Insertion { .. } => offset,
TextEdit::Deletion { start, .. } if offset <= *start => offset,
TextEdit::Deletion { start, end } if offset <= *end => *start,
TextEdit::Deletion { start, end } => offset - (end - start),
}
}
fn ends_at_confirmed_boundary(source: &str) -> bool {
if source.trim().is_empty() {
return true;
}
let Ok(recovered) = lex_str_recovering(source) else {
return false;
};
let framed = frame_recovering(recovered.value);
if framed.incomplete.is_some() {
return false;
}
let Some(last) = framed.cells.last() else {
return true;
};
ends_with_cell_semicolon(last) || source.ends_with('\n')
}
fn analyze_region(source: &str, document_start: usize) -> Vec<NativeRealtimeCell> {
let recovered = match lex_str_recovering(source) {
Ok(recovered) => recovered,
Err(error) => {
if source.is_empty() {
return Vec::new();
}
return vec![cell_from_lex_error(
source,
document_start..document_start + source.len(),
error,
)];
}
};
let framed = frame_recovering(recovered.value);
let complete_count = framed.cells.len();
let has_incomplete = framed.incomplete.is_some();
let mut start = 0;
let mut cells = Vec::with_capacity(complete_count + usize::from(has_incomplete));
for (index, tokens) in framed.cells.into_iter().enumerate() {
let has_following = index + 1 < complete_count || has_incomplete;
let mut end = end_span(&tokens)
.end()
.unwrap_or(source.len())
.min(source.len());
if !has_following && end < source.len() {
end = source.len();
}
let boundary = if has_following
|| ends_with_cell_semicolon(&tokens)
|| source[start..end].ends_with('\n')
{
NativeCellBoundary::EOC
} else {
NativeCellBoundary::EOF
};
cells.push(analyze_cell(
&source[start..end],
document_start + start..document_start + end,
boundary,
));
start = end;
}
if framed.incomplete.is_some() {
cells.push(analyze_cell(
&source[start..],
document_start + start..document_start + source.len(),
NativeCellBoundary::Unbounded,
));
}
cells
}
fn analyze_cell(
source: &str,
document_range: Range<usize>,
boundary: NativeCellBoundary,
) -> NativeRealtimeCell {
let recovered = match lex_str_recovering(source) {
Ok(recovered) => recovered,
Err(error) => return cell_from_lex_error(source, document_range, error),
};
let diagnostics = recovered.diagnostics;
let framed = frame_recovering(recovered.value);
let mut streams = framed.cells;
streams.extend(framed.incomplete);
let tokens = match streams.len() {
0 => CellTokenStream::default(),
1 => streams.pop().expect("one framed stream is present"),
_ => CellTokenStream::from_framed_cells(streams),
};
let source_owner = tokens.source().unwrap_or_else(|| Source::new(source));
let source_span = Span::ranged(source_owner, 0..source.len());
let (syntax, parse_error, state) = match parse_raw_cell(tokens.clone()) {
Ok(syntax) => {
let state = if boundary == NativeCellBoundary::Unbounded || !diagnostics.is_empty() {
NativeCellState::Incomplete
} else {
NativeCellState::Complete
};
(syntax, None, state)
}
Err(error) => {
let state = if boundary != NativeCellBoundary::EOC && incomplete_parse_error(&error) {
NativeCellState::Incomplete
} else {
NativeCellState::Invalid
};
(None, Some(error), state)
}
};
NativeRealtimeCell {
document_range,
source: source_span,
tokens,
syntax,
lexer_diagnostics: diagnostics,
parse_error,
state,
boundary,
}
}
fn cell_from_lex_error(
source: &str,
document_range: Range<usize>,
error: LexError,
) -> NativeRealtimeCell {
let state = if incomplete_lex_error(error.kind()) {
NativeCellState::Incomplete
} else {
NativeCellState::Invalid
};
let source_owner = Arc::clone(error.span().source());
NativeRealtimeCell {
document_range,
source: Span::ranged(source_owner, 0..source.len()),
tokens: CellTokenStream::default(),
syntax: None,
lexer_diagnostics: vec![error],
parse_error: None,
state,
boundary: NativeCellBoundary::Unbounded,
}
}