Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement LSP functions in Rust #477

Merged
merged 42 commits into from
May 15, 2023
Merged
Show file tree
Hide file tree
Changes from 39 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
6dc2c17
Reorder forrmula functions to match official docs
HactarCE Mar 10, 2023
69c8009
Refactor formulas
HactarCE Mar 14, 2023
84ce4a8
Fix clippy lints
HactarCE Mar 15, 2023
9083d76
Merge branch 'main' into ajf/more-formulas
HactarCE Mar 15, 2023
2a6750f
Add `col!` and `pos!` macros
HactarCE Mar 15, 2023
c3158b0
Add `parse_formula()` wasm function
HactarCE Mar 15, 2023
4e5abc8
Merge branch 'main' into ajf/more-formulas
HactarCE Mar 24, 2023
f292018
Run prettier
HactarCE Mar 31, 2023
1f821ea
Merge branch 'main' into ajf/more-formulas
HactarCE Mar 31, 2023
945dde0
Expose `column_name` and `column_from_name` to JS
HactarCE Mar 31, 2023
c243a98
Bump quadratic-core version
HactarCE Mar 31, 2023
4c06615
Merge branch 'main' into ajf/formulas-lsp
HactarCE May 2, 2023
b51add4
Implement LSP autocomplete via Rust
HactarCE May 8, 2023
4cc763e
Add some missing formula documentation
HactarCE May 8, 2023
65ca9c7
Merge branch 'main' into ajf/more-formulas
HactarCE May 8, 2023
af33e6e
Merge branch 'main' into ajf/more-formulas
HactarCE May 8, 2023
373948b
feat: add support for `Delete` key (#453)
davidfig May 1, 2023
1756037
Add sentry to api (#459)
davidkircos May 1, 2023
5defc51
Add sentry to api (#460)
davidkircos May 1, 2023
2c9ed66
fix: allow wider array of characters for initial cell input (#451)
jimniels May 1, 2023
b271e9f
chore: update browserslist (#461)
jimniels May 2, 2023
5e25650
Dont call captureException with a INFO level message (#464)
davidkircos May 2, 2023
f392578
chore: support hosting via vercel (#467)
jimniels May 3, 2023
75f91be
fix: gridSettings (#470)
jimniels May 5, 2023
26c609a
feat: AI ui/ux enhancements (#466)
jimniels May 5, 2023
4da99fc
fix: support spaces when importing file from url
jimniels May 5, 2023
789bc4b
fix: close editor when creating new file (#465)
jimniels May 5, 2023
9dfb0b5
fix: changing settings properly rerenders cells (#475)
davidfig May 8, 2023
fb167ce
fix: bug with upgrade file in validateGridFile (#476)
davidfig May 8, 2023
29aa236
Autogenerate Notion docs for formulas
HactarCE May 8, 2023
0fb69f7
Merge branch 'ajf/more-formulas' into ajf/formulas-lsp
HactarCE May 8, 2023
1dc0edc
Replace `CELL` with Excel-compatible `INDIRECT`
HactarCE May 8, 2023
be8cde9
Merge branch 'main' into ajf/more-formulas
HactarCE May 8, 2023
62ba32a
Update test to use `INDIRECT` instead of `CELL`
HactarCE May 8, 2023
81c7da3
Merge branch 'ajf/more-formulas' into ajf/formulas-lsp
HactarCE May 8, 2023
f815e6d
Add examples for all formula functions
HactarCE May 8, 2023
48a6e6b
Add macro for JS value manipulation
HactarCE May 9, 2023
de25001
https://tonsky.me/blog/font-size/
HactarCE May 9, 2023
340b6df
Merge branch 'main' into ajf/formulas-lsp
HactarCE May 9, 2023
23f15ae
Bump Rust version
HactarCE May 10, 2023
2b61165
Merge branch 'main' into ajf/formulas-lsp
HactarCE May 10, 2023
5e2a229
Run prettier
HactarCE May 10, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 33 additions & 10 deletions quadratic-core/Cargo.lock

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

5 changes: 5 additions & 0 deletions quadratic-core/Cargo.toml
HactarCE marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 4 additions & 0 deletions quadratic-core/src/formulas/functions/statistics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ fn get_functions() -> Vec<FormulaFunction> {
name: "AVERAGE",
arg_completion: "${1:a, b, ...}",
usages: &["a, b, ..."],
examples: &["AVERAGE(A1:A6)", "AVERAGE(A1, A3, A5, B1:B6)"],
doc: "Returns the arithmetic mean of all values.",
eval: util::pure_fn(|args| {
Ok(Value::Number(
Expand All @@ -25,13 +26,15 @@ fn get_functions() -> Vec<FormulaFunction> {
name: "COUNT",
arg_completion: "${1:a, b, ...}",
usages: &["a, b, ..."],
examples: &["COUNT(A1:C42, E17)", "SUM(A1:A10) / COUNT(A1:A10)"],
doc: "Returns the number of nonempty values.",
eval: util::pure_fn(|args| Ok(Value::Number(util::count(&args.inner) as f64))),
},
FormulaFunction {
name: "MIN",
arg_completion: "${1:a, b, ...}",
usages: &["a, b, ..."],
examples: &["MIN(A1:A6)", "MIN(0, A1:A6)"],
doc: "Returns the smallest value.\nReturns +∞ if given no values.",
eval: util::pure_fn(|args| {
Ok(Value::Number(
Expand All @@ -45,6 +48,7 @@ fn get_functions() -> Vec<FormulaFunction> {
name: "MAX",
arg_completion: "${1:a, b, ...}",
usages: &["a, b, ..."],
examples: &["MAX(A1:A6)", "MAX(0, A1:A6)"],
doc: "Returns the largest value.\nReturns -∞ if given no values.",
eval: util::pure_fn(|args| {
Ok(Value::Number(
Expand Down
Loading
Loading