Skip to content

Commit

Permalink
feat: implement LSP functions in Rust (#477)
Browse files Browse the repository at this point in the history
* Refactor formulas
    * Reorder formula functions to match official docs
    * Add some missing formula documentation
    * Documentation is now included alongside function definitions, so we can auto-generate formula function documentation soon
    * Add examples for all formula functions
* Add `parse_formula()` wasm function
* Expose `column_name` and `column_from_name` to JS
* Implement LSP autocomplete via Rust
* Replace `CELL` with Excel-compatible `INDIRECT`
    * Make grid access async so it should actually work now
* Autogenerate Notion docs for formulas

quadratic-core internal changes:
* Add macro for JS value manipulation
* Add `col!` and `pos!` macros
  • Loading branch information
HactarCE authored May 15, 2023
1 parent 18815f3 commit 2326f96
Show file tree
Hide file tree
Showing 18 changed files with 415 additions and 109 deletions.
45 changes: 34 additions & 11 deletions quadratic-core/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion quadratic-core/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "quadratic-core"
version = "0.1.4"
version = "0.1.5"
authors = ["Andrew Farkas <andrew.farkas@quadratic.to>"]
edition = "2021"
description = "Infinite data grid with Python, JavaScript, and SQL built-in"
Expand All @@ -12,6 +12,10 @@ license = "MIT"
[lib]
crate-type = ["cdylib", "rlib"]

[[bin]]
name = "docgen"
path = "src/bin/docgen.rs"

[features]
default = ["console_error_panic_hook"]

Expand All @@ -24,6 +28,7 @@ lazy_static = "1.4"
regex = "1.7"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_repr = "0.1"
serde-wasm-bindgen = "0.4.5"
smallvec = "1.10.0"
strum = "0.24.1"
Expand Down
11 changes: 11 additions & 0 deletions quadratic-core/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Quadratic Core

This contains the Rust code that powers Quadratic's client via WASM.

## Formula function documentation

Documentation for formula functions can be found in next to the Rust implementation of each function, in `src/formulas/functions/*.rs`. (`mod.rs` and `util.rs` do not contain any functions.)

### Rebuilding formula function documentation

Run `cargo run --bin docgen`, then copy/paste from `formula_docs_output.md` into Notion. Copying from VSCode will include formatting, so you may have to first paste it into a plaintext editor like Notepad, then copy/paste from there into Notion.
30 changes: 30 additions & 0 deletions quadratic-core/src/bin/docgen.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
use itertools::Itertools;

const OUT_FILENAME: &str = "formula_docs_output.md";

fn main() {
let mut output = String::new();

for category in quadratic_core::formulas::functions::CATEGORIES {
if !category.include_in_docs {
continue;
}

output.push_str(&format!("## {}\n\n", category.name));
output.push_str(category.docs);

// Table header.
output.push_str("| **Function** | **Description** |\n");
output.push_str("| ------------ | --------------- |\n");
for func in (category.get_functions)() {
let usages = func.usages_strings().map(|s| format!("`{s}`")).join("; ");
let doc = func.doc.replace('\n', " ");
output.push_str(&format!("| {usages} | {doc} |\n"));
}
output.push('\n');
}

std::fs::write(OUT_FILENAME, output).expect("failed to write to output file");

println!("Docs were written to {OUT_FILENAME}. No need to check this file into git.")
}
2 changes: 1 addition & 1 deletion quadratic-core/src/formulas/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ impl AstNode {
inner: arg_values,
};

let func_name = func.inner.to_ascii_uppercase();
let func_name = &func.inner;
match functions::lookup_function(&func_name) {
Some(f) => (f.eval)(&mut *ctx, spanned_arg_values).await?,
None => return Err(FormulaErrorMsg::BadFunctionName.with_span(func.span)),
Expand Down
10 changes: 10 additions & 0 deletions quadratic-core/src/formulas/functions/logic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,23 @@ fn get_functions() -> Vec<FormulaFunction> {
name: "TRUE",
arg_completion: "",
usages: &[""],
examples: &["TRUE()"],
doc: "Returns `TRUE`.",
eval: util::constant_fn(Value::Bool(true)),
},
FormulaFunction {
name: "FALSE",
arg_completion: "",
usages: &[""],
examples: &["FALSE()"],
doc: "Returns `FALSE`.",
eval: util::constant_fn(Value::Bool(false)),
},
FormulaFunction {
name: "NOT",
arg_completion: "${1:a}",
usages: &["a"],
examples: &["NOT(A113)"],
doc: "Returns `TRUE` if `a` is falsey and \
`FALSE` if `a` is truthy.",
eval: util::array_mapped(|[a]| Ok(Value::Bool(!a.to_bool()?))),
Expand All @@ -41,6 +44,7 @@ fn get_functions() -> Vec<FormulaFunction> {
name: "AND",
arg_completion: "${1:a, b, ...}",
usages: &["a, b, ..."],
examples: &["AND(A1:C1)", "AND(A1, B12)"],
doc: "Returns `TRUE` if all values are truthy \
and `FALSE` if any values is falsey.\n\\
Returns `TRUE` if given no values.",
Expand All @@ -54,6 +58,7 @@ fn get_functions() -> Vec<FormulaFunction> {
name: "OR",
arg_completion: "${1:a, b, ...}",
usages: &["a, b, ..."],
examples: &["OR(A1:C1)", "OR(A1, B12)"],
doc: "Returns `TRUE` if any value is truthy \
and `FALSE` if any value is falsey.\n\
Returns `FALSE` if given no values.",
Expand All @@ -67,6 +72,7 @@ fn get_functions() -> Vec<FormulaFunction> {
name: "XOR",
arg_completion: "${1:a, b, ...}",
usages: &["a, b, ..."],
examples: &["XOR(A1:C1)", "XOR(A1, B12)"],
doc: "Returns `TRUE` if an odd number of values \
are truthy and `FALSE` if an even number \
of values are truthy.\n\
Expand All @@ -81,6 +87,10 @@ fn get_functions() -> Vec<FormulaFunction> {
name: "IF",
arg_completion: "${1:cond}, ${2:t}, ${3:f}",
usages: &["cond, t, f"],
examples: &[
"IF(A2<0, \"A2 is negative\", \"A2 is nonnegative\")",
"IF(A2<0, \"A2 is negative\", IF(A2>0, \"A2 is positive\", \"A2 is zero\"))",
],
doc: "Returns `t` if `cond` is truthy and `f` if `cond` if falsey.",
eval: util::array_mapped(|[cond, t, f]| {
Ok(if cond.to_bool()? { t.inner } else { f.inner })
Expand Down
1 change: 1 addition & 0 deletions quadratic-core/src/formulas/functions/lookup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ fn get_functions() -> Vec<FormulaFunction> {
name: "INDIRECT",
arg_completion: "${1:cellref_string}",
usages: &["cellref_string"],
examples: &["INDIRECT(\"Cn7\")", "INDIRECT(\"F\" & B0)"],
doc: "Returns the value of the cell at a given location.",
eval: Box::new(|ctx, args| ctx.array_mapped_indirect(args).boxed_local()),
}]
Expand Down
2 changes: 2 additions & 0 deletions quadratic-core/src/formulas/functions/mathematics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@ fn get_functions() -> Vec<FormulaFunction> {
name: "SUM",
arg_completion: "${1:a, b, ...}",
usages: &["a, b, ..."],
examples: &["SUM(B2:C6, 15, E1)"],
doc: "Adds all values.\nReturns `0` if given no values.",
eval: util::pure_fn(|args| Ok(Value::Number(util::sum(&args.inner)?))),
},
FormulaFunction {
name: "PRODUCT",
arg_completion: "${1:a, b, ...}",
usages: &["a, b, ..."],
examples: &["PRODUCT(B2:C6, 0.002, E1)"],
doc: "Multiplies all values.\nReturns 1 if given no values.",
eval: util::pure_fn(|args| Ok(Value::Number(util::product(&args.inner)?))),
},
Expand Down
41 changes: 39 additions & 2 deletions quadratic-core/src/formulas/functions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ mod util;

use super::{Ctx, FormulaErrorMsg, FormulaResult, Span, Spanned, Value};

pub fn lookup_function(name: &str) -> Option<&FormulaFunction> {
ALL_FUNCTIONS.get(name)
pub fn lookup_function(name: &str) -> Option<&'static FormulaFunction> {
ALL_FUNCTIONS.get(name.to_ascii_uppercase().as_str())
}

pub const CATEGORIES: &[FormulaFunctionCategory] = &[
Expand Down Expand Up @@ -53,10 +53,13 @@ pub struct FormulaFunction {
pub name: &'static str,
pub arg_completion: &'static str,
pub usages: &'static [&'static str],
pub examples: &'static [&'static str],
pub doc: &'static str,
pub eval: FormulaFn,
}
impl FormulaFunction {
/// Constructs a function for an operator that can be called with one or two
/// arguments.
fn variadic_operator<const N1: usize, const N2: usize>(
name: &'static str,
eval_monadic: Option<NonVariadicFn<N1>>,
Expand All @@ -66,6 +69,7 @@ impl FormulaFunction {
name,
arg_completion: "",
usages: &[],
examples: &[],
doc: "",
eval: Box::new(move |ctx, args| {
async move {
Expand All @@ -86,15 +90,48 @@ impl FormulaFunction {
}
}

/// Constructs an operator that can be called with a fixed number of
/// arguments.
fn operator<const N: usize>(name: &'static str, eval_fn: NonVariadicPureFn<N>) -> Self {
Self {
name,
arg_completion: "",
usages: &[],
examples: &[],
doc: "",
eval: util::array_mapped(eval_fn),
}
}

/// Returns a user-friendly string containing the usages of this function,
/// delimited by newlines.
pub fn usages_strings(&self) -> impl Iterator<Item = String> {
let name = self.name;
self.usages
.iter()
.map(move |args| format!("{name}({args})"))
}

/// Returns the autocomplete snippet for this function.
pub fn autocomplete_snippet(&self) -> String {
let name = self.name;
let arg_completion = self.arg_completion;
format!("{name}({arg_completion})")
}

/// Returns the Markdown documentation for this function that should appear
/// in the formula editor via the language server.
pub fn lsp_full_docs(&self) -> String {
let mut ret = String::new();
if !self.doc.is_empty() {
ret.push_str(&format!("# Description\n\n{}\n\n", self.doc));
}
if !self.examples.is_empty() {
let examples = self.examples.iter().map(|s| format!("- `{s}`\n")).join("");
ret.push_str(&format!("# Examples\n\n{examples}\n\n"));
}
ret
}
}

pub struct FormulaFunctionCategory {
Expand Down
Loading

1 comment on commit 2326f96

@vercel
Copy link

@vercel vercel bot commented on 2326f96 May 15, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

quadratic – ./

quadratic-nu.vercel.app
quadratic-git-main-quadratic.vercel.app
quadratic-quadratic.vercel.app

Please sign in to comment.