rune/
editfns.rs

1//! Buffer editing utilities.
2use crate::core::{
3    env::{ArgSlice, Env},
4    gc::{Context, Rt},
5    object::{Object, ObjectType},
6};
7use anyhow::{Result, bail, ensure};
8use rune_macros::defun;
9use std::{fmt::Write as _, io::Write};
10
11#[defun]
12fn message(format_string: &str, args: &[Object]) -> Result<String> {
13    let message = format(format_string, args)?;
14    println!("MESSAGE: {message}");
15    std::io::stdout().flush()?;
16    Ok(message)
17}
18
19defvar!(MESSAGE_NAME);
20defvar!(MESSAGE_TYPE, "new message");
21
22#[defun]
23fn format(string: &str, objects: &[Object]) -> Result<String> {
24    let mut result = String::new();
25    let mut arguments = objects.iter();
26    let mut remaining = string;
27
28    let mut escaped = false;
29    let mut is_format_char = |c: char| {
30        if escaped {
31            escaped = false;
32            false
33        } else if c == '\\' {
34            escaped = true;
35            false
36        } else {
37            c == '%'
38        }
39    };
40    while let Some(start) = remaining.find(&mut is_format_char) {
41        result += &remaining[..start];
42        let Some(specifier) = remaining.as_bytes().get(start + 1) else {
43            bail!("Format string ends in middle of format specifier")
44        };
45        // "%%" inserts a single "%" in the output
46        if *specifier == b'%' {
47            result.push('%');
48        } else {
49            // TODO: currently handles all format types the same. Need to check the modifier characters.
50            let Some(val) = arguments.next() else {
51                bail!("Not enough arguments for format string")
52            };
53            match val.untag() {
54                ObjectType::String(string) => write!(result, "{string}")?,
55                obj => write!(result, "{obj}")?,
56            }
57        }
58        remaining = &remaining[start + 2..];
59    }
60    result += remaining;
61    ensure!(arguments.next().is_none(), "Too many arguments for format string");
62    Ok(result)
63}
64
65#[defun]
66fn format_message(string: &str, objects: &[Object]) -> Result<String> {
67    let formatted = format(string, objects)?;
68    // TODO: implement support for `text-quoting-style`.
69    Ok(formatted
70        .chars()
71        .map(|c| if matches!(c, '`' | '\'') { '"' } else { c })
72        .collect())
73}
74
75#[defun]
76fn string_to_char(string: &str) -> char {
77    string.chars().next().unwrap_or('\0')
78}
79
80#[defun]
81fn char_to_string(chr: char) -> String {
82    format!("{chr}")
83}
84
85#[defun]
86pub(crate) fn insert(args: ArgSlice, env: &mut Rt<Env>, cx: &Context) -> Result<()> {
87    let env = &mut **env; // Deref into rooted type so we can split the borrow
88    let buffer = env.current_buffer.get_mut();
89    let args = Rt::bind_slice(env.stack.arg_slice(args), cx);
90    for arg in args {
91        buffer.insert(*arg)?;
92    }
93    Ok(())
94}
95
96// TODO: this should not throw and error. Buffer will always be present.
97#[defun]
98pub(crate) fn goto_char(position: usize, env: &mut Rt<Env>) -> Result<()> {
99    let buffer = env.current_buffer.get_mut();
100    buffer.text.set_cursor(position);
101    Ok(())
102}
103
104// TODO: this should not throw and error. Buffer will always be present.
105#[defun]
106pub(crate) fn point_max(env: &mut Rt<Env>) -> Result<usize> {
107    let buffer = env.current_buffer.get_mut();
108    // TODO: Handle narrowing
109    Ok(buffer.text.len_chars() + 1)
110}
111
112#[defun]
113pub(crate) fn point_min() -> usize {
114    // TODO: Handle narrowing
115    1
116}
117
118#[defun]
119pub(crate) fn point_marker(env: &mut Rt<Env>) -> usize {
120    // TODO: Implement marker objects
121    env.current_buffer.get_mut().text.cursor().chars()
122}
123
124#[defun]
125fn delete_region(start: usize, end: usize, env: &mut Rt<Env>) -> Result<()> {
126    env.current_buffer.get_mut().delete(start, end)
127}
128
129#[defun]
130fn bolp(env: &Rt<Env>) -> bool {
131    let buf = env.current_buffer.get();
132    let chars = buf.text.cursor().chars();
133    chars == 0 || buf.text.char_at(chars - 1).unwrap() == '\n'
134}
135
136#[defun]
137fn point(env: &Rt<Env>) -> usize {
138    env.current_buffer.get().text.cursor().chars()
139}
140
141#[defun]
142fn system_name() -> String {
143    hostname::get()
144        .expect("Failed to get hostname")
145        .into_string()
146        .expect("Failed to convert OsString to String")
147}
148
149#[cfg(test)]
150mod test {
151    use crate::core::object::NIL;
152    use crate::{
153        buffer::{get_buffer_create, set_buffer},
154        core::gc::RootSet,
155    };
156    use rune_core::macros::root;
157
158    use super::*;
159
160    #[test]
161    fn test_format() {
162        assert_eq!(&format("%s", &[1.into()]).unwrap(), "1");
163        assert_eq!(&format("foo-%s", &[2.into()]).unwrap(), "foo-2");
164        assert_eq!(&format("%%", &[]).unwrap(), "%");
165        assert_eq!(&format("_%%_", &[]).unwrap(), "_%_");
166        assert_eq!(&format("foo-%s %s", &[3.into(), 4.into()]).unwrap(), "foo-3 4");
167        let sym = crate::core::env::sym::FUNCTION.into();
168        assert_eq!(&format("%s", &[sym]).unwrap(), "function");
169
170        assert!(&format("%s", &[]).is_err());
171        assert!(&format("%s", &[1.into(), 2.into()]).is_err());
172
173        assert!(format("`%s' %s%s%s", &[0.into(), 1.into(), 2.into(), 3.into()]).is_ok());
174    }
175
176    #[test]
177    fn test_insert() {
178        let roots = &RootSet::default();
179        let cx = &mut Context::new(roots);
180        root!(env, new(Env), cx);
181        let buffer = get_buffer_create(cx.add("test_insert"), Some(NIL), cx).unwrap();
182        set_buffer(buffer, env, cx).unwrap();
183        cx.garbage_collect(true);
184        env.stack.push(104);
185        env.stack.push(101);
186        env.stack.push(108);
187        env.stack.push(108);
188        env.stack.push(111);
189        insert(ArgSlice::new(5), env, cx).unwrap();
190        assert_eq!(env.current_buffer.get(), "hello");
191    }
192
193    #[test]
194    fn test_delete_region() {
195        let roots = &RootSet::default();
196        let cx = &mut Context::new(roots);
197        root!(env, new(Env), cx);
198        let buffer = get_buffer_create(cx.add("test_delete_region"), Some(NIL), cx).unwrap();
199        set_buffer(buffer, env, cx).unwrap();
200        cx.garbage_collect(true);
201        env.stack.push(cx.add("hello"));
202        env.stack.push(cx.add(" world"));
203        insert(ArgSlice::new(2), env, cx).unwrap();
204
205        assert_eq!(env.current_buffer.get(), "hello world");
206        delete_region(2, 4, env).unwrap();
207        assert_eq!(env.current_buffer.get(), "hlo world");
208    }
209}