use std::cell::RefCell;
use std::collections::HashMap;
use super::error::{EvalResult, Flow, signal};
use super::intern::resolve_sym;
use super::value::{RuntimeBindingValue, Value, ValueKind, VecLikeType, list_to_vec};
use crate::buffer::{Buffer, BufferManager};
thread_local! {
static STANDARD_SYNTAX_TABLE_OBJECT: RefCell<Option<Value>> = const { RefCell::new(None) };
}
pub fn reset_syntax_thread_locals() {
STANDARD_SYNTAX_TABLE_OBJECT.with(|slot| *slot.borrow_mut() = None);
}
pub(crate) fn restore_standard_syntax_table_object(table: Value) {
STANDARD_SYNTAX_TABLE_OBJECT.with(|slot| *slot.borrow_mut() = Some(table));
}
pub(crate) fn snapshot_standard_syntax_table_object() -> Option<Value> {
STANDARD_SYNTAX_TABLE_OBJECT.with(|slot| *slot.borrow())
}
pub fn collect_syntax_gc_roots(roots: &mut Vec<Value>) {
STANDARD_SYNTAX_TABLE_OBJECT.with(|slot| {
if let Some(v) = *slot.borrow() {
roots.push(v);
}
});
}
pub fn init_syntax_vars(
obarray: &mut super::symbol::Obarray,
_custom: &mut super::custom::CustomManager,
) {
obarray.set_symbol_value("parse-sexp-ignore-comments", Value::NIL);
obarray.set_symbol_value("parse-sexp-lookup-properties", Value::NIL);
obarray.set_symbol_value("syntax-propertize--done", Value::fixnum(-1));
obarray.set_symbol_value("words-include-escapes", Value::NIL);
obarray.set_symbol_value("multibyte-syntax-as-symbol", Value::NIL);
obarray.set_symbol_value("open-paren-in-column-0-is-defun-start", Value::T);
obarray.set_symbol_value(
"find-word-boundary-function-table",
super::chartable::make_char_table_value(Value::NIL, Value::NIL),
);
obarray.set_symbol_value("comment-end-can-be-escaped", Value::NIL);
obarray.set_symbol_value("forward-comment-function", Value::NIL);
for name in &[
"parse-sexp-ignore-comments",
"parse-sexp-lookup-properties",
"syntax-propertize--done",
"words-include-escapes",
"multibyte-syntax-as-symbol",
"open-paren-in-column-0-is-defun-start",
"find-word-boundary-function-table",
"comment-end-can-be-escaped",
"forward-comment-function",
] {
obarray.make_special(name);
}
for name in ["syntax-propertize--done", "comment-end-can-be-escaped"] {
let id = crate::emacs_core::intern::intern(name);
let default = obarray
.find_symbol_value(id)
.unwrap_or(crate::emacs_core::value::Value::NIL);
obarray.make_symbol_localized(id, default);
obarray.set_blv_local_if_set(id, true);
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[repr(u8)]
pub enum SyntaxClass {
Whitespace = 0,
Punctuation = 1,
Word = 2,
Symbol = 3,
Open = 4,
Close = 5,
Quote = 6,
StringDelim = 7,
Math = 8,
Escape = 9,
CharQuote = 10,
Comment = 11,
EndComment = 12,
InheritStd = 13,
CommentFence = 14,
StringFence = 15,
}
impl SyntaxClass {
pub fn from_char(ch: char) -> Option<SyntaxClass> {
match ch {
' ' | '-' => Some(SyntaxClass::Whitespace),
'w' => Some(SyntaxClass::Word),
'_' => Some(SyntaxClass::Symbol),
'.' => Some(SyntaxClass::Punctuation),
'(' => Some(SyntaxClass::Open),
')' => Some(SyntaxClass::Close),
'\'' => Some(SyntaxClass::Quote),
'"' => Some(SyntaxClass::StringDelim),
'$' => Some(SyntaxClass::Math),
'\\' => Some(SyntaxClass::Escape),
'/' => Some(SyntaxClass::CharQuote),
'<' => Some(SyntaxClass::Comment),
'>' => Some(SyntaxClass::EndComment),
'@' => Some(SyntaxClass::InheritStd),
'!' => Some(SyntaxClass::CommentFence),
'|' => Some(SyntaxClass::StringFence),
_ => None,
}
}
pub fn to_char(self) -> char {
match self {
SyntaxClass::Whitespace => ' ',
SyntaxClass::Word => 'w',
SyntaxClass::Symbol => '_',
SyntaxClass::Punctuation => '.',
SyntaxClass::Open => '(',
SyntaxClass::Close => ')',
SyntaxClass::Quote => '\'',
SyntaxClass::StringDelim => '"',
SyntaxClass::Math => '$',
SyntaxClass::Escape => '\\',
SyntaxClass::CharQuote => '/',
SyntaxClass::Comment => '<',
SyntaxClass::EndComment => '>',
SyntaxClass::InheritStd => '@',
SyntaxClass::CommentFence => '!',
SyntaxClass::StringFence => '|',
}
}
#[inline]
pub fn code(self) -> i64 {
self as i64
}
pub fn from_code(n: i64) -> Option<SyntaxClass> {
match n & 0xFF {
0 => Some(SyntaxClass::Whitespace),
1 => Some(SyntaxClass::Punctuation),
2 => Some(SyntaxClass::Word),
3 => Some(SyntaxClass::Symbol),
4 => Some(SyntaxClass::Open),
5 => Some(SyntaxClass::Close),
6 => Some(SyntaxClass::Quote),
7 => Some(SyntaxClass::StringDelim),
8 => Some(SyntaxClass::Math),
9 => Some(SyntaxClass::Escape),
10 => Some(SyntaxClass::CharQuote),
11 => Some(SyntaxClass::Comment),
12 => Some(SyntaxClass::EndComment),
13 => Some(SyntaxClass::InheritStd),
14 => Some(SyntaxClass::CommentFence),
15 => Some(SyntaxClass::StringFence),
_ => None,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct SyntaxFlags(u8);
impl SyntaxFlags {
pub const COMMENT_START_FIRST: SyntaxFlags = SyntaxFlags(0b0000_0001);
pub const COMMENT_START_SECOND: SyntaxFlags = SyntaxFlags(0b0000_0010);
pub const COMMENT_END_FIRST: SyntaxFlags = SyntaxFlags(0b0000_0100);
pub const COMMENT_END_SECOND: SyntaxFlags = SyntaxFlags(0b0000_1000);
pub const PREFIX: SyntaxFlags = SyntaxFlags(0b0001_0000);
pub const COMMENT_STYLE_B: SyntaxFlags = SyntaxFlags(0b0010_0000);
pub const COMMENT_NESTABLE: SyntaxFlags = SyntaxFlags(0b0100_0000);
pub const COMMENT_STYLE_C: SyntaxFlags = SyntaxFlags(0b1000_0000);
pub const fn new(bits: u8) -> Self {
SyntaxFlags(bits)
}
pub const fn empty() -> Self {
SyntaxFlags(0)
}
pub const fn is_empty(self) -> bool {
self.0 == 0
}
pub const fn contains(self, other: SyntaxFlags) -> bool {
(self.0 & other.0) == other.0
}
pub const fn bits(self) -> u8 {
self.0
}
}
impl std::ops::BitOr for SyntaxFlags {
type Output = SyntaxFlags;
fn bitor(self, rhs: SyntaxFlags) -> SyntaxFlags {
SyntaxFlags(self.0 | rhs.0)
}
}
impl std::ops::BitOrAssign for SyntaxFlags {
fn bitor_assign(&mut self, rhs: SyntaxFlags) {
self.0 |= rhs.0;
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SyntaxEntry {
pub class: SyntaxClass,
pub matching_char: Option<char>,
pub flags: SyntaxFlags,
}
impl SyntaxEntry {
pub fn simple(class: SyntaxClass) -> Self {
Self {
class,
matching_char: None,
flags: SyntaxFlags::empty(),
}
}
pub fn with_match(class: SyntaxClass, matching: char) -> Self {
Self {
class,
matching_char: Some(matching),
flags: SyntaxFlags::empty(),
}
}
}
pub fn string_to_syntax(s: &str) -> Result<SyntaxEntry, String> {
let chars: Vec<char> = s.chars().collect();
if chars.is_empty() {
return Err("Empty syntax descriptor".to_string());
}
let class = SyntaxClass::from_char(chars[0])
.ok_or_else(|| format!("Invalid syntax class character: '{}'", chars[0]))?;
let matching_char = if chars.len() > 1 && chars[1] != ' ' {
Some(chars[1])
} else {
None
};
let mut flags = SyntaxFlags::empty();
let flag_start = if chars.len() > 1 { 2 } else { 1 };
for &ch in chars.get(flag_start..).unwrap_or(&[]) {
match ch {
'1' => flags |= SyntaxFlags::COMMENT_START_FIRST,
'2' => flags |= SyntaxFlags::COMMENT_START_SECOND,
'3' => flags |= SyntaxFlags::COMMENT_END_FIRST,
'4' => flags |= SyntaxFlags::COMMENT_END_SECOND,
'p' => flags |= SyntaxFlags::PREFIX,
'b' => flags |= SyntaxFlags::COMMENT_STYLE_B,
'n' => flags |= SyntaxFlags::COMMENT_NESTABLE,
'c' => flags |= SyntaxFlags::COMMENT_STYLE_C,
' ' => {} _ => {} }
}
Ok(SyntaxEntry {
class,
matching_char,
flags,
})
}
fn syntax_runtime_string(value: &Value) -> Result<String, Flow> {
value.as_runtime_string_owned().ok_or_else(|| {
signal(
"wrong-type-argument",
vec![Value::symbol("stringp"), *value],
)
})
}
pub fn syntax_entry_to_value(entry: &SyntaxEntry) -> Value {
let code = entry.class.code() | ((entry.flags.bits() as i64) << 16);
let matching = match entry.matching_char {
Some(ch) => Value::fixnum(ch as i64),
None => Value::NIL,
};
Value::cons(Value::fixnum(code), matching)
}
#[derive(Clone, Copy, Debug)]
pub struct SyntaxTable {
chartable: Value,
}
impl SyntaxTable {
pub fn new_standard() -> Self {
match ensure_standard_syntax_table_object() {
Ok(table) => Self { chartable: table },
Err(_) => Self {
chartable: Value::NIL,
},
}
}
pub fn make_syntax_table() -> Self {
Self::new_standard()
}
pub(crate) fn from_chartable(chartable: Value) -> Self {
Self { chartable }
}
pub fn for_buffer(buf: &crate::buffer::buffer::Buffer) -> Self {
Self {
chartable: buf.syntax_chartable(),
}
}
pub fn isolate_for_buffer(buf: &mut crate::buffer::buffer::Buffer) -> Self {
use crate::buffer::buffer::BUFFER_SLOT_SYNTAX_TABLE;
let slot = buf.slots[BUFFER_SLOT_SYNTAX_TABLE];
let source = if slot.is_nil() {
ensure_standard_syntax_table_object().unwrap_or(Value::NIL)
} else {
slot
};
let own = if source.is_nil() {
Value::NIL
} else {
builtin_copy_syntax_table(vec![source]).unwrap_or(source)
};
buf.slots[BUFFER_SLOT_SYNTAX_TABLE] = own;
Self { chartable: own }
}
pub fn copy_syntax_table(&self) -> Self {
if self.chartable.is_nil() {
return self.clone();
}
match builtin_copy_syntax_table(vec![self.chartable]) {
Ok(copy) => Self { chartable: copy },
Err(_) => self.clone(),
}
}
pub(crate) fn chartable(&self) -> Value {
self.chartable
}
pub fn get_entry(&self, ch: char) -> Option<SyntaxEntry> {
self.get_entry_code(ch as u32)
}
pub fn get_entry_code(&self, code: u32) -> Option<SyntaxEntry> {
syntax_entry_at_char_code(&self.chartable, code)
}
pub fn char_syntax(&self, ch: char) -> SyntaxClass {
self.char_syntax_code(ch as u32)
}
pub fn char_syntax_code(&self, code: u32) -> SyntaxClass {
syntax_class_at_char_code(&self.chartable, code)
}
pub fn modify_syntax_entry(&mut self, ch: char, entry: SyntaxEntry) {
if self.chartable.is_nil() {
return;
}
let _ = super::chartable::builtin_set_char_table_range(vec![
self.chartable,
Value::fixnum(ch as i64),
syntax_entry_to_value(&entry),
]);
}
}
impl Default for SyntaxTable {
fn default() -> Self {
Self::new_standard()
}
}
pub fn forward_word(buf: &Buffer, table: &SyntaxTable, count: i64) -> usize {
forward_word_with_options(buf, table, count, false)
}
fn syntax_char_from_code(code: u32) -> char {
super::builtins::character_code_to_rust_char(code as i64).unwrap_or('\u{FFFD}')
}
fn buffer_chars_in_range(buf: &Buffer, start: usize, end: usize) -> Vec<char> {
let string = buf.buffer_substring_lisp_string(start, end);
crate::emacs_core::builtins::lisp_string_char_codes(&string)
.into_iter()
.map(syntax_char_from_code)
.collect()
}
fn forward_word_with_options(
buf: &Buffer,
table: &SyntaxTable,
count: i64,
honor_properties: bool,
) -> usize {
if count < 0 {
return backward_word_with_options(buf, table, -count, honor_properties);
}
let chars = buffer_chars_in_range(buf, buf.point_min(), buf.point_max());
let base = buf.point_min();
let rel_byte = buf.point().saturating_sub(base);
let mut idx = buf.text.emacs_byte_to_char(base + rel_byte) - buf.text.emacs_byte_to_char(base);
let accessible_char_start = buf.text.emacs_byte_to_char(base);
let accessible_char_end = buf.point_max_char();
let accessible_len = accessible_char_end - accessible_char_start;
for _ in 0..count {
while idx < accessible_len
&& !matches!(
effective_syntax_entry_for_abs_char(
buf,
table,
chars[idx],
accessible_char_start + idx,
honor_properties,
)
.class,
SyntaxClass::Word
)
{
idx += 1;
}
while idx < accessible_len
&& matches!(
effective_syntax_entry_for_abs_char(
buf,
table,
chars[idx],
accessible_char_start + idx,
honor_properties,
)
.class,
SyntaxClass::Word
)
{
idx += 1;
}
}
let abs_char = accessible_char_start + idx;
buf.text.char_to_emacs_byte(abs_char)
}
pub fn backward_word(buf: &Buffer, table: &SyntaxTable, count: i64) -> usize {
backward_word_with_options(buf, table, count, false)
}
fn backward_word_with_options(
buf: &Buffer,
table: &SyntaxTable,
count: i64,
honor_properties: bool,
) -> usize {
if count < 0 {
return forward_word_with_options(buf, table, -count, honor_properties);
}
let chars = buffer_chars_in_range(buf, buf.point_min(), buf.point_max());
let base = buf.point_min();
let rel_byte = buf.point().saturating_sub(base);
let mut idx = buf.text.emacs_byte_to_char(base + rel_byte) - buf.text.emacs_byte_to_char(base);
let accessible_char_start = buf.text.emacs_byte_to_char(base);
for _ in 0..count {
while idx > 0
&& !matches!(
effective_syntax_entry_for_abs_char(
buf,
table,
chars[idx - 1],
accessible_char_start + idx - 1,
honor_properties,
)
.class,
SyntaxClass::Word
)
{
idx -= 1;
}
while idx > 0
&& matches!(
effective_syntax_entry_for_abs_char(
buf,
table,
chars[idx - 1],
accessible_char_start + idx - 1,
honor_properties,
)
.class,
SyntaxClass::Word
)
{
idx -= 1;
}
}
let abs_char = accessible_char_start + idx;
buf.text.char_to_emacs_byte(abs_char)
}
pub fn skip_syntax_forward(
buf: &Buffer,
table: &SyntaxTable,
syntax_chars: &str,
limit: Option<usize>,
) -> usize {
skip_syntax_forward_with_options(buf, table, syntax_chars, limit, false)
}
fn skip_syntax_forward_with_options(
buf: &Buffer,
table: &SyntaxTable,
syntax_chars: &str,
limit: Option<usize>,
honor_properties: bool,
) -> usize {
let classes: Vec<SyntaxClass> = syntax_chars
.chars()
.filter_map(SyntaxClass::from_char)
.collect();
let chars = buffer_chars_in_range(buf, buf.point_min(), buf.point_max());
let base = buf.point_min();
let rel_byte = buf.point().saturating_sub(base);
let mut idx = buf.text.emacs_byte_to_char(base + rel_byte) - buf.text.emacs_byte_to_char(base);
let accessible_char_start = buf.text.emacs_byte_to_char(base);
let accessible_char_end = buf.point_max_char();
let accessible_len = accessible_char_end - accessible_char_start;
let char_limit = limit
.map(|lim| {
let lim_clamped = lim.min(buf.point_max());
buf.text.emacs_byte_to_char(lim_clamped) - accessible_char_start
})
.unwrap_or(accessible_len);
while idx < char_limit {
let syn = effective_syntax_entry_for_abs_char(
buf,
table,
chars[idx],
accessible_char_start + idx,
honor_properties,
)
.class;
if !classes.contains(&syn) {
break;
}
idx += 1;
}
let abs_char = accessible_char_start + idx;
buf.text.char_to_emacs_byte(abs_char)
}
pub fn skip_syntax_backward(
buf: &Buffer,
table: &SyntaxTable,
syntax_chars: &str,
limit: Option<usize>,
) -> usize {
skip_syntax_backward_with_options(buf, table, syntax_chars, limit, false)
}
fn skip_syntax_backward_with_options(
buf: &Buffer,
table: &SyntaxTable,
syntax_chars: &str,
limit: Option<usize>,
honor_properties: bool,
) -> usize {
let classes: Vec<SyntaxClass> = syntax_chars
.chars()
.filter_map(SyntaxClass::from_char)
.collect();
let chars = buffer_chars_in_range(buf, buf.point_min(), buf.point_max());
let base = buf.point_min();
let rel_byte = buf.point().saturating_sub(base);
let mut idx = buf.text.emacs_byte_to_char(base + rel_byte) - buf.text.emacs_byte_to_char(base);
let accessible_char_start = buf.text.emacs_byte_to_char(base);
let char_limit = limit
.map(|lim| {
let lim_clamped = lim.max(base);
buf.text.emacs_byte_to_char(lim_clamped) - accessible_char_start
})
.unwrap_or(0);
while idx > char_limit {
let syn = effective_syntax_entry_for_abs_char(
buf,
table,
chars[idx - 1],
accessible_char_start + idx - 1,
honor_properties,
)
.class;
if !classes.contains(&syn) {
break;
}
idx -= 1;
}
let abs_char = accessible_char_start + idx;
buf.text.char_to_emacs_byte(abs_char)
}
pub fn scan_sexps(
buf: &Buffer,
table: &SyntaxTable,
from: usize,
count: i64,
) -> Result<usize, String> {
scan_sexps_with_options(buf, table, from, count, false)
}
fn scan_sexps_with_options(
buf: &Buffer,
table: &SyntaxTable,
from: usize,
count: i64,
honor_properties: bool,
) -> Result<usize, String> {
if count == 0 {
return Ok(from);
}
let chars = buffer_chars_in_range(buf, 0, buf.total_bytes());
let total_chars = chars.len();
let mut idx = buf.text.emacs_byte_to_char(from);
if count > 0 {
for _ in 0..count {
idx = scan_sexp_forward(buf, &chars, total_chars, idx, table, honor_properties)?;
}
} else {
for _ in 0..(-count) {
idx = scan_sexp_backward(buf, &chars, idx, table, honor_properties)?;
}
}
Ok(buf.text.char_to_emacs_byte(idx))
}
fn skip_string_forward(
buf: &Buffer,
chars: &[char],
mut idx: usize,
stop: usize,
table: &SyntaxTable,
honor_properties: bool,
delimiter: char,
delimiter_class: SyntaxClass,
) -> Result<usize, String> {
while idx < stop {
let c = chars[idx];
let class = effective_syntax_entry_for_abs_char(buf, table, c, idx, honor_properties).class;
if class == delimiter_class
&& (delimiter_class == SyntaxClass::StringFence || c == delimiter)
{
return Ok(idx + 1);
}
if matches!(class, SyntaxClass::Escape | SyntaxClass::CharQuote) {
idx += 1;
}
idx += 1;
}
Err("Scan error: unbalanced parentheses".to_string())
}
fn skip_string_backward(
buf: &Buffer,
chars: &[char],
mut idx: usize,
stop: usize,
table: &SyntaxTable,
honor_properties: bool,
delimiter: char,
delimiter_class: SyntaxClass,
) -> Result<usize, String> {
while idx > stop {
idx -= 1;
let c = chars[idx];
let class = effective_syntax_entry_for_abs_char(buf, table, c, idx, honor_properties).class;
if class == delimiter_class
&& (delimiter_class == SyntaxClass::StringFence || c == delimiter)
{
return Ok(idx);
}
}
Err("Scan error: unbalanced parentheses".to_string())
}
fn scan_lists_with_options(
buf: &Buffer,
table: &SyntaxTable,
from: usize,
count: i64,
initial_depth: i64,
honor_properties: bool,
) -> Result<Option<usize>, String> {
let chars = buffer_chars_in_range(buf, 0, buf.total_bytes());
let mut idx = from;
let start = buf.point_min_char();
let stop = buf.point_max_char();
let mut depth = initial_depth;
let min_depth = if depth > 0 { 0 } else { depth };
if count > 0 {
let mut remaining = count;
while remaining > 0 {
let mut found = false;
while idx < stop {
let ch = chars[idx];
let class =
effective_syntax_entry_for_abs_char(buf, table, ch, idx, honor_properties)
.class;
idx += 1;
match class {
SyntaxClass::Open => {
depth += 1;
if depth == 0 {
found = true;
break;
}
}
SyntaxClass::Close => {
depth -= 1;
if depth == 0 {
found = true;
break;
}
if depth < min_depth {
return Err(
"Scan error: containing expression ends prematurely".to_string()
);
}
}
SyntaxClass::StringDelim | SyntaxClass::StringFence => {
idx = skip_string_forward(
buf,
&chars,
idx,
stop,
table,
honor_properties,
ch,
class,
)?;
}
SyntaxClass::Escape | SyntaxClass::CharQuote => {
if idx >= stop {
return Err("Scan error: unbalanced parentheses".to_string());
}
idx += 1;
}
_ => {}
}
}
if depth != 0 {
return Err("Scan error: unbalanced parentheses".to_string());
}
if !found {
return Ok(None);
}
remaining -= 1;
}
Ok(Some(idx))
} else if count < 0 {
let mut remaining = count;
while remaining < 0 {
let mut found = false;
while idx > start {
idx -= 1;
let ch = chars[idx];
let class =
effective_syntax_entry_for_abs_char(buf, table, ch, idx, honor_properties)
.class;
match class {
SyntaxClass::Close => {
depth += 1;
if depth == 0 {
found = true;
break;
}
}
SyntaxClass::Open => {
depth -= 1;
if depth == 0 {
found = true;
break;
}
if depth < min_depth {
return Err(
"Scan error: containing expression ends prematurely".to_string()
);
}
}
SyntaxClass::StringDelim | SyntaxClass::StringFence => {
idx = skip_string_backward(
buf,
&chars,
idx,
start,
table,
honor_properties,
ch,
class,
)?;
}
_ => {}
}
}
if depth != 0 {
return Err("Scan error: unbalanced parentheses".to_string());
}
if !found {
return Ok(None);
}
remaining += 1;
}
Ok(Some(idx))
} else {
Ok(Some(idx))
}
}
fn scan_sexp_forward(
buf: &Buffer,
chars: &[char],
len: usize,
start: usize,
table: &SyntaxTable,
honor_properties: bool,
) -> Result<usize, String> {
let mut idx = start;
while idx < len
&& matches!(
effective_syntax_entry_for_abs_char(buf, table, chars[idx], idx, honor_properties)
.class,
SyntaxClass::Whitespace
| SyntaxClass::Comment
| SyntaxClass::EndComment
| SyntaxClass::Punctuation
| SyntaxClass::Quote
)
{
idx += 1;
}
if idx >= len {
return Err("Scan error: unbalanced parentheses".to_string());
}
let ch = chars[idx];
let syn_entry = effective_syntax_entry_for_abs_char(buf, table, ch, idx, honor_properties);
let syn = syn_entry.class;
match syn {
SyntaxClass::Open => {
let close_char = syn_entry.matching_char.unwrap_or(')');
let mut depth = 1i32;
idx += 1;
while idx < len && depth > 0 {
let c = chars[idx];
let s =
effective_syntax_entry_for_abs_char(buf, table, c, idx, honor_properties).class;
match s {
SyntaxClass::Open => {
depth += 1;
}
SyntaxClass::Close => {
depth -= 1;
}
SyntaxClass::StringDelim | SyntaxClass::StringFence => {
let delim_class = s;
idx += 1;
while idx < len {
let sc = effective_syntax_entry_for_abs_char(
buf,
table,
chars[idx],
idx,
honor_properties,
)
.class;
if sc == delim_class
&& (s == SyntaxClass::StringFence || chars[idx] == c)
{
break;
}
if matches!(sc, SyntaxClass::Escape) {
idx += 1; }
idx += 1;
}
}
SyntaxClass::Escape => {
idx += 1; }
_ => {}
}
idx += 1;
}
if depth != 0 {
return Err(format!(
"Scan error: unbalanced parentheses (looking for '{}')",
close_char
));
}
Ok(idx)
}
SyntaxClass::Close => {
Err("Scan error: unbalanced parentheses (unexpected close)".to_string())
}
SyntaxClass::StringDelim | SyntaxClass::StringFence => {
let delim_class = syn;
idx += 1;
while idx < len {
let c = chars[idx];
let s =
effective_syntax_entry_for_abs_char(buf, table, c, idx, honor_properties).class;
if s == delim_class && (syn == SyntaxClass::StringFence || c == ch) {
break;
}
if matches!(s, SyntaxClass::Escape) {
idx += 1; }
idx += 1;
}
if idx >= len {
return Err("Scan error: unterminated string".to_string());
}
Ok(idx + 1) }
SyntaxClass::Word | SyntaxClass::Symbol => {
while idx < len
&& matches!(
effective_syntax_entry_for_abs_char(
buf,
table,
chars[idx],
idx,
honor_properties,
)
.class,
SyntaxClass::Word | SyntaxClass::Symbol
)
{
idx += 1;
}
Ok(idx)
}
SyntaxClass::Escape | SyntaxClass::CharQuote => {
idx += 1;
if idx < len {
idx += 1;
}
Ok(idx)
}
SyntaxClass::Math => {
let delim = ch;
idx += 1;
while idx < len && chars[idx] != delim {
idx += 1;
}
if idx >= len {
return Err("Scan error: unterminated math delimiter".to_string());
}
Ok(idx + 1)
}
_ => {
Ok(idx + 1)
}
}
}
fn scan_sexp_backward(
buf: &Buffer,
chars: &[char],
start: usize,
table: &SyntaxTable,
honor_properties: bool,
) -> Result<usize, String> {
let mut idx = start;
while idx > 0
&& matches!(
effective_syntax_entry_for_abs_char(
buf,
table,
chars[idx - 1],
idx - 1,
honor_properties
)
.class,
SyntaxClass::Whitespace
| SyntaxClass::Comment
| SyntaxClass::EndComment
| SyntaxClass::Punctuation
| SyntaxClass::Quote
)
{
idx -= 1;
}
if idx == 0 {
return Err("Scan error: beginning of buffer".to_string());
}
idx -= 1; let ch = chars[idx];
let syn_entry = effective_syntax_entry_for_abs_char(buf, table, ch, idx, honor_properties);
let syn = syn_entry.class;
match syn {
SyntaxClass::Close => {
let open_char = syn_entry.matching_char.unwrap_or('(');
let mut depth = 1i32;
while idx > 0 && depth > 0 {
idx -= 1;
let c = chars[idx];
let s =
effective_syntax_entry_for_abs_char(buf, table, c, idx, honor_properties).class;
match s {
SyntaxClass::Close => {
depth += 1;
}
SyntaxClass::Open => {
depth -= 1;
}
SyntaxClass::StringDelim | SyntaxClass::StringFence => {
let delim_class = s;
if idx > 0 {
idx -= 1;
while idx > 0 {
let sc = effective_syntax_entry_for_abs_char(
buf,
table,
chars[idx],
idx,
honor_properties,
)
.class;
if sc == delim_class
&& (s == SyntaxClass::StringFence || chars[idx] == c)
{
break;
}
idx -= 1;
}
}
}
_ => {}
}
}
if depth != 0 {
return Err(format!(
"Scan error: unbalanced parentheses (looking for '{}')",
open_char
));
}
Ok(idx)
}
SyntaxClass::Open => {
Err("Scan error: unbalanced parentheses (unexpected open)".to_string())
}
SyntaxClass::StringDelim | SyntaxClass::StringFence => {
let delim_class = syn;
if idx == 0 {
return Err("Scan error: unterminated string".to_string());
}
idx -= 1;
while idx > 0 {
let c = chars[idx];
let s =
effective_syntax_entry_for_abs_char(buf, table, c, idx, honor_properties).class;
if s == delim_class && (syn == SyntaxClass::StringFence || c == ch) {
break;
}
idx -= 1;
}
let c = chars[idx];
let s = effective_syntax_entry_for_abs_char(buf, table, c, idx, honor_properties).class;
if !(s == delim_class && (syn == SyntaxClass::StringFence || c == ch)) {
return Err("Scan error: unterminated string".to_string());
}
Ok(idx)
}
SyntaxClass::Word | SyntaxClass::Symbol => {
while idx > 0
&& matches!(
effective_syntax_entry_for_abs_char(
buf,
table,
chars[idx - 1],
idx - 1,
honor_properties,
)
.class,
SyntaxClass::Word | SyntaxClass::Symbol
)
{
idx -= 1;
}
Ok(idx)
}
SyntaxClass::Escape | SyntaxClass::CharQuote => {
Ok(idx)
}
SyntaxClass::Math => {
let delim = ch;
if idx == 0 {
return Err("Scan error: unterminated math delimiter".to_string());
}
idx -= 1;
while idx > 0 && chars[idx] != delim {
idx -= 1;
}
if chars[idx] != delim {
return Err("Scan error: unterminated math delimiter".to_string());
}
Ok(idx)
}
_ => {
Ok(idx)
}
}
}
pub(crate) fn builtin_string_to_syntax(args: Vec<Value>) -> EvalResult {
if args.len() != 1 {
return Err(signal(
"wrong-number-of-arguments",
vec![
Value::symbol("string-to-syntax"),
Value::fixnum(args.len() as i64),
],
));
}
let s = syntax_runtime_string(&args[0])?;
let entry = string_to_syntax(&s).map_err(|msg| signal("error", vec![Value::string(&msg)]))?;
if matches!(entry.class, SyntaxClass::InheritStd) {
return Ok(Value::NIL);
}
Ok(syntax_entry_to_value(&entry))
}
pub(crate) fn builtin_make_syntax_table(args: Vec<Value>) -> EvalResult {
if args.len() > 1 {
return Err(signal(
"wrong-number-of-arguments",
vec![
Value::symbol("make-syntax-table"),
Value::fixnum(args.len() as i64),
],
));
}
let table = super::chartable::make_char_table_value(Value::symbol("syntax-table"), Value::NIL);
let parent = if args.is_empty() || args[0].is_nil() {
ensure_standard_syntax_table_object()?
} else {
args[0]
};
if !parent.is_nil() {
super::chartable::builtin_set_char_table_parent(vec![table, parent])?;
}
Ok(table)
}
pub(crate) fn builtin_copy_syntax_table(args: Vec<Value>) -> EvalResult {
if args.len() > 1 {
return Err(signal(
"wrong-number-of-arguments",
vec![
Value::symbol("copy-syntax-table"),
Value::fixnum(args.len() as i64),
],
));
}
let source = if args.is_empty() || args[0].is_nil() {
builtin_standard_syntax_table(vec![])?
} else {
let table = args[0];
if builtin_syntax_table_p(vec![table])?.is_nil() {
return Err(signal(
"wrong-type-argument",
vec![Value::symbol("syntax-table-p"), table],
));
}
table
};
match source.kind() {
ValueKind::Veclike(VecLikeType::Vector) => {
let copy = Value::vector(source.as_vector_data().unwrap().clone());
super::chartable::builtin_set_char_table_range(vec![copy, Value::NIL, Value::NIL])?;
if super::chartable::builtin_char_table_parent(vec![copy])?.is_nil() {
super::chartable::builtin_set_char_table_parent(vec![
copy,
ensure_standard_syntax_table_object()?,
])?;
}
Ok(copy)
}
other => Err(signal(
"wrong-type-argument",
vec![Value::symbol("syntax-table-p"), source],
)),
}
}
fn ensure_standard_syntax_table_object() -> EvalResult {
STANDARD_SYNTAX_TABLE_OBJECT.with(|slot| {
if let Some(table) = slot.borrow().as_ref() {
return Ok(*table);
}
let whitespace = syntax_entry_to_value(&SyntaxEntry::simple(SyntaxClass::Whitespace));
let punctuation = syntax_entry_to_value(&SyntaxEntry::simple(SyntaxClass::Punctuation));
let word = syntax_entry_to_value(&SyntaxEntry::simple(SyntaxClass::Word));
let table =
super::chartable::make_char_table_value(Value::symbol("syntax-table"), whitespace);
for cp in 0..=(' ' as i64 - 1) {
super::chartable::builtin_set_char_table_range(vec![
table,
Value::fixnum(cp),
punctuation,
])?;
}
super::chartable::builtin_set_char_table_range(vec![
table,
Value::fixnum(0x7f),
punctuation,
])?;
let set = |ch: char, e: SyntaxEntry| -> Result<(), Flow> {
super::chartable::builtin_set_char_table_range(vec![
table,
Value::fixnum(ch as i64),
syntax_entry_to_value(&e),
])
.map(|_| ())
};
for ch in [' ', '\t', '\n', '\r', '\u{000c}'] {
set(ch, SyntaxEntry::simple(SyntaxClass::Whitespace))?;
}
for ch in 'a'..='z' {
set(ch, SyntaxEntry::simple(SyntaxClass::Word))?;
}
for ch in 'A'..='Z' {
set(ch, SyntaxEntry::simple(SyntaxClass::Word))?;
}
for ch in '0'..='9' {
set(ch, SyntaxEntry::simple(SyntaxClass::Word))?;
}
set('$', SyntaxEntry::simple(SyntaxClass::Word))?;
set('%', SyntaxEntry::simple(SyntaxClass::Word))?;
set('(', SyntaxEntry::with_match(SyntaxClass::Open, ')'))?;
set(')', SyntaxEntry::with_match(SyntaxClass::Close, '('))?;
set('[', SyntaxEntry::with_match(SyntaxClass::Open, ']'))?;
set(']', SyntaxEntry::with_match(SyntaxClass::Close, '['))?;
set('{', SyntaxEntry::with_match(SyntaxClass::Open, '}'))?;
set('}', SyntaxEntry::with_match(SyntaxClass::Close, '{'))?;
set('"', SyntaxEntry::simple(SyntaxClass::StringDelim))?;
set('\\', SyntaxEntry::simple(SyntaxClass::Escape))?;
for ch in ['_', '-', '+', '*', '/', '&', '|', '<', '>', '='] {
set(ch, SyntaxEntry::simple(SyntaxClass::Symbol))?;
}
for ch in ['.', ',', ';', ':', '?', '!', '#', '@', '~', '^', '\'', '`'] {
set(ch, SyntaxEntry::simple(SyntaxClass::Punctuation))?;
}
super::chartable::builtin_set_char_table_range(vec![
table,
Value::cons(Value::fixnum(0x80), Value::fixnum(0x3F_FFFF)),
word,
])?;
*slot.borrow_mut() = Some(table);
Ok(table)
})
}
fn current_buffer_syntax_table_object_in_buffers(
buffers: &mut BufferManager,
) -> Result<Value, Flow> {
use crate::buffer::buffer::BUFFER_SLOT_SYNTAX_TABLE;
let fallback = ensure_standard_syntax_table_object()?;
let current_id = buffers
.current_buffer_id()
.ok_or_else(|| signal("error", vec![Value::string("No current buffer")]))?;
let buf = buffers
.get_mut(current_id)
.ok_or_else(|| signal("error", vec![Value::string("No current buffer")]))?;
let value = buf.slots[BUFFER_SLOT_SYNTAX_TABLE];
if !value.is_nil() && builtin_syntax_table_p(vec![value])?.is_truthy() {
return Ok(value);
}
buf.slots[BUFFER_SLOT_SYNTAX_TABLE] = fallback;
Ok(fallback)
}
fn current_buffer_syntax_table_object(eval: &mut super::eval::Context) -> Result<Value, Flow> {
current_buffer_syntax_table_object_in_buffers(&mut eval.buffers)
}
pub(crate) fn sync_current_buffer_syntax_table_state(
ctx: &mut super::eval::Context,
) -> Result<(), Flow> {
let _ = current_buffer_syntax_table_object_in_buffers(&mut ctx.buffers)?;
Ok(())
}
fn set_current_buffer_syntax_table_object_in_buffers(
buffers: &mut BufferManager,
table: Value,
) -> Result<(), Flow> {
use crate::buffer::buffer::BUFFER_SLOT_SYNTAX_TABLE;
let current_id = buffers
.current_buffer_id()
.ok_or_else(|| signal("error", vec![Value::string("No current buffer")]))?;
let buf = buffers
.get_mut(current_id)
.ok_or_else(|| signal("error", vec![Value::string("No current buffer")]))?;
buf.slots[BUFFER_SLOT_SYNTAX_TABLE] = table;
buf.set_slot_local_flag(BUFFER_SLOT_SYNTAX_TABLE, true);
Ok(())
}
fn set_current_buffer_syntax_table_object(
eval: &mut super::eval::Context,
table: Value,
) -> Result<(), Flow> {
set_current_buffer_syntax_table_object_in_buffers(&mut eval.buffers, table)
}
pub(crate) fn syntax_entry_at_char(table: &Value, c: char) -> Option<SyntaxEntry> {
syntax_entry_at_char_code(table, c as u32)
}
pub(crate) fn syntax_entry_at_char_code(table: &Value, code: u32) -> Option<SyntaxEntry> {
let effective = if table.is_nil() {
ensure_standard_syntax_table_object().unwrap_or(Value::NIL)
} else {
*table
};
if effective.is_nil() {
return None;
}
let entry = super::chartable::ct_lookup(&effective, code as i64).ok()?;
syntax_entry_from_chartable_entry(&entry)
}
pub(crate) fn syntax_class_at_char(table: &Value, c: char) -> SyntaxClass {
syntax_class_at_char_code(table, c as u32)
}
pub(crate) fn syntax_class_at_char_code(table: &Value, code: u32) -> SyntaxClass {
match syntax_entry_at_char_code(table, code) {
Some(entry) => entry.class,
None => {
if code >= 0x80 {
SyntaxClass::Word
} else {
SyntaxClass::Whitespace
}
}
}
}
fn syntax_entry_from_chartable_entry(entry: &Value) -> Option<SyntaxEntry> {
match entry.kind() {
ValueKind::Nil => None,
ValueKind::Cons => {
let pair_car = entry.cons_car();
let pair_cdr = entry.cons_cdr();
let code = match pair_car.kind() {
ValueKind::Fixnum(code) => code,
_ => return None,
};
let class = SyntaxClass::from_code(code)?;
let matching_char = match pair_cdr.kind() {
ValueKind::Fixnum(n) => char::from_u32(n as u32),
ValueKind::Nil => None,
_ => None,
};
Some(SyntaxEntry {
class,
matching_char,
flags: SyntaxFlags::new(((code >> 16) & 0xFF) as u8),
})
}
ValueKind::Fixnum(code) => Some(SyntaxEntry {
class: SyntaxClass::from_code(code)?,
matching_char: None,
flags: SyntaxFlags::new(((code >> 16) & 0xFF) as u8),
}),
_ => None,
}
}
fn syntax_table_from_chartable(table: Value) -> Result<SyntaxTable, Flow> {
if builtin_syntax_table_p(vec![table])?.is_nil() {
return Err(signal(
"wrong-type-argument",
vec![Value::symbol("syntax-table-p"), table],
));
}
Ok(SyntaxTable::from_chartable(table))
}
fn syntax_entry_from_syntax_property(prop: Value, ch: char) -> Option<SyntaxEntry> {
if builtin_syntax_table_p(vec![prop]).ok()?.is_truthy() {
let raw = super::chartable::builtin_char_table_range(vec![prop, Value::fixnum(ch as i64)])
.ok()?;
syntax_entry_from_chartable_entry(&raw)
} else {
syntax_entry_from_chartable_entry(&prop)
}
}
fn effective_syntax_entry_for_char_at_byte(
buf: &Buffer,
table: &SyntaxTable,
ch: char,
byte_pos: usize,
honor_properties: bool,
) -> SyntaxEntry {
if honor_properties
&& let Some(prop) = buf
.text
.text_props_get_property(byte_pos, Value::symbol("syntax-table"))
&& let Some(entry) = syntax_entry_from_syntax_property(prop, ch)
{
return entry;
}
table
.get_entry(ch)
.unwrap_or_else(|| SyntaxEntry::simple(table.char_syntax(ch)))
}
fn effective_syntax_entry_for_abs_char(
buf: &Buffer,
table: &SyntaxTable,
ch: char,
abs_char: usize,
honor_properties: bool,
) -> SyntaxEntry {
let byte_pos = buf.text.char_to_emacs_byte(abs_char);
effective_syntax_entry_for_char_at_byte(buf, table, ch, byte_pos, honor_properties)
}
fn parse_sexp_lookup_properties_enabled(ctx: &super::eval::Context) -> bool {
ctx.obarray
.symbol_value("parse-sexp-lookup-properties")
.copied()
.unwrap_or(Value::NIL)
.is_truthy()
}
pub(crate) fn builtin_syntax_class_to_char(args: Vec<Value>) -> EvalResult {
if args.len() != 1 {
return Err(signal(
"wrong-number-of-arguments",
vec![
Value::symbol("syntax-class-to-char"),
Value::fixnum(args.len() as i64),
],
));
}
let class = match args[0].kind() {
ValueKind::Fixnum(n) => n,
other => {
return Err(signal(
"wrong-type-argument",
vec![Value::symbol("fixnump"), args[0]],
));
}
};
let ch = match class {
0 => ' ',
1 => '.',
2 => 'w',
3 => '_',
4 => '(',
5 => ')',
6 => '\'',
7 => '"',
8 => '$',
9 => '\\',
10 => '/',
11 => '<',
12 => '>',
13 => '@',
14 => '!',
15 => '|',
_ => {
return Err(signal(
"args-out-of-range",
vec![Value::fixnum(15), Value::fixnum(class)],
));
}
};
Ok(Value::char(ch))
}
pub(crate) fn builtin_matching_paren(
eval: &mut super::eval::Context,
args: Vec<Value>,
) -> EvalResult {
builtin_matching_paren_in_buffers(&eval.buffers, args)
}
pub(crate) fn builtin_matching_paren_in_buffers(
buffers: &BufferManager,
args: Vec<Value>,
) -> EvalResult {
if args.len() != 1 {
return Err(signal(
"wrong-number-of-arguments",
vec![
Value::symbol("matching-paren"),
Value::fixnum(args.len() as i64),
],
));
}
let ch = match args[0].kind() {
ValueKind::Fixnum(n) => char::from_u32(n as u32).ok_or_else(|| {
signal(
"wrong-type-argument",
vec![Value::symbol("characterp"), args[0]],
)
})?,
_ => {
return Err(signal(
"wrong-type-argument",
vec![Value::symbol("characterp"), args[0]],
));
}
};
if let Some(buf) = buffers.current_buffer() {
let entry = SyntaxTable::for_buffer(buf).get_entry(ch);
if let Some(e) = entry {
if matches!(e.class, SyntaxClass::Open | SyntaxClass::Close) {
if let Some(m) = e.matching_char {
return Ok(Value::char(m));
}
}
}
}
let out = match ch {
'(' => Some(')'),
')' => Some('('),
'[' => Some(']'),
']' => Some('['),
'{' => Some('}'),
'}' => Some('{'),
_ => None,
};
Ok(out.map_or(Value::NIL, Value::char))
}
pub(crate) fn builtin_standard_syntax_table(args: Vec<Value>) -> EvalResult {
if !args.is_empty() {
return Err(signal(
"wrong-number-of-arguments",
vec![
Value::symbol("standard-syntax-table"),
Value::fixnum(args.len() as i64),
],
));
}
ensure_standard_syntax_table_object()
}
pub(crate) fn builtin_syntax_table_p(args: Vec<Value>) -> EvalResult {
if args.len() != 1 {
return Err(signal(
"wrong-number-of-arguments",
vec![
Value::symbol("syntax-table-p"),
Value::fixnum(args.len() as i64),
],
));
}
let is_char_table = super::chartable::builtin_char_table_p(vec![args[0]])?;
if !is_char_table.is_truthy() {
return Ok(Value::NIL);
}
let subtype = super::chartable::builtin_char_table_subtype(vec![args[0]])?;
match subtype.kind() {
ValueKind::Symbol(id) if resolve_sym(id) == "syntax-table" => Ok(Value::T),
_ => Ok(Value::NIL),
}
}
pub(crate) fn builtin_syntax_table(
eval: &mut super::eval::Context,
args: Vec<Value>,
) -> EvalResult {
builtin_syntax_table_in_buffers(&mut eval.buffers, args)
}
pub(crate) fn builtin_syntax_table_in_buffers(
buffers: &mut BufferManager,
args: Vec<Value>,
) -> EvalResult {
if !args.is_empty() {
return Err(signal(
"wrong-number-of-arguments",
vec![
Value::symbol("syntax-table"),
Value::fixnum(args.len() as i64),
],
));
}
current_buffer_syntax_table_object_in_buffers(buffers)
}
pub(crate) fn builtin_set_syntax_table(
eval: &mut super::eval::Context,
args: Vec<Value>,
) -> EvalResult {
builtin_set_syntax_table_in_buffers(&mut eval.buffers, args)
}
pub(crate) fn builtin_set_syntax_table_in_buffers(
buffers: &mut BufferManager,
args: Vec<Value>,
) -> EvalResult {
if args.len() != 1 {
return Err(signal(
"wrong-number-of-arguments",
vec![
Value::symbol("set-syntax-table"),
Value::fixnum(args.len() as i64),
],
));
}
if builtin_syntax_table_p(vec![args[0]])?.is_nil() {
return Err(signal(
"wrong-type-argument",
vec![Value::symbol("syntax-table-p"), args[0]],
));
}
let table = args[0];
set_current_buffer_syntax_table_object_in_buffers(buffers, table)?;
Ok(table)
}
pub(crate) fn builtin_modify_syntax_entry(
eval: &mut super::eval::Context,
args: Vec<Value>,
) -> EvalResult {
modify_syntax_entry_in_buffers(&mut eval.buffers, &args)
}
pub(crate) fn modify_syntax_entry_in_buffers(
buffers: &mut BufferManager,
args: &[Value],
) -> EvalResult {
if args.len() < 2 || args.len() > 3 {
return Err(signal(
"wrong-number-of-arguments",
vec![
Value::symbol("modify-syntax-entry"),
Value::fixnum(args.len() as i64),
],
));
}
let descriptor = syntax_runtime_string(&args[1])?;
let entry =
string_to_syntax(&descriptor).map_err(|msg| signal("error", vec![Value::string(&msg)]))?;
let target_table = if let Some(table) = args.get(2) {
if builtin_syntax_table_p(vec![*table])?.is_nil() {
return Err(signal(
"wrong-type-argument",
vec![Value::symbol("syntax-table-p"), *table],
));
}
*table
} else {
current_buffer_syntax_table_object_in_buffers(buffers)?
};
let current_table = current_buffer_syntax_table_object_in_buffers(buffers)?;
let update_current_buffer_table = target_table == current_table;
let chartable_entry = if matches!(entry.class, SyntaxClass::InheritStd) {
Value::NIL
} else {
syntax_entry_to_value(&entry)
};
super::chartable::builtin_set_char_table_range(vec![target_table, args[0], chartable_entry])?;
if !update_current_buffer_table {
return Ok(Value::NIL);
}
let _ = target_table;
Ok(Value::NIL)
}
pub(crate) fn builtin_char_syntax(eval: &mut super::eval::Context, args: Vec<Value>) -> EvalResult {
builtin_char_syntax_in_buffers(&eval.buffers, args)
}
pub(crate) fn builtin_char_syntax_in_buffers(
buffers: &BufferManager,
args: Vec<Value>,
) -> EvalResult {
if args.len() != 1 {
return Err(signal(
"wrong-number-of-arguments",
vec![
Value::symbol("char-syntax"),
Value::fixnum(args.len() as i64),
],
));
}
let code = match args[0].kind() {
ValueKind::Fixnum(c)
if (0..=crate::emacs_core::emacs_char::MAX_CHAR as i64).contains(&c) =>
{
c as u32
}
ValueKind::Fixnum(_) => {
return Err(signal(
"wrong-type-argument",
vec![Value::symbol("characterp"), args[0]],
));
}
_ => {
return Err(signal(
"wrong-type-argument",
vec![Value::symbol("characterp"), args[0]],
));
}
};
let buf = buffers
.current_buffer()
.ok_or_else(|| signal("error", vec![Value::string("No current buffer")]))?;
let class = SyntaxTable::for_buffer(buf).char_syntax_code(code);
Ok(Value::char(class.to_char()))
}
pub(crate) fn builtin_syntax_after(
eval: &mut super::eval::Context,
args: Vec<Value>,
) -> EvalResult {
builtin_syntax_after_in_buffers(&eval.buffers, args)
}
pub(crate) fn builtin_syntax_after_in_buffers(
buffers: &BufferManager,
args: Vec<Value>,
) -> EvalResult {
if args.len() != 1 {
return Err(signal(
"wrong-number-of-arguments",
vec![
Value::symbol("syntax-after"),
Value::fixnum(args.len() as i64),
],
));
}
let pos = match args[0].kind() {
ValueKind::Fixnum(n) => n,
_ => {
return Err(signal(
"wrong-type-argument",
vec![Value::symbol("number-or-marker-p"), args[0]],
));
}
};
if pos <= 0 {
return Ok(Value::NIL);
}
let buf = buffers
.current_buffer()
.ok_or_else(|| signal("error", vec![Value::string("No current buffer")]))?;
let char_index = pos as usize - 1;
let byte_index = buf
.text
.char_to_emacs_byte(char_index.min(buf.text.char_count()));
let Some(ch) = buf.char_after(byte_index) else {
return Ok(Value::NIL);
};
let entry = effective_syntax_entry_for_char_at_byte(
buf,
&SyntaxTable::for_buffer(buf),
ch,
byte_index,
true,
);
Ok(syntax_entry_to_value(&entry))
}
pub(crate) fn builtin_forward_comment(
eval: &mut super::eval::Context,
args: Vec<Value>,
) -> EvalResult {
let honor_properties = parse_sexp_lookup_properties_enabled(eval);
builtin_forward_comment_in_buffers(&mut eval.buffers, args, honor_properties)
}
pub(crate) fn builtin_forward_comment_in_buffers(
buffers: &mut BufferManager,
args: Vec<Value>,
honor_properties: bool,
) -> EvalResult {
if args.len() != 1 {
return Err(signal(
"wrong-number-of-arguments",
vec![
Value::symbol("forward-comment"),
Value::fixnum(args.len() as i64),
],
));
}
let count = match args[0].kind() {
ValueKind::Fixnum(n) => n,
_ => {
return Err(signal(
"wrong-type-argument",
vec![Value::symbol("integerp"), args[0]],
));
}
};
let current_id = buffers
.current_buffer_id()
.ok_or_else(|| signal("error", vec![Value::string("No current buffer")]))?;
let buf = buffers
.get_mut(current_id)
.ok_or_else(|| signal("error", vec![Value::string("No current buffer")]))?;
if count == 0 {
return Ok(Value::T);
}
if count > 0 {
let ok = forward_comment_forward(buf, count as u64, honor_properties);
return Ok(if ok { Value::T } else { Value::NIL });
} else {
let ok = forward_comment_backward(buf, (-count) as u64, honor_properties);
return Ok(if ok { Value::T } else { Value::NIL });
}
}
fn forward_comment_forward(buf: &mut Buffer, count: u64, honor_properties: bool) -> bool {
let mut remaining = count;
while remaining > 0 {
loop {
let pt = buf.point();
let max = buf.point_max();
if pt >= max {
return false;
}
let Some(ch) = buf.char_after(pt) else {
return false;
};
let entry = effective_syntax_entry_for_char_at_byte(
buf,
&SyntaxTable::for_buffer(buf),
ch,
pt,
honor_properties,
);
let class = entry.class;
if class == SyntaxClass::Whitespace {
buf.goto_char(pt + ch.len_utf8());
continue;
}
if class == SyntaxClass::EndComment && ch == '\n' {
buf.goto_char(pt + ch.len_utf8());
continue;
}
break;
}
let pt = buf.point();
let max = buf.point_max();
if pt >= max {
return false;
}
let Some(ch) = buf.char_after(pt) else {
return false;
};
let entry = effective_syntax_entry_for_char_at_byte(
buf,
&SyntaxTable::for_buffer(buf),
ch,
pt,
honor_properties,
);
let class = entry.class;
let flags = entry.flags;
if class == SyntaxClass::Comment {
let style_b = flags.contains(SyntaxFlags::COMMENT_STYLE_B);
let nested = flags.contains(SyntaxFlags::COMMENT_NESTABLE);
buf.goto_char(pt + ch.len_utf8());
if !scan_forward_comment_body(buf, style_b, nested, honor_properties) {
return false;
}
remaining -= 1;
continue;
}
if class == SyntaxClass::CommentFence {
buf.goto_char(pt + ch.len_utf8());
if !scan_forward_comment_fence(buf, honor_properties) {
return false;
}
remaining -= 1;
continue;
}
if flags.contains(SyntaxFlags::COMMENT_START_FIRST) {
let next_pos = pt + ch.len_utf8();
if next_pos < max {
if let Some(ch2) = buf.char_after(next_pos) {
let entry2 = effective_syntax_entry_for_char_at_byte(
buf,
&SyntaxTable::for_buffer(buf),
ch2,
next_pos,
honor_properties,
);
let flags2 = entry2.flags;
if flags2.contains(SyntaxFlags::COMMENT_START_SECOND) {
let style_b = flags2.contains(SyntaxFlags::COMMENT_STYLE_B);
let nested = flags2.contains(SyntaxFlags::COMMENT_NESTABLE)
|| flags.contains(SyntaxFlags::COMMENT_NESTABLE);
buf.goto_char(next_pos + ch2.len_utf8());
if !scan_forward_comment_body(buf, style_b, nested, honor_properties) {
return false;
}
remaining -= 1;
continue;
}
}
}
}
return false;
}
true
}
fn scan_forward_comment_body(
buf: &mut Buffer,
style_b: bool,
nested: bool,
honor_properties: bool,
) -> bool {
let mut nesting = 1i32;
loop {
let pt = buf.point();
let max = buf.point_max();
if pt >= max {
return false;
}
let Some(ch) = buf.char_after(pt) else {
return false;
};
let entry = effective_syntax_entry_for_char_at_byte(
buf,
&SyntaxTable::for_buffer(buf),
ch,
pt,
honor_properties,
);
let class = entry.class;
let flags = entry.flags;
if class == SyntaxClass::Escape || class == SyntaxClass::CharQuote {
buf.goto_char(pt + ch.len_utf8());
let pt2 = buf.point();
if pt2 >= buf.point_max() {
return false;
}
if let Some(ch2) = buf.char_after(pt2) {
buf.goto_char(pt2 + ch2.len_utf8());
}
continue;
}
if nested {
if class == SyntaxClass::Comment {
let sf_b = flags.contains(SyntaxFlags::COMMENT_STYLE_B);
if sf_b == style_b {
nesting += 1;
buf.goto_char(pt + ch.len_utf8());
continue;
}
}
if flags.contains(SyntaxFlags::COMMENT_START_FIRST) {
let next_pos = pt + ch.len_utf8();
if next_pos < buf.point_max() {
if let Some(ch2) = buf.char_after(next_pos) {
let entry2 = effective_syntax_entry_for_char_at_byte(
buf,
&SyntaxTable::for_buffer(buf),
ch2,
next_pos,
honor_properties,
);
let flags2 = entry2.flags;
if flags2.contains(SyntaxFlags::COMMENT_START_SECOND) {
let sf_b = flags2.contains(SyntaxFlags::COMMENT_STYLE_B);
if sf_b == style_b {
nesting += 1;
buf.goto_char(next_pos + ch2.len_utf8());
continue;
}
}
}
}
}
}
if class == SyntaxClass::EndComment {
let se_b = flags.contains(SyntaxFlags::COMMENT_STYLE_B);
if se_b == style_b {
buf.goto_char(pt + ch.len_utf8());
nesting -= 1;
if nesting <= 0 {
return true;
}
continue;
}
}
if class == SyntaxClass::CommentFence {
buf.goto_char(pt + ch.len_utf8());
nesting -= 1;
if nesting <= 0 {
return true;
}
continue;
}
if flags.contains(SyntaxFlags::COMMENT_END_FIRST) {
let next_pos = pt + ch.len_utf8();
if next_pos < buf.point_max() {
if let Some(ch2) = buf.char_after(next_pos) {
let entry2 = effective_syntax_entry_for_char_at_byte(
buf,
&SyntaxTable::for_buffer(buf),
ch2,
next_pos,
honor_properties,
);
let flags2 = entry2.flags;
if flags2.contains(SyntaxFlags::COMMENT_END_SECOND) {
let se_b = flags2.contains(SyntaxFlags::COMMENT_STYLE_B);
if se_b == style_b {
buf.goto_char(next_pos + ch2.len_utf8());
nesting -= 1;
if nesting <= 0 {
return true;
}
continue;
}
}
}
}
}
buf.goto_char(pt + ch.len_utf8());
}
}
fn scan_forward_comment_fence(buf: &mut Buffer, honor_properties: bool) -> bool {
loop {
let pt = buf.point();
if pt >= buf.point_max() {
return false;
}
let Some(ch) = buf.char_after(pt) else {
return false;
};
let entry = effective_syntax_entry_for_char_at_byte(
buf,
&SyntaxTable::for_buffer(buf),
ch,
pt,
honor_properties,
);
let class = entry.class;
if class == SyntaxClass::Escape || class == SyntaxClass::CharQuote {
buf.goto_char(pt + ch.len_utf8());
let pt2 = buf.point();
if pt2 >= buf.point_max() {
return false;
}
if let Some(ch2) = buf.char_after(pt2) {
buf.goto_char(pt2 + ch2.len_utf8());
}
continue;
}
buf.goto_char(pt + ch.len_utf8());
if class == SyntaxClass::CommentFence {
return true;
}
}
}
fn forward_comment_backward(buf: &mut Buffer, count: u64, honor_properties: bool) -> bool {
let mut remaining = count;
while remaining > 0 {
loop {
let pt = buf.point();
let min = buf.point_min();
if pt <= min {
return false;
}
let Some(ch) = buf.char_before(pt) else {
return false;
};
let ch_pos = pt - ch.len_utf8();
let entry = effective_syntax_entry_for_char_at_byte(
buf,
&SyntaxTable::for_buffer(buf),
ch,
ch_pos,
honor_properties,
);
let class = entry.class;
let flags = entry.flags;
let mut code = class;
let mut comstyle_b = false;
let mut nested = flags.contains(SyntaxFlags::COMMENT_NESTABLE);
if class == SyntaxClass::EndComment {
comstyle_b = flags.contains(SyntaxFlags::COMMENT_STYLE_B);
}
if flags.contains(SyntaxFlags::COMMENT_END_SECOND) {
let prev_pos = pt - ch.len_utf8();
if prev_pos > min {
if let Some(ch2) = buf.char_before(prev_pos) {
let ch2_pos = prev_pos - ch2.len_utf8();
let entry2 = effective_syntax_entry_for_char_at_byte(
buf,
&SyntaxTable::for_buffer(buf),
ch2,
ch2_pos,
honor_properties,
);
let flags2 = entry2.flags;
if flags2.contains(SyntaxFlags::COMMENT_END_FIRST) {
code = SyntaxClass::EndComment;
comstyle_b = flags.contains(SyntaxFlags::COMMENT_STYLE_B);
nested = nested || flags2.contains(SyntaxFlags::COMMENT_NESTABLE);
buf.goto_char(prev_pos - ch2.len_utf8());
}
}
}
}
if code == SyntaxClass::CommentFence {
buf.goto_char(pt - ch.len_utf8());
if !scan_backward_comment_fence(buf, honor_properties) {
buf.goto_char(pt);
return false;
}
break;
}
if code == SyntaxClass::EndComment {
if buf.point() == pt {
buf.goto_char(pt - ch.len_utf8());
}
let saved = buf.point();
if scan_backward_comment_body(buf, comstyle_b, nested, honor_properties) {
break;
}
if ch == '\n' {
buf.goto_char(pt - ch.len_utf8());
continue;
}
if class != SyntaxClass::EndComment {
buf.goto_char(saved + ch.len_utf8());
} else {
buf.goto_char(pt);
}
return false;
}
if class == SyntaxClass::Whitespace {
buf.goto_char(pt - ch.len_utf8());
continue;
}
return false;
}
remaining -= 1;
}
true
}
fn scan_backward_comment_body(
buf: &mut Buffer,
style_b: bool,
nested: bool,
honor_properties: bool,
) -> bool {
let mut nesting = 1i32;
let mut comstart_pos: Option<usize> = None;
loop {
let pt = buf.point();
let min = buf.point_min();
if pt <= min {
break;
}
let Some(ch) = buf.char_before(pt) else {
break;
};
let ch_pos = pt - ch.len_utf8();
let entry = effective_syntax_entry_for_char_at_byte(
buf,
&SyntaxTable::for_buffer(buf),
ch,
ch_pos,
honor_properties,
);
let class = entry.class;
let flags = entry.flags;
if class == SyntaxClass::EndComment {
let se_b = flags.contains(SyntaxFlags::COMMENT_STYLE_B);
if se_b == style_b {
if nested {
nesting += 1;
buf.goto_char(pt - ch.len_utf8());
continue;
} else {
break;
}
}
}
if flags.contains(SyntaxFlags::COMMENT_END_SECOND) {
let prev_pos = pt - ch.len_utf8();
if prev_pos > min {
if let Some(ch2) = buf.char_before(prev_pos) {
let ch2_pos = prev_pos - ch2.len_utf8();
let entry2 = effective_syntax_entry_for_char_at_byte(
buf,
&SyntaxTable::for_buffer(buf),
ch2,
ch2_pos,
honor_properties,
);
let flags2 = entry2.flags;
if flags2.contains(SyntaxFlags::COMMENT_END_FIRST) {
let se_b = flags.contains(SyntaxFlags::COMMENT_STYLE_B);
if se_b == style_b {
if nested {
nesting += 1;
buf.goto_char(prev_pos - ch2.len_utf8());
continue;
} else {
break;
}
}
}
}
}
}
if class == SyntaxClass::Comment {
let sc_b = flags.contains(SyntaxFlags::COMMENT_STYLE_B);
if sc_b == style_b {
let new_pos = pt - ch.len_utf8();
if nested {
buf.goto_char(new_pos);
nesting -= 1;
if nesting <= 0 {
return true;
}
continue;
} else {
comstart_pos = Some(new_pos);
buf.goto_char(new_pos);
continue;
}
}
}
if class == SyntaxClass::CommentFence {
let new_pos = pt - ch.len_utf8();
buf.goto_char(new_pos);
if nested {
nesting -= 1;
if nesting <= 0 {
return true;
}
} else {
comstart_pos = Some(new_pos);
}
continue;
}
if flags.contains(SyntaxFlags::COMMENT_START_SECOND) {
let prev_pos = pt - ch.len_utf8();
if prev_pos > min {
if let Some(ch2) = buf.char_before(prev_pos) {
let ch2_pos = prev_pos - ch2.len_utf8();
let entry2 = effective_syntax_entry_for_char_at_byte(
buf,
&SyntaxTable::for_buffer(buf),
ch2,
ch2_pos,
honor_properties,
);
let flags2 = entry2.flags;
if flags2.contains(SyntaxFlags::COMMENT_START_FIRST) {
let sc_b = flags.contains(SyntaxFlags::COMMENT_STYLE_B);
if sc_b == style_b {
let new_pos = prev_pos - ch2.len_utf8();
if nested {
buf.goto_char(new_pos);
nesting -= 1;
if nesting <= 0 {
return true;
}
continue;
} else {
comstart_pos = Some(new_pos);
buf.goto_char(new_pos);
continue;
}
}
}
}
}
}
buf.goto_char(pt - ch.len_utf8());
}
if !nested {
if let Some(pos) = comstart_pos {
buf.goto_char(pos);
return true;
}
}
false
}
fn scan_backward_comment_fence(buf: &mut Buffer, honor_properties: bool) -> bool {
loop {
let pt = buf.point();
if pt <= buf.point_min() {
return false;
}
let Some(ch) = buf.char_before(pt) else {
return false;
};
let ch_pos = pt - ch.len_utf8();
let entry = effective_syntax_entry_for_char_at_byte(
buf,
&SyntaxTable::for_buffer(buf),
ch,
ch_pos,
honor_properties,
);
let class = entry.class;
buf.goto_char(pt - ch.len_utf8());
if class == SyntaxClass::CommentFence {
return true;
}
}
}
pub(crate) fn builtin_backward_prefix_chars(
eval: &mut super::eval::Context,
args: Vec<Value>,
) -> EvalResult {
let honor_properties = parse_sexp_lookup_properties_enabled(eval);
builtin_backward_prefix_chars_in_buffers(&mut eval.buffers, args, honor_properties)
}
pub(crate) fn builtin_backward_prefix_chars_in_buffers(
buffers: &mut BufferManager,
args: Vec<Value>,
honor_properties: bool,
) -> EvalResult {
if !args.is_empty() {
return Err(signal(
"wrong-number-of-arguments",
vec![
Value::symbol("backward-prefix-chars"),
Value::fixnum(args.len() as i64),
],
));
}
let current_id = buffers
.current_buffer_id()
.ok_or_else(|| signal("error", vec![Value::string("No current buffer")]))?;
let buf = buffers
.get_mut(current_id)
.ok_or_else(|| signal("error", vec![Value::string("No current buffer")]))?;
loop {
let pt = buf.point();
if pt <= buf.point_min() {
break;
}
let Some(ch) = buf.char_before(pt) else {
break;
};
let ch_pos = pt - ch.len_utf8();
let entry = effective_syntax_entry_for_char_at_byte(
buf,
&SyntaxTable::for_buffer(buf),
ch,
ch_pos,
honor_properties,
);
let is_prefix =
entry.class == SyntaxClass::Quote || entry.flags.contains(SyntaxFlags::PREFIX);
if !is_prefix {
break;
}
buf.goto_char(pt.saturating_sub(ch.len_utf8()));
}
Ok(Value::NIL)
}
pub(crate) fn builtin_forward_word(
eval: &mut super::eval::Context,
args: Vec<Value>,
) -> EvalResult {
let count = if args.is_empty() || args[0].is_nil() {
1
} else {
match args[0].kind() {
ValueKind::Fixnum(n) => n,
other => {
return Err(signal(
"wrong-type-argument",
vec![Value::symbol("integerp"), args[0]],
));
}
}
};
let buf = eval
.buffers
.current_buffer()
.ok_or_else(|| signal("error", vec![Value::string("No current buffer")]))?;
let table = SyntaxTable::for_buffer(buf);
let honor_properties = parse_sexp_lookup_properties_enabled(eval);
let new_pos = forward_word_with_options(buf, &table, count, honor_properties);
let current_id = eval
.buffers
.current_buffer_id()
.ok_or_else(|| signal("error", vec![Value::string("No current buffer")]))?;
let _ = eval.buffers.goto_buffer_byte(current_id, new_pos);
Ok(Value::NIL)
}
pub(crate) fn builtin_forward_word_in_buffers(
buffers: &mut BufferManager,
args: Vec<Value>,
) -> EvalResult {
let count = if args.is_empty() || args[0].is_nil() {
1
} else {
match args[0].kind() {
ValueKind::Fixnum(n) => n,
other => {
return Err(signal(
"wrong-type-argument",
vec![Value::symbol("integerp"), args[0]],
));
}
}
};
let buf = buffers
.current_buffer()
.ok_or_else(|| signal("error", vec![Value::string("No current buffer")]))?;
let table = SyntaxTable::for_buffer(buf);
let new_pos = forward_word(buf, &table, count);
let current_id = buffers
.current_buffer_id()
.ok_or_else(|| signal("error", vec![Value::string("No current buffer")]))?;
let _ = buffers.goto_buffer_byte(current_id, new_pos);
Ok(Value::NIL)
}
pub(crate) fn builtin_backward_word(
eval: &mut super::eval::Context,
args: Vec<Value>,
) -> EvalResult {
let count = if args.is_empty() || args[0].is_nil() {
1
} else {
match args[0].kind() {
ValueKind::Fixnum(n) => n,
other => {
return Err(signal(
"wrong-type-argument",
vec![Value::symbol("integerp"), args[0]],
));
}
}
};
let buf = eval
.buffers
.current_buffer()
.ok_or_else(|| signal("error", vec![Value::string("No current buffer")]))?;
let table = SyntaxTable::for_buffer(buf);
let honor_properties = parse_sexp_lookup_properties_enabled(eval);
let new_pos = backward_word_with_options(buf, &table, count, honor_properties);
let current_id = eval
.buffers
.current_buffer_id()
.ok_or_else(|| signal("error", vec![Value::string("No current buffer")]))?;
let _ = eval.buffers.goto_buffer_byte(current_id, new_pos);
Ok(Value::NIL)
}
pub(crate) fn builtin_forward_sexp(
eval: &mut super::eval::Context,
args: Vec<Value>,
) -> EvalResult {
let count = if args.is_empty() || args[0].is_nil() {
1i64
} else {
match args[0].kind() {
ValueKind::Fixnum(n) => n,
other => {
return Err(signal(
"wrong-type-argument",
vec![Value::symbol("integerp"), args[0]],
));
}
}
};
let buf = eval
.buffers
.current_buffer()
.ok_or_else(|| signal("error", vec![Value::string("No current buffer")]))?;
let table = SyntaxTable::for_buffer(buf);
let from = buf.point();
let honor_properties = parse_sexp_lookup_properties_enabled(eval);
let new_pos = scan_sexps_with_options(buf, &table, from, count, honor_properties)
.map_err(|msg| signal("scan-error", vec![Value::string(&msg)]))?;
let current_id = eval
.buffers
.current_buffer_id()
.ok_or_else(|| signal("error", vec![Value::string("No current buffer")]))?;
let _ = eval.buffers.goto_buffer_byte(current_id, new_pos);
Ok(Value::NIL)
}
pub(crate) fn builtin_backward_sexp(
eval: &mut super::eval::Context,
args: Vec<Value>,
) -> EvalResult {
let count = if args.is_empty() || args[0].is_nil() {
1i64
} else {
match args[0].kind() {
ValueKind::Fixnum(n) => n,
other => {
return Err(signal(
"wrong-type-argument",
vec![Value::symbol("integerp"), args[0]],
));
}
}
};
let buf = eval
.buffers
.current_buffer()
.ok_or_else(|| signal("error", vec![Value::string("No current buffer")]))?;
let table = SyntaxTable::for_buffer(buf);
let from = buf.point();
let honor_properties = parse_sexp_lookup_properties_enabled(eval);
let new_pos = scan_sexps_with_options(buf, &table, from, -count, honor_properties)
.map_err(|msg| signal("scan-error", vec![Value::string(&msg)]))?;
let current_id = eval
.buffers
.current_buffer_id()
.ok_or_else(|| signal("error", vec![Value::string("No current buffer")]))?;
let _ = eval.buffers.goto_buffer_byte(current_id, new_pos);
Ok(Value::NIL)
}
pub(crate) fn builtin_scan_lists(ctx: &mut super::eval::Context, args: Vec<Value>) -> EvalResult {
if args.len() != 3 {
return Err(signal(
"wrong-number-of-arguments",
vec![
Value::symbol("scan-lists"),
Value::fixnum(args.len() as i64),
],
));
}
let from = match args[0].kind() {
ValueKind::Fixnum(n) => n,
other => {
return Err(signal(
"wrong-type-argument",
vec![Value::symbol("integer-or-marker-p"), args[0]],
));
}
};
let count = match args[1].kind() {
ValueKind::Fixnum(n) => n,
other => {
return Err(signal(
"wrong-type-argument",
vec![Value::symbol("integerp"), args[1]],
));
}
};
let depth = match args[2].kind() {
ValueKind::Fixnum(n) => n,
other => {
return Err(signal(
"wrong-type-argument",
vec![Value::symbol("integerp"), args[2]],
));
}
};
let buf = ctx
.buffers
.current_buffer()
.ok_or_else(|| signal("error", vec![Value::string("No current buffer")]))?;
let table = SyntaxTable::for_buffer(buf);
let point_min = buf.point_min_char() as i64 + 1;
let point_max = buf.point_max_char() as i64 + 1;
let clipped_from = from.clamp(point_min, point_max);
let from_char = (clipped_from - 1) as usize;
let honor_properties = parse_sexp_lookup_properties_enabled(ctx);
match scan_lists_with_options(buf, &table, from_char, count, depth, honor_properties) {
Ok(Some(new_char)) => Ok(Value::fixnum(new_char as i64 + 1)),
Ok(None) => Ok(Value::NIL),
Err(msg) => Err(signal("scan-error", vec![Value::string(&msg)])),
}
}
pub(crate) fn builtin_scan_sexps(ctx: &mut super::eval::Context, args: Vec<Value>) -> EvalResult {
if args.len() != 2 {
return Err(signal(
"wrong-number-of-arguments",
vec![
Value::symbol("scan-sexps"),
Value::fixnum(args.len() as i64),
],
));
}
let from = match args[0].kind() {
ValueKind::Fixnum(n) => n,
other => {
return Err(signal(
"wrong-type-argument",
vec![Value::symbol("number-or-marker-p"), args[0]],
));
}
};
let count = match args[1].kind() {
ValueKind::Fixnum(n) => n,
other => {
return Err(signal(
"wrong-type-argument",
vec![Value::symbol("integerp"), args[1]],
));
}
};
let buf = ctx
.buffers
.current_buffer()
.ok_or_else(|| signal("error", vec![Value::string("No current buffer")]))?;
let table = SyntaxTable::for_buffer(buf);
let from_char = if from > 0 { from as usize - 1 } else { 0 };
let from_byte = buf
.text
.char_to_emacs_byte(from_char.min(buf.text.char_count()));
let honor_properties = parse_sexp_lookup_properties_enabled(ctx);
match scan_sexps_with_options(buf, &table, from_byte, count, honor_properties) {
Ok(new_byte) => Ok(Value::fixnum(
buf.text.emacs_byte_to_char(new_byte) as i64 + 1,
)),
Err(_) if count < 0 => Ok(Value::NIL),
Err(msg) => Err(signal("scan-error", vec![Value::string(&msg)])),
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum ParseStringState {
Delim(char),
Fence,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum ParseCommentState {
Syntax {
depth: i64,
style_b: bool,
nestable: bool,
},
Fence {
depth: i64,
},
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum CommentStopMode {
None,
Comment,
SyntaxTable,
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct PartialParseState {
depth: i64,
mindepth: i64,
levels: Vec<PartialParseLevel>,
in_string: Option<ParseStringState>,
in_comment: Option<ParseCommentState>,
comment_or_string_start: Option<i64>,
quoted: bool,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
struct PartialParseLevel {
last: Option<i64>,
prev: Option<i64>,
}
impl PartialParseState {
fn new() -> Self {
Self {
depth: 0,
mindepth: 0,
levels: vec![PartialParseLevel::default()],
in_string: None,
in_comment: None,
comment_or_string_start: None,
quoted: false,
}
}
fn from_oldstate(oldstate: Option<&Value>) -> Self {
let mut state = Self::new();
let Some(oldstate) = oldstate else {
return state;
};
let Some(items) = list_to_vec(oldstate) else {
return state;
};
state.depth = items.first().and_then(|v| v.as_fixnum()).unwrap_or(0);
if let Some(start) = items.get(8).and_then(|v| v.as_fixnum()) {
state.comment_or_string_start = Some(start);
}
if let Some(v) = items.get(5) {
if v.is_t() {
state.quoted = true;
}
}
if let Some(item) = items.get(3) {
state.in_string = match item.kind() {
ValueKind::Nil => None,
ValueKind::T => Some(ParseStringState::Fence),
ValueKind::Fixnum(n) => u32::try_from(n)
.ok()
.and_then(char::from_u32)
.map(ParseStringState::Delim),
_ => None,
};
}
if let Some(item) = items.get(4) {
state.in_comment = match item.kind() {
ValueKind::Nil => None,
ValueKind::T => Some(ParseCommentState::Syntax {
depth: 1,
style_b: false,
nestable: false,
}),
ValueKind::Fixnum(n) => Some(ParseCommentState::Syntax {
depth: n,
style_b: false,
nestable: true,
}),
_ => None,
};
}
if let Some(item) = items.get(9)
&& let Some(stack_items) = list_to_vec(item)
{
state.levels.clear();
state.levels.push(PartialParseLevel::default());
for start in stack_items.into_iter().filter_map(|v| v.as_fixnum()) {
if let Some(level) = state.levels.last_mut() {
level.last = Some(start);
}
state.levels.push(PartialParseLevel::default());
}
}
state
}
fn current_level_mut(&mut self) -> &mut PartialParseLevel {
self.levels
.last_mut()
.expect("partial parse state always has a current level")
}
fn finish_current_level_sexp(&mut self, start: i64) {
let level = self.current_level_mut();
level.last = Some(start);
level.prev = Some(start);
}
fn open_level(&mut self, start: i64) {
if let Some(level) = self.levels.last_mut() {
level.last = Some(start);
}
self.depth += 1;
self.levels.push(PartialParseLevel::default());
}
fn close_level(&mut self) {
self.depth -= 1;
self.mindepth = self.mindepth.min(self.depth);
if self.levels.len() > 1 {
self.levels.pop();
}
if let Some(start) = self.current_level_mut().last {
self.current_level_mut().prev = Some(start);
}
}
fn containing_sexp_start(&self) -> Option<i64> {
self.levels
.len()
.checked_sub(2)
.and_then(|idx| self.levels.get(idx))
.and_then(|level| level.last)
}
fn current_level_completed_sexp_start(&self) -> Option<i64> {
self.levels.last().and_then(|level| level.prev)
}
fn level_start_positions(&self) -> Vec<i64> {
if self.levels.len() <= 1 {
return Vec::new();
}
self.levels
.iter()
.take(self.levels.len() - 1)
.filter_map(|level| level.last)
.collect()
}
fn into_value(self) -> Value {
let containing_sexp_start = self.containing_sexp_start();
let completed_sexp_start = self.current_level_completed_sexp_start();
let level_starts = self.level_start_positions();
let stack_value = if level_starts.is_empty() {
Value::NIL
} else {
Value::list(level_starts.into_iter().map(Value::fixnum).collect())
};
let string_value = match self.in_string {
Some(ParseStringState::Delim(term)) => Value::fixnum(term as i64),
Some(ParseStringState::Fence) => Value::T,
None => Value::NIL,
};
let comment_value = match self.in_comment {
Some(ParseCommentState::Syntax {
depth: comment_depth,
nestable: false,
..
}) => {
debug_assert_eq!(comment_depth, 1);
Value::T
}
Some(ParseCommentState::Syntax {
depth: comment_depth,
nestable: true,
..
}) => Value::fixnum(comment_depth),
Some(ParseCommentState::Fence {
depth: comment_depth,
}) => Value::fixnum(comment_depth),
None => Value::NIL,
};
Value::list(vec![
Value::fixnum(self.depth),
containing_sexp_start.map_or(Value::NIL, Value::fixnum),
completed_sexp_start.map_or(Value::NIL, Value::fixnum),
string_value,
comment_value,
if self.quoted { Value::T } else { Value::NIL },
Value::fixnum(self.mindepth),
Value::NIL,
self.comment_or_string_start
.map_or(Value::NIL, Value::fixnum),
stack_value,
Value::NIL,
])
}
}
fn syntax_class_and_flags(
buf: &Buffer,
table: &SyntaxTable,
ch: char,
abs_char: usize,
honor_properties: bool,
) -> (SyntaxClass, SyntaxFlags) {
let entry = effective_syntax_entry_for_abs_char(buf, table, ch, abs_char, honor_properties);
(entry.class, entry.flags)
}
fn parse_commentstop_mode(arg: Option<&Value>) -> CommentStopMode {
match arg {
None => CommentStopMode::None,
Some(v) if v.is_nil() => CommentStopMode::None,
Some(v) => match v.kind() {
ValueKind::Symbol(sym) if resolve_sym(sym) == "syntax-table" => {
CommentStopMode::SyntaxTable
}
_ => CommentStopMode::Comment,
},
}
}
fn parse_state_from_range_with_options(
buf: &Buffer,
table: &SyntaxTable,
from: i64,
to: i64,
target_depth: Option<i64>,
stop_before: bool,
oldstate: Option<&Value>,
commentstop: CommentStopMode,
honor_properties: bool,
) -> (Value, i64) {
let point_min = buf.point_min_char();
let point_max = buf.point_max_char();
let from_char = if from > 0 { from as usize - 1 } else { 0 }.clamp(point_min, point_max);
let to_char = if to > 0 { to as usize - 1 } else { 0 }.clamp(point_min, point_max);
let chars = buffer_chars_in_range(
buf,
buf.text.char_to_emacs_byte(from_char),
buf.text.char_to_emacs_byte(to_char),
);
let to_idx = chars.len();
let mut state = PartialParseState::from_oldstate(oldstate);
let mut idx = 0;
let mut atom_start: Option<i64> = None;
let finish_atom = |state: &mut PartialParseState, atom_start: &mut Option<i64>| {
if let Some(start) = atom_start.take() {
state.finish_current_level_sexp(start);
}
};
while idx < to_idx {
let abs_char = from_char + idx;
let pos1 = (abs_char + 1) as i64;
let ch = chars[idx];
let (class, flags) = syntax_class_and_flags(buf, table, ch, abs_char, honor_properties);
if state.quoted {
state.quoted = false;
idx += 1;
continue;
}
if let Some(string_state) = state.in_string {
match class {
SyntaxClass::Escape | SyntaxClass::CharQuote => {
idx += 1;
if idx < to_idx {
idx += 1;
} else {
state.quoted = true;
}
continue;
}
SyntaxClass::StringFence if string_state == ParseStringState::Fence => {
if let Some(start) = state.comment_or_string_start {
state.finish_current_level_sexp(start);
}
state.in_string = None;
state.comment_or_string_start = None;
idx += 1;
if commentstop == CommentStopMode::SyntaxTable {
break;
}
continue;
}
SyntaxClass::StringDelim if matches!(string_state, ParseStringState::Delim(term) if ch == term) =>
{
if let Some(start) = state.comment_or_string_start {
state.finish_current_level_sexp(start);
}
state.in_string = None;
state.comment_or_string_start = None;
idx += 1;
if commentstop == CommentStopMode::SyntaxTable {
break;
}
continue;
}
_ => {
idx += 1;
continue;
}
}
}
if let Some(comment_state) = state.in_comment {
match comment_state {
ParseCommentState::Fence {
depth: comment_depth,
} => {
if class == SyntaxClass::CommentFence {
let next_depth = comment_depth - 1;
idx += 1;
if next_depth <= 0 {
state.in_comment = None;
state.comment_or_string_start = None;
} else {
state.in_comment = Some(ParseCommentState::Fence { depth: next_depth });
}
if commentstop == CommentStopMode::SyntaxTable {
break;
}
continue;
}
if matches!(class, SyntaxClass::Escape | SyntaxClass::CharQuote) {
idx += 1;
if idx < to_idx {
idx += 1;
} else {
state.quoted = true;
}
continue;
}
idx += 1;
continue;
}
ParseCommentState::Syntax {
depth: comment_depth,
style_b,
nestable,
} => {
if matches!(class, SyntaxClass::Escape | SyntaxClass::CharQuote) {
idx += 1;
if idx < to_idx {
idx += 1;
} else {
state.quoted = true;
}
continue;
}
if nestable {
if class == SyntaxClass::Comment
&& flags.contains(SyntaxFlags::COMMENT_STYLE_B) == style_b
{
state.in_comment = Some(ParseCommentState::Syntax {
depth: comment_depth + 1,
style_b,
nestable,
});
idx += 1;
continue;
}
if flags.contains(SyntaxFlags::COMMENT_START_FIRST) && idx + 1 < to_idx {
let (_, next_flags) = syntax_class_and_flags(
buf,
table,
chars[idx + 1],
abs_char + 1,
honor_properties,
);
if next_flags.contains(SyntaxFlags::COMMENT_START_SECOND)
&& next_flags.contains(SyntaxFlags::COMMENT_STYLE_B) == style_b
{
state.in_comment = Some(ParseCommentState::Syntax {
depth: comment_depth + 1,
style_b,
nestable,
});
idx += 2;
continue;
}
}
}
if class == SyntaxClass::EndComment
&& flags.contains(SyntaxFlags::COMMENT_STYLE_B) == style_b
{
let next_depth = comment_depth - 1;
idx += 1;
if next_depth <= 0 {
state.in_comment = None;
state.comment_or_string_start = None;
} else {
state.in_comment = Some(ParseCommentState::Syntax {
depth: next_depth,
style_b,
nestable,
});
}
if commentstop == CommentStopMode::SyntaxTable {
break;
}
continue;
}
if flags.contains(SyntaxFlags::COMMENT_END_FIRST) && idx + 1 < to_idx {
let (_, next_flags) = syntax_class_and_flags(
buf,
table,
chars[idx + 1],
abs_char + 1,
honor_properties,
);
if next_flags.contains(SyntaxFlags::COMMENT_END_SECOND)
&& next_flags.contains(SyntaxFlags::COMMENT_STYLE_B) == style_b
{
let next_depth = comment_depth - 1;
idx += 2;
if next_depth <= 0 {
state.in_comment = None;
state.comment_or_string_start = None;
} else {
state.in_comment = Some(ParseCommentState::Syntax {
depth: next_depth,
style_b,
nestable,
});
}
if commentstop == CommentStopMode::SyntaxTable {
break;
}
continue;
}
}
idx += 1;
continue;
}
}
}
if stop_before
&& matches!(
class,
SyntaxClass::Escape
| SyntaxClass::CharQuote
| SyntaxClass::Word
| SyntaxClass::Symbol
| SyntaxClass::Open
| SyntaxClass::StringDelim
| SyntaxClass::StringFence
)
{
break;
}
if !matches!(
class,
SyntaxClass::Word | SyntaxClass::Symbol | SyntaxClass::Quote
) {
finish_atom(&mut state, &mut atom_start);
}
if flags.contains(SyntaxFlags::COMMENT_START_FIRST) && idx + 1 < to_idx {
let (_, next_flags) =
syntax_class_and_flags(buf, table, chars[idx + 1], abs_char + 1, honor_properties);
if next_flags.contains(SyntaxFlags::COMMENT_START_SECOND) {
state.in_comment = Some(ParseCommentState::Syntax {
depth: 1,
style_b: next_flags.contains(SyntaxFlags::COMMENT_STYLE_B),
nestable: flags.contains(SyntaxFlags::COMMENT_NESTABLE)
|| next_flags.contains(SyntaxFlags::COMMENT_NESTABLE),
});
state.comment_or_string_start = Some(pos1);
idx += 2;
if commentstop != CommentStopMode::None {
break;
}
continue;
}
}
match class {
SyntaxClass::Open => {
state.open_level(pos1);
idx += 1;
if target_depth == Some(state.depth) {
break;
}
continue;
}
SyntaxClass::Close => {
state.close_level();
idx += 1;
if target_depth == Some(state.depth) {
break;
}
continue;
}
SyntaxClass::StringDelim => {
state.in_string = Some(ParseStringState::Delim(ch));
state.comment_or_string_start = Some(pos1);
idx += 1;
if commentstop == CommentStopMode::SyntaxTable {
break;
}
continue;
}
SyntaxClass::StringFence => {
state.in_string = Some(ParseStringState::Fence);
state.comment_or_string_start = Some(pos1);
idx += 1;
if commentstop == CommentStopMode::SyntaxTable {
break;
}
continue;
}
SyntaxClass::Comment => {
state.in_comment = Some(ParseCommentState::Syntax {
depth: 1,
style_b: flags.contains(SyntaxFlags::COMMENT_STYLE_B),
nestable: flags.contains(SyntaxFlags::COMMENT_NESTABLE),
});
state.comment_or_string_start = Some(pos1);
idx += 1;
if commentstop != CommentStopMode::None {
break;
}
continue;
}
SyntaxClass::CommentFence => {
state.in_comment = Some(ParseCommentState::Fence { depth: 1 });
state.comment_or_string_start = Some(pos1);
idx += 1;
if commentstop != CommentStopMode::None {
break;
}
continue;
}
SyntaxClass::Escape | SyntaxClass::CharQuote => {
if idx + 1 < to_idx {
idx += 2;
continue;
}
state.quoted = true;
idx += 1;
continue;
}
SyntaxClass::Word | SyntaxClass::Symbol | SyntaxClass::Quote => {
atom_start.get_or_insert(pos1);
}
SyntaxClass::Whitespace | SyntaxClass::EndComment => {}
_ => {}
}
idx += 1;
}
finish_atom(&mut state, &mut atom_start);
(state.into_value(), (from_char + idx) as i64 + 1)
}
fn parse_state_from_range(buf: &Buffer, table: &SyntaxTable, from: i64, to: i64) -> Value {
parse_state_from_range_with_options(
buf,
table,
from,
to,
None,
false,
None,
CommentStopMode::None,
false,
)
.0
}
pub(crate) fn builtin_parse_partial_sexp(
eval: &mut super::eval::Context,
args: Vec<Value>,
) -> EvalResult {
if args.len() < 2 || args.len() > 6 {
return Err(signal(
"wrong-number-of-arguments",
vec![
Value::symbol("parse-partial-sexp"),
Value::fixnum(args.len() as i64),
],
));
}
let from = match args[0].kind() {
ValueKind::Fixnum(n) => n,
other => {
return Err(signal(
"wrong-type-argument",
vec![Value::symbol("number-or-marker-p"), args[0]],
));
}
};
let to = match args[1].kind() {
ValueKind::Fixnum(n) => n,
other => {
return Err(signal(
"wrong-type-argument",
vec![Value::symbol("number-or-marker-p"), args[1]],
));
}
};
if to < from {
return Err(signal(
"error",
vec![Value::string("End position is smaller than start position")],
));
}
let buf = eval
.buffers
.current_buffer()
.ok_or_else(|| signal("error", vec![Value::string("No current buffer")]))?;
let table = SyntaxTable::for_buffer(buf);
let target_depth = match args.get(2) {
Some(v) if !v.is_nil() => match v.kind() {
ValueKind::Fixnum(n) => Some(n),
_ => {
return Err(signal(
"wrong-type-argument",
vec![Value::symbol("integerp"), *v],
));
}
},
_ => None,
};
let stop_before = args.get(3).is_some_and(|v| v.is_truthy());
let oldstate = args.get(4).filter(|v| !v.is_nil());
let commentstop = parse_commentstop_mode(args.get(5));
let honor_properties = parse_sexp_lookup_properties_enabled(eval);
let (state, stop_pos) = parse_state_from_range_with_options(
buf,
&table,
from,
to,
target_depth,
stop_before,
oldstate,
commentstop,
honor_properties,
);
let stop_byte = lisp_pos_to_byte(buf, stop_pos);
if let Some(buf_mut) = eval.buffers.current_buffer_mut() {
buf_mut.goto_char(stop_byte);
}
Ok(state)
}
pub(crate) fn builtin_syntax_ppss(eval: &mut super::eval::Context, args: Vec<Value>) -> EvalResult {
if args.len() > 1 {
return Err(signal(
"wrong-number-of-arguments",
vec![
Value::symbol("syntax-ppss"),
Value::fixnum(args.len() as i64),
],
));
}
let buf = eval
.buffers
.current_buffer()
.ok_or_else(|| signal("error", vec![Value::string("No current buffer")]))?;
let table = SyntaxTable::for_buffer(buf);
let pos = if args.is_empty() || args[0].is_nil() {
buf.point_char() as i64 + 1
} else {
match args[0].kind() {
ValueKind::Fixnum(n) => n,
other => {
return Err(signal(
"wrong-type-argument",
vec![Value::symbol("number-or-marker-p"), args[0]],
));
}
}
};
let honor_properties = parse_sexp_lookup_properties_enabled(eval);
Ok(parse_state_from_range_with_options(
buf,
&table,
1,
pos,
None,
false,
None,
CommentStopMode::None,
honor_properties,
)
.0)
}
pub(crate) fn builtin_syntax_ppss_flush_cache(
_eval: &mut super::eval::Context,
args: Vec<Value>,
) -> EvalResult {
if args.is_empty() {
return Err(signal(
"wrong-number-of-arguments",
vec![Value::symbol("syntax-ppss-flush-cache"), Value::fixnum(0)],
));
}
match args[0].kind() {
ValueKind::Fixnum(_) => Ok(Value::NIL),
other => Err(signal(
"wrong-type-argument",
vec![Value::symbol("number-or-marker-p"), args[0]],
)),
}
}
fn lisp_pos_to_byte(buf: &Buffer, raw: i64) -> usize {
buf.lisp_pos_to_accessible_byte(raw)
}
pub(crate) fn builtin_skip_syntax_forward(
eval: &mut super::eval::Context,
args: Vec<Value>,
) -> EvalResult {
let honor_properties = parse_sexp_lookup_properties_enabled(eval);
builtin_skip_syntax_forward_in_buffers(&mut eval.buffers, args, honor_properties)
}
pub(crate) fn builtin_skip_syntax_forward_in_buffers(
buffers: &mut BufferManager,
args: Vec<Value>,
honor_properties: bool,
) -> EvalResult {
if args.is_empty() {
return Err(signal(
"wrong-number-of-arguments",
vec![Value::symbol("skip-syntax-forward"), Value::fixnum(0)],
));
}
let syntax_chars = syntax_runtime_string(&args[0])?;
let limit = if args.len() > 1 && !args[1].is_nil() {
match args[1].kind() {
ValueKind::Fixnum(n) => Some(n),
other => {
return Err(signal(
"wrong-type-argument",
vec![Value::symbol("integerp"), args[1]],
));
}
}
} else {
None
};
let buf = buffers
.current_buffer()
.ok_or_else(|| signal("error", vec![Value::string("No current buffer")]))?;
let table = SyntaxTable::for_buffer(buf);
let limit = limit.map(|raw| lisp_pos_to_byte(buf, raw));
let new_pos =
skip_syntax_forward_with_options(buf, &table, &syntax_chars, limit, honor_properties);
let old_pt = buffers
.current_buffer()
.ok_or_else(|| signal("error", vec![Value::string("No current buffer")]))?
.point();
let current_id = buffers
.current_buffer_id()
.ok_or_else(|| signal("error", vec![Value::string("No current buffer")]))?;
let _ = buffers.goto_buffer_byte(current_id, new_pos);
let buf = buffers
.get(current_id)
.ok_or_else(|| signal("error", vec![Value::string("No current buffer")]))?;
let chars_moved = if new_pos >= old_pt {
buf.text.emacs_byte_to_char(new_pos) as i64 - buf.text.emacs_byte_to_char(old_pt) as i64
} else {
buf.text.emacs_byte_to_char(old_pt) as i64 - buf.text.emacs_byte_to_char(new_pos) as i64
};
Ok(Value::fixnum(chars_moved))
}
pub(crate) fn builtin_skip_syntax_backward(
eval: &mut super::eval::Context,
args: Vec<Value>,
) -> EvalResult {
let honor_properties = parse_sexp_lookup_properties_enabled(eval);
builtin_skip_syntax_backward_in_buffers(&mut eval.buffers, args, honor_properties)
}
pub(crate) fn builtin_skip_syntax_backward_in_buffers(
buffers: &mut BufferManager,
args: Vec<Value>,
honor_properties: bool,
) -> EvalResult {
if args.is_empty() {
return Err(signal(
"wrong-number-of-arguments",
vec![Value::symbol("skip-syntax-backward"), Value::fixnum(0)],
));
}
let syntax_chars = syntax_runtime_string(&args[0])?;
let limit = if args.len() > 1 && !args[1].is_nil() {
match args[1].kind() {
ValueKind::Fixnum(n) => Some(n),
other => {
return Err(signal(
"wrong-type-argument",
vec![Value::symbol("integerp"), args[1]],
));
}
}
} else {
None
};
let buf = buffers
.current_buffer()
.ok_or_else(|| signal("error", vec![Value::string("No current buffer")]))?;
let table = SyntaxTable::for_buffer(buf);
let limit = limit.map(|raw| lisp_pos_to_byte(buf, raw));
let new_pos =
skip_syntax_backward_with_options(buf, &table, &syntax_chars, limit, honor_properties);
let old_pt = buffers
.current_buffer()
.ok_or_else(|| signal("error", vec![Value::string("No current buffer")]))?
.point();
let current_id = buffers
.current_buffer_id()
.ok_or_else(|| signal("error", vec![Value::string("No current buffer")]))?;
let _ = buffers.goto_buffer_byte(current_id, new_pos);
let buf = buffers
.get(current_id)
.ok_or_else(|| signal("error", vec![Value::string("No current buffer")]))?;
let chars_moved = if old_pt >= new_pos {
-(buf.text.emacs_byte_to_char(old_pt) as i64 - buf.text.emacs_byte_to_char(new_pos) as i64)
} else {
buf.text.emacs_byte_to_char(new_pos) as i64 - buf.text.emacs_byte_to_char(old_pt) as i64
};
Ok(Value::fixnum(chars_moved))
}
#[cfg(test)]
#[path = "syntax_test.rs"]
mod tests;