emacs_wrapper.rs

Branches > main > cf38a7a > files / nvim / pack / plugins / opt / parinfer-rust / src / emacs_wrapper.rs

use super::parinfer::rc_process;
use emacs::{Env, IntoLisp, Result, Value};
use types::{Change, Error, Options, Request, SharedRequest, WrappedAnswer};

use std::{cell::RefCell, convert::TryFrom, fs::OpenOptions, io::Write, rc::Rc};

emacs::plugin_is_GPL_compatible!();

// This exists because the emacs library doesn't implement
// this type conversion directy
/// A helper function to Option<i64> to Option<usize>
fn to_usize(value: Option<i64>) -> Option<usize> {
    match value {
        Some(item) => match usize::try_from(item) {
            Ok(new_value) => Some(new_value),
            Err(_) => None,
        },
        None => None,
    }
}

fn to_i64(value: Option<usize>) -> Option<i64> {
    match value {
        Some(item) => match i64::try_from(item) {
            Ok(new_value) => Some(new_value),
            Err(_) => None,
        },
        None => None,
    }
}

#[emacs::module(name = "parinfer-rust")]
pub fn init(_: &Env) -> Result<()> {
    Ok(())
}

////////////////////////////////
// Entry point
///////////////////////////////
// Need to wrap Request into a specific lifetime for use as part of this struct are used in the Answer struct
// this is talked about here https://github.com/ubolonton/emacs-module-rs/issues/21
type AliasedRequest<'a> = &'a SharedRequest;

//  https://github.com/shaunlebron/parinfer/tree/master/lib#api
// text is the full text input.
// options is an object with the following properties:
//   cursorLine - zero-based line number of the cursor
//   cursorX - zero-based x-position of the cursor
//   prevCursorLine and prevCursorX is required by Smart Mode (previous cursor position)
//   selectionStartLine - first line of the current selection
//   changes - ordered array of change objects with the following:
//     lineNo - starting line number of the change
//     x - starting x of the change
//     oldText - original text that was replaced
//     newText - new text that replaced the original text
//   forceBalance - employ the aggressive paren-balancing rules from v1 (defaults to false)
//   partialResult - return partially processed text/cursor if an error occurs (defaults to false)

// success is a boolean indicating if the input was properly formatted enough to create a valid result
// text is the full text output (if success is false, returns original text unless partialResult is enabled)
// cursorX/cursorLine is the new position of the cursor (since parinfer may shift it around)
// error is an object populated if success is false:
//   name is the name of the error, which will be any of the following:
//     "quote-danger"
//     "eol-backslash"
//     "unclosed-quote"
//     "unclosed-paren"
//     "unmatched-close-paren"
//     "unhand led"
//   message is a message describing the error
//   lineNo is a zero-based line number where the error occurred
//   x is a zero-based column where the error occurred
//   extra has lineNo and x of open-paren for unmatched-close-paren
// tabStops is an array of objects representing Tab stops, which is populated if a cursor position or selection is supplied. We identify tab stops at relevant open-parens, and supply the following extra information so you may compute extra tab stops for one-space or two-space indentation conventions based on the type of open-paren.
//   x is a zero-based x-position of the tab stop
//   argX position of the first argument after x (e.g. position of bar in (foo bar)
//   lineNo is a zero-based line number of the open-paren responsible for the tab stop
//   ch is the character of the open-paren responsible for the tab stop (e.g. (,[,{)
// parenTrails is an array of object representing the Paren Trails at the end of each line that Parinfer may move
//   lineNo is a zero-based line number
//   startX is a zero-based x-position of the first close-paren
//   endX is a zero-based x-position after the last close-paren
#[defun(user_ptr, mod_in_name = false)]
/// Runs the parinfer algorithm on the given request
///
/// # Examples
///
/// ```elisp,no_run
/// (parinfer-rust-execute request)
/// ```
fn execute(request: AliasedRequest) -> Result<WrappedAnswer> {
    let answer = rc_process(&request);
    let wrapped_answer = unsafe { WrappedAnswer::new(request.clone(), answer) };
    Ok(wrapped_answer)
}
////////////////////////////////
// options
////////////////////////////////
#[defun(user_ptr, mod_in_name = false)]
// Create an Options Structure
// We need this because we can't pass in an optional variant of Options in the new_options function
/// Returns an Option with nil data for all fields
///
/// # Examples
///
/// ```elisp,no_run
/// (parinfer-make-option)
/// ```
fn make_option() -> Result<Options> {
    Ok(Options {
        cursor_x: None,
        cursor_line: None,
        prev_cursor_x: None,
        prev_cursor_line: None,
        prev_text: None,
        selection_start_line: None,
        changes: Vec::new(),
        partial_result: false,
        force_balance: false,
        return_parens: false,
        comment_char: ';',
        string_delimiters: vec!["\"".to_string()],
        lisp_vline_symbols: false,
        lisp_block_comments: false,
        guile_block_comments: false,
        scheme_sexp_comments: false,
        janet_long_strings: false,
        hy_bracket_strings: false,
    })
}

