feat: implement file editing capabilities with CodeMirror integration
- Added CodeMirror editor for file content editing in FilesView. - Introduced draft saving functionality with unsaved changes confirmation dialog. - Implemented file writing API to persist changes to the filesystem. - Enhanced language support for syntax highlighting based on file extensions. - Updated UI components to support new editing features, including save and discard options. - Refactored line selection logic for improved user experience on both desktop and mobile. - Added new utility functions for language detection by file extension. - Introduced a new theme for CodeMirror to align with the application's design.
This commit is contained in:
@@ -5,12 +5,32 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "openchamber-monorepo",
|
"name": "openchamber-monorepo",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@codemirror/autocomplete": "^6.20.0",
|
||||||
|
"@codemirror/commands": "^6.10.1",
|
||||||
|
"@codemirror/highlight": "^0.19.8",
|
||||||
|
"@codemirror/history": "^0.19.2",
|
||||||
|
"@codemirror/lang-css": "^6.3.1",
|
||||||
|
"@codemirror/lang-html": "^6.4.11",
|
||||||
|
"@codemirror/lang-javascript": "^6.2.4",
|
||||||
|
"@codemirror/lang-json": "^6.0.2",
|
||||||
|
"@codemirror/lang-markdown": "^6.5.0",
|
||||||
|
"@codemirror/lang-python": "^6.2.1",
|
||||||
|
"@codemirror/lang-rust": "^6.0.2",
|
||||||
|
"@codemirror/lang-sql": "^6.10.0",
|
||||||
|
"@codemirror/lang-xml": "^6.1.0",
|
||||||
|
"@codemirror/lang-yaml": "^6.1.2",
|
||||||
|
"@codemirror/language": "^6.12.1",
|
||||||
|
"@codemirror/lint": "^6.9.2",
|
||||||
|
"@codemirror/search": "^6.6.0",
|
||||||
|
"@codemirror/state": "^6.5.4",
|
||||||
|
"@codemirror/view": "^6.39.11",
|
||||||
"@fontsource/ibm-plex-mono": "^5.2.7",
|
"@fontsource/ibm-plex-mono": "^5.2.7",
|
||||||
"@fontsource/ibm-plex-sans": "^5.1.1",
|
"@fontsource/ibm-plex-sans": "^5.1.1",
|
||||||
"@heroui/scroll-shadow": "^2.3.18",
|
"@heroui/scroll-shadow": "^2.3.18",
|
||||||
"@heroui/system": "^2.4.23",
|
"@heroui/system": "^2.4.23",
|
||||||
"@heroui/theme": "^2.4.23",
|
"@heroui/theme": "^2.4.23",
|
||||||
"@ibm/plex": "^6.4.1",
|
"@ibm/plex": "^6.4.1",
|
||||||
|
"@lezer/highlight": "^1.2.3",
|
||||||
"@opencode-ai/sdk": "^1.1.19",
|
"@opencode-ai/sdk": "^1.1.19",
|
||||||
"@radix-ui/react-collapsible": "^1.1.12",
|
"@radix-ui/react-collapsible": "^1.1.12",
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
@@ -74,7 +94,7 @@
|
|||||||
},
|
},
|
||||||
"packages/desktop": {
|
"packages/desktop": {
|
||||||
"name": "@openchamber/desktop",
|
"name": "@openchamber/desktop",
|
||||||
"version": "1.4.8",
|
"version": "1.5.2",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@openchamber/ui": "workspace:*",
|
"@openchamber/ui": "workspace:*",
|
||||||
"@tauri-apps/plugin-notification": "^2.3.3",
|
"@tauri-apps/plugin-notification": "^2.3.3",
|
||||||
@@ -97,14 +117,36 @@
|
|||||||
},
|
},
|
||||||
"packages/ui": {
|
"packages/ui": {
|
||||||
"name": "@openchamber/ui",
|
"name": "@openchamber/ui",
|
||||||
"version": "1.4.8",
|
"version": "1.5.2",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@codemirror/autocomplete": "^6.20.0",
|
||||||
|
"@codemirror/commands": "^6.10.1",
|
||||||
|
"@codemirror/highlight": "^0.19.8",
|
||||||
|
"@codemirror/history": "^0.19.2",
|
||||||
|
"@codemirror/lang-css": "^6.3.1",
|
||||||
|
"@codemirror/lang-html": "^6.4.11",
|
||||||
|
"@codemirror/lang-javascript": "^6.2.4",
|
||||||
|
"@codemirror/lang-json": "^6.0.2",
|
||||||
|
"@codemirror/lang-markdown": "^6.5.0",
|
||||||
|
"@codemirror/lang-python": "^6.2.1",
|
||||||
|
"@codemirror/lang-rust": "^6.0.2",
|
||||||
|
"@codemirror/lang-sql": "^6.10.0",
|
||||||
|
"@codemirror/lang-xml": "^6.1.0",
|
||||||
|
"@codemirror/lang-yaml": "^6.1.2",
|
||||||
|
"@codemirror/language": "^6.12.1",
|
||||||
|
"@codemirror/language-data": "^6.5.2",
|
||||||
|
"@codemirror/legacy-modes": "^6.5.2",
|
||||||
|
"@codemirror/lint": "^6.9.2",
|
||||||
|
"@codemirror/search": "^6.6.0",
|
||||||
|
"@codemirror/state": "^6.5.4",
|
||||||
|
"@codemirror/view": "^6.39.11",
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@fontsource/ibm-plex-mono": "^5.2.7",
|
"@fontsource/ibm-plex-mono": "^5.2.7",
|
||||||
"@fontsource/ibm-plex-sans": "^5.1.1",
|
"@fontsource/ibm-plex-sans": "^5.1.1",
|
||||||
"@ibm/plex": "^6.4.1",
|
"@ibm/plex": "^6.4.1",
|
||||||
|
"@lezer/highlight": "^1.2.3",
|
||||||
"@opencode-ai/sdk": "^1.1.19",
|
"@opencode-ai/sdk": "^1.1.19",
|
||||||
"@pierre/diffs": "^1.0.5",
|
"@pierre/diffs": "^1.0.5",
|
||||||
"@radix-ui/react-collapsible": "^1.1.12",
|
"@radix-ui/react-collapsible": "^1.1.12",
|
||||||
@@ -169,7 +211,7 @@
|
|||||||
},
|
},
|
||||||
"packages/vscode": {
|
"packages/vscode": {
|
||||||
"name": "openchamber",
|
"name": "openchamber",
|
||||||
"version": "1.4.8",
|
"version": "1.5.2",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@openchamber/ui": "workspace:*",
|
"@openchamber/ui": "workspace:*",
|
||||||
"@opencode-ai/sdk": "^1.1.19",
|
"@opencode-ai/sdk": "^1.1.19",
|
||||||
@@ -192,7 +234,7 @@
|
|||||||
},
|
},
|
||||||
"packages/web": {
|
"packages/web": {
|
||||||
"name": "@openchamber/web",
|
"name": "@openchamber/web",
|
||||||
"version": "1.4.8",
|
"version": "1.5.2",
|
||||||
"bin": {
|
"bin": {
|
||||||
"openchamber": "./bin/cli.js",
|
"openchamber": "./bin/cli.js",
|
||||||
},
|
},
|
||||||
@@ -347,6 +389,74 @@
|
|||||||
|
|
||||||
"@chevrotain/utils": ["@chevrotain/utils@11.0.3", "", {}, "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ=="],
|
"@chevrotain/utils": ["@chevrotain/utils@11.0.3", "", {}, "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ=="],
|
||||||
|
|
||||||
|
"@codemirror/autocomplete": ["@codemirror/autocomplete@6.20.0", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0" } }, "sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg=="],
|
||||||
|
|
||||||
|
"@codemirror/commands": ["@codemirror/commands@6.10.1", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.4.0", "@codemirror/view": "^6.27.0", "@lezer/common": "^1.1.0" } }, "sha512-uWDWFypNdQmz2y1LaNJzK7fL7TYKLeUAU0npEC685OKTF3KcQ2Vu3klIM78D7I6wGhktme0lh3CuQLv0ZCrD9Q=="],
|
||||||
|
|
||||||
|
"@codemirror/highlight": ["@codemirror/highlight@0.19.8", "", { "dependencies": { "@codemirror/language": "^0.19.0", "@codemirror/rangeset": "^0.19.0", "@codemirror/state": "^0.19.3", "@codemirror/view": "^0.19.39", "@lezer/common": "^0.15.0", "style-mod": "^4.0.0" } }, "sha512-v/lzuHjrYR8MN2mEJcUD6fHSTXXli9C1XGYpr+ElV6fLBIUhMTNKR3qThp611xuWfXfwDxeL7ppcbkM/MzPV3A=="],
|
||||||
|
|
||||||
|
"@codemirror/history": ["@codemirror/history@0.19.2", "", { "dependencies": { "@codemirror/state": "^0.19.2", "@codemirror/view": "^0.19.0" } }, "sha512-unhP4t3N2smzmHoo/Yio6ueWi+il8gm9VKrvi6wlcdGH5fOfVDNkmjHQ495SiR+EdOG35+3iNebSPYww0vN7ow=="],
|
||||||
|
|
||||||
|
"@codemirror/lang-angular": ["@codemirror/lang-angular@0.1.4", "", { "dependencies": { "@codemirror/lang-html": "^6.0.0", "@codemirror/lang-javascript": "^6.1.2", "@codemirror/language": "^6.0.0", "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.3.3" } }, "sha512-oap+gsltb/fzdlTQWD6BFF4bSLKcDnlxDsLdePiJpCVNKWXSTAbiiQeYI3UmES+BLAdkmIC1WjyztC1pi/bX4g=="],
|
||||||
|
|
||||||
|
"@codemirror/lang-cpp": ["@codemirror/lang-cpp@6.0.3", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@lezer/cpp": "^1.0.0" } }, "sha512-URM26M3vunFFn9/sm6rzqrBzDgfWuDixp85uTY49wKudToc2jTHUrKIGGKs+QWND+YLofNNZpxcNGRynFJfvgA=="],
|
||||||
|
|
||||||
|
"@codemirror/lang-css": ["@codemirror/lang-css@6.3.1", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@lezer/common": "^1.0.2", "@lezer/css": "^1.1.7" } }, "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg=="],
|
||||||
|
|
||||||
|
"@codemirror/lang-go": ["@codemirror/lang-go@6.0.1", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.6.0", "@codemirror/state": "^6.0.0", "@lezer/common": "^1.0.0", "@lezer/go": "^1.0.0" } }, "sha512-7fNvbyNylvqCphW9HD6WFnRpcDjr+KXX/FgqXy5H5ZS0eC5edDljukm/yNgYkwTsgp2busdod50AOTIy6Jikfg=="],
|
||||||
|
|
||||||
|
"@codemirror/lang-html": ["@codemirror/lang-html@6.4.11", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/lang-css": "^6.0.0", "@codemirror/lang-javascript": "^6.0.0", "@codemirror/language": "^6.4.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0", "@lezer/css": "^1.1.0", "@lezer/html": "^1.3.12" } }, "sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw=="],
|
||||||
|
|
||||||
|
"@codemirror/lang-java": ["@codemirror/lang-java@6.0.2", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@lezer/java": "^1.0.0" } }, "sha512-m5Nt1mQ/cznJY7tMfQTJchmrjdjQ71IDs+55d1GAa8DGaB8JXWsVCkVT284C3RTASaY43YknrK2X3hPO/J3MOQ=="],
|
||||||
|
|
||||||
|
"@codemirror/lang-javascript": ["@codemirror/lang-javascript@6.2.4", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.6.0", "@codemirror/lint": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0", "@lezer/javascript": "^1.0.0" } }, "sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA=="],
|
||||||
|
|
||||||
|
"@codemirror/lang-jinja": ["@codemirror/lang-jinja@6.0.0", "", { "dependencies": { "@codemirror/lang-html": "^6.0.0", "@codemirror/language": "^6.0.0", "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.2.0", "@lezer/lr": "^1.4.0" } }, "sha512-47MFmRcR8UAxd8DReVgj7WJN1WSAMT7OJnewwugZM4XiHWkOjgJQqvEM1NpMj9ALMPyxmlziEI1opH9IaEvmaw=="],
|
||||||
|
|
||||||
|
"@codemirror/lang-json": ["@codemirror/lang-json@6.0.2", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@lezer/json": "^1.0.0" } }, "sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ=="],
|
||||||
|
|
||||||
|
"@codemirror/lang-less": ["@codemirror/lang-less@6.0.2", "", { "dependencies": { "@codemirror/lang-css": "^6.2.0", "@codemirror/language": "^6.0.0", "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-EYdQTG22V+KUUk8Qq582g7FMnCZeEHsyuOJisHRft/mQ+ZSZ2w51NupvDUHiqtsOy7It5cHLPGfHQLpMh9bqpQ=="],
|
||||||
|
|
||||||
|
"@codemirror/lang-liquid": ["@codemirror/lang-liquid@6.3.1", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/lang-html": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0", "@lezer/common": "^1.0.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.3.1" } }, "sha512-S/jE/D7iij2Pu70AC65ME6AYWxOOcX20cSJvaPgY5w7m2sfxsArAcUAuUgm/CZCVmqoi9KiOlS7gj/gyLipABw=="],
|
||||||
|
|
||||||
|
"@codemirror/lang-markdown": ["@codemirror/lang-markdown@6.5.0", "", { "dependencies": { "@codemirror/autocomplete": "^6.7.1", "@codemirror/lang-html": "^6.0.0", "@codemirror/language": "^6.3.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0", "@lezer/common": "^1.2.1", "@lezer/markdown": "^1.0.0" } }, "sha512-0K40bZ35jpHya6FriukbgaleaqzBLZfOh7HuzqbMxBXkbYMJDxfF39c23xOgxFezR+3G+tR2/Mup+Xk865OMvw=="],
|
||||||
|
|
||||||
|
"@codemirror/lang-php": ["@codemirror/lang-php@6.0.2", "", { "dependencies": { "@codemirror/lang-html": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@lezer/common": "^1.0.0", "@lezer/php": "^1.0.0" } }, "sha512-ZKy2v1n8Fc8oEXj0Th0PUMXzQJ0AIR6TaZU+PbDHExFwdu+guzOA4jmCHS1Nz4vbFezwD7LyBdDnddSJeScMCA=="],
|
||||||
|
|
||||||
|
"@codemirror/lang-python": ["@codemirror/lang-python@6.2.1", "", { "dependencies": { "@codemirror/autocomplete": "^6.3.2", "@codemirror/language": "^6.8.0", "@codemirror/state": "^6.0.0", "@lezer/common": "^1.2.1", "@lezer/python": "^1.1.4" } }, "sha512-IRjC8RUBhn9mGR9ywecNhB51yePWCGgvHfY1lWN/Mrp3cKuHr0isDKia+9HnvhiWNnMpbGhWrkhuWOc09exRyw=="],
|
||||||
|
|
||||||
|
"@codemirror/lang-rust": ["@codemirror/lang-rust@6.0.2", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@lezer/rust": "^1.0.0" } }, "sha512-EZaGjCUegtiU7kSMvOfEZpaCReowEf3yNidYu7+vfuGTm9ow4mthAparY5hisJqOHmJowVH3Upu+eJlUji6qqA=="],
|
||||||
|
|
||||||
|
"@codemirror/lang-sass": ["@codemirror/lang-sass@6.0.2", "", { "dependencies": { "@codemirror/lang-css": "^6.2.0", "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@lezer/common": "^1.0.2", "@lezer/sass": "^1.0.0" } }, "sha512-l/bdzIABvnTo1nzdY6U+kPAC51czYQcOErfzQ9zSm9D8GmNPD0WTW8st/CJwBTPLO8jlrbyvlSEcN20dc4iL0Q=="],
|
||||||
|
|
||||||
|
"@codemirror/lang-sql": ["@codemirror/lang-sql@6.10.0", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-6ayPkEd/yRw0XKBx5uAiToSgGECo/GY2NoJIHXIIQh1EVwLuKoU8BP/qK0qH5NLXAbtJRLuT73hx7P9X34iO4w=="],
|
||||||
|
|
||||||
|
"@codemirror/lang-vue": ["@codemirror/lang-vue@0.1.3", "", { "dependencies": { "@codemirror/lang-html": "^6.0.0", "@codemirror/lang-javascript": "^6.1.2", "@codemirror/language": "^6.0.0", "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.3.1" } }, "sha512-QSKdtYTDRhEHCfo5zOShzxCmqKJvgGrZwDQSdbvCRJ5pRLWBS7pD/8e/tH44aVQT6FKm0t6RVNoSUWHOI5vNug=="],
|
||||||
|
|
||||||
|
"@codemirror/lang-wast": ["@codemirror/lang-wast@6.0.2", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-Imi2KTpVGm7TKuUkqyJ5NRmeFWF7aMpNiwHnLQe0x9kmrxElndyH0K6H/gXtWwY6UshMRAhpENsgfpSwsgmC6Q=="],
|
||||||
|
|
||||||
|
"@codemirror/lang-xml": ["@codemirror/lang-xml@6.1.0", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.4.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0", "@lezer/common": "^1.0.0", "@lezer/xml": "^1.0.0" } }, "sha512-3z0blhicHLfwi2UgkZYRPioSgVTo9PV5GP5ducFH6FaHy0IAJRg+ixj5gTR1gnT/glAIC8xv4w2VL1LoZfs+Jg=="],
|
||||||
|
|
||||||
|
"@codemirror/lang-yaml": ["@codemirror/lang-yaml@6.1.2", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.2.0", "@lezer/lr": "^1.0.0", "@lezer/yaml": "^1.0.0" } }, "sha512-dxrfG8w5Ce/QbT7YID7mWZFKhdhsaTNOYjOkSIMt1qmC4VQnXSDSYVHHHn8k6kJUfIhtLo8t1JJgltlxWdsITw=="],
|
||||||
|
|
||||||
|
"@codemirror/language": ["@codemirror/language@6.12.1", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.23.0", "@lezer/common": "^1.5.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0", "style-mod": "^4.0.0" } }, "sha512-Fa6xkSiuGKc8XC8Cn96T+TQHYj4ZZ7RdFmXA3i9xe/3hLHfwPZdM+dqfX0Cp0zQklBKhVD8Yzc8LS45rkqcwpQ=="],
|
||||||
|
|
||||||
|
"@codemirror/language-data": ["@codemirror/language-data@6.5.2", "", { "dependencies": { "@codemirror/lang-angular": "^0.1.0", "@codemirror/lang-cpp": "^6.0.0", "@codemirror/lang-css": "^6.0.0", "@codemirror/lang-go": "^6.0.0", "@codemirror/lang-html": "^6.0.0", "@codemirror/lang-java": "^6.0.0", "@codemirror/lang-javascript": "^6.0.0", "@codemirror/lang-jinja": "^6.0.0", "@codemirror/lang-json": "^6.0.0", "@codemirror/lang-less": "^6.0.0", "@codemirror/lang-liquid": "^6.0.0", "@codemirror/lang-markdown": "^6.0.0", "@codemirror/lang-php": "^6.0.0", "@codemirror/lang-python": "^6.0.0", "@codemirror/lang-rust": "^6.0.0", "@codemirror/lang-sass": "^6.0.0", "@codemirror/lang-sql": "^6.0.0", "@codemirror/lang-vue": "^0.1.1", "@codemirror/lang-wast": "^6.0.0", "@codemirror/lang-xml": "^6.0.0", "@codemirror/lang-yaml": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/legacy-modes": "^6.4.0" } }, "sha512-CPkWBKrNS8stYbEU5kwBwTf3JB1kghlbh4FSAwzGW2TEscdeHHH4FGysREW86Mqnj3Qn09s0/6Ea/TutmoTobg=="],
|
||||||
|
|
||||||
|
"@codemirror/legacy-modes": ["@codemirror/legacy-modes@6.5.2", "", { "dependencies": { "@codemirror/language": "^6.0.0" } }, "sha512-/jJbwSTazlQEDOQw2FJ8LEEKVS72pU0lx6oM54kGpL8t/NJ2Jda3CZ4pcltiKTdqYSRk3ug1B3pil1gsjA6+8Q=="],
|
||||||
|
|
||||||
|
"@codemirror/lint": ["@codemirror/lint@6.9.2", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.35.0", "crelt": "^1.0.5" } }, "sha512-sv3DylBiIyi+xKwRCJAAsBZZZWo82shJ/RTMymLabAdtbkV5cSKwWDeCgtUq3v8flTaXS2y1kKkICuRYtUswyQ=="],
|
||||||
|
|
||||||
|
"@codemirror/rangeset": ["@codemirror/rangeset@0.19.9", "", { "dependencies": { "@codemirror/state": "^0.19.0" } }, "sha512-V8YUuOvK+ew87Xem+71nKcqu1SXd5QROMRLMS/ljT5/3MCxtgrRie1Cvild0G/Z2f1fpWxzX78V0U4jjXBorBQ=="],
|
||||||
|
|
||||||
|
"@codemirror/search": ["@codemirror/search@6.6.0", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.37.0", "crelt": "^1.0.5" } }, "sha512-koFuNXcDvyyotWcgOnZGmY7LZqEOXZaaxD/j6n18TCLx2/9HieZJ5H6hs1g8FiRxBD0DNfs0nXn17g872RmYdw=="],
|
||||||
|
|
||||||
|
"@codemirror/state": ["@codemirror/state@6.5.4", "", { "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } }, "sha512-8y7xqG/hpB53l25CIoit9/ngxdfoG+fx+V3SHBrinnhOtLvKHRyAJJuHzkWrR4YXXLX8eXBsejgAAxHUOdW1yw=="],
|
||||||
|
|
||||||
|
"@codemirror/text": ["@codemirror/text@0.19.6", "", {}, "sha512-T9jnREMIygx+TPC1bOuepz18maGq/92q2a+n4qTqObKwvNMg+8cMTslb8yxeEDEq7S3kpgGWxgO1UWbQRij0dA=="],
|
||||||
|
|
||||||
|
"@codemirror/view": ["@codemirror/view@6.39.11", "", { "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-bWdeR8gWM87l4DB/kYSF9A+dVackzDb/V56Tq7QVrQ7rn86W0rgZFtlL3g3pem6AeGcb9NQNoy3ao4WpW4h5tQ=="],
|
||||||
|
|
||||||
"@develar/schema-utils": ["@develar/schema-utils@2.6.5", "", { "dependencies": { "ajv": "^6.12.0", "ajv-keywords": "^3.4.1" } }, "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig=="],
|
"@develar/schema-utils": ["@develar/schema-utils@2.6.5", "", { "dependencies": { "ajv": "^6.12.0", "ajv-keywords": "^3.4.1" } }, "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig=="],
|
||||||
|
|
||||||
"@dnd-kit/accessibility": ["@dnd-kit/accessibility@3.1.1", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw=="],
|
"@dnd-kit/accessibility": ["@dnd-kit/accessibility@3.1.1", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw=="],
|
||||||
@@ -519,10 +629,46 @@
|
|||||||
|
|
||||||
"@kwsites/promise-deferred": ["@kwsites/promise-deferred@1.1.1", "", {}, "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw=="],
|
"@kwsites/promise-deferred": ["@kwsites/promise-deferred@1.1.1", "", {}, "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw=="],
|
||||||
|
|
||||||
|
"@lezer/common": ["@lezer/common@1.5.0", "", {}, "sha512-PNGcolp9hr4PJdXR4ix7XtixDrClScvtSCYW3rQG106oVMOOI+jFb+0+J3mbeL/53g1Zd6s0kJzaw6Ri68GmAA=="],
|
||||||
|
|
||||||
|
"@lezer/cpp": ["@lezer/cpp@1.1.5", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-DIhSXmYtJKLehrjzDFN+2cPt547ySQ41nA8yqcDf/GxMc+YM736xqltFkvADL2M0VebU5I+3+4ks2Vv+Kyq3Aw=="],
|
||||||
|
|
||||||
|
"@lezer/css": ["@lezer/css@1.3.0", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.3.0" } }, "sha512-pBL7hup88KbI7hXnZV3PQsn43DHy6TWyzuyk2AO9UyoXcDltvIdqWKE1dLL/45JVZ+YZkHe1WVHqO6wugZZWcw=="],
|
||||||
|
|
||||||
|
"@lezer/go": ["@lezer/go@1.0.1", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.3.0" } }, "sha512-xToRsYxwsgJNHTgNdStpcvmbVuKxTapV0dM0wey1geMMRc9aggoVyKgzYp41D2/vVOx+Ii4hmE206kvxIXBVXQ=="],
|
||||||
|
|
||||||
|
"@lezer/highlight": ["@lezer/highlight@1.2.3", "", { "dependencies": { "@lezer/common": "^1.3.0" } }, "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g=="],
|
||||||
|
|
||||||
|
"@lezer/html": ["@lezer/html@1.3.13", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg=="],
|
||||||
|
|
||||||
|
"@lezer/java": ["@lezer/java@1.1.3", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-yHquUfujwg6Yu4Fd1GNHCvidIvJwi/1Xu2DaKl/pfWIA2c1oXkVvawH3NyXhCaFx4OdlYBVX5wvz2f7Aoa/4Xw=="],
|
||||||
|
|
||||||
|
"@lezer/javascript": ["@lezer/javascript@1.5.4", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.1.3", "@lezer/lr": "^1.3.0" } }, "sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA=="],
|
||||||
|
|
||||||
|
"@lezer/json": ["@lezer/json@1.0.3", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ=="],
|
||||||
|
|
||||||
|
"@lezer/lr": ["@lezer/lr@1.4.7", "", { "dependencies": { "@lezer/common": "^1.0.0" } }, "sha512-wNIFWdSUfX9Jc6ePMzxSPVgTVB4EOfDIwLQLWASyiUdHKaMsiilj9bYiGkGQCKVodd0x6bgQCV207PILGFCF9Q=="],
|
||||||
|
|
||||||
|
"@lezer/markdown": ["@lezer/markdown@1.6.3", "", { "dependencies": { "@lezer/common": "^1.5.0", "@lezer/highlight": "^1.0.0" } }, "sha512-jpGm5Ps+XErS+xA4urw7ogEGkeZOahVQF21Z6oECF0sj+2liwZopd2+I8uH5I/vZsRuuze3OxBREIANLf6KKUw=="],
|
||||||
|
|
||||||
|
"@lezer/php": ["@lezer/php@1.0.5", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.1.0" } }, "sha512-W7asp9DhM6q0W6DYNwIkLSKOvxlXRrif+UXBMxzsJUuqmhE7oVU+gS3THO4S/Puh7Xzgm858UNaFi6dxTP8dJA=="],
|
||||||
|
|
||||||
|
"@lezer/python": ["@lezer/python@1.1.18", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-31FiUrU7z9+d/ElGQLJFXl+dKOdx0jALlP3KEOsGTex8mvj+SoE1FgItcHWK/axkxCHGUSpqIHt6JAWfWu9Rhg=="],
|
||||||
|
|
||||||
|
"@lezer/rust": ["@lezer/rust@1.0.2", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-Lz5sIPBdF2FUXcWeCu1//ojFAZqzTQNRga0aYv6dYXqJqPfMdCAI0NzajWUd4Xijj1IKJLtjoXRPMvTKWBcqKg=="],
|
||||||
|
|
||||||
|
"@lezer/sass": ["@lezer/sass@1.1.0", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-3mMGdCTUZ/84ArHOuXWQr37pnf7f+Nw9ycPUeKX+wu19b7pSMcZGLbaXwvD2APMBDOGxPmpK/O6S1v1EvLoqgQ=="],
|
||||||
|
|
||||||
|
"@lezer/xml": ["@lezer/xml@1.0.6", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-CdDwirL0OEaStFue/66ZmFSeppuL6Dwjlk8qk153mSQwiSH/Dlri4GNymrNWnUmPl2Um7QfV1FO9KFUyX3Twww=="],
|
||||||
|
|
||||||
|
"@lezer/yaml": ["@lezer/yaml@1.0.3", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.4.0" } }, "sha512-GuBLekbw9jDBDhGur82nuwkxKQ+a3W5H0GfaAthDXcAu+XdpS43VlnxA9E9hllkpSP5ellRDKjLLj7Lu9Wr6xA=="],
|
||||||
|
|
||||||
"@malept/cross-spawn-promise": ["@malept/cross-spawn-promise@1.1.1", "", { "dependencies": { "cross-spawn": "^7.0.1" } }, "sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ=="],
|
"@malept/cross-spawn-promise": ["@malept/cross-spawn-promise@1.1.1", "", { "dependencies": { "cross-spawn": "^7.0.1" } }, "sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ=="],
|
||||||
|
|
||||||
"@malept/flatpak-bundler": ["@malept/flatpak-bundler@0.4.0", "", { "dependencies": { "debug": "^4.1.1", "fs-extra": "^9.0.0", "lodash": "^4.17.15", "tmp-promise": "^3.0.2" } }, "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q=="],
|
"@malept/flatpak-bundler": ["@malept/flatpak-bundler@0.4.0", "", { "dependencies": { "debug": "^4.1.1", "fs-extra": "^9.0.0", "lodash": "^4.17.15", "tmp-promise": "^3.0.2" } }, "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q=="],
|
||||||
|
|
||||||
|
"@marijn/find-cluster-break": ["@marijn/find-cluster-break@1.0.2", "", {}, "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g=="],
|
||||||
|
|
||||||
"@mermaid-js/parser": ["@mermaid-js/parser@0.6.3", "", { "dependencies": { "langium": "3.3.1" } }, "sha512-lnjOhe7zyHjc+If7yT4zoedx2vo4sHaTmtkl1+or8BRTnCtDmcTpAjpzDSfCZrshM5bCoz0GyidzadJAH1xobA=="],
|
"@mermaid-js/parser": ["@mermaid-js/parser@0.6.3", "", { "dependencies": { "langium": "3.3.1" } }, "sha512-lnjOhe7zyHjc+If7yT4zoedx2vo4sHaTmtkl1+or8BRTnCtDmcTpAjpzDSfCZrshM5bCoz0GyidzadJAH1xobA=="],
|
||||||
|
|
||||||
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
|
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
|
||||||
@@ -1207,6 +1353,8 @@
|
|||||||
|
|
||||||
"crc32-stream": ["crc32-stream@4.0.3", "", { "dependencies": { "crc-32": "^1.2.0", "readable-stream": "^3.4.0" } }, "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw=="],
|
"crc32-stream": ["crc32-stream@4.0.3", "", { "dependencies": { "crc-32": "^1.2.0", "readable-stream": "^3.4.0" } }, "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw=="],
|
||||||
|
|
||||||
|
"crelt": ["crelt@1.0.6", "", {}, "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="],
|
||||||
|
|
||||||
"cross-env": ["cross-env@7.0.3", "", { "dependencies": { "cross-spawn": "^7.0.1" }, "bin": { "cross-env": "src/bin/cross-env.js", "cross-env-shell": "src/bin/cross-env-shell.js" } }, "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw=="],
|
"cross-env": ["cross-env@7.0.3", "", { "dependencies": { "cross-spawn": "^7.0.1" }, "bin": { "cross-env": "src/bin/cross-env.js", "cross-env-shell": "src/bin/cross-env-shell.js" } }, "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw=="],
|
||||||
|
|
||||||
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||||
@@ -2357,6 +2505,8 @@
|
|||||||
|
|
||||||
"stubborn-utils": ["stubborn-utils@1.0.2", "", {}, "sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg=="],
|
"stubborn-utils": ["stubborn-utils@1.0.2", "", {}, "sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg=="],
|
||||||
|
|
||||||
|
"style-mod": ["style-mod@4.1.3", "", {}, "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ=="],
|
||||||
|
|
||||||
"style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="],
|
"style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="],
|
||||||
|
|
||||||
"style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="],
|
"style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="],
|
||||||
@@ -2525,6 +2675,8 @@
|
|||||||
|
|
||||||
"vscode-uri": ["vscode-uri@3.0.8", "", {}, "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw=="],
|
"vscode-uri": ["vscode-uri@3.0.8", "", {}, "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw=="],
|
||||||
|
|
||||||
|
"w3c-keyname": ["w3c-keyname@2.2.8", "", {}, "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="],
|
||||||
|
|
||||||
"web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="],
|
"web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="],
|
||||||
|
|
||||||
"whatwg-encoding": ["whatwg-encoding@3.1.1", "", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ=="],
|
"whatwg-encoding": ["whatwg-encoding@3.1.1", "", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ=="],
|
||||||
@@ -2587,6 +2739,20 @@
|
|||||||
|
|
||||||
"@chevrotain/gast/lodash-es": ["lodash-es@4.17.21", "", {}, "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="],
|
"@chevrotain/gast/lodash-es": ["lodash-es@4.17.21", "", {}, "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="],
|
||||||
|
|
||||||
|
"@codemirror/highlight/@codemirror/language": ["@codemirror/language@0.19.10", "", { "dependencies": { "@codemirror/state": "^0.19.0", "@codemirror/text": "^0.19.0", "@codemirror/view": "^0.19.0", "@lezer/common": "^0.15.5", "@lezer/lr": "^0.15.0" } }, "sha512-yA0DZ3RYn2CqAAGW62VrU8c4YxscMQn45y/I9sjBlqB1e2OTQLg4CCkMBuMSLXk4xaqjlsgazeOQWaJQOKfV8Q=="],
|
||||||
|
|
||||||
|
"@codemirror/highlight/@codemirror/state": ["@codemirror/state@0.19.9", "", { "dependencies": { "@codemirror/text": "^0.19.0" } }, "sha512-psOzDolKTZkx4CgUqhBQ8T8gBc0xN5z4gzed109aF6x7D7umpDRoimacI/O6d9UGuyl4eYuDCZmDFr2Rq7aGOw=="],
|
||||||
|
|
||||||
|
"@codemirror/highlight/@codemirror/view": ["@codemirror/view@0.19.48", "", { "dependencies": { "@codemirror/rangeset": "^0.19.5", "@codemirror/state": "^0.19.3", "@codemirror/text": "^0.19.0", "style-mod": "^4.0.0", "w3c-keyname": "^2.2.4" } }, "sha512-0eg7D2Nz4S8/caetCTz61rK0tkHI17V/d15Jy0kLOT8dTLGGNJUponDnW28h2B6bERmPlVHKh8MJIr5OCp1nGw=="],
|
||||||
|
|
||||||
|
"@codemirror/highlight/@lezer/common": ["@lezer/common@0.15.12", "", {}, "sha512-edfwCxNLnzq5pBA/yaIhwJ3U3Kz8VAUOTRg0hhxaizaI1N+qxV7EXDv/kLCkLeq2RzSFvxexlaj5Mzfn2kY0Ig=="],
|
||||||
|
|
||||||
|
"@codemirror/history/@codemirror/state": ["@codemirror/state@0.19.9", "", { "dependencies": { "@codemirror/text": "^0.19.0" } }, "sha512-psOzDolKTZkx4CgUqhBQ8T8gBc0xN5z4gzed109aF6x7D7umpDRoimacI/O6d9UGuyl4eYuDCZmDFr2Rq7aGOw=="],
|
||||||
|
|
||||||
|
"@codemirror/history/@codemirror/view": ["@codemirror/view@0.19.48", "", { "dependencies": { "@codemirror/rangeset": "^0.19.5", "@codemirror/state": "^0.19.3", "@codemirror/text": "^0.19.0", "style-mod": "^4.0.0", "w3c-keyname": "^2.2.4" } }, "sha512-0eg7D2Nz4S8/caetCTz61rK0tkHI17V/d15Jy0kLOT8dTLGGNJUponDnW28h2B6bERmPlVHKh8MJIr5OCp1nGw=="],
|
||||||
|
|
||||||
|
"@codemirror/rangeset/@codemirror/state": ["@codemirror/state@0.19.9", "", { "dependencies": { "@codemirror/text": "^0.19.0" } }, "sha512-psOzDolKTZkx4CgUqhBQ8T8gBc0xN5z4gzed109aF6x7D7umpDRoimacI/O6d9UGuyl4eYuDCZmDFr2Rq7aGOw=="],
|
||||||
|
|
||||||
"@electron/asar/commander": ["commander@5.1.0", "", {}, "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg=="],
|
"@electron/asar/commander": ["commander@5.1.0", "", {}, "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg=="],
|
||||||
|
|
||||||
"@electron/asar/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
|
"@electron/asar/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
|
||||||
@@ -2831,6 +2997,8 @@
|
|||||||
|
|
||||||
"@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
"@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
||||||
|
|
||||||
|
"@codemirror/highlight/@codemirror/language/@lezer/lr": ["@lezer/lr@0.15.8", "", { "dependencies": { "@lezer/common": "^0.15.0" } }, "sha512-bM6oE6VQZ6hIFxDNKk8bKPa14hqFrV07J/vHGOeiAbJReIaQXmkVb6xQu4MR+JBTLa5arGRyAAjJe1qaQt3Uvg=="],
|
||||||
|
|
||||||
"@electron/get/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="],
|
"@electron/get/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="],
|
||||||
|
|
||||||
"@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="],
|
"@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="],
|
||||||
|
|||||||
+22
-2
@@ -58,12 +58,32 @@
|
|||||||
"release:test:arm": "./scripts/test-release-build.sh aarch64"
|
"release:test:arm": "./scripts/test-release-build.sh aarch64"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@codemirror/autocomplete": "^6.20.0",
|
||||||
|
"@codemirror/commands": "^6.10.1",
|
||||||
|
"@codemirror/highlight": "^0.19.8",
|
||||||
|
"@codemirror/history": "^0.19.2",
|
||||||
|
"@codemirror/lang-css": "^6.3.1",
|
||||||
|
"@codemirror/lang-html": "^6.4.11",
|
||||||
|
"@codemirror/lang-javascript": "^6.2.4",
|
||||||
|
"@codemirror/lang-json": "^6.0.2",
|
||||||
|
"@codemirror/lang-markdown": "^6.5.0",
|
||||||
|
"@codemirror/lang-python": "^6.2.1",
|
||||||
|
"@codemirror/lang-rust": "^6.0.2",
|
||||||
|
"@codemirror/lang-sql": "^6.10.0",
|
||||||
|
"@codemirror/lang-xml": "^6.1.0",
|
||||||
|
"@codemirror/lang-yaml": "^6.1.2",
|
||||||
|
"@codemirror/language": "^6.12.1",
|
||||||
|
"@codemirror/lint": "^6.9.2",
|
||||||
|
"@codemirror/search": "^6.6.0",
|
||||||
|
"@codemirror/state": "^6.5.4",
|
||||||
|
"@codemirror/view": "^6.39.11",
|
||||||
"@fontsource/ibm-plex-mono": "^5.2.7",
|
"@fontsource/ibm-plex-mono": "^5.2.7",
|
||||||
"@fontsource/ibm-plex-sans": "^5.1.1",
|
"@fontsource/ibm-plex-sans": "^5.1.1",
|
||||||
"@heroui/scroll-shadow": "^2.3.18",
|
"@heroui/scroll-shadow": "^2.3.18",
|
||||||
"@heroui/system": "^2.4.23",
|
"@heroui/system": "^2.4.23",
|
||||||
"@heroui/theme": "^2.4.23",
|
"@heroui/theme": "^2.4.23",
|
||||||
"@ibm/plex": "^6.4.1",
|
"@ibm/plex": "^6.4.1",
|
||||||
|
"@lezer/highlight": "^1.2.3",
|
||||||
"@opencode-ai/sdk": "^1.1.19",
|
"@opencode-ai/sdk": "^1.1.19",
|
||||||
"@radix-ui/react-collapsible": "^1.1.12",
|
"@radix-ui/react-collapsible": "^1.1.12",
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
@@ -76,16 +96,16 @@
|
|||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
"@remixicon/react": "^4.7.0",
|
"@remixicon/react": "^4.7.0",
|
||||||
"@types/react-syntax-highlighter": "^15.5.13",
|
"@types/react-syntax-highlighter": "^15.5.13",
|
||||||
"ghostty-web": "0.3.0",
|
"bun-pty": "^0.4.5",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"electron-context-menu": "^4.1.1",
|
"electron-context-menu": "^4.1.1",
|
||||||
"electron-store": "^11.0.2",
|
"electron-store": "^11.0.2",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
|
"ghostty-web": "0.3.0",
|
||||||
"http-proxy-middleware": "^3.0.5",
|
"http-proxy-middleware": "^3.0.5",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"bun-pty": "^0.4.5",
|
|
||||||
"node-pty": "^1.1.0",
|
"node-pty": "^1.1.0",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
|
|||||||
Generated
+1
-1
@@ -2977,7 +2977,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openchamber-desktop"
|
name = "openchamber-desktop"
|
||||||
version = "1.5.1"
|
version = "1.5.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"axum",
|
"axum",
|
||||||
|
|||||||
@@ -11,12 +11,34 @@
|
|||||||
"lint": "eslint \"./src/**/*.{ts,tsx}\" --config ../../eslint.config.js"
|
"lint": "eslint \"./src/**/*.{ts,tsx}\" --config ../../eslint.config.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@codemirror/autocomplete": "^6.20.0",
|
||||||
|
"@codemirror/commands": "^6.10.1",
|
||||||
|
"@codemirror/highlight": "^0.19.8",
|
||||||
|
"@codemirror/history": "^0.19.2",
|
||||||
|
"@codemirror/lang-css": "^6.3.1",
|
||||||
|
"@codemirror/lang-html": "^6.4.11",
|
||||||
|
"@codemirror/lang-javascript": "^6.2.4",
|
||||||
|
"@codemirror/lang-json": "^6.0.2",
|
||||||
|
"@codemirror/lang-markdown": "^6.5.0",
|
||||||
|
"@codemirror/lang-python": "^6.2.1",
|
||||||
|
"@codemirror/lang-rust": "^6.0.2",
|
||||||
|
"@codemirror/lang-sql": "^6.10.0",
|
||||||
|
"@codemirror/lang-xml": "^6.1.0",
|
||||||
|
"@codemirror/lang-yaml": "^6.1.2",
|
||||||
|
"@codemirror/language": "^6.12.1",
|
||||||
|
"@codemirror/language-data": "^6.5.2",
|
||||||
|
"@codemirror/legacy-modes": "^6.5.2",
|
||||||
|
"@codemirror/lint": "^6.9.2",
|
||||||
|
"@codemirror/search": "^6.6.0",
|
||||||
|
"@codemirror/state": "^6.5.4",
|
||||||
|
"@codemirror/view": "^6.39.11",
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@fontsource/ibm-plex-mono": "^5.2.7",
|
"@fontsource/ibm-plex-mono": "^5.2.7",
|
||||||
"@fontsource/ibm-plex-sans": "^5.1.1",
|
"@fontsource/ibm-plex-sans": "^5.1.1",
|
||||||
"@ibm/plex": "^6.4.1",
|
"@ibm/plex": "^6.4.1",
|
||||||
|
"@lezer/highlight": "^1.2.3",
|
||||||
"@opencode-ai/sdk": "^1.1.19",
|
"@opencode-ai/sdk": "^1.1.19",
|
||||||
"@pierre/diffs": "^1.0.5",
|
"@pierre/diffs": "^1.0.5",
|
||||||
"@radix-ui/react-collapsible": "^1.1.12",
|
"@radix-ui/react-collapsible": "^1.1.12",
|
||||||
|
|||||||
@@ -0,0 +1,152 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import type { Extension } from '@codemirror/state';
|
||||||
|
import { Compartment, EditorState, RangeSetBuilder } from '@codemirror/state';
|
||||||
|
import { Decoration, EditorView, ViewPlugin, gutters, keymap, lineNumbers } from '@codemirror/view';
|
||||||
|
import { defaultKeymap, indentWithTab, history, historyKeymap } from '@codemirror/commands';
|
||||||
|
import { indentUnit } from '@codemirror/language';
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
type CodeMirrorEditorProps = {
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
extensions?: Extension[];
|
||||||
|
className?: string;
|
||||||
|
readOnly?: boolean;
|
||||||
|
lineNumbersConfig?: Parameters<typeof lineNumbers>[0];
|
||||||
|
highlightLines?: { start: number; end: number };
|
||||||
|
};
|
||||||
|
|
||||||
|
const lineNumbersCompartment = new Compartment();
|
||||||
|
const editableCompartment = new Compartment();
|
||||||
|
const externalExtensionsCompartment = new Compartment();
|
||||||
|
const highlightLinesCompartment = new Compartment();
|
||||||
|
|
||||||
|
const createHighlightLinesExtension = (range?: { start: number; end: number }): Extension => {
|
||||||
|
if (!range) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = Math.max(1, range.start);
|
||||||
|
const end = Math.max(start, range.end);
|
||||||
|
|
||||||
|
return ViewPlugin.fromClass(class {
|
||||||
|
decorations;
|
||||||
|
|
||||||
|
constructor(view: EditorView) {
|
||||||
|
this.decorations = this.build(view);
|
||||||
|
}
|
||||||
|
|
||||||
|
update(update: import('@codemirror/view').ViewUpdate) {
|
||||||
|
if (update.docChanged || update.viewportChanged) {
|
||||||
|
this.decorations = this.build(update.view);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
build(view: EditorView) {
|
||||||
|
const builder = new RangeSetBuilder<Decoration>();
|
||||||
|
for (let lineNo = start; lineNo <= end && lineNo <= view.state.doc.lines; lineNo += 1) {
|
||||||
|
const line = view.state.doc.line(lineNo);
|
||||||
|
builder.add(line.from, line.from, Decoration.line({ class: 'oc-cm-selected-line' }));
|
||||||
|
}
|
||||||
|
return builder.finish();
|
||||||
|
}
|
||||||
|
}, { decorations: (v) => v.decorations });
|
||||||
|
};
|
||||||
|
|
||||||
|
export function CodeMirrorEditor({ value, onChange, extensions, className, readOnly, lineNumbersConfig, highlightLines }: CodeMirrorEditorProps) {
|
||||||
|
const hostRef = React.useRef<HTMLDivElement | null>(null);
|
||||||
|
const viewRef = React.useRef<EditorView | null>(null);
|
||||||
|
const valueRef = React.useRef(value);
|
||||||
|
const onChangeRef = React.useRef(onChange);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
valueRef.current = value;
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
onChangeRef.current = onChange;
|
||||||
|
}, [onChange]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!hostRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = EditorState.create({
|
||||||
|
doc: valueRef.current,
|
||||||
|
extensions: [
|
||||||
|
gutters({ fixed: true }),
|
||||||
|
lineNumbersCompartment.of(lineNumbers(lineNumbersConfig)),
|
||||||
|
history(),
|
||||||
|
indentUnit.of(' '),
|
||||||
|
keymap.of([indentWithTab, ...defaultKeymap, ...historyKeymap]),
|
||||||
|
EditorView.updateListener.of((update) => {
|
||||||
|
if (!update.docChanged) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const next = update.state.doc.toString();
|
||||||
|
valueRef.current = next;
|
||||||
|
onChangeRef.current(next);
|
||||||
|
}),
|
||||||
|
editableCompartment.of(EditorView.editable.of(!readOnly)),
|
||||||
|
externalExtensionsCompartment.of(extensions ?? []),
|
||||||
|
highlightLinesCompartment.of(createHighlightLinesExtension(highlightLines)),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
viewRef.current = new EditorView({
|
||||||
|
state,
|
||||||
|
parent: hostRef.current,
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
viewRef.current?.destroy();
|
||||||
|
viewRef.current = null;
|
||||||
|
};
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const view = viewRef.current;
|
||||||
|
if (!view) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
view.dispatch({
|
||||||
|
effects: [
|
||||||
|
lineNumbersCompartment.reconfigure(lineNumbers(lineNumbersConfig)),
|
||||||
|
editableCompartment.reconfigure(EditorView.editable.of(!readOnly)),
|
||||||
|
externalExtensionsCompartment.reconfigure(extensions ?? []),
|
||||||
|
highlightLinesCompartment.reconfigure(createHighlightLinesExtension(highlightLines)),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}, [extensions, highlightLines, lineNumbersConfig, readOnly]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const view = viewRef.current;
|
||||||
|
if (!view) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const current = view.state.doc.toString();
|
||||||
|
if (current !== value) {
|
||||||
|
view.dispatch({
|
||||||
|
changes: { from: 0, to: current.length, insert: value },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={hostRef}
|
||||||
|
className={cn(
|
||||||
|
'h-full w-full',
|
||||||
|
'[&_.cm-editor]:h-full [&_.cm-editor]:w-full',
|
||||||
|
'[&_.cm-scroller]:font-mono [&_.cm-scroller]:text-[var(--text-code)] [&_.cm-scroller]:leading-6',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
|
||||||
import createElement from 'react-syntax-highlighter/create-element';
|
|
||||||
import {
|
import {
|
||||||
RiArrowLeftSLine,
|
RiArrowLeftSLine,
|
||||||
RiClipboardLine,
|
RiClipboardLine,
|
||||||
@@ -8,11 +7,13 @@ import {
|
|||||||
RiCodeLine,
|
RiCodeLine,
|
||||||
RiFileImageLine,
|
RiFileImageLine,
|
||||||
RiFileTextLine,
|
RiFileTextLine,
|
||||||
|
RiFileCopy2Line,
|
||||||
RiFolder3Fill,
|
RiFolder3Fill,
|
||||||
RiFolderOpenFill,
|
RiFolderOpenFill,
|
||||||
RiLoader4Line,
|
RiLoader4Line,
|
||||||
RiRefreshLine,
|
RiRefreshLine,
|
||||||
RiSearchLine,
|
RiSearchLine,
|
||||||
|
RiSave3Line,
|
||||||
RiSendPlane2Line,
|
RiSendPlane2Line,
|
||||||
RiTextWrap,
|
RiTextWrap,
|
||||||
} from '@remixicon/react';
|
} from '@remixicon/react';
|
||||||
@@ -22,14 +23,25 @@ import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
|
|||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
import { CodeMirrorEditor } from '@/components/ui/CodeMirrorEditor';
|
||||||
|
import { languageByExtension } from '@/lib/codemirror/languageByExtension';
|
||||||
|
import { createFlexokiCodeMirrorTheme } from '@/lib/codemirror/flexokiTheme';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
import { useDebouncedValue } from '@/hooks/useDebouncedValue';
|
import { useDebouncedValue } from '@/hooks/useDebouncedValue';
|
||||||
import { useFileSearchStore } from '@/stores/useFileSearchStore';
|
import { useFileSearchStore } from '@/stores/useFileSearchStore';
|
||||||
import { useDeviceInfo } from '@/lib/device';
|
import { useDeviceInfo } from '@/lib/device';
|
||||||
import { cn, getModifierLabel } from '@/lib/utils';
|
import { cn, getModifierLabel, hasModifier } from '@/lib/utils';
|
||||||
import { getLanguageFromExtension, getImageMimeType, isImageFile } from '@/lib/toolHelpers';
|
import { getLanguageFromExtension, getImageMimeType, isImageFile } from '@/lib/toolHelpers';
|
||||||
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
|
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
|
||||||
|
import { EditorView } from '@codemirror/view';
|
||||||
import { useThemeSystem } from '@/contexts/useThemeSystem';
|
import { useThemeSystem } from '@/contexts/useThemeSystem';
|
||||||
import { generateSyntaxTheme } from '@/lib/theme/syntaxThemeGenerator';
|
|
||||||
import { useSessionStore } from '@/stores/useSessionStore';
|
import { useSessionStore } from '@/stores/useSessionStore';
|
||||||
import { useConfigStore } from '@/stores/useConfigStore';
|
import { useConfigStore } from '@/stores/useConfigStore';
|
||||||
import { useContextStore } from '@/stores/contextStore';
|
import { useContextStore } from '@/stores/contextStore';
|
||||||
@@ -81,7 +93,6 @@ const useEffectiveDirectory = () => {
|
|||||||
return worktreeMetadata?.path ?? sessionDirectory ?? fallbackDirectory ?? '';
|
return worktreeMetadata?.path ?? sessionDirectory ?? fallbackDirectory ?? '';
|
||||||
};
|
};
|
||||||
|
|
||||||
const MAX_HIGHLIGHT_CHARS = 200_000;
|
|
||||||
const MAX_VIEW_CHARS = 200_000;
|
const MAX_VIEW_CHARS = 200_000;
|
||||||
|
|
||||||
const CODE_EXTENSIONS = new Set([
|
const CODE_EXTENSIONS = new Set([
|
||||||
@@ -221,8 +232,7 @@ const getFileIcon = (extension?: string): React.ReactNode => {
|
|||||||
export const FilesView: React.FC = () => {
|
export const FilesView: React.FC = () => {
|
||||||
const { files, runtime } = useRuntimeAPIs();
|
const { files, runtime } = useRuntimeAPIs();
|
||||||
const { currentTheme } = useThemeSystem();
|
const { currentTheme } = useThemeSystem();
|
||||||
const syntaxTheme = React.useMemo(() => generateSyntaxTheme(currentTheme), [currentTheme]);
|
const { isMobile, screenWidth } = useDeviceInfo();
|
||||||
const { isMobile } = useDeviceInfo();
|
|
||||||
|
|
||||||
const currentDirectory = useEffectiveDirectory();
|
const currentDirectory = useEffectiveDirectory();
|
||||||
const root = normalizePath(currentDirectory);
|
const root = normalizePath(currentDirectory);
|
||||||
@@ -249,6 +259,14 @@ export const FilesView: React.FC = () => {
|
|||||||
const [fileError, setFileError] = React.useState<string | null>(null);
|
const [fileError, setFileError] = React.useState<string | null>(null);
|
||||||
const [desktopImageSrc, setDesktopImageSrc] = React.useState<string>('');
|
const [desktopImageSrc, setDesktopImageSrc] = React.useState<string>('');
|
||||||
|
|
||||||
|
const [draftContent, setDraftContent] = React.useState('');
|
||||||
|
const [isSaving, setIsSaving] = React.useState(false);
|
||||||
|
|
||||||
|
const [confirmDiscardOpen, setConfirmDiscardOpen] = React.useState(false);
|
||||||
|
const pendingSelectFileRef = React.useRef<FileNode | null>(null);
|
||||||
|
const pendingTabRef = React.useRef<import('@/stores/useUIStore').MainTab | null>(null);
|
||||||
|
const skipDirtyOnceRef = React.useRef(false);
|
||||||
|
|
||||||
// Line selection state for commenting
|
// Line selection state for commenting
|
||||||
const [lineSelection, setLineSelection] = React.useState<SelectedLineRange | null>(null);
|
const [lineSelection, setLineSelection] = React.useState<SelectedLineRange | null>(null);
|
||||||
const [commentText, setCommentText] = React.useState('');
|
const [commentText, setCommentText] = React.useState('');
|
||||||
@@ -263,62 +281,15 @@ export const FilesView: React.FC = () => {
|
|||||||
const getAgentModelForSession = useContextStore((state) => state.getAgentModelForSession);
|
const getAgentModelForSession = useContextStore((state) => state.getAgentModelForSession);
|
||||||
const getAgentModelVariantForSession = useContextStore((state) => state.getAgentModelVariantForSession);
|
const getAgentModelVariantForSession = useContextStore((state) => state.getAgentModelVariantForSession);
|
||||||
const setActiveMainTab = useUIStore((state) => state.setActiveMainTab);
|
const setActiveMainTab = useUIStore((state) => state.setActiveMainTab);
|
||||||
|
const setMainTabGuard = useUIStore((state) => state.setMainTabGuard);
|
||||||
const { inputBarOffset, isKeyboardOpen } = useUIStore();
|
const { inputBarOffset, isKeyboardOpen } = useUIStore();
|
||||||
|
|
||||||
// Line selection handlers
|
|
||||||
const handleLineClick = React.useCallback((lineNumber: number, shiftKey: boolean) => {
|
|
||||||
if (shiftKey && lineSelection) {
|
|
||||||
// Extend selection with shift+click
|
|
||||||
const newStart = Math.min(lineSelection.start, lineNumber);
|
|
||||||
const newEnd = Math.max(lineSelection.end, lineNumber);
|
|
||||||
setLineSelection({ start: newStart, end: newEnd });
|
|
||||||
} else {
|
|
||||||
// Start new selection
|
|
||||||
setLineSelection({ start: lineNumber, end: lineNumber });
|
|
||||||
}
|
|
||||||
}, [lineSelection]);
|
|
||||||
|
|
||||||
const handleLineMouseDown = React.useCallback((lineNumber: number, e: React.MouseEvent) => {
|
|
||||||
e.preventDefault(); // Prevent text selection while selecting lines
|
|
||||||
if (e.shiftKey && lineSelection) {
|
|
||||||
// Shift+click extends selection
|
|
||||||
handleLineClick(lineNumber, true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
isSelectingRef.current = true;
|
|
||||||
selectionStartRef.current = lineNumber;
|
|
||||||
setLineSelection({ start: lineNumber, end: lineNumber });
|
|
||||||
}, [handleLineClick, lineSelection]);
|
|
||||||
|
|
||||||
const handleLineMouseEnter = React.useCallback((lineNumber: number) => {
|
|
||||||
if (!isSelectingRef.current || selectionStartRef.current === null) return;
|
|
||||||
const start = Math.min(selectionStartRef.current, lineNumber);
|
|
||||||
const end = Math.max(selectionStartRef.current, lineNumber);
|
|
||||||
setLineSelection({ start, end });
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleLineMouseUp = React.useCallback(() => {
|
|
||||||
isSelectingRef.current = false;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Mobile: tap to extend selection
|
|
||||||
const handleLineTap = React.useCallback((lineNumber: number) => {
|
|
||||||
if (lineSelection) {
|
|
||||||
// Extend selection to tapped line
|
|
||||||
const newStart = Math.min(lineSelection.start, lineSelection.end, lineNumber);
|
|
||||||
const newEnd = Math.max(lineSelection.start, lineSelection.end, lineNumber);
|
|
||||||
if (lineNumber < lineSelection.start || lineNumber > lineSelection.end) {
|
|
||||||
setLineSelection({ start: newStart, end: newEnd });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setLineSelection({ start: lineNumber, end: lineNumber });
|
|
||||||
}, [lineSelection]);
|
|
||||||
|
|
||||||
// Global mouseup to end drag selection
|
// Global mouseup to end drag selection
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const handleGlobalMouseUp = () => {
|
const handleGlobalMouseUp = () => {
|
||||||
isSelectingRef.current = false;
|
isSelectingRef.current = false;
|
||||||
|
selectionStartRef.current = null;
|
||||||
};
|
};
|
||||||
document.addEventListener('mouseup', handleGlobalMouseUp);
|
document.addEventListener('mouseup', handleGlobalMouseUp);
|
||||||
return () => document.removeEventListener('mouseup', handleGlobalMouseUp);
|
return () => document.removeEventListener('mouseup', handleGlobalMouseUp);
|
||||||
@@ -328,7 +299,10 @@ export const FilesView: React.FC = () => {
|
|||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
setLineSelection(null);
|
setLineSelection(null);
|
||||||
setCommentText('');
|
setCommentText('');
|
||||||
}, [selectedFile?.path]);
|
setMainTabGuard(null);
|
||||||
|
setDraftContent('');
|
||||||
|
setIsSaving(false);
|
||||||
|
}, [selectedFile?.path, setMainTabGuard]);
|
||||||
|
|
||||||
// Click outside to dismiss selection
|
// Click outside to dismiss selection
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@@ -341,8 +315,8 @@ export const FilesView: React.FC = () => {
|
|||||||
const commentUI = document.querySelector('[data-comment-ui]');
|
const commentUI = document.querySelector('[data-comment-ui]');
|
||||||
if (commentUI?.contains(target)) return;
|
if (commentUI?.contains(target)) return;
|
||||||
|
|
||||||
// Check if click is on a line number (only line numbers should not dismiss)
|
// Check if click is on CM gutter (only gutter should not dismiss)
|
||||||
if (target.closest('[data-line-number]')) return;
|
if (target.closest('.cm-gutterElement')) return;
|
||||||
|
|
||||||
// Check if click is inside toast (sonner)
|
// Check if click is inside toast (sonner)
|
||||||
if (target.closest('[data-sonner-toast]') || target.closest('[data-sonner-toaster]')) return;
|
if (target.closest('[data-sonner-toast]') || target.closest('[data-sonner-toaster]')) return;
|
||||||
@@ -652,7 +626,94 @@ export const FilesView: React.FC = () => {
|
|||||||
return response.text();
|
return response.text();
|
||||||
}, [files]);
|
}, [files]);
|
||||||
|
|
||||||
|
const displayedContent = React.useMemo(() => {
|
||||||
|
return fileContent.length > MAX_VIEW_CHARS
|
||||||
|
? `${fileContent.slice(0, MAX_VIEW_CHARS)}\n\n… truncated …`
|
||||||
|
: fileContent;
|
||||||
|
}, [fileContent]);
|
||||||
|
|
||||||
|
const isDirty = React.useMemo(() => draftContent !== displayedContent, [draftContent, displayedContent]);
|
||||||
|
|
||||||
|
const saveDraft = React.useCallback(async () => {
|
||||||
|
if (!selectedFile || !files.writeFile) {
|
||||||
|
toast.error('Saving not supported');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isDirty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsSaving(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await files.writeFile(selectedFile.path, draftContent);
|
||||||
|
if (!result?.success) {
|
||||||
|
throw new Error('Failed to write file');
|
||||||
|
}
|
||||||
|
setFileContent(draftContent);
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(error instanceof Error ? error.message : 'Save failed');
|
||||||
|
} finally {
|
||||||
|
setIsSaving(false);
|
||||||
|
}
|
||||||
|
}, [draftContent, files, isDirty, selectedFile]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!isDirty) {
|
||||||
|
setMainTabGuard(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const guard = (_nextTab: import('@/stores/useUIStore').MainTab) => {
|
||||||
|
if (skipDirtyOnceRef.current) {
|
||||||
|
skipDirtyOnceRef.current = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
setConfirmDiscardOpen(true);
|
||||||
|
pendingTabRef.current = _nextTab;
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
setMainTabGuard(guard);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
const currentGuard = useUIStore.getState().mainTabGuard;
|
||||||
|
if (currentGuard === guard) {
|
||||||
|
setMainTabGuard(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [isDirty, setMainTabGuard]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (!hasModifier(e)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.key.toLowerCase() !== 's') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
if (!isSaving) {
|
||||||
|
void saveDraft();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||||
|
}, [isSaving, saveDraft]);
|
||||||
|
|
||||||
const handleSelectFile = React.useCallback(async (node: FileNode) => {
|
const handleSelectFile = React.useCallback(async (node: FileNode) => {
|
||||||
|
if (skipDirtyOnceRef.current) {
|
||||||
|
skipDirtyOnceRef.current = false;
|
||||||
|
} else if (isDirty) {
|
||||||
|
setConfirmDiscardOpen(true);
|
||||||
|
pendingSelectFileRef.current = node;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setSelectedFile(node);
|
setSelectedFile(node);
|
||||||
setFileError(null);
|
setFileError(null);
|
||||||
setDesktopImageSrc('');
|
setDesktopImageSrc('');
|
||||||
@@ -684,13 +745,68 @@ export const FilesView: React.FC = () => {
|
|||||||
try {
|
try {
|
||||||
const content = await readFile(node.path);
|
const content = await readFile(node.path);
|
||||||
setFileContent(content);
|
setFileContent(content);
|
||||||
|
setDraftContent(content.length > MAX_VIEW_CHARS
|
||||||
|
? `${content.slice(0, MAX_VIEW_CHARS)}\n\n… truncated …`
|
||||||
|
: content);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setFileContent('');
|
setFileContent('');
|
||||||
|
setDraftContent('');
|
||||||
setFileError(error instanceof Error ? error.message : 'Failed to read file');
|
setFileError(error instanceof Error ? error.message : 'Failed to read file');
|
||||||
} finally {
|
} finally {
|
||||||
setFileLoading(false);
|
setFileLoading(false);
|
||||||
}
|
}
|
||||||
}, [isMobile, readFile, runtime.isDesktop]);
|
}, [isDirty, isMobile, readFile, runtime.isDesktop]);
|
||||||
|
|
||||||
|
const discardAndContinue = React.useCallback(() => {
|
||||||
|
const nextFile = pendingSelectFileRef.current;
|
||||||
|
const nextTab = pendingTabRef.current;
|
||||||
|
|
||||||
|
pendingSelectFileRef.current = null;
|
||||||
|
pendingTabRef.current = null;
|
||||||
|
|
||||||
|
// Allow one guarded navigation (tab/file) without re-opening dialog.
|
||||||
|
skipDirtyOnceRef.current = true;
|
||||||
|
|
||||||
|
setConfirmDiscardOpen(false);
|
||||||
|
|
||||||
|
// Discard draft by reverting back to last loaded content
|
||||||
|
setDraftContent(displayedContent);
|
||||||
|
|
||||||
|
if (nextFile) {
|
||||||
|
void handleSelectFile(nextFile);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextTab) {
|
||||||
|
setMainTabGuard(null);
|
||||||
|
useUIStore.getState().setActiveMainTab(nextTab);
|
||||||
|
}
|
||||||
|
}, [displayedContent, handleSelectFile, setMainTabGuard]);
|
||||||
|
|
||||||
|
const saveAndContinue = React.useCallback(async () => {
|
||||||
|
const nextFile = pendingSelectFileRef.current;
|
||||||
|
const nextTab = pendingTabRef.current;
|
||||||
|
|
||||||
|
pendingSelectFileRef.current = null;
|
||||||
|
pendingTabRef.current = null;
|
||||||
|
|
||||||
|
// We'll proceed after saving; suppress guard reopening.
|
||||||
|
skipDirtyOnceRef.current = true;
|
||||||
|
|
||||||
|
setConfirmDiscardOpen(false);
|
||||||
|
|
||||||
|
await saveDraft();
|
||||||
|
|
||||||
|
if (nextFile) {
|
||||||
|
await handleSelectFile(nextFile);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextTab) {
|
||||||
|
setMainTabGuard(null);
|
||||||
|
useUIStore.getState().setActiveMainTab(nextTab);
|
||||||
|
}
|
||||||
|
}, [handleSelectFile, saveDraft, setMainTabGuard]);
|
||||||
|
|
||||||
const toggleDirectory = React.useCallback(async (dirPath: string) => {
|
const toggleDirectory = React.useCallback(async (dirPath: string) => {
|
||||||
const normalized = normalizePath(dirPath);
|
const normalized = normalizePath(dirPath);
|
||||||
@@ -763,11 +879,6 @@ export const FilesView: React.FC = () => {
|
|||||||
});
|
});
|
||||||
}, [childrenByDir, expandedDirs, handleSelectFile, selectedFile?.path, toggleDirectory]);
|
}, [childrenByDir, expandedDirs, handleSelectFile, selectedFile?.path, toggleDirectory]);
|
||||||
|
|
||||||
const viewerLanguage = selectedFile?.path ? getLanguageFromExtension(selectedFile.path) || 'text' : 'text';
|
|
||||||
const contentForViewer = fileContent.length > MAX_VIEW_CHARS
|
|
||||||
? `${fileContent.slice(0, MAX_VIEW_CHARS)}\n\n… truncated …`
|
|
||||||
: fileContent;
|
|
||||||
const shouldHighlight = contentForViewer.length <= MAX_HIGHLIGHT_CHARS;
|
|
||||||
const isSelectedImage = Boolean(selectedFile?.path && isImageFile(selectedFile.path));
|
const isSelectedImage = Boolean(selectedFile?.path && isImageFile(selectedFile.path));
|
||||||
const isSelectedSvg = Boolean(selectedFile?.path && selectedFile.path.toLowerCase().endsWith('.svg'));
|
const isSelectedSvg = Boolean(selectedFile?.path && selectedFile.path.toLowerCase().endsWith('.svg'));
|
||||||
const displaySelectedPath = React.useMemo(() => {
|
const displaySelectedPath = React.useMemo(() => {
|
||||||
@@ -783,6 +894,24 @@ export const FilesView: React.FC = () => {
|
|||||||
|
|
||||||
|
|
||||||
const canCopy = Boolean(selectedFile && (!isSelectedImage || isSelectedSvg) && fileContent.length > 0);
|
const canCopy = Boolean(selectedFile && (!isSelectedImage || isSelectedSvg) && fileContent.length > 0);
|
||||||
|
const canCopyPath = Boolean(selectedFile && displaySelectedPath.length > 0);
|
||||||
|
const canEdit = Boolean(selectedFile && !isSelectedImage && files.writeFile && fileContent.length <= MAX_VIEW_CHARS);
|
||||||
|
|
||||||
|
const editorExtensions = React.useMemo(() => {
|
||||||
|
if (!selectedFile?.path) {
|
||||||
|
return [createFlexokiCodeMirrorTheme(currentTheme)];
|
||||||
|
}
|
||||||
|
|
||||||
|
const extensions = [createFlexokiCodeMirrorTheme(currentTheme)];
|
||||||
|
const language = languageByExtension(selectedFile.path);
|
||||||
|
if (language) {
|
||||||
|
extensions.push(language);
|
||||||
|
}
|
||||||
|
if (wrapLines) {
|
||||||
|
extensions.push(EditorView.lineWrapping);
|
||||||
|
}
|
||||||
|
return extensions;
|
||||||
|
}, [currentTheme, selectedFile?.path, wrapLines]);
|
||||||
|
|
||||||
const imageSrc = selectedFile?.path && isSelectedImage
|
const imageSrc = selectedFile?.path && isSelectedImage
|
||||||
? (runtime.isDesktop
|
? (runtime.isDesktop
|
||||||
@@ -794,83 +923,7 @@ export const FilesView: React.FC = () => {
|
|||||||
: `/api/fs/raw?path=${encodeURIComponent(selectedFile.path)}`))
|
: `/api/fs/raw?path=${encodeURIComponent(selectedFile.path)}`))
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
const codeRenderer = React.useCallback(({
|
|
||||||
rows,
|
|
||||||
stylesheet,
|
|
||||||
useInlineStyles,
|
|
||||||
}: {
|
|
||||||
rows: unknown[];
|
|
||||||
stylesheet: unknown;
|
|
||||||
useInlineStyles: boolean;
|
|
||||||
}) => {
|
|
||||||
const gutterWidthCh = Math.max(3, String(rows.length).length + 1);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div data-code-viewer>
|
|
||||||
{rows.map((row, index) => {
|
|
||||||
const lineNumber = index + 1;
|
|
||||||
const isSelected = lineSelection !== null && lineNumber >= lineSelection.start && lineNumber <= lineSelection.end;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
data-line-row={lineNumber}
|
|
||||||
data-selected={isSelected ? 'true' : undefined}
|
|
||||||
onMouseEnter={isMobile ? undefined : () => handleLineMouseEnter(lineNumber)}
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'flex-start',
|
|
||||||
lineHeight: '1.5rem',
|
|
||||||
position: 'relative',
|
|
||||||
backgroundColor: isSelected ? 'color-mix(in srgb, var(--accent) 70%, transparent)' : undefined,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
data-line-number={lineNumber}
|
|
||||||
onMouseDown={isMobile ? undefined : (e) => handleLineMouseDown(lineNumber, e)}
|
|
||||||
onMouseUp={isMobile ? undefined : handleLineMouseUp}
|
|
||||||
onClick={isMobile ? () => handleLineTap(lineNumber) : undefined}
|
|
||||||
style={{
|
|
||||||
width: `calc(${gutterWidthCh}ch + 0.75rem + 0.75rem)`,
|
|
||||||
flexShrink: 0,
|
|
||||||
paddingLeft: '0.75rem',
|
|
||||||
paddingRight: '1.75ch',
|
|
||||||
textAlign: 'right',
|
|
||||||
color: 'hsl(var(--muted-foreground))',
|
|
||||||
opacity: 0.35,
|
|
||||||
fontSize: '0.8em',
|
|
||||||
lineHeight: '1.5rem',
|
|
||||||
userSelect: 'none',
|
|
||||||
WebkitUserSelect: 'none',
|
|
||||||
MozUserSelect: 'none',
|
|
||||||
msUserSelect: 'none' as const,
|
|
||||||
cursor: 'pointer',
|
|
||||||
touchAction: 'manipulation',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{lineNumber}
|
|
||||||
</span>
|
|
||||||
<code
|
|
||||||
style={{
|
|
||||||
flex: 1,
|
|
||||||
minWidth: 0,
|
|
||||||
display: 'block',
|
|
||||||
whiteSpace: wrapLines ? 'pre-wrap' : 'pre',
|
|
||||||
overflowWrap: wrapLines ? 'break-word' : 'normal',
|
|
||||||
tabSize: 2,
|
|
||||||
paddingRight: '0.75rem',
|
|
||||||
userSelect: lineSelection ? 'none' : undefined,
|
|
||||||
WebkitUserSelect: lineSelection ? 'none' : undefined,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{createElement({ node: row, stylesheet, useInlineStyles, key: index })}
|
|
||||||
</code>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}, [wrapLines, lineSelection, isMobile, handleLineMouseDown, handleLineMouseEnter, handleLineMouseUp, handleLineTap]);
|
|
||||||
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@@ -999,9 +1052,35 @@ export const FilesView: React.FC = () => {
|
|||||||
|
|
||||||
const fileViewer = (
|
const fileViewer = (
|
||||||
<div
|
<div
|
||||||
className="relative flex h-full min-h-0 flex-col"
|
className="relative flex h-full min-h-0 min-w-0 w-full flex-col overflow-hidden"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 border-b border-border/40 px-3 py-1.5 flex-shrink-0">
|
<Dialog open={confirmDiscardOpen} onOpenChange={(open) => {
|
||||||
|
// Intentionally no "cancel" action. Keep dialog modal.
|
||||||
|
if (!open) {
|
||||||
|
setConfirmDiscardOpen(true);
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<DialogContent showCloseButton={false} className="max-w-md">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Unsaved changes</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Save your edits before continuing?
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => void saveAndContinue()}
|
||||||
|
disabled={isSaving}
|
||||||
|
className="border-[var(--status-success-border)] bg-[var(--status-success-background)] text-[var(--status-success)] hover:bg-[rgb(var(--status-success)/0.2)]"
|
||||||
|
>
|
||||||
|
Save changes
|
||||||
|
</Button>
|
||||||
|
<Button variant="destructive" onClick={discardAndContinue}>Discard</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
<div className="flex min-w-0 items-center gap-2 border-b border-border/40 px-3 py-1.5 flex-shrink-0">
|
||||||
{isMobile && showMobilePageContent && (
|
{isMobile && showMobilePageContent && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -1024,7 +1103,29 @@ export const FilesView: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-0">
|
<div className="flex items-center gap-1">
|
||||||
|
{canEdit && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => void saveDraft()}
|
||||||
|
disabled={!isDirty || isSaving}
|
||||||
|
className="h-5 w-5 p-0 text-[color:var(--status-success)] opacity-70 hover:opacity-100"
|
||||||
|
title={`Save (${getModifierLabel()}+S)`}
|
||||||
|
aria-label={`Save (${getModifierLabel()}+S)`}
|
||||||
|
>
|
||||||
|
{isSaving ? (
|
||||||
|
<RiLoader4Line className="h-4 w-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<RiSave3Line className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{canEdit && selectedFile && !isSelectedImage && (
|
||||||
|
<span aria-hidden="true" className="mx-1 h-4 w-px bg-border/60" />
|
||||||
|
)}
|
||||||
|
|
||||||
{selectedFile && !isSelectedImage && (
|
{selectedFile && !isSelectedImage && (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@@ -1040,6 +1141,10 @@ export const FilesView: React.FC = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{(canCopy || canCopyPath) && (canEdit || (selectedFile && !isSelectedImage)) && (
|
||||||
|
<span aria-hidden="true" className="mx-1 h-4 w-px bg-border/60" />
|
||||||
|
)}
|
||||||
|
|
||||||
{canCopy && (
|
{canCopy && (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@@ -1052,17 +1157,38 @@ export const FilesView: React.FC = () => {
|
|||||||
toast.error('Copy failed');
|
toast.error('Copy failed');
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="gap-1"
|
className="h-5 w-5 p-0"
|
||||||
|
title="Copy file contents"
|
||||||
|
aria-label="Copy file contents"
|
||||||
>
|
>
|
||||||
<RiClipboardLine className="h-4 w-4" />
|
<RiClipboardLine className="h-4 w-4" />
|
||||||
Copy
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{canCopyPath && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(displaySelectedPath);
|
||||||
|
toast.success('Copied');
|
||||||
|
} catch {
|
||||||
|
toast.error('Copy failed');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="h-5 w-5 p-0"
|
||||||
|
title={`Copy file path (${displaySelectedPath})`}
|
||||||
|
aria-label={`Copy file path (${displaySelectedPath})`}
|
||||||
|
>
|
||||||
|
<RiFileCopy2Line className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 min-h-0 relative">
|
<div className="flex-1 min-h-0 min-w-0 relative">
|
||||||
<ScrollableOverlay outerClassName="h-full" className="h-full">
|
<ScrollableOverlay outerClassName="h-full min-w-0" className="h-full min-w-0">
|
||||||
{!selectedFile ? (
|
{!selectedFile ? (
|
||||||
<div className="p-3 typography-ui text-muted-foreground">Pick a file from the tree.</div>
|
<div className="p-3 typography-ui text-muted-foreground">Pick a file from the tree.</div>
|
||||||
) : fileLoading ? (
|
) : fileLoading ? (
|
||||||
@@ -1081,32 +1207,79 @@ export const FilesView: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="py-3">
|
<div className="h-full">
|
||||||
<SyntaxHighlighter
|
<CodeMirrorEditor
|
||||||
key={lineSelection ? `${lineSelection.start}-${lineSelection.end}` : 'none'}
|
value={draftContent}
|
||||||
language={shouldHighlight ? viewerLanguage : 'text'}
|
onChange={setDraftContent}
|
||||||
style={syntaxTheme}
|
extensions={editorExtensions}
|
||||||
PreTag="div"
|
className="h-full"
|
||||||
renderer={codeRenderer}
|
highlightLines={lineSelection
|
||||||
customStyle={{
|
? {
|
||||||
margin: 0,
|
start: Math.min(lineSelection.start, lineSelection.end),
|
||||||
padding: 0,
|
end: Math.max(lineSelection.start, lineSelection.end),
|
||||||
background: 'transparent',
|
}
|
||||||
fontSize: 'var(--text-code)',
|
: undefined}
|
||||||
lineHeight: '1.5rem',
|
lineNumbersConfig={{
|
||||||
overflowX: 'auto',
|
domEventHandlers: {
|
||||||
overflowY: 'visible',
|
mousedown: (view: EditorView, line: { from: number; to: number }, event: Event) => {
|
||||||
whiteSpace: 'normal',
|
if (!(event instanceof MouseEvent)) {
|
||||||
}}
|
return false;
|
||||||
codeTagProps={{
|
}
|
||||||
style: {
|
if (event.button !== 0) {
|
||||||
fontStyle: 'normal',
|
return false;
|
||||||
lineHeight: '1.5rem',
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const lineNumber = view.state.doc.lineAt(line.from).number;
|
||||||
|
|
||||||
|
// Mobile: tap-to-extend selection
|
||||||
|
if (isMobile && lineSelection && !event.shiftKey) {
|
||||||
|
const start = Math.min(lineSelection.start, lineSelection.end, lineNumber);
|
||||||
|
const end = Math.max(lineSelection.start, lineSelection.end, lineNumber);
|
||||||
|
setLineSelection({ start, end });
|
||||||
|
isSelectingRef.current = false;
|
||||||
|
selectionStartRef.current = null;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
isSelectingRef.current = true;
|
||||||
|
selectionStartRef.current = lineNumber;
|
||||||
|
|
||||||
|
if (lineSelection && event.shiftKey) {
|
||||||
|
const start = Math.min(lineSelection.start, lineNumber);
|
||||||
|
const end = Math.max(lineSelection.end, lineNumber);
|
||||||
|
setLineSelection({ start, end });
|
||||||
|
} else {
|
||||||
|
setLineSelection({ start: lineNumber, end: lineNumber });
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
mouseover: (view: EditorView, line: { from: number; to: number }, event: Event) => {
|
||||||
|
if (!(event instanceof MouseEvent)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (event.buttons !== 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!isSelectingRef.current || selectionStartRef.current === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lineNumber = view.state.doc.lineAt(line.from).number;
|
||||||
|
const start = Math.min(selectionStartRef.current, lineNumber);
|
||||||
|
const end = Math.max(selectionStartRef.current, lineNumber);
|
||||||
|
setLineSelection({ start, end });
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
mouseup: () => {
|
||||||
|
isSelectingRef.current = false;
|
||||||
|
selectionStartRef.current = null;
|
||||||
|
return false;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
{contentForViewer}
|
|
||||||
</SyntaxHighlighter>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</ScrollableOverlay>
|
</ScrollableOverlay>
|
||||||
@@ -1225,16 +1398,18 @@ export const FilesView: React.FC = () => {
|
|||||||
) : (
|
) : (
|
||||||
treePanel
|
treePanel
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-1 min-h-0 min-w-0 gap-3 px-3 pb-3 pt-2">
|
<div className="flex flex-1 min-h-0 min-w-0 gap-3 px-3 pb-3 pt-2">
|
||||||
<div className="w-72 flex-shrink-0 min-h-0 overflow-hidden">
|
{screenWidth >= 1024 && (
|
||||||
{treePanel}
|
<div className="w-72 flex-shrink-0 min-h-0 overflow-hidden">
|
||||||
</div>
|
{treePanel}
|
||||||
<div className="flex-1 min-h-0 min-w-0 overflow-hidden rounded-xl border border-border/60 bg-background">
|
</div>
|
||||||
{fileViewer}
|
)}
|
||||||
</div>
|
<div className="flex-1 min-h-0 min-w-0 overflow-hidden rounded-xl border border-border/60 bg-background">
|
||||||
</div>
|
{fileViewer}
|
||||||
)}
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -46,6 +46,10 @@ textarea[data-chat-input="true"]:hover {
|
|||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.oc-cm-selected-line {
|
||||||
|
background: color-mix(in srgb, var(--accent) 70%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
:root.vscode-runtime textarea[data-chat-input="true"]::placeholder {
|
:root.vscode-runtime textarea[data-chat-input="true"]::placeholder {
|
||||||
color: color-mix(in srgb, var(--vscode-input-placeholderForeground, var(--muted-foreground)) 65%, transparent);
|
color: color-mix(in srgb, var(--vscode-input-placeholderForeground, var(--muted-foreground)) 65%, transparent);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,193 @@
|
|||||||
|
import type { Extension } from '@codemirror/state';
|
||||||
|
|
||||||
|
import { EditorView } from '@codemirror/view';
|
||||||
|
import { HighlightStyle, syntaxHighlighting } from '@codemirror/language';
|
||||||
|
import { tags as t } from '@lezer/highlight';
|
||||||
|
|
||||||
|
import type { Theme } from '@/types/theme';
|
||||||
|
|
||||||
|
export function createFlexokiCodeMirrorTheme(theme: Theme): Extension {
|
||||||
|
const isDark = theme.metadata.variant === 'dark';
|
||||||
|
|
||||||
|
const monoFont = theme.config?.fonts?.mono || 'monospace';
|
||||||
|
const highlights = theme.colors.syntax.highlights || {};
|
||||||
|
const tokens = theme.colors.syntax.tokens || {};
|
||||||
|
|
||||||
|
const ui = EditorView.theme({
|
||||||
|
'&': {
|
||||||
|
backgroundColor: 'var(--background)',
|
||||||
|
color: theme.colors.syntax.base.foreground,
|
||||||
|
fontSize: 'var(--text-code)',
|
||||||
|
lineHeight: '1.5rem',
|
||||||
|
},
|
||||||
|
'.cm-scroller': {
|
||||||
|
fontFamily: monoFont,
|
||||||
|
backgroundColor: 'var(--background)',
|
||||||
|
},
|
||||||
|
|
||||||
|
/* StreamLanguage/legacy-modes tokens (class-based) */
|
||||||
|
'.cm-comment': {
|
||||||
|
color: theme.colors.syntax.base.comment,
|
||||||
|
},
|
||||||
|
'.cm-keyword': {
|
||||||
|
color: theme.colors.syntax.base.keyword,
|
||||||
|
},
|
||||||
|
'.cm-string': {
|
||||||
|
color: theme.colors.syntax.base.string,
|
||||||
|
},
|
||||||
|
'.cm-string-2': {
|
||||||
|
color: tokens.stringEscape || theme.colors.syntax.base.string,
|
||||||
|
},
|
||||||
|
'.cm-number': {
|
||||||
|
color: theme.colors.syntax.base.number,
|
||||||
|
},
|
||||||
|
'.cm-atom': {
|
||||||
|
color: tokens.boolean || theme.colors.syntax.base.number,
|
||||||
|
},
|
||||||
|
'.cm-builtin': {
|
||||||
|
color: tokens.functionCall || theme.colors.syntax.base.function,
|
||||||
|
},
|
||||||
|
'.cm-def': {
|
||||||
|
color: tokens.variableGlobal || theme.colors.syntax.base.variable,
|
||||||
|
},
|
||||||
|
// Legacy shell flags (--foo, -bar)
|
||||||
|
'.cm-attribute': {
|
||||||
|
color: tokens.variableOther || tokens.variableProperty || theme.colors.syntax.base.operator,
|
||||||
|
},
|
||||||
|
'.cm-meta': {
|
||||||
|
color: theme.colors.syntax.base.comment,
|
||||||
|
},
|
||||||
|
'.cm-property': {
|
||||||
|
color: tokens.variableProperty || theme.colors.syntax.base.keyword,
|
||||||
|
},
|
||||||
|
'.cm-variable': {
|
||||||
|
color: theme.colors.syntax.base.variable,
|
||||||
|
},
|
||||||
|
'.cm-variable-2': {
|
||||||
|
color: tokens.variableOther || theme.colors.syntax.base.function,
|
||||||
|
},
|
||||||
|
'.cm-variable-3': {
|
||||||
|
color: tokens.variableGlobal || theme.colors.syntax.base.type,
|
||||||
|
},
|
||||||
|
'.cm-tag': {
|
||||||
|
color: tokens.tag || theme.colors.syntax.base.keyword,
|
||||||
|
},
|
||||||
|
'.cm-link': {
|
||||||
|
color: tokens.url || theme.colors.syntax.base.keyword,
|
||||||
|
textDecoration: 'underline',
|
||||||
|
},
|
||||||
|
'.cm-content': {
|
||||||
|
caretColor: theme.colors.interactive.cursor,
|
||||||
|
},
|
||||||
|
'.cm-cursor, .cm-dropCursor': {
|
||||||
|
borderLeftColor: theme.colors.interactive.cursor,
|
||||||
|
},
|
||||||
|
'&.cm-focused .cm-selectionBackground, .cm-selectionBackground, ::selection': {
|
||||||
|
backgroundColor: theme.colors.interactive.selection,
|
||||||
|
},
|
||||||
|
'.cm-gutters': {
|
||||||
|
backgroundColor: 'var(--background)',
|
||||||
|
color: highlights.lineNumber || theme.colors.syntax.base.comment,
|
||||||
|
borderRight: `1px solid ${theme.colors.interactive.border}`,
|
||||||
|
position: 'sticky',
|
||||||
|
paddingRight: '8px',
|
||||||
|
left: 0,
|
||||||
|
zIndex: 2,
|
||||||
|
boxShadow: `0 0 0 var(--background)`,
|
||||||
|
},
|
||||||
|
'.cm-gutter': {
|
||||||
|
backgroundColor: 'var(--background)',
|
||||||
|
},
|
||||||
|
'.cm-gutterElement': {
|
||||||
|
backgroundColor: 'var(--background)',
|
||||||
|
},
|
||||||
|
'.cm-lineNumbers': {
|
||||||
|
backgroundColor: 'var(--background)',
|
||||||
|
},
|
||||||
|
'.cm-lineNumbers .cm-gutterElement': {
|
||||||
|
paddingLeft: '8px',
|
||||||
|
paddingRight: '8px',
|
||||||
|
minWidth: '42px',
|
||||||
|
},
|
||||||
|
'.cm-activeLineGutter': {
|
||||||
|
color: highlights.lineNumberActive || theme.colors.syntax.base.foreground,
|
||||||
|
},
|
||||||
|
'.cm-activeLine': {
|
||||||
|
backgroundColor: theme.colors.surface.overlay,
|
||||||
|
},
|
||||||
|
'&.cm-focused': {
|
||||||
|
outline: 'none',
|
||||||
|
},
|
||||||
|
}, { dark: isDark });
|
||||||
|
|
||||||
|
const syntax = HighlightStyle.define([
|
||||||
|
{ tag: t.comment, color: theme.colors.syntax.base.comment },
|
||||||
|
{ tag: t.docComment, color: tokens.commentDoc || theme.colors.syntax.base.comment },
|
||||||
|
|
||||||
|
{ tag: t.keyword, color: theme.colors.syntax.base.keyword },
|
||||||
|
{ tag: t.controlKeyword, color: theme.colors.syntax.base.keyword },
|
||||||
|
{ tag: t.operatorKeyword, color: theme.colors.syntax.base.operator },
|
||||||
|
{ tag: t.moduleKeyword, color: tokens.keywordImport || theme.colors.syntax.base.keyword },
|
||||||
|
{ tag: [t.definitionKeyword, t.modifier], color: tokens.storageModifier || theme.colors.syntax.base.keyword },
|
||||||
|
|
||||||
|
{ tag: t.atom, color: tokens.boolean || theme.colors.syntax.base.number },
|
||||||
|
{ tag: [t.null, t.self], color: theme.colors.syntax.base.number },
|
||||||
|
{ tag: [t.meta, t.documentMeta], color: theme.colors.syntax.base.comment },
|
||||||
|
|
||||||
|
{ tag: t.string, color: theme.colors.syntax.base.string },
|
||||||
|
{ tag: t.escape, color: tokens.stringEscape || theme.colors.syntax.base.foreground },
|
||||||
|
{ tag: t.regexp, color: tokens.regex || theme.colors.syntax.base.string },
|
||||||
|
|
||||||
|
{ tag: t.number, color: theme.colors.syntax.base.number },
|
||||||
|
{ tag: t.bool, color: tokens.boolean || theme.colors.syntax.base.number },
|
||||||
|
|
||||||
|
// Operators + punctuation
|
||||||
|
{ tag: t.operator, color: theme.colors.syntax.base.operator },
|
||||||
|
{ tag: [t.derefOperator, t.updateOperator, t.definitionOperator, t.typeOperator, t.controlOperator], color: theme.colors.syntax.base.operator },
|
||||||
|
{ tag: [t.logicOperator, t.bitwiseOperator, t.arithmeticOperator], color: theme.colors.syntax.base.operator },
|
||||||
|
{ tag: [t.compareOperator], color: tokens.diffModified || theme.colors.syntax.base.operator },
|
||||||
|
{ tag: [t.punctuation, t.separator, t.bracket, t.paren, t.brace, t.squareBracket, t.angleBracket], color: tokens.punctuation || theme.colors.syntax.base.comment },
|
||||||
|
|
||||||
|
// Calls vs definitions
|
||||||
|
{ tag: t.function(t.variableName), color: tokens.functionCall || theme.colors.syntax.base.function },
|
||||||
|
{ tag: t.function(t.definition(t.variableName)), color: theme.colors.syntax.base.function },
|
||||||
|
{ tag: t.function(t.propertyName), color: tokens.method || tokens.functionCall || theme.colors.syntax.base.function },
|
||||||
|
|
||||||
|
// Names
|
||||||
|
{ tag: t.namespace, color: tokens.namespace || theme.colors.syntax.base.type },
|
||||||
|
{ tag: t.moduleKeyword, color: tokens.module || theme.colors.syntax.base.keyword },
|
||||||
|
{ tag: t.macroName, color: tokens.macro || theme.colors.syntax.base.keyword },
|
||||||
|
{ tag: t.labelName, color: tokens.label || theme.colors.syntax.base.keyword },
|
||||||
|
{ tag: t.annotation, color: tokens.decorator || theme.colors.syntax.base.keyword },
|
||||||
|
|
||||||
|
// Variables/properties
|
||||||
|
{ tag: t.propertyName, color: tokens.variableProperty || theme.colors.syntax.base.keyword },
|
||||||
|
{ tag: t.attributeName, color: tokens.tagAttribute || theme.colors.syntax.base.keyword },
|
||||||
|
|
||||||
|
// StreamLanguage/legacy token tags resolve to these
|
||||||
|
{ tag: t.standard(t.variableName), color: tokens.method || theme.colors.syntax.base.function },
|
||||||
|
{ tag: t.definition(t.variableName), color: theme.colors.syntax.base.variable },
|
||||||
|
{ tag: t.local(t.variableName), color: theme.colors.syntax.base.variable },
|
||||||
|
{ tag: t.special(t.variableName), color: tokens.variableOther || theme.colors.syntax.base.function },
|
||||||
|
{ tag: t.variableName, color: theme.colors.syntax.base.variable },
|
||||||
|
{ tag: t.special(t.string), color: theme.colors.syntax.base.string },
|
||||||
|
|
||||||
|
// Types/constants
|
||||||
|
{ tag: t.className, color: tokens.className || theme.colors.syntax.base.type },
|
||||||
|
{ tag: t.typeName, color: theme.colors.syntax.base.type },
|
||||||
|
{ tag: t.constant(t.variableName), color: tokens.constant || theme.colors.syntax.base.variable },
|
||||||
|
{ tag: t.literal, color: tokens.constant || theme.colors.syntax.base.variable },
|
||||||
|
|
||||||
|
// Markup
|
||||||
|
{ tag: t.tagName, color: tokens.tag || theme.colors.syntax.base.keyword },
|
||||||
|
{ tag: t.attributeValue, color: tokens.tagAttributeValue || theme.colors.syntax.base.string },
|
||||||
|
|
||||||
|
// Markdown-ish
|
||||||
|
{ tag: [t.heading, t.heading1, t.heading2, t.heading3, t.heading4, t.heading5, t.heading6], color: theme.colors.syntax.base.keyword, fontWeight: '600' },
|
||||||
|
{ tag: t.monospace, color: theme.colors.syntax.base.string },
|
||||||
|
|
||||||
|
{ tag: t.link, color: tokens.url || theme.colors.syntax.base.keyword, textDecoration: 'underline' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
return [ui, syntaxHighlighting(syntax)];
|
||||||
|
}
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
import type { Extension } from '@codemirror/state';
|
||||||
|
|
||||||
|
import { javascript } from '@codemirror/lang-javascript';
|
||||||
|
import { json } from '@codemirror/lang-json';
|
||||||
|
import { css } from '@codemirror/lang-css';
|
||||||
|
import { html } from '@codemirror/lang-html';
|
||||||
|
import { markdown } from '@codemirror/lang-markdown';
|
||||||
|
import { languages } from '@codemirror/language-data';
|
||||||
|
import { python } from '@codemirror/lang-python';
|
||||||
|
import { sql } from '@codemirror/lang-sql';
|
||||||
|
import { xml } from '@codemirror/lang-xml';
|
||||||
|
import { yaml as yamlLanguage } from '@codemirror/lang-yaml';
|
||||||
|
import { rust } from '@codemirror/lang-rust';
|
||||||
|
|
||||||
|
import { Language, LanguageDescription, StreamLanguage } from '@codemirror/language';
|
||||||
|
import { shell } from '@codemirror/legacy-modes/mode/shell';
|
||||||
|
import { toml } from '@codemirror/legacy-modes/mode/toml';
|
||||||
|
import { diff } from '@codemirror/legacy-modes/mode/diff';
|
||||||
|
import { dockerFile } from '@codemirror/legacy-modes/mode/dockerfile';
|
||||||
|
import { ruby } from '@codemirror/legacy-modes/mode/ruby';
|
||||||
|
import { properties } from '@codemirror/legacy-modes/mode/properties';
|
||||||
|
|
||||||
|
const shellLanguage = StreamLanguage.define(shell);
|
||||||
|
const tomlLanguage = StreamLanguage.define(toml);
|
||||||
|
const diffLanguage = StreamLanguage.define(diff);
|
||||||
|
const dockerfileLanguage = StreamLanguage.define(dockerFile);
|
||||||
|
const rubyLanguage = StreamLanguage.define(ruby);
|
||||||
|
const propertiesLanguage = StreamLanguage.define(properties);
|
||||||
|
|
||||||
|
function codeBlockLanguageResolver(info: string): Language | LanguageDescription | null {
|
||||||
|
const normalized = info.trim().toLowerCase();
|
||||||
|
|
||||||
|
switch (normalized) {
|
||||||
|
case 'bash':
|
||||||
|
case 'sh':
|
||||||
|
case 'zsh':
|
||||||
|
case 'shell':
|
||||||
|
case 'shellsession':
|
||||||
|
case 'console':
|
||||||
|
return shellLanguage;
|
||||||
|
case 'toml':
|
||||||
|
return tomlLanguage;
|
||||||
|
case 'diff':
|
||||||
|
case 'patch':
|
||||||
|
return diffLanguage;
|
||||||
|
case 'json':
|
||||||
|
case 'jsonc':
|
||||||
|
case 'json5':
|
||||||
|
return json().language;
|
||||||
|
case 'js':
|
||||||
|
case 'javascript':
|
||||||
|
return javascript().language;
|
||||||
|
case 'jsx':
|
||||||
|
return javascript({ jsx: true }).language;
|
||||||
|
case 'ts':
|
||||||
|
case 'typescript':
|
||||||
|
return javascript({ typescript: true }).language;
|
||||||
|
case 'tsx':
|
||||||
|
return javascript({ typescript: true, jsx: true }).language;
|
||||||
|
case 'yaml':
|
||||||
|
case 'yml':
|
||||||
|
return yamlLanguage().language;
|
||||||
|
case 'html':
|
||||||
|
return html().language;
|
||||||
|
case 'css':
|
||||||
|
return css().language;
|
||||||
|
case 'xml':
|
||||||
|
case 'svg':
|
||||||
|
return xml().language;
|
||||||
|
case 'py':
|
||||||
|
case 'python':
|
||||||
|
return python().language;
|
||||||
|
case 'sql':
|
||||||
|
return sql().language;
|
||||||
|
case 'rs':
|
||||||
|
case 'rust':
|
||||||
|
return rust().language;
|
||||||
|
default:
|
||||||
|
return LanguageDescription.matchLanguageName(languages, normalized, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizeFileName = (filePath: string) => filePath.split('/').pop()?.toLowerCase() ?? '';
|
||||||
|
|
||||||
|
export function languageByExtension(filePath: string): Extension | null {
|
||||||
|
const normalized = filePath.toLowerCase();
|
||||||
|
const filename = normalizeFileName(normalized);
|
||||||
|
|
||||||
|
// Special filenames
|
||||||
|
switch (filename) {
|
||||||
|
case 'dockerfile':
|
||||||
|
return dockerfileLanguage;
|
||||||
|
case 'makefile':
|
||||||
|
case 'gnumakefile':
|
||||||
|
// No dedicated mode; shell is a decent fallback for Make-ish files.
|
||||||
|
return shellLanguage;
|
||||||
|
}
|
||||||
|
|
||||||
|
const idx = normalized.lastIndexOf('.');
|
||||||
|
const ext = idx >= 0 ? normalized.slice(idx + 1) : '';
|
||||||
|
|
||||||
|
switch (ext) {
|
||||||
|
// JavaScript/TypeScript
|
||||||
|
case 'ts':
|
||||||
|
case 'tsx':
|
||||||
|
case 'mts':
|
||||||
|
case 'cts':
|
||||||
|
return javascript({ typescript: true, jsx: ext === 'tsx' });
|
||||||
|
case 'js':
|
||||||
|
case 'jsx':
|
||||||
|
case 'mjs':
|
||||||
|
case 'cjs':
|
||||||
|
return javascript({ typescript: false, jsx: ext === 'jsx' });
|
||||||
|
|
||||||
|
// Web
|
||||||
|
case 'json':
|
||||||
|
case 'jsonc':
|
||||||
|
case 'json5':
|
||||||
|
case 'jsonl':
|
||||||
|
case 'ndjson':
|
||||||
|
case 'geojson':
|
||||||
|
return json();
|
||||||
|
case 'css':
|
||||||
|
case 'scss':
|
||||||
|
case 'sass':
|
||||||
|
case 'less':
|
||||||
|
return css();
|
||||||
|
case 'html':
|
||||||
|
case 'htm':
|
||||||
|
return html();
|
||||||
|
case 'md':
|
||||||
|
case 'mdx':
|
||||||
|
case 'markdown':
|
||||||
|
case 'mdown':
|
||||||
|
case 'mkd':
|
||||||
|
return markdown({
|
||||||
|
codeLanguages: codeBlockLanguageResolver,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Data/config
|
||||||
|
case 'yml':
|
||||||
|
case 'yaml':
|
||||||
|
return yamlLanguage();
|
||||||
|
case 'toml':
|
||||||
|
return tomlLanguage;
|
||||||
|
case 'ini':
|
||||||
|
case 'cfg':
|
||||||
|
case 'conf':
|
||||||
|
case 'config':
|
||||||
|
case 'properties':
|
||||||
|
return propertiesLanguage;
|
||||||
|
|
||||||
|
// Shell
|
||||||
|
case 'sh':
|
||||||
|
case 'bash':
|
||||||
|
case 'zsh':
|
||||||
|
case 'fish':
|
||||||
|
case 'env':
|
||||||
|
return shellLanguage;
|
||||||
|
|
||||||
|
// Languages we already ship
|
||||||
|
case 'py':
|
||||||
|
case 'pyw':
|
||||||
|
case 'pyi':
|
||||||
|
return python();
|
||||||
|
case 'sql':
|
||||||
|
case 'psql':
|
||||||
|
case 'plsql':
|
||||||
|
return sql();
|
||||||
|
case 'xml':
|
||||||
|
case 'xsl':
|
||||||
|
case 'xslt':
|
||||||
|
case 'xsd':
|
||||||
|
case 'dtd':
|
||||||
|
case 'plist':
|
||||||
|
case 'svg':
|
||||||
|
return xml();
|
||||||
|
case 'rs':
|
||||||
|
return rust();
|
||||||
|
|
||||||
|
// Legacy modes
|
||||||
|
case 'rb':
|
||||||
|
case 'erb':
|
||||||
|
case 'rake':
|
||||||
|
case 'gemspec':
|
||||||
|
return rubyLanguage;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,8 @@ import { getSafeStorage } from './utils/safeStorage';
|
|||||||
import { SEMANTIC_TYPOGRAPHY, getTypographyVariable, type SemanticTypographyKey } from '@/lib/typography';
|
import { SEMANTIC_TYPOGRAPHY, getTypographyVariable, type SemanticTypographyKey } from '@/lib/typography';
|
||||||
|
|
||||||
export type MainTab = 'chat' | 'git' | 'diff' | 'terminal' | 'files';
|
export type MainTab = 'chat' | 'git' | 'diff' | 'terminal' | 'files';
|
||||||
|
|
||||||
|
export type MainTabGuard = (nextTab: MainTab) => boolean;
|
||||||
export type EventStreamStatus =
|
export type EventStreamStatus =
|
||||||
| 'idle'
|
| 'idle'
|
||||||
| 'connecting'
|
| 'connecting'
|
||||||
@@ -24,6 +26,7 @@ interface UIStore {
|
|||||||
hasManuallyResizedLeftSidebar: boolean;
|
hasManuallyResizedLeftSidebar: boolean;
|
||||||
isSessionSwitcherOpen: boolean;
|
isSessionSwitcherOpen: boolean;
|
||||||
activeMainTab: MainTab;
|
activeMainTab: MainTab;
|
||||||
|
mainTabGuard: MainTabGuard | null;
|
||||||
pendingDiffFile: string | null;
|
pendingDiffFile: string | null;
|
||||||
isMobile: boolean;
|
isMobile: boolean;
|
||||||
isKeyboardOpen: boolean;
|
isKeyboardOpen: boolean;
|
||||||
@@ -64,6 +67,7 @@ interface UIStore {
|
|||||||
setSidebarWidth: (width: number) => void;
|
setSidebarWidth: (width: number) => void;
|
||||||
setSessionSwitcherOpen: (open: boolean) => void;
|
setSessionSwitcherOpen: (open: boolean) => void;
|
||||||
setActiveMainTab: (tab: MainTab) => void;
|
setActiveMainTab: (tab: MainTab) => void;
|
||||||
|
setMainTabGuard: (guard: MainTabGuard | null) => void;
|
||||||
setPendingDiffFile: (filePath: string | null) => void;
|
setPendingDiffFile: (filePath: string | null) => void;
|
||||||
navigateToDiff: (filePath: string) => void;
|
navigateToDiff: (filePath: string) => void;
|
||||||
consumePendingDiffFile: () => string | null;
|
consumePendingDiffFile: () => string | null;
|
||||||
@@ -120,6 +124,7 @@ export const useUIStore = create<UIStore>()(
|
|||||||
hasManuallyResizedLeftSidebar: false,
|
hasManuallyResizedLeftSidebar: false,
|
||||||
isSessionSwitcherOpen: false,
|
isSessionSwitcherOpen: false,
|
||||||
activeMainTab: 'chat',
|
activeMainTab: 'chat',
|
||||||
|
mainTabGuard: null,
|
||||||
pendingDiffFile: null,
|
pendingDiffFile: null,
|
||||||
isMobile: false,
|
isMobile: false,
|
||||||
isKeyboardOpen: false,
|
isKeyboardOpen: false,
|
||||||
@@ -195,7 +200,15 @@ export const useUIStore = create<UIStore>()(
|
|||||||
set({ isSessionSwitcherOpen: open });
|
set({ isSessionSwitcherOpen: open });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setMainTabGuard: (guard) => {
|
||||||
|
set({ mainTabGuard: guard });
|
||||||
|
},
|
||||||
|
|
||||||
setActiveMainTab: (tab) => {
|
setActiveMainTab: (tab) => {
|
||||||
|
const guard = get().mainTabGuard;
|
||||||
|
if (guard && !guard(tab)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
set({ activeMainTab: tab });
|
set({ activeMainTab: tab });
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -204,6 +217,10 @@ export const useUIStore = create<UIStore>()(
|
|||||||
},
|
},
|
||||||
|
|
||||||
navigateToDiff: (filePath) => {
|
navigateToDiff: (filePath) => {
|
||||||
|
const guard = get().mainTabGuard;
|
||||||
|
if (guard && !guard('diff')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
set({ pendingDiffFile: filePath, activeMainTab: 'diff' });
|
set({ pendingDiffFile: filePath, activeMainTab: 'diff' });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -138,4 +138,24 @@ export const createWebFilesAPI = (): FilesAPI => ({
|
|||||||
const content = await response.text();
|
const content = await response.text();
|
||||||
return { content, path: target };
|
return { content, path: target };
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async writeFile(path: string, content: string): Promise<{ success: boolean; path: string }> {
|
||||||
|
const target = normalizePath(path);
|
||||||
|
const response = await fetch('/api/fs/write', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ path: target, content }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json().catch(() => ({ error: response.statusText }));
|
||||||
|
throw new Error((error as { error?: string }).error || 'Failed to write file');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json().catch(() => ({}));
|
||||||
|
return {
|
||||||
|
success: Boolean((result as { success?: boolean }).success),
|
||||||
|
path: typeof (result as { path?: string }).path === 'string' ? normalizePath((result as { path: string }).path) : target,
|
||||||
|
};
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user