ISML combines markup with server-side expressions. Intellij SFCC uses its scripting engine to analyze ${ … } expressions and <isscript> blocks, while the markup service handles tags and template structure. This connects script completion, hover, signatures, navigation, references, and diagnostics to your template source
Start with a typed expression
<isscript>
var ProductMgr = require('dw/catalog/ProductMgr');
var product = ProductMgr.getProduct('demo-product');
</isscript>
<isif condition="${product}">
<isprint value="${product.name}" />
<a href="${URLUtils.url('Product-Show', 'pid', product.ID)}">
View product
</a>
</isif>Invoke completion after product. in the expression. The type comes from ProductMgr.getProduct, so its API members can be suggested. Hover a member, use Go to Definition, or invoke signature help inside URLUtils.url. Recognized route arguments can also offer project route completion
Template variables and pdict
The scripting view declares the standard template helpers Resource, URLUtils, StringUtils, and out, and models pdict as dw.system.PipelineDictionary. They can participate in scripting intelligence without imports in each expression
Knowing the type of pdict does not establish the shape of every arbitrary value passed into it. A property named pdict.product is not automatically proved to be a dw.catalog.Product. It might be an SFRA view model or a plain object
When the template's contract is known, describe the actual shape:
<isscript>
/** @type {{ id: string, displayName: string }} */
var tile = pdict.tile;
</isscript>
<isprint value="${tile.displayName}" />An annotation documents an expectation; the controller must still supply a matching value. Prefer the real view-model shape over assigning an SFCC API type to a different object
isset, loops, and status variables
The engine models variables introduced by <isset> and <isloop> so they can be used in later expressions. Type precision follows the expression that produces each value
<isset name="heading" value="${Resource.msg('product.title', 'product', null)}" scope="page" />
<isprint value="${heading}" />heading becomes a known variable. Find references to follow its recognized uses, see Template variable navigation. Included templates see it too, see Variables across templates
<isloop items="${pdict.tiles}" var="tile" status="loopState">
<div class="${loopState.first ? 'first' : ''}">
<isprint value="${tile.displayName}" />
</div>
</isloop>The items expression is analyzed like any other expression, and tile and loopState are declared for the expressions inside the loop. loopState. completes count, index, first, last, odd and even. The loop variable has no type, though. It does not take the element type of the collection, even when items is typed, so tile. offers no members and member access on it is not checked
To work with typed members inside the loop, read the item into an annotated variable in a script block:
<isloop items="${pdict.tiles}" var="tile">
<isscript>
/** @type {{ id: string, displayName: string }} */
var current = tile;
</isscript>
<isprint value="${current.displayName}" />
</isloop>Template variable navigation
A variable declared in a template leads to its declaration and to its other uses, in the same template and in the templates it reaches
Cmd+Click a variable in a ${ … } expression or a script block (Ctrl+Click on Windows and Linux), or put the caret on it and press F12 (Go to Definition), to move to its declaration:
| Declared with | The cursor lands on |
|---|---|
var in an <isscript> block | The name after var |
<isset name="…"> | The name value |
<isloop var="…" status="…"> | The var or status value |
Go to References (Shift+F12) or Find All References (Shift+Option+F12 on macOS, Shift+Alt+F12 on Windows and Linux) on a variable in an expression or a script block lists its declaration and every use in <isscript> blocks and ${ … } expressions
- A variable that no other template sets or reads is searched in its own template only. There, uses are matched by name, so a property with the same name, such as
pdict.applyUrl, is listed with them. Names inside strings and comments are skipped - A variable with the same name in an unrelated template is not listed
- Rename Symbol (
F2) on a variable edits the places Find References lists, see Rename a template variable
Variables across templates
An <isset> variable, and the var and status of an <isloop>, reach the templates rendered in place while they are set: <isinclude template="…">, a custom tag and <isdecorate>. What a rendered template sets comes back to the template that renders it, after the tag
<isset name="badge" value="${Resource.msg('tile.badge', 'product', null)}" scope="page" />
<isloop items="${pdict.products}" var="product" status="loopState">
<isinclude template="product/productTile" />
</isloop><div class="tile" data-index="${loopState.index}">
<isprint value="${badge}" />
</div>In productTile.isml, badge hovers as string, the type of its isset value in the grid
| Feature | In the rendered template |
|---|---|
| Completion | badge, product and loopState come after the template's own names. The label details show the declaring template, such as search/productGrid.isml, and the detail starts with isset (page) in or isloop in |
| Hover and types | The type of the declared value, as if the variable were declared in this template. A loop var stays untyped |
| Go to Definition | Opens the declaring template on the isset name, or on the isloop var or status. pdict.x also finds an isset of x with scope="request" or scope="pdict" |
| Find References | The declaration and every read in the templates connected to it. pdict.x reads are not listed |
| Diagnostics | No "Cannot find name" or "used before being assigned" error for a name a connected template sets. An isset that another template reads is not reported as unused |
- A
sessionvariable, anissetwith noscopeorscope="session", is readable in every template. It is listed last in completion, and a global with the same name, such asrequest, wins <isinclude url="…">is a separate request, so no variables cross itpdict,out,Resource,URLUtilsandStringUtilsbelong to each template and never cross- A name that a connected template does not set at that point is still reported, for example a read before the
<isinclude>that sets it
Imports and cartridge context
A require() in a script block uses the same cartridge resolution as a server script. Types from local metadata can participate when an expression resolves to an appropriate SFCC object
Press F12 on the path in require('*/cartridge/scripts/checkout/constants') to open the module. In the other direction, references to the module's exports list the template lines that read them, see Find template usages from scripts
Legacy template code can also use importScript:
<isscript>
importScript('util/helpers.ds');
</isscript>
<isprint value="${formatMoney(pdict.total)}" />When helpers.ds declares formatMoney, the imported symbol can complete, hover, and navigate back to its declaration. The analyzer models the import relationship; it does not execute the import or guarantee the runtime type of pdict.total. See Digital Script for import scope and shadowing checks
How positions stay connected
The server builds a JavaScript analysis view of the template and maps locations back to the original ISML. You invoke completion, navigate, and inspect diagnostics in .isml, not in a generated JavaScript file
Complex or dynamically assembled expressions can limit the information available. A missing diagnostic on an untyped value is not evidence that its runtime property exists
Related
The scripting pages cover the shared engine in more depth, including ISML expression completion, inlay hints, which also appear in templates, references to ISML variables, and template and resource strings