Compare commits

..
Author SHA1 Message Date
ys-ll 714fda4575 Bump version to 6.0.0-uniterm.1 2026-09-20 19:20:47 +08:00
ys-ll d07813da37 Add cell color override API for text recoloring
Add setCellColorOverrides/clearCellColorOverrides to the terminal API,
backed by a per-line sparse override store consumed by the DOM renderer's
row factory. Covered cells render their original text in the given color,
with override equality participating in span merging so runs break only at
color boundaries. Overrides are keyed by absolute buffer line of the active
buffer; refresh is limited to the affected viewport rows.

This gives embedders a cheap way to recolor terminal text (e.g. keyword
highlighting) without decoration objects, which fragment text spans and
carry per-cell lookup cost in the DOM renderer, and which are force-hidden
while the alternate buffer is active.
2026-09-20 19:20:47 +08:00
Daniel Imms f447274f43 Merge pull request #5463 from Tyriar/tyriar/v6
v6.0.0, bump addon versions, publish unicode graphemes
2025-12-22 05:48:49 -08:00
Daniel Imms f68d1e5d58 Skip sync output tests for now 2025-12-21 13:03:25 -08:00
Daniel Imms 30691e8b46 v6.0.0, bump addon versions, publish unicode graphemes
Part of #5147
2025-12-21 12:49:37 -08:00
Daniel Imms 5fa632518e Merge pull request #5462 from Tyriar/tyriar/deprecated
Remove deprecated windowsMode and fastScrollModifier
2025-12-21 05:06:56 -08:00
Daniel Imms 3571d1f51a Remove windowsMode in favor of windowsPty 2025-12-21 05:01:15 -08:00
Daniel Imms ebc0d83d8e Remove deprecated/unused fastScrollModifier 2025-12-21 04:58:45 -08:00
Daniel Imms d1c50c14e3 Merge pull request #5461 from Tyriar/tyriar/glob
Remove need for glob module
2025-12-20 08:44:33 -08:00
Daniel Imms 92b43bdf9b Remove only 2025-12-20 08:33:56 -08:00
Daniel Imms a9d3ca19f4 Merge pull request #5449 from iSuslov/exports/esm-headless-node
Configure "exports" field in package.json [headless]
2025-12-20 07:47:58 -08:00
Daniel Imms cb4d1ef2c0 Merge pull request #5453 from chrislloyd/feat/synchronized-output
Add synchronized output support (DEC mode 2026)
2025-12-20 07:44:32 -08:00
Daniel Imms 34fd0dcf53 Remove need for glob module 2025-12-20 07:44:24 -08:00
Daniel Imms 7335f15943 Merge pull request #5460 from Tyriar/tyriar/test-results
Git ignore test-results/
2025-12-20 07:37:43 -08:00
Daniel Imms 5e9cd6208f Git ignore test-results/ 2025-12-20 07:37:20 -08:00
Daniel Imms 6733e13a44 Invert initial assertion 2025-12-20 07:33:18 -08:00
Daniel Imms ad07f2cf76 Revert last frame request cache clearing 2025-12-20 07:23:20 -08:00
Daniel Imms 5396243bce Merge pull request #5450 from iSuslov/fix-esm-path-addon-unicode-graphemes
Fix wrong export path for ESM target in [addon-unicode-graphemes]
2025-12-20 07:20:08 -08:00
Daniel Imms 12c4f9536e Use underline cursor style to ensure cursor color isn't reported 2025-12-20 07:13:41 -08:00
Daniel Imms e9ba31f1b6 Explicitly set background color in playwright tests 2025-12-20 06:40:16 -08:00
Daniel Imms ed0ab5f9e4 Add missing property to headless API test assertion 2025-12-20 06:39:27 -08:00
Daniel Imms 0741f8be5c Merge pull request #5452 from iSuslov/upgrade-glob-version-fix-tsc-build
Fix tsc build - upgrade glob version
2025-12-20 06:37:06 -08:00
Daniel Imms 2edd121f1b Add node about serialize addon support 2025-12-20 06:17:31 -08:00
Daniel Imms 1eee5cdf18 Add synchronizedOutput mode to headless terminal object 2025-12-20 06:14:45 -08:00
Daniel Imms 20c1674d00 Update package-lock.json 2025-12-20 06:12:41 -08:00
Daniel Imms 32553fff06 Add synchronizedOutputMode to xterm-headless.d.ts 2025-12-20 06:05:36 -08:00
Daniel Imms 832aa54478 Fix colors and clear cached frames to pass tests 2025-12-20 06:04:11 -08:00
Daniel Imms 7f50b56ae4 Fix one playwright test 2025-12-10 08:44:13 -08:00
Daniel Imms bd361af105 Fix lint 2025-12-10 07:59:52 -08:00
Daniel Imms 9ea059de6f Move SynchronizedOutputHandler to bottom of file 2025-12-10 07:50:59 -08:00
Daniel Imms 7cdf3f9dd7 Prefer const enum so the number is inlined 2025-12-10 07:50:28 -08:00
Daniel Imms 7f81df5a3a Add BSU and ESU to VT tab in demo 2025-12-10 07:49:07 -08:00
Chris LloydandClaude af5dbfcd2c Add DECRQM support for synchronized output mode
Applications can now detect synchronized output support by querying
CSI ? 2026 $ p and receiving CSI ? 2026 ; <mode> $ y response.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-09 08:26:04 -08:00
Chris LloydandClaude 5d0eae8576 Fix spec URL to use master branch
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-09 08:23:37 -08:00
Chris LloydandClaude 6b63c5f260 Address PR review feedback for synchronized output
- Remove synchronizedOutputTimeout public API, hardcode 1s timeout
- Extract SynchronizedOutputHandler class for cleaner code
- Update spec URL to contour-terminal/vt-extensions
- Remove unnecessary comment in InputHandler
- Delete unit tests, add integration tests in SharedRendererTests
- Fix whitespace issue

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-09 07:58:31 -08:00
Chris LloydandClaude 7228e2e2dc Add synchronized output support (DEC mode 2026)
Implement synchronized output mode (CSI ? 2026 h/l) which allows
applications to batch terminal updates and render them atomically,
preventing screen tearing during rapid output.

Features:
- BSU (CSI ? 2026 h) pauses rendering, buffering row updates
- ESU (CSI ? 2026 l) flushes buffer and renders atomically
- Configurable timeout via synchronizedOutputTimeout option (default 5s)
- Exposed via terminal.modes.synchronizedOutputMode

