From 897392bbbd6e58da3097cea98f8fdb91a8824ada Mon Sep 17 00:00:00 2001 From: Joshua Reusch Date: Sat, 12 Oct 2024 12:07:20 +0200 Subject: [PATCH] add fold_raw_pieces, use in align/position/tabs_to_spaces as a performance boost --- .gitignore | 1 + bench/sw-slow-mutation.mjs | 23 ++++++ bench/sw-slow-raw.mjs | 21 ++++++ bench/sw-slow.mjs | 19 +++++ bench/sw.mjs | 5 +- src/string_width.gleam | 142 ++++++++++++++++++++++++------------- 6 files changed, 161 insertions(+), 50 deletions(-) create mode 100644 bench/sw-slow-mutation.mjs create mode 100644 bench/sw-slow-raw.mjs create mode 100644 bench/sw-slow.mjs diff --git a/.gitignore b/.gitignore index b01f73b..8ffa676 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ erl_crash.dump /node_modules /build_* +*.0x/ diff --git a/bench/sw-slow-mutation.mjs b/bench/sw-slow-mutation.mjs new file mode 100644 index 0000000..e239351 --- /dev/null +++ b/bench/sw-slow-mutation.mjs @@ -0,0 +1,23 @@ +import { fold_raw } from '../build/dev/javascript/string_width/string_width.mjs' +import * as sw from '../build/dev/javascript/string_width/string_width.mjs' + +let options = sw.new$() +function string_width(str) { + const {curr, max} = fold_raw(str, options, {curr: 0, max: 0}, function(state, chr, width) { + if (chr == "\n") { + state.max = Math.max(state.curr, state.max) + state.curr = 0 + } else { + state.curr += width + } + return state + }) + + return Math.max(curr, max) +} + +let sum = 0; +for(let i = 0; i < 1000000; ++i) { + sum += string_width("\x1b[1;95m· \x1b[0m\x1b[K\x1b[33mdeno\x1b[0m\x1b[K, \x1b[33mtypescript\x1b[0m\x1b[K, \x1b[33mts\x1b[0m\x1b[K") +} +console.log(sum) diff --git a/bench/sw-slow-raw.mjs b/bench/sw-slow-raw.mjs new file mode 100644 index 0000000..d5a3bc6 --- /dev/null +++ b/bench/sw-slow-raw.mjs @@ -0,0 +1,21 @@ +import { fold_raw } from '../build/dev/javascript/string_width/string_width.mjs' +import * as sw from '../build/dev/javascript/string_width/string_width.mjs' + +let options = sw.new$() +function string_width(str) { + const {curr, max} = fold_raw(str, options, {curr: 0, max: 0}, function({curr, max}, chr, width) { + if (chr == "\n") { + return { curr: 0, max: Math.max(curr, max) } + } else { + return { curr: curr + width, max } + } + }) + + return Math.max(curr, max) +} + +let sum = 0; +for(let i = 0; i < 1000000; ++i) { + sum += string_width("\x1b[1;95m· \x1b[0m\x1b[K\x1b[33mdeno\x1b[0m\x1b[K, \x1b[33mtypescript\x1b[0m\x1b[K, \x1b[33mts\x1b[0m\x1b[K") +} +console.log(sum) diff --git a/bench/sw-slow.mjs b/bench/sw-slow.mjs new file mode 100644 index 0000000..9eede98 --- /dev/null +++ b/bench/sw-slow.mjs @@ -0,0 +1,19 @@ +import { fold } from '../build/dev/javascript/string_width/string_width.mjs' + +function string_width(str) { + const {curr, max} = fold(str, {curr: 0, max: 0}, function({curr, max}, piece) { + if (piece.piece == "\n") { + return { curr: 0, max: Math.max(curr, max) } + } else { + return { curr: curr + piece.width, max } + } + }) + + return Math.max(curr, max) +} + +let sum = 0; +for(let i = 0; i < 1000000; ++i) { + sum += string_width("\x1b[1;95m· \x1b[0m\x1b[K\x1b[33mdeno\x1b[0m\x1b[K, \x1b[33mtypescript\x1b[0m\x1b[K, \x1b[33mts\x1b[0m\x1b[K") +} +console.log(sum) diff --git a/bench/sw.mjs b/bench/sw.mjs index d66590a..24b6967 100644 --- a/bench/sw.mjs +++ b/bench/sw.mjs @@ -1,7 +1,8 @@ -import { line as string_width } from '../build/dev/javascript/string_width/string_width.mjs' +import { line_with as string_width } from '../build/dev/javascript/string_width/string_width.mjs' +import * as sw from '../build/dev/javascript/string_width/string_width.mjs' let sum = 0; -let options = {ambiguous_width: 1} +let options = sw.new$() for(let i = 0; i < 1000000; ++i) { sum += string_width("\x1b[1;95m· \x1b[0m\x1b[K\x1b[33mdeno\x1b[0m\x1b[K, \x1b[33mtypescript\x1b[0m\x1b[K, \x1b[33mts\x1b[0m\x1b[K", options) } diff --git a/src/string_width.gleam b/src/string_width.gleam index e105424..2db3b8c 100644 --- a/src/string_width.gleam +++ b/src/string_width.gleam @@ -15,7 +15,7 @@ //// [at_tab_offset](#at_tab_offset), [with_tab_width](#with_tab_width) //// //// #### Advanced -//// [fold](#fold), [fold_with](#fold_with), [fold_raw](#fold_raw), +//// [fold](#fold), [fold_with](#fold_with), [fold_raw](#fold_raw), [fold_raw_pieces](#fold_raw_pieces) //// [is_ansi_component](#is_ansi_component) //// @@ -36,6 +36,8 @@ import string_width/internal/tables // Mode 2027: https://github.com/contour-terminal/terminal-unicode-core // https://gitlab.freedesktop.org/terminal-wg/specifications/-/issues/36 +const max_safe_integer = 0x1fffffffffffff + // -- OPTIONS ------------------------------------------------------------------ /// Options to change the default behaviour of the functions in this library. @@ -177,7 +179,7 @@ pub type Size { /// // --> 5 /// ``` pub fn line(str: String) -> Int { - let Size(columns:, rows: _) = dimensions_with(str, default_options) + let #(_, columns, _) = do_measure(str, default_options) columns } @@ -621,7 +623,7 @@ pub fn tabs_to_spaces(str: String) -> String { /// // --> "hi cutie~" /// ``` pub fn tabs_to_spaces_with(str: String, using options: Options) -> String { - use acc, piece <- fold_with(str, options, from: "") + use acc, piece <- fold_raw_pieces(str, options, from: "") case piece.piece { "\t" -> acc <> string.repeat(" ", piece.width) _ -> acc <> piece.piece @@ -647,7 +649,7 @@ pub type Placement { /// /// **Tip:** If you only want to align a string on one axis, you can provide /// `0` for the orthogonal one! Extra characters and lines will be kept. -/// +/// /// If the space strings' width does not evenly divide the missing amount of /// columns, the extra spacer will overflow the max width. /// @@ -678,46 +680,45 @@ pub fn position_with( ) -> String { let #(str_rows, str_cols_max, str_cols_min) = do_measure(str, options) let space_width = line_with(space, options) - let size = dimensions_with(str, options) // horizontal align - we can skip this if every line is at least columns wide let str = case bounding_box.columns > str_cols_min { True -> { - let align = case alignment { - Left -> fn(line, line_width) { - let missing = div_up(bounding_box.columns - line_width, space_width) - line <> string.repeat(space, missing) - } + let align = case alignment { + Left -> fn(line, line_width) { + let missing = div_up(bounding_box.columns - line_width, space_width) + line <> string.repeat(space, missing) + } - Right -> { - let missing_left = + Right -> { + let missing_left = div_up(bounding_box.columns - str_cols_max, space_width) - let space_left = string.repeat(space, missing_left) + let space_left = string.repeat(space, missing_left) - fn(line, line_width) { - let missing = - { bounding_box.columns - line_width } / space_width - missing_left + fn(line, line_width) { + let missing = + { bounding_box.columns - line_width } / space_width - missing_left - space_left <> line <> string.repeat(space, missing) - } - } + space_left <> line <> string.repeat(space, missing) + } + } - Center -> { - let missing_total = + Center -> { + let missing_total = div_up(bounding_box.columns - str_cols_max, space_width) - let missing_left = missing_total / 2 - let space_left = string.repeat(space, missing_left) - let space_right = string.repeat(space, missing_total - missing_left) + let missing_left = missing_total / 2 + let space_left = string.repeat(space, missing_left) + let space_right = string.repeat(space, missing_total - missing_left) - fn(line, line_width) { - let missing = + fn(line, line_width) { + let missing = { bounding_box.columns - line_width } / space_width - missing_total - space_left <> line <> string.repeat(space, missing) <> space_right - } - } + space_left <> line <> string.repeat(space, missing) <> space_right + } + } } do_align(str, options, align) } @@ -765,7 +766,7 @@ pub fn position_with( /// /// align(" Welcome ", to: 20, align: Center, with: "==") /// // --> "====== Welcome ======" // (len = 21) -/// +/// /// align("Trans\nrights\nare\nhuman\nrights", to: 7, align: Right, with: " ") /// // --> " Trans\n rights\n are\n human\n rights" /// ``` @@ -817,11 +818,11 @@ fn do_align(str, options, align: fn(String, Int) -> String) { let state = #("", "", 0) let state = { - use state, piece <- fold_with(str, options, state) + use state, piece <- fold_raw_pieces(str, options, state) let #(acc, line, line_width) = state case piece.piece { - "\n" | "\n\r" -> push_line(state) + "\n" -> push_line(state) "\t" -> #( acc, line <> string.repeat(" ", piece.width), @@ -886,20 +887,7 @@ pub fn fold_with( from state: state, with fun: fn(state, Piece) -> state, ) -> state { - let fun = fn(state, piece, width) { - let #(state, row, column) = state - let width = case piece { - "\t" -> tab(options, column) - column - _ -> width - } - - let state = fun(state, Piece(piece:, row:, column:, width:)) - - case piece { - "\n" -> #(state, row + 1, options.tab_offset) - _ -> #(state, row, column + width) - } - } + let #(state, fun) = piece_reducer(state, options, fun) let on_chars = case options.mode { ModeWcwidth -> fn(state, str) { @@ -918,9 +906,8 @@ pub fn fold_with( let on_range = fn(state, ansi, _) { fun(state, ansi, 0) } - let initial = #(state, 0, options.tab_offset) let #(state, _, _) = - fold_parts(str, 0, ansi_ranges, initial, on_chars, on_range) + fold_parts(str, 0, ansi_ranges, state, on_chars, on_range) state } @@ -981,6 +968,65 @@ pub fn fold_raw( fold_parts(string, 0, ansi_ranges, state, on_chars, on_range) } +/// A version of `fold_raw` that does keep track of some state for you to +/// handle tabs, and count the current row/column, passing `Piece` values to +/// you instead. +pub fn fold_raw_pieces( + over string: String, + using options: Options, + from state: state, + with fun: fn(state, Piece) -> state, +) -> state { + let ansi_ranges = case options.count_ansi_codes { + True -> [] + False -> ansi.match(string) + } + + let #(state, fun) = piece_reducer(state, options, fun) + + let on_chars = case options.mode { + ModeWcwidth -> fn(state, str) { fold_chars_raw(options, str, state, fun) } + Mode2027 -> fn(state, str) { fold_chars_2027(options, str, state, fun) } + Mode2027Ext -> fn(state, str) { + fold_chars_2027_ext(options, str, state, fun) + } + } + + let on_range = fn(state, ansi, _) { fun(state, ansi, 0) } + + let #(state, _, _) = + fold_parts(string, 0, ansi_ranges, state, on_chars, on_range) + + state +} + +fn piece_reducer( + state: state, + options: Options, + fun: fn(state, Piece) -> state, +) -> #( + #(state, Int, Int), + fn(#(state, Int, Int), String, Int) -> #(state, Int, Int), +) { + let initial = #(state, 0, options.tab_offset) + let fun = fn(state, piece, width) { + let #(state, row, column) = state + let width = case piece { + "\t" -> tab(options, column) - column + _ -> width + } + + let state = fun(state, Piece(piece:, row:, column:, width:)) + + case piece { + "\n" -> #(state, row + 1, options.tab_offset) + _ -> #(state, row, column + width) + } + } + + #(initial, fun) +} + fn fold_parts( over string: String, at offset: Int, -- knotmirror 0.0.1