Place the cursor at the end of the first task that is not @done

Hello all,

I am looking to create a JavaScript that will place the cursor at the end of the first task that is not @done

I don’t want to focus in—just move the cursor.

Any ideas are appreciated.

Thanks!

Jim

You may find some bits of it in this draft:

Expand disclosure triangle to view JS Source
(() => {
    "use strict";

    // Rob Trew @2021

    // ------------------- JXA CONTEXT -------------------

    // jxaMain :: IO ()
    const jxaMain = () => {
        const docs = Application("TaskPaper").documents;

        return 0 < docs.length ? (
            docs.at(0).evaluate({
                script: `${TaskPaperContext}`,
                withOptions: {
                    itemPath: "//not @done"
                }
            })
        ) : "No documents open in TaskPaper";
    };

    // ---------------- TASKPAPER CONTEXT ----------------

    // TaskPaperContext :: Editor -> Dict -> IO ()
    const TaskPaperContext = (editor, options) => {

        const cursorToEndOfItem = item => (
            editor.moveSelectionToItems(
                item, item.bodyString.length
            ),
            `Cursor at ${editor.selection.location}`
        );

        const
            outline = editor.outline,
            matches = outline.evaluateItemPath(
                options.itemPath
            )
            // Perhaps ignoring blank lines ?
            .filter(
                item => Boolean(item.bodyString.trim())
            );

        return 0 < matches.length ? (
            cursorToEndOfItem(matches[0]),
            `Cursor at ${editor.selection.location}`
        ) : `No matches for '${options.itemPath}'`;
    };

    return jxaMain();
})();
1 Like

Excellent @complexpoint — In early tests, I cannot find any issues in your draft. Thank you!

If you do want to exclude blank lines, another approach (as an alternative to the .filter above), might be to specify non-blank lines in the itemPath.

@jessegrosjean might be able to think of a more elegant expression, but this at least seems to work:

//not @done and @text matches "."

(i.e. a regular expression that requires at least one character in the line)

1 Like

I presume that the quotes around the period need to be escaped, right?

Example:
itemPath: "//not @done and @text matches \"@\""

This is really good @complexpoint !

Experimenting with:

itemPath: "//not @done and @text matches today"
and
itemPath: "//not @done and @text matches next"

That’s right – quoting allows, for example, for the inclusion of white space in a regex.

JS gives you a couple of options there for the outermost quotes. You could do without escaped inner quote by writing, perhaps:

'//not @done and @text matches "."'

