/* Copyright 2024 New Vector Ltd. Copyright 2019 The Matrix.org Foundation C.I.C. SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial Please see LICENSE files in the repository root for full details. */ import DocumentOffset from "./offset"; import EditorModel from "./model"; import { Part } from "./parts"; export interface IPosition { index: number; offset: number; } type Callback = (part: Part, startIdx: number, endIdx: number) => void; export type Predicate = (index: number, offset: number, part: Part) => boolean; export default class DocumentPosition implements IPosition { public constructor( public readonly index: number, public readonly offset: number, ) {} public compare(otherPos: DocumentPosition): number { if (this.index === otherPos.index) { return this.offset - otherPos.offset; } else { return this.index - otherPos.index; } } public iteratePartsBetween(other: DocumentPosition, model: EditorModel, callback: Callback): void { if (this.index === -1 || other.index === -1) { return; } const [startPos, endPos] = this.compare(other) < 0 ? [this, other] : [other, this]; if (startPos.index === endPos.index) { callback(model.parts[this.index], startPos.offset, endPos.offset); } else { const firstPart = model.parts[startPos.index]; callback(firstPart, startPos.offset, firstPart.text.length); for (let i = startPos.index + 1; i < endPos.index; ++i) { const part = model.parts[i]; callback(part, 0, part.text.length); } const lastPart = model.parts[endPos.index]; callback(lastPart, 0, endPos.offset); } } public forwardsWhile(model: EditorModel, predicate: Predicate): DocumentPosition { if (this.index === -1) { return this; } let { index, offset } = this; const { parts } = model; while (index < parts.length) { const part = parts[index]; while (offset < part.text.length) { if (!predicate(index, offset, part)) { return new DocumentPosition(index, offset); } offset += 1; } // end reached if (index === parts.length - 1) { return new DocumentPosition(index, offset); } else { index += 1; offset = 0; } } return this; // impossible but Typescript doesn't believe us } public backwardsWhile(model: EditorModel, predicate: Predicate): DocumentPosition { if (this.index === -1) { return this; } let { index, offset } = this; const parts = model.parts; while (index >= 0) { const part = parts[index]; while (offset > 0) { if (!predicate(index, offset - 1, part)) { return new DocumentPosition(index, offset); } offset -= 1; } // start reached if (index === 0) { return new DocumentPosition(index, offset); } else { index -= 1; offset = parts[index].text.length; } } return this; // impossible but Typescript doesn't believe us } public asOffset(model: EditorModel): DocumentOffset { if (this.index === -1) { return new DocumentOffset(0, true); } let offset = 0; for (let i = 0; i < this.index; ++i) { offset += model.parts[i].text.length; } offset += this.offset; const lastPart = model.parts[this.index]; const atEnd = !lastPart || offset >= lastPart.text.length; // if no last part, we're at the end return new DocumentOffset(offset, atEnd); } public isAtEnd(model: EditorModel): boolean { if (model.parts.length === 0) { return true; } const lastPartIdx = model.parts.length - 1; const lastPart = model.parts[lastPartIdx]; return this.index === lastPartIdx && this.offset === lastPart.text.length; } public isAtStart(): boolean { return this.index === 0 && this.offset === 0; } }