How Go To the Nth Line in Script Editor?

I rarely execute scripts in Script Editor that are more than 10 or 20 lines. However, since Script Debugger doesn’t support executing JXA scripts, I don’t have any choice when working with those. I am flummoxed when I run an error message that refers to a line number, since Script Editor doesn’t seem to be able to display those or go to the Nth line. (Pitiful.)

What are some strategies people use to locate the Nth line? All I can think of is dump the whole script into a more capable editor (Atom or Emacs, e.g.) and go to line N there, either by viewing line numbers or by a command that goes to the Nth line (or go to top and then N lines down). This gets tedious. Any better ideas?

Well, the obvious answer is a question: Why the heck are you using Script Editor and not Atom? :slight_smile: Although with that said, to my knowledge, you can’t click on JXA error line numbers in Atom and go directly to that line either, but at least there’s ^G to go directly to a line number.

But to answer your question, a macro could “type” the keystroke for “go to start of document”, followed by the right number of down-arrows. This would work, right? Not elegant, but it should still work.

Speaking of which:

I haven't tested this (as I really don't need it), but I wonder if the reported error line includes comment lines? If it does not, then this approach wouldn't work well.

OK, I got curious, and ran a test.
Script Editor 2.8.1 (183.1) on macOS 10.11.4

Here is what I found for the Script Editor using JavaScript:

  1. The reported error number DOES include comment lines
  2. The error line is highlighted

Since the error line is automatically selected/highlighted, I don't see the need for a "go to line number" action.

I'm pretty sure you could make such a thing :slight_smile: Shouldn't be too too hard either. The hardest part is knowing what line it is (from the error message given) :stuck_out_tongue: Sometimes there can be multiple errors throught a script for instance. Could perhaps parse the error log with regex though :slight_smile: If you haven't already you should really mess around a bit in the Atom developer tools. You will soon figure your way around the code :slight_smile: It really is fairly straight forward :smiley:

I'm sure you're correct that I am capable of making it. I will not, however, be doing it in the near future. :slight_smile:

1 Like

Hey Mitchell,

Considering the Script Editor is over 20 years old now it's astonishing just how brain-dead it is.

A programming editor that doesn't even have a Go-To Line function... :skull:

Nevertheless – you're a programmer and scripter aren't you? So have a close look at the app's dictionary, before you get too flummoxed.

-------------------------------------------------------------------------
# Auth: Christopher Stone
# dCre: 2017/01/28 05:00
# dMod: 2017/01/28 05:16
# Appl: Script Editor
# Task: Go-To the Specifed Line
# Libs: None
# Osax: None
# Tags: @Applescript, @Script, @Script_Editor, @Go-To, @Specifed, @Line
-------------------------------------------------------------------------

tell application "Script Editor"
   
   set goToLine to text returned of (display dialog "Insert a Line Number:" default answer "" with title "Go-To Line" giving up after 30)
   
   tell front document
      set docSrc to contents
      set theLines to paragraphs of docSrc
      if length of theLines ≥ goToLine then
         set theLines to items 1 thru goToLine of theLines
         set theoffset to count of characters of item goToLine of theLines
         set AppleScript's text item delimiters to linefeed
         set theLines to theLines as text
         set charCount to count of characters of theLines
         set selection to characters (charCount - theoffset + 1) thru charCount
      else
         error "The specified line is outside the scope of the text!"
      end if
   end tell
   
end tell

-------------------------------------------------------------------------

** I'm not doing error-checking for empty lines and other pitfalls – I leave that as an exercise for the reader.

Now then – the above is not how I would tackle this problem on my system – I'd use the Satimage.osax.

-------------------------------------------------------------------------
# Auth: Christopher Stone
# dCre: 2017/01/28 05:00
# dMod: 2017/01/28 07:12
# Appl: Script Editor
# Task: Go-To the Specifed Line Using the Satimage.osax
# Libs: None
# Osax: Satimage.osax { http://tinyurl.com/dc3soh }
# Tags: @Applescript, @Script, @Script_Editor, @Go-To, @Specifed, @Line, @Satimage.osax
-------------------------------------------------------------------------

tell application "Script Editor"
   set goToLine to 10
   
   tell front document
      set docSrc to contents
      set matchRecList to fnd("^.+", docSrc, true, false) of me
      if length of matchRecList ≥ goToLine then
         set {match_Pos, match_Len, match_Result} to {matchPos, matchLen, matchResult} of (item goToLine of matchRecList)
         set selection to characters (match_Pos + 1) thru (match_Pos + 1 + match_Len)
      end if
   end tell
   
end tell

-------------------------------------------------------------------------
--» HANDLERS
-------------------------------------------------------------------------
on fnd(_find, _data, _all, strRslt)
   try
      find text _find in _data all occurrences _all string result strRslt with regexp without case sensitive
   on error
      return false
   end try
end fnd
-------------------------------------------------------------------------

** Again – I'm not doing the error-checking I'd do for a production script – this is left to the reader.

I use techniques similar to this to work with text in Script Debugger as well. I do such things as Select Current Line and Duplicate Selection – which oddly enough are features SD does NOT have.

NOTE: Setting the selection in the Script Editor does NOT move the view of the editing pane to the location of the selection. If you want to do that then you need to add the keystroke for “Jump to Selection” (J) to your macro.

** Script Editor comes with quite a few scripts you can use as examples.

So. This stuff is not exactly fun – but it's fairly easily done.

-Chris

1 Like

Here is an old (Jan 2015) set of generic functions for Script Editor (modelled on the functions used by iOS Drafts).

Haven’t really used them since I switched to Atom, so you may well need to lint/fix some edge cases:

