or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

array-operations.mdawareness.mddocument-management.mdindex.mdmap-operations.mdposition-undo.mdsynchronization.mdtext-operations.mdxml-support.md

text-operations.mddocs/

0

# Text Operations

1

2

## Overview

3

4

The `Text` type in pycrdt provides collaborative text editing capabilities similar to Python strings, but with automatic conflict resolution across multiple clients. It supports rich text formatting with attributes, embedded objects, and comprehensive change tracking through delta operations.

5

6

## Core Types

7

8

### Text

9

10

Collaborative text editing with string-like interface and rich formatting support.

11

12

```python { .api }

13

class Text:

14

def __init__(

15

self,

16

init: str | None = None,

17

*,

18

_doc: Doc | None = None,

19

_integrated: _Text | None = None,

20

) -> None:

21

"""

22

Create a new collaborative text object.

23

24

Args:

25

init (str, optional): Initial text content

26

_doc (Doc, optional): Parent document

27

_integrated (_Text, optional): Native text instance

28

"""

29

30

# String-like interface

31

def __len__(self) -> int:

32

"""Get the length of the text."""

33

34

def __str__(self) -> str:

35

"""Get the text content as a string."""

36

37

def __iter__(self) -> Iterator[str]:

38

"""Iterate over characters in the text."""

39

40

def __contains__(self, item: str) -> bool:

41

"""Check if substring exists in text."""

42

43

def __getitem__(self, key: int | slice) -> str:

44

"""Get character or substring by index/slice."""

45

46

def __setitem__(self, key: int | slice, value: str) -> None:

47

"""Set character or substring by index/slice."""

48

49

def __delitem__(self, key: int | slice) -> None:

50

"""Delete character or substring by index/slice."""

51

52

def __iadd__(self, value: str) -> Text:

53

"""Append text using += operator."""

54

55

# Text manipulation methods

56

def insert(self, index: int, value: str, attrs: dict[str, Any] | None = None) -> None:

57

"""

58

Insert text at the specified index.

59

60

Args:

61

index (int): Position to insert text

62

value (str): Text to insert

63

attrs (dict, optional): Formatting attributes for the inserted text

64

"""

65

66

def insert_embed(self, index: int, value: Any, attrs: dict[str, Any] | None = None) -> None:

67

"""

68

Insert an embedded object at the specified index.

69

70

Args:

71

index (int): Position to insert object

72

value: Object to embed

73

attrs (dict, optional): Formatting attributes for the embedded object

74

"""

75

76

def format(self, start: int, stop: int, attrs: dict[str, Any]) -> None:

77

"""

78

Apply formatting attributes to a text range.

79

80

Args:

81

start (int): Start index of the range

82

stop (int): End index of the range

83

attrs (dict): Formatting attributes to apply

84

"""

85

86

def diff(self) -> list[tuple[Any, dict[str, Any] | None]]:

87

"""

88

Get the formatted text as a list of (content, attributes) tuples.

89

90

Returns:

91

list: List of (content, attributes) pairs representing formatted text

92

"""

93

94

def clear(self) -> None:

95

"""Remove all text content."""

96

97

def to_py(self) -> str | None:

98

"""

99

Convert text to a Python string.

100

101

Returns:

102

str | None: Text content as string, or None if empty

103

"""

104

105

def observe(self, callback: Callable[[TextEvent], None]) -> Subscription:

106

"""

107

Observe text changes.

108

109

Args:

110

callback: Function called when text changes occur

111

112

Returns:

113

Subscription: Handle for unsubscribing

114

"""

115

116

def observe_deep(self, callback: Callable[[list[TextEvent]], None]) -> Subscription:

117

"""

118

Observe deep changes including nested structures.

119

120

Args:

121

callback: Function called with list of change events

122

123

Returns:

124

Subscription: Handle for unsubscribing

125

"""

126

127

def unobserve(self, subscription: Subscription) -> None:

128

"""

129

Remove an event observer.

130

131

Args:

132

subscription: Subscription handle to remove

133

"""

134

135

async def events(

136

self,

137

deep: bool = False,

138

max_buffer_size: float = float("inf")

139

) -> MemoryObjectReceiveStream:

140

"""

141

Get an async stream of text events.

142

143

Args:

144

deep (bool): Include deep change events

145

max_buffer_size (float): Maximum event buffer size

146

147

Returns:

148

MemoryObjectReceiveStream: Async event stream

149

"""

150

151

def sticky_index(self, index: int, assoc: Assoc = Assoc.AFTER) -> StickyIndex:

152

"""

153

Create a sticky index that maintains its position during edits.

154

155

Args:

156

index (int): Initial index position

157

assoc (Assoc): Association type (BEFORE or AFTER)

158

159

Returns:

160

StickyIndex: Persistent position tracker

161

"""

162

```

163

164

### TextEvent

165

166

Event emitted when text changes occur.

167

168

