diff --git a/Cargo.lock b/Cargo.lock index fd613d7f7..2fa56aaa7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -775,6 +775,7 @@ dependencies = [ "cfg-if 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)", "js-sys 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", "rustpython_parser 0.0.1", "rustpython_vm 0.1.0", "wasm-bindgen 0.2.29 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/parser/src/lexer.rs b/parser/src/lexer.rs index 619b2dd1a..0c3d88780 100644 --- a/parser/src/lexer.rs +++ b/parser/src/lexer.rs @@ -55,6 +55,7 @@ pub struct Lexer> { pub enum LexicalError { StringError, NestingError, + UnrecognizedToken { tok: char }, } #[derive(Clone, Debug, Default, PartialEq)] @@ -687,7 +688,9 @@ where match self.chr0 { Some('0'..='9') => return Some(self.lex_number()), - Some('_') | Some('a'..='z') | Some('A'..='Z') => return Some(self.lex_identifier()), + Some('_') | Some('a'..='z') | Some('A'..='Z') => { + return Some(self.lex_identifier()); + } Some('#') => { self.lex_comment(); continue; @@ -1033,7 +1036,7 @@ where None => return None, _ => { let c = self.next_char(); - panic!("Not impl {:?}", c) + return Some(Err(LexicalError::UnrecognizedToken { tok: c.unwrap() })); } // Ignore all the rest.. } } diff --git a/parser/src/parser.rs b/parser/src/parser.rs index 2a39a41f8..4587e457e 100644 --- a/parser/src/parser.rs +++ b/parser/src/parser.rs @@ -306,7 +306,9 @@ mod tests { #[test] fn test_parse_class() { - let source = String::from("class Foo(A, B):\n def __init__(self):\n pass\n def method_with_default(self, arg='default'):\n pass\n"); + let source = String::from( + "class Foo(A, B):\n def __init__(self):\n pass\n def method_with_default(self, arg='default'):\n pass\n", + ); assert_eq!( parse_statement(&source), Ok(ast::LocatedStatement { diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 000000000..32a9786fa --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1 @@ +edition = "2018" diff --git a/tests/snippets/ints.py b/tests/snippets/ints.py index 9aa83eba4..3bf91491d 100644 --- a/tests/snippets/ints.py +++ b/tests/snippets/ints.py @@ -1,3 +1,5 @@ +from testutils import assert_raises + # int to int comparisons assert 1 == 1 @@ -36,6 +38,7 @@ assert (2).__rtruediv__(1) == 0.5 assert (1).real == 1 assert (1).imag == 0 +assert_raises(OverflowError, lambda: 1 << 10 ** 100000) assert (1).__eq__(1.0) == NotImplemented assert (1).__ne__(1.0) == NotImplemented diff --git a/tests/snippets/list.py b/tests/snippets/list.py index 881279f07..9e385abdc 100644 --- a/tests/snippets/list.py +++ b/tests/snippets/list.py @@ -117,3 +117,42 @@ b.append(c) assert a == b assert [foo] == [foo] + +for size in [1, 2, 3, 4, 5, 8, 10, 100, 1000]: + lst = list(range(size)) + orig = lst[:] + lst.sort() + assert lst == orig + assert sorted(lst) == orig + assert_raises(ZeroDivisionError, lambda: sorted(lst, key=lambda x: 1/x)) + lst.reverse() + assert sorted(lst) == orig + assert sorted(lst, reverse=True) == lst + assert sorted(lst, key=lambda x: -x) == lst + assert sorted(lst, key=lambda x: -x, reverse=True) == orig + +assert sorted([(1, 2, 3), (0, 3, 6)]) == [(0, 3, 6), (1, 2, 3)] +assert sorted([(1, 2, 3), (0, 3, 6)], key=lambda x: x[0]) == [(0, 3, 6), (1, 2, 3)] +assert sorted([(1, 2, 3), (0, 3, 6)], key=lambda x: x[1]) == [(1, 2, 3), (0, 3, 6)] +assert sorted([(1, 2), (), (5,)], key=len) == [(), (5,), (1, 2)] + +lst = [3, 1, 5, 2, 4] +class C: + def __init__(self, x): self.x = x + def __lt__(self, other): return self.x < other.x +lst.sort(key=C) +assert lst == [1, 2, 3, 4, 5] + +lst = [3, 1, 5, 2, 4] +class C: + def __init__(self, x): self.x = x + def __gt__(self, other): return self.x > other.x +lst.sort(key=C) +assert lst == [1, 2, 3, 4, 5] + +lst = [5, 1, 2, 3, 4] +def f(x): + lst.append(1) + return x +assert_raises(ValueError, lambda: lst.sort(key=f)) # "list modified during sort" +assert lst == [1, 2, 3, 4, 5] diff --git a/vm/src/builtins.rs b/vm/src/builtins.rs index 57f77c81d..870b3573e 100644 --- a/vm/src/builtins.rs +++ b/vm/src/builtins.rs @@ -689,7 +689,16 @@ fn builtin_setattr(vm: &mut VirtualMachine, args: PyFuncArgs) -> PyResult { } // builtin_slice -// builtin_sorted + +fn builtin_sorted(vm: &mut VirtualMachine, mut args: PyFuncArgs) -> PyResult { + arg_check!(vm, args, required = [(iterable, None)]); + let items = vm.extract_elements(iterable)?; + let lst = vm.ctx.new_list(items); + + args.shift(); + vm.call_method_pyargs(&lst, "sort", args)?; + Ok(lst) +} fn builtin_sum(vm: &mut VirtualMachine, args: PyFuncArgs) -> PyResult { arg_check!(vm, args, required = [(iterable, None)]); @@ -763,6 +772,7 @@ pub fn make_module(ctx: &PyContext) -> PyObjectRef { "round" => ctx.new_rustfunc(builtin_round), "set" => ctx.set_type(), "setattr" => ctx.new_rustfunc(builtin_setattr), + "sorted" => ctx.new_rustfunc(builtin_sorted), "slice" => ctx.slice_type(), "staticmethod" => ctx.staticmethod_type(), "str" => ctx.str_type(), diff --git a/vm/src/compile.rs b/vm/src/compile.rs index 6ec3fd6cc..1fcce93c9 100644 --- a/vm/src/compile.rs +++ b/vm/src/compile.rs @@ -1389,7 +1389,7 @@ impl Compiler { // Low level helper functions: fn emit(&mut self, instruction: Instruction) { let location = self.current_source_location.clone(); - let mut cur_code_obj = self.current_code_object(); + let cur_code_obj = self.current_code_object(); cur_code_obj.instructions.push(instruction); cur_code_obj.locations.push(location); // TODO: insert source filename diff --git a/vm/src/import.rs b/vm/src/import.rs index b0b53156c..454cf8442 100644 --- a/vm/src/import.rs +++ b/vm/src/import.rs @@ -60,7 +60,7 @@ pub fn import_module( return Ok(module); } let module = import_uncached_module(vm, current_path, module_name)?; - vm.ctx.set_item(&sys_modules, module_name, module.clone()); + sys_modules.set_item(&vm.ctx, module_name, module.clone()); Ok(module) } diff --git a/vm/src/obj/objint.rs b/vm/src/obj/objint.rs index 9f5e48893..8c240fba3 100644 --- a/vm/src/obj/objint.rs +++ b/vm/src/obj/objint.rs @@ -246,8 +246,7 @@ fn int_lshift(vm: &mut VirtualMachine, args: PyFuncArgs) -> PyResult { match get_value(i2) { ref v if *v < BigInt::zero() => Err(vm.new_value_error("negative shift count".to_string())), ref v if *v > BigInt::from(usize::max_value()) => { - // TODO: raise OverflowError - panic!("Failed converting {} to rust usize", get_value(i2)); + Err(vm.new_overflow_error("the number is too large to convert to int".to_string())) } _ => panic!("Failed converting {} to rust usize", get_value(i2)), } @@ -276,8 +275,7 @@ fn int_rshift(vm: &mut VirtualMachine, args: PyFuncArgs) -> PyResult { match get_value(i2) { ref v if *v < BigInt::zero() => Err(vm.new_value_error("negative shift count".to_string())), ref v if *v > BigInt::from(usize::max_value()) => { - // TODO: raise OverflowError - panic!("Failed converting {} to rust usize", get_value(i2)); + Err(vm.new_overflow_error("the number is too large to convert to int".to_string())) } _ => panic!("Failed converting {} to rust usize", get_value(i2)), } diff --git a/vm/src/obj/objlist.rs b/vm/src/obj/objlist.rs index 8c72bc9a9..e09f15a8b 100644 --- a/vm/src/obj/objlist.rs +++ b/vm/src/obj/objlist.rs @@ -3,8 +3,8 @@ use std::cell::{Cell, RefCell}; use super::objbool; use super::objint; use super::objsequence::{ - get_elements, get_item, get_mut_elements, seq_equal, seq_ge, seq_gt, seq_le, seq_lt, seq_mul, - PySliceableSequence, + get_elements, get_elements_cell, get_item, get_mut_elements, seq_equal, seq_ge, seq_gt, seq_le, + seq_lt, seq_mul, PySliceableSequence, }; use super::objstr; use super::objtype; @@ -334,12 +334,99 @@ fn list_reverse(vm: &mut VirtualMachine, args: PyFuncArgs) -> PyResult { Ok(vm.get_none()) } +fn quicksort( + vm: &mut VirtualMachine, + keys: &mut [PyObjectRef], + values: &mut [PyObjectRef], +) -> PyResult<()> { + let len = values.len(); + if len >= 2 { + let pivot = partition(vm, keys, values)?; + quicksort(vm, &mut keys[0..pivot], &mut values[0..pivot])?; + quicksort(vm, &mut keys[pivot + 1..len], &mut values[pivot + 1..len])?; + } + Ok(()) +} + +fn partition( + vm: &mut VirtualMachine, + keys: &mut [PyObjectRef], + values: &mut [PyObjectRef], +) -> PyResult { + let len = values.len(); + let pivot = len / 2; + + values.swap(pivot, len - 1); + keys.swap(pivot, len - 1); + + let mut store_idx = 0; + for i in 0..len - 1 { + let result = vm._lt(keys[i].clone(), keys[len - 1].clone())?; + let boolval = objbool::boolval(vm, result)?; + if boolval { + values.swap(i, store_idx); + keys.swap(i, store_idx); + store_idx += 1; + } + } + + values.swap(store_idx, len - 1); + keys.swap(store_idx, len - 1); + Ok(store_idx) +} + +fn do_sort( + vm: &mut VirtualMachine, + values: &mut Vec, + key_func: Option, + reverse: bool, +) -> PyResult<()> { + // build a list of keys. If no keyfunc is provided, it's a copy of the list. + let mut keys: Vec = vec![]; + for x in values.iter() { + keys.push(match &key_func { + None => x.clone(), + Some(ref func) => vm.invoke( + (*func).clone(), + PyFuncArgs { + args: vec![x.clone()], + kwargs: vec![], + }, + )?, + }); + } + + quicksort(vm, &mut keys, values)?; + + if reverse { + values.reverse(); + } + + Ok(()) +} + fn list_sort(vm: &mut VirtualMachine, args: PyFuncArgs) -> PyResult { arg_check!(vm, args, required = [(list, Some(vm.ctx.list_type()))]); - let mut _elements = get_mut_elements(list); - unimplemented!("TODO: figure out how to invoke `sort_by` on a Vec"); - // elements.sort_by(); - // Ok(vm.get_none()) + let key_func = args.get_optional_kwarg("key"); + let reverse = args.get_optional_kwarg("reverse"); + let reverse = match reverse { + None => false, + Some(val) => objbool::boolval(vm, val)?, + }; + + let elements_cell = get_elements_cell(list); + // replace list contents with [] for duration of sort. + // this prevents keyfunc from messing with the list and makes it easy to + // check if it tries to append elements to it. + let mut elements = elements_cell.replace(vec![]); + do_sort(vm, &mut elements, key_func, reverse)?; + let temp_elements = elements_cell.replace(elements); + + if !temp_elements.is_empty() { + return Err(vm.new_value_error("list modified during sort".to_string())); + } + + Ok(vm.get_none()) } fn list_contains(vm: &mut VirtualMachine, args: PyFuncArgs) -> PyResult { diff --git a/vm/src/obj/objobject.rs b/vm/src/obj/objobject.rs index 67414373a..73a4df608 100644 --- a/vm/src/obj/objobject.rs +++ b/vm/src/obj/objobject.rs @@ -1,8 +1,8 @@ use super::objstr; use super::objtype; use crate::pyobject::{ - AttributeProtocol, IdProtocol, PyContext, PyFuncArgs, PyObject, PyObjectPayload, PyObjectRef, - PyResult, TypeProtocol, + AttributeProtocol, DictProtocol, IdProtocol, PyContext, PyFuncArgs, PyObject, PyObjectPayload, + PyObjectRef, PyResult, TypeProtocol, }; use crate::vm::VirtualMachine; use std::cell::RefCell; @@ -182,7 +182,7 @@ fn object_dict(vm: &mut VirtualMachine, args: PyFuncArgs) -> PyResult { PyObjectPayload::Class { ref dict, .. } | PyObjectPayload::Instance { ref dict, .. } => { let new_dict = vm.new_dict(); for (attr, value) in dict.borrow().iter() { - vm.ctx.set_item(&new_dict, &attr, value.clone()); + new_dict.set_item(&vm.ctx, &attr, value.clone()); } Ok(new_dict) } diff --git a/vm/src/obj/objsequence.rs b/vm/src/obj/objsequence.rs index 58437b98e..46912c1b8 100644 --- a/vm/src/obj/objsequence.rs +++ b/vm/src/obj/objsequence.rs @@ -302,6 +302,14 @@ pub fn seq_mul(elements: &[PyObjectRef], product: &PyObjectRef) -> Vec(obj: &'a PyObjectRef) -> &'a RefCell> { + if let PyObjectPayload::Sequence { ref elements } = obj.payload { + elements + } else { + panic!("Cannot extract elements from non-sequence"); + } +} + pub fn get_elements<'a>(obj: &'a PyObjectRef) -> impl Deref> + 'a { if let PyObjectPayload::Sequence { ref elements } = obj.payload { elements.borrow() diff --git a/vm/src/pyobject.rs b/vm/src/pyobject.rs index 54201d643..d29650b26 100644 --- a/vm/src/pyobject.rs +++ b/vm/src/pyobject.rs @@ -833,6 +833,7 @@ pub trait DictProtocol { fn contains_key(&self, k: &str) -> bool; fn get_item(&self, k: &str) -> Option; fn get_key_value_pairs(&self) -> Vec<(PyObjectRef, PyObjectRef)>; + fn set_item(&self, ctx: &PyContext, key: &str, v: PyObjectRef); } impl DictProtocol for PyObjectRef { @@ -851,8 +852,9 @@ impl DictProtocol for PyObjectRef { PyObjectPayload::Dict { ref elements } => { objdict::content_get_key_str(&elements.borrow(), k) } + PyObjectPayload::Module { ref dict, .. } => dict.get_item(k), PyObjectPayload::Scope { ref scope } => scope.borrow().locals.get_item(k), - _ => panic!("TODO"), + ref k => panic!("TODO {:?}", k), } } @@ -864,6 +866,23 @@ impl DictProtocol for PyObjectRef { _ => panic!("TODO"), } } + + // Item set/get: + fn set_item(&self, ctx: &PyContext, key: &str, v: PyObjectRef) { + match &self.payload { + PyObjectPayload::Dict { elements } => { + let key = ctx.new_str(key.to_string()); + objdict::set_item_in_content(&mut elements.borrow_mut(), &key, &v); + } + PyObjectPayload::Module { dict, .. } => { + dict.set_item(ctx, key, v); + } + PyObjectPayload::Scope { scope, .. } => { + scope.borrow().locals.set_item(ctx, key, v); + } + ref k => panic!("TODO {:?}", k), + }; + } } pub trait BufferProtocol { diff --git a/vm/src/stdlib/json.rs b/vm/src/stdlib/json.rs index 78fc05dc9..fef284000 100644 --- a/vm/src/stdlib/json.rs +++ b/vm/src/stdlib/json.rs @@ -171,7 +171,7 @@ impl<'de> Visitor<'de> for PyObjectDeserializer<'de> { PyObjectPayload::String { ref value } => value.clone(), _ => unimplemented!("map keys must be strings"), }; - self.vm.ctx.set_item(&dict, &key, value); + dict.set_item(&self.vm.ctx, &key, value); } Ok(dict) } diff --git a/vm/src/sysmodule.rs b/vm/src/sysmodule.rs index 00453f10e..fbc3fc912 100644 --- a/vm/src/sysmodule.rs +++ b/vm/src/sysmodule.rs @@ -1,4 +1,4 @@ -use crate::pyobject::{PyContext, PyFuncArgs, PyObjectRef, PyResult, TypeProtocol}; +use crate::pyobject::{DictProtocol, PyContext, PyFuncArgs, PyObjectRef, PyResult, TypeProtocol}; use crate::vm::VirtualMachine; use std::rc::Rc; use std::{env, mem}; @@ -132,7 +132,7 @@ settrace() -- set the global debug tracing function "_getframe" => ctx.new_rustfunc(getframe), }); - ctx.set_item(&modules, sys_name, sys_mod.clone()); + modules.set_item(&ctx, sys_name, sys_mod.clone()); ctx.set_attr(&sys_mod, "modules", modules); sys_mod diff --git a/vm/src/vm.rs b/vm/src/vm.rs index 0a0f97ce5..d14d92668 100644 --- a/vm/src/vm.rs +++ b/vm/src/vm.rs @@ -26,6 +26,7 @@ use crate::pyobject::{ }; use crate::stdlib; use crate::sysmodule; +use num_bigint::ToBigInt; // use objects::objects; @@ -53,7 +54,7 @@ impl VirtualMachine { // Add builtins as builtins module: let modules = sysmod.get_attr("modules").unwrap(); - ctx.set_item(&modules, "builtins", builtins.clone()); + modules.set_item(&ctx, "builtins", builtins.clone()); let stdlib_inits = stdlib::get_module_inits(); VirtualMachine { @@ -76,6 +77,11 @@ impl VirtualMachine { self.ctx.new_str(s) } + /// Create a new python int object. + pub fn new_int(&self, i: T) -> PyObjectRef { + self.ctx.new_int(i) + } + /// Create a new python bool object. pub fn new_bool(&self, b: bool) -> PyObjectRef { self.ctx.new_bool(b) @@ -395,7 +401,7 @@ impl VirtualMachine { self.ctx.set_attr(scope, &name, value); } else if let Some(d) = &kwargs { - self.ctx.set_item(d, &name, value); + d.set_item(&self.ctx, &name, value); } else { return Err( self.new_type_error(format!("Got an unexpected keyword argument '{}'", name)) diff --git a/wasm/demo/src/main.js b/wasm/demo/src/main.js index f18dac7d0..0735f8d22 100644 --- a/wasm/demo/src/main.js +++ b/wasm/demo/src/main.js @@ -12,7 +12,11 @@ const editor = CodeMirror.fromTextArea(document.getElementById('code'), { 'Cmd-Enter': runCodeFromTextarea, 'Shift-Tab': 'indentLess', 'Ctrl-/': 'toggleComment', - 'Cmd-/': 'toggleComment' + 'Cmd-/': 'toggleComment', + Tab: editor => { + var spaces = Array(editor.getOption('indentUnit') + 1).join(' '); + editor.replaceSelection(spaces); + } }, lineNumbers: true, mode: 'text/x-python', @@ -20,10 +24,10 @@ const editor = CodeMirror.fromTextArea(document.getElementById('code'), { autofocus: true }); -function runCodeFromTextarea() { - const consoleElement = document.getElementById('console'); - const errorElement = document.getElementById('error'); +const consoleElement = document.getElementById('console'); +const errorElement = document.getElementById('error'); +function runCodeFromTextarea() { // Clean the console and errors consoleElement.value = ''; errorElement.textContent = ''; @@ -51,7 +55,7 @@ document const snippets = document.getElementById('snippets'); -snippets.addEventListener('change', () => { +const updateSnippet = () => { const selected = snippets.value; // the require here creates a webpack context; it's fine to use it @@ -60,9 +64,11 @@ snippets.addEventListener('change', () => { const snippet = require(`raw-loader!../snippets/${selected}.py`); editor.setValue(snippet); - runCodeFromTextarea(); -}); +}; -// Run once for demo -runCodeFromTextarea(); +snippets.addEventListener('change', updateSnippet); + +// Run once for demo (updateSnippet b/c the browser might try to keep the same +// option selected for the `select`, but the textarea won't be updated) +updateSnippet(); diff --git a/wasm/lib/Cargo.toml b/wasm/lib/Cargo.toml index 0c7e06b53..5c4e6c86d 100644 --- a/wasm/lib/Cargo.toml +++ b/wasm/lib/Cargo.toml @@ -18,6 +18,7 @@ wasm-bindgen = "0.2" wasm-bindgen-futures = "0.3" js-sys = "0.3" futures = "0.1" +num-traits = "0.2" [dependencies.web-sys] version = "0.3" diff --git a/wasm/lib/src/browser_module.rs b/wasm/lib/src/browser_module.rs index bfc39d87c..3923a8875 100644 --- a/wasm/lib/src/browser_module.rs +++ b/wasm/lib/src/browser_module.rs @@ -1,6 +1,7 @@ use crate::{convert, vm_class::AccessibleVM, wasm_builtins::window}; use futures::Future; use js_sys::Promise; +use num_traits::cast::ToPrimitive; use rustpython_vm::obj::{objint, objstr}; use rustpython_vm::pyobject::{ PyContext, PyFuncArgs, PyObject, PyObjectPayload, PyObjectRef, PyResult, TypeProtocol, @@ -139,11 +140,12 @@ fn browser_request_animation_frame(vm: &mut VirtualMachine, args: PyFuncArgs) -> fn browser_cancel_animation_frame(vm: &mut VirtualMachine, args: PyFuncArgs) -> PyResult { arg_check!(vm, args, required = [(id, Some(vm.ctx.int_type()))]); - // questionable, but it's probably fine - let id = objint::get_value(id) - .to_string() - .parse() - .expect("bigint.to_string() to be parsable as i32"); + let id = objint::get_value(id).to_i32().ok_or_else(|| { + vm.new_exception( + vm.ctx.exceptions.value_error.clone(), + "Integer too large to convert to i32 for animationFrame id".into(), + ) + })?; window() .cancel_animation_frame(id) diff --git a/wasm/lib/src/convert.rs b/wasm/lib/src/convert.rs index 4672f7f2d..1e02d4a0d 100644 --- a/wasm/lib/src/convert.rs +++ b/wasm/lib/src/convert.rs @@ -2,7 +2,7 @@ use crate::browser_module; use crate::vm_class::{AccessibleVM, WASMVirtualMachine}; use js_sys::{Array, ArrayBuffer, Object, Promise, Reflect, Uint8Array}; use rustpython_vm::obj::{objbytes, objtype}; -use rustpython_vm::pyobject::{self, PyFuncArgs, PyObjectRef, PyResult}; +use rustpython_vm::pyobject::{self, DictProtocol, PyFuncArgs, PyObjectRef, PyResult}; use rustpython_vm::VirtualMachine; use wasm_bindgen::{closure::Closure, prelude::*, JsCast}; @@ -147,8 +147,7 @@ pub fn js_to_py(vm: &mut VirtualMachine, js_val: JsValue) -> PyObjectRef { for pair in object_entries(&Object::from(js_val)) { let (key, val) = pair.expect("iteration over object to not fail"); let py_val = js_to_py(vm, val); - vm.ctx - .set_item(&dict, &String::from(js_sys::JsString::from(key)), py_val); + dict.set_item(&vm.ctx, &String::from(js_sys::JsString::from(key)), py_val); } dict } diff --git a/wasm/lib/src/wasm_builtins.rs b/wasm/lib/src/wasm_builtins.rs index b8f4e121f..7f2b7a622 100644 --- a/wasm/lib/src/wasm_builtins.rs +++ b/wasm/lib/src/wasm_builtins.rs @@ -26,8 +26,18 @@ pub fn print_to_html(text: &str, selector: &str) -> Result<(), JsValue> { let textarea = element .dyn_ref::() .ok_or_else(|| js_sys::TypeError::new("Element must be a textarea"))?; + let value = textarea.value(); + + let scroll_height = textarea.scroll_height(); + let scrolled_to_bottom = scroll_height - textarea.scroll_top() == textarea.client_height(); + textarea.set_value(&format!("{}{}", value, text)); + + if scrolled_to_bottom { + textarea.scroll_with_x_and_y(0.0, scroll_height.into()); + } + Ok(()) }