Word count for selected text

Is there a way to get Bike to give me the word count only for selected text or branches?

1 Like

There isn’t right now. It makes sense as a feature that I should add, just hasn’t made it to top of priority list yet.

There is this script:

Which could be modified to get the counts you want until I build the feature in.

In the simplest case of directly selected text, it should suffice, I think, to write:

(() => {
    "use strict";

    const doc = Application("Bike").documents.at(0);

    return doc.exists() ? (
        doc.selectedText().split(/\s+/gu).length
    ) : "No document open in Bike.";
})();

but could you expand a little on selected branches ?

When a collapsed row is selected, are you looking for:

  • a count only of the directly selected words in that row ?
  • a count of words in that row and all its descendants ?
  • something slightly different ?

If a selected row is not collapsed but has descendants which are, do you want:

  • A full count of the words in that row and all its descendants ?
  • A count of the words in the row and all visible descendants ?
  • Something else ?

We could, for example write two scripts:

  • Count of words in selected rows and all their descendants.
  • Count of words in selected rows and visible descendants.

My use case is composing text from a starting outline. Typically, i’m trying to get my text below some limit, so i would prefer the word count to err on the side of including too much rather than including too little (because i hid something). Thus, i’d like to have the word count of selected rows and all their descendants (including hidden ones).

Well, here’s a couple of drafts to experiment with.

Imagine we have selected Nu and Rho below

Screenshot 2023-04-23 at 12.05.57

both of the branch word count versions below should return a word count of 7 (selected rows and descendants thereof)

but if we now collapse Nu

Screenshot 2023-04-23 at 12.09.17

then

  • the first script should still return 7
  • while the second script (ignoring hidden lines) should now return 4
Expand disclosure triangle to view JS source (Branch Word Count)
(() => {
    "use strict";

    // Rob Trew @2023

    // Count of words in selected Bike branches,
    // including rows hidden by folding.

    // Ver 0.1

    const doc = Application("Bike").documents.at(0);

    return doc.exists() ? (() => {
        const
            rows = doc.rows,
            selnParents = rows.where({
                containsRows: true,
                selected: true
            });

        // Ids of selected rows and descendants,
        return Array.from(
            new Set([
                ...selnParents.id(),
                ...selnParents.entireContents().flatMap(
                    xs => xs.map(x => x.id())
                ),
                ...rows.where({
                    containsRows: false,
                    selected: true
                }).id()
            ])
        )
        // with texts joined together and counted.
        .map(x => rows.byId(x).name())
        .join(" ")
        .split(/\s+/gu)
        .length;
    })() : "No document open in Bike.";
})();
Expand disclosure triangle to view variant skipping hidden descendants
(() => {
    "use strict";

    // Rob Trew @2023

    // Count of words in selected Bike branches,
    // ignoring rows hidden by folding.

    // Ver 0.1

    const doc = Application("Bike").documents.at(0);

    return doc.exists() ? (() => {
        const
            rows = doc.rows,
            selnParents = rows.where({
                containsRows: true,
                selected: true
            });

        // Ids of selected rows and descendants,
        return Array.from(
            new Set([
                ...selnParents.id(),
                ...selnParents.entireContents().flatMap(
                    xs => xs.map(x => x.id())
                ),
                ...rows.where({
                    containsRows: false,
                    selected: true
                }).id()
            ])
        )
        // except any invisible rows,
        .flatMap(x => {
            const row = rows.byId(x);

            return row.visible()
                ? [row.name()]
                : [];
        })
        // with texts joined together and counted.
        .join(" ")
        .split(/\s+/gu)
        .length;
    })() : "No document open in Bike.";
})();

To test in Script Editor, set language selector at top left to JavaScript.

See: Using Scripts - Bike


Just drafts – it’s possible that there are more efficient ways of writing them which haven’t occurred to me this morning :slight_smile:

that’s pretty cool. thank you.

in fact, being able to word-count folded text means that i only have to have my cursor in a parent, instead of having to select text. very convenient.

I’m a newbie with applescript, so I’m running it within apple script editor. (I added it to the Apple script menu, but i can’t figure out how i would see the output word count).

I tend to use Keyboard Maestro or FastScripts for the display and key-binding side, but here’s a combined version which aims to:

  • display both counts in a dialog
  • and also copy the counts to the clipboard.
Expand disclosure triangle to view JS source
(() => {
    "use strict";

    // Rob Trew @2023

    // Count of words in selected Bike branches,
    // both with and without rows hidden by folding.

    // Report displayed and copied to clipboard.

    // Ver 0.3

    // eslint-disable-next-line max-lines-per-function
    const main = () => {
        const doc = Application("Bike").documents.at(0);

        return doc.exists() ? (() => {
            const
                rows = doc.rows,
                selnParents = rows.where({
                    containsRows: true,
                    selected: true
                }),

                // Selected rows and descendants,
                // visible and hidden.
                [nVisible, nHidden] = partition(
                    row => row.visible()
                )(
                    Array.from(
                        new Set([
                            ...selnParents.id(),
                            ...selnParents.entireContents()
                            .flatMap(
                                xs => xs.map(x => x.id())
                            ),
                            ...rows.where({
                                containsRows: false,
                                selected: true
                            }).id()
                        ])
                    )
                    .map(x => rows.byId(x))
                )
                .map(
                    xs => xs.map(row => row.name())
                    .join(" ")
                    .split(/\s+/ug)
                    .length
                ),
                nAll = nVisible + nHidden,
                report = [
                    `Words in selected branches: ${nAll}`,
                    `Excluding hidden rows: ${nVisible}`
                ]
                .join("\n");

            const
                sa = Object.assign(
                    Application.currentApplication(),
                    {includeStandardAdditions: true}
                );

            return (
                sa.setTheClipboardTo(report),
                sa.activate(),
                sa.displayDialog(report, {
                    withTitle: "Words in selected Bike branches",
                    buttons: ["OK"],
                    defaultButton: "OK"
                }),
                report
            );
        })() : "No document open in Bike.";
    };

    // --------------------- GENERIC ---------------------

    // first :: (a -> b) -> ((a, c) -> (b, c))
    const first = f =>
    // A simple function lifted to one which applies
    // to a tuple, transforming only its first item.
        xy => [f(xy[0]), xy[1]];


    // partition :: (a -> Bool) -> [a] -> ([a], [a])
    const partition = p =>
        // A tuple of two lists - those elements in
        // xs which match p, and those which do not.
        xs => [...xs].reduce(
            (a, x) => (
                p(x) ? (
                    first
                ) : second
            )(ys => [...ys, x])(a),
            [[], []]
        );

    // second :: (a -> b) -> ((c, a) -> (c, b))
    const second = f =>
    // A function over a simple value lifted
    // to a function over a tuple.
    // f (a, b) -> (a, f(b))
        xy => [xy[0], f(xy[1])];

    // MAIN
    return main();
})();

works great! thanks very much.

After a bit of use, i ended up commenting out the line copying the results to clipboard:

// sa.setTheClipboardTo(report)

2 Likes