#[defun(user_ptr, mod_in_name = false)]
/// Creates an Options type based on inputs
///
/// # Examples
///
/// ```elisp,no_run
/// (parinfer-new-option 1 1 nil options changes)
/// ```
fn new_options(
    cursor_x: Option<i64>,
    cursor_line: Option<i64>,
    selection_start_line: Option<i64>,
    old_options: &Options,
    changes: &Vec<Change>,
) -> Result<Options> {
    Ok(Options {
        cursor_x: to_usize(cursor_x),
        cursor_line: to_usize(cursor_line),
        prev_cursor_x: old_options.cursor_x,
        prev_cursor_line: old_options.cursor_line,
        selection_start_line: to_usize(selection_start_line),
        changes: changes.clone(),
        prev_text: None,
        partial_result: false,
        force_balance: false,
        return_parens: false,
        comment_char: ';',
        string_delimiters: vec!["\"".to_string()],
        lisp_vline_symbols: false,
        lisp_block_comments: false,
        guile_block_comments: false,
        scheme_sexp_comments: false,
        janet_long_strings: false,
        hy_bracket_strings: false,
    })
}

#[defun(mod_in_name = false)]
/// Returns a string representation of the Options type
///
/// # Examples
///
/// ```elisp,no_run
/// (parinfer-print-option options)
/// ```
fn print_options<'a>(options: &Options) -> Result<String> {
    Ok(format!("{:?}", options.clone()).to_string())
}

////////////////////////////////
// Changes
////////////////////////////////
#[defun(user_ptr, mod_in_name = false)]

/// Creates an empty list of changes
///
/// # Examples
///
/// ```elisp,no_run
/// (parinfer-make-changes)
/// ```
fn make_changes() -> Result<Vec<Change>> {
    Ok(Vec::new())
}
#[defun(user_ptr, mod_in_name = false)]
fn new_change(line_number: i64, x: i64, old_text: String, new_text: String) -> Result<Change> {
    let line_no = usize::try_from(line_number)?;
    let new_x = usize::try_from(x)?;
    let change = Change {
        x: new_x,
        line_no,
        old_text,
        new_text,
    };
    Ok(change)
}

#[defun(mod_in_name = false)]
/// Creates an empty list of changes
///
/// # Examples
///
/// ```elisp,no_run
/// (parinfer-make-changes)
/// ```
fn add_change(changes: &mut Vec<Change>, change: &Change) -> Result<()> {
    Ok(changes.push(change.clone()))
}

#[defun(mod_in_name = false)]
/// Returns a string representing a list of changes
///
/// # Examples
///
/// ```elisp,no_run
/// (parinfer-print-changes changes)
/// ```
fn print_changes<'a>(env: &'a Env, changes: &mut Vec<Change>) -> Result<Value<'a>> {
    format!("{:?}", changes).into_lisp(env)
}

////////////////////////////////
// Request
////////////////////////////////
#[defun(mod_in_name = false)]

/// Creates a Request from the given mode, current buffer text, and the set of Options
///
/// # Examples
///
/// ```elisp,no_run
/// (parinfer-make-request "paren" (buffer-substring-no-properties) options)
/// ```
//
fn make_request(mode: String, text: String, options: &mut Options) -> Result<SharedRequest> {
    let request = Request {
        mode,
        text,
        options: options.clone(),
    };
    Ok(Rc::new(request))
}