```python { .api }

169

class TextEvent:

170

@property

171

def target(self) -> Text:

172

"""Get the text object that changed."""

173

174

@property

175

def delta(self) -> list[dict[str, Any]]:

176

"""

177

Get the delta describing the changes.

178

179

Delta format:

180

- {"retain": n} - Keep n characters unchanged

181

- {"insert": "text", "attributes": {...}} - Insert text with attributes

182

- {"delete": n} - Delete n characters

183

"""

184

185

@property

186

def path(self) -> list[int | str]:

187

"""Get the path to the changed text within the document structure."""

188

```

189

190

## Usage Examples

191

192

### Basic Text Operations

193

194

```python

195

from pycrdt import Doc, Text

196

197

doc = Doc()

198

text = doc.get("content", type=Text)

199

200

# Basic string operations

201

text.insert(0, "Hello, world!")

202

print(str(text)) # "Hello, world!"

203

print(len(text)) # 13

204

205

# String-like access

206

print(text[0]) # "H"

207

print(text[0:5]) # "Hello"

208

print("world" in text) # True

209

210

# Modification

211

text[7:12] = "Python"

212

print(str(text)) # "Hello, Python!"

213

214

# Append text

215

text += " How are you?"

216

print(str(text)) # "Hello, Python! How are you?"

217

```

218

219

### Rich Text Formatting

220

221

```python

222

from pycrdt import Doc, Text

223

224

doc = Doc()

225

text = doc.get("document", type=Text)

226

227

# Insert text with formatting

228

text.insert(0, "Bold Text", {"bold": True})

229

text.insert(9, " and ", None)

230

text.insert(14, "Italic Text", {"italic": True})

231

232

# Apply formatting to existing text

233

text.format(0, 4, {"color": "red"}) # Make "Bold" red

234

text.format(19, 25, {"underline": True}) # Underline "Italic"

235

236

# Get formatted content

237

diff = text.diff()

238

for content, attrs in diff:

239

print(f"Content: {content}, Attributes: {attrs}")

240

```

241

242

### Embedded Objects

243

244

```python

245

from pycrdt import Doc, Text

246

247

doc = Doc()

248

text = doc.get("rich_content", type=Text)

249

250

# Insert text and embedded objects

251

text.insert(0, "Check out this image: ")

252

text.insert_embed(22, {"type": "image", "src": "photo.jpg"}, {"width": 300})

253

text.insert(23, " and this link: ")

254

text.insert_embed(39, {"type": "link", "url": "https://example.com"}, {"color": "blue"})

255

256

# Process mixed content

257

diff = text.diff()

258

for content, attrs in diff:

259

if isinstance(content, dict):

260

print(f"Embedded object: {content}")

261

else:

262

print(f"Text: {content}")

263

```

264

265

### Position Tracking

266

267

```python

268

from pycrdt import Doc, Text, Assoc

269

270

doc = Doc()

271

text = doc.get("content", type=Text)

272

273

text.insert(0, "Hello, world!")

274

275

# Create sticky indices

276

start_pos = text.sticky_index(7, Assoc.BEFORE) # Before "world"

277

end_pos = text.sticky_index(12, Assoc.AFTER) # After "world"

278

279

# Insert text before the tracked region

280

text.insert(0, "Well, ")

281

print(f"Start position: {start_pos.get_index()}") # Adjusted position

282

print(f"End position: {end_pos.get_index()}") # Adjusted position

283

284

# Extract text using sticky positions

285

with doc.transaction() as txn:

286

start_idx = start_pos.get_index(txn)

287

end_idx = end_pos.get_index(txn)

288

tracked_text = text[start_idx:end_idx]

289

print(f"Tracked text: {tracked_text}") # "world"

290

```

291

292

### Event Observation

293

294

```python

295

from pycrdt import Doc, Text, TextEvent

296

297

doc = Doc()

298

text = doc.get("content", type=Text)

299

300

def on_text_change(event: TextEvent):

301

print(f"Text changed in: {event.target}")

302

print(f"Delta: {event.delta}")

303

for op in event.delta:

304

if "retain" in op:

305

print(f" Retain {op['retain']} characters")

306

elif "insert" in op:

307

attrs = op.get("attributes", {})

308

print(f" Insert '{op['insert']}' with {attrs}")

309

elif "delete" in op:

310

print(f" Delete {op['delete']} characters")

311

312

# Subscribe to changes

313

subscription = text.observe(on_text_change)

314

315

# Make changes to trigger events

316

text.insert(0, "Hello")

317

text.insert(5, ", world!")

318

text.format(0, 5, {"bold": True})

319

320

# Clean up

321

text.unobserve(subscription)

322

```

323

324

### Async Event Streaming

325

326