(footnote:: JSON needs double-quoted strings, but within full JS code the notation for a key or string value in a Dictionary/Object, or just a plain string, can use single quotes (or for template notation, back-ticks)

1 Like

Note that:

  • The matches keyword is only really needed if the following string contains some regex syntax
  • simple matches are assumed to be against the @text value anyway

So if you wanted, I think you could pare the itemPath pattern down to things like:

//not @done and today
1 Like

The above has served me well for years—thank you again @complexpoint !

I have now found a desire to move the cursor to the end of the first task that is not @done and is in the project that the cursor is currently in.

In this example, the cursor is somewhere in Project B (or in one of its children). After the script is run, the cursor would be at the end of Task 3b

Project A:
Task 1a
Project B:
Task 1b @done
Task 2b @done
Task 3b

If this desire easy to implement in the above script? Or would this be a major rewrite?

Here’s a rough edit.

(messages could be improved – for example when the cursor is not enclosed by any project, or the enclosing project’s tasks are all marked @done)

Expand disclosure triangle to view JS source
(() => {
    "use strict";

    // Rob Trew @2021, 2024

    // First not @done selected in project containing cursor

    // ------------------- JXA CONTEXT -------------------

    // main :: IO ()
    const main = () => {
        const doc = Application("TaskPaper").documents.at(0);

        return doc.exists()
            ? doc.evaluate({
                script: `${TaskPaperContext}`
            })
            : "No documents open in TaskPaper";
    };

    // ---------------- TASKPAPER CONTEXT ----------------

    // TaskPaperContext :: Editor -> IO ()
    const TaskPaperContext = editor => {

        const cursorToEndOfItem = item => (
            editor.moveSelectionToItems(
                item, item.bodyString.length
            ),
            `Cursor at ${editor.selection.location}`
        );

        const
            cursorID = editor.selection.startItem.id,
            itemPath = (
                `//@id=${cursorID}/ancestor-or-self::project//task not @done[0]`
            ),
            matches = editor.outline.evaluateItemPath(itemPath);

        return 0 < matches.length
            ? (
                cursorToEndOfItem(matches[0]),
                `Cursor at ${editor.selection.location}`
            )
            : `No matches for '${itemPath}'`;
    };

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

1 Like

Solid, with one desire: if the cursor is in the project line, I’d still want it to move it to the first not @done.

Example (the cursor is the vertical line between the e and c):

Proje|ct B:
    Task 1b @done
    Task 2b @done
    Task 3b

When would the cursor not be in any project? A note? A blank line?

You could experiment with affixing -or-self to the ancestor token in the path.

In the copy above, I’ve now edited the path template to:

`//@id=${cursorID}/ancestor-or-self::project//task not @done[0]`

When it’s in any untabbed (full left) notes or bullet items.

(Perhaps document notes before the first project, or between others, for example)

1 Like

For slightly fuller messages (perhaps for a Keyboard Maestro notification) you could:

  1. First identify any enclosing project, and then
  2. (if a project is found) use a slightly simpler (project-anchored) path.

Perhaps, for a first draft:

//@id=${projectID}//@type=task and not @done[0]

Expand disclosure triangle to view JS source
(() => {
    "use strict";

    // Rob Trew @2021, 2024

    // First not @done selected in project containing cursor

    // Ver 2.03

    // ------------------- JXA CONTEXT -------------------

    // main :: IO ()
    const main = () => {
        const doc = Application("TaskPaper").documents.at(0);

        return doc.exists()
            ? doc.evaluate({
                script: `${TaskPaperContext}`
            })
            : "No documents open in TaskPaper";
    };

    // ---------------- TASKPAPER CONTEXT ----------------

    // TaskPaperContext :: Editor -> IO ()
    const TaskPaperContext = editor => {

        const cursorToEndOfItem = item => (
            editor.moveSelectionToItems(
                item, item.bodyString.length
            ),
            `Cursor at ${editor.selection.location}`
        );

        const
            startItem = editor.selection.startItem,

            project = [...startItem.ancestors, startItem].findLast(
                x => "project" === x.getAttribute("data-type")
            ) || {},
            
            projectID = project.id;

        return undefined !== projectID
            ? (() => {
                const
                    matches = editor.outline.evaluateItemPath(
                        `//@id=${projectID}//@type=task and not @done[0]`
                    );

                return 0 < matches.length
                    ? (
                        cursorToEndOfItem(matches[0]),
                        `${matches[0].bodyContentString}`
                    )
                    : `No remaining tasks in "${project.bodyContentString}".`
            })()
            : `Not in a project :: "${startItem.bodyContentString}".`;
    };

    // MAIN ---
    return main();
})();
1 Like

PS it should be possible, I think, to replace longer paths which search on @id values with shorter paths applied to a contextItem, but I seem to be missing the trick of that, and getting the wrong match.

1 Like

I like!

It returns an extra backslash and quote:

"Vitamins\""

Changing the scope, as you have a habit of further opening my eyes… :slight_smile:

If there are no remaining undone tasks in the current project, or the cursor is not in a project, can the script fall back to positioning the cursor at the end of the first task that is not done (like in the original script?).

Amended above

1 Like

Not this one :slight_smile:

2 Likes

Fair. :slight_smile:

Thank you yet again for your help!

2 Likes

I’ll take a look at the weekend

1 Like