// Ver 0.4 Rob Trew 
// Header for using OSX Yosemite Script Editor scripts
// written in an idiom compatible with FoldingText, Drafts, 1Writer, TextWell
// In iOS Drafts 4 scripts, the header is simply:
//    var drafts = this;
// For headers for FoldingText and other editors, see:
// See: http://support.foldingtext.com/t/writing-scripts-which-run-on-both-foldingtext-and-ios-drafts-4/652/7

// Call functions, after this header, with the prefix:
//        drafts.
// e.g.
//        var drafts = Library('DraftsSE');
//        var dctSelnRange = drafts.getSelectedRange(),
//            iFrom = dctSelnRange[0],
//            iTo = iFrom + dctSelnRange[1];

// These functions are intended to be compatible with Agile Tortoise's Drafts documentation at:
// https://agiletortoise.zendesk.com/hc/en-us/articles/202465564-Script-Keys

// getText()                               getSelectedLineRange()
// setText(string)                         getSelectedRange()
// getSelectedText()                       setSelectedRange(start, length)
// setSelectedText(string)                 getClipboard()
// getTextInRange(start, length)           setClipboard(string)
// setTextInRange(start, length, string)

// Installation: Save this as a compiled .scpt to ~/Library/Script Libraries/DraftsSE.scpt
// and then invoke in other scripts as above, through the JXA Library object.

var appSE = Application("Script Editor"),
    lstDocs = appSE.documents(),
    oDoc = lstDocs.length ? lstDocs[0] : null;

appSE.includeStandardAdditions = true;
appSE.activate();

function getText() {
    if (oDoc) return oDoc.text();
}

function setText(strText) {
    if (oDoc) oDoc.text = strText;
}

function getSelectedText() {
    if (oDoc) return oDoc.selection.contents();
}

function setSelectedText(strText) {
    if (oDoc) oDoc.selection.contents = strText;
}

function getTextInRange(iStart, iLength) {
    if (oDoc) return oDoc.text().substring(iStart, iStart + iLength);
}

function setTextInRange(iStart, iLength, strText) {
    if (oDoc) { // record the existing selection coordinates
        var lngDelta = (strText.length - iLength) - 2,
            oSeln = oDoc.selection, dct = oSeln.characterRange(),
            iFrom = dct.x, iTo = dct.y;

        try { // use the selection to edit elsewhere, then restore with any adjustment
            oDoc.selection = oDoc.text.characters.slice(iStart, iStart + iLength);
            oSeln.contents = strText;
            oDoc.selection = oDoc.text.characters.slice(
                (iStart < iFrom) ? iFrom + lngDelta : iFrom,
                ((iStart + iLength) < iTo) ? iTo + lngDelta : iTo
            );
        } catch (e) {}
    }
}

function getSelectedLineRange() {
    // works, but let me know if you see a shorter route :-)
    var dct, strFull, iFrom, iTo, lngChars;
    if (oDoc) {
        dct = oDoc.selection.characterRange();
        strFull = oDoc.text();
        lngChars = strFull.length;
        iFrom = dct.x;
        while (iFrom--)
            if (strFull[iFrom] === '\n') break;
        iFrom += 1;
        iTo = dct.y - 1;
        while (iTo++ < lngChars)
            if (strFull[iTo] === '\n') break;
        return [iFrom, (iTo - iFrom)];
    }
}

function getSelectedRange() {
    var dct, iFrom, iTo;
    if (oDoc) {
        dct = oDoc.selection.characterRange();
    }
    iFrom = dct.x;
    iTo = dct.y;
    return [iFrom, iTo - iFrom];
}

function setSelectedRange(iStart, iLength) {
    if (oDoc) {
        oDoc.selection = oDoc.text.characters.slice(iStart, iStart + iLength);
    }
}

function getClipboard() {
    return appSE.theClipboard();
}

function setClipboard(strText) {
    appSE.setTheClipboardTo(strText);
}

It is the obvious answer, and yes it would work, but try going to line 180, or whatever. Awful. But yes, it works.

I am using Atom, but as you know running from Atom can't get you into the debugger, so I am doing the dance you addressed in MACRO(s): Debug JXA from Atom, Finish Debugging JXA from Atom - macro - Keyboard Maestro Discourse, though I am doing it a little differently.

1 Like

Agree on both, and your code is great. I was just looking for strategies, though, not solutions. Thought maybe someone had something relatively simple for this.

Good stuff, and I'll probably use some of it, but I don't see a way to go to a line number using it — nothing to find the range of line N.

Are you talking about what is in /Library/Scripts/Script Editor Scripts? Because all I see there are snippets.

Hmmmm. I can't remember the situation in which I needed to manually go to a line number. I may have started this discussion for naught. Sorry. I'll come back if I run across the problem again. Thanks all.

My recollection is that the API is line-ending agnostic, and so models the buffer as a stream of indexed character positions rather than lines.

Once you have decided/detected what the line delimiters are, you probably just need a pair of functions (including String -> Array splits) which translate between character positions and line numbers.

e.g. this kind of thing ?

( Tho life is more rewarding in Atom :slight_smile: )

(function () {
    'use strict';

    var drafts = Library('DraftsSE');

    // selectNthLine :: Int -> IO ()
    function selectNthLine(n) {
        var lines = drafts.getText()
            .split('\n'),
            lngLines = lines.length;

        if (n <= lngLines) {
            drafts.setSelectedRange(n > 0 ? lines.slice(0, n)
                .reduce(function (a, x) {
                    return a + x.length;
                }, n) : 0, lines[n].length);
        }
    }

    selectNthLine(8);
})();
1 Like

Caught one. I just copied an entire script from Atom to a new window in Script Editor. It had an invalid character somewhere in the middle. It gave me this dialog:
and there was no message at the bottom of the page with a line number I could click. So that's why I needed it.