@effect-tui/react 0.14.1 → 0.14.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -1
- package/dist/src/components/MultilineTextInput.d.ts.map +1 -1
- package/dist/src/components/MultilineTextInput.js +2 -1
- package/dist/src/components/MultilineTextInput.js.map +1 -1
- package/dist/src/components/text-editing.d.ts +2 -4
- package/dist/src/components/text-editing.d.ts.map +1 -1
- package/dist/src/components/text-editing.js +2 -8
- package/dist/src/components/text-editing.js.map +1 -1
- package/dist/src/hosts/text.js +4 -4
- package/dist/src/hosts/text.js.map +1 -1
- package/dist/src/utils/index.d.ts +1 -0
- package/dist/src/utils/index.d.ts.map +1 -1
- package/dist/src/utils/index.js +1 -0
- package/dist/src/utils/index.js.map +1 -1
- package/dist/src/utils/text-wrap.d.ts.map +1 -1
- package/dist/src/utils/text-wrap.js +4 -3
- package/dist/src/utils/text-wrap.js.map +1 -1
- package/dist/src/utils/word-boundaries.d.ts +7 -0
- package/dist/src/utils/word-boundaries.d.ts.map +1 -0
- package/dist/src/utils/word-boundaries.js +40 -0
- package/dist/src/utils/word-boundaries.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -6
- package/src/components/MultilineTextInput.tsx +2 -1
- package/src/components/text-editing.ts +2 -9
- package/src/hosts/text.ts +4 -4
- package/src/utils/index.ts +1 -0
- package/src/utils/text-wrap.ts +4 -3
- package/src/utils/word-boundaries.ts +40 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effect-tui/react",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.3",
|
|
4
4
|
"description": "React bindings for @effect-tui/core",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -83,8 +83,8 @@
|
|
|
83
83
|
"prepublishOnly": "bun run typecheck && bun run build"
|
|
84
84
|
},
|
|
85
85
|
"dependencies": {
|
|
86
|
-
"@effect-tui/core": "^0.14.
|
|
87
|
-
"@effect/platform": "^0.94.
|
|
86
|
+
"@effect-tui/core": "^0.14.3",
|
|
87
|
+
"@effect/platform": "^0.94.1",
|
|
88
88
|
"@effect/platform-bun": "^0.87.0",
|
|
89
89
|
"@effect/rpc": "^0.73.0",
|
|
90
90
|
"@parcel/watcher": "^2.5.1",
|
|
@@ -92,8 +92,8 @@
|
|
|
92
92
|
"shiki": "^3.20.0"
|
|
93
93
|
},
|
|
94
94
|
"peerDependencies": {
|
|
95
|
-
"effect": "^3.
|
|
96
|
-
"react": "^19.
|
|
95
|
+
"effect": "^3.0.0",
|
|
96
|
+
"react": "^19.0.0",
|
|
97
97
|
"@effect/opentelemetry": "^0.60.0",
|
|
98
98
|
"@opentelemetry/api": "^1.9.0",
|
|
99
99
|
"@opentelemetry/sdk-trace-base": "^2.2.0"
|
|
@@ -119,7 +119,7 @@
|
|
|
119
119
|
"@types/node": "^25.0.3",
|
|
120
120
|
"@types/react": "^19.0.0",
|
|
121
121
|
"@types/react-reconciler": "^0.32.3",
|
|
122
|
-
"effect": "^3.19.
|
|
122
|
+
"effect": "^3.19.14",
|
|
123
123
|
"react": "^19.2.3",
|
|
124
124
|
"typescript": "^5.9.3",
|
|
125
125
|
"vitest": "^4.0.16"
|
|
@@ -2,6 +2,7 @@ import { type Color, Colors, displayWidth, graphemes } from "@effect-tui/core"
|
|
|
2
2
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
|
3
3
|
import { useKeyboard } from "../hooks/use-keyboard.js"
|
|
4
4
|
import { usePaste } from "../hooks/use-paste.js"
|
|
5
|
+
import { isWhitespace } from "../utils/word-boundaries.js"
|
|
5
6
|
import type { DrawContext } from "../hosts/canvas.js"
|
|
6
7
|
import {
|
|
7
8
|
deleteCharBackwardMultiline,
|
|
@@ -97,7 +98,7 @@ function wrapLogicalLine(line: string, logicalRow: number, maxWidth: number): Li
|
|
|
97
98
|
}
|
|
98
99
|
|
|
99
100
|
// Find break opportunities (after spaces)
|
|
100
|
-
const isBreakable = graphemeList.map((g) =>
|
|
101
|
+
const isBreakable = graphemeList.map((g) => isWhitespace(g))
|
|
101
102
|
|
|
102
103
|
const visualLines: VisualLine[] = []
|
|
103
104
|
let start = 0
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { graphemes } from "@effect-tui/core"
|
|
8
|
+
import { matchNextWord, matchPrevWord } from "../utils/word-boundaries.js"
|
|
8
9
|
|
|
9
10
|
// ============================================================================
|
|
10
11
|
// Types
|
|
@@ -34,15 +35,7 @@ export interface EditResult<T> {
|
|
|
34
35
|
// Word boundary helpers (shared by TextInput and MultilineTextInput)
|
|
35
36
|
// ============================================================================
|
|
36
37
|
|
|
37
|
-
|
|
38
|
-
export function matchPrevWord(beforeCursor: string): string | null {
|
|
39
|
-
return beforeCursor.match(/(\S+\s*|\s+)$/)?.[0] ?? null
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** Match the next word (including leading whitespace). */
|
|
43
|
-
export function matchNextWord(afterCursor: string): string | null {
|
|
44
|
-
return afterCursor.match(/^\s*\S+/)?.[0] ?? null
|
|
45
|
-
}
|
|
38
|
+
export { matchNextWord, matchPrevWord }
|
|
46
39
|
|
|
47
40
|
// ============================================================================
|
|
48
41
|
// Single-line operations (for TextInput)
|
package/src/hosts/text.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type CellBuffer, type Color, displayWidth, type Palette } from "@effect-tui/core"
|
|
2
2
|
import type { ColorMotionValue } from "../motion/color-motion-value.js"
|
|
3
3
|
import type { CommonProps, HostContext, HostInstance, Rect, Size } from "../reconciler/types.js"
|
|
4
|
-
import { resolveInheritedBgStyle, styleIdFromProps, wrapSpans } from "../utils/index.js"
|
|
4
|
+
import { isWhitespace, resolveInheritedBgStyle, splitWords, styleIdFromProps, wrapSpans } from "../utils/index.js"
|
|
5
5
|
import { BaseHost, getInheritedBg } from "./base.js"
|
|
6
6
|
|
|
7
7
|
/** Color prop that can be a static Color or a spring-animated ColorMotionValue */
|
|
@@ -230,19 +230,19 @@ export class TextHost extends BaseHost {
|
|
|
230
230
|
}
|
|
231
231
|
|
|
232
232
|
// Split into words (keeping whitespace as separate tokens)
|
|
233
|
-
const tokens = rawLine
|
|
233
|
+
const tokens = splitWords(rawLine)
|
|
234
234
|
let line = ""
|
|
235
235
|
let lineW = 0
|
|
236
236
|
|
|
237
237
|
for (const token of tokens) {
|
|
238
238
|
const tokenW = displayWidth(token)
|
|
239
|
-
const
|
|
239
|
+
const isWs = isWhitespace(token)
|
|
240
240
|
|
|
241
241
|
if (lineW + tokenW <= maxWidth) {
|
|
242
242
|
// Token fits on current line
|
|
243
243
|
line += token
|
|
244
244
|
lineW += tokenW
|
|
245
|
-
} else if (
|
|
245
|
+
} else if (isWs) {
|
|
246
246
|
// Whitespace doesn't fit - just skip it (don't start new line with space)
|
|
247
247
|
continue
|
|
248
248
|
} else if (tokenW <= maxWidth) {
|
package/src/utils/index.ts
CHANGED
package/src/utils/text-wrap.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { displayWidth } from "@effect-tui/core"
|
|
2
|
+
import { isWhitespace, splitWords } from "./word-boundaries.js"
|
|
2
3
|
|
|
3
4
|
type SpanLike = { text: string }
|
|
4
5
|
|
|
@@ -12,18 +13,18 @@ export function wrapSpans<T extends SpanLike>(spans: T[], maxWidth: number): T[]
|
|
|
12
13
|
|
|
13
14
|
for (const span of spans) {
|
|
14
15
|
// Split span text into words (keeping whitespace as separate tokens)
|
|
15
|
-
const tokens = span.text
|
|
16
|
+
const tokens = splitWords(span.text)
|
|
16
17
|
|
|
17
18
|
for (const token of tokens) {
|
|
18
19
|
if (!token) continue
|
|
19
20
|
const tokenWidth = displayWidth(token)
|
|
20
|
-
const
|
|
21
|
+
const isWs = isWhitespace(token)
|
|
21
22
|
|
|
22
23
|
if (lineWidth + tokenWidth <= maxWidth) {
|
|
23
24
|
// Token fits on current line
|
|
24
25
|
lines[lines.length - 1].push({ ...span, text: token })
|
|
25
26
|
lineWidth += tokenWidth
|
|
26
|
-
} else if (
|
|
27
|
+
} else if (isWs) {
|
|
27
28
|
// Skip whitespace at line break
|
|
28
29
|
continue
|
|
29
30
|
} else if (tokenWidth <= maxWidth) {
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const whitespaceRegex = /^\s+$/u
|
|
2
|
+
|
|
3
|
+
export function isWhitespace(text: string): boolean {
|
|
4
|
+
return whitespaceRegex.test(text)
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function splitWords(text: string): string[] {
|
|
8
|
+
if (!text) return []
|
|
9
|
+
return text.split(/(\s+)/u).filter((token) => token.length > 0)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Match the previous word (and trailing whitespace) or whitespace-only chunk. */
|
|
13
|
+
export function matchPrevWord(beforeCursor: string): string | null {
|
|
14
|
+
const tokens = splitWords(beforeCursor)
|
|
15
|
+
if (tokens.length === 0) return null
|
|
16
|
+
const last = tokens[tokens.length - 1]
|
|
17
|
+
if (isWhitespace(last)) {
|
|
18
|
+
const prev = tokens[tokens.length - 2]
|
|
19
|
+
if (prev && !isWhitespace(prev)) {
|
|
20
|
+
return prev + last
|
|
21
|
+
}
|
|
22
|
+
return last
|
|
23
|
+
}
|
|
24
|
+
return last
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Match the next word (including leading whitespace). */
|
|
28
|
+
export function matchNextWord(afterCursor: string): string | null {
|
|
29
|
+
const tokens = splitWords(afterCursor)
|
|
30
|
+
if (tokens.length === 0) return null
|
|
31
|
+
const first = tokens[0]
|
|
32
|
+
if (isWhitespace(first)) {
|
|
33
|
+
const next = tokens[1]
|
|
34
|
+
if (next && !isWhitespace(next)) {
|
|
35
|
+
return first + next
|
|
36
|
+
}
|
|
37
|
+
return null
|
|
38
|
+
}
|
|
39
|
+
return first
|
|
40
|
+
}
|