/// Creates a Request from the given mode, current buffer text, and the set of Options
///
/// # Examples
///
/// ```elisp,no_run
/// (parinfer-print-request request)
/// ```
//
#[defun(mod_in_name = false)]
fn print_request(request: AliasedRequest) -> Result<String> {
    Ok(format!("{:?}", &request).to_string())
}
////////////////////////////////
// Answer
////////////////////////////////
#[defun(mod_in_name = false)]
/// Gives a hashmap like interface to extracting values from the Answer type
/// Accepted keys are 'text', 'success', 'cursor_x', 'cursor_line', and 'error'
///
/// # Examples
///
/// ```elisp,no_run
/// (parinfer-get-in-answer answer "success")
/// ```
fn get_in_answer<'a>(
    env: &'a Env,
    answer: &WrappedAnswer,
    key: Option<String>,
) -> Result<Value<'a>> {
    let unwrapped_answer = answer.inner();
    let query = match key {
        Some(key) => key,
        None => return env.message("Missing 'key'"),
    };

    // I only care about some nested structures at the moment, errors,
    // so leave tab_stops, paren_trails, and parens as unsupported
    match query.as_ref() {
        "text" => unwrapped_answer.text.to_string().into_lisp(env),
        "success" => unwrapped_answer.success.into_lisp(env),
        "cursor_x" => to_i64(unwrapped_answer.cursor_x).into_lisp(env),
        "cursor_line" => to_i64(unwrapped_answer.cursor_line).into_lisp(env),
        "error" => match unwrapped_answer.error.clone() {
            Some(error) => Ok(RefCell::new(error).into_lisp(env)?),
            None => ().into_lisp(env),
        },
        // "tab_stops"
        // "paren_trails"
        // "parens"
        _ => {
            env.message(format!("Key '{}' unsupported", query))?;
            ().into_lisp(env)
        }
    }
}

#[defun(mod_in_name = false)]
/// Returns a string representation of an Answer
///
/// # Examples
///
/// ```elisp,no_run
/// (parinfer-rust-print-answer answer)
/// ```
fn print_answer(answer: &WrappedAnswer) -> Result<String> {
    Ok(format!("{:?}", answer.inner()).to_string())
}

#[defun(mod_in_name = false)]
/// Prints the current Options and Answer to the specified file
///
/// # Examples
///
/// ```elisp,no_run
/// (parinfer-rust-debug "/tmp/parinfer.txt" options answer)
/// ```
fn debug(
    env: &Env,
    filename: String,
    options: &Options,
    wrapped_answer: &WrappedAnswer,
) -> Result<()> {
    let answer = wrapped_answer.inner();
    let file = match OpenOptions::new().append(true).create(true).open(&filename) {
        Ok(file) => file,
        Err(_) => {
            env.message(&format!("Unable to open file {}", filename))?;
            return Ok(());
        }
    };

    match write!(&file, "Options:\n{:?}\nResponse:\n{:?}\n", options, answer) {
        Ok(_) => {
            env.message(&format!("Wrote debug information to {}", filename))?;
        }
        Err(_) => {
            env.message(&format!("Unable to write to file {}", filename))?;
        }
    };
    Ok(())
}

////////////////////////////////
// Error
////////////////////////////////
#[defun(mod_in_name = false)]
/// Gives a hashmap like interface to extracting values from the error type
/// Accepted keys are 'text', 'success', 'cursor_x', 'cursor_line', and 'error'
///
/// # Examples
///
/// ```elisp,no_run
/// (parinfer-get-in-error error "message")
/// ```
fn get_in_error<'a>(env: &'a Env, error: &Error, key: Option<String>) -> Result<Value<'a>> {
    let query = match key {
        Some(key) => key,
        None => "".to_string(),
    };

    match query.as_ref() {
        "name" => error.name.to_string().into_lisp(env),
        "message" => error.message.clone().into_lisp(env),
        "x" => to_i64(Some(error.x)).into_lisp(env),
        "line_no" => to_i64(Some(error.line_no)).into_lisp(env),
        "input_x" => to_i64(Some(error.input_x)).into_lisp(env),
        "input_line_no" => to_i64(Some(error.input_line_no)).into_lisp(env),
        _ => {
            env.message(format!("Key '{}' unsupported", query))?; // Can return an error
            ().into_lisp(env)
        }
    }
}

#[defun(mod_in_name = false)]
/// Returns a string representation of an Error
///
/// # Examples
///
/// ```elisp,no_run
/// (parinfer-rust-print-error error)
/// ```
fn print_error(error: &Error) -> Result<String> {
    Ok(format!("{:?}", error).to_string())
}

#[defun(mod_in_name = false)]
/// Returns the version of the parinfer-rust library
///
/// # Examples
///
/// ```elisp,no_run
/// (parinfer-rust-version)
/// ```
fn version() -> Result<String> {
    Ok(env!("CARGO_PKG_VERSION").to_string())
}