```python

327

import anyio

328

from pycrdt import Doc, Text

329

330

async def monitor_text_changes(text: Text):

331

async with text.events() as event_stream:

332

async for event in event_stream:

333

print(f"Text event: {event.delta}")

334

335

doc = Doc()

336

text = doc.get("content", type=Text)

337

338

# Start monitoring in background

339

async def main():

340

async with anyio.create_task_group() as tg:

341

tg.start_soon(monitor_text_changes, text)

342

343

# Make changes

344

await anyio.sleep(0.1)

345

text.insert(0, "Hello")

346

await anyio.sleep(0.1)

347

text += ", World!"

348

349

anyio.run(main)

350

```

351

352

### Collaborative Editing Simulation

353

354

```python

355

from pycrdt import Doc, Text

356

357

# Simulate two clients editing the same document

358

doc1 = Doc(client_id=1)

359

doc2 = Doc(client_id=2)

360

361

text1 = doc1.get("shared_text", type=Text)

362

text2 = doc2.get("shared_text", type=Text)

363

364

# Client 1 makes changes

365

with doc1.transaction(origin="client1") as txn:

366

text1.insert(0, "Hello from client 1")

367

368

# Get update and apply to client 2

369

update = doc1.get_update()

370

doc2.apply_update(update)

371

372

print(str(text2)) # "Hello from client 1"

373

374

# Client 2 makes concurrent changes

375

with doc2.transaction(origin="client2") as txn:

376

text2.insert(0, "Hi! ")

377

text2.insert(len(text2), " - and client 2")

378

379

# Sync back to client 1

380

update = doc2.get_update(doc1.get_state())

381

doc1.apply_update(update)

382

383

print(str(text1)) # "Hi! Hello from client 1 - and client 2"

384

```

385

386

### Complex Text Processing

387

388

```python

389

from pycrdt import Doc, Text

390

391

doc = Doc()

392

text = doc.get("document", type=Text)

393

394

# Build a document with mixed content

395

text.insert(0, "Document Title", {"heading": 1, "bold": True})

396

text.insert(14, "\n\n")

397

text.insert(16, "This is the first paragraph with ", {"paragraph": True})

398

text.insert(50, "bold text", {"bold": True})

399

text.insert(59, " and ")

400

text.insert(64, "italic text", {"italic": True})

401

text.insert(75, ".")

402

403

text.insert(76, "\n\n")

404

text.insert(78, "Second paragraph with a ")

405

text.insert_embed(102, {"type": "link", "url": "example.com", "text": "link"})

406

text.insert(103, " and an ")

407

text.insert_embed(111, {"type": "image", "src": "diagram.png", "alt": "Diagram"})

408

text.insert(112, ".")

409

410

# Process the document

411

def analyze_content(text: Text):

412

"""Analyze text content and structure."""

413

diff = text.diff()

414

415

text_parts = []

416

embeds = []

417

418

for content, attrs in diff:

419

if isinstance(content, dict):

420

embeds.append(content)

421

else:

422

if attrs and "heading" in attrs:

423

text_parts.append(("heading", content))

424

elif attrs and "paragraph" in attrs:

425

text_parts.append(("paragraph", content))

426

else:

427

text_parts.append(("text", content))

428

429

return text_parts, embeds

430

431

parts, objects = analyze_content(text)

432

print(f"Text parts: {len(parts)}")

433

print(f"Embedded objects: {len(objects)}")

434

```

435

436

## Delta Operations

437

438

Text changes are represented as delta operations that describe insertions, deletions, and retains:

439

440

```python

441

# Example delta operations

442

delta_examples = [

443

{"retain": 5}, # Keep 5 characters

444

{"insert": "Hello", "attributes": {"bold": True}}, # Insert formatted text

445

{"delete": 3}, # Delete 3 characters

446

{"insert": {"type": "image", "src": "photo.jpg"}}, # Insert embed

447

]

448

449

# Processing deltas

450

def apply_delta(text: Text, delta: list[dict]):

451

"""Apply a delta to text (conceptual example)."""

452

pos = 0

453

for op in delta:

454

if "retain" in op:

455

pos += op["retain"]

456

elif "insert" in op:

457

content = op["insert"]

458

attrs = op.get("attributes")

459

if isinstance(content, str):

460

text.insert(pos, content, attrs)

461

pos += len(content)

462

else:

463

text.insert_embed(pos, content, attrs)

464

pos += 1

465

elif "delete" in op:

466

del text[pos:pos + op["delete"]]

467

```

468

469

## Error Handling

470

471

```python

472

from pycrdt import Doc, Text

473

474

doc = Doc()

475

text = doc.get("content", type=Text)

476

477

try:

478

# Invalid index operations

479

text.insert(-1, "Invalid") # May raise ValueError

480

481

# Invalid slice operations

482

text[100:200] = "Out of bounds" # May raise ValueError

483

484

# Invalid formatting

485

text.format(10, 5, {"invalid": "range"}) # start > stop

486

487

except (ValueError, IndexError) as e:

488

print(f"Text operation failed: {e}")

489

```