fix(ui): use CodeMirror selection handles on iOS

This commit is contained in:
Bohdan Triapitsyn
2026-08-18 21:37:57 +03:00
parent 423f5b9652
commit 584b9304fd
7 changed files with 371 additions and 129 deletions
+51 -39
View File
@@ -6,25 +6,25 @@
"name": "openchamber-monorepo",
"dependencies": {
"@base-ui/react": "^1.4.0",
"@codemirror/autocomplete": "^6.20.0",
"@codemirror/commands": "^6.10.1",
"@codemirror/autocomplete": "^6.20.3",
"@codemirror/commands": "^6.11.0",
"@codemirror/lang-cpp": "^6.0.3",
"@codemirror/lang-css": "^6.3.1",
"@codemirror/lang-go": "^6.0.1",
"@codemirror/lang-html": "^6.4.11",
"@codemirror/lang-javascript": "^6.2.4",
"@codemirror/lang-html": "^6.4.12",
"@codemirror/lang-javascript": "^6.2.5",
"@codemirror/lang-json": "^6.0.2",
"@codemirror/lang-markdown": "^6.5.0",
"@codemirror/lang-markdown": "^6.5.2",
"@codemirror/lang-python": "^6.2.1",
"@codemirror/lang-rust": "^6.0.2",
"@codemirror/lang-sql": "^6.10.0",
"@codemirror/lang-xml": "^6.1.0",
"@codemirror/lang-yaml": "^6.1.2",
"@codemirror/language": "6.12.2",
"@codemirror/lint": "^6.9.2",
"@codemirror/search": "^6.6.0",
"@codemirror/state": "^6.5.4",
"@codemirror/view": "6.39.13",
"@codemirror/lang-yaml": "^6.1.3",
"@codemirror/language": "6.12.4",
"@codemirror/lint": "^6.9.7",
"@codemirror/search": "^6.7.1",
"@codemirror/state": "^6.7.1",
"@codemirror/view": "6.43.9",
"@heroui/scroll-shadow": "^2.3.18",
"@heroui/system": "^2.4.23",
"@heroui/theme": "^2.4.23",
@@ -143,34 +143,34 @@
"@capacitor/keyboard": "^8.0.0",
"@capacitor/push-notifications": "^8.1.1",
"@capacitor/status-bar": "^8.0.0",
"@codemirror/autocomplete": "^6.20.0",
"@codemirror/commands": "^6.10.1",
"@codemirror/autocomplete": "^6.20.3",
"@codemirror/commands": "^6.11.0",
"@codemirror/lang-cpp": "^6.0.3",
"@codemirror/lang-css": "^6.3.1",
"@codemirror/lang-go": "^6.0.1",
"@codemirror/lang-html": "^6.4.11",
"@codemirror/lang-javascript": "^6.2.4",
"@codemirror/lang-html": "^6.4.12",
"@codemirror/lang-javascript": "^6.2.5",
"@codemirror/lang-json": "^6.0.2",
"@codemirror/lang-markdown": "^6.5.0",
"@codemirror/lang-markdown": "^6.5.2",
"@codemirror/lang-python": "^6.2.1",
"@codemirror/lang-rust": "^6.0.2",
"@codemirror/lang-sql": "^6.10.0",
"@codemirror/lang-xml": "^6.1.0",
"@codemirror/lang-yaml": "^6.1.2",
"@codemirror/language": "6.12.2",
"@codemirror/lang-yaml": "^6.1.3",
"@codemirror/language": "6.12.4",
"@codemirror/language-data": "^6.5.2",
"@codemirror/legacy-modes": "^6.5.2",
"@codemirror/lint": "^6.9.2",
"@codemirror/search": "^6.6.0",
"@codemirror/state": "^6.5.4",
"@codemirror/view": "6.39.13",
"@codemirror/legacy-modes": "^6.5.3",
"@codemirror/lint": "^6.9.7",
"@codemirror/search": "^6.7.1",
"@codemirror/state": "^6.7.1",
"@codemirror/view": "6.43.9",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@lezer/highlight": "^1.2.3",
"@opencode-ai/sdk": "1.18.18",
"@pierre/diffs": "1.3.0-beta.6",
"@replit/codemirror-vim": "^6.3.0",
"@replit/codemirror-vim": "^6.4.0",
"@simplewebauthn/browser": "13.3.0",
"@tanstack/react-virtual": "3.14.5",
"@xenova/transformers": "^2.17.2",
@@ -355,8 +355,18 @@
"bun-pty@0.4.8": "bun-patches/bun-pty@0.4.8.patch",
},
"overrides": {
"@codemirror/language": "6.12.2",
"@codemirror/view": "6.39.13",
"@codemirror/autocomplete": "6.20.3",
"@codemirror/commands": "6.11.0",
"@codemirror/lang-html": "6.4.12",
"@codemirror/lang-javascript": "6.2.5",
"@codemirror/lang-markdown": "6.5.2",
"@codemirror/lang-yaml": "6.1.3",
"@codemirror/language": "6.12.4",
"@codemirror/legacy-modes": "6.5.3",
"@codemirror/lint": "6.9.7",
"@codemirror/search": "6.7.1",
"@codemirror/state": "6.7.1",
"@codemirror/view": "6.43.9",
},
"packages": {
"7zip-bin": ["7zip-bin@5.2.0", "", {}, "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A=="],
@@ -603,9 +613,9 @@
"@clack/prompts": ["@clack/prompts@1.1.0", "", { "dependencies": { "@clack/core": "1.1.0", "sisteransi": "^1.0.5" } }, "sha512-pkqbPGtohJAvm4Dphs2M8xE29ggupihHdy1x84HNojZuMtFsHiUlRvqD24tM2+XmI+61LlfNceM3Wr7U5QES5g=="],
"@codemirror/autocomplete": ["@codemirror/autocomplete@6.20.0", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0" } }, "sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg=="],
"@codemirror/autocomplete": ["@codemirror/autocomplete@6.20.3", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0" } }, "sha512-tlosUqb+3BbxCxZdu4tKeRghPFC+QM7q4X5YhKV2eCmPG+1r2F3f4AaSz5sCrFqUtX4Jh20VFTKecl16MgiV9g=="],
"@codemirror/commands": ["@codemirror/commands@6.10.2", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.4.0", "@codemirror/view": "^6.27.0", "@lezer/common": "^1.1.0" } }, "sha512-vvX1fsih9HledO1c9zdotZYUZnE4xV0m6i3m25s5DIfXofuprk6cRcLUZvSk3CASUbwjQX21tOGbkY2BH8TpnQ=="],
"@codemirror/commands": ["@codemirror/commands@6.11.0", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.7.0", "@codemirror/view": "^6.27.0", "@lezer/common": "^1.1.0" } }, "sha512-/K4Rl5BN0OtTiPWmJCdqODu38XnDMsDxKY5rgrPnCkutPTJf2wVbkoixLfealF5Kwse/s8P8M5jAiURiwSwnFA=="],
"@codemirror/lang-angular": ["@codemirror/lang-angular@0.1.4", "", { "dependencies": { "@codemirror/lang-html": "^6.0.0", "@codemirror/lang-javascript": "^6.1.2", "@codemirror/language": "^6.0.0", "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.3.3" } }, "sha512-oap+gsltb/fzdlTQWD6BFF4bSLKcDnlxDsLdePiJpCVNKWXSTAbiiQeYI3UmES+BLAdkmIC1WjyztC1pi/bX4g=="],
@@ -615,11 +625,11 @@
"@codemirror/lang-go": ["@codemirror/lang-go@6.0.1", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.6.0", "@codemirror/state": "^6.0.0", "@lezer/common": "^1.0.0", "@lezer/go": "^1.0.0" } }, "sha512-7fNvbyNylvqCphW9HD6WFnRpcDjr+KXX/FgqXy5H5ZS0eC5edDljukm/yNgYkwTsgp2busdod50AOTIy6Jikfg=="],
"@codemirror/lang-html": ["@codemirror/lang-html@6.4.11", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/lang-css": "^6.0.0", "@codemirror/lang-javascript": "^6.0.0", "@codemirror/language": "^6.4.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0", "@lezer/css": "^1.1.0", "@lezer/html": "^1.3.12" } }, "sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw=="],
"@codemirror/lang-html": ["@codemirror/lang-html@6.4.12", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/lang-css": "^6.0.0", "@codemirror/lang-javascript": "^6.0.0", "@codemirror/language": "^6.4.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0", "@lezer/css": "^1.1.0", "@lezer/html": "^1.3.12" } }, "sha512-pw2ReWKUqSkbvh76RAT4NYxiogRu+PWkR2ukAwO9uOgrm8uipkzjtKKtNpyeAQwHOqxEeSvAXZ6vr3AfyB9y/w=="],
"@codemirror/lang-java": ["@codemirror/lang-java@6.0.2", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@lezer/java": "^1.0.0" } }, "sha512-m5Nt1mQ/cznJY7tMfQTJchmrjdjQ71IDs+55d1GAa8DGaB8JXWsVCkVT284C3RTASaY43YknrK2X3hPO/J3MOQ=="],
"@codemirror/lang-javascript": ["@codemirror/lang-javascript@6.2.4", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.6.0", "@codemirror/lint": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0", "@lezer/javascript": "^1.0.0" } }, "sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA=="],
"@codemirror/lang-javascript": ["@codemirror/lang-javascript@6.2.5", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.6.0", "@codemirror/lint": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0", "@lezer/javascript": "^1.0.0" } }, "sha512-zD4e5mS+50htS7F+TYjBPsiIFGanfVqg4HyUz6WNFikgOPf2BgKlx+TQedI1w6n/IqRBVBbBWmGFdLB/7uxO4A=="],
"@codemirror/lang-jinja": ["@codemirror/lang-jinja@6.0.0", "", { "dependencies": { "@codemirror/lang-html": "^6.0.0", "@codemirror/language": "^6.0.0", "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.2.0", "@lezer/lr": "^1.4.0" } }, "sha512-47MFmRcR8UAxd8DReVgj7WJN1WSAMT7OJnewwugZM4XiHWkOjgJQqvEM1NpMj9ALMPyxmlziEI1opH9IaEvmaw=="],
@@ -629,7 +639,7 @@
"@codemirror/lang-liquid": ["@codemirror/lang-liquid@6.3.2", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/lang-html": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0", "@lezer/common": "^1.0.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.3.1" } }, "sha512-6PDVU3ZnfeYyz1at1E/ttorErZvZFXXt1OPhtfe1EZJ2V2iDFa0CwPqPgG5F7NXN0yONGoBogKmFAafKTqlwIw=="],
"@codemirror/lang-markdown": ["@codemirror/lang-markdown@6.5.0", "", { "dependencies": { "@codemirror/autocomplete": "^6.7.1", "@codemirror/lang-html": "^6.0.0", "@codemirror/language": "^6.3.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0", "@lezer/common": "^1.2.1", "@lezer/markdown": "^1.0.0" } }, "sha512-0K40bZ35jpHya6FriukbgaleaqzBLZfOh7HuzqbMxBXkbYMJDxfF39c23xOgxFezR+3G+tR2/Mup+Xk865OMvw=="],
"@codemirror/lang-markdown": ["@codemirror/lang-markdown@6.5.2", "", { "dependencies": { "@codemirror/autocomplete": "^6.7.1", "@codemirror/lang-html": "^6.0.0", "@codemirror/language": "^6.3.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0", "@lezer/common": "^1.2.1", "@lezer/markdown": "^1.0.0" } }, "sha512-AwBOdkWYuA//WcM0xO5PfHPUcmz/O2i5o0Nsg1U69SII/loCJlFI1Romd9xp2HYb1kYJRGZotyqRghuHH5n8Kw=="],
"@codemirror/lang-php": ["@codemirror/lang-php@6.0.2", "", { "dependencies": { "@codemirror/lang-html": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@lezer/common": "^1.0.0", "@lezer/php": "^1.0.0" } }, "sha512-ZKy2v1n8Fc8oEXj0Th0PUMXzQJ0AIR6TaZU+PbDHExFwdu+guzOA4jmCHS1Nz4vbFezwD7LyBdDnddSJeScMCA=="],
@@ -647,21 +657,21 @@
"@codemirror/lang-xml": ["@codemirror/lang-xml@6.1.0", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.4.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0", "@lezer/common": "^1.0.0", "@lezer/xml": "^1.0.0" } }, "sha512-3z0blhicHLfwi2UgkZYRPioSgVTo9PV5GP5ducFH6FaHy0IAJRg+ixj5gTR1gnT/glAIC8xv4w2VL1LoZfs+Jg=="],
"@codemirror/lang-yaml": ["@codemirror/lang-yaml@6.1.2", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.2.0", "@lezer/lr": "^1.0.0", "@lezer/yaml": "^1.0.0" } }, "sha512-dxrfG8w5Ce/QbT7YID7mWZFKhdhsaTNOYjOkSIMt1qmC4VQnXSDSYVHHHn8k6kJUfIhtLo8t1JJgltlxWdsITw=="],
"@codemirror/lang-yaml": ["@codemirror/lang-yaml@6.1.3", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.2.0", "@lezer/lr": "^1.0.0", "@lezer/yaml": "^1.0.0" } }, "sha512-AZ8DJBuXGVHybpBQhmZtgew5//4hv3tdkXnr3vDmOUMJRuB6vn/uuwtmTOTlqEaQFg3hQSVeA90NmvIQyUV6FQ=="],
"@codemirror/language": ["@codemirror/language@6.12.2", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.23.0", "@lezer/common": "^1.5.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0", "style-mod": "^4.0.0" } }, "sha512-jEPmz2nGGDxhRTg3lTpzmIyGKxz3Gp3SJES4b0nAuE5SWQoKdT5GoQ69cwMmFd+wvFUhYirtDTr0/DRHpQAyWg=="],
"@codemirror/language": ["@codemirror/language@6.12.4", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.23.0", "@lezer/common": "^1.5.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0", "style-mod": "^4.0.0" } }, "sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A=="],
"@codemirror/language-data": ["@codemirror/language-data@6.5.2", "", { "dependencies": { "@codemirror/lang-angular": "^0.1.0", "@codemirror/lang-cpp": "^6.0.0", "@codemirror/lang-css": "^6.0.0", "@codemirror/lang-go": "^6.0.0", "@codemirror/lang-html": "^6.0.0", "@codemirror/lang-java": "^6.0.0", "@codemirror/lang-javascript": "^6.0.0", "@codemirror/lang-jinja": "^6.0.0", "@codemirror/lang-json": "^6.0.0", "@codemirror/lang-less": "^6.0.0", "@codemirror/lang-liquid": "^6.0.0", "@codemirror/lang-markdown": "^6.0.0", "@codemirror/lang-php": "^6.0.0", "@codemirror/lang-python": "^6.0.0", "@codemirror/lang-rust": "^6.0.0", "@codemirror/lang-sass": "^6.0.0", "@codemirror/lang-sql": "^6.0.0", "@codemirror/lang-vue": "^0.1.1", "@codemirror/lang-wast": "^6.0.0", "@codemirror/lang-xml": "^6.0.0", "@codemirror/lang-yaml": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/legacy-modes": "^6.4.0" } }, "sha512-CPkWBKrNS8stYbEU5kwBwTf3JB1kghlbh4FSAwzGW2TEscdeHHH4FGysREW86Mqnj3Qn09s0/6Ea/TutmoTobg=="],
"@codemirror/legacy-modes": ["@codemirror/legacy-modes@6.5.2", "", { "dependencies": { "@codemirror/language": "^6.0.0" } }, "sha512-/jJbwSTazlQEDOQw2FJ8LEEKVS72pU0lx6oM54kGpL8t/NJ2Jda3CZ4pcltiKTdqYSRk3ug1B3pil1gsjA6+8Q=="],
"@codemirror/legacy-modes": ["@codemirror/legacy-modes@6.5.3", "", { "dependencies": { "@codemirror/language": "^6.0.0" } }, "sha512-xCsmIzH78MyWkib9jlPaaun57XNkfbMIhagfaZVd0iLTqlpw3jXaIcbZm72MTmmn64eTZpBVNjbyYh+QXnxRsg=="],
"@codemirror/lint": ["@codemirror/lint@6.9.4", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.35.0", "crelt": "^1.0.5" } }, "sha512-ABc9vJ8DEmvOWuH26P3i8FpMWPQkduD9Rvba5iwb6O3hxASgclm3T3krGo8NASXkHCidz6b++LWlzWIUfEPSWw=="],
"@codemirror/lint": ["@codemirror/lint@6.9.7", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.42.0", "crelt": "^1.0.5" } }, "sha512-28/+iWLYxKxsvGYhSYL7zaCZqLz5+FFFDq9tVsvGv9kv8RY4fFAchJ5WX9M3YrrRlTIsECjsXPqeNgnSmNP2dg=="],
"@codemirror/search": ["@codemirror/search@6.6.0", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.37.0", "crelt": "^1.0.5" } }, "sha512-koFuNXcDvyyotWcgOnZGmY7LZqEOXZaaxD/j6n18TCLx2/9HieZJ5H6hs1g8FiRxBD0DNfs0nXn17g872RmYdw=="],
"@codemirror/search": ["@codemirror/search@6.7.1", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.37.0", "crelt": "^1.0.5" } }, "sha512-uMe5UO6PamJtSHrXhhHOzSX3ReWtiJrva6GnPMwSOrZtiExb5X5eExhr2OUZQVvdxPsKpY3Ro2mFbQadpPWmHA=="],
"@codemirror/state": ["@codemirror/state@6.5.4", "", { "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } }, "sha512-8y7xqG/hpB53l25CIoit9/ngxdfoG+fx+V3SHBrinnhOtLvKHRyAJJuHzkWrR4YXXLX8eXBsejgAAxHUOdW1yw=="],
"@codemirror/state": ["@codemirror/state@6.7.1", "", { "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } }, "sha512-9QzNDgE4EYDnAHfrTlR2lwiPciiOymLtwKK+8yHQzCc7GXhAP9xdEbEJFy2IWB1j9UGUl9BsgMmTo/ImA02T7A=="],
"@codemirror/view": ["@codemirror/view@6.39.13", "", { "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-QBO8ZsgJLCbI28KdY0/oDy5NQLqOQVZCozBknxc2/7L98V+TVYFHnfaCsnGh1U+alpd2LOkStVwYY7nW2R1xbw=="],
"@codemirror/view": ["@codemirror/view@6.43.9", "", { "dependencies": { "@codemirror/state": "^6.7.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-sTuUzTpPMFebRhg6dawChoKKgndIwfjmJgKVxBefPElcU2NwQ6AFroupk0SFqEerQyZOGRfDNnSN8Dw/lMAsXw=="],
"@date-fns/tz": ["@date-fns/tz@1.4.1", "", {}, "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA=="],
@@ -1191,7 +1201,9 @@
"@remixicon/react": ["@remixicon/react@4.9.0", "", { "peerDependencies": { "react": ">=18.2.0" } }, "sha512-5/jLDD4DtKxH2B4QVXTobvV1C2uL8ab9D5yAYNtFt+w80O0Ys1xFOrspqROL3fjrZi+7ElFUWE37hBfaAl6U+Q=="],
"@replit/codemirror-vim": ["@replit/codemirror-vim@6.3.0", "", { "peerDependencies": { "@codemirror/commands": "6.x.x", "@codemirror/language": "6.x.x", "@codemirror/search": "6.x.x", "@codemirror/state": "6.x.x", "@codemirror/view": "6.x.x" } }, "sha512-aTx931ULAMuJx6xLf7KQDOL7CxD+Sa05FktTDrtLaSy53uj01ll3Zf17JdKsriER248oS55GBzg0CfCTjEneAQ=="],
"@replit/codemirror-vim": ["@replit/codemirror-vim@6.4.0", "", { "dependencies": { "@replit/codemirror-vim-core": "^0.1.0" }, "peerDependencies": { "@codemirror/commands": "6.x.x", "@codemirror/language": "6.x.x", "@codemirror/search": "6.x.x", "@codemirror/state": "6.x.x", "@codemirror/view": "6.x.x" } }, "sha512-t9UMDNhkmeAkl0uRbiJVotv97bGD6mf4GyJJoEbrjUqa/Pov0s9eL+4AaI0Xto3OGri17i9qwIpT0JbVlVXt8A=="],
"@replit/codemirror-vim-core": ["@replit/codemirror-vim-core@0.1.0", "", {}, "sha512-1i6EBKpcNfDKvTmTh6N6g9lL6udD5t+uFNh4JCqozRnVlvUGOps7h/QzS2ne4zcvPUjvApKpmcP7Grc3fNbZiQ=="],
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.3", "", {}, "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q=="],
+23 -13
View File
@@ -91,25 +91,25 @@
},
"dependencies": {
"@base-ui/react": "^1.4.0",
"@codemirror/autocomplete": "^6.20.0",
"@codemirror/commands": "^6.10.1",
"@codemirror/autocomplete": "^6.20.3",
"@codemirror/commands": "^6.11.0",
"@codemirror/lang-cpp": "^6.0.3",
"@codemirror/lang-css": "^6.3.1",
"@codemirror/lang-go": "^6.0.1",
"@codemirror/lang-html": "^6.4.11",
"@codemirror/lang-javascript": "^6.2.4",
"@codemirror/lang-html": "^6.4.12",
"@codemirror/lang-javascript": "^6.2.5",
"@codemirror/lang-json": "^6.0.2",
"@codemirror/lang-markdown": "^6.5.0",
"@codemirror/lang-markdown": "^6.5.2",
"@codemirror/lang-python": "^6.2.1",
"@codemirror/lang-rust": "^6.0.2",
"@codemirror/lang-sql": "^6.10.0",
"@codemirror/lang-xml": "^6.1.0",
"@codemirror/lang-yaml": "^6.1.2",
"@codemirror/language": "6.12.2",
"@codemirror/lint": "^6.9.2",
"@codemirror/search": "^6.6.0",
"@codemirror/state": "^6.5.4",
"@codemirror/view": "6.39.13",
"@codemirror/lang-yaml": "^6.1.3",
"@codemirror/language": "6.12.4",
"@codemirror/lint": "^6.9.7",
"@codemirror/search": "^6.7.1",
"@codemirror/state": "^6.7.1",
"@codemirror/view": "6.43.9",
"@heroui/scroll-shadow": "^2.3.18",
"@heroui/system": "^2.4.23",
"@heroui/theme": "^2.4.23",
@@ -148,8 +148,18 @@
"zustand": "^5.0.8"
},
"overrides": {
"@codemirror/language": "6.12.2",
"@codemirror/view": "6.39.13"
"@codemirror/autocomplete": "6.20.3",
"@codemirror/commands": "6.11.0",
"@codemirror/lang-html": "6.4.12",
"@codemirror/lang-javascript": "6.2.5",
"@codemirror/lang-markdown": "6.5.2",
"@codemirror/lang-yaml": "6.1.3",
"@codemirror/language": "6.12.4",
"@codemirror/legacy-modes": "6.5.3",
"@codemirror/lint": "6.9.7",
"@codemirror/search": "6.7.1",
"@codemirror/state": "6.7.1",
"@codemirror/view": "6.43.9"
},
"devDependencies": {
"@clack/prompts": "^1.1.0",
+13 -13
View File
@@ -19,34 +19,34 @@
"@capacitor/keyboard": "^8.0.0",
"@capacitor/push-notifications": "^8.1.1",
"@capacitor/status-bar": "^8.0.0",
"@codemirror/autocomplete": "^6.20.0",
"@codemirror/commands": "^6.10.1",
"@codemirror/autocomplete": "^6.20.3",
"@codemirror/commands": "^6.11.0",
"@codemirror/lang-cpp": "^6.0.3",
"@codemirror/lang-css": "^6.3.1",
"@codemirror/lang-go": "^6.0.1",
"@codemirror/lang-html": "^6.4.11",
"@codemirror/lang-javascript": "^6.2.4",
"@codemirror/lang-html": "^6.4.12",
"@codemirror/lang-javascript": "^6.2.5",
"@codemirror/lang-json": "^6.0.2",
"@codemirror/lang-markdown": "^6.5.0",
"@codemirror/lang-markdown": "^6.5.2",
"@codemirror/lang-python": "^6.2.1",
"@codemirror/lang-rust": "^6.0.2",
"@codemirror/lang-sql": "^6.10.0",
"@codemirror/lang-xml": "^6.1.0",
"@codemirror/lang-yaml": "^6.1.2",
"@codemirror/language": "6.12.2",
"@codemirror/lang-yaml": "^6.1.3",
"@codemirror/language": "6.12.4",
"@codemirror/language-data": "^6.5.2",
"@codemirror/legacy-modes": "^6.5.2",
"@codemirror/lint": "^6.9.2",
"@codemirror/search": "^6.6.0",
"@codemirror/state": "^6.5.4",
"@codemirror/view": "6.39.13",
"@codemirror/legacy-modes": "^6.5.3",
"@codemirror/lint": "^6.9.7",
"@codemirror/search": "^6.7.1",
"@codemirror/state": "^6.7.1",
"@codemirror/view": "6.43.9",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@lezer/highlight": "^1.2.3",
"@opencode-ai/sdk": "1.18.18",
"@pierre/diffs": "1.3.0-beta.6",
"@replit/codemirror-vim": "^6.3.0",
"@replit/codemirror-vim": "^6.4.0",
"@simplewebauthn/browser": "13.3.0",
"@tanstack/react-virtual": "3.14.5",
"@xenova/transformers": "^2.17.2",
@@ -61,20 +61,46 @@ question of design, not of feasibility.
Selection rendering: every device runs CodeMirror's `drawSelection()` — it
keeps typing on the drawn-selection code path, and removing it makes
CodeMirror enforce cursor association on the native selection, which iOS
answers with severe input lag. Every device also layers
`composerNativeSelectionExtension` (`editor/theme.ts`) on top: it re-shows
answers with severe input lag. **That much is not platform-specific and must
not be undone.** What differs is who paints the selection, and
`composerSelectionExtension` (`editor/theme.ts`) picks that per platform.
When CodeMirror 6.43.9's iOS predicate does not match,
`composerNativeSelectionExtension` layers over `drawSelection()`: it re-shows
the native selection, and — only while a range is selected — the native caret,
hiding the painted layers those replace. The native selection is the one that
shows for two reasons: the painted layer sits behind the content, so tokens
with their own background (inline code, fences) cover it completely; and
iOS's selection drag handles attach to the visible native selection and take
their colour from the caret, so a transparent caret means invisible handles.
The range-only caret scoping is load-bearing — a native caret visible while
typing makes WebKit re-render its caret UI after every keystroke, felt as
severe input lag. The selection tint comes from `--primary`, not the selection
token:
themes define `--interactive-selection` with its own alpha, so a translucent
mix of it is nearly invisible.
with their own background (inline code, fences) cover it completely; and the
platform's selection drag handles attach to the visible native selection and
take their colour from the caret, so a transparent caret means invisible
handles. The range-only caret scoping is load-bearing — a native caret visible
while typing makes the browser re-render its caret UI after every keystroke,
felt as severe input lag.
When CodeMirror 6.43.9's exact iOS predicate matches,
`composerIOSSelectionExtension` leaves selection-handle geometry and appearance
to CodeMirror. CodeMirror puts the handles in `.cm-selectionLayer`, normally at
`z-index: -1`; the extension raises that layer above the content so opaque
token backgrounds cannot cover them, and leaves it transparent to touch.
The handle dots extend 8px past their range; matching scroller padding and
negative margin expand the clip area without moving the text or changing the
composer height. iOS still paints its taller system selection overlay even
when CSS makes `::selection` transparent. The extension therefore suppresses
CodeMirror's synthetic selection rectangles on iOS while leaving its handles,
cursor path and `nativeSelectionHidden` facet active. Otherwise the grey system
highlight and themed rectangle overlap with visibly different heights.
Do not add a second custom layer or custom handles here: overlapping translucent
rectangles make selection darker at their seams and imitated handles drift from
the geometry WebKit actually manipulates. What iOS avoids is installing the
native-selection workaround above: explicitly restoring native paint and caret
makes WebKit re-measure them after every decoration redraw, and the composer
rebuilds every decoration on every keystroke. That cost is felt worst during
IME composition.
The non-iOS native selection tint comes from `--primary`, not the selection
token: themes define `--interactive-selection` with its own alpha, so mixing it
with transparent again is nearly invisible. The iOS system overlay owns its
visible selection fill.
`composerLanguage.ts` retokenizes the whole document on every change. The
composer holds a prompt, not a source file: it is short enough that a full pass
@@ -36,7 +36,7 @@ import { cn } from '@/lib/utils';
import type { ComposerLanguageContext } from '../language/tokenize';
import { composerLanguage, setLanguageContext } from './composerLanguage';
import type { ComposerEditorViewStore } from './viewStore';
import { composerEditorTheme, composerNativeSelectionExtension } from './theme';
import { composerEditorTheme, composerSelectionExtension } from './theme';
import { handleComposerHostMouseDown } from './hostMouseDown';
export interface ComposerSelection {
@@ -234,14 +234,13 @@ export const ComposerEditor = React.forwardRef<ComposerEditorHandle, ComposerEdi
doc: handlersRef.current.value,
extensions: [
history(),
// `drawSelection()` must stay even though the native
// selection is what actually shows (see the theme's
// comment on `composerNativeSelectionExtension`):
// removing it makes CodeMirror enforce cursor
// association on the native selection, which iOS
// answers with severe input lag.
// `drawSelection()` must stay on every platform.
// `composerSelectionExtension()` changes only who
// paints the selection; removing `drawSelection()`
// makes CodeMirror enforce cursor association on the
// native selection, which iOS answers with severe lag.
drawSelection(),
composerNativeSelectionExtension,
composerSelectionExtension(),
EditorView.lineWrapping,
// Highest precedence: the composer's own keys must win
// over CodeMirror's defaults (Enter sends, ArrowUp
@@ -1,16 +1,29 @@
import { describe, expect, test } from 'bun:test';
import { EditorState } from '@codemirror/state';
import { EditorState, type Extension } from '@codemirror/state';
import {
COMPOSER_EDITOR_THEME_SPEC,
IOS_SELECTION_THEME_SPEC,
NATIVE_SELECTION_THEME_SPEC,
composerEditorTheme,
composerIOSSelectionExtension,
composerNativeSelectionExtension,
composerSelectionExtension,
isCodeMirrorIOSNavigator,
} from '../theme';
const selectors = Object.keys(COMPOSER_EDITOR_THEME_SPEC);
const declarations = JSON.stringify(COMPOSER_EDITOR_THEME_SPEC);
function installationError(extension: Extension): string | null {
try {
EditorState.create({ extensions: [extension] });
return null;
} catch (error) {
return String(error);
}
}
describe('composerEditorTheme', () => {
/**
* EditorView.theme compiles its selectors when this module is imported and
@@ -20,13 +33,7 @@ describe('composerEditorTheme', () => {
* surfaces only in the running app, where it takes the composer down.
*/
test('its selectors compile and the theme can be installed', () => {
let failure: unknown = null;
try {
EditorState.create({ extensions: [composerEditorTheme] });
} catch (error) {
failure = error;
}
expect(failure).toBeNull();
expect(installationError(composerEditorTheme)).toBeNull();
});
/**
@@ -101,6 +108,10 @@ describe('composerEditorTheme', () => {
expect(rule.background.includes('transparent')).toBe(true);
}
});
test('the common theme does not re-show the native selection', () => {
expect(selectors.some((selector) => selector.includes('::selection'))).toBe(false);
});
});
describe('composerNativeSelectionTheme', () => {
@@ -108,21 +119,16 @@ describe('composerNativeSelectionTheme', () => {
const nativeDeclarations = JSON.stringify(NATIVE_SELECTION_THEME_SPEC);
/**
* Every device layers this over `drawSelection()`: the native selection
* paints over token backgrounds (the painted layer is hidden behind them)
* and iOS attaches its selection handles to it. `drawSelection()` must
* NOT be removed for that: without it CodeMirror starts enforcing cursor
* association on the native selection while typing in wrapped text, and
* iOS answers those programmatic selection moves with severe input lag.
* Every device except iOS layers this over `drawSelection()`: the native
* selection paints over token backgrounds (the painted layer is hidden
* behind them) and the platform attaches its selection handles to it.
* `drawSelection()` must NOT be removed for that: without it CodeMirror
* starts enforcing cursor association on the native selection while typing
* in wrapped text, and iOS answers those programmatic selection moves with
* severe input lag.
*/
test('it compiles and can be installed', () => {
let failure: unknown = null;
try {
EditorState.create({ extensions: [composerNativeSelectionExtension] });
} catch (error) {
failure = error;
}
expect(failure).toBeNull();
expect(installationError(composerNativeSelectionExtension)).toBeNull();
});
/**
@@ -150,9 +156,9 @@ describe('composerNativeSelectionTheme', () => {
});
/**
* iOS colours its selection drag handles from the caret colour. With
* `drawSelection()`'s `caret-color: transparent !important` in effect the
* handles are drawn — invisibly. The native caret must come back with
* A platform showing native handles colours them from the caret colour.
* With `drawSelection()`'s `caret-color: transparent !important` in effect
* the handles are drawn — invisibly. The native caret must come back with
* enough weight to win, and the drawn cursor layer must go so there are
* not two carets.
*
@@ -195,3 +201,106 @@ describe('composerNativeSelectionTheme', () => {
expect(tokens.filter((token) => /[A-Z]/.test(token))).toEqual([]);
});
});
describe('composerIOSSelectionExtension', () => {
const layerRule = IOS_SELECTION_THEME_SPEC['& .cm-scroller > .cm-selectionLayer'];
const scrollerRule = IOS_SELECTION_THEME_SPEC['& .cm-scroller'];
const selectionBackgroundRule = IOS_SELECTION_THEME_SPEC['& .cm-selectionBackground'];
test('it compiles and can be installed', () => {
expect(installationError(composerIOSSelectionExtension)).toBeNull();
});
/**
* CodeMirror renders its selection layer at `z-index: -1`, behind the
* text. Inline code and code fences have opaque backgrounds and otherwise
* cover both the selection and the iOS handles. The base value is inline,
* so raising it without `!important` silently does nothing.
*/
test('CodeMirror selection and handles are raised above token backgrounds', () => {
expect(layerRule.zIndex).toBe('100 !important');
});
/**
* The layer now sits over the content and would intercept taps and drags
* by default. It only paints; CodeMirror/WebKit still own the gestures.
*/
test('the layer does not intercept touch', () => {
expect(layerRule.pointerEvents).toBe('none');
});
/**
* A higher z-index cannot escape overflow clipping. CodeMirror's dots
* extend 8px past the range, so the scroller needs that much internal room;
* the matching negative margin keeps the text and composer height fixed.
*/
test('the scroller reserves unclipped room for both handles', () => {
expect(scrollerRule.paddingBlock).toBe('8px');
expect(scrollerRule.marginBlock).toBe('-8px');
});
test('the CodeMirror fill does not stack over the iOS system highlight', () => {
expect(selectionBackgroundRule.background).toBe('transparent !important');
});
/**
* A second custom layer was visually indistinguishable from duplicate
* native selection UI. iOS must only reposition the one layer that
* CodeMirror already uses for both selection rectangles and handles.
*/
test('it does not add a second selection implementation', () => {
expect(Object.keys(IOS_SELECTION_THEME_SPEC)).toEqual([
'& .cm-scroller',
'& .cm-scroller > .cm-selectionLayer',
'& .cm-selectionBackground',
]);
});
});
describe('composerSelectionExtension', () => {
/**
* The split is the point: iOS is the only platform that pays for a visible
* native selection during composition, and CodeMirror 6.43.9 draws its
* handles. Collapsing the two branches into one would
* either restore the latency on iOS or leave every other platform without
* discoverable range selection.
*/
test('the CodeMirror iOS path uses its handles; other platforms keep native selection', () => {
expect(composerSelectionExtension(true)).toBe(composerIOSSelectionExtension);
expect(composerSelectionExtension(false)).toBe(composerNativeSelectionExtension);
});
/**
* The composer may remove the native fallback only when CodeMirror's own
* browser predicate enables its replacement handles. This deliberately
* includes CodeMirror's vendor and touch thresholds rather than using a
* broader application-level iOS heuristic.
*/
test('the platform predicate matches CodeMirror 6.43.9', () => {
expect(isCodeMirrorIOSNavigator(
'Mozilla/5.0 (iPhone; CPU iPhone OS 18_6) Mobile/15E148 Safari/604.1',
'Apple Computer, Inc.',
5,
)).toBe(true);
expect(isCodeMirrorIOSNavigator(
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15)',
'Apple Computer, Inc.',
5,
)).toBe(true);
expect(isCodeMirrorIOSNavigator(
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15)',
'Google Inc.',
5,
)).toBe(false);
expect(isCodeMirrorIOSNavigator(
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15)',
'Apple Computer, Inc.',
0,
)).toBe(false);
expect(isCodeMirrorIOSNavigator(
'Mozilla/5.0 (Windows NT 10.0; Trident/7.0; rv:11.0)',
'Apple Computer, Inc.',
5,
)).toBe(false);
});
});
@@ -5,6 +5,7 @@
* language layer emits, so the composer and the message list stay in step.
*/
import type { Extension } from '@codemirror/state';
import { EditorView } from '@codemirror/view';
/**
@@ -78,23 +79,16 @@ export const COMPOSER_EDITOR_THEME_SPEC = {
'&.cm-editor.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground': {
background: 'color-mix(in srgb, var(--interactive-selection) 55%, transparent)',
},
// The native selection still shows through in places CodeMirror does not
// draw over, such as the placeholder. Same colour as the native-selection
// theme below, for the same reason: the selection token carries its own
// alpha and reads as nearly invisible when mixed down again.
'& ::selection': {
background: 'color-mix(in srgb, var(--primary) 25%, transparent)',
},
};
export const composerEditorTheme = EditorView.theme(COMPOSER_EDITOR_THEME_SPEC);
/**
* Every device keeps `drawSelection()` but shows the NATIVE selection through
* it, for two independent reasons:
* Outside CodeMirror's iOS branch, devices keep `drawSelection()` but show the
* NATIVE selection through it, for two independent reasons:
*
* - iOS attaches its selection handles (the draggable pins after a
* double-tap) to the *visible* native selection, and `drawSelection()`
* - Their selection drag handles (the draggable pins after a double-tap)
* attach to the *visible* native selection, and `drawSelection()`
* hides it with `.cm-line ::selection { background: transparent
* !important }`, so the handles never appear and range selection is
* undiscoverable.
@@ -103,12 +97,15 @@ export const composerEditorTheme = EditorView.theme(COMPOSER_EDITOR_THEME_SPEC);
* the selection is invisible inside those spans. The native selection
* paints over element backgrounds.
*
* Dropping `drawSelection()` entirely is NOT an option: without it CodeMirror
* clears the `nativeSelectionHidden` facet and starts enforcing cursor
* association on the native selection while typing in wrapped text —
* programmatic selection moves that iOS answers with severe input lag (each
* one also resets the keyboard's autocorrect context). Typing must stay on
* the drawn-selection code path; only the paint changes.
* Dropping `drawSelection()` entirely is NOT an option, on any platform:
* without it CodeMirror clears the `nativeSelectionHidden` facet and starts
* enforcing cursor association on the native selection while typing in
* wrapped text — programmatic selection moves that iOS answers with severe
* input lag (each one also resets the keyboard's autocorrect context). Typing
* must stay on the drawn-selection code path; only the paint changes.
*
* CodeMirror's iOS branch does NOT use this arrangement —
* `composerIOSSelectionExtension` below explains why.
*
* Both rules below fight `drawSelection()`'s own `Prec.highest` theme, so
* they carry `!important` and one class more specificity
@@ -155,14 +152,103 @@ export const NATIVE_SELECTION_THEME_SPEC = {
const composerNativeSelectionTheme = EditorView.theme(NATIVE_SELECTION_THEME_SPEC);
/**
* The native-selection arrangement, installed on every device: the theme
* above plus the `.oc-native-range` marker class that scopes its caret rules
* to the moments a range is actually selected. `editorAttributes`
* The native-selection arrangement, installed outside CodeMirror's iOS branch:
* the theme above plus the `.oc-native-range` marker class that scopes its
* caret rules to the moments a range is actually selected. `editorAttributes`
* re-evaluates on every update, so the class follows the selection with no
* listener of its own.
*/
export const composerNativeSelectionExtension = [
export const composerNativeSelectionExtension: Extension = [
composerNativeSelectionTheme,
EditorView.editorAttributes.of((view) =>
view.state.selection.main.empty ? null : { class: 'oc-native-range' }),
];
/**
* When its iOS predicate matches, CodeMirror 6.43.9 draws the range handles
* into the same layer as the selection, so CodeMirror owns both their geometry
* and appearance.
*
* That layer normally renders at `z-index: -1`, behind the content. Inline
* code and code fences have opaque backgrounds and would cover both the tint
* and handles. Raising the one existing layer fixes that without introducing
* a second set of rectangles or trying to imitate WebKit's controls. The
* layer remains transparent to touch so WebKit receives selection gestures.
*
* What iOS avoids is the native-selection workaround above: explicitly
* restoring the native highlight and caret makes WebKit re-measure and repaint
* that UI after every decoration redraw. `composerLanguage.ts` rebuilds the
* whole decoration set on every keystroke, so the cost is felt worst during
* IME composition where each intermediate replacement pays for it. WebKit's
* unavoidable system selection overlay remains the only visible fill.
*/
export const IOS_SELECTION_THEME_SPEC = {
// The handles extend 8px above/below their range. The scroller clips them
// at its own edge even when the layer has a high z-index, so reserve that
// room inside the clipping box and pull the box outward by the same amount.
// Text and composer height stay where they were; only the clip area grows.
'& .cm-scroller': {
marginBlock: '-8px',
paddingBlock: '8px',
},
'& .cm-scroller > .cm-selectionLayer': {
// CodeMirror writes `z-index: -1` inline. `!important` is intentional:
// without it token backgrounds cover the selection and its handles.
zIndex: '100 !important',
pointerEvents: 'none',
},
// iOS keeps showing its taller system selection overlay even when
// ::selection is transparent. Painting CodeMirror's themed rectangles as
// well produces two visibly misaligned fills, so only the synthetic
// background is suppressed. The handles in this layer remain visible.
'& .cm-selectionBackground': {
background: 'transparent !important',
},
};
export const composerIOSSelectionExtension: Extension =
EditorView.theme(IOS_SELECTION_THEME_SPEC);
/**
* Which selection paint the composer installs. The split is the platform's,
* not a preference: iOS is the one place where restoring native selection
* paint and caret costs measurable input latency, and the only place
* CodeMirror supplies replacement drag handles.
*
* The caller can pass the policy, so the choice stays testable and is made
* once per editor rather than once per module load.
*/
export function composerSelectionExtension(
useCodeMirrorIOSHandles: boolean = usesCodeMirrorIOSSelectionHandles(),
): Extension {
return useCodeMirrorIOSHandles
? composerIOSSelectionExtension
: composerNativeSelectionExtension;
}
/**
* Mirrors @codemirror/view 6.43.9's iOS predicate. This branch may only rely
* on the drawn handles when CodeMirror itself will create them; a broader iOS
* heuristic could remove the native fallback without installing a replacement.
*/
export function isCodeMirrorIOSNavigator(
userAgent: string,
vendor: string,
maxTouchPoints: number,
): boolean {
const isIE = /Edge\/(\d+)/.test(userAgent)
|| /MSIE \d/.test(userAgent)
|| /Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.test(userAgent);
if (isIE || !/Apple Computer/.test(vendor)) return false;
return /Mobile\/\w+/.test(userAgent) || maxTouchPoints > 2;
}
function usesCodeMirrorIOSSelectionHandles(): boolean {
const nav = globalThis.navigator;
if (!nav) return false;
return isCodeMirrorIOSNavigator(
nav.userAgent || '',
nav.vendor || '',
nav.maxTouchPoints ?? 0,
);
}