Locating source with F8

Locate links the editor and the output. With Locate on, each cursor in the editor is shown as a box over the element it is in, and pointing at an element in the output shows that element's source in the editor. Locate reads the source map of IOSync and never writes into the output's DOM. This guide shows how to turn it on, what each gesture does, and how to look up authored source from a script.

Turning Locate on

Locate is off by default. Press F8 to toggle it. A toggle by you shows a toast, Locate enabled or Locate disabled, that replaces the previous one. The state is kept in the URL while it is on, and a page that opens with it on applies it silently.

WayHow
KeyF8, with focus in the editor or in the output.
CommandToggle Locate, in the F1 palette and in the sync group of the editor's context menu.
URLsm=1 turns it on at load. sm=0 is the default.
Scriptsync.scrollLock, see Scripting Locate.

Try it

Turn Locate on, put the cursor inside the paragraph, and then point at the list and click the button.

html
<h1>Locate</h1>
<p>Place the cursor in this <b>sentence</b>, then point at the list.</p>
<ul>
  <li>First</li>
  <li><em>Second</em></li>
</ul>
<button id=b>Clicked 0 times</button>
<script type=module>
  let n = 0
  b.onclick = () => b.textContent = `Clicked ${++n} times`
</script>

The cursor shows as a cyan box over the paragraph. Pointing at the list previews its source in the editor, and clicking the button selects the button's source instead of running its handler. See Clicks are consumed.

From the editor to the output

Cursor boxes

The active end of every selection in the editor is mapped through the source map to the enclosing live element. Each mapped element gets an unlabeled cyan box. With several cursors, you see several boxes.

  • The playground draws the boxes in an overlay above the output frame, clipped to it. Locate writes nothing into the output's DOM.
  • Runtime elements, which scripts or the user created, have no source, so they get no box.
  • Updates are debounced by 30 ms. When Locate turns on, or after a reload, the first boxes can take a moment: Locate tries up to six times over about three seconds, and stops as soon as a box appears.

Scrolling to the cursor

When there is exactly one cursor or selection, collapsed or not, the mapped element is scrolled into view in the output if needed. Late layout is retried for up to 12 seconds. Scrolling happens only while no output element is pointed at, and any scroll gesture of yours in the output cancels the retries: the wheel, touch, a pointer press, PageUp, PageDown, Home, End, the arrow keys and Space, or any scroll that Locate did not cause. The boxes follow when the output scrolls or resizes, and when you move the split, zoom or change the viewport.

From the output to the editor

With Locate on, the output answers three gestures:

GestureWhat happens
Point at an elementA hover box appears, and the editor previews the source: it selects the element's inner range and reveals it, without taking focus. When the pointer leaves the output, the previous editor selection returns, if you did not change it meanwhile.
Click an elementSelects its source range and focuses the editor. The click is consumed.
Shift + wheelWalks the ancestry of the pointed element. Wheel up goes from the element's inner range to the whole element, then to the parent's inner range, the whole parent, and so on. Wheel down reverses the sequence. The wheel does not scroll sideways meanwhile.

Inner and outer ranges

The inner range of an element runs from the end of its start tag to the start of its end tag. Void and self-closing elements have no inside, so they use the whole element. The outer range is the whole element, tags included. A click selects the inner range, or the range that Shift + wheel stepped to if the element is already the pointed one.

Clicks are consumed

Warning: While Locate is on, page click handlers and link navigation do not run for practically any click on authored content. The click is stopped in the capture phase. pointerdown, mousedown, pointerup and mouseup still reach the page. Turn Locate off with F8 to use the page.

Pointing at runtime elements

An element that scripts created has no source of its own. Pointing at one resolves to its nearest ancestor that does, through the composed path, so it crosses shadow roots. When the target is <body> or <html>, Locate picks the smallest mapped element under the pointer, out of at most 256 candidates.

A key you press in the editor, other than a modifier, clears the pointer box until the pointer moves at least 8 CSS pixels. Modifier-only keys such as Shift keep it, so Shift + wheel works.

Locate while you edit

During an edit, the cursor boxes pause and the pointer box clears. They resume once an output acknowledges the new revision of the source. Locate reads the acknowledged source even while a change from the output is waiting for the editor, and it works through nested declarative shadow roots; it was tested to three levels. See Live sync and reloads.

Scripting Locate

Toggling Locate from a script

Experimental sync.scrollLock is the Locate object of the playground window. The name is historical and has nothing to do with the ScrollLock key.

active

A getter: whether Locate is on.

set(active, options?)

Turns Locate on or off, and returns the resulting Boolean.

toggle(options?)

Toggles Locate, and returns the resulting Boolean.

refresh(options?)

Seeds the cursor boxes again, and returns a promise.

js
sync.scrollLock.set(true)    // Locate on, without a toast
sync.scrollLock.active       // true

F8 also shows the toast and writes sm to the URL at once. set() and toggle() show no toast and do not write the URL themselves. sm follows at the next URL write. See sync.

Note: This API is experimental and may change.

Looking up authored source

The same source map answers questions from a script. During an Agent Folder Share, io.eval.js can ask for the exact authored source of live output elements:

await o.input.pos(selector)

An array with a { start, end } object per match: zero-based UTF-16 offsets into the editor source, end exclusive.

await o.input.sel(selector)

An array with a [sl, sc, el, ec, 0] selection per match, one-based, ready to assign to sel.

await o.input.inner(selector) and innerAll(selector)

The authored inner source of the first match, or of every match.

await o.input.outer(selector) and outerAll(selector)

The authored outer source of the first match, or of every match.

js
await o.input.outer('main > h1')           // the authored source of the first match
sel = await o.input.sel('.card')            // select every match in the editor
  • The strings are the exact authored text, sliced from the source, with its formatting, entities and attribute quoting. They are not serialized DOM.
  • A runtime element, or one without a match in the source, gives null. innerAll.concat(selector) and outerAll.concat(selector) join the non-null strings with a newline.
  • Selectors are matched depth-first from the document element. A host's open shadow tree comes before its light children, and descendant combinators do not cross shadow boundaries.
  • The output must be in sync with the current editor text. If the map is stale, or the input changes during the lookup, the call throws an InvalidStateError.

In o.eval.js, the output side has sync.input with the same methods. It accepts a selector, an element or an iterable of elements of that output document:

js
await sync.input.outerAll.concat(document.querySelectorAll('nav a'))

See o and File evals.

Limits

The blog post that introduced IOSync reports that Locate's median answer fell from about 1 s to about 12 ms (83 times faster), with 0.032 ms for the engine lookup itself.

See also