初始提交:terminalX 可运行态(M0/M1/M1.5 已真机验证)
- M0: libghostty SSH 终端(渲染/输入/连接)+ 白屏修复(OutputGate) + 会话状态机/自动重连 - M1: tsnet 用户态组网 + SSH-over-tsnet(fd 桥),shell 级真机验证;R5(Go+gvisor+C+++Swift 同进程) retire - M1.5: tmux -CC 原生 tab(MVP) - 结构: packages/(TXCore·TXTransport), apps/TerminalX, vendor/(libghostty-spm/libssh2/mbedtls/tsnet-bridge), artifacts/ - 文档: CLAUDE.md + docs/HANDOFF.md(新会话入口) - 环境: 认证代理→依赖 vendor 本地化;Go 在 ~/.local/go;仅模拟器/未签名 - 待续: M2 mosh, tmux 多 pane, M4 安全(host key/SE) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
221
vendor/libghostty-spm/Tests/GhosttyKitTest/TerminalSelectionAnchorTests.swift
vendored
Normal file
221
vendor/libghostty-spm/Tests/GhosttyKitTest/TerminalSelectionAnchorTests.swift
vendored
Normal file
@@ -0,0 +1,221 @@
|
||||
import Foundation
|
||||
@testable import GhosttyTerminal
|
||||
import Testing
|
||||
|
||||
// All test cases use host-point units. cellWidthPoints = 10, cellHeightPoints = 20
|
||||
// unless otherwise noted. The function's contract is "pointX / cellWidthPoints
|
||||
// = expected cell column" — that identity holds regardless of physical device
|
||||
// scale, so callers passing host points consistently get correct results on
|
||||
// @1x/2x/3x devices alike. A caller passing surface pixels without dividing
|
||||
// by displayScale would compute the wrong expectedColumn — that's a caller
|
||||
// bug, not a function bug. See plan §1 and §10审 for the unit contract.
|
||||
|
||||
struct TerminalSelectionAnchorTests {
|
||||
@Test
|
||||
func `single line ASCII`() {
|
||||
let range = TerminalSelectionAnchor.resolveRange(
|
||||
in: "hello world",
|
||||
word: "world",
|
||||
pointX: 60, pointY: 0,
|
||||
cellWidthPoints: 10, cellHeightPoints: 20
|
||||
)
|
||||
#expect(range == NSRange(location: 6, length: 5))
|
||||
}
|
||||
|
||||
@Test
|
||||
func `multi line locates row`() {
|
||||
let range = TerminalSelectionAnchor.resolveRange(
|
||||
in: "aaa\nbbb\nworld",
|
||||
word: "world",
|
||||
pointX: 0, pointY: 40,
|
||||
cellWidthPoints: 10, cellHeightPoints: 20
|
||||
)
|
||||
#expect(range == NSRange(location: 8, length: 5))
|
||||
}
|
||||
|
||||
@Test
|
||||
func `same word across rows only picks target row`() {
|
||||
let range = TerminalSelectionAnchor.resolveRange(
|
||||
in: "foo\nfoo\nfoo",
|
||||
word: "foo",
|
||||
pointX: 0, pointY: 20,
|
||||
cellWidthPoints: 10, cellHeightPoints: 20
|
||||
)
|
||||
#expect(range == NSRange(location: 4, length: 3))
|
||||
}
|
||||
|
||||
@Test
|
||||
func `empty rows preserved`() {
|
||||
let range = TerminalSelectionAnchor.resolveRange(
|
||||
in: "\n\nhello",
|
||||
word: "hello",
|
||||
pointX: 0, pointY: 40,
|
||||
cellWidthPoints: 10, cellHeightPoints: 20
|
||||
)
|
||||
#expect(range == NSRange(location: 2, length: 5))
|
||||
}
|
||||
|
||||
@Test
|
||||
func `word not found returns nil`() {
|
||||
let range = TerminalSelectionAnchor.resolveRange(
|
||||
in: "abc",
|
||||
word: "xyz",
|
||||
pointX: 0, pointY: 0,
|
||||
cellWidthPoints: 10, cellHeightPoints: 20
|
||||
)
|
||||
#expect(range == nil)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `row out of bounds returns nil`() {
|
||||
let range = TerminalSelectionAnchor.resolveRange(
|
||||
in: "abc",
|
||||
word: "abc",
|
||||
pointX: 0, pointY: 100,
|
||||
cellWidthPoints: 10, cellHeightPoints: 20
|
||||
)
|
||||
#expect(range == nil)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `emoji surrogate pair`() {
|
||||
let text = "hi 👋"
|
||||
let range = TerminalSelectionAnchor.resolveRange(
|
||||
in: text,
|
||||
word: "👋",
|
||||
pointX: 30, pointY: 0,
|
||||
cellWidthPoints: 10, cellHeightPoints: 20
|
||||
)
|
||||
let nsText = text as NSString
|
||||
#expect(range != nil)
|
||||
if let r = range {
|
||||
#expect(r == NSRange(location: 3, length: 2))
|
||||
#expect(NSMaxRange(r) <= nsText.length)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func `cjk full width`() {
|
||||
let range = TerminalSelectionAnchor.resolveRange(
|
||||
in: "你好 world",
|
||||
word: "你好",
|
||||
pointX: 0, pointY: 0,
|
||||
cellWidthPoints: 10, cellHeightPoints: 20
|
||||
)
|
||||
#expect(range == NSRange(location: 0, length: 2))
|
||||
}
|
||||
|
||||
@Test
|
||||
func `zero cell dimensions`() {
|
||||
let r1 = TerminalSelectionAnchor.resolveRange(
|
||||
in: "abc", word: "abc",
|
||||
pointX: 0, pointY: 0,
|
||||
cellWidthPoints: 0, cellHeightPoints: 20
|
||||
)
|
||||
let r2 = TerminalSelectionAnchor.resolveRange(
|
||||
in: "abc", word: "abc",
|
||||
pointX: 0, pointY: 0,
|
||||
cellWidthPoints: 10, cellHeightPoints: 0
|
||||
)
|
||||
#expect(r1 == nil)
|
||||
#expect(r2 == nil)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `empty word`() {
|
||||
let range = TerminalSelectionAnchor.resolveRange(
|
||||
in: "abc", word: "",
|
||||
pointX: 0, pointY: 0,
|
||||
cellWidthPoints: 10, cellHeightPoints: 20
|
||||
)
|
||||
#expect(range == nil)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `negative coordinates`() {
|
||||
let r1 = TerminalSelectionAnchor.resolveRange(
|
||||
in: "abc", word: "abc",
|
||||
pointX: -1, pointY: 0,
|
||||
cellWidthPoints: 10, cellHeightPoints: 20
|
||||
)
|
||||
let r2 = TerminalSelectionAnchor.resolveRange(
|
||||
in: "abc", word: "abc",
|
||||
pointX: 0, pointY: -1,
|
||||
cellWidthPoints: 10, cellHeightPoints: 20
|
||||
)
|
||||
#expect(r1 == nil)
|
||||
#expect(r2 == nil)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `substring disambiguation by point X`() {
|
||||
// `catalog cat` long-pressed at the end `cat` (column 8) — must pick
|
||||
// the standalone `cat` at location 8, not the prefix inside `catalog`
|
||||
// at location 0. literals at {0, 8}, expectedColumn=8 → 8.
|
||||
let range = TerminalSelectionAnchor.resolveRange(
|
||||
in: "catalog cat",
|
||||
word: "cat",
|
||||
pointX: 80, pointY: 0,
|
||||
cellWidthPoints: 10, cellHeightPoints: 20
|
||||
)
|
||||
#expect(range == NSRange(location: 8, length: 3))
|
||||
}
|
||||
|
||||
@Test
|
||||
func `triple repeat picked by point X`() {
|
||||
// literals at {0, 4, 8}, expectedColumn=8 → 8.
|
||||
let range = TerminalSelectionAnchor.resolveRange(
|
||||
in: "cat cat cat",
|
||||
word: "cat",
|
||||
pointX: 80, pointY: 0,
|
||||
cellWidthPoints: 10, cellHeightPoints: 20
|
||||
)
|
||||
#expect(range == NSRange(location: 8, length: 3))
|
||||
}
|
||||
|
||||
@Test
|
||||
func `non word characters in token`() {
|
||||
// literals at {4, 15}, expectedColumn=15 → 15.
|
||||
let range = TerminalSelectionAnchor.resolveRange(
|
||||
in: "see /usr/local /usr/local",
|
||||
word: "/usr/local",
|
||||
pointX: 150, pointY: 0,
|
||||
cellWidthPoints: 10, cellHeightPoints: 20
|
||||
)
|
||||
#expect(range == NSRange(location: 15, length: 10))
|
||||
}
|
||||
|
||||
@Test
|
||||
func `non word prefix token`() {
|
||||
// literals at {1, 6}, expectedColumn=6 → 6.
|
||||
let range = TerminalSelectionAnchor.resolveRange(
|
||||
in: "x/foo /foo",
|
||||
word: "/foo",
|
||||
pointX: 60, pointY: 0,
|
||||
cellWidthPoints: 10, cellHeightPoints: 20
|
||||
)
|
||||
#expect(range == NSRange(location: 6, length: 4))
|
||||
}
|
||||
|
||||
@Test
|
||||
func `nan and infinity guarded`() {
|
||||
let r1 = TerminalSelectionAnchor.resolveRange(
|
||||
in: "abc", word: "abc",
|
||||
pointX: .nan, pointY: 0,
|
||||
cellWidthPoints: 10, cellHeightPoints: 20
|
||||
)
|
||||
let r2 = TerminalSelectionAnchor.resolveRange(
|
||||
in: "abc", word: "abc",
|
||||
pointX: .infinity, pointY: 0,
|
||||
cellWidthPoints: 10, cellHeightPoints: 20
|
||||
)
|
||||
let r3 = TerminalSelectionAnchor.resolveRange(
|
||||
in: "abc", word: "abc",
|
||||
pointX: 0, pointY: 0,
|
||||
cellWidthPoints: .nan, cellHeightPoints: 20
|
||||
)
|
||||
#expect(r1 == nil)
|
||||
#expect(r2 == nil)
|
||||
#expect(r3 == nil)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user