feat(files): add CodeMirror 6 editor with save, dirty tracking, and 15 language modes

This commit is contained in:
Hibryda 2026-03-10 03:11:32 +01:00
parent 0ffbd93b8b
commit 3bb972fc01
6 changed files with 941 additions and 75 deletions

483
v2/package-lock.json generated
View file

@ -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",

View file

@ -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"
}

View file

@ -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,

View file

@ -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 });
}

View 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>

View file

@ -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>