feat(files): add CodeMirror 6 editor with save, dirty tracking, and 15 language modes
This commit is contained in:
parent
0ffbd93b8b
commit
3bb972fc01
6 changed files with 941 additions and 75 deletions
483
v2/package-lock.json
generated
483
v2/package-lock.json
generated
|
|
@ -9,12 +9,27 @@
|
|||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.2.70",
|
||||
"@codemirror/lang-cpp": "^6.0.3",
|
||||
"@codemirror/lang-css": "^6.3.1",
|
||||
"@codemirror/lang-go": "^6.0.1",
|
||||
"@codemirror/lang-html": "^6.4.11",
|
||||
"@codemirror/lang-java": "^6.0.2",
|
||||
"@codemirror/lang-javascript": "^6.2.5",
|
||||
"@codemirror/lang-json": "^6.0.2",
|
||||
"@codemirror/lang-markdown": "^6.5.0",
|
||||
"@codemirror/lang-php": "^6.0.2",
|
||||
"@codemirror/lang-python": "^6.2.1",
|
||||
"@codemirror/lang-rust": "^6.0.2",
|
||||
"@codemirror/lang-sql": "^6.10.0",
|
||||
"@codemirror/lang-xml": "^6.1.0",
|
||||
"@codemirror/lang-yaml": "^6.1.2",
|
||||
"@tauri-apps/api": "^2.10.1",
|
||||
"@tauri-apps/plugin-dialog": "^2.6.0",
|
||||
"@tauri-apps/plugin-updater": "^2.10.0",
|
||||
"@xterm/addon-canvas": "^0.7.0",
|
||||
"@xterm/addon-fit": "^0.11.0",
|
||||
"@xterm/xterm": "^6.0.0",
|
||||
"codemirror": "^6.0.2",
|
||||
"marked": "^17.0.4",
|
||||
"shiki": "^4.0.1"
|
||||
},
|
||||
|
|
@ -81,6 +96,269 @@
|
|||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/autocomplete": {
|
||||
"version": "6.20.1",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.1.tgz",
|
||||
"integrity": "sha512-1cvg3Vz1dSSToCNlJfRA2WSI4ht3K+WplO0UMOgmUYPivCyy2oueZY6Lx7M9wThm7SDUBViRmuT+OG/i8+ON9A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.17.0",
|
||||
"@lezer/common": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/commands": {
|
||||
"version": "6.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.2.tgz",
|
||||
"integrity": "sha512-vvX1fsih9HledO1c9zdotZYUZnE4xV0m6i3m25s5DIfXofuprk6cRcLUZvSk3CASUbwjQX21tOGbkY2BH8TpnQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.4.0",
|
||||
"@codemirror/view": "^6.27.0",
|
||||
"@lezer/common": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-cpp": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-cpp/-/lang-cpp-6.0.3.tgz",
|
||||
"integrity": "sha512-URM26M3vunFFn9/sm6rzqrBzDgfWuDixp85uTY49wKudToc2jTHUrKIGGKs+QWND+YLofNNZpxcNGRynFJfvgA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@lezer/cpp": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-css": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.1.tgz",
|
||||
"integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==",
|
||||
"license": "MIT",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-go": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-go/-/lang-go-6.0.1.tgz",
|
||||
"integrity": "sha512-7fNvbyNylvqCphW9HD6WFnRpcDjr+KXX/FgqXy5H5ZS0eC5edDljukm/yNgYkwTsgp2busdod50AOTIy6Jikfg==",
|
||||
"license": "MIT",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-html": {
|
||||
"version": "6.4.11",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.11.tgz",
|
||||
"integrity": "sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw==",
|
||||
"license": "MIT",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-java": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-java/-/lang-java-6.0.2.tgz",
|
||||
"integrity": "sha512-m5Nt1mQ/cznJY7tMfQTJchmrjdjQ71IDs+55d1GAa8DGaB8JXWsVCkVT284C3RTASaY43YknrK2X3hPO/J3MOQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@lezer/java": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-javascript": {
|
||||
"version": "6.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.5.tgz",
|
||||
"integrity": "sha512-zD4e5mS+50htS7F+TYjBPsiIFGanfVqg4HyUz6WNFikgOPf2BgKlx+TQedI1w6n/IqRBVBbBWmGFdLB/7uxO4A==",
|
||||
"license": "MIT",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-json": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.2.tgz",
|
||||
"integrity": "sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@lezer/json": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-markdown": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.5.0.tgz",
|
||||
"integrity": "sha512-0K40bZ35jpHya6FriukbgaleaqzBLZfOh7HuzqbMxBXkbYMJDxfF39c23xOgxFezR+3G+tR2/Mup+Xk865OMvw==",
|
||||
"license": "MIT",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-php": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-php/-/lang-php-6.0.2.tgz",
|
||||
"integrity": "sha512-ZKy2v1n8Fc8oEXj0Th0PUMXzQJ0AIR6TaZU+PbDHExFwdu+guzOA4jmCHS1Nz4vbFezwD7LyBdDnddSJeScMCA==",
|
||||
"license": "MIT",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-python": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-python/-/lang-python-6.2.1.tgz",
|
||||
"integrity": "sha512-IRjC8RUBhn9mGR9ywecNhB51yePWCGgvHfY1lWN/Mrp3cKuHr0isDKia+9HnvhiWNnMpbGhWrkhuWOc09exRyw==",
|
||||
"license": "MIT",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-rust": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-rust/-/lang-rust-6.0.2.tgz",
|
||||
"integrity": "sha512-EZaGjCUegtiU7kSMvOfEZpaCReowEf3yNidYu7+vfuGTm9ow4mthAparY5hisJqOHmJowVH3Upu+eJlUji6qqA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@lezer/rust": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-sql": {
|
||||
"version": "6.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-sql/-/lang-sql-6.10.0.tgz",
|
||||
"integrity": "sha512-6ayPkEd/yRw0XKBx5uAiToSgGECo/GY2NoJIHXIIQh1EVwLuKoU8BP/qK0qH5NLXAbtJRLuT73hx7P9X34iO4w==",
|
||||
"license": "MIT",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-xml": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-xml/-/lang-xml-6.1.0.tgz",
|
||||
"integrity": "sha512-3z0blhicHLfwi2UgkZYRPioSgVTo9PV5GP5ducFH6FaHy0IAJRg+ixj5gTR1gnT/glAIC8xv4w2VL1LoZfs+Jg==",
|
||||
"license": "MIT",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-yaml": {
|
||||
"version": "6.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-yaml/-/lang-yaml-6.1.2.tgz",
|
||||
"integrity": "sha512-dxrfG8w5Ce/QbT7YID7mWZFKhdhsaTNOYjOkSIMt1qmC4VQnXSDSYVHHHn8k6kJUfIhtLo8t1JJgltlxWdsITw==",
|
||||
"license": "MIT",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/language": {
|
||||
"version": "6.12.2",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.2.tgz",
|
||||
"integrity": "sha512-jEPmz2nGGDxhRTg3lTpzmIyGKxz3Gp3SJES4b0nAuE5SWQoKdT5GoQ69cwMmFd+wvFUhYirtDTr0/DRHpQAyWg==",
|
||||
"license": "MIT",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lint": {
|
||||
"version": "6.9.5",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.5.tgz",
|
||||
"integrity": "sha512-GElsbU9G7QT9xXhpUg1zWGmftA/7jamh+7+ydKRuT0ORpWS3wOSP0yT1FOlIZa7mIJjpVPipErsyvVqB9cfTFA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.35.0",
|
||||
"crelt": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/search": {
|
||||
"version": "6.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.6.0.tgz",
|
||||
"integrity": "sha512-koFuNXcDvyyotWcgOnZGmY7LZqEOXZaaxD/j6n18TCLx2/9HieZJ5H6hs1g8FiRxBD0DNfs0nXn17g872RmYdw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.37.0",
|
||||
"crelt": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/state": {
|
||||
"version": "6.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.4.tgz",
|
||||
"integrity": "sha512-8y7xqG/hpB53l25CIoit9/ngxdfoG+fx+V3SHBrinnhOtLvKHRyAJJuHzkWrR4YXXLX8eXBsejgAAxHUOdW1yw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@marijn/find-cluster-break": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/view": {
|
||||
"version": "6.39.16",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.39.16.tgz",
|
||||
"integrity": "sha512-m6S22fFpKtOWhq8HuhzsI1WzUP/hB9THbDj0Tl5KX4gbO6Y91hwBl7Yky33NdvB6IffuRFiBxf1R8kJMyXmA4Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.5.0",
|
||||
"crelt": "^1.0.6",
|
||||
"style-mod": "^4.1.0",
|
||||
"w3c-keyname": "^2.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz",
|
||||
|
|
@ -1401,6 +1679,178 @@
|
|||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/common": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.1.tgz",
|
||||
"integrity": "sha512-6YRVG9vBkaY7p1IVxL4s44n5nUnaNnGM2/AckNgYOnxTG2kWh1vR8BMxPseWPjRNpb5VtXnMpeYAEAADoRV1Iw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@lezer/cpp": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/cpp/-/cpp-1.1.5.tgz",
|
||||
"integrity": "sha512-DIhSXmYtJKLehrjzDFN+2cPt547ySQ41nA8yqcDf/GxMc+YM736xqltFkvADL2M0VebU5I+3+4ks2Vv+Kyq3Aw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/css": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.3.1.tgz",
|
||||
"integrity": "sha512-PYAKeUVBo3HFThruRyp/iK91SwiZJnzXh8QzkQlwijB5y+N5iB28+iLk78o2zmKqqV0uolNhCwFqB8LA7b0Svg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/go": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/go/-/go-1.0.1.tgz",
|
||||
"integrity": "sha512-xToRsYxwsgJNHTgNdStpcvmbVuKxTapV0dM0wey1geMMRc9aggoVyKgzYp41D2/vVOx+Ii4hmE206kvxIXBVXQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/highlight": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz",
|
||||
"integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/html": {
|
||||
"version": "1.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.13.tgz",
|
||||
"integrity": "sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/java": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/java/-/java-1.1.3.tgz",
|
||||
"integrity": "sha512-yHquUfujwg6Yu4Fd1GNHCvidIvJwi/1Xu2DaKl/pfWIA2c1oXkVvawH3NyXhCaFx4OdlYBVX5wvz2f7Aoa/4Xw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/javascript": {
|
||||
"version": "1.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.4.tgz",
|
||||
"integrity": "sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.1.3",
|
||||
"@lezer/lr": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/json": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.3.tgz",
|
||||
"integrity": "sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/lr": {
|
||||
"version": "1.4.8",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.8.tgz",
|
||||
"integrity": "sha512-bPWa0Pgx69ylNlMlPvBPryqeLYQjyJjqPx+Aupm5zydLIF3NE+6MMLT8Yi23Bd9cif9VS00aUebn+6fDIGBcDA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/markdown": {
|
||||
"version": "1.6.3",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.6.3.tgz",
|
||||
"integrity": "sha512-jpGm5Ps+XErS+xA4urw7ogEGkeZOahVQF21Z6oECF0sj+2liwZopd2+I8uH5I/vZsRuuze3OxBREIANLf6KKUw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.5.0",
|
||||
"@lezer/highlight": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/php": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/php/-/php-1.0.5.tgz",
|
||||
"integrity": "sha512-W7asp9DhM6q0W6DYNwIkLSKOvxlXRrif+UXBMxzsJUuqmhE7oVU+gS3THO4S/Puh7Xzgm858UNaFi6dxTP8dJA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/python": {
|
||||
"version": "1.1.18",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/python/-/python-1.1.18.tgz",
|
||||
"integrity": "sha512-31FiUrU7z9+d/ElGQLJFXl+dKOdx0jALlP3KEOsGTex8mvj+SoE1FgItcHWK/axkxCHGUSpqIHt6JAWfWu9Rhg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/rust": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/rust/-/rust-1.0.2.tgz",
|
||||
"integrity": "sha512-Lz5sIPBdF2FUXcWeCu1//ojFAZqzTQNRga0aYv6dYXqJqPfMdCAI0NzajWUd4Xijj1IKJLtjoXRPMvTKWBcqKg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/xml": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/xml/-/xml-1.0.6.tgz",
|
||||
"integrity": "sha512-CdDwirL0OEaStFue/66ZmFSeppuL6Dwjlk8qk153mSQwiSH/Dlri4GNymrNWnUmPl2Um7QfV1FO9KFUyX3Twww==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/yaml": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/yaml/-/yaml-1.0.4.tgz",
|
||||
"integrity": "sha512-2lrrHqxalACEbxIbsjhqGpSW8kWpUKuY6RHgnSAFZa6qK62wvnPxA8hGOwOoDbwHcOFs5M4o27mjGu+P7TvBmw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@marijn/find-cluster-break": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz",
|
||||
"integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@pkgjs/parseargs": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
||||
|
|
@ -3502,6 +3952,21 @@
|
|||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/codemirror": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz",
|
||||
"integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
"@codemirror/commands": "^6.0.0",
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/lint": "^6.0.0",
|
||||
"@codemirror/search": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
|
|
@ -3641,6 +4106,12 @@
|
|||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/crelt": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
|
||||
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
|
|
@ -7749,6 +8220,12 @@
|
|||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/style-mod": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz",
|
||||
"integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/supports-color": {
|
||||
"version": "8.1.1",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
|
||||
|
|
@ -8320,6 +8797,12 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/w3c-keyname": {
|
||||
"version": "2.2.8",
|
||||
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
|
||||
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/wait-port": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/wait-port/-/wait-port-1.1.0.tgz",
|
||||
|
|
|
|||
|
|
@ -31,12 +31,27 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.2.70",
|
||||
"@codemirror/lang-cpp": "^6.0.3",
|
||||
"@codemirror/lang-css": "^6.3.1",
|
||||
"@codemirror/lang-go": "^6.0.1",
|
||||
"@codemirror/lang-html": "^6.4.11",
|
||||
"@codemirror/lang-java": "^6.0.2",
|
||||
"@codemirror/lang-javascript": "^6.2.5",
|
||||
"@codemirror/lang-json": "^6.0.2",
|
||||
"@codemirror/lang-markdown": "^6.5.0",
|
||||
"@codemirror/lang-php": "^6.0.2",
|
||||
"@codemirror/lang-python": "^6.2.1",
|
||||
"@codemirror/lang-rust": "^6.0.2",
|
||||
"@codemirror/lang-sql": "^6.10.0",
|
||||
"@codemirror/lang-xml": "^6.1.0",
|
||||
"@codemirror/lang-yaml": "^6.1.2",
|
||||
"@tauri-apps/api": "^2.10.1",
|
||||
"@tauri-apps/plugin-dialog": "^2.6.0",
|
||||
"@tauri-apps/plugin-updater": "^2.10.0",
|
||||
"@xterm/addon-canvas": "^0.7.0",
|
||||
"@xterm/addon-fit": "^0.11.0",
|
||||
"@xterm/xterm": "^6.0.0",
|
||||
"codemirror": "^6.0.2",
|
||||
"marked": "^17.0.4",
|
||||
"shiki": "^4.0.1"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -566,6 +566,19 @@ fn read_file_content(path: String) -> Result<FileContent, String> {
|
|||
Ok(FileContent::Text { content, lang })
|
||||
}
|
||||
|
||||
// --- Write file ---
|
||||
|
||||
#[tauri::command]
|
||||
fn write_file_content(path: String, content: String) -> Result<(), String> {
|
||||
let file_path = std::path::Path::new(&path);
|
||||
// Safety: only write to existing files (no arbitrary path creation)
|
||||
if !file_path.is_file() {
|
||||
return Err(format!("Not an existing file: {path}"));
|
||||
}
|
||||
std::fs::write(&path, content.as_bytes())
|
||||
.map_err(|e| format!("Failed to write file: {e}"))
|
||||
}
|
||||
|
||||
// Directory picker: custom rfd command with parent window for modal behavior on Linux
|
||||
#[tauri::command]
|
||||
async fn pick_directory(window: tauri::Window) -> Result<Option<String>, String> {
|
||||
|
|
@ -743,6 +756,7 @@ pub fn run() {
|
|||
discover_markdown_files,
|
||||
list_directory_children,
|
||||
read_file_content,
|
||||
write_file_content,
|
||||
agent_messages_save,
|
||||
agent_messages_load,
|
||||
project_agent_state_save,
|
||||
|
|
|
|||
|
|
@ -20,3 +20,7 @@ export function listDirectoryChildren(path: string): Promise<DirEntry[]> {
|
|||
export function readFileContent(path: string): Promise<FileContent> {
|
||||
return invoke<FileContent>('read_file_content', { path });
|
||||
}
|
||||
|
||||
export function writeFileContent(path: string, content: string): Promise<void> {
|
||||
return invoke<void>('write_file_content', { path, content });
|
||||
}
|
||||
|
|
|
|||
330
v2/src/lib/components/Workspace/CodeEditor.svelte
Normal file
330
v2/src/lib/components/Workspace/CodeEditor.svelte
Normal file
|
|
@ -0,0 +1,330 @@
|
|||
<script lang="ts">
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { EditorView, keymap, lineNumbers, highlightActiveLine, highlightActiveLineGutter, drawSelection, rectangularSelection, crosshairCursor, dropCursor } from '@codemirror/view';
|
||||
import { EditorState } from '@codemirror/state';
|
||||
import { defaultKeymap, history, historyKeymap, indentWithTab } from '@codemirror/commands';
|
||||
import { syntaxHighlighting, defaultHighlightStyle, indentOnInput, bracketMatching, foldGutter, foldKeymap } from '@codemirror/language';
|
||||
import { closeBrackets, closeBracketsKeymap, autocompletion, completionKeymap } from '@codemirror/autocomplete';
|
||||
import { highlightSelectionMatches, searchKeymap } from '@codemirror/search';
|
||||
|
||||
interface Props {
|
||||
content: string;
|
||||
lang: string;
|
||||
onchange?: (content: string) => void;
|
||||
onsave?: () => void;
|
||||
onblur?: () => void;
|
||||
}
|
||||
|
||||
let { content, lang, onchange, onsave, onblur }: Props = $props();
|
||||
|
||||
let container: HTMLDivElement | undefined = $state();
|
||||
let view: EditorView | undefined = $state();
|
||||
|
||||
// Map lang hint to CodeMirror language extension
|
||||
async function getLangExtension(lang: string) {
|
||||
switch (lang) {
|
||||
case 'javascript':
|
||||
case 'jsx': {
|
||||
const { javascript } = await import('@codemirror/lang-javascript');
|
||||
return javascript({ jsx: true });
|
||||
}
|
||||
case 'typescript':
|
||||
case 'tsx': {
|
||||
const { javascript } = await import('@codemirror/lang-javascript');
|
||||
return javascript({ jsx: true, typescript: true });
|
||||
}
|
||||
case 'html':
|
||||
case 'svelte': {
|
||||
const { html } = await import('@codemirror/lang-html');
|
||||
return html();
|
||||
}
|
||||
case 'css':
|
||||
case 'scss':
|
||||
case 'less': {
|
||||
const { css } = await import('@codemirror/lang-css');
|
||||
return css();
|
||||
}
|
||||
case 'json': {
|
||||
const { json } = await import('@codemirror/lang-json');
|
||||
return json();
|
||||
}
|
||||
case 'markdown': {
|
||||
const { markdown } = await import('@codemirror/lang-markdown');
|
||||
return markdown();
|
||||
}
|
||||
case 'python': {
|
||||
const { python } = await import('@codemirror/lang-python');
|
||||
return python();
|
||||
}
|
||||
case 'rust': {
|
||||
const { rust } = await import('@codemirror/lang-rust');
|
||||
return rust();
|
||||
}
|
||||
case 'xml': {
|
||||
const { xml } = await import('@codemirror/lang-xml');
|
||||
return xml();
|
||||
}
|
||||
case 'sql': {
|
||||
const { sql } = await import('@codemirror/lang-sql');
|
||||
return sql();
|
||||
}
|
||||
case 'yaml': {
|
||||
const { yaml } = await import('@codemirror/lang-yaml');
|
||||
return yaml();
|
||||
}
|
||||
case 'cpp':
|
||||
case 'c':
|
||||
case 'h': {
|
||||
const { cpp } = await import('@codemirror/lang-cpp');
|
||||
return cpp();
|
||||
}
|
||||
case 'java': {
|
||||
const { java } = await import('@codemirror/lang-java');
|
||||
return java();
|
||||
}
|
||||
case 'php': {
|
||||
const { php } = await import('@codemirror/lang-php');
|
||||
return php();
|
||||
}
|
||||
case 'go': {
|
||||
const { go } = await import('@codemirror/lang-go');
|
||||
return go();
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Catppuccin Mocha-inspired theme that reads CSS custom properties
|
||||
const catppuccinTheme = EditorView.theme({
|
||||
'&': {
|
||||
backgroundColor: 'var(--ctp-base)',
|
||||
color: 'var(--ctp-text)',
|
||||
fontFamily: 'var(--term-font-family, "JetBrains Mono", monospace)',
|
||||
fontSize: '0.775rem',
|
||||
},
|
||||
'.cm-content': {
|
||||
caretColor: 'var(--ctp-rosewater)',
|
||||
lineHeight: '1.55',
|
||||
},
|
||||
'.cm-cursor, .cm-dropCursor': {
|
||||
borderLeftColor: 'var(--ctp-rosewater)',
|
||||
},
|
||||
'&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection': {
|
||||
backgroundColor: 'color-mix(in srgb, var(--ctp-blue) 25%, transparent)',
|
||||
},
|
||||
'.cm-panels': {
|
||||
backgroundColor: 'var(--ctp-mantle)',
|
||||
color: 'var(--ctp-text)',
|
||||
},
|
||||
'.cm-panels.cm-panels-top': {
|
||||
borderBottom: '1px solid var(--ctp-surface0)',
|
||||
},
|
||||
'.cm-searchMatch': {
|
||||
backgroundColor: 'color-mix(in srgb, var(--ctp-yellow) 25%, transparent)',
|
||||
},
|
||||
'.cm-searchMatch.cm-searchMatch-selected': {
|
||||
backgroundColor: 'color-mix(in srgb, var(--ctp-peach) 30%, transparent)',
|
||||
},
|
||||
'.cm-activeLine': {
|
||||
backgroundColor: 'color-mix(in srgb, var(--ctp-surface0) 40%, transparent)',
|
||||
},
|
||||
'.cm-selectionMatch': {
|
||||
backgroundColor: 'color-mix(in srgb, var(--ctp-teal) 15%, transparent)',
|
||||
},
|
||||
'.cm-matchingBracket, .cm-nonmatchingBracket': {
|
||||
backgroundColor: 'color-mix(in srgb, var(--ctp-blue) 20%, transparent)',
|
||||
outline: '1px solid color-mix(in srgb, var(--ctp-blue) 40%, transparent)',
|
||||
},
|
||||
'.cm-gutters': {
|
||||
backgroundColor: 'var(--ctp-mantle)',
|
||||
color: 'var(--ctp-overlay0)',
|
||||
border: 'none',
|
||||
borderRight: '1px solid var(--ctp-surface0)',
|
||||
},
|
||||
'.cm-activeLineGutter': {
|
||||
backgroundColor: 'color-mix(in srgb, var(--ctp-surface0) 40%, transparent)',
|
||||
color: 'var(--ctp-text)',
|
||||
},
|
||||
'.cm-foldPlaceholder': {
|
||||
backgroundColor: 'var(--ctp-surface0)',
|
||||
border: 'none',
|
||||
color: 'var(--ctp-overlay1)',
|
||||
},
|
||||
'.cm-tooltip': {
|
||||
backgroundColor: 'var(--ctp-surface0)',
|
||||
color: 'var(--ctp-text)',
|
||||
border: '1px solid var(--ctp-surface1)',
|
||||
borderRadius: '0.25rem',
|
||||
},
|
||||
'.cm-tooltip .cm-tooltip-arrow:before': {
|
||||
borderTopColor: 'var(--ctp-surface1)',
|
||||
borderBottomColor: 'var(--ctp-surface1)',
|
||||
},
|
||||
'.cm-tooltip .cm-tooltip-arrow:after': {
|
||||
borderTopColor: 'var(--ctp-surface0)',
|
||||
borderBottomColor: 'var(--ctp-surface0)',
|
||||
},
|
||||
'.cm-tooltip-autocomplete': {
|
||||
'& > ul > li[aria-selected]': {
|
||||
backgroundColor: 'color-mix(in srgb, var(--ctp-blue) 20%, transparent)',
|
||||
color: 'var(--ctp-text)',
|
||||
},
|
||||
},
|
||||
}, { dark: true });
|
||||
|
||||
async function createEditor() {
|
||||
if (!container) return;
|
||||
|
||||
const langExt = await getLangExtension(lang);
|
||||
|
||||
const extensions = [
|
||||
lineNumbers(),
|
||||
highlightActiveLineGutter(),
|
||||
history(),
|
||||
foldGutter(),
|
||||
drawSelection(),
|
||||
dropCursor(),
|
||||
EditorState.allowMultipleSelections.of(true),
|
||||
indentOnInput(),
|
||||
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
|
||||
bracketMatching(),
|
||||
closeBrackets(),
|
||||
autocompletion(),
|
||||
rectangularSelection(),
|
||||
crosshairCursor(),
|
||||
highlightActiveLine(),
|
||||
highlightSelectionMatches(),
|
||||
keymap.of([
|
||||
...closeBracketsKeymap,
|
||||
...defaultKeymap,
|
||||
...searchKeymap,
|
||||
...historyKeymap,
|
||||
...foldKeymap,
|
||||
...completionKeymap,
|
||||
indentWithTab,
|
||||
{ key: 'Mod-s', run: () => { onsave?.(); return true; } },
|
||||
]),
|
||||
catppuccinTheme,
|
||||
EditorView.updateListener.of(update => {
|
||||
if (update.docChanged) {
|
||||
onchange?.(update.state.doc.toString());
|
||||
}
|
||||
}),
|
||||
EditorView.domEventHandlers({
|
||||
blur: () => { onblur?.(); },
|
||||
}),
|
||||
EditorView.lineWrapping,
|
||||
];
|
||||
|
||||
if (langExt) extensions.push(langExt);
|
||||
|
||||
view = new EditorView({
|
||||
state: EditorState.create({ doc: content, extensions }),
|
||||
parent: container,
|
||||
});
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
createEditor();
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
view?.destroy();
|
||||
});
|
||||
|
||||
// When content prop changes externally (different file loaded), replace editor content
|
||||
let lastContent = content;
|
||||
$effect(() => {
|
||||
if (view && content !== lastContent) {
|
||||
const currentDoc = view.state.doc.toString();
|
||||
if (content !== currentDoc) {
|
||||
view.dispatch({
|
||||
changes: { from: 0, to: view.state.doc.length, insert: content },
|
||||
});
|
||||
}
|
||||
lastContent = content;
|
||||
}
|
||||
});
|
||||
|
||||
// When lang changes, recreate editor
|
||||
let lastLang = lang;
|
||||
$effect(() => {
|
||||
if (lang !== lastLang && view) {
|
||||
lastLang = lang;
|
||||
const currentContent = view.state.doc.toString();
|
||||
view.destroy();
|
||||
// Small delay to let DOM settle
|
||||
queueMicrotask(async () => {
|
||||
const langExt = await getLangExtension(lang);
|
||||
const extensions = [
|
||||
lineNumbers(),
|
||||
highlightActiveLineGutter(),
|
||||
history(),
|
||||
foldGutter(),
|
||||
drawSelection(),
|
||||
dropCursor(),
|
||||
EditorState.allowMultipleSelections.of(true),
|
||||
indentOnInput(),
|
||||
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
|
||||
bracketMatching(),
|
||||
closeBrackets(),
|
||||
autocompletion(),
|
||||
rectangularSelection(),
|
||||
crosshairCursor(),
|
||||
highlightActiveLine(),
|
||||
highlightSelectionMatches(),
|
||||
keymap.of([
|
||||
...closeBracketsKeymap,
|
||||
...defaultKeymap,
|
||||
...searchKeymap,
|
||||
...historyKeymap,
|
||||
...foldKeymap,
|
||||
...completionKeymap,
|
||||
indentWithTab,
|
||||
{ key: 'Mod-s', run: () => { onsave?.(); return true; } },
|
||||
]),
|
||||
catppuccinTheme,
|
||||
EditorView.updateListener.of(update => {
|
||||
if (update.docChanged) {
|
||||
onchange?.(update.state.doc.toString());
|
||||
}
|
||||
}),
|
||||
EditorView.domEventHandlers({
|
||||
blur: () => { onblur?.(); },
|
||||
}),
|
||||
EditorView.lineWrapping,
|
||||
];
|
||||
if (langExt) extensions.push(langExt);
|
||||
view = new EditorView({
|
||||
state: EditorState.create({ doc: currentContent, extensions }),
|
||||
parent: container!,
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export function getContent(): string {
|
||||
return view?.state.doc.toString() ?? content;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="code-editor" bind:this={container}></div>
|
||||
|
||||
<style>
|
||||
.code-editor {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.code-editor :global(.cm-editor) {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.code-editor :global(.cm-scroller) {
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
<script lang="ts">
|
||||
import { listDirectoryChildren, readFileContent, type DirEntry, type FileContent } from '../../adapters/files-bridge';
|
||||
import { getHighlighter, highlightCode, escapeHtml } from '../../utils/highlight';
|
||||
import { listDirectoryChildren, readFileContent, writeFileContent, type DirEntry, type FileContent } from '../../adapters/files-bridge';
|
||||
import { getSetting } from '../../adapters/settings-bridge';
|
||||
import { convertFileSrc } from '@tauri-apps/api/core';
|
||||
import CodeEditor from './CodeEditor.svelte';
|
||||
|
||||
interface Props {
|
||||
cwd: string;
|
||||
|
|
@ -22,11 +23,12 @@
|
|||
name: string;
|
||||
pinned: boolean;
|
||||
content: FileContent | null;
|
||||
dirty: boolean;
|
||||
editContent: string; // current editor content (may differ from saved)
|
||||
}
|
||||
|
||||
let roots = $state<TreeNode[]>([]);
|
||||
let expandedPaths = $state<Set<string>>(new Set());
|
||||
let highlighterReady = $state(false);
|
||||
|
||||
// Tab state: open file tabs + active tab
|
||||
let fileTabs = $state<FileTab[]>([]);
|
||||
|
|
@ -38,16 +40,21 @@
|
|||
let sidebarWidth = $state(14); // rem
|
||||
let resizing = $state(false);
|
||||
|
||||
// Settings
|
||||
let saveOnBlur = $state(false);
|
||||
|
||||
// Derived: active tab's content
|
||||
let activeTab = $derived(fileTabs.find(t => t.path === activeTabPath) ?? null);
|
||||
|
||||
// Load root directory
|
||||
// Load root directory + settings
|
||||
$effect(() => {
|
||||
const dir = cwd;
|
||||
loadDirectory(dir).then(entries => {
|
||||
roots = entries.map(e => ({ ...e, depth: 0 }));
|
||||
});
|
||||
getHighlighter().then(() => { highlighterReady = true; });
|
||||
getSetting('files_save_on_blur').then(v => {
|
||||
saveOnBlur = v === 'true';
|
||||
});
|
||||
});
|
||||
|
||||
async function loadDirectory(path: string): Promise<DirEntry[]> {
|
||||
|
|
@ -96,6 +103,8 @@
|
|||
name: node.name,
|
||||
pinned: false,
|
||||
content: null,
|
||||
dirty: false,
|
||||
editContent: '',
|
||||
};
|
||||
|
||||
if (existing) {
|
||||
|
|
@ -116,7 +125,10 @@
|
|||
try {
|
||||
const content = await readFileContent(node.path);
|
||||
const target = fileTabs.find(t => t.path === node.path);
|
||||
if (target) target.content = content;
|
||||
if (target) {
|
||||
target.content = content;
|
||||
target.editContent = content.type === 'Text' ? content.content : '';
|
||||
}
|
||||
} catch (e) {
|
||||
const target = fileTabs.find(t => t.path === node.path);
|
||||
if (target) target.content = { type: 'Binary', message: `Error: ${e}` };
|
||||
|
|
@ -142,6 +154,11 @@
|
|||
}
|
||||
|
||||
function closeTab(path: string) {
|
||||
const tab = fileTabs.find(t => t.path === path);
|
||||
if (tab?.dirty) {
|
||||
// Save before closing if dirty
|
||||
saveTab(tab);
|
||||
}
|
||||
fileTabs = fileTabs.filter(t => t.path !== path);
|
||||
if (activeTabPath === path) {
|
||||
activeTabPath = fileTabs[fileTabs.length - 1]?.path ?? null;
|
||||
|
|
@ -184,20 +201,44 @@
|
|||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
}
|
||||
|
||||
function renderHighlighted(content: string, lang: string): string {
|
||||
if (!highlighterReady || lang === 'text' || lang === 'csv') {
|
||||
return `<pre><code>${escapeHtml(content)}</code></pre>`;
|
||||
}
|
||||
const highlighted = highlightCode(content, lang);
|
||||
if (highlighted !== escapeHtml(content)) return highlighted;
|
||||
return `<pre><code>${escapeHtml(content)}</code></pre>`;
|
||||
}
|
||||
|
||||
function isImageExt(path: string): boolean {
|
||||
const ext = path.split('.').pop()?.toLowerCase() ?? '';
|
||||
return ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg', 'ico', 'bmp'].includes(ext);
|
||||
}
|
||||
|
||||
// Editor change handler
|
||||
function handleEditorChange(tabPath: string, newContent: string) {
|
||||
const tab = fileTabs.find(t => t.path === tabPath);
|
||||
if (!tab || tab.content?.type !== 'Text') return;
|
||||
tab.editContent = newContent;
|
||||
tab.dirty = newContent !== tab.content.content;
|
||||
}
|
||||
|
||||
// Save a tab to disk
|
||||
async function saveTab(tab: FileTab) {
|
||||
if (!tab.dirty || tab.content?.type !== 'Text') return;
|
||||
try {
|
||||
await writeFileContent(tab.path, tab.editContent);
|
||||
// Update the saved content reference
|
||||
tab.content = { type: 'Text', content: tab.editContent, lang: tab.content.lang };
|
||||
tab.dirty = false;
|
||||
} catch (e) {
|
||||
console.warn('Failed to save file:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Save active tab
|
||||
function saveActiveTab() {
|
||||
if (activeTab?.dirty) saveTab(activeTab);
|
||||
}
|
||||
|
||||
// Blur handler: save if setting enabled
|
||||
function handleEditorBlur(tabPath: string) {
|
||||
if (!saveOnBlur) return;
|
||||
const tab = fileTabs.find(t => t.path === tabPath);
|
||||
if (tab?.dirty) saveTab(tab);
|
||||
}
|
||||
|
||||
// Drag-resize sidebar
|
||||
function startResize(e: MouseEvent) {
|
||||
e.preventDefault();
|
||||
|
|
@ -279,10 +320,13 @@
|
|||
class:preview={!tab.pinned}
|
||||
onclick={() => activeTabPath = tab.path}
|
||||
ondblclick={() => { tab.pinned = true; }}
|
||||
onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); activeTabPath = tab.path; } }}
|
||||
role="tab"
|
||||
tabindex="0"
|
||||
>
|
||||
<span class="file-tab-name" class:italic={!tab.pinned}>{tab.name}</span>
|
||||
<span class="file-tab-name" class:italic={!tab.pinned}>
|
||||
{tab.name}{#if tab.dirty}<span class="dirty-dot"></span>{/if}
|
||||
</span>
|
||||
<button class="file-tab-close" onclick={(e) => { e.stopPropagation(); closeTab(tab.path); }}>×</button>
|
||||
</div>
|
||||
{/each}
|
||||
|
|
@ -308,17 +352,24 @@
|
|||
<div class="viewer-state">{activeTab.content.message}</div>
|
||||
{/if}
|
||||
{:else if activeTab.content?.type === 'Text'}
|
||||
<div class="viewer-code">
|
||||
{#if activeTab.content.lang === 'csv'}
|
||||
<pre class="csv-content"><code>{activeTab.content.content}</code></pre>
|
||||
{:else}
|
||||
{@html renderHighlighted(activeTab.content.content, activeTab.content.lang)}
|
||||
{/if}
|
||||
</div>
|
||||
{#key activeTabPath}
|
||||
<CodeEditor
|
||||
content={activeTab.editContent}
|
||||
lang={activeTab.content.lang}
|
||||
onchange={(c) => handleEditorChange(activeTab!.path, c)}
|
||||
onsave={saveActiveTab}
|
||||
onblur={() => handleEditorBlur(activeTab!.path)}
|
||||
/>
|
||||
{/key}
|
||||
{/if}
|
||||
|
||||
{#if activeTab}
|
||||
<div class="viewer-path">{activeTab.path}</div>
|
||||
<div class="viewer-path">
|
||||
{activeTab.path}
|
||||
{#if activeTab.dirty}
|
||||
<span class="path-dirty">(unsaved)</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</main>
|
||||
</div>
|
||||
|
|
@ -517,12 +568,24 @@
|
|||
.file-tab-name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.file-tab-name.italic {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.dirty-dot {
|
||||
display: inline-block;
|
||||
width: 0.375rem;
|
||||
height: 0.375rem;
|
||||
border-radius: 50%;
|
||||
background: var(--ctp-peach);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.file-tab-close {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -581,57 +644,6 @@
|
|||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.viewer-code {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
|
||||
.viewer-code :global(pre) {
|
||||
margin: 0;
|
||||
font-family: var(--term-font-family, 'JetBrains Mono', monospace);
|
||||
font-size: 0.775rem;
|
||||
line-height: 1.55;
|
||||
color: var(--ctp-text);
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.viewer-code :global(code) {
|
||||
font-family: inherit;
|
||||
background: none;
|
||||
padding: 0;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.viewer-code :global(.shiki) {
|
||||
background: transparent !important;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
white-space: pre-wrap !important;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.viewer-code :global(.shiki code) {
|
||||
white-space: pre-wrap !important;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.csv-content {
|
||||
font-family: var(--term-font-family, monospace);
|
||||
font-size: 0.75rem;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
tab-size: 4;
|
||||
}
|
||||
|
||||
.viewer-image {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
|
|
@ -655,5 +667,13 @@
|
|||
font-family: var(--term-font-family, monospace);
|
||||
color: var(--ctp-overlay0);
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.path-dirty {
|
||||
color: var(--ctp-peach);
|
||||
font-size: 0.6rem;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue