Hi @dublove, here’s my idea: we use an `afterSelectionChanged` event listener to reset the mode. Try this script:
/**
* @file Reset Or Increment Leading.js
*
* First run on a text selection: resets the leading of the selected text to
* the leading of each paragraph's applied paragraph style. Other overrides
* are left untouched.
*
* Each subsequent run on the *same* selection adds LEADING_INCREMENT to the
* leading. Any change of selection (even re-selecting the same text) re-arms
* the reset.
*
* Notes:
* - runs in a persistent engine, so that its state survives between runs.
* - an `afterSelectionChanged` listener disarms the increment mode as soon
* as the selection changes. It stays installed for the session, but
* does nothing while disarmed.
* - an insertion point selection targets its whole paragraph.
* - best assigned to a keyboard shortcut.
*
* @author m1b
* @version 2026-10-21
* @discussion https://community.adobe.com/questions-671/how-to-determine-the-continuous-execution-of-code-i-only-want-a-piece-of-code-to-run-once-1652200?tid=1652200
*/
//@targetengine 'resetOrIncrementLeading'
function main() {
const LEADING_INCREMENT = 1;
const LISTENER_NAME = 'resetOrIncrementLeading';
if (0 === app.documents.length)
return;
var target = _getTargetText(app.activeDocument.selection);
if (!target)
return alert('Please select some text and try again.');
var state = _getState();
var identity = _getSelectionIdentity();
var chunks = _getParagraphChunks(target);
if (
null !== state.identity
&& identity === state.identity
) {
incrementLeading(chunks, LEADING_INCREMENT);
}
else {
resetLeading(chunks);
}
// arm: the next run on this same selection will increment
state.identity = identity;
_addListener(LISTENER_NAME);
};
var savedMeasurementUnit = app.scriptPreferences.measurementUnit;
try {
app.scriptPreferences.measurementUnit = MeasurementUnits.POINTS;
app.doScript(main, ScriptLanguage.JAVASCRIPT, undefined, UndoModes.ENTIRE_SCRIPT, 'Reset Or Increment Leading');
} catch (error) {
alert('Reset Or Increment Leading: ' + error.message + ' (line ' + error.line + ')');
}
finally {
app.scriptPreferences.measurementUnit = savedMeasurementUnit;
}
/**
* Sets each chunk's leading to its paragraph style's leading.
* @author m1b
* @version 2026-10-21
* @param {Array<Object>} chunks - the chunks, see _getParagraphChunks.
*/
function resetLeading(chunks) {
for (var i = 0; i < chunks.length; i++)
chunks[i].text.leading = chunks[i].paragraphStyle.leading;
};
/**
* Adds `increment` to each chunk's leading, converting
* auto leading to its absolute value first.
* @author m1b
* @version 2026-10-21
* @param {Array<Object>} chunks - the chunks, see _getParagraphChunks.
* @param {Number} increment - the amount to add, in points.
*/
function incrementLeading(chunks, increment) {
for (var i = 0; i < chunks.length; i++) {
var firstCharacter = chunks[i].text.characters[0];
var leading = firstCharacter.leading;
if (Leading.AUTO === leading)
leading = firstCharacter.pointSize * firstCharacter.autoLeading / 100;
chunks[i].text.leading = leading + increment;
}
};
/**
* Returns the text to work on, or nothing if the selection isn't text.
* @author m1b
* @version 2026-10-21
* @param {Array} items - the document selection.
* @returns {Text?}
*/
function _getTargetText(items) {
if (1 !== items.length)
return;
var item = items[0];
if ('InsertionPoint' === item.constructor.name)
return item.paragraphs[0];
if (/^(Text|Character|Word|Line|Paragraph|TextColumn|TextStyleRange)$/.test(item.constructor.name))
return item;
};
/**
* Splits `text` at paragraph boundaries, so each part
* can be paired with its own paragraph style.
* @author m1b
* @version 2026-10-21
* @param {Text} text - the text to split.
* @returns {Array<Object>} - [{text: Text, paragraphStyle: ParagraphStyle}].
*/
function _getParagraphChunks(text) {
var chunks = [];
var characters = text.characters;
var start = text.index;
var end = start + characters.length;
var paragraphs = text.paragraphs.everyItem().getElements();
for (var i = 0; i < paragraphs.length; i++) {
var paragraphStart = paragraphs[i].index;
var paragraphEnd = paragraphStart + paragraphs[i].characters.length;
var from = Math.max(start, paragraphStart) - start;
var to = Math.min(end, paragraphEnd) - start - 1;
if (to < from)
continue;
chunks.push({
text: characters.itemByRange(from, to).texts[0].getElements()[0],
paragraphStyle: paragraphs[i].appliedParagraphStyle
});
}
return chunks;
};
/**
* Returns a string identifying the current selection, or
* an empty string if there isn't exactly one selected item.
* @author m1b
* @version 2026-10-21
* @returns {String}
*/
function _getSelectionIdentity() {
try {
if (
0 === app.documents.length
|| 1 !== app.selection.length
)
return '';
return app.selection[0].toSpecifier();
} catch (error) {
return '';
}
};
/**
* Returns the persistent state object, creating it if necessary.
* @author m1b
* @version 2026-10-21
* @returns {Object} - {identity: String|null}.
*/
function _getState() {
if (undefined === $.global.resetOrIncrementLeadingState)
$.global.resetOrIncrementLeadingState = { identity: null };
return $.global.resetOrIncrementLeadingState;
};
/**
* Installs the selection listener, first removing any listeners
* left by previous runs, so only one runs the current handler code.
* @author m1b
* @version 2026-10-21
* @param {String} name - the name that identifies this script's listeners.
*/
function _addListener(name) {
var listeners = app.eventListeners.everyItem().getElements();
for (var i = listeners.length - 1; i >= 0; i--) {
if (name === listeners[i].name)
listeners[i].remove();
}
var listener = app.addEventListener('afterSelectionChanged', _onSelectionChanged);
listener.name = name;
};
/**
* Disarms the increment mode when the selection has really changed.
* Note: the listener must not remove itself here; InDesign
* throws "the object no longer exists" if it does.
* @author m1b
* @version 2026-10-21
*/
function _onSelectionChanged() {
var state = _getState();
if (null === state.identity)
return;
// ignore events that don't actually change the selection
if (state.identity === _getSelectionIdentity())
return;
state.identity = null;
};