# Ranges and collections

Where a range sits after later edits, and what a collection lists once the document changes under it.

A range is a span of the document your add-in holds across `context.sync()` calls: a search hit, a `getRange` result, the range an `insertText` returns. Word decides where a range sits by how your add-in reached it, and the simulator follows. A search hit made in a sync that only read keeps its offsets when a later sync edits its paragraph, so replacing every hit of one search writes the later hits at stale offsets:

```js
await Word.run(async context => {
  context.document.body.insertParagraph('a a a', 'End');
  await context.sync();
});
await Word.run(async context => {
  const hits = context.document.body.search('a');
  hits.load('items');
  await context.sync();
  hits.items.forEach(hit => hit.insertText('bb', 'Replace'));
  await context.sync();
  const paragraphs = context.document.body.paragraphs;
  paragraphs.load('items/text');
  await context.sync();
  // paragraphs.items.map(p => p.text) -> ["", "bbbbbb a"]
});
```

Walk paragraphs with `getNext` and `getPrevious`. Past either end they fail as [Error format](https://www.rystic.ai/docs/simulators/word.md#error-format) says. A range's `paragraphs` lists the paragraphs it touches. How an `insertOoxml` moves ranges is on [Ranges and insertOoxml](https://www.rystic.ai/docs/simulators/word/features/ranges-and-insertooxml.md).

### A collection lists what it held when the run first reached it

A collection lists the items Word resolved when your `Word.run` first reached it, each read as it is now, however late it first loads them. `office.js` sends a collection to Word in the first `context.sync()` after your code makes it, whether `body.paragraphs`, a `search` or `getComments()`.

After an untracked `delete`, `body.paragraphs` reached before it, loaded or not, still lists the deleted paragraph, with empty text, and counts it. A paragraph inserted since is not among them. Reading the deleted paragraph itself, or calling anything on it, throws `ItemNotFound`. A range's paragraphs, a search's hits, comments and a thread's replies do the same.

A comment or reply deleted since stays listed with its `id`, author, date and `resolved` state. Reading it then goes this way:

| Read | Result |
|---|---|
| its `content` | throws `GeneralException` |
| the collection's items, loaded with every property | throws `GeneralException` |
| the deleted comment itself, or the collection's `getFirst` that returns it | throws `ItemNotFound` |

A tracked change collection alone lists the changes as they are at each load.

To see an edit, take a new collection after it, such as `body.getRange().paragraphs` or a new `search`. Or read in the next `Word.run`: `body.paragraphs` is one object for the whole run.

A search collection loaded again lists each hit where it is now.

**Differs from Word for the web:** Word lists a hit an edit moved, and that your add-in hasn't read or touched since, at its old offsets, until you read the hit itself.

### A range moves with the text unless a sync that only read made it

A range a `search` or `getRange` made in a `context.sync()` that only read keeps its offsets in its paragraph when a later sync edits the paragraph. It then reads whatever text sits there. So replacing every hit of one search in a later sync writes the later hits at stale offsets.

These ranges move with the text instead:

- one made in a sync that wrote anything, such as an edit anywhere, or a `changeTrackingMode` set to any mode, even the one in force;
- one made in the edit's own sync;
- one an `insertText` returned;
- one an `insertText` or `delete` went through.

| Edit | What a moving range does |
|---|---|
| text inserted at its start | the text lands before it |
| text inserted at its end | the text lands after it |
| text inserted inside it | it grows |
| a delete | it keeps what is left of it |

Ranges in other paragraphs never move.

### A range inside a deleted paragraph goes with it

Once an untracked `delete()` removes a paragraph, the document's last included, its ranges go with it:

| Range | After the delete |
|---|---|
| made inside it by `search` or `getRange`, in an earlier `context.sync()` that only read | throws `ItemNotFound` for the rest of the run, whether you read it, insert at it or call anything on it |
| any other range inside it | reads empty at the start of the next paragraph, and text inserted at it lands there |
| running into it from before | ends where the text before it ends, short of that text's paragraph mark, or throws `ItemNotFound` when that text went too |
| ending at its start | keeps the mark before it |
| running out of it | starts at the next paragraph |

"Any other range" includes one made in the same sync as the delete or in a sync that wrote, and one `insertText` returned. It also includes a comment's range, since a comment's `getRange()` is itself a write.

A search made after the delete finds the document as it is. A tracked `delete()` keeps the paragraph and its ranges.

### A range keeps its offsets when its text shrinks

With tracking off, a range a `search` or `getRange` made in an earlier `context.sync()` that only read keeps its offsets when an untracked `delete` shortens or empties its paragraph's text. It reads no further than the text's end. So a search's hit in a paragraph whose whole text you deleted reads empty text.

Text inserted at a range past the text, at any location, lands at the text's end. The range `insertText` returns keeps offsets past the text too, so it reads empty. `insertOoxml` instead returns the range its content landed in.

An end past the paragraph mark, as `getRange('Whole')` has, stays past it as the text grows or shrinks. Text inserted at that range's `End` lands before the mark.

---

Source: https://www.rystic.ai/docs/simulators/word/features/ranges-and-collections · Markdown: https://www.rystic.ai/docs/simulators/word/features/ranges-and-collections.md