Closes #3375

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 11:16:25 -08:00
Ivan Suslov a3e1ecf91b Fix tsc build - upgrade glob version 2025-12-04 07:28:40 -05:00
Ivan Suslov b41d3ef130 Fix ESM export path for addon-unicode-graphemes 2025-12-04 05:02:12 -05:00
Ivan Suslov 8338418471 Added exports field to headless package.json 2025-12-04 03:56:32 -05:00
Daniel Imms 46c510a76d Merge pull request #5413 from afska/patch-1
Adding EmuDevz to the "Real-world uses" list
2025-11-23 13:47:40 -08:00
Daniel Imms 80c3164070 Merge pull request #5446 from Tyriar/tyriar/npm
Migrate from yarn to npm, node 18 -> 22, use npm workspaces
2025-11-23 13:47:15 -08:00
Daniel Imms 236004ce6c Merge branch 'master' into tyriar/npm 2025-11-23 12:53:21 -08:00
Daniel Imms 741d9c3273 Remove install-addon calls 2025-11-23 10:00:21 -08:00
Daniel Imms bf4fb3ad64 Migrate to npm workspaces 2025-11-23 09:54:17 -08:00
Daniel Imms da4ab8a01e Remove peer dependency, generate lock files 2025-11-23 09:35:43 -08:00
Daniel Imms 92eddf8a61 Move to npm 2025-11-23 09:01:24 -08:00
Daniel Imms 637231e6e1 Merge pull request #5445 from Tyriar/tyriar/perf
Prefer performance.now() over Date.now()
2025-11-23 08:46:24 -08:00
Daniel Imms 6468a78237 Fix comment lint 2025-11-23 08:40:35 -08:00
Daniel Imms 98e234992b Prefer performance.now() over Date.now()
performance.now() is faster than Date.now() as there's no need to
consult the system clock. The more important part of this is that
performance.now() is monotonic and is guaranteed to never to backwards.
2025-11-23 08:32:35 -08:00
Daniel Imms e1023d7ec2 Merge pull request #5442 from Tyriar/tyriar/5441
Fix headless module path
2025-11-14 10:18:17 -08:00
Daniel Imms d186a3a115 Fix headless module path
Fixes #5441
2025-11-12 09:30:11 -08:00
Daniel Imms 65a0e03b66 Merge pull request #5437 from xtermjs/anthonykim1/preventPageScrollWhenMouseOff
Prevent entire page from scrolling when scrolling in alt buffer with mouse event off
2025-11-02 03:54:46 -08:00
Anthony Kim 6ae020f51e add this.cancel(ev, true) when line is zero on wheel handler 2025-10-31 14:39:42 -07:00
Daniel Imms 6c79a46513 Merge pull request #5436 from xtermjs/merogge/serialize-range
add range to IHTMLSerializeOptions
2025-10-28 09:41:17 -07:00
meganrogge 4a62856153 fix test 2025-10-28 12:30:54 -04:00
meganrogge 5121867bbb rm line 2025-10-28 11:56:56 -04:00
meganrogge 5a467da137 pass in startLine, endLine, startCol 2025-10-28 11:46:41 -04:00
meganrogge 18bafaf9c9 fix indentation 2025-10-28 10:30:22 -04:00
meganrogge 5141a91bba add range to IHTMLSerializeOptions 2025-10-28 10:27:00 -04:00
Daniel Imms 9fce067266 Merge pull request #5425 from xtermjs/anthonykim1/parsingDataCharCode
Only show char codes for parsing data on trace
2025-10-22 19:08:47 -07:00
Daniel Imms 59241e467c Update src/common/InputHandler.ts 2025-10-22 19:02:14 -07:00
Anthony Kim 09e26980d3 I dont think we need () => in front of typeof data === 'string' 2025-10-22 16:23:35 -07:00
Anthony Kim c3bb36bad9 if trace, debug each AND remove array.from 2025-10-22 16:11:22 -07:00
Anthony Kim 2c57b3219b Only show char codes for parsing data on trace 2025-10-22 09:47:33 -07:00
Daniel Imms e9e8206467 Merge pull request #5423 from Tyriar/tyriar/resize5300
Clear selection on vertical resize
2025-10-19 01:21:28 -07:00
Daniel Imms 9a7517d3eb Clear selection on vertical resize
Fixes #5300
2025-10-19 01:15:30 -07:00
Daniel Imms 1bf90d9f47 Merge pull request #5422 from xtermjs/Tyriar-patch-1
Explain filtering unit tests by file
2025-10-18 13:49:38 -07:00
Daniel Imms 1527e89ad5 Explain filtering unit tests by file 2025-10-18 06:40:45 -07:00
Daniel Imms 95cc31566a Merge pull request #5421 from xtermjs/Tyriar-patch-1
Use Windows PowerShell instead of pwsh 7 in demo
2025-10-18 06:20:41 -07:00
Daniel Imms 7274e1bbea Use Windows PowerShell instead of pwsh 7 in demo
When pwsh 7 isn't installed it's hard to figure out what the problem is.
2025-10-18 06:15:19 -07:00
Daniel Imms a8fabcfa01 Merge pull request #5420 from xtermjs/anthonykim1/charCodesForVerbose
Only log character codes for trace log
2025-10-17 11:39:21 -07:00
Anthony Kim 67cf25989a only send codes, not the data when in trace 2025-10-17 11:26:34 -07:00
Anthony KimandDaniel Imms f87e574971 Update src/common/services/CoreService.ts
Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
2025-10-17 11:23:24 -07:00
Anthony Kim 5269462817 Only log character codes for verbose/trace 2025-10-17 10:12:10 -07:00
Anthony Kim f4baddaaf1 Merge pull request #5411 from xtermjs/anthonykim1/fixTeleportWhenAltScroll
Fix teleport when exiting out of alt buffer. v2
2025-10-17 09:20:23 -07:00
Rodrigo Alfonso 3aa836e5d7 Adding EmuDevz to the "Real-world uses" list 2025-10-10 03:24:12 -03:00
Anthony Kim 2a6e22e8f8 set this._latestYDisp = undefined onBufferActivate 2025-10-01 20:36:06 -07:00
Anthony Kim 32c3cb01a1 Make sure this._latestYDisp is up to date 2025-09-30 23:04:04 -07:00
Daniel Imms a1d8e96fdd Merge pull request #5406 from Tyriar/tyriar/search_refactor
Refactor search addon into multiple files, add unit tests
2025-09-26 18:24:44 +09:00
Daniel Imms c4780e54fd Merge branch 'master' into tyriar/search_refactor 2025-09-26 17:52:47 +09:00
Daniel Imms c56336b32b Fix unit tests 2025-09-26 17:49:20 +09:00
Daniel Imms d11f411c17 Bring back respecting of cached search term 2025-09-26 17:09:54 +09:00
Daniel Imms f03dfbb3ba Merge pull request #5407 from Tyriar/tyriar/bd09_copilot_instructions
Improve Copilot instructions and add test guidelines
2025-09-26 16:51:46 +09:00
Daniel Imms ffd6dc6ab7 Fix import issue 2025-09-19 09:06:53 -07:00
Daniel Imms 5b4b93acc6 Improve Copilot instructions and add test guidelines
- Enhanced copilot instructions with detailed architecture overview
- Added comprehensive development workflow documentation
- Documented addon development patterns and key examples
- Added project-specific conventions and TypeScript structure
- Included common patterns for parser integration and buffer access
- Added critical implementation details for rendering and events
- Created unit test instructions file with addon testing rules
- Specified preferences for assertions and self-documenting tests
2025-09-19 09:03:02 -07:00
Daniel Imms 2b329eb7b7 Search engine tests 2025-09-19 08:58:30 -07:00
Daniel Imms 9c5644ddc0 Add SearchEngine tests 2025-09-19 08:52:14 -07:00
Daniel Imms 398e87490d Add coding guidelines and lint-fix script
- Add coding guidelines section to copilot instructions
- Specify rule about avoiding comments in unit tests
- Add lint-fix npm script for auto-fixing ESLint issues
2025-09-18 13:27:12 -07:00
Daniel Imms a191370d54 refactor: remove unnecessary interface exports
- Remove export from IHighlight and IMultiHighlight interfaces
- Remove export from ISelectedDecoration interface
- Remove export from ISearchPosition interface
- Keep ISearchResult exported as it's used across multiple files
- Make ISelectedDecoration extend IDisposable for consistency
2025-09-18 13:26:43 -07:00
Daniel Imms bf9ce3bfaf Clean up SearchLineCache.test.ts comments
- Remove unnecessary inline comments from unit tests
- Add descriptive assertion messages where appropriate
- Follow coding guidelines for test comment usage
2025-09-18 13:21:07 -07:00
Daniel Imms ca7e8ca3b7 refactor(addon-search): extract components
- Extract SearchEngine for core search algorithms
- Extract DecorationManager for visual decorations
- Extract SearchState for state management
- Extract SearchResultTracker for result tracking
- Simplify SearchAddon by delegating to components
- Improve modularity and maintainability
2025-09-18 07:38:42 -07:00
Daniel Imms ab43a3bd22 Merge pull request #5405 from Tyriar/tyriar/b8d4_search_interface_refactor
Refactor search addon interfaces
2025-09-18 06:31:18 -07:00
Daniel Imms 32e58c71fe Refactor search addon interfaces
- Move ISearchAddonOptions and ISearchResultChangeEvent to types
- Make noScroll property required in IInternalSearchOptions
- Change exported interfaces to internal scope where appropriate
2025-09-18 06:25:12 -07:00
Daniel Imms b12a9393f1 Merge pull request #5404 from Tyriar/tyriar/1de3_timeout_cleanup
Replace timeout handling with disposableTimeout
2025-09-17 15:12:00 -07:00
Daniel Imms c27af4fa2e Remove thenable from more places 2025-09-17 15:06:33 -07:00
Daniel Imms 90bf20d9d8 Remove Thenable 2025-09-17 15:02:27 -07:00
Daniel Imms 8ffcbd98c4 Replace timeout handling with disposableTimeout
- Use disposableTimeout instead of manual timeout management
- Replace MutableDisposable pattern for SearchAddon highlight timeout
- Replace SearchLineCache timeout with MutableDisposable approach
- Clean up timeout disposal logic in both classes
2025-09-17 14:48:11 -07:00
Daniel Imms 15d90826f9 Merge pull request #5403 from Tyriar/tyriar/76c0_search_line_cache
Add SearchLineCache for improved search performance
2025-09-17 14:38:34 -07:00
Daniel Imms 304143a166 Add SearchLineCache for improved search performance
- Add SearchLineCache class to cache translated buffer lines
- Integrate caching into SearchAddon for line translation
- Improve search performance by avoiding repeated translation
- Add cache invalidation on terminal resize and writes
2025-09-17 14:28:03 -07:00
Daniel Imms 02b3704af0 Merge pull request #5402 from Tyriar/tyriar/9436_copilot_instructions
Update GitHub Copilot instructions with test details
2025-09-17 09:28:07 -07:00
Daniel Imms eb1330ca18 Merge pull request #5401 from Tyriar/tyriar/search-refactor
Refactor search addon types and internal handling
2025-09-17 09:22:07 -07:00
Daniel Imms 3a15c8080d Update GitHub Copilot instructions with test details
- Add comprehensive unit test examples with yarn test-unit
- Include integration test examples with yarn test-integration
- Document test filtering by file path and wildcard patterns
- Explain test runner frameworks (mocha and playwright)
2025-09-17 09:21:40 -07:00
Daniel Imms 6ac9f59ef4 Fix tests 2025-09-17 09:16:20 -07:00
Daniel Imms bffdc59901 Merge branch 'master' into tyriar/search-refactor 2025-09-17 08:53:54 -07:00
Daniel Imms 76a8a8e347 Refactor search addon types and internal handling
- Import search types from addon-search package
- Add IInternalSearchOptions interface for noScroll param
- Update _updateMatches to use internal search methods
- Pass internal options through search method chain
2025-09-17 08:52:38 -07:00
Daniel Imms e37aba5924 Merge pull request #5399 from Tyriar/tyriar/search-constants-refactor
Refactor search constants and improve documentation
2025-09-17 08:16:51 -07:00
Daniel Imms 5c0107462e Merge pull request #5379 from xtermjs/dependabot/npm_and_yarn/addons/addon-ligatures/form-data-4.0.4
Bump form-data from 4.0.0 to 4.0.4 in /addons/addon-ligatures
2025-09-17 08:13:12 -07:00
Daniel Imms 87a45acae7 Refactor search constants and improve documentation
- Convert standalone constants to Constants enum with detailed docs
- Fix spacing issue in SearchAddon class declaration
- Update all references to use Constants enum for consistency
2025-09-17 08:06:56 -07:00
Daniel Imms ab01a5fd65 Merge pull request #5398 from Tyriar/tyriar/search-api-typing
Add proper typing for search results change event
2025-09-17 07:54:29 -07:00
Daniel Imms 6e833e5d5d Add proper typing for search results change event
- Export ISearchResultChangeEvent interface in addon-search
- Use proper Event<T> typing instead of inline object
- Improve API consistency and type safety for consumers
2025-09-17 07:33:43 -07:00
Daniel Imms d7d7f1b023 Merge pull request #5391 from Tyriar/tyriar/mouse_events
Bring back partial wheel tracking
2025-09-09 18:42:38 -07:00
Anthony Kim 9c42fb348b ignore fastScrollModifier 2025-09-09 13:21:37 -07:00
Anthony Kim e4f222f6b0 Make test happy 2025-09-09 12:03:51 -07:00
Anthony Kim 38bee71630 More clear comments 2025-09-09 12:00:19 -07:00
Anthony Kim 1a6a807f68 Remove stale comment 2025-09-09 11:59:29 -07:00
Anthony Kim 03ce26c697 Questions, possible concern 2025-09-09 11:56:28 -07:00
Anthony Kim cf6b868dfb fastScrollModifier seems deprecated 2025-09-09 11:51:27 -07:00
Anthony Kim d862c82386 no stale comments 2025-09-08 21:14:20 -07:00
Anthony Kim 50df7a69e6 remove unused var 2025-09-08 21:06:37 -07:00
Anthony Kim 26ecba8de1 Fix sluggish mouse problem. Now similar to trackpad 2025-09-08 21:03:05 -07:00
Daniel Imms 0d1e6b7a5e Merge pull request #5390 from xtermjs/anthonykim1/fixTeleportAltBuffer
Fix scrollbar teleport after exiting alt buffer
2025-08-29 05:41:52 -07:00
Anthony Kim b104a5b953 Respect original formatting 2025-08-28 22:56:15 -07:00
Anthony Kim ebddd5d346 Move from CoreBrowerTerminal to CoreMouseService. Still slow though 2025-08-28 22:54:50 -07:00
Daniel Imms 5bd7b4aaec Bring back partial wheel tracking
Part of microsoft/vscode#224750
2025-08-28 10:28:19 -07:00
Anthony Kim a6fdf043ce Respect original formatting 2025-08-28 08:57:25 -07:00
Anthony Kim 287702610c More clean up after removing test 2025-08-28 08:54:01 -07:00
Anthony Kim 4d59eca5f9 Clean up 2025-08-28 08:52:46 -07:00
Anthony Kim 753ea81fc0 BufferService listen to onBufferActivate, remove tests 2025-08-28 08:46:27 -07:00
Anthony Kim 675b2c3a70 Improve test 2025-08-27 21:31:34 -07:00
Anthony Kim 86745e1320 Add some tests from copilot 2025-08-27 21:12:15 -07:00
Anthony Kim a3ab942ea1 Fix teleport issue when coming back from alt buffer 2025-08-27 20:45:52 -07:00
Daniel Imms e969832946 Merge pull request #5385 from anthonykim1/anthonykim1/fixWrappedTerminalFind
Fix terminal find when wrapped
2025-08-21 12:40:01 -07:00
Anthony Kim 95a9531ec1 Try to add tests 2025-08-20 22:49:03 -07:00
Daniel Imms 7637a2de19 Merge pull request #5386 from alexkozy/safe-guards
fix: ensure that currentRow can not go out of the range
2025-08-20 14:15:08 -07:00
Alexey Kozy a589be2826 fix: ensure that currentRow can not go out of the range 2025-08-20 13:47:40 -07:00
Anthony Kim b53096f4a2 Clean code 2025-08-20 10:55:47 -07:00
Anthony Kim 687d8277e7 Fix terminal find when wrapped 2025-08-20 10:53:40 -07:00
Daniel Imms 4c0cf27cfc Merge pull request #5334 from CodinGame/webgl-renderer-shadow-dom
Support shadow dom in webgl renderer
2025-07-31 08:32:44 -07:00
Loïc Mangeonjean 149527ddb8 refactor: apply requested style change 2025-07-31 17:08:30 +02:00
dependabot[bot] 9c3eb170d2 Bump form-data from 4.0.0 to 4.0.4 in /addons/addon-ligatures
Bumps [form-data](https://github.com/form-data/form-data) from 4.0.0 to 4.0.4.
- [Release notes](https://github.com/form-data/form-data/releases)
- [Changelog](https://github.com/form-data/form-data/blob/master/CHANGELOG.md)
- [Commits](https://github.com/form-data/form-data/compare/v4.0.0...v4.0.4)

---
updated-dependencies:
- dependency-name: form-data
  dependency-version: 4.0.4
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-21 23:07:47 +00:00
Daniel Imms c4b707e9ca Merge pull request #5362 from Tyriar/tyriar/steps2
Move copilot steps to correct folder
2025-06-21 11:01:00 -07:00
Daniel Imms 9d35d59fb3 Move copilot steps to correct folder 2025-06-21 11:00:33 -07:00
Loïc Mangeonjean 3ad83dd70e chore: add webgl shadow dom test 2025-05-21 21:51:01 +02:00
Loïc Mangeonjean 496b53d84d fix: support shadow dom in webgl renderer 2025-05-20 16:31:17 +02:00
96 changed files with 14506 additions and 5647 deletions
+1 -1
View File
@@ -9,7 +9,7 @@
"forwardPorts": [
3000
],
"postCreateCommand": "yarn install && yarn setup",
"postCreateCommand": "npm install && npm run setup",
"customizations": {
"vscode": {
"extensions": [
+88 -7
View File
@@ -1,10 +1,91 @@
This is a TypeScript-based repository with a Ruby client for certain API endpoints. It includes several sub projects called addons which are built separately. It contains a demo application which showcases the functionality of the project.
# xterm.js Copilot Instructions
Please follow these guidelines when contributing:
## Architecture Overview
## Development Flow
**Core Structure**: xterm.js is a multi-target terminal emulator with three main distributions:
- `src/browser/`: Full-featured browser terminal with DOM rendering
- `src/headless/`: Server-side terminal for Node.js (no DOM)
- `src/common/`: Shared core logic (parsing, buffer management, terminal state)
- Install dependencies: `npm install && npm run setup`
- Build and bundle demo: `npm run build && npm run esbuild`
- Run unit tests: `npm run test-unit`
- Run playwright-based integration tests: `npm run test-integration`
**Key Classes**:
- `Terminal` (browser/headless): Public API wrapper
- `CoreTerminal` (common): Core terminal logic and state
- `CoreBrowserTerminal` (browser): Browser-specific terminal implementation
## Development Workflow
**Build System**:
```bash
npm run build && npm run esbuild # Build all TypeScript and bundle
```
**Testing**:
- Unit tests: `npm run test-unit` (Mocha)
- Unit tests filtering to file: `npm run test-unit -- **/fileName.ts
- Per-addon unit tests: `npm run test-unit addons/addon-image/out-esbuild/*.test.js`
- Integration tests: `npm run test-integration` (Playwright across Chrome/Firefox/WebKit)
- Per-addon integration tests: `npm run test-integration --suite=addon-search`
## Addon Development Pattern
All addons follow this structure:
```typescript
export class MyAddon implements ITerminalAddon {
activate(terminal: Terminal): void {
// Called when loaded via terminal.loadAddon()
// Register handlers, access terminal APIs
}
dispose(): void {
// Cleanup when addon is disposed
}
}
```
**Key Examples**:
- `addons/addon-fit/`: Terminal sizing
- `addons/addon-webgl/`: GPU-accelerated rendering
- `addons/addon-search/`: Text search functionality
## Project-Specific Conventions
**TypeScript Project Structure**: Uses TypeScript project references (`tsconfig.all.json`) for incremental builds across browser/headless/addons.
**API Design**:
- Browser and headless terminals share the same public API
- Proposed APIs require `allowProposedApi: true` option
- Constructor-only options (cols, rows) cannot be changed after instantiation
**Testing Utilities**: Use `TestUtils.ts` helpers:
- `openTerminal(ctx, options)` for setup
- `pollFor(page, fn, expectedValue)` for async assertions
- `writeSync(page, data)` for terminal input
## Common Patterns
**Parser Integration**: Register custom escape sequence handlers:
```typescript
terminal.parser.registerCsiHandler('m', params => {
// Handle SGR sequences
return true; // Handled
});
```
**Buffer Access**: Read terminal content via buffer API:
```typescript
const line = terminal.buffer.active.getLine(0);
const cell = line?.getCell(0);
```
**Events**: All terminals emit standard events (onData, onResize, onRender) plus platform-specific ones.
## Critical Implementation Details
- Terminal rendering uses either DOM or WebGL renderers
- Buffer lines are immutable; create new instances for modifications
- Character width handling supports Unicode 11+ and grapheme clustering
- Mouse events translate web events to terminal protocols (X10, VT200, etc.)
- Color theming supports both palette and true color modes
## Writing unit tests
- Unit tests live alongside the source code file of the thing it's testing with a .test.ts suffix.
@@ -0,0 +1,8 @@
---
applyTo: '**/*.test.ts'
---
When writing unit tests follow these rules:
- When writing unit tests for addons, always create a real xterm.js instance instead of mocking it.
- Prefer `assert.ok` over `assert.notStrictEqual` when checking something is undefined or not.
- Avoid comments as most tests should be self-documenting.
+41 -46
View File
@@ -12,17 +12,17 @@ jobs:
timeout-minutes: 10
steps:
- uses: actions/checkout@v3
- name: Use Node.js 18.x
- name: Use Node.js 22.x
uses: actions/setup-node@v3
with:
node-version: 18.x
cache: 'yarn'
node-version: 22.x
cache: 'npm'
- name: Install dependencies
run: yarn --frozen-lockfile
run: npm ci
- name: Setup and run tsc
run: yarn setup
run: npm run setup
- name: Esbuild
run: yarn esbuild
run: npm run esbuild
- name: Zip artifacts
run: |
zip -r compressed-build \
@@ -77,21 +77,20 @@ jobs:
timeout-minutes: 10
steps:
- uses: actions/checkout@v3
- name: Use Node.js 18.x
- name: Use Node.js 22.x
uses: actions/setup-node@v3
with:
node-version: 18.x
cache: 'yarn'
node-version: 22.x
cache: 'npm'
- name: Install dependencies
run: |
yarn --frozen-lockfile
yarn install-addons
npm ci
- name: Lint code
env:
NODE_OPTIONS: --max_old_space_size=4096
run: yarn lint
run: npm run lint
- name: Lint API
run: yarn lint-api
run: npm run lint-api
test-unit-coverage:
needs: build
@@ -99,15 +98,14 @@ jobs:
timeout-minutes: 10
steps:
- uses: actions/checkout@v3
- name: Use Node.js 18.x
- name: Use Node.js 22.x
uses: actions/setup-node@v3
with:
node-version: 18.x
cache: 'yarn'
node-version: 22.x
cache: 'npm'
- name: Install dependencies
run: |
yarn --frozen-lockfile
yarn install-addons
npm ci
- uses: actions/download-artifact@v4
with:
name: build-artifacts
@@ -122,7 +120,7 @@ jobs:
ls -R
- name: Unit test coverage
run: |
yarn test-unit-coverage --forbid-only
npm run test-unit-coverage --forbid-only
EXIT_CODE=$?
./node_modules/.bin/nyc report --reporter=cobertura
exit $EXIT_CODE
@@ -131,7 +129,7 @@ jobs:
timeout-minutes: 20
strategy:
matrix:
node-version: [18]
node-version: [22]
runs-on: [ubuntu, macos, windows]
runs-on: ${{ matrix.runs-on }}-latest
steps:
@@ -140,11 +138,10 @@ jobs:
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}.x
cache: 'yarn'
cache: 'npm'
- name: Install dependencies
run: |
yarn --frozen-lockfile
yarn install-addons
npm ci
- name: Wait for build job
uses: NathanFirmo/wait-for-other-job@v1.1.1
with:
@@ -163,13 +160,13 @@ jobs:
fi
ls -R
- name: Unit tests
run: yarn test-unit --forbid-only
run: npm run test-unit --forbid-only
test-integration:
timeout-minutes: 20
strategy:
matrix:
node-version: [18] # just one as integration tests are about testing in browser
node-version: [22] # just one as integration tests are about testing in browser
runs-on: [ubuntu-22.04] # macos is flaky
browser: [chromium, firefox, webkit]
runs-on: ${{ matrix.runs-on }}
@@ -179,11 +176,10 @@ jobs:
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}.x
cache: 'yarn'
cache: 'npm'
- name: Install dependencies
run: |
yarn --frozen-lockfile
yarn install-addons
npm ci
- name: Install playwright
run: npx playwright install --with-deps ${{ matrix.browser }}
- name: Wait for build job
@@ -204,49 +200,48 @@ jobs:
fi
ls -R
- name: Build demo
run: yarn esbuild-demo
run: npm run esbuild-demo
- name: Integration tests (core) # Tests use 50% workers to reduce flakiness
run: yarn test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=core
run: npm run test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=core
- name: Integration tests (addon-attach)
run: yarn test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-attach
run: npm run test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-attach
- name: Integration tests (addon-clipboard)
run: yarn test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-clipboard
run: npm run test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-clipboard
- name: Integration tests (addon-fit)
run: yarn test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-fit
run: npm run test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-fit
- name: Integration tests (addon-image)
run: yarn test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-image
run: npm run test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-image
- name: Integration tests (addon-progress)
run: yarn test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-progress
run: npm run test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-progress
- name: Integration tests (addon-search)
run: yarn test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-search
run: npm run test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-search
- name: Integration tests (addon-serialize)
run: yarn test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-serialize
run: npm run test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-serialize
- name: Integration tests (addon-unicode-graphemes)
run: yarn test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-unicode-graphemes
run: npm run test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-unicode-graphemes
- name: Integration tests (addon-unicode11)
run: yarn test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-unicode11
run: npm run test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-unicode11
- name: Integration tests (addon-web-links)
run: yarn test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-web-links
run: npm run test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-web-links
- name: Integration tests (addon-webgl)
run: yarn test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-webgl
run: npm run test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-webgl
release-dry-run:
needs: build
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18]
node-version: [22]
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}.x
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}.x
cache: 'yarn'
cache: 'npm'
- name: Install dependencies
run: |
yarn --frozen-lockfile
yarn install-addons
npm ci
- name: Install playwright
run: npx playwright install
- uses: actions/download-artifact@v4
@@ -263,7 +258,7 @@ jobs:
ls -R
- name: Package headless
run: |
yarn package-headless
npm run package-headless
node ./bin/package_headless.js
- name: Publish to npm (dry run)
run: node ./bin/publish.js --dry
@@ -19,18 +19,14 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Use Node.js 20.x
- name: Use Node.js 22.x
uses: actions/setup-node@v3
with:
node-version: 20.x
cache: 'yarn'
node-version: 22.x
cache: 'npm'
- name: Install dependencies
run: yarn --frozen-lockfile
run: npm ci
- name: Setup and run tsc
run: yarn setup
run: npm run setup
- name: Esbuild
run: yarn esbuild
run: npm run esbuild
+6 -6
View File
@@ -11,18 +11,18 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Use Node.js 18.x
- name: Use Node.js 22.x
uses: actions/setup-node@v3
with:
node-version: 18.x
cache: 'yarn'
node-version: 22.x
cache: 'npm'
- name: Install dependencies
run: yarn --frozen-lockfile
run: npm ci
- name: Build
run: yarn setup
run: npm run setup
- name: Package headless
run: |
yarn package-headless
npm run package-headless
node ./bin/package_headless.js
- name: Publish to npm
env:
+2 -1
View File
@@ -18,7 +18,8 @@ npm-debug.log
.env
build/
.DS_Store
package-lock.json
yarn.lock
test-results/
# Keep bundled code out of Git
dist/
+10 -10
View File
@@ -65,40 +65,40 @@ One area that always needs attention is improving out unit test coverage, you ca
### Unit tests
Unit tests are run with `yarn test-unit`:
Unit tests are run with `npm run test-unit`:
```sh
# All unit tests
yarn test-unit
npm run test-unit
# Absolute file path
yarn test-unit out-esbuild/browser/Terminal.test.js
npm run test-unit out-esbuild/browser/Terminal.test.js
# Filter by wildcard
yarn test-unit out-esbuild/**/Terminal.test.js
npm run test-unit out-esbuild/**/Terminal.test.js
# Specific addon unit tests tests
yarn test-unit addons/addon-image/out-esbuild/*.test.js
npm run test-unit addons/addon-image/out-esbuild/*.test.js
# Multiple files
yarn test-unit out-esbuild/**/Terminal.test.js out-esbuild/**/InputHandler.test.js
npm run test-unit out-esbuild/**/Terminal.test.js out-esbuild/**/InputHandler.test.js
```
These use mocha to run all `.test.js` files within the esbuild output (`out-esbuild/`).
### Integration tests
Integration tests are run with `yarn test-integration`:
Integration tests are run with `npm run test-integration`:
```sh
# All integration tests
yarn test-integration
npm run test-integration
# Core integration tests
yarn test-integration --suite=core
npm run test-integration --suite=core
# Specific addon integration tests
yarn test-integration --suite=addon-search
npm run test-integration --suite=addon-search
```
These use `@playwright/test` to run all tests within the esbuild test output (`out-esbuild-test/`).
+1
View File
@@ -229,6 +229,7 @@ Xterm.js is used in several world-class applications to provide great terminal e
- [**pyTermTk**](https://github.com/ceccopierangiolieugenio/pyTermTk): Python Terminal Toolkit - a Spiced Up Cross Compatible TUI Library 🌶️, use xterm.js for the [HTML5 exporter](https://ceccopierangiolieugenio.github.io/pyTermTk/sandbox/sandbox.html).
- [**ecmaOS**](https://ecmaos.sh): A kernel and suite of applications tying modern web technologies into a browser-based operating system.
- [**LabEx**](https://labex.io): Interactive learning platform with hands-on labs and xterm.js-based online terminals, focused on learn-by-doing approach.
- [**EmuDevz**](https://afska.github.io/emudevz): A free coding game where players learn how to build an emulator from scratch.
- [And much more...](https://github.com/xtermjs/xterm.js/network/dependents?package_id=UGFja2FnZS0xNjYzMjc4OQ%3D%3D)
Do you use xterm.js in your application as well? Please [open a Pull Request](https://github.com/sourcelair/xterm.js/pulls) to include it here. We would love to have it on our list. Note: Please add any new contributions to the end of the list only.
+1 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@xterm/addon-attach",
"version": "0.11.0",
"version": "0.12.0",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
@@ -21,8 +21,5 @@
"package": "../../node_modules/.bin/webpack",
"prepublishOnly": "npm run package",
"start": "node ../../demo/start"
},
"peerDependencies": {
"@xterm/xterm": "^5.0.0"
}
}
+22
View File
@@ -0,0 +1,22 @@
{
"name": "@xterm/addon-clipboard",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@xterm/addon-clipboard",
"version": "0.1.0",
"license": "MIT",
"dependencies": {
"js-base64": "^3.7.5"
}
},
"node_modules/js-base64": {
"version": "3.7.7",
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.7.tgz",
"integrity": "sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==",
"license": "BSD-3-Clause"
}
}
}
+1 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@xterm/addon-clipboard",
"version": "0.1.0",
"version": "0.2.0",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
@@ -22,9 +22,6 @@
"prepublishOnly": "npm run package",
"start": "node ../../demo/start"
},
"peerDependencies": {
"@xterm/xterm": "^5.4.0"
},
"dependencies": {
"js-base64": "^3.7.5"
}
+1 -1
View File
@@ -4,5 +4,5 @@
js-base64@^3.7.5:
version "3.7.7"
resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-3.7.7.tgz#e51b84bf78fbf5702b9541e2cb7bfcb893b43e79"
resolved "https://registry.npmjs.org/js-base64/-/js-base64-3.7.7.tgz"
integrity sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==
+1 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@xterm/addon-fit",
"version": "0.10.0",
"version": "0.11.0",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
@@ -21,8 +21,5 @@
"package": "../../node_modules/.bin/webpack",
"prepublishOnly": "npm run package",
"start": "node ../../demo/start"
},
"peerDependencies": {
"@xterm/xterm": "^5.0.0"
}
}
-1
View File
@@ -17,7 +17,6 @@ npm-debug.log
.env
build/
.DS_Store
package-lock.json
yarn.lock
# Keep bundled code out of Git
+817
View File
@@ -0,0 +1,817 @@
{
"name": "@xterm/addon-image",
"version": "0.8.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@xterm/addon-image",
"version": "0.8.0",
"license": "MIT",
"devDependencies": {
"sixel": "^0.16.0",
"xterm-wasm-parts": "^0.1.0"
}
},
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
"dev": true,
"license": "ISC",
"dependencies": {
"string-width": "^5.1.2",
"string-width-cjs": "npm:string-width@^4.2.0",
"strip-ansi": "^7.0.1",
"strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
"wrap-ansi": "^8.1.0",
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@pkgjs/parseargs": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
"dev": true,
"license": "MIT",
"optional": true,
"engines": {
"node": ">=14"
}
},
"node_modules/acorn": {
"version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/acorn-walk": {
"version": "8.3.4",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
"integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
"dev": true,
"license": "MIT",
"dependencies": {
"acorn": "^8.11.0"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/ansi-regex": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/ansi-styles": {
"version": "6.2.3",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/anymatch": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
"dev": true,
"license": "ISC",
"dependencies": {
"normalize-path": "^3.0.0",
"picomatch": "^2.0.4"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true,
"license": "MIT"
},
"node_modules/binary-extensions": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/braces": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dev": true,
"license": "MIT",
"dependencies": {
"fill-range": "^7.1.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
"dev": true,
"license": "MIT",
"dependencies": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
"glob-parent": "~5.1.2",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
"readdirp": "~3.6.0"
},
"engines": {
"node": ">= 8.10.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true,
"license": "MIT"
},
"node_modules/colorette": {
"version": "2.0.20",
"resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
"integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
"dev": true,
"license": "MIT"
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
"which": "^2.0.1"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/eastasianwidth": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
"dev": true,
"license": "MIT"
},
"node_modules/emoji-regex": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
"dev": true,
"license": "MIT"
},
"node_modules/fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dev": true,
"license": "MIT",
"dependencies": {
"to-regex-range": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/foreground-child": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
"integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
"dev": true,
"license": "ISC",
"dependencies": {
"cross-spawn": "^7.0.6",
"signal-exit": "^4.0.1"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/glob": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
"integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
"dev": true,
"license": "ISC",
"dependencies": {
"foreground-child": "^3.1.0",
"jackspeak": "^3.1.2",
"minimatch": "^9.0.4",
"minipass": "^7.1.2",
"package-json-from-dist": "^1.0.0",
"path-scurry": "^1.11.1"
},
"bin": {
"glob": "dist/esm/bin.mjs"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/glob-parent": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/inwasm": {
"version": "0.0.13",
"resolved": "https://registry.npmjs.org/inwasm/-/inwasm-0.0.13.tgz",
"integrity": "sha512-gmULhw1wfF3tQ19y0TvcNH6A5jN7IuTD51kbZuy+ittUU59d+ZTQMb53wbGQuciMrledgagL3/ohnjUj5qJikQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"acorn": "^8.8.2",
"acorn-walk": "^8.2.0",
"chokidar": "^3.5.3",
"colorette": "^2.0.20",
"glob": "^10.0.0",
"wabt": "^1.0.32"
},
"bin": {
"inwasm": "lib/cli.js"
}
},
"node_modules/is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
"dev": true,
"license": "MIT",
"dependencies": {
"binary-extensions": "^2.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/is-glob": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-extglob": "^2.1.1"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.12.0"
}
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"dev": true,
"license": "ISC"
},
"node_modules/jackspeak": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
"integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/cliui": "^8.0.2"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
},
"optionalDependencies": {
"@pkgjs/parseargs": "^0.11.0"
}
},
"node_modules/lru-cache": {
"version": "10.4.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
"dev": true,
"license": "ISC"
},
"node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/minipass": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/package-json-from-dist": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
"dev": true,
"license": "BlueOak-1.0.0"
},
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/path-scurry": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
"integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"lru-cache": "^10.2.0",
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
},
"engines": {
"node": ">=16 || 14 >=14.18"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8.6"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"dev": true,
"license": "MIT",
"dependencies": {
"picomatch": "^2.2.1"
},
"engines": {
"node": ">=8.10.0"
}
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dev": true,
"license": "MIT",
"dependencies": {
"shebang-regex": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/shebang-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/signal-exit": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/sixel": {
"version": "0.16.0",
"resolved": "https://registry.npmjs.org/sixel/-/sixel-0.16.0.tgz",
"integrity": "sha512-xicu6Y6Cyhmv5rjyHxq2r5RnKerlL/nyZEGjOU5bLCshXkZryc9JFJThTCKPOAtWXCfeWquEKFVFfMPcTD25PA==",
"dev": true,
"license": "MIT"
},
"node_modules/string-width": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
"dev": true,
"license": "MIT",
"dependencies": {
"eastasianwidth": "^0.2.0",
"emoji-regex": "^9.2.2",
"strip-ansi": "^7.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/string-width-cjs": {
"name": "string-width",
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/string-width-cjs/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/string-width-cjs/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true,
"license": "MIT"
},
"node_modules/string-width-cjs/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
"integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^6.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/strip-ansi-cjs": {
"name": "strip-ansi",
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-number": "^7.0.0"
},
"engines": {
"node": ">=8.0"
}
},
"node_modules/wabt": {
"version": "1.0.39",
"resolved": "https://registry.npmjs.org/wabt/-/wabt-1.0.39.tgz",
"integrity": "sha512-ba+dRL/75VQQY7RkU/CgriGbkoWAfS8TDyUlJfJhJ8KhtXgMl5dhNvoPNUcQ9IWRhW8u41glMSuZeTvsYq2rRg==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"wasm-decompile": "bin/wasm-decompile",
"wasm-interp": "bin/wasm-interp",
"wasm-objdump": "bin/wasm-objdump",
"wasm-stats": "bin/wasm-stats",
"wasm-strip": "bin/wasm-strip",
"wasm-validate": "bin/wasm-validate",
"wasm2c": "bin/wasm2c",
"wasm2wat": "bin/wasm2wat",
"wat2wasm": "bin/wat2wasm"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"license": "ISC",
"dependencies": {
"isexe": "^2.0.0"
},
"bin": {
"node-which": "bin/node-which"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/wrap-ansi": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^6.1.0",
"string-width": "^5.0.1",
"strip-ansi": "^7.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrap-ansi-cjs": {
"name": "wrap-ansi",
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true,
"license": "MIT"
},
"node_modules/wrap-ansi-cjs/node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/xterm-wasm-parts": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/xterm-wasm-parts/-/xterm-wasm-parts-0.1.0.tgz",
"integrity": "sha512-GFE8yNJfdkytGpcsOZhkL3B8XyUqkR/Du3SdxRyFbJg+BBCKm3raaaflgIM4TwNQ2AOLz01BEEuB5FZXx8aNTQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"inwasm": "^0.0.13"
}
}
}
}
+1 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@xterm/addon-image",
"version": "0.8.0",
"version": "0.9.0",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
@@ -23,9 +23,6 @@
"prepublishOnly": "npm run package",
"start": "node ../../demo/start"
},
"peerDependencies": {
"@xterm/xterm": "^5.2.0"
},
"devDependencies": {
"sixel": "^0.16.0",
"xterm-wasm-parts": "^0.1.0"
-1
View File
@@ -10,4 +10,3 @@ fonts/
*.swp
*.tgz
npm-debug.log*
yarn-error.log*
+661
View File
@@ -0,0 +1,661 @@
{
"name": "@xterm/addon-ligatures",
"version": "0.9.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@xterm/addon-ligatures",
"version": "0.9.0",
"license": "MIT",
"dependencies": {
"font-finder": "^1.1.0",
"font-ligatures": "^1.4.1"
},
"devDependencies": {
"@types/sinon": "^5.0.1",
"axios": "^1.6.0",
"mkdirp": "0.5.5",
"sinon": "6.3.5",
"yauzl": "^2.10.0"
},
"engines": {
"node": ">8.0.0"
}
},
"node_modules/@sinonjs/commons": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.0.tgz",
"integrity": "sha512-wEj54PfsZ5jGSwMX68G8ZXFawcSglQSXqCftWX3ec8MDUzQdHgcKvw97awHbY0efQEL5iKUOAmmVtoYgmrSG4Q==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"type-detect": "4.0.8"
}
},
"node_modules/@sinonjs/formatio": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-3.2.2.tgz",
"integrity": "sha512-B8SEsgd8gArBLMD6zpRw3juQ2FVSsmdd7qlevyDqzS9WTCtvF55/gAL+h6gue8ZvPYcdiPdvueM/qm//9XzyTQ==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"@sinonjs/commons": "^1",
"@sinonjs/samsam": "^3.1.0"
}
},
"node_modules/@sinonjs/formatio/node_modules/@sinonjs/samsam": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-3.3.3.tgz",
"integrity": "sha512-bKCMKZvWIjYD0BLGnNrxVuw4dkWCYsLqFOUWw8VgKF/+5Y+mE7LfHWPIYoDXowH+3a9LsWDMo0uAP8YDosPvHQ==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"@sinonjs/commons": "^1.3.0",
"array-from": "^2.1.1",
"lodash": "^4.17.15"
}
},
"node_modules/@sinonjs/samsam": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-2.1.3.tgz",
"integrity": "sha512-8zNeBkSKhU9a5cRNbpCKau2WWPfan+Q2zDlcXvXyhn9EsMqgYs4qzo0XHNVlXC6ABQL8fT6nV+zzo5RTHJzyXw==",
"dev": true,
"license": "BSD-3-Clause"
},
"node_modules/@sinonjs/text-encoding": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz",
"integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==",
"dev": true,
"license": "(Unlicense OR Apache-2.0)"
},
"node_modules/@types/sinon": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-5.0.1.tgz",
"integrity": "sha512-yxzBCIjE3lp9lYjfBbIK/LRCoXgCLLbIIBIje7eNCcUIIR2CZZtyX5uto2hVoMSMqLrsRrT6mwwUEd0yFgOwpA==",
"dev": true,
"license": "MIT"
},
"node_modules/array-from": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/array-from/-/array-from-2.1.1.tgz",
"integrity": "sha1-z+nYwmYoudxa7MYqn12PHzUsEZU= sha512-GQTc6Uupx1FCavi5mPzBvVT7nEOeWMmUA9P95wpfpW1XwMSKs+KaymD5C2Up7KAUKg/mYwbsUYzdZWcoajlNZg==",
"dev": true,
"license": "MIT"
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"dev": true,
"license": "MIT"
},
"node_modules/axios": {
"version": "1.8.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.2.tgz",
"integrity": "sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==",
"dev": true,
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/buffer-crc32": {
"version": "0.2.13",
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
"integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": "*"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dev": true,
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/diff": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz",
"integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.3.1"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/fd-slicer": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
"integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
"dev": true,
"license": "MIT",
"dependencies": {
"pend": "~1.2.0"
}
},
"node_modules/follow-redirects": {
"version": "1.15.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
"dev": true,
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/font-finder": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/font-finder/-/font-finder-1.1.0.tgz",
"integrity": "sha512-wpCL2uIbi6GurJbU7ZlQ3nGd61Ho+dSU6U83/xJT5UPFfN35EeCW/rOtS+5k+IuEZu2SYmHzDIPL9eA5tSYRAw==",
"license": "MIT",
"dependencies": {
"get-system-fonts": "^2.0.0",
"promise-stream-reader": "^1.0.1"
},
"engines": {
"node": ">8.0.0"
}
},
"node_modules/font-ligatures": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/font-ligatures/-/font-ligatures-1.4.1.tgz",
"integrity": "sha512-7W6zlfyhvCqShZ5ReUWqmSd9vBaUudW0Hxis+tqUjtHhsPU+L3Grf8mcZAtCiXHTzorhwdRTId2WeH/88gdFkw==",
"license": "MIT",
"dependencies": {
"font-finder": "^1.0.3",
"lru-cache": "^6.0.0",
"opentype.js": "^0.8.0"
},
"engines": {
"node": ">8.0.0"
}
},
"node_modules/form-data": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"dev": true,
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"dev": true,
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/get-system-fonts": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/get-system-fonts/-/get-system-fonts-2.0.0.tgz",
"integrity": "sha512-iiM/DavyF2nnLdELzPBSHojzQJVai9WiwrRzn5gp2dutJuerC8qHyBoh4lxfVdKGbnb9eZ4p8Oefbuc3yExB7Q==",
"license": "MIT",
"engines": {
"node": ">8.0.0"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==",
"dev": true,
"license": "MIT"
},
"node_modules/just-extend": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.1.0.tgz",
"integrity": "sha512-ApcjaOdVTJ7y4r08xI5wIqpvwS48Q0PBG4DJROcEkH1f8MdAiNFyFxz3xoL0LWAVwjrwPYZdVHHxhRHcx/uGLA==",
"dev": true,
"license": "MIT"
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true,
"license": "MIT"
},
"node_modules/lodash.get": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
"integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==",
"deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.",
"dev": true,
"license": "MIT"
},
"node_modules/lolex": {
"version": "2.7.5",
"resolved": "https://registry.npmjs.org/lolex/-/lolex-2.7.5.tgz",
"integrity": "sha512-l9x0+1offnKKIzYVjyXU2SiwhXDLekRzKyhnbyldPHvC7BvLPVpdNUNR2KeMAiCN2D/kLNttZgQD5WjSxuBx3Q==",
"dev": true,
"license": "BSD-3-Clause"
},
"node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"license": "ISC",
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dev": true,
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/minimist": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==",
"dev": true,
"license": "MIT"
},
"node_modules/mkdirp": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"minimist": "^1.2.5"
},
"bin": {
"mkdirp": "bin/cmd.js"
}
},
"node_modules/nise": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/nise/-/nise-1.5.3.tgz",
"integrity": "sha512-Ymbac/94xeIrMf59REBPOv0thr+CJVFMhrlAkW/gjCIE58BGQdCj0x7KRCb3yz+Ga2Rz3E9XXSvUyyxqqhjQAQ==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"@sinonjs/formatio": "^3.2.1",
"@sinonjs/text-encoding": "^0.7.1",
"just-extend": "^4.0.2",
"lolex": "^5.0.1",
"path-to-regexp": "^1.7.0"
}
},
"node_modules/nise/node_modules/lolex": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/lolex/-/lolex-5.1.2.tgz",
"integrity": "sha512-h4hmjAvHTmd+25JSwrtTIuwbKdwg5NzZVRMLn9saij4SZaepCrTCxPr35H/3bjwfMJtN+t3CX8672UIkglz28A==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"@sinonjs/commons": "^1.7.0"
}
},
"node_modules/opentype.js": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/opentype.js/-/opentype.js-0.8.0.tgz",
"integrity": "sha512-FQHR4oGP+a0m/f6yHoRpBOIbn/5ZWxKd4D/djHVJu8+KpBTYrJda0b7mLcgDEMWXE9xBCJm+qb0yv6FcvPjukg==",
"license": "MIT",
"dependencies": {
"tiny-inflate": "^1.0.2"
},
"bin": {
"ot": "bin/ot"
}
},
"node_modules/path-to-regexp": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz",
"integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==",
"dev": true,
"license": "MIT",
"dependencies": {
"isarray": "0.0.1"
}
},
"node_modules/pend": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
"dev": true,
"license": "MIT"
},
"node_modules/promise-stream-reader": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/promise-stream-reader/-/promise-stream-reader-1.0.1.tgz",
"integrity": "sha512-Tnxit5trUjBAqqZCGWwjyxhmgMN4hGrtpW3Oc/tRI4bpm/O2+ej72BB08l6JBnGQgVDGCLvHFGjGgQS6vzhwXg==",
"license": "MIT",
"engines": {
"node": ">8.0.0"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"dev": true,
"license": "MIT"
},
"node_modules/sinon": {
"version": "6.3.5",
"resolved": "https://registry.npmjs.org/sinon/-/sinon-6.3.5.tgz",
"integrity": "sha512-xgoZ2gKjyVRcF08RrIQc+srnSyY1JDJtxu3Nsz07j1ffjgXoY6uPLf/qja6nDBZgzYYEovVkFryw2+KiZz11xQ==",
"deprecated": "16.1.1",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"@sinonjs/commons": "^1.0.2",
"@sinonjs/formatio": "^3.0.0",
"@sinonjs/samsam": "^2.1.2",
"diff": "^3.5.0",
"lodash.get": "^4.4.2",
"lolex": "^2.7.5",
"nise": "^1.4.5",
"supports-color": "^5.5.0",
"type-detect": "^4.0.8"
}
},
"node_modules/supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^3.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/tiny-inflate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.2.tgz",
"integrity": "sha512-fEndoHfuiSlXtvG0EA1gB4uAdy2dbQ3WfOtQw1/Y8NQ9rxtGbGkENwfGmN0wK7OkxDz1lgc5Ei2LQ0oxKDUcVw==",
"license": "MIT"
},
"node_modules/type-detect": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
"integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"license": "ISC"
},
"node_modules/yauzl": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
"integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
"dev": true,
"license": "MIT",
"dependencies": {
"buffer-crc32": "~0.2.3",
"fd-slicer": "~1.1.0"
}
}
}
}
+1 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@xterm/addon-ligatures",
"version": "0.9.0",
"version": "0.10.0",
"description": "Add support for programming ligatures to xterm.js",
"author": {
"name": "The xterm.js authors",
@@ -41,8 +41,5 @@
"mkdirp": "0.5.5",
"sinon": "6.3.5",
"yauzl": "^2.10.0"
},
"peerDependencies": {
"@xterm/xterm": "^5.0.0"
}
}
+166 -55
View File
@@ -4,14 +4,14 @@
"@sinonjs/commons@^1", "@sinonjs/commons@^1.0.2", "@sinonjs/commons@^1.3.0", "@sinonjs/commons@^1.7.0":
version "1.8.0"
resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.0.tgz#c8d68821a854c555bba172f3b06959a0039b236d"
resolved "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.0.tgz"
integrity sha512-wEj54PfsZ5jGSwMX68G8ZXFawcSglQSXqCftWX3ec8MDUzQdHgcKvw97awHbY0efQEL5iKUOAmmVtoYgmrSG4Q==
dependencies:
type-detect "4.0.8"
"@sinonjs/formatio@^3.0.0", "@sinonjs/formatio@^3.2.1":
version "3.2.2"
resolved "https://registry.yarnpkg.com/@sinonjs/formatio/-/formatio-3.2.2.tgz#771c60dfa75ea7f2d68e3b94c7e888a78781372c"
resolved "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-3.2.2.tgz"
integrity sha512-B8SEsgd8gArBLMD6zpRw3juQ2FVSsmdd7qlevyDqzS9WTCtvF55/gAL+h6gue8ZvPYcdiPdvueM/qm//9XzyTQ==
dependencies:
"@sinonjs/commons" "^1"
@@ -19,12 +19,12 @@
"@sinonjs/samsam@^2.1.2":
version "2.1.3"
resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-2.1.3.tgz#62cf2a9b624edc795134135fe37fc2ae8ea36be3"
resolved "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-2.1.3.tgz"
integrity sha512-8zNeBkSKhU9a5cRNbpCKau2WWPfan+Q2zDlcXvXyhn9EsMqgYs4qzo0XHNVlXC6ABQL8fT6nV+zzo5RTHJzyXw==
"@sinonjs/samsam@^3.1.0":
version "3.3.3"
resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-3.3.3.tgz#46682efd9967b259b81136b9f120fd54585feb4a"
resolved "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-3.3.3.tgz"
integrity sha512-bKCMKZvWIjYD0BLGnNrxVuw4dkWCYsLqFOUWw8VgKF/+5Y+mE7LfHWPIYoDXowH+3a9LsWDMo0uAP8YDosPvHQ==
dependencies:
"@sinonjs/commons" "^1.3.0"
@@ -33,26 +33,27 @@
"@sinonjs/text-encoding@^0.7.1":
version "0.7.1"
resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz#8da5c6530915653f3a1f38fd5f101d8c3f8079c5"
resolved "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz"
integrity sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==
"@types/sinon@^5.0.1":
version "5.0.1"
resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-5.0.1.tgz#a15b36ec42f1f53166617491feabd1734cb03e21"
resolved "https://registry.npmjs.org/@types/sinon/-/sinon-5.0.1.tgz"
integrity sha512-yxzBCIjE3lp9lYjfBbIK/LRCoXgCLLbIIBIje7eNCcUIIR2CZZtyX5uto2hVoMSMqLrsRrT6mwwUEd0yFgOwpA==
array-from@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/array-from/-/array-from-2.1.1.tgz#cfe9d8c26628b9dc5aecc62a9f5d8f1f352c1195"
integrity sha1-z+nYwmYoudxa7MYqn12PHzUsEZU=
resolved "https://registry.npmjs.org/array-from/-/array-from-2.1.1.tgz"
integrity sha1-z+nYwmYoudxa7MYqn12PHzUsEZU= sha512-GQTc6Uupx1FCavi5mPzBvVT7nEOeWMmUA9P95wpfpW1XwMSKs+KaymD5C2Up7KAUKg/mYwbsUYzdZWcoajlNZg==
asynckit@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz"
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
axios@^1.6.0:
version "1.8.2"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.8.2.tgz#fabe06e241dfe83071d4edfbcaa7b1c3a40f7979"
resolved "https://registry.npmjs.org/axios/-/axios-1.8.2.tgz"
integrity sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==
dependencies:
follow-redirects "^1.15.6"
@@ -61,45 +62,85 @@ axios@^1.6.0:
buffer-crc32@~0.2.3:
version "0.2.13"
resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242"
resolved "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz"
integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==
call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2:
version "1.0.2"
resolved "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz"
integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==
dependencies:
es-errors "^1.3.0"
function-bind "^1.1.2"
combined-stream@^1.0.8:
version "1.0.8"
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz"
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
dependencies:
delayed-stream "~1.0.0"
delayed-stream@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz"
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
diff@^3.5.0:
version "3.5.0"
resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12"
resolved "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz"
integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==
dunder-proto@^1.0.1:
version "1.0.1"
resolved "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz"
integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==
dependencies:
call-bind-apply-helpers "^1.0.1"
es-errors "^1.3.0"
gopd "^1.2.0"
es-define-property@^1.0.1:
version "1.0.1"
resolved "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz"
integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==
es-errors@^1.3.0:
version "1.3.0"
resolved "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz"
integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==
es-object-atoms@^1.0.0, es-object-atoms@^1.1.1:
version "1.1.1"
resolved "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz"
integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==
dependencies:
es-errors "^1.3.0"
es-set-tostringtag@^2.1.0:
version "2.1.0"
resolved "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz"
integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==
dependencies:
es-errors "^1.3.0"
get-intrinsic "^1.2.6"
has-tostringtag "^1.0.2"
hasown "^2.0.2"
fd-slicer@~1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e"
resolved "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz"
integrity sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==
dependencies:
pend "~1.2.0"
follow-redirects@^1.15.6:
version "1.15.9"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1"
resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz"
integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==
font-finder@^1.0.3:
version "1.0.4"
resolved "https://registry.yarnpkg.com/font-finder/-/font-finder-1.0.4.tgz#2ca944954dd8d0e1b5bdc4c596cc08607761d89b"
dependencies:
get-system-fonts "^2.0.0"
promise-stream-reader "^1.0.1"
font-finder@^1.1.0:
font-finder@^1.0.3, font-finder@^1.1.0:
version "1.1.0"
resolved "https://registry.npmjs.org/font-finder/-/font-finder-1.1.0.tgz#2bff2b2762acba720239c8bec898a96daae90858"
resolved "https://registry.npmjs.org/font-finder/-/font-finder-1.1.0.tgz"
integrity sha512-wpCL2uIbi6GurJbU7ZlQ3nGd61Ho+dSU6U83/xJT5UPFfN35EeCW/rOtS+5k+IuEZu2SYmHzDIPL9eA5tSYRAw==
dependencies:
get-system-fonts "^2.0.0"
@@ -107,7 +148,7 @@ font-finder@^1.1.0:
font-ligatures@^1.4.1:
version "1.4.1"
resolved "https://registry.npmjs.org/font-ligatures/-/font-ligatures-1.4.1.tgz#e8c9df7cf6bee80c0cca4a9a578618c7b1f5d8de"
resolved "https://registry.npmjs.org/font-ligatures/-/font-ligatures-1.4.1.tgz"
integrity sha512-7W6zlfyhvCqShZ5ReUWqmSd9vBaUudW0Hxis+tqUjtHhsPU+L3Grf8mcZAtCiXHTzorhwdRTId2WeH/88gdFkw==
dependencies:
font-finder "^1.0.3"
@@ -115,86 +156,150 @@ font-ligatures@^1.4.1:
opentype.js "^0.8.0"
form-data@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452"
integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==
version "4.0.4"
resolved "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz"
integrity sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==
dependencies:
asynckit "^0.4.0"
combined-stream "^1.0.8"
es-set-tostringtag "^2.1.0"
hasown "^2.0.2"
mime-types "^2.1.12"
function-bind@^1.1.2:
version "1.1.2"
resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz"
integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
get-intrinsic@^1.2.6:
version "1.3.0"
resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz"
integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==
dependencies:
call-bind-apply-helpers "^1.0.2"
es-define-property "^1.0.1"
es-errors "^1.3.0"
es-object-atoms "^1.1.1"
function-bind "^1.1.2"
get-proto "^1.0.1"
gopd "^1.2.0"
has-symbols "^1.1.0"
hasown "^2.0.2"
math-intrinsics "^1.1.0"
get-proto@^1.0.1:
version "1.0.1"
resolved "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz"
integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==
dependencies:
dunder-proto "^1.0.1"
es-object-atoms "^1.0.0"
get-system-fonts@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/get-system-fonts/-/get-system-fonts-2.0.0.tgz#a43b9a33f05c0715a60176d2aad5ce6e98f0a3c6"
resolved "https://registry.npmjs.org/get-system-fonts/-/get-system-fonts-2.0.0.tgz"
integrity sha512-iiM/DavyF2nnLdELzPBSHojzQJVai9WiwrRzn5gp2dutJuerC8qHyBoh4lxfVdKGbnb9eZ4p8Oefbuc3yExB7Q==
gopd@^1.2.0:
version "1.2.0"
resolved "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz"
integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==
has-flag@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
resolved "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz"
integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==
has-symbols@^1.0.3, has-symbols@^1.1.0:
version "1.1.0"
resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz"
integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==
has-tostringtag@^1.0.2:
version "1.0.2"
resolved "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz"
integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==
dependencies:
has-symbols "^1.0.3"
hasown@^2.0.2:
version "2.0.2"
resolved "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz"
integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==
dependencies:
function-bind "^1.1.2"
isarray@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
resolved "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz"
integrity sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==
just-extend@^4.0.2:
version "4.1.0"
resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-4.1.0.tgz#7278a4027d889601640ee0ce0e5a00b992467da4"
resolved "https://registry.npmjs.org/just-extend/-/just-extend-4.1.0.tgz"
integrity sha512-ApcjaOdVTJ7y4r08xI5wIqpvwS48Q0PBG4DJROcEkH1f8MdAiNFyFxz3xoL0LWAVwjrwPYZdVHHxhRHcx/uGLA==
lodash.get@^4.4.2:
version "4.4.2"
resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
resolved "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz"
integrity sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==
lodash@^4.17.15:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
lolex@^2.7.5:
version "2.7.5"
resolved "https://registry.yarnpkg.com/lolex/-/lolex-2.7.5.tgz#113001d56bfc7e02d56e36291cc5c413d1aa0733"
resolved "https://registry.npmjs.org/lolex/-/lolex-2.7.5.tgz"
integrity sha512-l9x0+1offnKKIzYVjyXU2SiwhXDLekRzKyhnbyldPHvC7BvLPVpdNUNR2KeMAiCN2D/kLNttZgQD5WjSxuBx3Q==
lolex@^5.0.1:
version "5.1.2"
resolved "https://registry.yarnpkg.com/lolex/-/lolex-5.1.2.tgz#953694d098ce7c07bc5ed6d0e42bc6c0c6d5a367"
resolved "https://registry.npmjs.org/lolex/-/lolex-5.1.2.tgz"
integrity sha512-h4hmjAvHTmd+25JSwrtTIuwbKdwg5NzZVRMLn9saij4SZaepCrTCxPr35H/3bjwfMJtN+t3CX8672UIkglz28A==
dependencies:
"@sinonjs/commons" "^1.7.0"
lru-cache@^6.0.0:
version "6.0.0"
resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz"
integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
dependencies:
yallist "^4.0.0"
math-intrinsics@^1.1.0:
version "1.1.0"
resolved "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz"
integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==
mime-db@1.52.0:
version "1.52.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz"
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
mime-types@^2.1.12:
version "2.1.35"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz"
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
dependencies:
mime-db "1.52.0"
minimist@^1.2.5:
version "1.2.6"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz"
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
mkdirp@0.5.5:
version "0.5.5"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def"
resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz"
integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==
dependencies:
minimist "^1.2.5"
nise@^1.4.5:
version "1.5.3"
resolved "https://registry.yarnpkg.com/nise/-/nise-1.5.3.tgz#9d2cfe37d44f57317766c6e9408a359c5d3ac1f7"
resolved "https://registry.npmjs.org/nise/-/nise-1.5.3.tgz"
integrity sha512-Ymbac/94xeIrMf59REBPOv0thr+CJVFMhrlAkW/gjCIE58BGQdCj0x7KRCb3yz+Ga2Rz3E9XXSvUyyxqqhjQAQ==
dependencies:
"@sinonjs/formatio" "^3.2.1"
@@ -205,33 +310,36 @@ nise@^1.4.5:
opentype.js@^0.8.0:
version "0.8.0"
resolved "https://registry.yarnpkg.com/opentype.js/-/opentype.js-0.8.0.tgz#acabcfa1642fbe894a3e4d759e43ba694e02bd35"
resolved "https://registry.npmjs.org/opentype.js/-/opentype.js-0.8.0.tgz"
integrity sha512-FQHR4oGP+a0m/f6yHoRpBOIbn/5ZWxKd4D/djHVJu8+KpBTYrJda0b7mLcgDEMWXE9xBCJm+qb0yv6FcvPjukg==
dependencies:
tiny-inflate "^1.0.2"
path-to-regexp@^1.7.0:
version "1.9.0"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.9.0.tgz#5dc0753acbf8521ca2e0f137b4578b917b10cf24"
resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz"
integrity sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==
dependencies:
isarray "0.0.1"
pend@~1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"
resolved "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz"
integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==
promise-stream-reader@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/promise-stream-reader/-/promise-stream-reader-1.0.1.tgz#4e793a79c9d49a73ccd947c6da9c127f12923649"
resolved "https://registry.npmjs.org/promise-stream-reader/-/promise-stream-reader-1.0.1.tgz"
integrity sha512-Tnxit5trUjBAqqZCGWwjyxhmgMN4hGrtpW3Oc/tRI4bpm/O2+ej72BB08l6JBnGQgVDGCLvHFGjGgQS6vzhwXg==
proxy-from-env@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
resolved "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz"
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
sinon@6.3.5:
version "6.3.5"
resolved "https://registry.yarnpkg.com/sinon/-/sinon-6.3.5.tgz#0f6d6a5b4ebaad1f6e8e019395542d1d02c144a0"
resolved "https://registry.npmjs.org/sinon/-/sinon-6.3.5.tgz"
integrity sha512-xgoZ2gKjyVRcF08RrIQc+srnSyY1JDJtxu3Nsz07j1ffjgXoY6uPLf/qja6nDBZgzYYEovVkFryw2+KiZz11xQ==
dependencies:
"@sinonjs/commons" "^1.0.2"
@@ -246,27 +354,30 @@ sinon@6.3.5:
supports-color@^5.5.0:
version "5.5.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
resolved "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz"
integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
dependencies:
has-flag "^3.0.0"
tiny-inflate@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/tiny-inflate/-/tiny-inflate-1.0.2.tgz#93d9decffc8805bd57eae4310f0b745e9b6fb3a7"
resolved "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.2.tgz"
integrity sha512-fEndoHfuiSlXtvG0EA1gB4uAdy2dbQ3WfOtQw1/Y8NQ9rxtGbGkENwfGmN0wK7OkxDz1lgc5Ei2LQ0oxKDUcVw==
type-detect@4.0.8, type-detect@^4.0.8:
type-detect@^4.0.8, type-detect@4.0.8:
version "4.0.8"
resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c"
resolved "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz"
integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==
yallist@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
resolved "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz"
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
yauzl@^2.10.0:
version "2.10.0"
resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9"
resolved "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz"
integrity sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==
dependencies:
buffer-crc32 "~0.2.3"
fd-slicer "~1.1.0"
+1 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@xterm/addon-progress",
"version": "0.1.0",
"version": "0.2.0",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
@@ -21,8 +21,5 @@
"package": "../../node_modules/.bin/webpack",
"prepublishOnly": "npm run package",
"start": "node ../../demo/start"
},
"peerDependencies": {
"@xterm/xterm": "^5.0.0"
}
}
+1 -2
View File
@@ -12,8 +12,7 @@
"removeComments": true,
"strict": true,
"types": [
"../../../node_modules/@types/mocha",
"../../../src/vs/typings/thenable"
"../../../node_modules/@types/mocha"
],
"paths": {
"browser/*": [
+1 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@xterm/addon-search",
"version": "0.15.0",
"version": "0.16.0",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
@@ -20,8 +20,5 @@
"package": "../../node_modules/.bin/webpack",
"prepublishOnly": "npm run package",
"start": "node ../../demo/start"
},
"peerDependencies": {
"@xterm/xterm": "^5.0.0"
}
}
@@ -0,0 +1,157 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import type { Terminal, IDisposable, IDecoration } from '@xterm/xterm';
import type { ISearchDecorationOptions } from '@xterm/addon-search';
import { dispose, Disposable, toDisposable } from 'vs/base/common/lifecycle';
import type { ISearchResult } from './SearchEngine';
/**
* Interface for managing a highlight decoration.
*/
interface IHighlight extends IDisposable {
decoration: IDecoration;
match: ISearchResult;
}
/**
* Interface for managing multiple decorations for a single match.
*/
interface IMultiHighlight extends IDisposable {
decorations: IDecoration[];
match: ISearchResult;
}
/**
* Manages visual decorations for search results including highlighting and active selection
* indicators. This class handles the creation, styling, and disposal of search-related decorations.
*/
export class DecorationManager extends Disposable {
private _highlightDecorations: IHighlight[] = [];
private _highlightedLines: Set<number> = new Set();
constructor(private readonly _terminal: Terminal) {
super();
this._register(toDisposable(() => this.clearHighlightDecorations()));
}
/**
* Creates decorations for all provided search results.
* @param results The search results to create decorations for.
* @param options The decoration options.
*/
public createHighlightDecorations(results: ISearchResult[], options: ISearchDecorationOptions): void {
this.clearHighlightDecorations();
for (const match of results) {
const decorations = this._createResultDecorations(match, options, false);
if (decorations) {
for (const decoration of decorations) {
this._storeDecoration(decoration, match);
}
}
}
}
/**
* Creates decorations for the currently active search result.
* @param result The active search result.
* @param options The decoration options.
* @returns The multi-highlight decoration or undefined if creation failed.
*/
public createActiveDecoration(result: ISearchResult, options: ISearchDecorationOptions): IMultiHighlight | undefined {
const decorations = this._createResultDecorations(result, options, true);
if (decorations) {
return { decorations, match: result, dispose() { dispose(decorations); } };
}
return undefined;
}
/**
* Clears all highlight decorations.
*/
public clearHighlightDecorations(): void {
dispose(this._highlightDecorations);
this._highlightDecorations = [];
this._highlightedLines.clear();
}
/**
* Stores a decoration and tracks it for management.
* @param decoration The decoration to store.
* @param match The search result this decoration represents.
*/
private _storeDecoration(decoration: IDecoration, match: ISearchResult): void {
this._highlightedLines.add(decoration.marker.line);
this._highlightDecorations.push({ decoration, match, dispose() { decoration.dispose(); } });
}
/**
* Applies styles to the decoration when it is rendered.
* @param element The decoration's element.
* @param borderColor The border color to apply.
* @param isActiveResult Whether the element is part of the active search result.
*/
private _applyStyles(element: HTMLElement, borderColor: string | undefined, isActiveResult: boolean): void {
if (!element.classList.contains('xterm-find-result-decoration')) {
element.classList.add('xterm-find-result-decoration');
if (borderColor) {
element.style.outline = `1px solid ${borderColor}`;
}
}
if (isActiveResult) {
element.classList.add('xterm-find-active-result-decoration');
}
}
/**
* Creates a decoration for the result and applies styles
* @param result the search result for which to create the decoration
* @param options the options for the decoration
* @param isActiveResult whether this is the currently active result
* @returns the decorations or undefined if the marker has already been disposed of
*/
private _createResultDecorations(result: ISearchResult, options: ISearchDecorationOptions, isActiveResult: boolean): IDecoration[] | undefined {
// Gather decoration ranges for this match as it could wrap
const decorationRanges: [number, number, number][] = [];
let currentCol = result.col;
let remainingSize = result.size;
let markerOffset = -this._terminal.buffer.active.baseY - this._terminal.buffer.active.cursorY + result.row;
while (remainingSize > 0) {
const amountThisRow = Math.min(this._terminal.cols - currentCol, remainingSize);
decorationRanges.push([markerOffset, currentCol, amountThisRow]);
currentCol = 0;
remainingSize -= amountThisRow;
markerOffset++;
}
// Create the decorations
const decorations: IDecoration[] = [];
for (const range of decorationRanges) {
const marker = this._terminal.registerMarker(range[0]);
const decoration = this._terminal.registerDecoration({
marker,
x: range[1],
width: range[2],
backgroundColor: isActiveResult ? options.activeMatchBackground : options.matchBackground,
overviewRulerOptions: this._highlightedLines.has(marker.line) ? undefined : {
color: isActiveResult ? options.activeMatchColorOverviewRuler : options.matchOverviewRuler,
position: 'center'
}
});
if (decoration) {
const disposables: IDisposable[] = [];
disposables.push(marker);
disposables.push(decoration.onRender((e) => this._applyStyles(e, isActiveResult ? options.activeMatchBorder : options.matchBorder, false)));
disposables.push(decoration.onDispose(() => dispose(disposables)));
decorations.push(decoration);
}
}
return decorations.length === 0 ? undefined : decorations;
}
}
+112 -634
View File
@@ -3,129 +3,87 @@
* @license MIT
*/
import type { Terminal, IDisposable, ITerminalAddon, IDecoration } from '@xterm/xterm';
import type { SearchAddon as ISearchApi } from '@xterm/addon-search';
import { Emitter } from 'vs/base/common/event';
import { combinedDisposable, Disposable, dispose, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle';
import type { Terminal, IDisposable, ITerminalAddon } from '@xterm/xterm';
import type { SearchAddon as ISearchApi, ISearchOptions, ISearchAddonOptions, ISearchResultChangeEvent } from '@xterm/addon-search';
import { Event } from 'vs/base/common/event';
import { Disposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { disposableTimeout } from 'vs/base/common/async';
import { SearchLineCache } from './SearchLineCache';
import { SearchState } from './SearchState';
import { SearchEngine, type ISearchResult } from './SearchEngine';
import { DecorationManager } from './DecorationManager';
import { SearchResultTracker } from './SearchResultTracker';
export interface ISearchOptions {
regex?: boolean;
wholeWord?: boolean;
caseSensitive?: boolean;
incremental?: boolean;
decorations?: ISearchDecorationOptions;
noScroll?: boolean;
interface IInternalSearchOptions {
noScroll: boolean;
}
interface ISearchDecorationOptions {
matchBackground?: string;
matchBorder?: string;
matchOverviewRuler: string;
activeMatchBackground?: string;
activeMatchBorder?: string;
activeMatchColorOverviewRuler: string;
}
export interface ISearchPosition {
startCol: number;
startRow: number;
}
export interface ISearchAddonOptions {
highlightLimit: number;
}
export interface ISearchResult {
term: string;
col: number;
row: number;
size: number;
}
type LineCacheEntry = [
/**
* Configuration constants for the search addon functionality.
*/
const enum Constants {
/**
* The string representation of a line (as opposed to the buffer cell representation).
* Default maximum number of search results to highlight simultaneously. This limit prevents
* performance degradation when searching for very common terms that would result in excessive
* highlighting decorations.
*/
lineAsString: string,
/**
* The offsets where each line starts when the entry describes a wrapped line.
*/
lineOffsets: number[]
];
interface IHighlight extends IDisposable {
decoration: IDecoration;
match: ISearchResult;
DEFAULT_HIGHLIGHT_LIMIT = 1000
}
interface IMultiHighlight extends IDisposable {
decorations: IDecoration[];
match: ISearchResult;
}
const NON_WORD_CHARACTERS = ' ~!@#$%^&*()+`-=[]{}|\\;:"\',./<>?';
const LINES_CACHE_TIME_TO_LIVE = 15 * 1000; // 15 secs
const DEFAULT_HIGHLIGHT_LIMIT = 1000;
export class SearchAddon extends Disposable implements ITerminalAddon , ISearchApi {
export class SearchAddon extends Disposable implements ITerminalAddon, ISearchApi {
private _terminal: Terminal | undefined;
private _cachedSearchTerm: string | undefined;
private _highlightedLines: Set<number> = new Set();
private _highlightDecorations: IHighlight[] = [];
private _selectedDecoration: MutableDisposable<IMultiHighlight> = this._register(new MutableDisposable());
private _highlightLimit: number;
private _lastSearchOptions: ISearchOptions | undefined;
private _highlightTimeout: number | undefined;
/**
* translateBufferLineToStringWithWrap is a fairly expensive call.
* We memoize the calls into an array that has a time based ttl.
* _linesCache is also invalidated when the terminal cursor moves.
*/
private _linesCache: LineCacheEntry[] | undefined;
private _linesCacheTimeoutId = 0;
private _linesCacheDisposables = new MutableDisposable();
private _highlightTimeout = this._register(new MutableDisposable<IDisposable>());
private _lineCache = this._register(new MutableDisposable<SearchLineCache>());
private readonly _onDidChangeResults = this._register(new Emitter<{ resultIndex: number, resultCount: number }>());
public readonly onDidChangeResults = this._onDidChangeResults.event;
// Component instances
private _state = new SearchState();
private _engine: SearchEngine | undefined;
private _decorationManager: DecorationManager | undefined;
private _resultTracker = this._register(new SearchResultTracker());
public get onDidChangeResults(): Event<ISearchResultChangeEvent> {
return this._resultTracker.onDidChangeResults;
}
constructor(options?: Partial<ISearchAddonOptions>) {
super();
this._highlightLimit = options?.highlightLimit ?? DEFAULT_HIGHLIGHT_LIMIT;
this._highlightLimit = options?.highlightLimit ?? Constants.DEFAULT_HIGHLIGHT_LIMIT;
}
public activate(terminal: Terminal): void {
this._terminal = terminal;
this._lineCache.value = new SearchLineCache(terminal);
this._engine = new SearchEngine(terminal, this._lineCache.value);
this._decorationManager = new DecorationManager(terminal);
this._register(this._terminal.onWriteParsed(() => this._updateMatches()));
this._register(this._terminal.onResize(() => this._updateMatches()));
this._register(toDisposable(() => this.clearDecorations()));
}
private _updateMatches(): void {
if (this._highlightTimeout) {
window.clearTimeout(this._highlightTimeout);
}
if (this._cachedSearchTerm && this._lastSearchOptions?.decorations) {
this._highlightTimeout = setTimeout(() => {
const term = this._cachedSearchTerm;
this._cachedSearchTerm = undefined;
this.findPrevious(term!, { ...this._lastSearchOptions, incremental: true, noScroll: true });
this._highlightTimeout.clear();
if (this._state.cachedSearchTerm && this._state.lastSearchOptions?.decorations) {
this._highlightTimeout.value = disposableTimeout(() => {
const term = this._state.cachedSearchTerm;
this._state.clearCachedTerm();
this.findPrevious(term!, { ...this._state.lastSearchOptions, incremental: true }, { noScroll: true });
}, 200);
}
}
public clearDecorations(retainCachedSearchTerm?: boolean): void {
this._selectedDecoration.clear();
dispose(this._highlightDecorations);
this._highlightDecorations = [];
this._highlightedLines.clear();
this._resultTracker.clearSelectedDecoration();
this._decorationManager?.clearHighlightDecorations();
this._resultTracker.clearResults();
if (!retainCachedSearchTerm) {
this._cachedSearchTerm = undefined;
this._state.clearCachedTerm();
}
}
public clearActiveDecoration(): void {
this._selectedDecoration.clear();
this._resultTracker.clearSelectedDecoration();
}
/**
@@ -135,174 +93,74 @@ export class SearchAddon extends Disposable implements ITerminalAddon , ISearchA
* @param searchOptions Search options.
* @returns Whether a result was found.
*/
public findNext(term: string, searchOptions?: ISearchOptions): boolean {
if (!this._terminal) {
public findNext(term: string, searchOptions?: ISearchOptions, internalSearchOptions?: IInternalSearchOptions): boolean {
if (!this._terminal || !this._engine) {
throw new Error('Cannot use addon until it has been loaded');
}
const didOptionsChanged = this._lastSearchOptions ? this._didOptionsChange(this._lastSearchOptions, searchOptions) : true;
this._lastSearchOptions = searchOptions;
if (searchOptions?.decorations) {
if (this._cachedSearchTerm === undefined || term !== this._cachedSearchTerm || didOptionsChanged) {
this._highlightAllMatches(term, searchOptions);
}
this._state.lastSearchOptions = searchOptions;
if (this._state.shouldUpdateHighlighting(term, searchOptions)) {
this._highlightAllMatches(term, searchOptions!);
}
const found = this._findNextAndSelect(term, searchOptions);
const found = this._findNextAndSelect(term, searchOptions, internalSearchOptions);
this._fireResults(searchOptions);
this._cachedSearchTerm = term;
this._state.cachedSearchTerm = term;
return found;
}
private _highlightAllMatches(term: string, searchOptions: ISearchOptions): void {
if (!this._terminal) {
if (!this._terminal || !this._engine || !this._decorationManager) {
throw new Error('Cannot use addon until it has been loaded');
}
if (!term || term.length === 0) {
if (!this._state.isValidSearchTerm(term)) {
this.clearDecorations();
return;
}
searchOptions = searchOptions || {};
// new search, clear out the old decorations
this.clearDecorations(true);
const searchResultsWithHighlight: ISearchResult[] = [];
const results: ISearchResult[] = [];
let prevResult: ISearchResult | undefined = undefined;
let result = this._find(term, 0, 0, searchOptions);
let result = this._engine.find(term, 0, 0, searchOptions);
while (result && (prevResult?.row !== result.row || prevResult?.col !== result.col)) {
if (searchResultsWithHighlight.length >= this._highlightLimit) {
if (results.length >= this._highlightLimit) {
break;
}
prevResult = result;
searchResultsWithHighlight.push(prevResult);
result = this._find(
results.push(prevResult);
result = this._engine.find(
term,
prevResult.col + prevResult.term.length >= this._terminal.cols ? prevResult.row + 1 : prevResult.row,
prevResult.col + prevResult.term.length >= this._terminal.cols ? 0 : prevResult.col + 1,
searchOptions
);
}
for (const match of searchResultsWithHighlight) {
const decorations = this._createResultDecorations(match, searchOptions.decorations!, false);
if (decorations) {
for (const decoration of decorations) {
this._storeDecoration(decoration, match);
}
}
this._resultTracker.updateResults(results, this._highlightLimit);
if (searchOptions.decorations) {
this._decorationManager.createHighlightDecorations(results, searchOptions.decorations);
}
}
private _storeDecoration(decoration: IDecoration, match: ISearchResult): void {
this._highlightedLines.add(decoration.marker.line);
this._highlightDecorations.push({ decoration, match, dispose() { decoration.dispose(); } });
}
private _find(term: string, startRow: number, startCol: number, searchOptions?: ISearchOptions): ISearchResult | undefined {
if (!this._terminal || !term || term.length === 0) {
this._terminal?.clearSelection();
this.clearDecorations();
return undefined;
private _findNextAndSelect(term: string, searchOptions?: ISearchOptions, internalSearchOptions?: IInternalSearchOptions): boolean {
if (!this._terminal || !this._engine) {
return false;
}
if (startCol > this._terminal.cols) {
throw new Error(`Invalid col: ${startCol} to search in terminal of ${this._terminal.cols} cols`);
}
let result: ISearchResult | undefined = undefined;
this._initLinesCache();
const searchPosition: ISearchPosition = {
startRow,
startCol
};
// Search startRow
result = this._findInLine(term, searchPosition, searchOptions);
// Search from startRow + 1 to end
if (!result) {
for (let y = startRow + 1; y < this._terminal.buffer.active.baseY + this._terminal.rows; y++) {
searchPosition.startRow = y;
searchPosition.startCol = 0;
// If the current line is wrapped line, increase index of column to ignore the previous scan
// Otherwise, reset beginning column index to zero with set new unwrapped line index
result = this._findInLine(term, searchPosition, searchOptions);
if (result) {
break;
}
}
}
return result;
}
private _findNextAndSelect(term: string, searchOptions?: ISearchOptions): boolean {
if (!this._terminal || !term || term.length === 0) {
this._terminal?.clearSelection();
if (!this._state.isValidSearchTerm(term)) {
this._terminal.clearSelection();
this.clearDecorations();
return false;
}
const prevSelectedPos = this._terminal.getSelectionPosition();
this._terminal.clearSelection();
let startCol = 0;
let startRow = 0;
if (prevSelectedPos) {
if (this._cachedSearchTerm === term) {
startCol = prevSelectedPos.end.x;
startRow = prevSelectedPos.end.y;
} else {
startCol = prevSelectedPos.start.x;
startRow = prevSelectedPos.start.y;
}
}
this._initLinesCache();
const searchPosition: ISearchPosition = {
startRow,
startCol
};
// Search startRow
let result = this._findInLine(term, searchPosition, searchOptions);
// Search from startRow + 1 to end
if (!result) {
for (let y = startRow + 1; y < this._terminal.buffer.active.baseY + this._terminal.rows; y++) {
searchPosition.startRow = y;
searchPosition.startCol = 0;
// If the current line is wrapped line, increase index of column to ignore the previous scan
// Otherwise, reset beginning column index to zero with set new unwrapped line index
result = this._findInLine(term, searchPosition, searchOptions);
if (result) {
break;
}
}
}
// If we hit the bottom and didn't search from the very top wrap back up
if (!result && startRow !== 0) {
for (let y = 0; y < startRow; y++) {
searchPosition.startRow = y;
searchPosition.startCol = 0;
result = this._findInLine(term, searchPosition, searchOptions);
if (result) {
break;
}
}
}
// If there is only one result, wrap back and return selection if it exists.
if (!result && prevSelectedPos) {
searchPosition.startRow = prevSelectedPos.start.y;
searchPosition.startCol = 0;
result = this._findInLine(term, searchPosition, searchOptions);
}
// Set selection and scroll if a result was found
return this._selectResult(result, searchOptions?.decorations, searchOptions?.noScroll);
const result = this._engine.findNextWithSelection(term, searchOptions, this._state.cachedSearchTerm);
return this._selectResult(result, searchOptions?.decorations, internalSearchOptions?.noScroll);
}
/**
* Find the previous instance of the term, then scroll to and select it. If it
* doesn't exist, do nothing.
@@ -310,357 +168,40 @@ export class SearchAddon extends Disposable implements ITerminalAddon , ISearchA
* @param searchOptions Search options.
* @returns Whether a result was found.
*/
public findPrevious(term: string, searchOptions?: ISearchOptions): boolean {
if (!this._terminal) {
public findPrevious(term: string, searchOptions?: ISearchOptions, internalSearchOptions?: IInternalSearchOptions): boolean {
if (!this._terminal || !this._engine) {
throw new Error('Cannot use addon until it has been loaded');
}
const didOptionsChanged = this._lastSearchOptions ? this._didOptionsChange(this._lastSearchOptions, searchOptions) : true;
this._lastSearchOptions = searchOptions;
if (searchOptions?.decorations) {
if (this._cachedSearchTerm === undefined || term !== this._cachedSearchTerm || didOptionsChanged) {
this._highlightAllMatches(term, searchOptions);
}
this._state.lastSearchOptions = searchOptions;
if (this._state.shouldUpdateHighlighting(term, searchOptions)) {
this._highlightAllMatches(term, searchOptions!);
}
const found = this._findPreviousAndSelect(term, searchOptions);
const found = this._findPreviousAndSelect(term, searchOptions, internalSearchOptions);
this._fireResults(searchOptions);
this._cachedSearchTerm = term;
this._state.cachedSearchTerm = term;
return found;
}
private _didOptionsChange(lastSearchOptions: ISearchOptions, searchOptions?: ISearchOptions): boolean {
if (!searchOptions) {
private _fireResults(searchOptions?: ISearchOptions): void {
this._resultTracker.fireResultsChanged(!!searchOptions?.decorations);
}
private _findPreviousAndSelect(term: string, searchOptions?: ISearchOptions, internalSearchOptions?: IInternalSearchOptions): boolean {
if (!this._terminal || !this._engine) {
return false;
}
if (lastSearchOptions.caseSensitive !== searchOptions.caseSensitive) {
return true;
}
if (lastSearchOptions.regex !== searchOptions.regex) {
return true;
}
if (lastSearchOptions.wholeWord !== searchOptions.wholeWord) {
return true;
}
return false;
}
private _fireResults(searchOptions?: ISearchOptions): void {
if (searchOptions?.decorations) {
let resultIndex = -1;
if (this._selectedDecoration.value) {
const selectedMatch = this._selectedDecoration.value.match;
for (let i = 0; i < this._highlightDecorations.length; i++) {
const match = this._highlightDecorations[i].match;
if (match.row === selectedMatch.row && match.col === selectedMatch.col && match.size === selectedMatch.size) {
resultIndex = i;
break;
}
}
}
this._onDidChangeResults.fire({ resultIndex, resultCount: this._highlightDecorations.length });
}
}
private _findPreviousAndSelect(term: string, searchOptions?: ISearchOptions): boolean {
if (!this._terminal) {
throw new Error('Cannot use addon until it has been loaded');
}
if (!this._terminal || !term || term.length === 0) {
this._terminal?.clearSelection();
if (!this._state.isValidSearchTerm(term)) {
this._terminal.clearSelection();
this.clearDecorations();
return false;
}
const prevSelectedPos = this._terminal.getSelectionPosition();
this._terminal.clearSelection();
let startRow = this._terminal.buffer.active.baseY + this._terminal.rows - 1;
let startCol = this._terminal.cols;
const isReverseSearch = true;
this._initLinesCache();
const searchPosition: ISearchPosition = {
startRow,
startCol
};
let result: ISearchResult | undefined;
if (prevSelectedPos) {
searchPosition.startRow = startRow = prevSelectedPos.start.y;
searchPosition.startCol = startCol = prevSelectedPos.start.x;
if (this._cachedSearchTerm !== term) {
// Try to expand selection to right first.
result = this._findInLine(term, searchPosition, searchOptions, false);
if (!result) {
// If selection was not able to be expanded to the right, then try reverse search
searchPosition.startRow = startRow = prevSelectedPos.end.y;
searchPosition.startCol = startCol = prevSelectedPos.end.x;
}
}
}
if (!result) {
result = this._findInLine(term, searchPosition, searchOptions, isReverseSearch);
}
// Search from startRow - 1 to top
if (!result) {
searchPosition.startCol = Math.max(searchPosition.startCol, this._terminal.cols);
for (let y = startRow - 1; y >= 0; y--) {
searchPosition.startRow = y;
result = this._findInLine(term, searchPosition, searchOptions, isReverseSearch);
if (result) {
break;
}
}
}
// If we hit the top and didn't search from the very bottom wrap back down
if (!result && startRow !== (this._terminal.buffer.active.baseY + this._terminal.rows - 1)) {
for (let y = (this._terminal.buffer.active.baseY + this._terminal.rows - 1); y >= startRow; y--) {
searchPosition.startRow = y;
result = this._findInLine(term, searchPosition, searchOptions, isReverseSearch);
if (result) {
break;
}
}
}
// Set selection and scroll if a result was found
return this._selectResult(result, searchOptions?.decorations, searchOptions?.noScroll);
}
/**
* Sets up a line cache with a ttl
*/
private _initLinesCache(): void {
const terminal = this._terminal!;
if (!this._linesCache) {
this._linesCache = new Array(terminal.buffer.active.length);
this._linesCacheDisposables.value = combinedDisposable(
terminal.onLineFeed(() => this._destroyLinesCache()),
terminal.onCursorMove(() => this._destroyLinesCache()),
terminal.onResize(() => this._destroyLinesCache())
);
}
window.clearTimeout(this._linesCacheTimeoutId);
this._linesCacheTimeoutId = window.setTimeout(() => this._destroyLinesCache(), LINES_CACHE_TIME_TO_LIVE);
}
private _destroyLinesCache(): void {
this._linesCache = undefined;
this._linesCacheDisposables.clear();
if (this._linesCacheTimeoutId) {
window.clearTimeout(this._linesCacheTimeoutId);
this._linesCacheTimeoutId = 0;
}
}
/**
* A found substring is a whole word if it doesn't have an alphanumeric character directly
* adjacent to it.
* @param searchIndex starting indext of the potential whole word substring
* @param line entire string in which the potential whole word was found
* @param term the substring that starts at searchIndex
*/
private _isWholeWord(searchIndex: number, line: string, term: string): boolean {
return ((searchIndex === 0) || (NON_WORD_CHARACTERS.includes(line[searchIndex - 1]))) &&
(((searchIndex + term.length) === line.length) || (NON_WORD_CHARACTERS.includes(line[searchIndex + term.length])));
}
/**
* Searches a line for a search term. Takes the provided terminal line and searches the text line,
* which may contain subsequent terminal lines if the text is wrapped. If the provided line number
* is part of a wrapped text line that started on an earlier line then it is skipped since it will
* be properly searched when the terminal line that the text starts on is searched.
* @param term The search term.
* @param searchPosition The position to start the search.
* @param searchOptions Search options.
* @param isReverseSearch Whether the search should start from the right side of the terminal and
* search to the left.
* @returns The search result if it was found.
*/
protected _findInLine(term: string, searchPosition: ISearchPosition, searchOptions: ISearchOptions = {}, isReverseSearch: boolean = false): ISearchResult | undefined {
const terminal = this._terminal!;
const row = searchPosition.startRow;
const col = searchPosition.startCol;
// Ignore wrapped lines, only consider on unwrapped line (first row of command string).
const firstLine = terminal.buffer.active.getLine(row);
if (firstLine?.isWrapped) {
if (isReverseSearch) {
searchPosition.startCol += terminal.cols;
return;
}
// This will iterate until we find the line start.
// When we find it, we will search using the calculated start column.
searchPosition.startRow--;
searchPosition.startCol += terminal.cols;
return this._findInLine(term, searchPosition, searchOptions);
}
let cache = this._linesCache?.[row];
if (!cache) {
cache = this._translateBufferLineToStringWithWrap(row, true);
if (this._linesCache) {
this._linesCache[row] = cache;
}
}
const [stringLine, offsets] = cache;
const offset = this._bufferColsToStringOffset(row, col);
let searchTerm = term;
let searchStringLine = stringLine;
if (!searchOptions.regex) {
searchTerm = searchOptions.caseSensitive ? term : term.toLowerCase();
searchStringLine = searchOptions.caseSensitive ? stringLine : stringLine.toLowerCase();
}
let resultIndex = -1;
if (searchOptions.regex) {
const searchRegex = RegExp(searchTerm, searchOptions.caseSensitive ? 'g' : 'gi');
let foundTerm: RegExpExecArray | null;
if (isReverseSearch) {
// This loop will get the resultIndex of the _last_ regex match in the range 0..offset
while (foundTerm = searchRegex.exec(searchStringLine.slice(0, offset))) {
resultIndex = searchRegex.lastIndex - foundTerm[0].length;
term = foundTerm[0];
searchRegex.lastIndex -= (term.length - 1);
}
} else {
foundTerm = searchRegex.exec(searchStringLine.slice(offset));
if (foundTerm && foundTerm[0].length > 0) {
resultIndex = offset + (searchRegex.lastIndex - foundTerm[0].length);
term = foundTerm[0];
}
}
} else {
if (isReverseSearch) {
if (offset - searchTerm.length >= 0) {
resultIndex = searchStringLine.lastIndexOf(searchTerm, offset - searchTerm.length);
}
} else {
resultIndex = searchStringLine.indexOf(searchTerm, offset);
}
}
if (resultIndex >= 0) {
if (searchOptions.wholeWord && !this._isWholeWord(resultIndex, searchStringLine, term)) {
return;
}
// Adjust the row number and search index if needed since a "line" of text can span multiple
// rows
let startRowOffset = 0;
while (startRowOffset < offsets.length - 1 && resultIndex >= offsets[startRowOffset + 1]) {
startRowOffset++;
}
let endRowOffset = startRowOffset;
while (endRowOffset < offsets.length - 1 && resultIndex + term.length >= offsets[endRowOffset + 1]) {
endRowOffset++;
}
const startColOffset = resultIndex - offsets[startRowOffset];
const endColOffset = resultIndex + term.length - offsets[endRowOffset];
const startColIndex = this._stringLengthToBufferSize(row + startRowOffset, startColOffset);
const endColIndex = this._stringLengthToBufferSize(row + endRowOffset, endColOffset);
const size = endColIndex - startColIndex + terminal.cols * (endRowOffset - startRowOffset);
return {
term,
col: startColIndex,
row: row + startRowOffset,
size
};
}
}
private _stringLengthToBufferSize(row: number, offset: number): number {
const line = this._terminal!.buffer.active.getLine(row);
if (!line) {
return 0;
}
for (let i = 0; i < offset; i++) {
const cell = line.getCell(i);
if (!cell) {
break;
}
// Adjust the searchIndex to normalize emoji into single chars
const char = cell.getChars();
if (char.length > 1) {
offset -= char.length - 1;
}
// Adjust the searchIndex for empty characters following wide unicode
// chars (eg. CJK)
const nextCell = line.getCell(i + 1);
if (nextCell && nextCell.getWidth() === 0) {
offset++;
}
}
return offset;
}
private _bufferColsToStringOffset(startRow: number, cols: number): number {
const terminal = this._terminal!;
let lineIndex = startRow;
let offset = 0;
let line = terminal.buffer.active.getLine(lineIndex);
while (cols > 0 && line) {
for (let i = 0; i < cols && i < terminal.cols; i++) {
const cell = line.getCell(i);
if (!cell) {
break;
}
if (cell.getWidth()) {
// Treat null characters as whitespace to align with the translateToString API
offset += cell.getCode() === 0 ? 1 : cell.getChars().length;
}
}
lineIndex++;
line = terminal.buffer.active.getLine(lineIndex);
if (line && !line.isWrapped) {
break;
}
cols -= terminal.cols;
}
return offset;
}
/**
* Translates a buffer line to a string, including subsequent lines if they are wraps.
* Wide characters will count as two columns in the resulting string. This
* function is useful for getting the actual text underneath the raw selection
* position.
* @param lineIndex The index of the line being translated.
* @param trimRight Whether to trim whitespace to the right.
*/
private _translateBufferLineToStringWithWrap(lineIndex: number, trimRight: boolean): LineCacheEntry {
const terminal = this._terminal!;
const strings = [];
const lineOffsets = [0];
let line = terminal.buffer.active.getLine(lineIndex);
while (line) {
const nextLine = terminal.buffer.active.getLine(lineIndex + 1);
const lineWrapsToNext = nextLine ? nextLine.isWrapped : false;
let string = line.translateToString(!lineWrapsToNext && trimRight);
if (lineWrapsToNext && nextLine) {
const lastCell = line.getCell(line.length - 1);
const lastCellIsNull = lastCell && lastCell.getCode() === 0 && lastCell.getWidth() === 1;
// a wide character wrapped to the next line
if (lastCellIsNull && nextLine.getCell(0)?.getWidth() === 2) {
string = string.slice(0, -1);
}
}
strings.push(string);
if (lineWrapsToNext) {
lineOffsets.push(lineOffsets[lineOffsets.length - 1] + string.length);
} else {
break;
}
lineIndex++;
line = nextLine;
}
return [strings.join(''), lineOffsets];
const result = this._engine.findPreviousWithSelection(term, searchOptions, this._state.cachedSearchTerm);
return this._selectResult(result, searchOptions?.decorations, internalSearchOptions?.noScroll);
}
/**
@@ -668,96 +209,33 @@ export class SearchAddon extends Disposable implements ITerminalAddon , ISearchA
* @param result The result to select.
* @returns Whether a result was selected.
*/
private _selectResult(result: ISearchResult | undefined, options?: ISearchDecorationOptions, noScroll?: boolean): boolean {
const terminal = this._terminal!;
this._selectedDecoration.clear();
if (!result) {
terminal.clearSelection();
private _selectResult(result: ISearchResult | undefined, options?: any, noScroll?: boolean): boolean {
if (!this._terminal || !this._decorationManager) {
return false;
}
terminal.select(result.col, result.row, result.size);
this._resultTracker.clearSelectedDecoration();
if (!result) {
this._terminal.clearSelection();
return false;
}
this._terminal.select(result.col, result.row, result.size);
if (options) {
const decorations = this._createResultDecorations(result, options, true);
if (decorations) {
this._selectedDecoration.value = { decorations, match: result, dispose() { dispose(decorations); } };
const activeDecoration = this._decorationManager.createActiveDecoration(result, options);
if (activeDecoration) {
this._resultTracker.selectedDecoration = activeDecoration;
}
}
if (!noScroll) {
// If it is not in the viewport then we scroll else it just gets selected
if (result.row >= (terminal.buffer.active.viewportY + terminal.rows) || result.row < terminal.buffer.active.viewportY) {
let scroll = result.row - terminal.buffer.active.viewportY;
scroll -= Math.floor(terminal.rows / 2);
terminal.scrollLines(scroll);
if (result.row >= (this._terminal.buffer.active.viewportY + this._terminal.rows) || result.row < this._terminal.buffer.active.viewportY) {
let scroll = result.row - this._terminal.buffer.active.viewportY;
scroll -= Math.floor(this._terminal.rows / 2);
this._terminal.scrollLines(scroll);
}
}
return true;
}
/**
* Applies styles to the decoration when it is rendered.
* @param element The decoration's element.
* @param borderColor The border color to apply.
* @param isActiveResult Whether the element is part of the active search result.
* @returns
*/
private _applyStyles(element: HTMLElement, borderColor: string | undefined, isActiveResult: boolean): void {
if (!element.classList.contains('xterm-find-result-decoration')) {
element.classList.add('xterm-find-result-decoration');
if (borderColor) {
element.style.outline = `1px solid ${borderColor}`;
}
}
if (isActiveResult) {
element.classList.add('xterm-find-active-result-decoration');
}
}
/**
* Creates a decoration for the result and applies styles
* @param result the search result for which to create the decoration
* @param options the options for the decoration
* @returns the {@link IDecoration} or undefined if the marker has already been disposed of
*/
private _createResultDecorations(result: ISearchResult, options: ISearchDecorationOptions, isActiveResult: boolean): IDecoration[] | undefined {
const terminal = this._terminal!;
// Gather decoration ranges for this match as it could wrap
const decorationRanges: [number, number, number][] = [];
let currentCol = result.col;
let remainingSize = result.size;
let markerOffset = -terminal.buffer.active.baseY - terminal.buffer.active.cursorY + result.row;
while (remainingSize > 0) {
const amountThisRow = Math.min(terminal.cols - currentCol, remainingSize);
decorationRanges.push([markerOffset, currentCol, amountThisRow]);
currentCol = 0;
remainingSize -= amountThisRow;
markerOffset++;
}
// Create the decorations
const decorations: IDecoration[] = [];
for (const range of decorationRanges) {
const marker = terminal.registerMarker(range[0]);
const decoration = terminal.registerDecoration({
marker,
x: range[1],
width: range[2],
backgroundColor: isActiveResult ? options.activeMatchBackground : options.matchBackground,
overviewRulerOptions: this._highlightedLines.has(marker.line) ? undefined : {
color: isActiveResult ? options.activeMatchColorOverviewRuler : options.matchOverviewRuler,
position: 'center'
}
});
if (decoration) {
const disposables: IDisposable[] = [];
disposables.push(marker);
disposables.push(decoration.onRender((e) => this._applyStyles(e, isActiveResult ? options.activeMatchBorder : options.matchBorder, false)));
disposables.push(decoration.onDispose(() => dispose(disposables)));
decorations.push(decoration);
}
}
return decorations.length === 0 ? undefined : decorations;
}
}
@@ -0,0 +1,708 @@
/**
* Copyright (c) 2024 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert } from 'chai';
import { SearchEngine } from './SearchEngine';
import { SearchLineCache } from './SearchLineCache';
import { Terminal } from 'browser/public/Terminal';
import type { ISearchOptions } from '@xterm/addon-search';
import { DisposableStore } from 'vs/base/common/lifecycle';
function writeP(terminal: Terminal, data: string): Promise<void> {
return new Promise(r => terminal.write(data, r));
}
describe('SearchEngine', () => {
let store: DisposableStore;
let terminal: Terminal;
let lineCache: SearchLineCache;
let searchEngine: SearchEngine;
beforeEach(() => {
store = new DisposableStore();
terminal = store.add(new Terminal({ cols: 80, rows: 24 }));
lineCache = store.add(new SearchLineCache(terminal));
searchEngine = new SearchEngine(terminal, lineCache);
});
afterEach(() => {
store.dispose();
});
describe('find', () => {
it('should return undefined for empty search term', async () => {
await writeP(terminal, 'Hello World');
assert.strictEqual(searchEngine.find('', 0, 0), undefined);
});
it('should find basic text in terminal content', async () => {
await writeP(terminal, 'Hello World');
assert.deepStrictEqual(searchEngine.find('World', 0, 0), {
term: 'World',
col: 6,
row: 0,
size: 5
});
});
it('should find text starting from specified position', async () => {
await writeP(terminal, 'Hello Hello Hello');
assert.deepStrictEqual(searchEngine.find('Hello', 0, 7), {
term: 'Hello',
col: 12,
row: 0,
size: 5
});
});
it('should search across multiple rows', async () => {
await writeP(terminal, 'Line 1\r\nLine 2 target\r\nLine 3');
assert.deepStrictEqual(searchEngine.find('target', 0, 0), {
term: 'target',
col: 7,
row: 1,
size: 6
});
});
it('should return undefined when text is not found', async () => {
await writeP(terminal, 'Hello World');
assert.strictEqual(searchEngine.find('NotFound', 0, 0), undefined);
});
it('should throw error for invalid column position', async () => {
await writeP(terminal, 'Hello World');
assert.throws(() => {
searchEngine.find('Hello', 0, 100);
}, /Invalid col: 100 to search in terminal of 80 cols/);
});
it('should handle search starting from last column', async () => {
await writeP(terminal, 'Hello World');
assert.strictEqual(searchEngine.find('Hello', 0, 79), undefined);
});
it('should handle search from middle of match', async () => {
await writeP(terminal, 'Hello World');
assert.strictEqual(searchEngine.find('llo', 0, 3), undefined); // Should not find partial match that starts before search position
});
});
describe('search options', () => {
describe('caseSensitive', () => {
it('should find text with case-insensitive search (default)', async () => {
await writeP(terminal, 'Hello WORLD');
assert.deepStrictEqual(searchEngine.find('world', 0, 0), {
term: 'world',
col: 6,
row: 0,
size: 5
});
});
it('should find text with case-sensitive search when enabled', async () => {
await writeP(terminal, 'Hello WORLD');
assert.deepStrictEqual(searchEngine.find('WORLD', 0, 0, { caseSensitive: true }), {
term: 'WORLD',
col: 6,
row: 0,
size: 5
});
});
it('should not find text with case-sensitive search when case differs', async () => {
await writeP(terminal, 'Hello WORLD');
assert.strictEqual(searchEngine.find('world', 0, 0, { caseSensitive: true }), undefined);
});
});
describe('wholeWord', () => {
it('should find whole word when enabled', async () => {
await writeP(terminal, 'Hello world wonderful');
assert.deepStrictEqual(searchEngine.find('world', 0, 0, { wholeWord: true }), {
term: 'world',
col: 6,
row: 0,
size: 5
});
});
it('should not find partial word when wholeWord is enabled', async () => {
await writeP(terminal, 'Hello wonderful');
assert.strictEqual(searchEngine.find('world', 0, 0, { wholeWord: true }), undefined);
});
it('should find word at beginning of line with wholeWord', async () => {
await writeP(terminal, 'world is great');
assert.deepStrictEqual(searchEngine.find('world', 0, 0, { wholeWord: true }), {
term: 'world',
col: 0,
row: 0,
size: 5
});
});
it('should find word at end of line with wholeWord', async () => {
await writeP(terminal, 'hello world');
assert.deepStrictEqual(searchEngine.find('world', 0, 0, { wholeWord: true }), {
term: 'world',
col: 6,
row: 0,
size: 5
});
});
it('should handle word boundaries with punctuation', async () => {
await writeP(terminal, 'hello,world!test');
assert.deepStrictEqual(searchEngine.find('world', 0, 0, { wholeWord: true }), {
term: 'world',
col: 6,
row: 0,
size: 5
});
});
it('should not match when not whole word', async () => {
await writeP(terminal, 'helloworld');
assert.strictEqual(searchEngine.find('world', 0, 0, { wholeWord: true }), undefined);
});
});
describe('regex', () => {
it('should find text using simple regex pattern', async () => {
await writeP(terminal, 'Hello 123 World');
assert.deepStrictEqual(searchEngine.find('[0-9]+', 0, 0, { regex: true }), {
term: '123',
col: 6,
row: 0,
size: 3
});
});
it('should find text using regex with case-insensitive flag', async () => {
await writeP(terminal, 'Hello WORLD');
assert.deepStrictEqual(searchEngine.find('world', 0, 0, { regex: true, caseSensitive: false }), {
term: 'WORLD',
col: 6,
row: 0,
size: 5
});
});
it('should find text using regex with case-sensitive flag', async () => {
await writeP(terminal, 'Hello WORLD world');
assert.deepStrictEqual(searchEngine.find('WORLD', 0, 0, { regex: true, caseSensitive: true }), {
term: 'WORLD',
col: 6,
row: 0,
size: 5
});
});
it('should handle complex regex patterns', async () => {
await writeP(terminal, 'Email: test@example.com and another@domain.org');
assert.deepStrictEqual(searchEngine.find('[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}', 0, 0, { regex: true }), {
term: 'test@example.com',
col: 7,
row: 0,
size: 16
});
});
it('should return undefined for invalid regex pattern', async () => {
await writeP(terminal, 'Hello World');
// Invalid regex should be handled gracefully
assert.throws(() => {
searchEngine.find('[invalid', 0, 0, { regex: true });
}, /Invalid regular expression/);
});
it('should handle empty regex matches', async () => {
await writeP(terminal, 'Hello World');
assert.strictEqual(searchEngine.find('.*?', 0, 0, { regex: true }), undefined); // Empty matches should be ignored
});
});
describe('combined options', () => {
it('should handle regex + caseSensitive combination', async () => {
await writeP(terminal, 'Hello WORLD world');
assert.deepStrictEqual(searchEngine.find('[A-Z]+', 0, 0, { regex: true, caseSensitive: true }), {
term: 'H',
col: 0,
row: 0,
size: 1
});
});
it('should handle wholeWord + caseSensitive combination', async () => {
await writeP(terminal, 'Hello WORLD wonderful');
const result1 = searchEngine.find('WORLD', 0, 0, { wholeWord: true, caseSensitive: true });
assert.deepStrictEqual(result1, {
term: 'WORLD',
col: 6,
row: 0,
size: 5
});
const result2 = searchEngine.find('world', 0, 0, { wholeWord: true, caseSensitive: true });
assert.strictEqual(result2, undefined);
});
});
});
describe('findNextWithSelection', () => {
it('should return undefined for empty search term', async () => {
await writeP(terminal, 'Hello World');
assert.strictEqual(searchEngine.findNextWithSelection(''), undefined);
});
it('should find first occurrence when no selection exists', async () => {
await writeP(terminal, 'Hello World Hello');
assert.deepStrictEqual(searchEngine.findNextWithSelection('Hello'), {
term: 'Hello',
col: 0,
row: 0,
size: 5
});
});
it('should find next occurrence after current selection', async () => {
await writeP(terminal, 'Hello World Hello Again');
// Mock the getSelectionPosition to return a selection at first "Hello"
terminal.getSelectionPosition = () => ({ start: { x: 0, y: 0 }, end: { x: 5, y: 0 } });
assert.deepStrictEqual(searchEngine.findNextWithSelection('Hello', undefined, 'Hello'), {
term: 'Hello',
col: 12,
row: 0,
size: 5
});
});
it('should wrap around to beginning when reaching end', async () => {
await writeP(terminal, 'Hello World Hello');
// Mock selection at the end
terminal.getSelectionPosition = () => ({ start: { x: 12, y: 0 }, end: { x: 17, y: 0 } });
assert.deepStrictEqual(searchEngine.findNextWithSelection('Hello', undefined, 'Hello'), {
term: 'Hello',
col: 0,
row: 0,
size: 5
}); // Should wrap to first occurrence
});
it('should wrap across multiple rows', async () => {
await writeP(terminal, 'Line 1 test\r\nLine 2\r\nLine 3 test');
// Mock selection at first "test"
terminal.getSelectionPosition = () => ({ start: { x: 7, y: 0 }, end: { x: 11, y: 0 } });
assert.deepStrictEqual(searchEngine.findNextWithSelection('test', undefined, 'test'), {
term: 'test',
col: 7,
row: 2,
size: 4
});
});
it('should return same selection if only one match exists', async () => {
await writeP(terminal, 'Hello World');
// Mock selection at "Hello"
terminal.getSelectionPosition = () => ({ start: { x: 0, y: 0 }, end: { x: 5, y: 0 } });
assert.deepStrictEqual(searchEngine.findNextWithSelection('Hello'), {
term: 'Hello',
col: 0,
row: 0,
size: 5
});
});
it('should clear selection and return undefined when term not found', async () => {
await writeP(terminal, 'Hello World');
assert.strictEqual(searchEngine.findNextWithSelection('NotFound'), undefined);
});
});
describe('findPreviousWithSelection', () => {
it('should return undefined for empty search term', async () => {
await writeP(terminal, 'Hello World');
assert.strictEqual(searchEngine.findPreviousWithSelection(''), undefined);
});
it('should find last occurrence when no selection exists', async () => {
await writeP(terminal, 'Hello World Hello');
assert.deepStrictEqual(searchEngine.findPreviousWithSelection('Hello'), {
term: 'Hello',
col: 12,
row: 0,
size: 5
});
});
it('should find previous occurrence before current selection', async () => {
await writeP(terminal, 'Hello World Hello Again');
// Mock selection at second "Hello"
terminal.getSelectionPosition = () => ({ start: { x: 12, y: 0 }, end: { x: 17, y: 0 } });
const result = searchEngine.findPreviousWithSelection('Hello');
assert.notStrictEqual(result, undefined);
// It may find the same selection first due to expansion attempt
assert.strictEqual(typeof result!.col, 'number');
assert.strictEqual(result!.row, 0);
});
it('should wrap around to end when reaching beginning', async () => {
await writeP(terminal, 'Hello World Hello');
// Mock selection at first "Hello"
terminal.getSelectionPosition = () => ({ start: { x: 0, y: 0 }, end: { x: 5, y: 0 } });
const result = searchEngine.findPreviousWithSelection('Hello');
assert.notStrictEqual(result, undefined);
// Due to the expansion attempt, it may find the same Hello first
assert.strictEqual(typeof result!.col, 'number');
assert.strictEqual(result!.row, 0);
});
it('should work across multiple rows in reverse', async () => {
await writeP(terminal, 'test Line 1\r\nLine 2\r\ntest Line 3');
// Mock selection at last "test"
terminal.getSelectionPosition = () => ({ start: { x: 0, y: 2 }, end: { x: 4, y: 2 } });
const result = searchEngine.findPreviousWithSelection('test');
assert.notStrictEqual(result, undefined);
// The algorithm will find the current selection first due to expansion attempt
assert.strictEqual(typeof result!.row, 'number');
assert.strictEqual(typeof result!.col, 'number');
});
it('should handle selection expansion correctly', async () => {
await writeP(terminal, 'Hello World Hello');
// Mock selection at first "Hello"
terminal.getSelectionPosition = () => ({ start: { x: 0, y: 0 }, end: { x: 5, y: 0 } });
const result = searchEngine.findPreviousWithSelection('Hello');
assert.notStrictEqual(result, undefined);
// The algorithm tries expansion first, so it may find the same Hello
assert.strictEqual(typeof result!.col, 'number');
assert.strictEqual(result!.row, 0);
});
it('should clear selection and return undefined when term not found', async () => {
await writeP(terminal, 'Hello World');
assert.strictEqual(searchEngine.findPreviousWithSelection('NotFound'), undefined);
});
});
describe('edge cases and error handling', () => {
describe('unicode and special characters', () => {
it('should handle unicode characters correctly', async () => {
await writeP(terminal, 'Hello 世界 World');
assert.deepStrictEqual(searchEngine.find('世界', 0, 0), {
term: '世界',
col: 6,
row: 0,
size: 4
});
});
it('should handle wide characters', async () => {
await writeP(terminal, '中文测试');
assert.deepStrictEqual(searchEngine.find('测试', 0, 0), {
term: '测试',
col: 4,
row: 0,
size: 4
});
});
});
describe('wrapped lines', () => {
it('should handle search across wrapped lines', async () => {
const longText = 'A'.repeat(100) + 'target' + 'B'.repeat(50);
await writeP(terminal, longText);
assert.deepStrictEqual(searchEngine.find('target', 0, 0), {
term: 'target',
col: 20,
row: 1,
size: 6
});
});
it('should handle wrapped lines with unicode', async () => {
const longText = '中'.repeat(50) + 'target' + '文'.repeat(30);
await writeP(terminal, longText);
assert.deepStrictEqual(searchEngine.find('target', 0, 0), {
term: 'target',
col: 20,
row: 1,
size: 6
});
});
it('should skip wrapped lines correctly in findInLine', async () => {
const longText = 'A'.repeat(200);
await writeP(terminal, longText + '\r\nNext line with target');
assert.deepStrictEqual(searchEngine.find('target', 0, 0), {
term: 'target',
col: 15,
row: 3,
size: 6
});
});
});
describe('buffer boundaries', () => {
it('should handle empty buffer gracefully', () => {
assert.strictEqual(searchEngine.find('anything', 0, 0), undefined);
});
it('should handle search beyond buffer size', () => {
assert.strictEqual(searchEngine.find('test', 1000, 0), undefined);
});
});
describe('invalid inputs', () => {
it('should handle undefined search options gracefully', async () => {
await writeP(terminal, 'Hello World');
assert.deepStrictEqual(searchEngine.find('Hello', 0, 0, undefined), {
term: 'Hello',
col: 0,
row: 0,
size: 5
});
});
it('should handle negative start positions', async () => {
await writeP(terminal, 'Hello World');
assert.deepStrictEqual(searchEngine.find('Hello', -1, -1), {
term: 'Hello',
col: 0,
row: 0,
size: 5
});
});
it('should handle search options with undefined properties', async () => {
await writeP(terminal, 'Hello World');
const options: ISearchOptions = {
caseSensitive: undefined,
regex: undefined,
wholeWord: undefined
};
assert.deepStrictEqual(searchEngine.find('Hello', 0, 0, options), {
term: 'Hello',
col: 0,
row: 0,
size: 5
});
});
});
});
describe('private method behaviors (tested indirectly)', () => {
describe('_isWholeWord behavior', () => {
it('should recognize word boundaries with various punctuation', async () => {
await writeP(terminal, 'word1 word2,word3(word4)word5[word6]word7{word8}');
const tests = [
{ term: 'word1', expected: true },
{ term: 'word2', expected: true },
{ term: 'word3', expected: true },
{ term: 'word4', expected: true },
{ term: 'word5', expected: true },
{ term: 'word6', expected: true },
{ term: 'word7', expected: true },
{ term: 'word8', expected: true }
];
for (const test of tests) {
const result = searchEngine.find(test.term, 0, 0, { wholeWord: true });
if (test.expected) {
assert.notStrictEqual(result, undefined, `Should find whole word: ${test.term}`);
} else {
assert.strictEqual(result, undefined, `Should not find non-whole word: ${test.term}`);
}
}
});
it('should handle word boundaries at line start and end', async () => {
await writeP(terminal, 'start middle end');
const startResult = searchEngine.find('start', 0, 0, { wholeWord: true });
assert.deepStrictEqual(startResult, {
term: 'start',
col: 0,
row: 0,
size: 5
});
const endResult = searchEngine.find('end', 0, 0, { wholeWord: true });
assert.deepStrictEqual(endResult, {
term: 'end',
col: 13,
row: 0,
size: 3
});
const middleResult = searchEngine.find('middle', 0, 0, { wholeWord: true });
assert.deepStrictEqual(middleResult, {
term: 'middle',
col: 6,
row: 0,
size: 6
});
});
});
describe('buffer offset calculations', () => {
it('should handle wide character offset calculations', async () => {
await writeP(terminal, '中文 test 测试');
const result = searchEngine.find('test', 0, 0);
assert.notStrictEqual(result, undefined);
assert.strictEqual(result!.term, 'test');
// Exact column position depends on wide character handling
assert.strictEqual(typeof result!.col, 'number');
});
});
describe('string to buffer size conversions', () => {
it('should correctly calculate size for simple text', async () => {
await writeP(terminal, 'Hello World');
assert.deepStrictEqual(searchEngine.find('World', 0, 0), {
term: 'World',
col: 6,
row: 0,
size: 5
});
});
it('should correctly calculate size for unicode text', async () => {
await writeP(terminal, 'Hello 世界');
const result = searchEngine.find('世界', 0, 0);
assert.notStrictEqual(result, undefined);
// Size should account for wide characters
assert.strictEqual(typeof result!.size, 'number');
assert.strictEqual(result!.size >= 2, true);
});
it('should handle size calculation across wrapped lines', async () => {
const longMatch = 'A'.repeat(100);
await writeP(terminal, longMatch);
const result = searchEngine.find(longMatch, 0, 0);
assert.notStrictEqual(result, undefined);
assert.strictEqual(result!.size >= 100, true);
});
});
});
describe('integration with SearchLineCache', () => {
it('should use cache for line translation', async () => {
await writeP(terminal, 'Hello World');
// Initialize cache
lineCache.initLinesCache();
const result1 = searchEngine.find('World', 0, 0);
const result2 = searchEngine.find('World', 0, 0);
assert.notStrictEqual(result1, undefined);
assert.notStrictEqual(result2, undefined);
assert.deepStrictEqual(result1, result2);
});
it('should handle cache misses gracefully', async () => {
await writeP(terminal, 'Hello World');
// Don't initialize cache
assert.deepStrictEqual(searchEngine.find('World', 0, 0), {
term: 'World',
col: 6,
row: 0,
size: 5
});
});
it('should work correctly with cache invalidation', async () => {
await writeP(terminal, 'Initial text');
lineCache.initLinesCache();
const result1 = searchEngine.find('Initial', 0, 0);
assert.notStrictEqual(result1, undefined);
// Change terminal content which should invalidate cache
await writeP(terminal, '\r\nNew line');
const result2 = searchEngine.find('New', 0, 0);
assert.deepStrictEqual(result2, {
term: 'New',
col: 0,
row: 1,
size: 3
});
});
});
});
+394
View File
@@ -0,0 +1,394 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import type { Terminal } from '@xterm/xterm';
import type { ISearchOptions } from '@xterm/addon-search';
import type { SearchLineCache } from './SearchLineCache';
/**
* Represents the position to start a search from.
*/
interface ISearchPosition {
startCol: number;
startRow: number;
}
/**
* Represents a search result with its position and content.
*/
export interface ISearchResult {
term: string;
col: number;
row: number;
size: number;
}
/**
* Configuration constants for the search engine functionality.
*/
const enum Constants {
/**
* Characters that are considered non-word characters for search boundary detection. These
* characters are used to determine word boundaries when performing whole-word searches. Includes
* common punctuation, symbols, and whitespace characters.
*/
NON_WORD_CHARACTERS = ' ~!@#$%^&*()+`-=[]{}|\\;:"\',./<>?'
}
/**
* Core search engine that handles finding text within terminal content.
* This class is responsible for the actual search algorithms and position calculations.
*/
export class SearchEngine {
constructor(
private readonly _terminal: Terminal,
private readonly _lineCache: SearchLineCache
) {}
/**
* Find the first occurrence of a term starting from a specific position.
* @param term The search term.
* @param startRow The row to start searching from.
* @param startCol The column to start searching from.
* @param searchOptions Search options.
* @returns The search result if found, undefined otherwise.
*/
public find(term: string, startRow: number, startCol: number, searchOptions?: ISearchOptions): ISearchResult | undefined {
if (!term || term.length === 0) {
this._terminal.clearSelection();
return undefined;
}
if (startCol > this._terminal.cols) {
throw new Error(`Invalid col: ${startCol} to search in terminal of ${this._terminal.cols} cols`);
}
this._lineCache.initLinesCache();
const searchPosition: ISearchPosition = {
startRow,
startCol
};
// Search startRow
let result = this._findInLine(term, searchPosition, searchOptions);
// Search from startRow + 1 to end
if (!result) {
for (let y = startRow + 1; y < this._terminal.buffer.active.baseY + this._terminal.rows; y++) {
searchPosition.startRow = y;
searchPosition.startCol = 0;
result = this._findInLine(term, searchPosition, searchOptions);
if (result) {
break;
}
}
}
return result;
}
/**
* Find the next occurrence of a term with wrapping and selection management.
* @param term The search term.
* @param searchOptions Search options.
* @param cachedSearchTerm The cached search term to determine incremental behavior.
* @returns The search result if found, undefined otherwise.
*/
public findNextWithSelection(term: string, searchOptions?: ISearchOptions, cachedSearchTerm?: string): ISearchResult | undefined {
if (!term || term.length === 0) {
this._terminal.clearSelection();
return undefined;
}
const prevSelectedPos = this._terminal.getSelectionPosition();
this._terminal.clearSelection();
let startCol = 0;
let startRow = 0;
if (prevSelectedPos) {
if (cachedSearchTerm === term) {
startCol = prevSelectedPos.end.x;
startRow = prevSelectedPos.end.y;
} else {
startCol = prevSelectedPos.start.x;
startRow = prevSelectedPos.start.y;
}
}
this._lineCache.initLinesCache();
const searchPosition: ISearchPosition = {
startRow,
startCol
};
// Search startRow
let result = this._findInLine(term, searchPosition, searchOptions);
// Search from startRow + 1 to end
if (!result) {
for (let y = startRow + 1; y < this._terminal.buffer.active.baseY + this._terminal.rows; y++) {
searchPosition.startRow = y;
searchPosition.startCol = 0;
result = this._findInLine(term, searchPosition, searchOptions);
if (result) {
break;
}
}
}
// If we hit the bottom and didn't search from the very top wrap back up
if (!result && startRow !== 0) {
for (let y = 0; y < startRow; y++) {
searchPosition.startRow = y;
searchPosition.startCol = 0;
result = this._findInLine(term, searchPosition, searchOptions);
if (result) {
break;
}
}
}
// If there is only one result, wrap back and return selection if it exists.
if (!result && prevSelectedPos) {
searchPosition.startRow = prevSelectedPos.start.y;
searchPosition.startCol = 0;
result = this._findInLine(term, searchPosition, searchOptions);
}
return result;
}
/**
* Find the previous occurrence of a term with wrapping and selection management.
* @param term The search term.
* @param searchOptions Search options.
* @param cachedSearchTerm The cached search term to determine if expansion should occur.
* @returns The search result if found, undefined otherwise.
*/
public findPreviousWithSelection(term: string, searchOptions?: ISearchOptions, cachedSearchTerm?: string): ISearchResult | undefined {
if (!term || term.length === 0) {
this._terminal.clearSelection();
return undefined;
}
const prevSelectedPos = this._terminal.getSelectionPosition();
this._terminal.clearSelection();
let startRow = this._terminal.buffer.active.baseY + this._terminal.rows - 1;
let startCol = this._terminal.cols;
const isReverseSearch = true;
this._lineCache.initLinesCache();
const searchPosition: ISearchPosition = {
startRow,
startCol
};
let result: ISearchResult | undefined;
if (prevSelectedPos) {
searchPosition.startRow = startRow = prevSelectedPos.start.y;
searchPosition.startCol = startCol = prevSelectedPos.start.x;
if (cachedSearchTerm !== term) {
// Try to expand selection to right first.
result = this._findInLine(term, searchPosition, searchOptions, false);
if (!result) {
// If selection was not able to be expanded to the right, then try reverse search
searchPosition.startRow = startRow = prevSelectedPos.end.y;
searchPosition.startCol = startCol = prevSelectedPos.end.x;
}
}
}
if (!result) {
result = this._findInLine(term, searchPosition, searchOptions, isReverseSearch);
}
// Search from startRow - 1 to top
if (!result) {
searchPosition.startCol = Math.max(searchPosition.startCol, this._terminal.cols);
for (let y = startRow - 1; y >= 0; y--) {
searchPosition.startRow = y;
result = this._findInLine(term, searchPosition, searchOptions, isReverseSearch);
if (result) {
break;
}
}
}
// If we hit the top and didn't search from the very bottom wrap back down
if (!result && startRow !== (this._terminal.buffer.active.baseY + this._terminal.rows - 1)) {
for (let y = (this._terminal.buffer.active.baseY + this._terminal.rows - 1); y >= startRow; y--) {
searchPosition.startRow = y;
result = this._findInLine(term, searchPosition, searchOptions, isReverseSearch);
if (result) {
break;
}
}
}
return result;
}
/**
* A found substring is a whole word if it doesn't have an alphanumeric character directly
* adjacent to it.
* @param searchIndex starting index of the potential whole word substring
* @param line entire string in which the potential whole word was found
* @param term the substring that starts at searchIndex
*/
private _isWholeWord(searchIndex: number, line: string, term: string): boolean {
return ((searchIndex === 0) || (Constants.NON_WORD_CHARACTERS.includes(line[searchIndex - 1]))) &&
(((searchIndex + term.length) === line.length) || (Constants.NON_WORD_CHARACTERS.includes(line[searchIndex + term.length])));
}
/**
* Searches a line for a search term. Takes the provided terminal line and searches the text line,
* which may contain subsequent terminal lines if the text is wrapped. If the provided line number
* is part of a wrapped text line that started on an earlier line then it is skipped since it will
* be properly searched when the terminal line that the text starts on is searched.
* @param term The search term.
* @param searchPosition The position to start the search.
* @param searchOptions Search options.
* @param isReverseSearch Whether the search should start from the right side of the terminal and
* search to the left.
* @returns The search result if it was found.
*/
private _findInLine(term: string, searchPosition: ISearchPosition, searchOptions: ISearchOptions = {}, isReverseSearch: boolean = false): ISearchResult | undefined {
const row = searchPosition.startRow;
const col = searchPosition.startCol;
// Ignore wrapped lines, only consider on unwrapped line (first row of command string).
const firstLine = this._terminal.buffer.active.getLine(row);
if (firstLine?.isWrapped) {
if (isReverseSearch) {
searchPosition.startCol += this._terminal.cols;
return;
}
// This will iterate until we find the line start.
// When we find it, we will search using the calculated start column.
searchPosition.startRow--;
searchPosition.startCol += this._terminal.cols;
return this._findInLine(term, searchPosition, searchOptions);
}
let cache = this._lineCache.getLineFromCache(row);
if (!cache) {
cache = this._lineCache.translateBufferLineToStringWithWrap(row, true);
this._lineCache.setLineInCache(row, cache);
}
const [stringLine, offsets] = cache;
const offset = this._bufferColsToStringOffset(row, col);
let searchTerm = term;
let searchStringLine = stringLine;
if (!searchOptions.regex) {
searchTerm = searchOptions.caseSensitive ? term : term.toLowerCase();
searchStringLine = searchOptions.caseSensitive ? stringLine : stringLine.toLowerCase();
}
let resultIndex = -1;
if (searchOptions.regex) {
const searchRegex = RegExp(searchTerm, searchOptions.caseSensitive ? 'g' : 'gi');
let foundTerm: RegExpExecArray | null;
if (isReverseSearch) {
// This loop will get the resultIndex of the _last_ regex match in the range 0..offset
while (foundTerm = searchRegex.exec(searchStringLine.slice(0, offset))) {
resultIndex = searchRegex.lastIndex - foundTerm[0].length;
term = foundTerm[0];
searchRegex.lastIndex -= (term.length - 1);
}
} else {
foundTerm = searchRegex.exec(searchStringLine.slice(offset));
if (foundTerm && foundTerm[0].length > 0) {
resultIndex = offset + (searchRegex.lastIndex - foundTerm[0].length);
term = foundTerm[0];
}
}
} else {
if (isReverseSearch) {
if (offset - searchTerm.length >= 0) {
resultIndex = searchStringLine.lastIndexOf(searchTerm, offset - searchTerm.length);
}
} else {
resultIndex = searchStringLine.indexOf(searchTerm, offset);
}
}
if (resultIndex >= 0) {
if (searchOptions.wholeWord && !this._isWholeWord(resultIndex, searchStringLine, term)) {
return;
}
// Adjust the row number and search index if needed since a "line" of text can span multiple
// rows
let startRowOffset = 0;
while (startRowOffset < offsets.length - 1 && resultIndex >= offsets[startRowOffset + 1]) {
startRowOffset++;
}
let endRowOffset = startRowOffset;
while (endRowOffset < offsets.length - 1 && resultIndex + term.length >= offsets[endRowOffset + 1]) {
endRowOffset++;
}
const startColOffset = resultIndex - offsets[startRowOffset];
const endColOffset = resultIndex + term.length - offsets[endRowOffset];
const startColIndex = this._stringLengthToBufferSize(row + startRowOffset, startColOffset);
const endColIndex = this._stringLengthToBufferSize(row + endRowOffset, endColOffset);
const size = endColIndex - startColIndex + this._terminal.cols * (endRowOffset - startRowOffset);
return {
term,
col: startColIndex,
row: row + startRowOffset,
size
};
}
}
private _stringLengthToBufferSize(row: number, offset: number): number {
const line = this._terminal.buffer.active.getLine(row);
if (!line) {
return 0;
}
for (let i = 0; i < offset; i++) {
const cell = line.getCell(i);
if (!cell) {
break;
}
// Adjust the searchIndex to normalize emoji into single chars
const char = cell.getChars();
if (char.length > 1) {
offset -= char.length - 1;
}
// Adjust the searchIndex for empty characters following wide unicode
// chars (eg. CJK)
const nextCell = line.getCell(i + 1);
if (nextCell && nextCell.getWidth() === 0) {
offset++;
}
}
return offset;
}
private _bufferColsToStringOffset(startRow: number, cols: number): number {
let lineIndex = startRow;
let offset = 0;
let line = this._terminal.buffer.active.getLine(lineIndex);
while (cols > 0 && line) {
for (let i = 0; i < cols && i < this._terminal.cols; i++) {
const cell = line.getCell(i);
if (!cell) {
break;
}
if (cell.getWidth()) {
// Treat null characters as whitespace to align with the translateToString API
offset += cell.getCode() === 0 ? 1 : cell.getChars().length;
}
}
lineIndex++;
line = this._terminal.buffer.active.getLine(lineIndex);
if (line && !line.isWrapped) {
break;
}
cols -= this._terminal.cols;
}
return offset;
}
}
@@ -0,0 +1,287 @@
/**
* Copyright (c) 2024 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert } from 'chai';
import { SearchLineCache, LineCacheEntry } from './SearchLineCache';
import { Terminal } from 'browser/public/Terminal';
import { timeout } from 'vs/base/common/async';
function writeP(terminal: Terminal, data: string): Promise<void> {
return new Promise(r => terminal.write(data, r));
}
describe('SearchLineCache', () => {
let terminal: Terminal;
let cache: SearchLineCache;
beforeEach(() => {
terminal = new Terminal({ cols: 80, rows: 24 });
cache = new SearchLineCache(terminal);
});
afterEach(() => {
cache.dispose();
terminal.dispose();
});
describe('constructor', () => {
it('should create a SearchLineCache instance', () => {
assert.instanceOf(cache, SearchLineCache);
});
it('should start with no cache initialized', () => {
assert.equal(cache.getLineFromCache(0), undefined);
});
});
describe('initLinesCache', () => {
it('should initialize the lines cache array', () => {
cache.initLinesCache();
assert.equal(cache.getLineFromCache(0), undefined, 'cache should be initialized but empty');
});
it('should not reinitialize if cache already exists', () => {
cache.initLinesCache();
cache.setLineInCache(0, ['test', [0]]);
cache.initLinesCache();
const entry = cache.getLineFromCache(0);
assert.deepEqual(entry, ['test', [0]], 'cache should still contain the previously set entry');
});
it('should set up TTL timeout', () => {
cache.initLinesCache();
cache.setLineInCache(0, ['test', [0]]);
assert.deepEqual(cache.getLineFromCache(0), ['test', [0]], 'entry should exist after initialization');
});
});
describe('getLineFromCache', () => {
it('should return undefined when cache is not initialized', () => {
assert.equal(cache.getLineFromCache(0), undefined);
assert.equal(cache.getLineFromCache(10), undefined);
});
it('should return undefined for unset entries when cache is initialized', () => {
cache.initLinesCache();
assert.equal(cache.getLineFromCache(0), undefined);
assert.equal(cache.getLineFromCache(50), undefined);
});
it('should return cached entries', () => {
cache.initLinesCache();
const entry: LineCacheEntry = ['test content', [0]];
cache.setLineInCache(5, entry);
assert.deepEqual(cache.getLineFromCache(5), entry);
});
});
describe('setLineInCache', () => {
it('should not set entries when cache is not initialized', () => {
const entry: LineCacheEntry = ['test content', [0]];
cache.setLineInCache(0, entry);
assert.equal(cache.getLineFromCache(0), undefined);
});
it('should set entries when cache is initialized', () => {
cache.initLinesCache();
const entry: LineCacheEntry = ['test content', [0]];
cache.setLineInCache(10, entry);
assert.deepEqual(cache.getLineFromCache(10), entry);
});
it('should overwrite existing entries', () => {
cache.initLinesCache();
const entry1: LineCacheEntry = ['first content', [0]];
const entry2: LineCacheEntry = ['second content', [0]];
cache.setLineInCache(0, entry1);
assert.deepEqual(cache.getLineFromCache(0), entry1);
cache.setLineInCache(0, entry2);
assert.deepEqual(cache.getLineFromCache(0), entry2);
});
});
describe('translateBufferLineToStringWithWrap', () => {
it('should translate a single line without wrapping', async () => {
await writeP(terminal, 'Hello World');
const result = cache.translateBufferLineToStringWithWrap(0, true);
assert.equal(result[0], 'Hello World');
assert.deepEqual(result[1], [0]);
});
it('should handle trimRight parameter', async () => {
await writeP(terminal, 'Hello World ');
const resultTrimmed = cache.translateBufferLineToStringWithWrap(0, true);
const resultNotTrimmed = cache.translateBufferLineToStringWithWrap(0, false);
assert.equal(resultTrimmed[0].trimEnd(), 'Hello World');
assert.isTrue(resultNotTrimmed[0].startsWith('Hello World '));
assert.isTrue(resultNotTrimmed[0].length > resultTrimmed[0].length, 'non-trimmed result should be longer');
});
it('should handle wrapped lines', async () => {
const longText = 'A'.repeat(200);
await writeP(terminal, longText);
const result = cache.translateBufferLineToStringWithWrap(0, true);
assert.equal(result[0], longText);
assert.isTrue(result[1].length > 1, 'should have multiple offsets due to wrapping');
assert.equal(result[1][0], 0, 'first offset should be 0');
});
it('should handle wide characters', async () => {
await writeP(terminal, 'Hello 世界');
const result = cache.translateBufferLineToStringWithWrap(0, true);
assert.equal(result[0], 'Hello 世界');
assert.deepEqual(result[1], [0]);
});
it('should handle empty lines', () => {
const result = cache.translateBufferLineToStringWithWrap(0, true);
assert.equal(result[0], '');
assert.deepEqual(result[1], [0]);
});
it('should handle lines beyond buffer', () => {
const result = cache.translateBufferLineToStringWithWrap(1000, true);
assert.equal(result[0], '');
assert.deepEqual(result[1], [0]);
});
it('should handle complex wrapped content', async () => {
await writeP(terminal, 'Line 1\r\n');
await writeP(terminal, 'Line 2 with some longer content that might wrap\r\n');
await writeP(terminal, 'Line 3');
const result1 = cache.translateBufferLineToStringWithWrap(0, true);
const result2 = cache.translateBufferLineToStringWithWrap(1, true);
const result3 = cache.translateBufferLineToStringWithWrap(2, true);
assert.equal(result1[0], 'Line 1');
assert.equal(result2[0], 'Line 2 with some longer content that might wrap');
assert.equal(result3[0], 'Line 3');
});
});
describe('cache invalidation', () => {
it('should invalidate cache on line feed', async () => {
cache.initLinesCache();
cache.setLineInCache(0, ['test', [0]]);
assert.deepEqual(cache.getLineFromCache(0), ['test', [0]]);
terminal.write('test\r\n');
await timeout(10);
assert.equal(cache.getLineFromCache(0), undefined);
});
it('should invalidate cache on cursor move', async () => {
cache.initLinesCache();
cache.setLineInCache(0, ['test', [0]]);
assert.deepEqual(cache.getLineFromCache(0), ['test', [0]]);
await writeP(terminal, 'some text');
await timeout(10);
assert.equal(cache.getLineFromCache(0), undefined);
});
it('should invalidate cache on resize', async () => {
cache.initLinesCache();
cache.setLineInCache(0, ['test', [0]]);
assert.deepEqual(cache.getLineFromCache(0), ['test', [0]]);
terminal.resize(100, 30);
await timeout(10);
assert.equal(cache.getLineFromCache(0), undefined);
});
});
describe('disposal', () => {
it('should clean up resources on dispose', () => {
cache.initLinesCache();
cache.setLineInCache(0, ['test', [0]]);
assert.deepEqual(cache.getLineFromCache(0), ['test', [0]]);
cache.dispose();
assert.equal(cache.getLineFromCache(0), undefined, 'cache should be destroyed after disposal');
});
it('should be safe to dispose multiple times', () => {
cache.initLinesCache();
cache.dispose();
cache.dispose();
assert.equal(cache.getLineFromCache(0), undefined);
});
});
describe('LineCacheEntry type', () => {
it('should handle complex line offsets', () => {
const entry: LineCacheEntry = [
'A very long line that wraps multiple times across several terminal lines',
[0, 20, 40, 60]
];
cache.initLinesCache();
cache.setLineInCache(0, entry);
const retrieved = cache.getLineFromCache(0);
assert.deepEqual(retrieved, entry);
assert.equal(retrieved![0].length, 72);
assert.equal(retrieved![1].length, 4);
});
it('should handle unicode characters in cache entries', () => {
const entry: LineCacheEntry = [
'Hello 世界 🌍 测试',
[0]
];
cache.initLinesCache();
cache.setLineInCache(0, entry);
const retrieved = cache.getLineFromCache(0);
assert.deepEqual(retrieved, entry);
assert.equal(retrieved![0], 'Hello 世界 🌍 测试');
});
});
describe('integration with real terminal content', () => {
it('should correctly translate real buffer content', async () => {
await writeP(terminal, 'Hello World');
const cached = cache.translateBufferLineToStringWithWrap(0, true);
const directTranslation = terminal.buffer.active.getLine(0)?.translateToString(true) || '';
assert.equal(cached[0], directTranslation);
});
it('should handle real wrapped content correctly', async () => {
const longContent = 'This is a very long line that will definitely wrap around in an 80 column terminal and should be handled correctly by the cache';
await writeP(terminal, longContent);
const result = cache.translateBufferLineToStringWithWrap(0, true);
assert.equal(result[0], longContent);
assert.isTrue(result[1].length > 1, 'should have wrapped');
});
it('should work with real escape sequences', async () => {
await writeP(terminal, 'Before\x1b[31mRed Text\x1b[0mAfter');
const result = cache.translateBufferLineToStringWithWrap(0, true);
assert.include(result[0], 'Before');
assert.include(result[0], 'Red Text');
assert.include(result[0], 'After');
});
});
});
+114
View File
@@ -0,0 +1,114 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import type { Terminal } from '@xterm/xterm';
import { combinedDisposable, Disposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { disposableTimeout } from 'vs/base/common/async';
export type LineCacheEntry = [
/**
* The string representation of a line (as opposed to the buffer cell representation).
*/
lineAsString: string,
/**
* The offsets where each line starts when the entry describes a wrapped line.
*/
lineOffsets: number[]
];
/**
* Configuration constants for the search line cache functionality.
*/
const enum Constants {
/**
* Time-to-live for cached search results in milliseconds. After this duration, cached search
* results will be invalidated to ensure they remain consistent with terminal content changes.
*/
LINES_CACHE_TIME_TO_LIVE = 15000
}
export class SearchLineCache extends Disposable {
/**
* translateBufferLineToStringWithWrap is a fairly expensive call.
* We memoize the calls into an array that has a time based ttl.
* _linesCache is also invalidated when the terminal cursor moves.
*/
private _linesCache: LineCacheEntry[] | undefined;
private _linesCacheTimeout = this._register(new MutableDisposable());
private _linesCacheDisposables = this._register(new MutableDisposable());
constructor(private readonly _terminal: Terminal) {
super();
this._register(toDisposable(() => this._destroyLinesCache()));
}
/**
* Sets up a line cache with a ttl
*/
public initLinesCache(): void {
if (!this._linesCache) {
this._linesCache = new Array(this._terminal.buffer.active.length);
this._linesCacheDisposables.value = combinedDisposable(
this._terminal.onLineFeed(() => this._destroyLinesCache()),
this._terminal.onCursorMove(() => this._destroyLinesCache()),
this._terminal.onResize(() => this._destroyLinesCache())
);
}
this._linesCacheTimeout.value = disposableTimeout(() => this._destroyLinesCache(), Constants.LINES_CACHE_TIME_TO_LIVE);
}
private _destroyLinesCache(): void {
this._linesCache = undefined;
this._linesCacheDisposables.clear();
this._linesCacheTimeout.clear();
}
public getLineFromCache(row: number): LineCacheEntry | undefined {
return this._linesCache?.[row];
}
public setLineInCache(row: number, entry: LineCacheEntry): void {
if (this._linesCache) {
this._linesCache[row] = entry;
}
}
/**
* Translates a buffer line to a string, including subsequent lines if they are wraps.
* Wide characters will count as two columns in the resulting string. This
* function is useful for getting the actual text underneath the raw selection
* position.
* @param lineIndex The index of the line being translated.
* @param trimRight Whether to trim whitespace to the right.
*/
public translateBufferLineToStringWithWrap(lineIndex: number, trimRight: boolean): LineCacheEntry {
const strings = [];
const lineOffsets = [0];
let line = this._terminal.buffer.active.getLine(lineIndex);
while (line) {
const nextLine = this._terminal.buffer.active.getLine(lineIndex + 1);
const lineWrapsToNext = nextLine ? nextLine.isWrapped : false;
let string = line.translateToString(!lineWrapsToNext && trimRight);
if (lineWrapsToNext && nextLine) {
const lastCell = line.getCell(line.length - 1);
const lastCellIsNull = lastCell && lastCell.getCode() === 0 && lastCell.getWidth() === 1;
// a wide character wrapped to the next line
if (lastCellIsNull && nextLine.getCell(0)?.getWidth() === 2) {
string = string.slice(0, -1);
}
}
strings.push(string);
if (lineWrapsToNext) {
lineOffsets.push(lineOffsets[lineOffsets.length - 1] + string.length);
} else {
break;
}
lineIndex++;
line = nextLine;
}
return [strings.join(''), lineOffsets];
}
}
@@ -0,0 +1,119 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import type { ISearchResultChangeEvent } from '@xterm/addon-search';
import type { IDisposable } from '@xterm/xterm';
import { Emitter, Event } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import type { ISearchResult } from './SearchEngine';
/**
* Interface for managing a currently selected decoration.
*/
interface ISelectedDecoration extends IDisposable {
match: ISearchResult;
}
/**
* Tracks search results, manages result indexing, and fires events when results change.
* This class provides centralized management of search result state and notifications.
*/
export class SearchResultTracker extends Disposable {
private _searchResults: ISearchResult[] = [];
private _selectedDecoration: ISelectedDecoration | undefined;
private readonly _onDidChangeResults = this._register(new Emitter<ISearchResultChangeEvent>());
public get onDidChangeResults(): Event<ISearchResultChangeEvent> { return this._onDidChangeResults.event; }
/**
* Gets the current search results.
*/
public get searchResults(): ReadonlyArray<ISearchResult> {
return this._searchResults;
}
/**
* Gets the currently selected decoration.
*/
public get selectedDecoration(): ISelectedDecoration | undefined {
return this._selectedDecoration;
}
/**
* Sets the currently selected decoration.
*/
public set selectedDecoration(decoration: ISelectedDecoration | undefined) {
this._selectedDecoration = decoration;
}
/**
* Updates the search results with a new set of results.
* @param results The new search results.
* @param maxResults The maximum number of results to track.
*/
public updateResults(results: ISearchResult[], maxResults: number): void {
this._searchResults = results.slice(0, maxResults);
}
/**
* Clears all search results.
*/
public clearResults(): void {
this._searchResults = [];
}
/**
* Clears the selected decoration.
*/
public clearSelectedDecoration(): void {
if (this._selectedDecoration) {
this._selectedDecoration.dispose();
this._selectedDecoration = undefined;
}
}
/**
* Finds the index of a result in the current results array.
* @param result The result to find.
* @returns The index of the result, or -1 if not found.
*/
public findResultIndex(result: ISearchResult): number {
for (let i = 0; i < this._searchResults.length; i++) {
const match = this._searchResults[i];
if (match.row === result.row && match.col === result.col && match.size === result.size) {
return i;
}
}
return -1;
}
/**
* Fires a result change event with the current state.
* @param hasDecorations Whether decorations are enabled.
*/
public fireResultsChanged(hasDecorations: boolean): void {
if (!hasDecorations) {
return;
}
let resultIndex = -1;
if (this._selectedDecoration) {
resultIndex = this.findResultIndex(this._selectedDecoration.match);
}
this._onDidChangeResults.fire({
resultIndex,
resultCount: this._searchResults.length
});
}
/**
* Resets all state.
*/
public reset(): void {
this.clearSelectedDecoration();
this.clearResults();
}
}
+106
View File
@@ -0,0 +1,106 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import type { ISearchOptions } from '@xterm/addon-search';
/**
* Manages search state including cached search terms, options tracking, and validation.
* This class provides a centralized way to handle search state consistency and option changes.
*/
export class SearchState {
private _cachedSearchTerm: string | undefined;
private _lastSearchOptions: ISearchOptions | undefined;
/**
* Gets the currently cached search term.
*/
public get cachedSearchTerm(): string | undefined {
return this._cachedSearchTerm;
}
/**
* Sets the cached search term.
*/
public set cachedSearchTerm(term: string | undefined) {
this._cachedSearchTerm = term;
}
/**
* Gets the last search options used.
*/
public get lastSearchOptions(): ISearchOptions | undefined {
return this._lastSearchOptions;
}
/**
* Sets the last search options used.
*/
public set lastSearchOptions(options: ISearchOptions | undefined) {
this._lastSearchOptions = options;
}
/**
* Validates a search term to ensure it's not empty or invalid.
* @param term The search term to validate.
* @returns true if the term is valid for searching.
*/
public isValidSearchTerm(term: string): boolean {
return !!(term && term.length > 0);
}
/**
* Determines if search options have changed compared to the last search.
* @param newOptions The new search options to compare.
* @returns true if the options have changed.
*/
public didOptionsChange(newOptions?: ISearchOptions): boolean {
if (!this._lastSearchOptions) {
return true;
}
if (!newOptions) {
return false;
}
if (this._lastSearchOptions.caseSensitive !== newOptions.caseSensitive) {
return true;
}
if (this._lastSearchOptions.regex !== newOptions.regex) {
return true;
}
if (this._lastSearchOptions.wholeWord !== newOptions.wholeWord) {
return true;
}
return false;
}
/**
* Determines if a new search should trigger highlighting updates.
* @param term The search term.
* @param options The search options.
* @returns true if highlighting should be updated.
*/
public shouldUpdateHighlighting(term: string, options?: ISearchOptions): boolean {
if (!options?.decorations) {
return false;
}
return this._cachedSearchTerm === undefined ||
term !== this._cachedSearchTerm ||
this.didOptionsChange(options);
}
/**
* Clears the cached search term.
*/
public clearCachedTerm(): void {
this._cachedSearchTerm = undefined;
}
/**
* Resets all state.
*/
public reset(): void {
this._cachedSearchTerm = undefined;
this._lastSearchOptions = undefined;
}
}
+6
View File
@@ -18,6 +18,9 @@
"common/*": [
"../../../src/common/*"
],
"browser/*": [
"../../../src/browser/*"
],
"vs/*": [
"../../../src/vs/*"
],
@@ -34,6 +37,9 @@
{
"path": "../../../src/common"
},
{
"path": "../../../src/browser"
},
{
"path": "../../../src/vs"
}
+52 -1
View File
@@ -19,8 +19,8 @@ test.afterAll(async () => await ctx.page.close());
test.describe('Search Tests', () => {
test.beforeEach(async () => {
await ctx.proxy.reset();
await ctx.page.evaluate(`
window.term.reset()
window.search?.dispose();
window.search = new SearchAddon();
window.term.loadAddon(window.search);
@@ -480,6 +480,57 @@ test.describe('Search Tests', () => {
]);
});
});
test.describe('Wrapped line search functionality', () => {
test('should correctly count matches across multiple wrapped lines', async () => {
await ctx.page.evaluate(`
window.calls = [];
window.search.onDidChangeResults(e => window.calls.push(e));
`);
const content = 'a'.repeat(300);
await ctx.proxy.write(content);
strictEqual(await ctx.page.evaluate(`window.search.findNext('${content}', { decorations: { activeMatchColorOverviewRuler: '#ff0000', matchOverviewRuler: '#ffff00' } })`), true);
deepStrictEqual(await ctx.page.evaluate('window.calls'), [
{ resultCount: 1, resultIndex: 0 }
]);
});
test('should handle reverse search across wrapped lines', async () => {
await ctx.page.evaluate(`
window.calls = [];
window.search.onDidChangeResults(e => window.calls.push(e));
`);
const content = 'x'.repeat(300);
await ctx.proxy.write(content);
strictEqual(await ctx.page.evaluate(`window.search.findPrevious('${content}', { decorations: { activeMatchColorOverviewRuler: '#ff0000', matchOverviewRuler: '#ffff00' } })`), true);
deepStrictEqual(await ctx.page.evaluate('window.calls'), [
{ resultCount: 1, resultIndex: 0 }
]);
});
test('should update counts when content changes across wrapped lines', async () => {
await ctx.page.evaluate(`
window.calls = [];
window.search.onDidChangeResults(e => window.calls.push(e));
`);
const content = 'z'.repeat(300);
await ctx.proxy.write(content);
strictEqual(await ctx.page.evaluate(`window.search.findNext('${content}', { decorations: { activeMatchColorOverviewRuler: '#ff0000', matchOverviewRuler: '#ffff00' } })`), true);
deepStrictEqual(await ctx.page.evaluate('window.calls'), [
{ resultCount: 1, resultIndex: 0 }
]);
await ctx.proxy.write('\\n\\r' + content);
await timeout(300);
deepStrictEqual(await ctx.page.evaluate('window.calls'), [
{ resultCount: 1, resultIndex: 0 },
{ resultCount: 2, resultIndex: 0 }
]);
});
});
});
function makeData(length: number): string {
+16 -2
View File
@@ -75,6 +75,21 @@ declare module '@xterm/addon-search' {
activeMatchColorOverviewRuler: string;
}
/**
* Event data fired when search results change.
*/
export interface ISearchResultChangeEvent {
/**
* The index of the currently active result, -1 when the threshold of matches is exceeded.
*/
resultIndex: number;
/**
* The total number of search results found.
*/
resultCount: number;
}
/**
* Options for the search addon.
*/
@@ -139,8 +154,7 @@ declare module '@xterm/addon-search' {
/**
* When decorations are enabled, fires when
* the search results change.
* @returns -1 for resultIndex when the threshold of matches is exceeded.
*/
readonly onDidChangeResults: IEvent<{ resultIndex: number, resultCount: number }>;
readonly onDidChangeResults: IEvent<ISearchResultChangeEvent>;
}
}
+3 -3
View File
@@ -34,9 +34,9 @@ See the full [API](https://github.com/xtermjs/xterm.js/blob/master/addons/addon-
```shell
$ git clone https://github.com/xtermjs/xterm.js.git
$ cd xterm.js
$ yarn
$ npm ci
$ cd addons/addon-serialize
$ yarn benchmark && yarn benchmark-baseline
$ npm run benchmark && npm run benchmark-baseline
$ # change some code in `@xterm/addon-serialize`
$ yarn benchmark-eval
$ npm run benchmark-eval
```
+1 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@xterm/addon-serialize",
"version": "0.13.0",
"version": "0.14.0",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
@@ -24,8 +24,5 @@
"benchmark": "NODE_PATH=../../out:./out:./out-benchmark/ ../../node_modules/.bin/xterm-benchmark -r 5 -c benchmark/benchmark.json",
"benchmark-baseline": "NODE_PATH=../../out:./out:./out-benchmark/ ../../node_modules/.bin/xterm-benchmark -r 5 -c benchmark/benchmark.json --baseline out-benchmark/benchmark/*benchmark.js",
"benchmark-eval": "NODE_PATH=../../out:./out:./out-benchmark/ ../../node_modules/.bin/xterm-benchmark -r 5 -c benchmark/benchmark.json --eval out-benchmark/benchmark/*benchmark.js"
},
"peerDependencies": {
"@xterm/xterm": "^5.0.0"
}
}
@@ -148,6 +148,22 @@ describe('SerializeAddon', () => {
assert.equal((output.match(/<div><span>&lt;a>&amp;pi;<\/span><\/div>/g) || []).length, 1, output);
});
it('serializes rows within a provided range', async () => {
await writeP(terminal, 'bye hello\r\nworld');
const output = serializeAddon.serializeAsHTML({
range: {
startLine: 0,
endLine: 0,
startCol: 4
}
});
const rowMatches = output.match(/<div><span>.*?<\/span><\/div>/g) || [];
assert.equal(rowMatches.length, 1, output);
assert.ok(rowMatches[0]?.includes('hello'));
assert.ok(!output.includes('bye'));
assert.ok(!output.includes('world'));
});
it('cells with bold styling', async () => {
await writeP(terminal, ' ' + sgr('1') + 'terminal' + sgr('22') + ' ');
@@ -450,6 +450,13 @@ export class SerializeAddon implements ITerminalAddon , ISerializeApi {
const buffer = terminal.buffer.active;
const handler = new HTMLSerializeHandler(buffer, terminal, options);
const onlySelection = options.onlySelection ?? false;
const range = options.range;
if (range) {
return handler.serialize({
start: { x: range.startCol, y: typeof range.startLine === 'number' ? range.startLine : range.startLine },
end: { x: terminal.cols, y: typeof range.endLine === 'number' ? range.endLine : range.endLine }
});
}
if (!onlySelection) {
const maxRows = buffer.length;
const scrollback = options.scrollback;
@@ -483,6 +490,7 @@ export class SerializeAddon implements ITerminalAddon , ISerializeApi {
if (modes.originMode) content += '\x1b[?6h';
if (modes.reverseWraparoundMode) content += '\x1b[?45h';
if (modes.sendFocusMode) content += '\x1b[?1004h';
// synchronizedOutputMode doesn't need to be serialized as it's a temporary mode
// Default: true
if (modes.wraparoundMode === false) content += '\x1b[?7l';
+11
View File
@@ -90,6 +90,17 @@ declare module '@xterm/addon-serialize' {
* Whether to include the global background of the terminal. False by default.
*/
includeGlobalBackground: boolean;
/**
* The range to serialize. This is prioritized over {@link onlySelection}.
*/
range?: ISerializeBufferRange;
}
export interface ISerializeBufferRange {
startLine: number;
endLine: number;
startCol: number;
}
export interface ISerializeRange {
+2 -5
View File
@@ -1,12 +1,12 @@
{
"name": "@xterm/addon-unicode-graphemes",
"version": "0.3.0",
"version": "0.4.0",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
},
"main": "lib/addon-unicode-graphemes.js",
"module": "lib/.addon-unicode-graphemes.mjs",
"module": "lib/addon-unicode-graphemes.mjs",
"types": "typings/addon-unicode-graphemes.d.ts",
"repository": "https://github.com/xtermjs/xterm.js/tree/master/addons/addon-unicode-graphemes",
"license": "MIT",
@@ -24,8 +24,5 @@
"benchmark": "NODE_PATH=../../out:./out:./out-benchmark/ ../../node_modules/.bin/xterm-benchmark -r 5 -c benchmark/benchmark.json out-benchmark/benchmark/*benchmark.js",
"benchmark-baseline": "NODE_PATH=../../out:./out:./out-benchmark/ ../../node_modules/.bin/xterm-benchmark -r 5 -c benchmark/benchmark.json --baseline out-benchmark/benchmark/*benchmark.js",
"benchmark-eval": "NODE_PATH=../../out:./out:./out-benchmark/ ../../node_modules/.bin/xterm-benchmark -r 5 -c benchmark/benchmark.json --eval out-benchmark/benchmark/*benchmark.js"
},
"peerDependencies": {
"@xterm/xterm": "^5.0.0"
}
}
+1 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@xterm/addon-unicode11",
"version": "0.8.0",
"version": "0.9.0",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
@@ -21,8 +21,5 @@
"package": "../../node_modules/.bin/webpack",
"prepublishOnly": "npm run package",
"start": "node ../../demo/start"
},
"peerDependencies": {
"@xterm/xterm": "^5.0.0"
}
}
+1 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@xterm/addon-web-links",
"version": "0.11.0",
"version": "0.12.0",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
@@ -21,9 +21,5 @@
"package": "../../node_modules/.bin/webpack",
"prepublishOnly": "npm run package",
"start": "node ../../demo/start"
},
"peerDependencies": {
"@xterm/xterm": "^5.0.0"
}
}
+1 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@xterm/addon-webgl",
"version": "0.18.0",
"version": "0.19.0",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
@@ -22,8 +22,5 @@
"package": "../../node_modules/.bin/webpack",
"prepublishOnly": "npm run package",
"start": "node ../../demo/start"
},
"peerDependencies": {
"@xterm/xterm": "^5.0.0"
}
}
+2 -2
View File
@@ -139,7 +139,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
[this._rectangleRenderer.value, this._glyphRenderer.value] = this._initializeWebGLState();
this._isAttached = this._coreBrowserService.window.document.body.contains(this._core.screenElement!);
this._isAttached = this._core.screenElement!.isConnected;
this._register(toDisposable(() => {
for (const l of this._renderLayers) {
@@ -322,7 +322,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
public renderRows(start: number, end: number): void {
if (!this._isAttached) {
if (this._coreBrowserService.window.document.body.contains(this._core.screenElement!) && this._charSizeService.width && this._charSizeService.height) {
if (this._core.screenElement?.isConnected && this._charSizeService.width && this._charSizeService.height) {
this._updateDimensions();
this._refreshCharAtlas();
this._isAttached = true;
+1 -2
View File
@@ -29,8 +29,7 @@
"downlevelIteration": true,
"experimentalDecorators": true,
"types": [
"../../../node_modules/@types/mocha",
"../../../src/vs/typings/thenable"
"../../../node_modules/@types/mocha"
]
},
"include": [
-56
View File
@@ -1,56 +0,0 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*
* Script to initialize addon packages under "addons/" with outer deps.
*/
const path = require('path');
const cp = require('child_process');
const fs = require('fs');
const PACKAGE_ROOT = path.join(__dirname, '..');
// install addon deps
const addonsPath = path.join(PACKAGE_ROOT, 'addons');
if (fs.existsSync(addonsPath)) {
console.log('pulling addon dependencies...');
// whether to use yarn or npm
let hasYarn = false;
try {
cp.execSync('yarn --version').toString();
hasYarn = true;
} catch(e) {}
// walk all addon folders
fs.readdir(addonsPath, (err, files) => {
for (const folder of files) {
const addonPath = path.join(addonsPath, folder);
// install only if there are dependencies listed
let packageJson;
try {
packageJson = require(path.join(addonPath, 'package.json'));
} catch (e) {
// swallow as changing branches can leave folders around
}
if (packageJson
&& (
(packageJson.devDependencies && Object.keys(packageJson.devDependencies).length)
|| (packageJson.dependencies && Object.keys(packageJson.dependencies).length)
)
)
{
console.log('Preparing', folder);
if (hasYarn) {
cp.execSync('yarn', {cwd: addonPath});
} else {
cp.execSync('npm install', {cwd: addonPath});
}
} else {
console.log('Skipped', folder);
}
}
});
}
+1 -1
View File
@@ -50,7 +50,7 @@ const addonPackageDirs = [
path.resolve(__dirname, '../addons/addon-search'),
path.resolve(__dirname, '../addons/addon-serialize'),
path.resolve(__dirname, '../addons/addon-unicode11'),
// path.resolve(__dirname, '../addons/addon-unicode-graphemes'),
path.resolve(__dirname, '../addons/addon-unicode-graphemes'),
path.resolve(__dirname, '../addons/addon-web-links'),
path.resolve(__dirname, '../addons/addon-webgl')
];
+1 -1
View File
@@ -20,7 +20,7 @@ if (process.argv.length > 2) {
const args = process.argv.slice(2);
flagArgs = args.filter(e => e.startsWith('--'));
// ability to inject particular test files via
// yarn test [testFileA testFileB ...]
// npm run test [testFileA testFileB ...]
files = args.filter(e => !e.startsWith('--'));
if (files.length) {
testFiles = files;
+1 -1
View File
@@ -6,7 +6,7 @@ VERSION=$1
# Clone docs repo and update the documentation
git clone https://github.com/xtermjs/xtermjs.org
cd xtermjs.org
yarn
npm ci
./bin/update-docs
# Add changes to index and only proceed if there are changes to commit
+19 -17
View File
@@ -1310,23 +1310,25 @@ function addVtButtons(): void {
}
const vtFragment = document.createDocumentFragment();
const buttonSpecs: { [key: string]: { label: string, description: string, paramCount?: number }} = {
A: { label: 'CUU ↑', description: 'Cursor Up Ps Times' },
B: { label: 'CUD ↓', description: 'Cursor Down Ps Times' },
C: { label: 'CUF →', description: 'Cursor Forward Ps Times' },
D: { label: 'CUB ←', description: 'Cursor Backward Ps Times' },
E: { label: 'CNL', description: 'Cursor Next Line Ps Times' },
F: { label: 'CPL', description: 'Cursor Preceding Line Ps Times' },
G: { label: 'CHA', description: 'Cursor Character Absolute' },
H: { label: 'CUP', description: 'Cursor Position [row;column]', paramCount: 2 },
I: { label: 'CHT', description: 'Cursor Forward Tabulation Ps tab stops' },
J: { label: 'ED', description: 'Erase in Display' },
'?|J': { label: 'DECSED', description: 'Erase in Display' },
K: { label: 'EL', description: 'Erase in Line' },
'?|K': { label: 'DECSEL', description: 'Erase in Line' },
L: { label: 'IL', description: 'Insert Ps Line(s)' },
M: { label: 'DL', description: 'Delete Ps Line(s)' },
P: { label: 'DCH', description: 'Delete Ps Character(s)' },
' q': { label: 'DECSCUSR', description: 'Set Cursor Style', paramCount: 1 }
A: { label: 'CUU ↑', description: 'Cursor Up Ps Times' },
B: { label: 'CUD ↓', description: 'Cursor Down Ps Times' },
C: { label: 'CUF →', description: 'Cursor Forward Ps Times' },
D: { label: 'CUB ←', description: 'Cursor Backward Ps Times' },
E: { label: 'CNL', description: 'Cursor Next Line Ps Times' },
F: { label: 'CPL', description: 'Cursor Preceding Line Ps Times' },
G: { label: 'CHA', description: 'Cursor Character Absolute' },
H: { label: 'CUP', description: 'Cursor Position [row;column]', paramCount: 2 },
I: { label: 'CHT', description: 'Cursor Forward Tabulation Ps tab stops' },
J: { label: 'ED', description: 'Erase in Display' },
'?|J': { label: 'DECSED', description: 'Erase in Display' },
K: { label: 'EL', description: 'Erase in Line' },
'?|K': { label: 'DECSEL', description: 'Erase in Line' },
L: { label: 'IL', description: 'Insert Ps Line(s)' },
M: { label: 'DL', description: 'Delete Ps Line(s)' },
P: { label: 'DCH', description: 'Delete Ps Character(s)' },
' q': { label: 'DECSCUSR', description: 'Set Cursor Style' },
'?2026h': { label: 'BSU', description: 'Begin synchronized update', paramCount: 0 },
'?2026l': { label: 'ESU', description: 'End synchronized update', paramCount: 0 }
};
for (const s of Object.keys(buttonSpecs)) {
const spec = buttonSpecs[s];
+1 -1
View File
@@ -60,7 +60,7 @@ function startServer() {
const cols = parseInt(req.query.cols);
const rows = parseInt(req.query.rows);
const isWindows = process.platform === 'win32';
const term = pty.spawn(isWindows ? 'pwsh.exe' : 'bash', [], {
const term = pty.spawn(isWindows ? 'powershell.exe' : 'bash', [], {
name: 'xterm-256color',
cols: cols ?? 80,
rows: rows ?? 24,
+7 -2
View File
@@ -3,8 +3,13 @@
"description": "A headless terminal component that runs in Node.js",
"version": "5.5.0",
"main": "lib-headless/xterm-headless.js",
"module": "lib/xterm.mjs",
"module": "lib-headless/xterm-headless.mjs",
"types": "typings/xterm-headless.d.ts",
"exports": {
"types": "./typings/xterm-headless.d.ts",
"import": "./lib-headless/xterm-headless.mjs",
"require": "./lib-headless/xterm-headless.js"
},
"repository": "https://github.com/xtermjs/xterm.js",
"license": "MIT",
"keywords": [
@@ -22,4 +27,4 @@
"webgl",
"xterm"
]
}
}
+1589 -1
View File
File diff suppressed because one or more lines are too long
+8303
View File
File diff suppressed because it is too large Load Diff
+5 -5
View File
@@ -1,13 +1,16 @@
{
"name": "@xterm/xterm",
"description": "Full xterm terminal, in your browser",
"version": "5.5.0",
"version": "6.0.0-uniterm.1",
"main": "lib/xterm.js",
"module": "lib/xterm.mjs",
"style": "css/xterm.css",
"types": "typings/xterm.d.ts",
"repository": "https://github.com/xtermjs/xterm.js",
"license": "MIT",
"workspaces": [
"addons/*"
],
"keywords": [
"cli",
"command-line",
@@ -25,8 +28,6 @@
],
"scripts": {
"setup": "npm run build",
"presetup": "npm run install-addons",
"install-addons": "node ./bin/install-addons.js",
"start": "node demo/start",
"build": "npm run tsc",
"watch": "npm run tsc-watch",
@@ -42,6 +43,7 @@
"test": "npm run test-unit",
"posttest": "npm run lint",
"lint": "eslint -c .eslintrc.json --max-warnings 0 --ext .ts src/ addons/",
"lint-fix": "eslint -c .eslintrc.json --fix --ext .ts src/ addons/",
"lint-api": "eslint --no-eslintrc -c .eslintrc.json.typings --max-warnings 0 --no-ignore --ext .d.ts typings/",
"test-unit": "node ./bin/test_unit.js",
"test-unit-coverage": "node ./bin/test_unit.js --coverage",
@@ -73,7 +75,6 @@
"@types/deep-equal": "^1.0.1",
"@types/express": "4",
"@types/express-ws": "^3.0.1",
"@types/glob": "^7.2.0",
"@types/jsdom": "^16.2.13",
"@types/mocha": "^9.0.0",
"@types/node": "^18.16.0",
@@ -91,7 +92,6 @@
"eslint-plugin-jsdoc": "^46.9.1",
"express": "^4.19.2",
"express-ws": "^5.0.2",
"glob": "^7.2.0",
"jsdom": "^18.0.1",
"mocha": "^10.1.0",
"mustache": "^4.2.0",
+41
View File
@@ -35,6 +35,7 @@ import { IRenderer } from 'browser/renderer/shared/Types';
import { CharSizeService } from 'browser/services/CharSizeService';
import { CharacterJoinerService } from 'browser/services/CharacterJoinerService';
import { CoreBrowserService } from 'browser/services/CoreBrowserService';
import { CellColorOverrideStore, ICellColorSpan } from 'browser/services/CellColorOverrideStore';
import { LinkProviderService } from 'browser/services/LinkProviderService';
import { MouseService } from 'browser/services/MouseService';
import { RenderService } from 'browser/services/RenderService';
@@ -646,6 +647,14 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
if (deltaY === 0) {
return false;
}
const lines = self.coreMouseService.consumeWheelEvent(
ev as WheelEvent,
self._renderService?.dimensions?.device?.cell?.height,
self._coreBrowserService?.dpr
);
if (lines === 0) {
return false;
}
action = deltaY < 0 ? CoreMouseAction.UP : CoreMouseAction.DOWN;
but = CoreMouseButton.WHEEL;
break;
@@ -817,6 +826,15 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
return false;
}
const lines = self.coreMouseService.consumeWheelEvent(
ev as WheelEvent,
self._renderService?.dimensions?.device?.cell?.height,
self._coreBrowserService?.dpr
);
if (lines === 0) {
return this.cancel(ev, true);
}
// Construct and send sequences
const sequence = C0.ESC + (this.coreService.decPrivateModes.applicationCursorKeys ? 'O' : '[') + (ev.deltaY < 0 ? 'A' : 'B');
this.coreService.triggerDataEvent(sequence, true);
@@ -936,6 +954,29 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
return this._decorationService.registerDecoration(decorationOptions);
}
public readonly cellColorOverrides: CellColorOverrideStore = new CellColorOverrideStore();
/**
* Recolor a contiguous cell range on one absolute buffer line. Pass null to
* clear the line. The affected viewport row is re-rendered lazily. Overrides
* are keyed by the active buffer's absolute line index; the caller must
* clear/re-set them when indices shift (scrollback trim, resize reflow) or
* when the alternate buffer changes.
*/
public setCellColorOverrides(lineY: number, spans: ICellColorSpan[] | null): void {
if (this.cellColorOverrides.set(lineY, spans)) {
const row = lineY - this._bufferService.buffer.ydisp;
if (row >= 0 && row < this._bufferService.rows) {
this._renderService?.refreshRows(row, row);
}
}
}
public clearCellColorOverrides(): void {
this.cellColorOverrides.clear();
this._renderService?.refreshRows(0, this._bufferService.rows - 1);
}
/**
* Gets whether the terminal has an active selection.
*/
-41
View File
@@ -1079,47 +1079,6 @@ describe('Terminal', () => {
assert.equal(windowsModeTerminal.buffer.lines.get(2)!.isWrapped, false);
});
});
describe('Windows Mode', () => {
it('should mark lines as wrapped when the line ends in a non-null character after a LF', async () => {
const data = [
'aaaaaaaaaa\n\r', // cannot wrap as it's the first
'aaaaaaaaa\n\r', // wrapped (windows mode only)
'aaaaaaaaa' // not wrapped
];
const normalTerminal = new TestTerminal({ rows: 5, cols: 10, windowsMode: false });
await normalTerminal.writeP(data.join(''));
assert.equal(normalTerminal.buffer.lines.get(0)!.isWrapped, false);
assert.equal(normalTerminal.buffer.lines.get(1)!.isWrapped, false);
assert.equal(normalTerminal.buffer.lines.get(2)!.isWrapped, false);
const windowsModeTerminal = new TestTerminal({ rows: 5, cols: 10, windowsMode: true });
await windowsModeTerminal.writeP(data.join(''));
assert.equal(windowsModeTerminal.buffer.lines.get(0)!.isWrapped, false);
assert.equal(windowsModeTerminal.buffer.lines.get(1)!.isWrapped, true, 'This line should wrap in Windows mode as the previous line ends in a non-null character');
assert.equal(windowsModeTerminal.buffer.lines.get(2)!.isWrapped, false);
});
it('should mark lines as wrapped when the line ends in a non-null character after a CUP', async () => {
const data = [
'aaaaaaaaaa\x1b[2;1H', // cannot wrap as it's the first
'aaaaaaaaa\x1b[3;1H', // wrapped (windows mode only)
'aaaaaaaaa' // not wrapped
];
const normalTerminal = new TestTerminal({ rows: 5, cols: 10, windowsMode: false });
await normalTerminal.writeP(data.join(''));
assert.equal(normalTerminal.buffer.lines.get(0)!.isWrapped, false);
assert.equal(normalTerminal.buffer.lines.get(1)!.isWrapped, false);
assert.equal(normalTerminal.buffer.lines.get(2)!.isWrapped, false);
const windowsModeTerminal = new TestTerminal({ rows: 5, cols: 10, windowsMode: true });
await windowsModeTerminal.writeP(data.join(''));
assert.equal(windowsModeTerminal.buffer.lines.get(0)!.isWrapped, false);
assert.equal(windowsModeTerminal.buffer.lines.get(1)!.isWrapped, true, 'This line should wrap in Windows mode as the previous line ends in a non-null character');
assert.equal(windowsModeTerminal.buffer.lines.get(2)!.isWrapped, false);
});
});
it('convertEol setting', async () => {
// not converting
const termNotConverting = new TestTerminal({ cols: 15, rows: 10 });
+7 -5
View File
@@ -3,7 +3,6 @@
* @license MIT
*/
import * as glob from 'glob';
import * as path from 'path';
import * as os from 'os';
import * as fs from 'fs';
@@ -15,7 +14,10 @@ import { IDisposable } from '@xterm/xterm';
const COLS = 80;
const ROWS = 25;
const TESTFILES = glob.sync('**/escape_sequence_files/*.in', { cwd: path.join(__dirname, '../..')});
const escapeSequenceFilesDir = path.join(__dirname, '../../fixtures/escape_sequence_files');
const TESTFILES = fs.readdirSync(escapeSequenceFilesDir)
.filter(f => f.endsWith('.in'))
.map(f => path.join(escapeSequenceFilesDir, f));
const SKIP_FILES = [
't0055-EL.in', // EL/ED handle cursor at cols differently (see #3362)
't0084-CBT.in',
@@ -33,7 +35,7 @@ if (os.platform() === 'darwin') {
);
}
// filter skipFilenames
const FILES = TESTFILES.filter(value => !SKIP_FILES.includes(value.split('/').slice(-1)[0]));
const FILES = TESTFILES.filter(value => !SKIP_FILES.includes(path.basename(value)));
describe('Escape Sequence Files', function(): void {
this.timeout(1000);
@@ -62,7 +64,7 @@ describe('Escape Sequence Files', function(): void {
});
for (const filename of FILES) {
(process.platform === 'win32' ? it.skip : it)(filename.split('/').slice(-1)[0], async () => {
(process.platform === 'win32' ? it.skip : it)(path.basename(filename), async () => {
// reset terminal and handler
if (customHandler) {
customHandler.dispose();
@@ -88,7 +90,7 @@ describe('Escape Sequence Files', function(): void {
});
// compare with expected output (right trimmed)
const expected = fs.readFileSync(filename.split('.')[0] + '.text', 'utf8');
const expected = fs.readFileSync(filename.replace(/\.in$/, '.text'), 'utf8');
const expectedRightTrimmed = expected.split('\n').map(l => l.replace(/\s+$/, '')).join('\n');
if (content !== expectedRightTrimmed) {
throw new Error(formatError(fs.readFileSync(filename, 'utf8'), content, expected));
+9 -1
View File
@@ -3,7 +3,8 @@
* @license MIT
*/
import { IDisposable, IMarker, ILinkProvider, IDecorationOptions, IDecoration } from '@xterm/xterm';
import { CellColorOverrideStore } from 'browser/services/CellColorOverrideStore';
import { ICellColorSpan, IDisposable, IMarker, ILinkProvider, IDecorationOptions, IDecoration } from '@xterm/xterm';
import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IMouseService, IRenderService, ISelectionService, IThemeService } from 'browser/services/Services';
import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/shared/Types';
import { IColorSet, ITerminal, ILinkifier2, IBrowser, IViewport, ICompositionHelper, CharacterJoinerHandler, IBufferRange, ReadonlyColorSet, IBufferElementProvider } from 'browser/Types';
@@ -30,6 +31,7 @@ export class TestTerminal extends CoreBrowserTerminal {
}
export class MockTerminal implements ITerminal {
public readonly cellColorOverrides = new CellColorOverrideStore();
public onBlur!: Event<void>;
public onFocus!: Event<void>;
public onA11yChar!: Event<string>;
@@ -108,6 +110,12 @@ export class MockTerminal implements ITerminal {
public registerLinkProvider(linkProvider: ILinkProvider): IDisposable {
throw new Error('Method not implemented.');
}
public setCellColorOverrides(lineY: number, spans: ICellColorSpan[] | null): void {
throw new Error('Not implemented');
}
public clearCellColorOverrides(): void {
throw new Error('Not implemented');
}
public registerDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined {
throw new Error('Method not implemented.');
}
+2 -2
View File
@@ -45,7 +45,7 @@ export class TimeBasedDebouncer implements IRenderDebouncer {
// Only refresh if the time since last refresh is above a threshold, otherwise wait for
// enough time to pass before refreshing again.
const refreshRequestTime: number = Date.now();
const refreshRequestTime: number = performance.now();
if (refreshRequestTime - this._lastRefreshMs >= this._debounceThresholdMS) {
// Enough time has lapsed since the last refresh; refresh immediately
this._lastRefreshMs = refreshRequestTime;
@@ -57,7 +57,7 @@ export class TimeBasedDebouncer implements IRenderDebouncer {
this._additionalRefreshRequested = true;
this._refreshTimeoutID = window.setTimeout(() => {
this._lastRefreshMs = Date.now();
this._lastRefreshMs = performance.now();
this._innerRefresh();
this._additionalRefreshRequested = false;
this._refreshTimeoutID = undefined; // No longer need to clear the timeout
+2
View File
@@ -8,6 +8,7 @@ import { IBuffer } from 'common/buffer/Types';
import { IDisposable, Terminal as ITerminalApi } from '@xterm/xterm';
import { channels, css } from 'common/Color';
import type { Event } from 'vs/base/common/event';
import { CellColorOverrideStore } from 'browser/services/CellColorOverrideStore';
/**
* A portion of the public API that are implemented identially internally and simply passed through.
@@ -20,6 +21,7 @@ export interface ITerminal extends InternalPassthroughApis, ICoreTerminal {
buffer: IBuffer;
linkifier: ILinkifier2 | undefined;
options: Required<ITerminalOptions>;
cellColorOverrides: CellColorOverrideStore;
onBlur: Event<void>;
onFocus: Event<void>;
+7 -2
View File
@@ -94,7 +94,12 @@ export class Viewport extends Disposable {
}));
this._register(this._bufferService.onResize(() => this.queueSync()));
this._register(this._bufferService.buffers.onBufferActivate(() => this.queueSync()));
this._register(this._bufferService.buffers.onBufferActivate(() => {
// Reset _latestYDisp when switching buffers to prevent stale scroll position
// from alt buffer contaminating normal buffer scroll position
this._latestYDisp = undefined;
this.queueSync();
}));
this._register(this._bufferService.onScroll(() => this._sync()));
this._register(this._scrollableElement.onScroll(e => this._handleScroll(e)));
@@ -157,7 +162,7 @@ export class Viewport extends Disposable {
});
this._suppressOnScrollHandler = false;
// If ydisp has been changed by some other copmonent (input/buffer), then stop animating smooth
// If ydisp has been changed by some other component (input/buffer), then stop animating smooth
// scroll and scroll there immediately.
if (ydisp !== this._latestYDisp) {
this._scrollableElement.setScrollPosition({
+3 -1
View File
@@ -199,7 +199,9 @@ function bufferLine(
let currentRow = startRow;
let bufferStr = '';
while (currentCol !== endCol || currentRow !== endRow) {
while ((currentCol !== endCol || currentRow !== endRow) &&
currentRow >= 0 &&
currentRow < bufferService.buffer.lines.length) {
currentCol += forward ? 1 : -1;
if (forward && currentCol > bufferService.cols - 1) {
+9 -1
View File
@@ -12,7 +12,7 @@ import { AddonManager } from 'common/public/AddonManager';
import { BufferNamespaceApi } from 'common/public/BufferNamespaceApi';
import { ParserApi } from 'common/public/ParserApi';
import { UnicodeApi } from 'common/public/UnicodeApi';
import { IBufferNamespace as IBufferNamespaceApi, IDecoration, IDecorationOptions, IDisposable, ILinkProvider, ILocalizableStrings, IMarker, IModes, IParser, ITerminalAddon, Terminal as ITerminalApi, ITerminalInitOnlyOptions, IUnicodeHandling } from '@xterm/xterm';
import { IBufferNamespace as IBufferNamespaceApi, IDecoration, IDecorationOptions, IDisposable, ILinkProvider, ILocalizableStrings, IMarker, IModes, IParser, ITerminalAddon, Terminal as ITerminalApi, ITerminalInitOnlyOptions, IUnicodeHandling, ICellColorSpan } from '@xterm/xterm';
import type { Event } from 'vs/base/common/event';
/**
@@ -123,6 +123,7 @@ export class Terminal extends Disposable implements ITerminalApi {
originMode: m.origin,
reverseWraparoundMode: m.reverseWraparound,
sendFocusMode: m.sendFocus,
synchronizedOutputMode: m.synchronizedOutput,
wraparoundMode: m.wraparound
};
}
@@ -176,6 +177,13 @@ export class Terminal extends Disposable implements ITerminalApi {
this._verifyPositiveIntegers(decorationOptions.x ?? 0, decorationOptions.width ?? 0, decorationOptions.height ?? 0);
return this._core.registerDecoration(decorationOptions);
}
public setCellColorOverrides(lineY: number, spans: ICellColorSpan[] | null): void {
this._verifyIntegers(lineY);
this._core.setCellColorOverrides(lineY, spans);
}
public clearCellColorOverrides(): void {
this._core.clearCellColorOverrides();
}
public hasSelection(): boolean {
return this._core.hasSelection();
}
+1 -1
View File
@@ -80,7 +80,7 @@ export class DomRenderer extends Disposable implements IRenderer {
this._register(this._themeService.onChangeColors(e => this._injectCss(e)));
this._injectCss(this._themeService.colors);
this._rowFactory = instantiationService.createInstance(DomRendererRowFactory, document);
this._rowFactory = instantiationService.createInstance(DomRendererRowFactory, document, this._terminal.cellColorOverrides);
this._element.classList.add(TERMINAL_CLASS_PREFIX + this._terminalClass);
this._screenElement.appendChild(this._rowContainer);
@@ -6,6 +6,7 @@
import jsdom = require('jsdom');
import { assert } from 'chai';
import { DomRendererRowFactory } from 'browser/renderer/dom/DomRendererRowFactory';
import { CellColorOverrideStore } from 'browser/services/CellColorOverrideStore';
import { NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR, DEFAULT_ATTR, FgFlags, BgFlags, Attributes, UnderlineStyle } from 'common/buffer/Constants';
import { BufferLine, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
import { IBufferLine } from 'common/Types';
@@ -27,6 +28,7 @@ describe('DomRendererRowFactory', () => {
dom = new jsdom.JSDOM('');
rowFactory = new DomRendererRowFactory(
dom.window.document,
new CellColorOverrideStore(),
new MockCharacterJoinerService(),
new MockOptionsService({ drawBoldTextInBrightColors: true }),
new MockCoreBrowserService(),
@@ -77,6 +79,7 @@ describe('DomRendererRowFactory', () => {
coreBrowserService.isFocused = false;
const rowFactory = new DomRendererRowFactory(
dom.window.document,
new CellColorOverrideStore(),
new MockCharacterJoinerService(),
new MockOptionsService({ drawBoldTextInBrightColors: true }),
coreBrowserService,
@@ -101,6 +104,7 @@ describe('DomRendererRowFactory', () => {
coreService.isCursorInitialized = false;
const rowFactory = new DomRendererRowFactory(
dom.window.document,
new CellColorOverrideStore(),
new MockCharacterJoinerService(),
new MockOptionsService(),
new MockCoreBrowserService(),
@@ -10,6 +10,7 @@ import { CellData } from 'common/buffer/CellData';
import { ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
import { channels, color } from 'common/Color';
import { ICharacterJoinerService, ICoreBrowserService, IThemeService } from 'browser/services/Services';
import { CellColorOverrideStore } from 'browser/services/CellColorOverrideStore';
import { JoinedCellData } from 'browser/services/CharacterJoinerService';
import { treatGlyphAsBackgroundColor } from 'browser/renderer/shared/RendererUtils';
import { AttributeData } from 'common/buffer/AttributeData';
@@ -44,6 +45,7 @@ export class DomRendererRowFactory {
constructor(
private readonly _document: Document,
private readonly _colorOverrides: CellColorOverrideStore,
@ICharacterJoinerService private readonly _characterJoinerService: ICharacterJoinerService,
@IOptionsService private readonly _optionsService: IOptionsService,
@ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService,
@@ -95,6 +97,13 @@ export class DomRendererRowFactory {
let skipJoinedCheckUntilX = 0;
const classes: string[] = [];
// Cell color overrides for this line (absolute row index). Looked up once
// per row, then consumed with a moving pointer since x only advances.
const colorOverrideSpans = this._colorOverrides.get(row);
let colorOverrideIdx = 0;
let colorOverride: IColor | undefined;
let oldColorOverride: IColor | undefined;
const hasHover = linkStart !== -1 && linkEnd !== -1;
for (let x = 0; x < lineLength; x++) {
@@ -154,6 +163,21 @@ export class DomRendererRowFactory {
const isCursorCell = isCursorRow && x === cursorX;
const isLinkHover = hasHover && x >= linkStart && x <= linkEnd;
// Resolve the color override covering this cell (if any). Spans are
// sorted by column and don't overlap, so a single moving pointer works.
if (colorOverrideSpans) {
while (
colorOverrideIdx < colorOverrideSpans.length &&
colorOverrideSpans[colorOverrideIdx].col + colorOverrideSpans[colorOverrideIdx].width <= x
) {
colorOverrideIdx++;
}
const span = colorOverrideSpans[colorOverrideIdx];
colorOverride = (span && x >= span.col) ? span.color : undefined;
} else {
colorOverride = undefined;
}
let isDecorated = false;
this._decorationService.forEachDecorationAtCell(x, row, undefined, d => {
isDecorated = true;
@@ -194,6 +218,7 @@ export class DomRendererRowFactory {
&& cell.extended.ext === oldExt
&& isLinkHover === oldLinkHover
&& spacing === oldSpacing
&& colorOverride === oldColorOverride
&& !isCursorCell
&& !isJoined
&& !isDecorated
@@ -228,6 +253,7 @@ export class DomRendererRowFactory {
oldLinkHover = isLinkHover;
oldSpacing = spacing;
oldIsInSelection = isInSelection;
oldColorOverride = colorOverride;
if (isJoined) {
// The DOM renderer colors the background of the cursor but for ligatures all cells are
@@ -417,7 +443,14 @@ export class DomRendererRowFactory {
}
// Foreground
switch (fgColorMode) {
if (colorOverride) {
// Cell color override: recolor the text directly, winning over the
// cell/decoration colors but still running through the minimum
// contrast adjustment like every other foreground source.
if (!this._applyMinimumContrast(charElement, resolvedBg, colorOverride, cell, bgOverride, colorOverride)) {
this._addStyle(charElement, `color:${colorOverride.css}`);
}
} else switch (fgColorMode) {
case Attributes.CM_P16:
case Attributes.CM_P256:
if (cell.isBold() && fg < 8 && this._optionsService.rawOptions.drawBoldTextInBrightColors) {
@@ -0,0 +1,81 @@
/**
* Copyright (c) 2025 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IColor } from 'common/Types';
import { css } from 'common/Color';
/**
* A single contiguous cell range on one buffer line that should render its
* text in a specific color.
*/
export interface ICellColorSpan {
/** Zero-based column of the first cell. */
col: number;
/** Number of cells covered (wide chars count as 2). */
width: number;
/** CSS color string (#rrggbb / rgb(...) etc.), parsed on acceptance. */
color: string;
}
interface StoredSpan {
col: number;
width: number;
color: IColor;
}
/**
* Sparse per-line cell color overrides consumed by the DOM renderer's row
* factory. Keyed by absolute buffer line index of the active buffer; the
* owner is responsible for invalidating keys when the buffer shifts
* (scrollback trim, resize reflow, alternate buffer switch).
*/
export class CellColorOverrideStore {
private _overrides = new Map<number, StoredSpan[]>();
/** Replace the overrides of one buffer line. Returns true when the stored
* state changed and the line needs a re-render. Invalid color strings and
* zero-width spans are dropped. */
public set(lineY: number, spans: ICellColorSpan[] | null): boolean {
if (!spans || spans.length === 0) {
return this._overrides.delete(lineY);
}
const stored: StoredSpan[] = [];
for (const span of spans) {
if (span.width <= 0) {
continue;
}
try {
stored.push({ col: span.col, width: span.width, color: css.toColor(span.color) });
} catch {
// Unsupported color format — skip the span rather than fail the batch.
}
}
if (stored.length === 0) {
return this._overrides.delete(lineY);
}
const existing = this._overrides.get(lineY);
if (
existing &&
existing.length === stored.length &&
existing.every((e, i) =>
e.col === stored[i].col &&
e.width === stored[i].width &&
e.color.css === stored[i].color.css
)
) {
return false;
}
this._overrides.set(lineY, stored);
return true;
}
public get(lineY: number): StoredSpan[] | undefined {
return this._overrides.get(lineY);
}
public clear(): void {
this._overrides.clear();
}
}
+101 -10
View File
@@ -9,7 +9,7 @@ import { IRenderDimensions, IRenderer } from 'browser/renderer/shared/Types';
import { ICharSizeService, ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services';
import { Disposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { DebouncedIdleTask } from 'common/TaskQueue';
import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services';
import { IBufferService, ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
import { Emitter } from 'vs/base/common/event';
interface ISelectionState {
@@ -18,6 +18,10 @@ interface ISelectionState {
columnSelectMode: boolean;
}
const enum Constants {
SYNCHRONIZED_OUTPUT_TIMEOUT_MS = 1000
}
export class RenderService extends Disposable implements IRenderService {
public serviceBrand: undefined;
@@ -32,6 +36,7 @@ export class RenderService extends Disposable implements IRenderService {
private _needsSelectionRefresh: boolean = false;
private _canvasWidth: number = 0;
private _canvasHeight: number = 0;
private _syncOutputHandler: SynchronizedOutputHandler;
private _selectionState: ISelectionState = {
start: undefined,
end: undefined,
@@ -52,23 +57,31 @@ export class RenderService extends Disposable implements IRenderService {
constructor(
private _rowCount: number,
screenElement: HTMLElement,
@IOptionsService optionsService: IOptionsService,
@IOptionsService private readonly _optionsService: IOptionsService,
@ICharSizeService private readonly _charSizeService: ICharSizeService,
@ICoreService private readonly _coreService: ICoreService,
@IDecorationService decorationService: IDecorationService,
@IBufferService bufferService: IBufferService,
@ICoreBrowserService coreBrowserService: ICoreBrowserService,
@ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService,
@IThemeService themeService: IThemeService
) {
super();
this._renderDebouncer = new RenderDebouncer((start, end) => this._renderRows(start, end), coreBrowserService);
this._renderDebouncer = new RenderDebouncer((start, end) => this._renderRows(start, end), this._coreBrowserService);
this._register(this._renderDebouncer);
this._register(coreBrowserService.onDprChange(() => this.handleDevicePixelRatioChange()));
this._syncOutputHandler = new SynchronizedOutputHandler(
this._coreBrowserService,
this._coreService,
() => this._fullRefresh()
);
this._register(toDisposable(() => this._syncOutputHandler.dispose()));
this._register(this._coreBrowserService.onDprChange(() => this.handleDevicePixelRatioChange()));
this._register(bufferService.onResize(() => this._fullRefresh()));
this._register(bufferService.buffers.onBufferActivate(() => this._renderer.value?.clear()));
this._register(optionsService.onOptionChange(() => this._handleOptionsChanged()));
this._register(this._optionsService.onOptionChange(() => this._handleOptionsChanged()));
this._register(this._charSizeService.onCharSizeChange(() => this.handleCharSizeChanged()));
// Do a full refresh whenever any decoration is added or removed. This may not actually result
@@ -78,7 +91,7 @@ export class RenderService extends Disposable implements IRenderService {
this._register(decorationService.onDecorationRemoved(() => this._fullRefresh()));
// Clear the renderer when the a change that could affect glyphs occurs
this._register(optionsService.onMultipleOptionChange([
this._register(this._optionsService.onMultipleOptionChange([
'customGlyphs',
'drawBoldTextInBrightColors',
'letterSpacing',
@@ -96,15 +109,15 @@ export class RenderService extends Disposable implements IRenderService {
}));
// Refresh the cursor line when the cursor changes
this._register(optionsService.onMultipleOptionChange([
this._register(this._optionsService.onMultipleOptionChange([
'cursorBlink',
'cursorStyle'
], () => this.refreshRows(bufferService.buffer.y, bufferService.buffer.y, true)));
this._register(themeService.onChangeColors(() => this._fullRefresh()));
this._registerIntersectionObserver(coreBrowserService.window, screenElement);
this._register(coreBrowserService.onWindowChange((w) => this._registerIntersectionObserver(w, screenElement)));
this._registerIntersectionObserver(this._coreBrowserService.window, screenElement);
this._register(this._coreBrowserService.onWindowChange((w) => this._registerIntersectionObserver(w, screenElement)));
}
private _registerIntersectionObserver(w: Window & typeof globalThis, screenElement: HTMLElement): void {
@@ -137,6 +150,18 @@ export class RenderService extends Disposable implements IRenderService {
this._needsFullRefresh = true;
return;
}
if (this._coreService.decPrivateModes.synchronizedOutput) {
this._syncOutputHandler.bufferRows(start, end);
return;
}
const buffered = this._syncOutputHandler.flush();
if (buffered) {
start = Math.min(start, buffered.start);
end = Math.max(end, buffered.end);
}
if (!isRedrawOnly) {
this._isNextRenderRedrawOnly = false;
}
@@ -148,6 +173,13 @@ export class RenderService extends Disposable implements IRenderService {
return;
}
// Skip rendering if synchronized output mode is enabled. This check must happen here
// (in addition to refreshRows) to handle renders that were queued before the mode was enabled.
if (this._coreService.decPrivateModes.synchronizedOutput) {
this._syncOutputHandler.bufferRows(start, end);
return;
}
// Since this is debounced, a resize event could have happened between the time a refresh was
// requested and when this triggers. Clamp the values of start and end to ensure they're valid
// given the current viewport state.
@@ -283,3 +315,62 @@ export class RenderService extends Disposable implements IRenderService {
this._renderer.value?.clear();
}
}
/**
* Buffers row refresh requests during synchronized output mode (DEC mode 2026).
* When the mode is disabled, the accumulated row range is flushed for rendering.
* A safety timeout ensures rendering occurs even if the end sequence is not received.
*/
class SynchronizedOutputHandler {
private _start: number = 0;
private _end: number = 0;
private _timeout: number | undefined;
private _isBuffering: boolean = false;
constructor(
private readonly _coreBrowserService: ICoreBrowserService,
private readonly _coreService: ICoreService,
private readonly _onTimeout: () => void
) {}
public bufferRows(start: number, end: number): void {
if (!this._isBuffering) {
this._start = start;
this._end = end;
this._isBuffering = true;
} else {
this._start = Math.min(this._start, start);
this._end = Math.max(this._end, end);
}
if (this._timeout === undefined) {
this._timeout = this._coreBrowserService.window.setTimeout(() => {
this._timeout = undefined;
this._coreService.decPrivateModes.synchronizedOutput = false;
this._onTimeout();
}, Constants.SYNCHRONIZED_OUTPUT_TIMEOUT_MS);
}
}
public flush(): { start: number, end: number } | undefined {
if (this._timeout !== undefined) {
this._coreBrowserService.window.clearTimeout(this._timeout);
this._timeout = undefined;
}
if (!this._isBuffering) {
return undefined;
}
const result = { start: this._start, end: this._end };
this._isBuffering = false;
return result;
}
public dispose(): void {
if (this._timeout !== undefined) {
this._coreBrowserService.window.clearTimeout(this._timeout);
this._timeout = undefined;
}
}
}
+8
View File
@@ -152,6 +152,14 @@ export class SelectionService extends Disposable implements ISelectionService {
this._register(toDisposable(() => {
this._removeMouseDownListeners();
}));
// Clear selection when resizing vertically. This experience could be improved, this is the
// simple option to fix the buggy behavior. https://github.com/xtermjs/xterm.js/issues/5300
this._register(this._bufferService.onResize(e => {
if (e.rowsChanged) {
this.clearSelection();
}
}));
}
public reset(): void {
+1 -2
View File
@@ -7,8 +7,7 @@
],
"outDir": "../../out",
"types": [
"../../node_modules/@types/mocha",
"../vs/typings/thenable.d.ts"
"../../node_modules/@types/mocha"
],
"baseUrl": "..",
"paths": {
+1 -1
View File
@@ -2314,7 +2314,7 @@ describe('InputHandler', () => {
});
it('DEC privates with set/reset semantic', async () => {
// initially reset
const reset = [1, 6, 9, 12, 45, 66, 1000, 1002, 1003, 1004, 1006, 1016, 47, 1047, 1049, 2004];
const reset = [1, 6, 9, 12, 45, 66, 1000, 1002, 1003, 1004, 1006, 1016, 47, 1047, 1049, 2004, 2026];
for (const mode of reset) {
await inputHandler.parseP(`\x1b[?${mode}$p`);
assert.deepEqual(reportStack.pop(), `\x1b[?${mode};2$y`); // initial reset
+14 -3
View File
@@ -445,7 +445,10 @@ export class InputHandler extends Disposable implements IInputHandler {
// Log debug data, the log level gate is to prevent extra work in this hot path
if (this._logService.logLevel <= LogLevelEnum.DEBUG) {
this._logService.debug(`parsing data${typeof data === 'string' ? ` "${data}"` : ` "${Array.prototype.map.call(data, e => String.fromCharCode(e)).join('')}"`}`, typeof data === 'string'
this._logService.debug(`parsing data ${typeof data === 'string' ? ` "${data}"` : ` "${Array.prototype.map.call(data, e => String.fromCharCode(e)).join('')}"`}`);
}
if (this._logService.logLevel === LogLevelEnum.TRACE) {
this._logService.trace(`parsing data (codes)`, typeof data === 'string'
? data.split('').map(e => e.charCodeAt(0))
: data
);
@@ -606,7 +609,7 @@ export class InputHandler extends Disposable implements IInputHandler {
// since an empty cell is only set by fullwidth chars
bufferRow.addCodepointToCell(this._activeBuffer.x - offset,
code, chWidth);
for (let delta = chWidth - oldWidth; --delta >= 0; ) {
for (let delta = chWidth - oldWidth; --delta >= 0;) {
bufferRow.setCellFromCodepoint(this._activeBuffer.x++, 0, 0, curAttr);
}
continue;
@@ -1622,7 +1625,7 @@ export class InputHandler extends Disposable implements IInputHandler {
const text = bufferRow.getString(x);
const data = new Uint32Array(text.length * length);
let idata = 0;
for (let itext = 0; itext < text.length; ) {
for (let itext = 0; itext < text.length;) {
const ch = text.codePointAt(itext) || 0;
data[idata++] = ch;
itext += ch > 0xffff ? 2 : 1;
@@ -1966,6 +1969,9 @@ export class InputHandler extends Disposable implements IInputHandler {
case 2004: // bracketed paste mode (https://cirw.in/blog/bracketed-paste)
this._coreService.decPrivateModes.bracketedPasteMode = true;
break;
case 2026: // synchronized output (https://github.com/contour-terminal/vt-extensions/blob/master/synchronized-output.md)
this._coreService.decPrivateModes.synchronizedOutput = true;
break;
}
}
return true;
@@ -2194,6 +2200,10 @@ export class InputHandler extends Disposable implements IInputHandler {
case 2004: // bracketed paste mode (https://cirw.in/blog/bracketed-paste)
this._coreService.decPrivateModes.bracketedPasteMode = false;
break;
case 2026: // synchronized output (https://github.com/contour-terminal/vt-extensions/blob/master/synchronized-output.md)
this._coreService.decPrivateModes.synchronizedOutput = false;
this._onRequestRefreshRows.fire(undefined);
break;
}
}
return true;
@@ -2288,6 +2298,7 @@ export class InputHandler extends Disposable implements IInputHandler {
if (p === 1048) return f(p, V.SET); // xterm always returns SET here
if (p === 47 || p === 1047 || p === 1049) return f(p, b2v(active === alt));
if (p === 2004) return f(p, b2v(dm.bracketedPasteMode));
if (p === 2026) return f(p, b2v(dm.synchronizedOutput));
return f(p, V.NOT_RECOGNIZED);
}
+7 -7
View File
@@ -74,14 +74,14 @@ abstract class TaskQueue implements ITaskQueue {
let lastDeadlineRemaining = deadline.timeRemaining();
let deadlineRemaining = 0;
while (this._i < this._tasks.length) {
taskDuration = Date.now();
taskDuration = performance.now();
if (!this._tasks[this._i]()) {
this._i++;
}
// other than performance.now, Date.now might not be stable (changes on wall clock changes),
// this is not an issue here as a clock change during a short running task is very unlikely
// in case it still happened and leads to negative duration, simply assume 1 msec
taskDuration = Math.max(1, Date.now() - taskDuration);
// other than performance.now, performance.now might not be stable (changes on wall clock
// changes), this is not an issue here as a clock change during a short running task is very
// unlikely in case it still happened and leads to negative duration, simply assume 1 msec
taskDuration = Math.max(1, performance.now() - taskDuration);
longestTask = Math.max(taskDuration, longestTask);
// Guess the following task will take a similar time to the longest task in this batch, allow
// additional room to try avoid exceeding the deadline
@@ -116,9 +116,9 @@ export class PriorityTaskQueue extends TaskQueue {
}
private _createDeadline(duration: number): ITaskDeadline {
const end = Date.now() + duration;
const end = performance.now() + duration;
return {
timeRemaining: () => Math.max(0, end - Date.now())
timeRemaining: () => Math.max(0, end - performance.now())
};
}
}
+11 -2
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { IBufferService, ICoreService, ILogService, IOptionsService, ITerminalOptions, ICoreMouseService, ICharsetService, UnicodeCharProperties, UnicodeCharWidth, IUnicodeService, IUnicodeVersionProvider, LogLevelEnum, IDecorationService, IInternalDecoration, IOscLinkService } from 'common/services/Services';
import { IBufferService, ICoreService, ILogService, IOptionsService, ITerminalOptions, ICoreMouseService, ICharsetService, UnicodeCharProperties, UnicodeCharWidth, IUnicodeService, IUnicodeVersionProvider, LogLevelEnum, IDecorationService, IInternalDecoration, IOscLinkService, type IBufferResizeEvent } from 'common/services/Services';
import { UnicodeService } from 'common/services/UnicodeService';
import { clone } from 'common/Clone';
import { DEFAULT_OPTIONS } from 'common/services/OptionsService';
@@ -18,8 +18,9 @@ export class MockBufferService implements IBufferService {
public serviceBrand: any;
public get buffer(): IBuffer { return this.buffers.active; }
public buffers: IBufferSet = {} as any;
public onResize: Event<{ cols: number, rows: number }> = new Emitter<{ cols: number, rows: number }>().event;
public onResize: Event<IBufferResizeEvent> = new Emitter<IBufferResizeEvent>().event;
public onScroll: Event<number> = new Emitter<number>().event;
private readonly _onScroll = new Emitter<number>();
public isUserScrolling: boolean = false;
constructor(
public cols: number,
@@ -27,6 +28,10 @@ export class MockBufferService implements IBufferService {
optionsService: IOptionsService = new MockOptionsService()
) {
this.buffers = new BufferSet(optionsService, this);
// Listen to buffer activation events and automatically fire scroll events
this.buffers.onBufferActivate(e => {
this._onScroll.fire(e.activeBuffer.ydisp);
});
}
public scrollPages(pageCount: number): void {
throw new Error('Method not implemented.');
@@ -66,6 +71,9 @@ export class MockCoreMouseService implements ICoreMouseService {
public explainEvents(events: CoreMouseEventType): { [event: string]: boolean } {
throw new Error('Method not implemented.');
}
public consumeWheelEvent(ev: WheelEvent, cellHeight: number, dpr: number): number {
return 1;
}
}
export class MockCharsetService implements ICharsetService {
@@ -94,6 +102,7 @@ export class MockCoreService implements ICoreService {
origin: false,
reverseWraparound: false,
sendFocus: false,
synchronizedOutput: false,
wraparound: true
};
public onData: Event<string> = new Emitter<string>().event;
+1
View File
@@ -273,6 +273,7 @@ export interface IDecPrivateModes {
origin: boolean;
reverseWraparound: boolean;
sendFocus: boolean;
synchronizedOutput: boolean;
wraparound: boolean; // defaults: xterm - true, vt100 - false
}
+6 -5
View File
@@ -137,7 +137,7 @@ export class WriteBuffer extends Disposable {
* effectively lowering the redrawing needs, schematically:
*
* macroTask _innerWrite:
* if (Date.now() - (lastTime | 0) < WRITE_TIMEOUT_MS):
* if (performance.now() - (lastTime | 0) < WRITE_TIMEOUT_MS):
* schedule microTask _innerWrite(lastTime)
* else:
* schedule macroTask _innerWrite(0)
@@ -158,7 +158,7 @@ export class WriteBuffer extends Disposable {
* Note, for pure sync code `lastTime` and `promiseResult` have no meaning.
*/
protected _innerWrite(lastTime: number = 0, promiseResult: boolean = true): void {
const startTime = lastTime || Date.now();
const startTime = lastTime || performance.now();
while (this._writeBuffer.length > this._bufferOffset) {
const data = this._writeBuffer[this._bufferOffset];
const result = this._action(data, promiseResult);
@@ -186,7 +186,7 @@ export class WriteBuffer extends Disposable {
* responsibility to slice hard work), but we can at least schedule a screen update as we
* gain control.
*/
const continuation: (r: boolean) => void = (r: boolean) => Date.now() - startTime >= WRITE_TIMEOUT_MS
const continuation: (r: boolean) => void = (r: boolean) => performance.now() - startTime >= WRITE_TIMEOUT_MS
? setTimeout(() => this._innerWrite(0, r))
: this._innerWrite(startTime, r);
@@ -202,7 +202,8 @@ export class WriteBuffer extends Disposable {
* throughput by eval'ing `startTime` upfront pulling at least one more chunk into the
* current microtask queue (executed before setTimeout).
*/
// const continuation: (r: boolean) => void = Date.now() - startTime >= WRITE_TIMEOUT_MS
// const continuation: (r: boolean) => void = performance.now() - startTime >=
// WRITE_TIMEOUT_MS
// ? r => setTimeout(() => this._innerWrite(0, r))
// : r => this._innerWrite(startTime, r);
@@ -222,7 +223,7 @@ export class WriteBuffer extends Disposable {
this._bufferOffset++;
this._pendingData -= data.length;
if (Date.now() - startTime >= WRITE_TIMEOUT_MS) {
if (performance.now() - startTime >= WRITE_TIMEOUT_MS) {
break;
}
}
+8 -5
View File
@@ -7,7 +7,7 @@ import { Disposable } from 'vs/base/common/lifecycle';
import { IAttributeData, IBufferLine } from 'common/Types';
import { BufferSet } from 'common/buffer/BufferSet';
import { IBuffer, IBufferSet } from 'common/buffer/Types';
import { IBufferService, IOptionsService } from 'common/services/Services';
import { IBufferService, IOptionsService, type IBufferResizeEvent } from 'common/services/Services';
import { Emitter } from 'vs/base/common/event';
export const MINIMUM_COLS = 2; // Less than 2 can mess with wide chars
@@ -22,7 +22,7 @@ export class BufferService extends Disposable implements IBufferService {
/** Whether the user is scrolling (locks the scroll position) */
public isUserScrolling: boolean = false;
private readonly _onResize = this._register(new Emitter<{ cols: number, rows: number }>());
private readonly _onResize = this._register(new Emitter<IBufferResizeEvent>());
public readonly onResize = this._onResize.event;
private readonly _onScroll = this._register(new Emitter<number>());
public readonly onScroll = this._onScroll.event;
@@ -37,15 +37,18 @@ export class BufferService extends Disposable implements IBufferService {
this.cols = Math.max(optionsService.rawOptions.cols || 0, MINIMUM_COLS);
this.rows = Math.max(optionsService.rawOptions.rows || 0, MINIMUM_ROWS);
this.buffers = this._register(new BufferSet(optionsService, this));
this._register(this.buffers.onBufferActivate(e => {
this._onScroll.fire(e.activeBuffer.ydisp);
}));
}
public resize(cols: number, rows: number): void {
const colsChanged = this.cols !== cols;
const rowsChanged = this.rows !== rows;
this.cols = cols;
this.rows = rows;
this.buffers.resize(cols, rows);
// TODO: This doesn't fire when scrollback changes - add a resize event to BufferSet and forward
// event
this._onResize.fire({ cols, rows });
this._onResize.fire({ cols, rows, colsChanged, rowsChanged });
}
public reset(): void {
+10 -9
View File
@@ -3,13 +3,14 @@
* @license MIT
*/
import { CoreMouseService } from 'common/services/CoreMouseService';
import { MockCoreService, MockBufferService } from 'common/TestUtils.test';
import { MockCoreService, MockBufferService, MockOptionsService } from 'common/TestUtils.test';
import { assert } from 'chai';
import { ICoreMouseEvent, CoreMouseEventType, CoreMouseButton, CoreMouseAction } from 'common/Types';
// needed mock services
const bufferService = new MockBufferService(300, 100);
const coreService = new MockCoreService();
const optionsService = new MockOptionsService();
function toBytes(s: string | undefined): number[] {
if (!s) {
@@ -24,20 +25,20 @@ function toBytes(s: string | undefined): number[] {
describe('CoreMouseService', () => {
it('init', () => {
const cms = new CoreMouseService(bufferService, coreService);
const cms = new CoreMouseService(bufferService, coreService, optionsService);
assert.equal(cms.activeEncoding, 'DEFAULT');
assert.equal(cms.activeProtocol, 'NONE');
});
it('default protocols - NONE, X10, VT200, DRAG, ANY', () => {
const cms = new CoreMouseService(bufferService, coreService);
const cms = new CoreMouseService(bufferService, coreService, optionsService);
assert.deepEqual(Object.keys((cms as any)._protocols), ['NONE', 'X10', 'VT200', 'DRAG', 'ANY']);
});
it('default encodings - DEFAULT, SGR', () => {
const cms = new CoreMouseService(bufferService, coreService);
const cms = new CoreMouseService(bufferService, coreService, optionsService);
assert.deepEqual(Object.keys((cms as any)._encodings), ['DEFAULT', 'SGR', 'SGR_PIXELS']);
});
it('protocol/encoding setter, reset', () => {
const cms = new CoreMouseService(bufferService, coreService);
const cms = new CoreMouseService(bufferService, coreService, optionsService);
cms.activeEncoding = 'SGR';
cms.activeProtocol = 'ANY';
assert.equal(cms.activeEncoding, 'SGR');
@@ -49,19 +50,19 @@ describe('CoreMouseService', () => {
assert.throws(() => { cms.activeProtocol = 'xyz'; }, 'unknown protocol "xyz"');
});
it('addEncoding', () => {
const cms = new CoreMouseService(bufferService, coreService);
const cms = new CoreMouseService(bufferService, coreService, optionsService);
cms.addEncoding('XYZ', (e: ICoreMouseEvent) => '');
cms.activeEncoding = 'XYZ';
assert.equal(cms.activeEncoding, 'XYZ');
});
it('addProtocol', () => {
const cms = new CoreMouseService(bufferService, coreService);
const cms = new CoreMouseService(bufferService, coreService, optionsService);
cms.addProtocol('XYZ', { events: CoreMouseEventType.NONE, restrict: (e: ICoreMouseEvent) => false });
cms.activeProtocol = 'XYZ';
assert.equal(cms.activeProtocol, 'XYZ');
});
it('onProtocolChange', () => {
const cms = new CoreMouseService(bufferService, coreService);
const cms = new CoreMouseService(bufferService, coreService, optionsService);
const wantedEvents: CoreMouseEventType[] = [];
cms.onProtocolChange(events => wantedEvents.push(events));
cms.activeProtocol = 'NONE';
@@ -76,7 +77,7 @@ describe('CoreMouseService', () => {
let cms: CoreMouseService;
let reports: string[];
beforeEach(() => {
cms = new CoreMouseService(bufferService, coreService);
cms = new CoreMouseService(bufferService, coreService, optionsService);
reports = [];
coreService.triggerDataEvent = (data: string, userInput?: boolean) => reports.push(data);
coreService.triggerBinaryEvent = (data: string) => reports.push(data);
+48 -3
View File
@@ -2,7 +2,7 @@
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IBufferService, ICoreService, ICoreMouseService } from 'common/services/Services';
import { IBufferService, ICoreService, ICoreMouseService, IOptionsService } from 'common/services/Services';
import { ICoreMouseProtocol, ICoreMouseEvent, CoreMouseEncoding, CoreMouseEventType, CoreMouseButton, CoreMouseAction } from 'common/Types';
import { Disposable } from 'vs/base/common/lifecycle';
import { Emitter } from 'vs/base/common/event';
@@ -174,13 +174,15 @@ export class CoreMouseService extends Disposable implements ICoreMouseService {
private _activeProtocol: string = '';
private _activeEncoding: string = '';
private _lastEvent: ICoreMouseEvent | null = null;
private _wheelPartialScroll: number = 0;
private readonly _onProtocolChange = this._register(new Emitter<CoreMouseEventType>());
public readonly onProtocolChange = this._onProtocolChange.event;
public readonly onProtocolChange = this._onProtocolChange.event;
constructor(
@IBufferService private readonly _bufferService: IBufferService,
@ICoreService private readonly _coreService: ICoreService
@ICoreService private readonly _coreService: ICoreService,
@IOptionsService private readonly _optionsService: IOptionsService
) {
super();
// register default protocols and encodings
@@ -229,6 +231,49 @@ export class CoreMouseService extends Disposable implements ICoreMouseService {
this.activeProtocol = 'NONE';
this.activeEncoding = 'DEFAULT';
this._lastEvent = null;
this._wheelPartialScroll = 0;
}
/**
* Processes a wheel event, accounting for partial scrolls for trackpad, mouse scrolls.
* This prevents hyper-sensitive scrolling in alt buffer.
*/
public consumeWheelEvent(ev: WheelEvent, cellHeight?: number, dpr?: number): number {
// Do nothing if it's not a vertical scroll event
if (ev.deltaY === 0 || ev.shiftKey) {
return 0;
}
if (cellHeight === undefined || dpr === undefined) {
return 0;
}
const targetWheelEventPixels = cellHeight / dpr;
let amount = this._applyScrollModifier(ev.deltaY, ev);
if (ev.deltaMode === WheelEvent.DOM_DELTA_PIXEL) {
amount /= (targetWheelEventPixels + 0.0); // Prevent integer division
const isLikelyTrackpad = Math.abs(ev.deltaY) < 50;
if (isLikelyTrackpad) {
amount *= 0.3;
}
this._wheelPartialScroll += amount;
amount = Math.floor(Math.abs(this._wheelPartialScroll)) * (this._wheelPartialScroll > 0 ? 1 : -1);
this._wheelPartialScroll %= 1;
} else if (ev.deltaMode === WheelEvent.DOM_DELTA_PAGE) {
amount *= this._bufferService.rows;
}
return amount;
}
private _applyScrollModifier(amount: number, ev: WheelEvent): number {
// Multiply the scroll speed when the modifier key is pressed
if (ev.altKey || ev.ctrlKey || ev.shiftKey) {
return amount * this._optionsService.rawOptions.fastScrollSensitivity * this._optionsService.rawOptions.scrollSensitivity;
}
return amount * this._optionsService.rawOptions.scrollSensitivity;
}
/**
+5 -2
View File
@@ -22,6 +22,7 @@ const DEFAULT_DEC_PRIVATE_MODES: IDecPrivateModes = Object.freeze({
origin: false,
reverseWraparound: false,
sendFocus: false,
synchronizedOutput: false,
wraparound: true // defaults: xterm - true, vt100 - false
});
@@ -75,7 +76,8 @@ export class CoreService extends Disposable implements ICoreService {
}
// Fire onData API
this._logService.debug(`sending data "${data}"`, () => data.split('').map(e => e.charCodeAt(0)));
this._logService.debug(`sending data "${data}"`);
this._logService.trace(`sending data (codes)`, () => data.split('').map(e => e.charCodeAt(0)));
this._onData.fire(data);
}
@@ -83,7 +85,8 @@ export class CoreService extends Disposable implements ICoreService {
if (this._optionsService.rawOptions.disableStdin) {
return;
}
this._logService.debug(`sending binary "${data}"`, () => data.split('').map(e => e.charCodeAt(0)));
this._logService.debug(`sending binary "${data}"`);
this._logService.trace(`sending binary (codes)`, () => data.split('').map(e => e.charCodeAt(0)));
this._onBinary.fire(data);
}
}
+13 -1
View File
@@ -18,7 +18,7 @@ export interface IBufferService {
readonly buffer: IBuffer;
readonly buffers: IBufferSet;
isUserScrolling: boolean;
onResize: Event<{ cols: number, rows: number }>;
onResize: Event<IBufferResizeEvent>;
onScroll: Event<number>;
scroll(eraseAttr: IAttributeData, isWrapped?: boolean): void;
scrollLines(disp: number, suppressScrollEvent?: boolean): void;
@@ -26,6 +26,13 @@ export interface IBufferService {
reset(): void;
}
export interface IBufferResizeEvent {
cols: number;
rows: number;
colsChanged: boolean;
rowsChanged: boolean;
}
export const ICoreMouseService = createDecorator<ICoreMouseService>('CoreMouseService');
export interface ICoreMouseService {
serviceBrand: undefined;
@@ -58,6 +65,11 @@ export interface ICoreMouseService {
* Human readable version of mouse events.
*/
explainEvents(events: CoreMouseEventType): { [event: string]: boolean };
/**
* Process wheel event taking partial scroll into account.
*/
consumeWheelEvent(ev: WheelEvent, cellHeight?: number, dpr?: number): number;
}
export const ICoreService = createDecorator<ICoreService>('CoreService');
+1
View File
@@ -400,6 +400,7 @@ describe('Headless API Tests', function (): void {
originMode: false,
reverseWraparoundMode: false,
sendFocusMode: false,
synchronizedOutputMode: false,
wraparoundMode: true
});
});
+1
View File
@@ -124,6 +124,7 @@ export class Terminal extends Disposable implements ITerminalApi {
originMode: m.origin,
reverseWraparoundMode: m.reverseWraparound,
sendFocusMode: m.sendFocus,
synchronizedOutputMode: m.synchronizedOutput,
wraparoundMode: m.wraparound
};
}
+1 -1
View File
@@ -16,7 +16,7 @@ node ./bin/vs_base_find_unused.js
The last step is to do a once over of the resulting bundled xterm.js file to ensure it isn't too large:
1. Run `yarn esbuild`
1. Run `npm run esbuild`
2. Open up `xterm.mjs`
3. Search for `src/vs/base/`
+1 -1
View File
@@ -124,7 +124,7 @@ export function raceTimeout<T>(promise: Promise<T>, timeout: number, onTimeout?:
]);
}
export function asPromise<T>(callback: () => T | Thenable<T>): Promise<T> {
export function asPromise<T>(callback: () => T | PromiseLike<T>): Promise<T> {
return new Promise<T>((resolve, reject) => {
const item = callback();
if (isThenable<T>(item)) {
-12
View File
@@ -1,12 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/**
* Thenable is a common denominator between ES6 promises, Q, jquery.Deferred, WinJS.Promise,
* and others. This API makes no assumption about what promise library is being used which
* enables reusing existing code without migrating to a specific promise implementation. Still,
* we recommend the use of native promises which are available in VS Code.
*/
interface Thenable<T> extends PromiseLike<T> { }
+88 -6
View File
@@ -5,8 +5,9 @@
import { IImage32, decodePng } from '@lunapaint/png-codec';
import { LocatorScreenshotOptions, test } from '@playwright/test';
import { ITheme } from '@xterm/xterm';
import { ITestContext, MaybeAsync, openTerminal, pollFor, pollForApproximate } from './TestUtils';
import { ITheme, type ITerminalOptions } from '@xterm/xterm';
import { ITestContext, MaybeAsync, openTerminal, pollFor, pollForApproximate, timeout } from './TestUtils';
import { notDeepStrictEqual } from 'node:assert';
export interface ISharedRendererTestContext {
value: ITestContext;
@@ -1267,6 +1268,80 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [128, 0, 0, 255]);
});
});
// TODO: These tests are a little too flaky atm
test.describe.skip('synchronized output', () => {
test.beforeEach(async () => {
const theme: ITheme = {
background: '#000000FF',
red: '#FF0000FF',
green: '#00FF00FF',
blue: '#0000FFFF'
};
const options: ITerminalOptions = {}
await ctx.value.page.evaluate(`
window.term.options.theme = ${JSON.stringify(theme)};
window.term.options.cursorStyle = 'underline';
`);
});
test('defers rendering until ESU', async () => {
await ctx.value.proxy.write('\x1b[?2026h'); // BSU
await ctx.value.proxy.write('\x1b[31m■');
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [255, 0, 0, 255], undefined, {
equalityFn: (a, b) => {
return !(a[0] === b[0] && a[1] === b[1] && a[2] === b[2] && a[3] === b[3]);
}
});
await ctx.value.proxy.write('\x1b[?2026l'); // ESU
frameDetails = undefined;
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [255, 0, 0, 255]);
});
test('batches multiple writes', async () => {
await ctx.value.proxy.write('\x1b[?2026h'); // BSU
await ctx.value.proxy.write('\x1b[31m■\x1b[32m■\x1b[34m■');
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [255, 0, 0, 255], undefined, {
equalityFn: (a, b) => {
return !(a[0] === b[0] && a[1] === b[1] && a[2] === b[2] && a[3] === b[3]);
}
});
await ctx.value.proxy.write('\x1b[?2026l'); // ESU
frameDetails = undefined;
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [255, 0, 0, 255]);
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 2, 1), [0, 255, 0, 255]);
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 3, 1), [0, 0, 255, 255]);
});
test('nested BSU is idempotent', async () => {
await ctx.value.proxy.write('\x1b[?2026h'); // BSU
await ctx.value.proxy.write('\x1b[31m■');
await ctx.value.proxy.write('\x1b[?2026h'); // BSU
await ctx.value.proxy.write('\x1b[32m■');
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [255, 0, 0, 255], undefined, {
equalityFn: (a, b) => {
return !(a[0] === b[0] && a[1] === b[1] && a[2] === b[2] && a[3] === b[3]);
}
});
await ctx.value.proxy.write('\x1b[?2026l'); // ESU
frameDetails = undefined;
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [255, 0, 0, 255]);
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 2, 1), [0, 255, 0, 255]);
});
test('timeout flushes without ESU', async () => {
await ctx.value.proxy.write('\x1b[?2026h'); // BSU
await ctx.value.proxy.write('\x1b[31m■');
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [255, 0, 0, 255], undefined, {
equalityFn: (a, b) => {
return !(a[0] === b[0] && a[1] === b[1] && a[2] === b[2] && a[3] === b[3]);
}
});
await ctx.value.page.waitForTimeout(1000); // Timeout hard coded
frameDetails = undefined;
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [255, 0, 0, 255]);
});
});
}
enum CellColorPosition {
@@ -1280,10 +1355,10 @@ enum CellColorPosition {
* treatment.
*/
export function injectSharedRendererTestsStandalone(ctx: ISharedRendererTestContext, setupCb: () => Promise<void> | void): void {
test.describe('standalone tests', () => {
const setupTests = ({ shadowDom }: { shadowDom: boolean }): void => {
test.beforeEach(async () => {
// Recreate terminal
await openTerminal(ctx.value);
await openTerminal(ctx.value, {}, { useShadowDom: shadowDom });
await ctx.value.page.evaluate(`
window.term.options.minimumContrastRatio = 1;
window.term.options.allowTransparency = false;
@@ -1308,6 +1383,13 @@ export function injectSharedRendererTestsStandalone(ctx: ISharedRendererTestCont
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [0, 0, 0, 255]);
});
});
};
test.describe('standalone tests', () => {
setupTests({ shadowDom: false });
});
test.describe('standalone tests (Shadow dom)', () => {
setupTests({ shadowDom: true });
});
}
@@ -1317,9 +1399,9 @@ export function injectSharedRendererTestsStandalone(ctx: ISharedRendererTestCont
* @param col The 1-based column index to get the color for.
* @param row The 1-based row index to get the color for.
*/
function getCellColor(ctx: ITestContext, col: number, row: number, position: CellColorPosition = CellColorPosition.CENTER): MaybeAsync<[red: number, green: number, blue: number, alpha: number]> {
async function getCellColor(ctx: ITestContext, col: number, row: number, position: CellColorPosition = CellColorPosition.CENTER): Promise<[red: number, green: number, blue: number, alpha: number]> {
if (!frameDetails) {
return getFrameDetails(ctx).then(frameDetails => getCellColorInner(frameDetails, col, row));
frameDetails = await getFrameDetails(ctx);
}
switch (position) {
case CellColorPosition.CENTER:
+1
View File
@@ -609,6 +609,7 @@ test.describe('API Integration Tests', () => {
originMode: false,
reverseWraparoundMode: false,
sendFocusMode: false,
synchronizedOutputMode: false,
wraparoundMode: true
});
});
+49 -5
View File
@@ -10,7 +10,7 @@ import type { IRenderService } from 'browser/services/Services';
import type { ICoreTerminal, IDisposable, IMarker } from 'common/Types';
import * as playwright from '@playwright/test';
import { PageFunction } from 'playwright-core/types/structs';
import { IBuffer, IBufferCell, IBufferLine, IBufferNamespace, IBufferRange, IDecoration, IDecorationOptions, IModes, ITerminalInitOnlyOptions, ITerminalOptions, Terminal } from '@xterm/xterm';
import { ICellColorSpan, IBuffer, IBufferCell, IBufferLine, IBufferNamespace, IBufferRange, IDecoration, IDecorationOptions, IModes, ITerminalInitOnlyOptions, ITerminalOptions, Terminal } from '@xterm/xterm';
export interface ITestContext {
browser: Browser;
@@ -279,6 +279,8 @@ export class TerminalProxy implements ITerminalProxyCustomMethods, PlaywrightApi
public async resize(cols: number, rows: number): Promise<void> { return this._page.evaluate(([term, cols, rows]) => term.resize(cols, rows), [await this.getHandle(), cols, rows] as const); }
public async registerMarker(y?: number | undefined): Promise<IMarker> { return this._page.evaluate(([term, y]) => term.registerMarker(y), [await this.getHandle(), y] as const); }
public async registerDecoration(decorationOptions: IDecorationOptions): Promise<IDecoration | undefined> { return this._page.evaluate(([term, decorationOptions]) => term.registerDecoration(decorationOptions), [await this.getHandle(), decorationOptions] as const); }
public setCellColorOverrides(lineY: number, spans: ICellColorSpan[] | null): void { throw new Error('not supported via proxy'); }
public async clearCellColorOverrides(): Promise<void> { return this._page.evaluate((term) => term.clearCellColorOverrides(), await this.getHandle()); }
public async clearTextureAtlas(): Promise<void> { return this.evaluate(([term]) => term.clearTextureAtlas()); }
// #endregion
@@ -412,7 +414,14 @@ class TerminalCoreProxy {
}
}
export async function openTerminal(ctx: ITestContext, options: ITerminalOptions | ITerminalInitOnlyOptions = {}, testOptions: { loadUnicodeGraphemesAddon: boolean } = { loadUnicodeGraphemesAddon: true }): Promise<void> {
export async function openTerminal(
ctx: ITestContext,
options: ITerminalOptions | ITerminalInitOnlyOptions = {},
testOptions: { useShadowDom?: boolean, loadUnicodeGraphemesAddon?: boolean } = {}
): Promise<void> {
testOptions.useShadowDom ??= false;
testOptions.loadUnicodeGraphemesAddon ??= true;
await ctx.page.evaluate(`
if ('term' in window) {
try {
@@ -423,10 +432,45 @@ export async function openTerminal(ctx: ITestContext, options: ITerminalOptions
// HACK: Tests may have side effects that could cause the terminal not to be removed. This
// assertion catches this case early.
strictEqual(await ctx.page.evaluate(`document.querySelector('#terminal-container').children.length`), 0, 'there must be no terminals on the page');
await ctx.page.evaluate(`
let script = `
window.term = new window.Terminal(${JSON.stringify({ allowProposedApi: true, ...options })});
window.term.open(document.querySelector('#terminal-container'));
`);
let element = document.querySelector('#terminal-container');
// Remove shadow root if it exists
const newElement = element.cloneNode(false);
element.replaceWith(newElement);
element = newElement
`;
if (testOptions.useShadowDom) {
script += `
const shadowRoot = element.attachShadow({ mode: "open" });
// Copy parent styles to shadow DOM
const styles = Array.from(document.querySelectorAll('link[rel="stylesheet"]'));
styles.forEach((styleEl) => {
const clone = document.createElement('link');
clone.rel = 'stylesheet';
clone.href = styleEl.href;
shadowRoot.appendChild(clone);
});
// Create new element inside the shadow DOM
element = document.createElement('div');
element.style.width = '100%';
element.style.height = '100%';
shadowRoot.appendChild(element);
`;
}
script += `
window.term.open(element);
`;
await ctx.page.evaluate(script);
// HACK: This is a soft layer breaker that's temporarily included until unicode graphemes have
// more complete integration tests. See https://github.com/xtermjs/xterm.js/pull/4519#discussion_r1285234453
if (testOptions.loadUnicodeGraphemesAddon) {
+7
View File
@@ -1370,6 +1370,13 @@ declare module '@xterm/headless' {
* Send FocusIn/FocusOut events: `CSI ? 1 0 0 4 h`
*/
readonly sendFocusMode: boolean;
/**
* Synchronized Output Mode: `CSI ? 2 0 2 6 h`
*
* When enabled, output is buffered and only rendered when the mode is
* disabled, allowing for atomic screen updates without tearing.
*/
readonly synchronizedOutputMode: boolean;
/**
* Auto-Wrap Mode (DECAWM): `CSI ? 7 h`
*/
+36 -26
View File
@@ -107,13 +107,6 @@ declare module '@xterm/xterm' {
*/
drawBoldTextInBrightColors?: boolean;
/**
* The modifier key hold to multiply scroll speed.
* @deprecated This option is no longer available and will always use alt.
* Setting this will be ignored.
*/
fastScrollModifier?: 'none' | 'alt' | 'ctrl' | 'shift';
/**
* The scroll speed multiplier used for fast scrolling when `Alt` is held.
*/
@@ -291,25 +284,6 @@ declare module '@xterm/xterm' {
*/
theme?: ITheme;
/**
* Whether "Windows mode" is enabled. Because Windows backends winpty and
* conpty operate by doing line wrapping on their side, xterm.js does not
* have access to wrapped lines. When Windows mode is enabled the following
* changes will be in effect:
*
* - Reflow is disabled.
* - Lines are assumed to be wrapped if the last character of the line is
* not whitespace.
*
* When using conpty on Windows 11 version >= 21376, it is recommended to
* disable this because native text wrapping sequences are output correctly
* thanks to https://github.com/microsoft/terminal/issues/405
*
* @deprecated Use {@link windowsPty}. This value will be ignored if
* windowsPty is set.
*/
windowsMode?: boolean;
/**
* Compatibility information when the pty is known to be hosted on Windows.
* Setting this will turn on certain heuristics/workarounds depending on the
@@ -638,6 +612,19 @@ declare module '@xterm/xterm' {
overviewRulerOptions?: IDecorationOverviewRulerOptions;
}
/**
* A contiguous cell range on one buffer line whose text should be rendered
* in a specific color, used with {@link Terminal.setCellColorOverrides}.
*/
export interface ICellColorSpan {
/** Zero-based column of the first cell. */
col: number;
/** Number of cells covered (wide characters count as 2 cells). */
width: number;
/** CSS color string (#rrggbb, rgb(...), etc.). */
color: string;
}
/**
* The set of localizable strings.
*/
@@ -1182,6 +1169,22 @@ declare module '@xterm/xterm' {
*/
registerDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined;
/**
* Recolor a contiguous range of cells on one buffer line by drawing the
* covered text in the given color. Pass null for spans to clear the line.
* Line indexes are absolute within the active buffer; the caller must
* clear/re-set overrides when indexes shift (scrollback trim, resize
* reflow) or when the alternate buffer changes.
* @param lineY The absolute buffer line index (ydisp-independent).
* @param spans The cell ranges and colors to apply, or null to clear.
*/
setCellColorOverrides(lineY: number, spans: ICellColorSpan[] | null): void;
/**
* Clears all cell color overrides set by setCellColorOverrides.
*/
clearCellColorOverrides(): void;
/**
* Gets whether the terminal has an active selection.
*/
@@ -1968,6 +1971,13 @@ declare module '@xterm/xterm' {
* Send FocusIn/FocusOut events: `CSI ? 1 0 0 4 h`
*/
readonly sendFocusMode: boolean;
/**
* Synchronized Output Mode: `CSI ? 2 0 2 6 h`
*
* When enabled, output is buffered and only rendered when the mode is
* disabled, allowing for atomic screen updates without tearing.
*/
readonly synchronizedOutputMode: boolean;
/**
* Auto-Wrap Mode (DECAWM): `CSI ? 7 h`
*/
+3 -3
View File
@@ -7,9 +7,9 @@ const path = require('path');
/**
* This webpack config does a production build for xterm.js headless. It works by taking the output
* from tsc (via `yarn watch` or `yarn prebuild`) which are put into `out/` and webpacks them into a
* production mode umd library module in `lib-headless/`. The aliases are used fix up the absolute
* paths output by tsc (because of `baseUrl` and `paths` in `tsconfig.json`.
* from tsc (via `npm run watch` or `npm run prebuild`) which are put into `out/` and webpacks them
* into a production mode umd library module in `lib-headless/`. The aliases are used fix up the
* absolute paths output by tsc (because of `baseUrl` and `paths` in `tsconfig.json`.
*
* @type {import('webpack').Configuration}
*/
+1 -1
View File
@@ -9,7 +9,7 @@ const path = require('path');
/**
* This webpack config does a production build for xterm.js. It works by taking the output from tsc
* (via `yarn watch` or `yarn prebuild`) which are put into `out/` and webpacks them into a
* (via `npm run watch` or `npm run prebuild`) which are put into `out/` and webpacks them into a
* production mode umd library module in `lib/`. The aliases are used fix up the absolute paths
* output by tsc (because of `baseUrl` and `paths` in `tsconfig.json`.
*
-4567
View File
File diff suppressed because it is too large Load Diff