use super::analysis::{worker, AnalysisTarget};
use super::Editor;
use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use strop_core::id::{BufferRevision, DocumentId};
use strop_core::worker::{Completion, FailureKind, Outcome, Ticket};
use strop_core::Buffer;
use strop_grammar::{delimiter_pair, matching_delimiter_at, MatchCancelled};
fn near_delimiter(buf: &Buffer, caret: usize, insert: bool) -> bool {
let on = buf
.byte_at(caret)
.is_some_and(|byte| delimiter_pair(byte).is_some());
let typed = insert
&& caret
.checked_sub(1)
.and_then(|before| buf.byte_at(before))
.is_some_and(|byte| delimiter_pair(byte).is_some());
on || typed
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct MatchKey {
pub target: AnalysisTarget,
pub revision: BufferRevision,
pub caret: usize,
pub insert: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct PairMatch {
pub first: usize,
pub second: usize,
}
struct PendingMatch {
ticket: Ticket<MatchKey>,
cancel: Arc<AtomicBool>,
}
#[derive(Default)]
pub(crate) struct PairState {
pending: HashMap<AnalysisTarget, PendingMatch>,
cache: HashMap<AnalysisTarget, Vec<(MatchKey, Option<PairMatch>)>>,
}
impl PairState {
pub(crate) fn pending_empty(&self) -> bool {
self.pending.is_empty()
}
fn pending_covers(&self, key: &MatchKey) -> bool {
self.pending
.get(&key.target)
.is_some_and(|pending| pending.ticket.key == *key)
}
pub(crate) fn cancel_target(&mut self, target: &AnalysisTarget) {
if let Some(pending) = self.pending.remove(target) {
pending.cancel.store(true, Ordering::Release);
}
}
pub(crate) fn cancel_all(&mut self) {
for pending in self.pending.drain().map(|(_, pending)| pending) {
pending.cancel.store(true, Ordering::Release);
}
}
pub(crate) fn forget(&mut self, target: &AnalysisTarget) {
self.cancel_target(target);
self.cache.remove(target);
}
fn lookup(&self, key: &MatchKey) -> Option<Option<PairMatch>> {
self.cache
.get(&key.target)?
.iter()
.find(|(k, _)| k == key)
.map(|(_, value)| *value)
}
fn store(&mut self, key: MatchKey, value: Option<PairMatch>) {
let entries = self.cache.entry(key.target.clone()).or_default();
entries.retain(|(k, _)| *k != key);
const CACHED_MATCHES: usize = 8;
while entries.len() >= CACHED_MATCHES {
entries.remove(0);
}
entries.push((key, value));
}
}
pub(crate) fn match_delimiters(
buf: &Buffer,
caret: usize,
insert: bool,
cancelled: impl Fn() -> bool,
) -> Result<Option<(usize, usize)>, MatchCancelled> {
if cancelled() {
return Err(MatchCancelled);
}
let probe = if buf.byte_at(caret).and_then(delimiter_pair).is_some() {
Some(caret)
} else {
caret
.checked_sub(1)
.filter(|_| insert)
.filter(|&at| buf.byte_at(at).and_then(delimiter_pair).is_some())
};
let Some(probe) = probe else { return Ok(None) };
matching_delimiter_at(buf, probe, cancelled)
.map(|mate| mate.map(|mate| (probe.min(mate), probe.max(mate))))
}
impl Editor {
pub fn pair_highlight(
&mut self,
doc: DocumentId,
caret: usize,
insert: bool,
) -> [Option<usize>; 2] {
const NONE: [Option<usize>; 2] = [None, None];
if self.finishing {
return NONE;
}
let Some((source, source_caret)) = self.source_position(doc, caret) else {
return NONE;
};
let near = match self.docs.get(source) {
Some(document) => near_delimiter(&document.buf, source_caret, insert),
None => return NONE,
};
if !near {
self.analysis
.pair
.cancel_target(&AnalysisTarget::Document(source));
return NONE;
}
let Some(document) = self.docs.get(source) else {
return NONE;
};
let key = MatchKey {
target: AnalysisTarget::Document(source),
revision: document.buf.revision(),
caret: source_caret,
insert,
};
let rope = document.buf.snapshot();
match self.analysis.pair.lookup(&key) {
Some(Some(pair)) => [
self.view_byte_for_source(doc, source, pair.first),
self.view_byte_for_source(doc, source, pair.second),
],
Some(None) => NONE,
None => {
self.request_match(key, rope);
NONE
}
}
}
pub fn pair_highlight_cached(
&self,
doc: DocumentId,
caret: usize,
insert: bool,
) -> [Option<usize>; 2] {
const NONE: [Option<usize>; 2] = [None, None];
if self.finishing {
return NONE;
}
let Some((source, source_caret)) = self.source_position(doc, caret) else {
return NONE;
};
let near = match self.docs.get(source) {
Some(document) => near_delimiter(&document.buf, source_caret, insert),
None => return NONE,
};
if !near {
return NONE;
}
let Some(document) = self.docs.get(source) else {
return NONE;
};
let key = MatchKey {
target: AnalysisTarget::Document(source),
revision: document.buf.revision(),
caret: source_caret,
insert,
};
match self.analysis.pair.lookup(&key) {
Some(Some(pair)) => [
self.view_byte_for_source(doc, source, pair.first),
self.view_byte_for_source(doc, source, pair.second),
],
_ => NONE,
}
}
fn view_byte_for_source(
&self,
doc: DocumentId,
source: DocumentId,
byte: usize,
) -> Option<usize> {
if !self.collections.contains_key(&doc) {
return (source == doc).then_some(byte);
}
let collection = self.collections.get(&doc)?;
let source_buf = &self.docs.get(source)?.buf;
let view_buf = &self.docs.get(doc)?.buf;
let target_line = source_buf.line_of(byte.min(source_buf.len_bytes()));
for excerpt in &collection.excerpts {
if excerpt.source != source {
continue;
}
let first_line = source_buf.line_of(excerpt.start);
if target_line < first_line || target_line >= first_line + excerpt.view_lines {
continue;
}
let view_line = excerpt.view_line + 1 + (target_line - first_line);
if view_line > view_buf.last_content_line() {
return None;
}
let col = byte.saturating_sub(source_buf.line_start(target_line));
let start = view_buf.line_start(view_line);
return Some((start + col).min(view_buf.line_end(view_line)));
}
None
}
fn request_match(&mut self, key: MatchKey, rope: ropey::Rope) {
if self.analysis.pair.pending_covers(&key) {
return; }
if let Err(error) = self.analysis.start(&self.tape) {
self.message = format!("analysis: {error}");
return;
}
self.analysis.pair.cancel_target(&key.target);
let request = match self.worker_ids.allocate() {
Ok(id) => id,
Err(error) => {
self.message = error.message;
return;
}
};
let ticket = Ticket {
request,
key: key.clone(),
};
let cancel = Arc::new(AtomicBool::new(false));
self.analysis.register(key.target.clone());
self.analysis.pair.pending.insert(
key.target.clone(),
PendingMatch {
ticket: ticket.clone(),
cancel: cancel.clone(),
},
);
match self.tape.request("analysis.match", &ticket) {
Ok(false) => return,
Ok(true) => {}
Err(error) => {
self.handle_match(Completion {
ticket,
outcome: Outcome::failed(FailureKind::Protocol, error.to_string()),
});
return;
}
}
let work = worker::MatchWork {
ticket,
rope,
cancel,
};
let failed = match self.analysis.worker() {
Some(worker) => worker.match_work(work).err(),
None => Some(Box::new(work)),
};
if let Some(work) = failed {
self.handle_match(Completion {
ticket: work.ticket,
outcome: Outcome::failed(
FailureKind::Disconnected,
"display analysis worker stopped",
),
});
}
}
pub(crate) fn handle_match(&mut self, completion: Completion<MatchKey, Option<PairMatch>>) {
let key = completion.ticket.key;
if !self
.analysis
.pair
.pending
.get(&key.target)
.is_some_and(|pending| pending.ticket.request == completion.ticket.request)
{
return;
}
self.analysis.pair.pending.remove(&key.target);
let current = match &key.target {
AnalysisTarget::Document(document) => self
.docs
.get(*document)
.is_some_and(|document| document.buf.revision() == key.revision),
AnalysisTarget::Preview(path) => self.previews.contains_key(path),
};
if !current {
return;
}
match completion.outcome {
Outcome::Success(value) => self.analysis.pair.store(key, value),
Outcome::Failed { .. } => self.analysis.pair.store(key, None),
Outcome::Cancelled(_) => {}
}
}
}
#[cfg(test)]
mod tests;