When a row and its own descendant are both selected


#1

Many scripts act on selected rows.

A difficulty can arise, in the context of an outline, if the selection includes rows that are descendants of other selected rows – a script may need to derive a subset of the selected rows, acting only on the top level selections, and ignoring any selected descendants of them.

Sketches here of one approach – using an intersect(xs, ys) function to skip any selected rows which have selected ancestors – in AppleScript and then in JavaScript for Automation.

(We can take advantage of the fact that a selected a row is always listed before any selected descendants that it may have – the selected rows property returns rows in document order rather than selection order. )

AppleScript

-- SELECTED ROWS (Excluding descendents of other selected rows) ----------

-- topLevelSelections :: OO Document -> [OO Row]
on topLevelSelections(oDoc)
    using terms from application "OmniOutliner"
        tell oDoc
            script noAncestorSelected
                on |λ|(acc, x)
                    if intersect(id of ancestors of x, acc) ≠ {} then
                        acc -- unchanged accumulator (skips this x)
                    else
                        acc & id of x -- expanded accumulator
                    end if
                end |λ|
            end script
            
            script rowByID
                on |λ|(x)
                    row id x of oDoc
                end |λ|
            end script
            
            my map(rowByID, my foldl(noAncestorSelected, {}, selected rows))
        end tell
    end using terms from
end topLevelSelections


-- TEST ----------------------------------------------------------------------
-- List only topics of:  selected rows which have no selected ancestors
on run
    tell application "OmniOutliner"
        
        script rowTopic
            on |λ|(x)
                topic of x
            end |λ|
        end script
        
        my map(rowTopic, my topLevelSelections(front document))
    end tell
end run


-- GENERIC FUNCTIONS ---------------------------------------------------------

-- filter :: (a -> Bool) -> [a] -> [a]
on filter(f, xs)
    tell mReturn(f)
        set lst to {}
        set lng to length of xs
        repeat with i from 1 to lng
            set v to item i of xs
            if |λ|(v, i, xs) then set end of lst to v
        end repeat
        return lst
    end tell
end filter

-- foldl :: (a -> b -> a) -> a -> [b] -> a
on foldl(f, startValue, xs)
    tell mReturn(f)
        set v to startValue
        set lng to length of xs
        repeat with i from 1 to lng
            set v to |λ|(v, item i of xs, i, xs)
        end repeat
        return v
    end tell
end foldl

-- intersect :: (Eq a) => [a] -> [a] -> [a]
on intersect(xs, ys)
    script found
        on |λ|(x)
            ys contains x
        end |λ|
    end script
    filter(found, xs)
end intersect

-- map :: (a -> b) -> [a] -> [b]
on map(f, xs)
    tell mReturn(f)
        set lng to length of xs
        set lst to {}
        repeat with i from 1 to lng
            set end of lst to |λ|(item i of xs, i, xs)
        end repeat
        return lst
    end tell
end map

-- Lift 2nd class handler function into 1st class script wrapper 
-- mReturn :: Handler -> Script
on mReturn(f)
    if class of f is script then
        f
    else
        script
            property |λ| : f
        end script
    end if
end mReturn

JavaScript for Automation (JXA)

(() => {
    'use strict';

    // IGNORING SELECTED DESCENDANTS OF OTHER SELECTED ROWS --------------

        // topLevelSelections :: OO Document -> [OO Row]
        const topLevelSelections = ooDoc => map(
            strID => ooDoc.rows.byId(strID),
            foldl(
                (acc, row) => isNull(intersect(row.ancestors.id(), acc)) ? (
                    cons(row.id(), acc)
                ) : acc, [],
                ooDoc.selectedRows()
            )
        );

        // GENERIC FUNCTIONS -------------------------------------------------

        // cons :: a -> [a] -> [a]
        const cons = (x, xs) => [x].concat(xs);

        //  intersect :: (Eq a) => [a] -> [a] -> [a]
        const intersect = (xs, ys) =>
            xs.length && ys.length ? (
                xs.filter(x => ys.indexOf(x) !== -1)
            ) : [];

        // isNull :: [a] | String -> Bool
        const isNull = xs =>
            Array.isArray(xs) || typeof xs === 'string' ? (
                xs.length < 1
            ) : undefined;

        // map :: (a -> b) -> [a] -> [b]
        const map = (f, xs) => xs.map(f);

        // foldl :: (b -> a -> b) -> b -> [a] -> b
        const foldl = (f, a, xs) => xs.reduce(f, a);

        // unlines :: [String] -> String
        const unlines = xs => xs.join('\n');

        // TEST --------------------------------------------------------------
        // List only topics of:  selected rows which have no selected ancestors

        const
            docs = Application('OmniOutliner')
            .documents,
            maybeDoc = docs.length > 0 ? {
                just: docs.at(0)
            } : {
                nothing: true
            };

        return maybeDoc.nothing ? '' : unlines(
            map(x => x.topic(), topLevelSelections(maybeDoc.just))
        );
    })();