From 392243f54fd72527dcb76a1ce8fd3e66ed9d213d Mon Sep 17 00:00:00 2001 From: Hikari Date: Wed, 28 Jan 2026 16:12:51 -0800 Subject: [PATCH] feat: add built-in file editor with syntax highlighting - Add CodeMirror 6 editor with support for 40+ languages - Add file browser sidebar with directory tree navigation - Add multi-tab support with dirty state indicators - Add keyboard shortcuts (Ctrl+E, Ctrl+B, Ctrl+S, Ctrl+W) - Add editor toggle button to status bar - Editor uses current session's CWD and requires connection - Add Tauri commands for file operations (list, read, write) --- package.json | 26 + pnpm-lock.yaml | 549 ++++++++++++++++++ src-tauri/src/commands.rs | 69 +++ src-tauri/src/lib.rs | 3 + .../components/KeyboardShortcutsModal.svelte | 10 + src/lib/components/StatusBar.svelte | 29 + src/lib/components/editor/CodeEditor.svelte | 179 ++++++ src/lib/components/editor/EditorPanel.svelte | 253 ++++++++ src/lib/components/editor/EditorTabs.svelte | 170 ++++++ src/lib/components/editor/FileBrowser.svelte | 151 +++++ src/lib/components/editor/FileTreeItem.svelte | 195 +++++++ src/lib/stores/editor.ts | 285 +++++++++ src/lib/types/editor.ts | 27 + src/routes/+page.svelte | 72 ++- 14 files changed, 2015 insertions(+), 3 deletions(-) create mode 100644 src/lib/components/editor/CodeEditor.svelte create mode 100644 src/lib/components/editor/EditorPanel.svelte create mode 100644 src/lib/components/editor/EditorTabs.svelte create mode 100644 src/lib/components/editor/FileBrowser.svelte create mode 100644 src/lib/components/editor/FileTreeItem.svelte create mode 100644 src/lib/stores/editor.ts create mode 100644 src/lib/types/editor.ts diff --git a/package.json b/package.json index 709c615..2305e62 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,31 @@ }, "license": "MIT", "dependencies": { + "@codemirror/lang-angular": "^0.1.4", + "@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.4", + "@codemirror/lang-json": "^6.0.2", + "@codemirror/lang-less": "^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-sass": "^6.0.2", + "@codemirror/lang-sql": "^6.10.0", + "@codemirror/lang-vue": "^0.1.3", + "@codemirror/lang-wast": "^6.0.2", + "@codemirror/lang-xml": "^6.1.0", + "@codemirror/lang-yaml": "^6.1.2", + "@codemirror/language": "^6.12.1", + "@codemirror/legacy-modes": "^6.5.2", + "@codemirror/state": "^6.5.4", + "@codemirror/theme-one-dark": "^6.1.3", + "@codemirror/view": "^6.39.11", + "@lezer/highlight": "^1.2.3", "@tauri-apps/api": "^2", "@tauri-apps/plugin-clipboard-manager": "^2.3.2", "@tauri-apps/plugin-dialog": "^2", @@ -36,6 +61,7 @@ "@tauri-apps/plugin-os": "^2", "@tauri-apps/plugin-shell": "^2.3.4", "@tauri-apps/plugin-store": "^2", + "codemirror": "^6.0.2", "highlight.js": "^11.11.1", "marked": "^17.0.1" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 83f6404..debd5b9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,81 @@ importers: .: dependencies: + '@codemirror/lang-angular': + specifier: ^0.1.4 + version: 0.1.4 + '@codemirror/lang-cpp': + specifier: ^6.0.3 + version: 6.0.3 + '@codemirror/lang-css': + specifier: ^6.3.1 + version: 6.3.1 + '@codemirror/lang-go': + specifier: ^6.0.1 + version: 6.0.1 + '@codemirror/lang-html': + specifier: ^6.4.11 + version: 6.4.11 + '@codemirror/lang-java': + specifier: ^6.0.2 + version: 6.0.2 + '@codemirror/lang-javascript': + specifier: ^6.2.4 + version: 6.2.4 + '@codemirror/lang-json': + specifier: ^6.0.2 + version: 6.0.2 + '@codemirror/lang-less': + specifier: ^6.0.2 + version: 6.0.2 + '@codemirror/lang-markdown': + specifier: ^6.5.0 + version: 6.5.0 + '@codemirror/lang-php': + specifier: ^6.0.2 + version: 6.0.2 + '@codemirror/lang-python': + specifier: ^6.2.1 + version: 6.2.1 + '@codemirror/lang-rust': + specifier: ^6.0.2 + version: 6.0.2 + '@codemirror/lang-sass': + specifier: ^6.0.2 + version: 6.0.2 + '@codemirror/lang-sql': + specifier: ^6.10.0 + version: 6.10.0 + '@codemirror/lang-vue': + specifier: ^0.1.3 + version: 0.1.3 + '@codemirror/lang-wast': + specifier: ^6.0.2 + version: 6.0.2 + '@codemirror/lang-xml': + specifier: ^6.1.0 + version: 6.1.0 + '@codemirror/lang-yaml': + specifier: ^6.1.2 + version: 6.1.2 + '@codemirror/language': + specifier: ^6.12.1 + version: 6.12.1 + '@codemirror/legacy-modes': + specifier: ^6.5.2 + version: 6.5.2 + '@codemirror/state': + specifier: ^6.5.4 + version: 6.5.4 + '@codemirror/theme-one-dark': + specifier: ^6.1.3 + version: 6.1.3 + '@codemirror/view': + specifier: ^6.39.11 + version: 6.39.11 + '@lezer/highlight': + specifier: ^1.2.3 + version: 1.2.3 '@tauri-apps/api': specifier: ^2 version: 2.9.1 @@ -35,6 +110,9 @@ importers: '@tauri-apps/plugin-store': specifier: ^2 version: 2.4.2 + codemirror: + specifier: ^6.0.2 + version: 6.0.2 highlight.js: specifier: ^11.11.1 version: 11.11.1 @@ -158,6 +236,90 @@ packages: resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} + '@codemirror/autocomplete@6.20.0': + resolution: {integrity: sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg==} + + '@codemirror/commands@6.10.1': + resolution: {integrity: sha512-uWDWFypNdQmz2y1LaNJzK7fL7TYKLeUAU0npEC685OKTF3KcQ2Vu3klIM78D7I6wGhktme0lh3CuQLv0ZCrD9Q==} + + '@codemirror/lang-angular@0.1.4': + resolution: {integrity: sha512-oap+gsltb/fzdlTQWD6BFF4bSLKcDnlxDsLdePiJpCVNKWXSTAbiiQeYI3UmES+BLAdkmIC1WjyztC1pi/bX4g==} + + '@codemirror/lang-cpp@6.0.3': + resolution: {integrity: sha512-URM26M3vunFFn9/sm6rzqrBzDgfWuDixp85uTY49wKudToc2jTHUrKIGGKs+QWND+YLofNNZpxcNGRynFJfvgA==} + + '@codemirror/lang-css@6.3.1': + resolution: {integrity: sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==} + + '@codemirror/lang-go@6.0.1': + resolution: {integrity: sha512-7fNvbyNylvqCphW9HD6WFnRpcDjr+KXX/FgqXy5H5ZS0eC5edDljukm/yNgYkwTsgp2busdod50AOTIy6Jikfg==} + + '@codemirror/lang-html@6.4.11': + resolution: {integrity: sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw==} + + '@codemirror/lang-java@6.0.2': + resolution: {integrity: sha512-m5Nt1mQ/cznJY7tMfQTJchmrjdjQ71IDs+55d1GAa8DGaB8JXWsVCkVT284C3RTASaY43YknrK2X3hPO/J3MOQ==} + + '@codemirror/lang-javascript@6.2.4': + resolution: {integrity: sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA==} + + '@codemirror/lang-json@6.0.2': + resolution: {integrity: sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==} + + '@codemirror/lang-less@6.0.2': + resolution: {integrity: sha512-EYdQTG22V+KUUk8Qq582g7FMnCZeEHsyuOJisHRft/mQ+ZSZ2w51NupvDUHiqtsOy7It5cHLPGfHQLpMh9bqpQ==} + + '@codemirror/lang-markdown@6.5.0': + resolution: {integrity: sha512-0K40bZ35jpHya6FriukbgaleaqzBLZfOh7HuzqbMxBXkbYMJDxfF39c23xOgxFezR+3G+tR2/Mup+Xk865OMvw==} + + '@codemirror/lang-php@6.0.2': + resolution: {integrity: sha512-ZKy2v1n8Fc8oEXj0Th0PUMXzQJ0AIR6TaZU+PbDHExFwdu+guzOA4jmCHS1Nz4vbFezwD7LyBdDnddSJeScMCA==} + + '@codemirror/lang-python@6.2.1': + resolution: {integrity: sha512-IRjC8RUBhn9mGR9ywecNhB51yePWCGgvHfY1lWN/Mrp3cKuHr0isDKia+9HnvhiWNnMpbGhWrkhuWOc09exRyw==} + + '@codemirror/lang-rust@6.0.2': + resolution: {integrity: sha512-EZaGjCUegtiU7kSMvOfEZpaCReowEf3yNidYu7+vfuGTm9ow4mthAparY5hisJqOHmJowVH3Upu+eJlUji6qqA==} + + '@codemirror/lang-sass@6.0.2': + resolution: {integrity: sha512-l/bdzIABvnTo1nzdY6U+kPAC51czYQcOErfzQ9zSm9D8GmNPD0WTW8st/CJwBTPLO8jlrbyvlSEcN20dc4iL0Q==} + + '@codemirror/lang-sql@6.10.0': + resolution: {integrity: sha512-6ayPkEd/yRw0XKBx5uAiToSgGECo/GY2NoJIHXIIQh1EVwLuKoU8BP/qK0qH5NLXAbtJRLuT73hx7P9X34iO4w==} + + '@codemirror/lang-vue@0.1.3': + resolution: {integrity: sha512-QSKdtYTDRhEHCfo5zOShzxCmqKJvgGrZwDQSdbvCRJ5pRLWBS7pD/8e/tH44aVQT6FKm0t6RVNoSUWHOI5vNug==} + + '@codemirror/lang-wast@6.0.2': + resolution: {integrity: sha512-Imi2KTpVGm7TKuUkqyJ5NRmeFWF7aMpNiwHnLQe0x9kmrxElndyH0K6H/gXtWwY6UshMRAhpENsgfpSwsgmC6Q==} + + '@codemirror/lang-xml@6.1.0': + resolution: {integrity: sha512-3z0blhicHLfwi2UgkZYRPioSgVTo9PV5GP5ducFH6FaHy0IAJRg+ixj5gTR1gnT/glAIC8xv4w2VL1LoZfs+Jg==} + + '@codemirror/lang-yaml@6.1.2': + resolution: {integrity: sha512-dxrfG8w5Ce/QbT7YID7mWZFKhdhsaTNOYjOkSIMt1qmC4VQnXSDSYVHHHn8k6kJUfIhtLo8t1JJgltlxWdsITw==} + + '@codemirror/language@6.12.1': + resolution: {integrity: sha512-Fa6xkSiuGKc8XC8Cn96T+TQHYj4ZZ7RdFmXA3i9xe/3hLHfwPZdM+dqfX0Cp0zQklBKhVD8Yzc8LS45rkqcwpQ==} + + '@codemirror/legacy-modes@6.5.2': + resolution: {integrity: sha512-/jJbwSTazlQEDOQw2FJ8LEEKVS72pU0lx6oM54kGpL8t/NJ2Jda3CZ4pcltiKTdqYSRk3ug1B3pil1gsjA6+8Q==} + + '@codemirror/lint@6.9.3': + resolution: {integrity: sha512-y3YkYhdnhjDBAe0VIA0c4wVoFOvnp8CnAvfLqi0TqotIv92wIlAAP7HELOpLBsKwjAX6W92rSflA6an/2zBvXw==} + + '@codemirror/search@6.6.0': + resolution: {integrity: sha512-koFuNXcDvyyotWcgOnZGmY7LZqEOXZaaxD/j6n18TCLx2/9HieZJ5H6hs1g8FiRxBD0DNfs0nXn17g872RmYdw==} + + '@codemirror/state@6.5.4': + resolution: {integrity: sha512-8y7xqG/hpB53l25CIoit9/ngxdfoG+fx+V3SHBrinnhOtLvKHRyAJJuHzkWrR4YXXLX8eXBsejgAAxHUOdW1yw==} + + '@codemirror/theme-one-dark@6.1.3': + resolution: {integrity: sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==} + + '@codemirror/view@6.39.11': + resolution: {integrity: sha512-bWdeR8gWM87l4DB/kYSF9A+dVackzDb/V56Tq7QVrQ7rn86W0rgZFtlL3g3pem6AeGcb9NQNoy3ao4WpW4h5tQ==} + '@csstools/color-helpers@5.1.0': resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} engines: {node: '>=18'} @@ -425,6 +587,60 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@lezer/common@1.5.0': + resolution: {integrity: sha512-PNGcolp9hr4PJdXR4ix7XtixDrClScvtSCYW3rQG106oVMOOI+jFb+0+J3mbeL/53g1Zd6s0kJzaw6Ri68GmAA==} + + '@lezer/cpp@1.1.5': + resolution: {integrity: sha512-DIhSXmYtJKLehrjzDFN+2cPt547ySQ41nA8yqcDf/GxMc+YM736xqltFkvADL2M0VebU5I+3+4ks2Vv+Kyq3Aw==} + + '@lezer/css@1.3.0': + resolution: {integrity: sha512-pBL7hup88KbI7hXnZV3PQsn43DHy6TWyzuyk2AO9UyoXcDltvIdqWKE1dLL/45JVZ+YZkHe1WVHqO6wugZZWcw==} + + '@lezer/go@1.0.1': + resolution: {integrity: sha512-xToRsYxwsgJNHTgNdStpcvmbVuKxTapV0dM0wey1geMMRc9aggoVyKgzYp41D2/vVOx+Ii4hmE206kvxIXBVXQ==} + + '@lezer/highlight@1.2.3': + resolution: {integrity: sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==} + + '@lezer/html@1.3.13': + resolution: {integrity: sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg==} + + '@lezer/java@1.1.3': + resolution: {integrity: sha512-yHquUfujwg6Yu4Fd1GNHCvidIvJwi/1Xu2DaKl/pfWIA2c1oXkVvawH3NyXhCaFx4OdlYBVX5wvz2f7Aoa/4Xw==} + + '@lezer/javascript@1.5.4': + resolution: {integrity: sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==} + + '@lezer/json@1.0.3': + resolution: {integrity: sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==} + + '@lezer/lr@1.4.8': + resolution: {integrity: sha512-bPWa0Pgx69ylNlMlPvBPryqeLYQjyJjqPx+Aupm5zydLIF3NE+6MMLT8Yi23Bd9cif9VS00aUebn+6fDIGBcDA==} + + '@lezer/markdown@1.6.3': + resolution: {integrity: sha512-jpGm5Ps+XErS+xA4urw7ogEGkeZOahVQF21Z6oECF0sj+2liwZopd2+I8uH5I/vZsRuuze3OxBREIANLf6KKUw==} + + '@lezer/php@1.0.5': + resolution: {integrity: sha512-W7asp9DhM6q0W6DYNwIkLSKOvxlXRrif+UXBMxzsJUuqmhE7oVU+gS3THO4S/Puh7Xzgm858UNaFi6dxTP8dJA==} + + '@lezer/python@1.1.18': + resolution: {integrity: sha512-31FiUrU7z9+d/ElGQLJFXl+dKOdx0jALlP3KEOsGTex8mvj+SoE1FgItcHWK/axkxCHGUSpqIHt6JAWfWu9Rhg==} + + '@lezer/rust@1.0.2': + resolution: {integrity: sha512-Lz5sIPBdF2FUXcWeCu1//ojFAZqzTQNRga0aYv6dYXqJqPfMdCAI0NzajWUd4Xijj1IKJLtjoXRPMvTKWBcqKg==} + + '@lezer/sass@1.1.0': + resolution: {integrity: sha512-3mMGdCTUZ/84ArHOuXWQr37pnf7f+Nw9ycPUeKX+wu19b7pSMcZGLbaXwvD2APMBDOGxPmpK/O6S1v1EvLoqgQ==} + + '@lezer/xml@1.0.6': + resolution: {integrity: sha512-CdDwirL0OEaStFue/66ZmFSeppuL6Dwjlk8qk153mSQwiSH/Dlri4GNymrNWnUmPl2Um7QfV1FO9KFUyX3Twww==} + + '@lezer/yaml@1.0.3': + resolution: {integrity: sha512-GuBLekbw9jDBDhGur82nuwkxKQ+a3W5H0GfaAthDXcAu+XdpS43VlnxA9E9hllkpSP5ellRDKjLLj7Lu9Wr6xA==} + + '@marijn/find-cluster-break@1.0.2': + resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==} + '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} @@ -1015,6 +1231,9 @@ packages: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} + codemirror@6.0.2: + resolution: {integrity: sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -1029,6 +1248,9 @@ packages: resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} engines: {node: '>= 0.6'} + crelt@1.0.6: + resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -1663,6 +1885,9 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + style-mod@4.1.3: + resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==} + supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -1842,6 +2067,9 @@ packages: jsdom: optional: true + w3c-keyname@2.2.8: + resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} + w3c-xmlserializer@5.0.0: resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} engines: {node: '>=18'} @@ -1949,6 +2177,216 @@ snapshots: '@bcoe/v8-coverage@1.0.2': {} + '@codemirror/autocomplete@6.20.0': + dependencies: + '@codemirror/language': 6.12.1 + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.11 + '@lezer/common': 1.5.0 + + '@codemirror/commands@6.10.1': + dependencies: + '@codemirror/language': 6.12.1 + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.11 + '@lezer/common': 1.5.0 + + '@codemirror/lang-angular@0.1.4': + dependencies: + '@codemirror/lang-html': 6.4.11 + '@codemirror/lang-javascript': 6.2.4 + '@codemirror/language': 6.12.1 + '@lezer/common': 1.5.0 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + + '@codemirror/lang-cpp@6.0.3': + dependencies: + '@codemirror/language': 6.12.1 + '@lezer/cpp': 1.1.5 + + '@codemirror/lang-css@6.3.1': + dependencies: + '@codemirror/autocomplete': 6.20.0 + '@codemirror/language': 6.12.1 + '@codemirror/state': 6.5.4 + '@lezer/common': 1.5.0 + '@lezer/css': 1.3.0 + + '@codemirror/lang-go@6.0.1': + dependencies: + '@codemirror/autocomplete': 6.20.0 + '@codemirror/language': 6.12.1 + '@codemirror/state': 6.5.4 + '@lezer/common': 1.5.0 + '@lezer/go': 1.0.1 + + '@codemirror/lang-html@6.4.11': + dependencies: + '@codemirror/autocomplete': 6.20.0 + '@codemirror/lang-css': 6.3.1 + '@codemirror/lang-javascript': 6.2.4 + '@codemirror/language': 6.12.1 + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.11 + '@lezer/common': 1.5.0 + '@lezer/css': 1.3.0 + '@lezer/html': 1.3.13 + + '@codemirror/lang-java@6.0.2': + dependencies: + '@codemirror/language': 6.12.1 + '@lezer/java': 1.1.3 + + '@codemirror/lang-javascript@6.2.4': + dependencies: + '@codemirror/autocomplete': 6.20.0 + '@codemirror/language': 6.12.1 + '@codemirror/lint': 6.9.3 + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.11 + '@lezer/common': 1.5.0 + '@lezer/javascript': 1.5.4 + + '@codemirror/lang-json@6.0.2': + dependencies: + '@codemirror/language': 6.12.1 + '@lezer/json': 1.0.3 + + '@codemirror/lang-less@6.0.2': + dependencies: + '@codemirror/lang-css': 6.3.1 + '@codemirror/language': 6.12.1 + '@lezer/common': 1.5.0 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + + '@codemirror/lang-markdown@6.5.0': + dependencies: + '@codemirror/autocomplete': 6.20.0 + '@codemirror/lang-html': 6.4.11 + '@codemirror/language': 6.12.1 + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.11 + '@lezer/common': 1.5.0 + '@lezer/markdown': 1.6.3 + + '@codemirror/lang-php@6.0.2': + dependencies: + '@codemirror/lang-html': 6.4.11 + '@codemirror/language': 6.12.1 + '@codemirror/state': 6.5.4 + '@lezer/common': 1.5.0 + '@lezer/php': 1.0.5 + + '@codemirror/lang-python@6.2.1': + dependencies: + '@codemirror/autocomplete': 6.20.0 + '@codemirror/language': 6.12.1 + '@codemirror/state': 6.5.4 + '@lezer/common': 1.5.0 + '@lezer/python': 1.1.18 + + '@codemirror/lang-rust@6.0.2': + dependencies: + '@codemirror/language': 6.12.1 + '@lezer/rust': 1.0.2 + + '@codemirror/lang-sass@6.0.2': + dependencies: + '@codemirror/lang-css': 6.3.1 + '@codemirror/language': 6.12.1 + '@codemirror/state': 6.5.4 + '@lezer/common': 1.5.0 + '@lezer/sass': 1.1.0 + + '@codemirror/lang-sql@6.10.0': + dependencies: + '@codemirror/autocomplete': 6.20.0 + '@codemirror/language': 6.12.1 + '@codemirror/state': 6.5.4 + '@lezer/common': 1.5.0 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + + '@codemirror/lang-vue@0.1.3': + dependencies: + '@codemirror/lang-html': 6.4.11 + '@codemirror/lang-javascript': 6.2.4 + '@codemirror/language': 6.12.1 + '@lezer/common': 1.5.0 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + + '@codemirror/lang-wast@6.0.2': + dependencies: + '@codemirror/language': 6.12.1 + '@lezer/common': 1.5.0 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + + '@codemirror/lang-xml@6.1.0': + dependencies: + '@codemirror/autocomplete': 6.20.0 + '@codemirror/language': 6.12.1 + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.11 + '@lezer/common': 1.5.0 + '@lezer/xml': 1.0.6 + + '@codemirror/lang-yaml@6.1.2': + dependencies: + '@codemirror/autocomplete': 6.20.0 + '@codemirror/language': 6.12.1 + '@codemirror/state': 6.5.4 + '@lezer/common': 1.5.0 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + '@lezer/yaml': 1.0.3 + + '@codemirror/language@6.12.1': + dependencies: + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.11 + '@lezer/common': 1.5.0 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + style-mod: 4.1.3 + + '@codemirror/legacy-modes@6.5.2': + dependencies: + '@codemirror/language': 6.12.1 + + '@codemirror/lint@6.9.3': + dependencies: + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.11 + crelt: 1.0.6 + + '@codemirror/search@6.6.0': + dependencies: + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.11 + crelt: 1.0.6 + + '@codemirror/state@6.5.4': + dependencies: + '@marijn/find-cluster-break': 1.0.2 + + '@codemirror/theme-one-dark@6.1.3': + dependencies: + '@codemirror/language': 6.12.1 + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.11 + '@lezer/highlight': 1.2.3 + + '@codemirror/view@6.39.11': + dependencies: + '@codemirror/state': 6.5.4 + crelt: 1.0.6 + style-mod: 4.1.3 + w3c-keyname: 2.2.8 + '@csstools/color-helpers@5.1.0': {} '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': @@ -2127,6 +2565,101 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@lezer/common@1.5.0': {} + + '@lezer/cpp@1.1.5': + dependencies: + '@lezer/common': 1.5.0 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + + '@lezer/css@1.3.0': + dependencies: + '@lezer/common': 1.5.0 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + + '@lezer/go@1.0.1': + dependencies: + '@lezer/common': 1.5.0 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + + '@lezer/highlight@1.2.3': + dependencies: + '@lezer/common': 1.5.0 + + '@lezer/html@1.3.13': + dependencies: + '@lezer/common': 1.5.0 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + + '@lezer/java@1.1.3': + dependencies: + '@lezer/common': 1.5.0 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + + '@lezer/javascript@1.5.4': + dependencies: + '@lezer/common': 1.5.0 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + + '@lezer/json@1.0.3': + dependencies: + '@lezer/common': 1.5.0 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + + '@lezer/lr@1.4.8': + dependencies: + '@lezer/common': 1.5.0 + + '@lezer/markdown@1.6.3': + dependencies: + '@lezer/common': 1.5.0 + '@lezer/highlight': 1.2.3 + + '@lezer/php@1.0.5': + dependencies: + '@lezer/common': 1.5.0 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + + '@lezer/python@1.1.18': + dependencies: + '@lezer/common': 1.5.0 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + + '@lezer/rust@1.0.2': + dependencies: + '@lezer/common': 1.5.0 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + + '@lezer/sass@1.1.0': + dependencies: + '@lezer/common': 1.5.0 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + + '@lezer/xml@1.0.6': + dependencies: + '@lezer/common': 1.5.0 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + + '@lezer/yaml@1.0.3': + dependencies: + '@lezer/common': 1.5.0 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + + '@marijn/find-cluster-break@1.0.2': {} + '@polka/url@1.0.0-next.29': {} '@rollup/rollup-android-arm-eabi@4.55.1': @@ -2678,6 +3211,16 @@ snapshots: clsx@2.1.1: {} + codemirror@6.0.2: + dependencies: + '@codemirror/autocomplete': 6.20.0 + '@codemirror/commands': 6.10.1 + '@codemirror/language': 6.12.1 + '@codemirror/lint': 6.9.3 + '@codemirror/search': 6.6.0 + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.11 + color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -2688,6 +3231,8 @@ snapshots: cookie@0.6.0: {} + crelt@1.0.6: {} + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -3304,6 +3849,8 @@ snapshots: strip-json-comments@3.1.1: {} + style-mod@4.1.3: {} + supports-color@7.2.0: dependencies: has-flag: 4.0.0 @@ -3463,6 +4010,8 @@ snapshots: - tsx - yaml + w3c-keyname@2.2.8: {} + w3c-xmlserializer@5.0.0: dependencies: xml-name-validator: 5.0.0 diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 1f99752..f832d6d 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -394,6 +394,75 @@ pub async fn get_file_size(file_path: String) -> Result { Ok(metadata.len()) } +// ==================== Editor File Operations ==================== + +#[derive(Debug, Clone, serde::Serialize)] +pub struct FileEntry { + pub name: String, + pub path: String, + #[serde(rename = "isDirectory")] + pub is_directory: bool, +} + +#[tauri::command] +pub async fn list_directory(path: String) -> Result, String> { + use std::fs; + use std::path::Path; + + let dir_path = Path::new(&path); + + if !dir_path.exists() { + return Err(format!("Directory does not exist: {}", path)); + } + + if !dir_path.is_dir() { + return Err(format!("Path is not a directory: {}", path)); + } + + let entries = fs::read_dir(dir_path) + .map_err(|e| format!("Failed to read directory: {}", e))?; + + let mut file_entries = Vec::new(); + + for entry in entries { + let entry = entry.map_err(|e| format!("Failed to read entry: {}", e))?; + let path = entry.path(); + let name = entry + .file_name() + .to_string_lossy() + .to_string(); + + // Skip hidden files by default (can be made configurable later) + if name.starts_with('.') { + continue; + } + + file_entries.push(FileEntry { + name, + path: path.to_string_lossy().to_string(), + is_directory: path.is_dir(), + }); + } + + Ok(file_entries) +} + +#[tauri::command] +pub async fn read_file_content(path: String) -> Result { + use std::fs; + + fs::read_to_string(&path) + .map_err(|e| format!("Failed to read file: {}", e)) +} + +#[tauri::command] +pub async fn write_file_content(path: String, content: String) -> Result<(), String> { + use std::fs; + + fs::write(&path, content) + .map_err(|e| format!("Failed to write file: {}", e)) +} + #[cfg(test)] mod tests { use super::*; diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 0dc2487..2c8b9de 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -151,6 +151,9 @@ pub fn run() { search_clipboard_entries, get_clipboard_languages, update_clipboard_language, + list_directory, + read_file_content, + write_file_content, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src/lib/components/KeyboardShortcutsModal.svelte b/src/lib/components/KeyboardShortcutsModal.svelte index 535af64..e87e04b 100644 --- a/src/lib/components/KeyboardShortcutsModal.svelte +++ b/src/lib/components/KeyboardShortcutsModal.svelte @@ -12,6 +12,7 @@ { keys: ["Escape"], description: "Close modals and panels" }, { keys: ["Ctrl", "L"], description: "Clear the terminal" }, { keys: ["Ctrl", ","], description: "Open settings" }, + { keys: ["Ctrl", "E"], description: "Toggle file editor" }, { keys: ["Ctrl", "Shift", "M"], description: "Toggle compact mode" }, { keys: ["Ctrl", "Shift", "S"], description: "Toggle streamer mode" }, ], @@ -26,6 +27,15 @@ { keys: ["↓"], description: "Next input from history" }, ], }, + { + category: "File Editor", + items: [ + { keys: ["Ctrl", "E"], description: "Toggle editor view" }, + { keys: ["Ctrl", "B"], description: "Toggle file browser" }, + { keys: ["Ctrl", "S"], description: "Save current file" }, + { keys: ["Ctrl", "W"], description: "Close current tab" }, + ], + }, { category: "Slash Commands", items: [ diff --git a/src/lib/components/StatusBar.svelte b/src/lib/components/StatusBar.svelte index 8905293..be2942e 100644 --- a/src/lib/components/StatusBar.svelte +++ b/src/lib/components/StatusBar.svelte @@ -13,6 +13,7 @@ import { get } from "svelte/store"; import { claudeStore } from "$lib/stores/claude"; import { configStore, type HikariConfig, isStreamerMode } from "$lib/stores/config"; + import { editorStore } from "$lib/stores/editor"; import type { ConnectionStatus } from "$lib/types/messages"; import { onMount } from "svelte"; import StatsDisplay from "./StatsDisplay.svelte"; @@ -80,6 +81,15 @@ streamerModeActive = value; }); + let editorVisible = $state(false); + editorStore.isEditorVisible.subscribe((value) => { + editorVisible = value; + }); + + function toggleEditor() { + editorStore.toggleEditor(); + } + onMount(async () => { appVersion = await getVersion(); }); @@ -307,6 +317,25 @@ /> + + +
+ + {#if $activeTab} + + {/if} + + + {#if $saveError} +
+ {$saveError} + +
+ {/if} + +
+ {#if $isFileBrowserOpen} +
+ +
+ {/if} + +
+ + +
+ {#if $activeTab} + {#key $activeTab.id} + + {/key} + {:else} +
+
+ + + + +

Select a file to edit

+

Use the file browser on the left or press Ctrl+B to toggle it

+
+
+ {/if} +
+
+
+ + + diff --git a/src/lib/components/editor/EditorTabs.svelte b/src/lib/components/editor/EditorTabs.svelte new file mode 100644 index 0000000..bd89b2c --- /dev/null +++ b/src/lib/components/editor/EditorTabs.svelte @@ -0,0 +1,170 @@ + + +
+ {#if $tabs.length === 0} +
No files open
+ {:else} +
+ {#each $tabs as tab (tab.id)} + + {/each} +
+ {/if} +
+ + diff --git a/src/lib/components/editor/FileBrowser.svelte b/src/lib/components/editor/FileBrowser.svelte new file mode 100644 index 0000000..3e0b0ac --- /dev/null +++ b/src/lib/components/editor/FileBrowser.svelte @@ -0,0 +1,151 @@ + + +
+
+ Files + +
+ +
+ {#if $isLoadingTree} +
+ + + + + + Loading... +
+ {:else if $fileTree.length === 0} +
+ No files found +
+ {:else} +
+ {#each $fileTree as entry (entry.path)} + + {/each} +
+ {/if} +
+
+ + diff --git a/src/lib/components/editor/FileTreeItem.svelte b/src/lib/components/editor/FileTreeItem.svelte new file mode 100644 index 0000000..7825b71 --- /dev/null +++ b/src/lib/components/editor/FileTreeItem.svelte @@ -0,0 +1,195 @@ + + +
+ + + {#if entry.isDirectory && isExpanded && children.length > 0} +
+ {#each children as child (child.path)} + + {/each} +
+ {/if} +
+ + diff --git a/src/lib/stores/editor.ts b/src/lib/stores/editor.ts new file mode 100644 index 0000000..df9a035 --- /dev/null +++ b/src/lib/stores/editor.ts @@ -0,0 +1,285 @@ +import { writable, derived, get } from "svelte/store"; +import { invoke } from "@tauri-apps/api/core"; +import type { EditorState, EditorTab, FileEntry } from "$lib/types/editor"; + +const defaultState: EditorState = { + tabs: [], + activeTabId: null, + isFileBrowserOpen: true, + currentDirectory: "", + fileTree: [], + isLoadingTree: false, +}; + +function getLanguageFromPath(filePath: string): string { + const ext = filePath.split(".").pop()?.toLowerCase() || ""; + const languageMap: Record = { + js: "javascript", + jsx: "javascript", + ts: "typescript", + tsx: "typescript", + py: "python", + rs: "rust", + go: "go", + java: "java", + c: "c", + cpp: "cpp", + h: "c", + hpp: "cpp", + cs: "csharp", + rb: "ruby", + php: "php", + swift: "swift", + kt: "kotlin", + scala: "scala", + html: "html", + htm: "html", + css: "css", + scss: "scss", + sass: "sass", + less: "less", + json: "json", + xml: "xml", + yaml: "yaml", + yml: "yaml", + toml: "toml", + md: "markdown", + markdown: "markdown", + sql: "sql", + sh: "shell", + bash: "shell", + zsh: "shell", + ps1: "powershell", + dockerfile: "dockerfile", + svelte: "svelte", + vue: "vue", + graphql: "graphql", + gql: "graphql", + lua: "lua", + r: "r", + dart: "dart", + elm: "elm", + ex: "elixir", + exs: "elixir", + erl: "erlang", + hs: "haskell", + clj: "clojure", + lisp: "lisp", + ml: "ocaml", + fs: "fsharp", + zig: "zig", + nim: "nim", + v: "v", + wasm: "wasm", + wat: "wasm", + }; + return languageMap[ext] || "plaintext"; +} + +function generateTabId(): string { + return `tab-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`; +} + +function createEditorStore() { + const state = writable(defaultState); + const isEditorVisible = writable(false); + const saveError = writable(null); + + async function loadDirectory(dirPath: string): Promise { + try { + const entries = await invoke("list_directory", { path: dirPath }); + return entries.sort((a, b) => { + if (a.isDirectory && !b.isDirectory) return -1; + if (!a.isDirectory && b.isDirectory) return 1; + return a.name.localeCompare(b.name); + }); + } catch (error) { + console.error("Failed to load directory:", error); + return []; + } + } + + async function initializeFileTree(rootPath: string) { + state.update((s) => ({ ...s, isLoadingTree: true, currentDirectory: rootPath })); + try { + const entries = await loadDirectory(rootPath); + state.update((s) => ({ ...s, fileTree: entries, isLoadingTree: false })); + } catch (error) { + console.error("Failed to initialize file tree:", error); + state.update((s) => ({ ...s, isLoadingTree: false })); + } + } + + async function toggleDirectory(entry: FileEntry) { + if (!entry.isDirectory) return; + + state.update((s) => { + const updateTree = (entries: FileEntry[]): FileEntry[] => { + return entries.map((e) => { + if (e.path === entry.path) { + return { ...e, isExpanded: !e.isExpanded, isLoading: !e.isExpanded && !e.children }; + } + if (e.children) { + return { ...e, children: updateTree(e.children) }; + } + return e; + }); + }; + return { ...s, fileTree: updateTree(s.fileTree) }; + }); + + if (!entry.isExpanded && !entry.children) { + const children = await loadDirectory(entry.path); + state.update((s) => { + const updateTree = (entries: FileEntry[]): FileEntry[] => { + return entries.map((e) => { + if (e.path === entry.path) { + return { ...e, children, isLoading: false }; + } + if (e.children) { + return { ...e, children: updateTree(e.children) }; + } + return e; + }); + }; + return { ...s, fileTree: updateTree(s.fileTree) }; + }); + } + } + + async function openFile(filePath: string) { + const currentState = get(state); + + const existingTab = currentState.tabs.find((t) => t.filePath === filePath); + if (existingTab) { + state.update((s) => ({ ...s, activeTabId: existingTab.id })); + return; + } + + try { + const content = await invoke("read_file_content", { path: filePath }); + const fileName = filePath.split(/[/\\]/).pop() || "untitled"; + const language = getLanguageFromPath(filePath); + const newTab: EditorTab = { + id: generateTabId(), + filePath, + fileName, + content, + originalContent: content, + isDirty: false, + language, + }; + + state.update((s) => ({ + ...s, + tabs: [...s.tabs, newTab], + activeTabId: newTab.id, + })); + } catch (error) { + console.error("Failed to open file:", error); + saveError.set(`Failed to open file: ${error}`); + } + } + + async function saveFile(tabId?: string) { + const currentState = get(state); + const tab = tabId + ? currentState.tabs.find((t) => t.id === tabId) + : currentState.tabs.find((t) => t.id === currentState.activeTabId); + + if (!tab) return; + + saveError.set(null); + try { + await invoke("write_file_content", { path: tab.filePath, content: tab.content }); + state.update((s) => ({ + ...s, + tabs: s.tabs.map((t) => + t.id === tab.id ? { ...t, originalContent: t.content, isDirty: false } : t + ), + })); + } catch (error) { + console.error("Failed to save file:", error); + saveError.set(`Failed to save file: ${error}`); + throw error; + } + } + + function updateTabContent(tabId: string, content: string) { + state.update((s) => ({ + ...s, + tabs: s.tabs.map((t) => + t.id === tabId ? { ...t, content, isDirty: content !== t.originalContent } : t + ), + })); + } + + function closeTab(tabId: string) { + state.update((s) => { + const tabIndex = s.tabs.findIndex((t) => t.id === tabId); + const newTabs = s.tabs.filter((t) => t.id !== tabId); + + let newActiveId = s.activeTabId; + if (s.activeTabId === tabId) { + if (newTabs.length === 0) { + newActiveId = null; + } else if (tabIndex >= newTabs.length) { + newActiveId = newTabs[newTabs.length - 1].id; + } else { + newActiveId = newTabs[tabIndex].id; + } + } + + return { ...s, tabs: newTabs, activeTabId: newActiveId }; + }); + } + + function setActiveTab(tabId: string) { + state.update((s) => ({ ...s, activeTabId: tabId })); + } + + function toggleFileBrowser() { + state.update((s) => ({ ...s, isFileBrowserOpen: !s.isFileBrowserOpen })); + } + + function toggleEditor() { + isEditorVisible.update((v) => !v); + } + + function showEditor() { + isEditorVisible.set(true); + } + + function hideEditor() { + isEditorVisible.set(false); + } + + return { + state: { subscribe: state.subscribe }, + isEditorVisible: { subscribe: isEditorVisible.subscribe }, + saveError: { subscribe: saveError.subscribe }, + + initializeFileTree, + toggleDirectory, + openFile, + saveFile, + updateTabContent, + closeTab, + setActiveTab, + toggleFileBrowser, + toggleEditor, + showEditor, + hideEditor, + + tabs: derived(state, ($state) => $state.tabs), + activeTab: derived(state, ($state) => $state.tabs.find((t) => t.id === $state.activeTabId)), + activeTabId: derived(state, ($state) => $state.activeTabId), + fileTree: derived(state, ($state) => $state.fileTree), + isFileBrowserOpen: derived(state, ($state) => $state.isFileBrowserOpen), + isLoadingTree: derived(state, ($state) => $state.isLoadingTree), + currentDirectory: derived(state, ($state) => $state.currentDirectory), + hasDirtyTabs: derived(state, ($state) => $state.tabs.some((t) => t.isDirty)), + }; +} + +export const editorStore = createEditorStore(); diff --git a/src/lib/types/editor.ts b/src/lib/types/editor.ts new file mode 100644 index 0000000..011136a --- /dev/null +++ b/src/lib/types/editor.ts @@ -0,0 +1,27 @@ +export interface FileEntry { + name: string; + path: string; + isDirectory: boolean; + isExpanded?: boolean; + children?: FileEntry[]; + isLoading?: boolean; +} + +export interface EditorTab { + id: string; + filePath: string; + fileName: string; + content: string; + originalContent: string; + isDirty: boolean; + language: string; +} + +export interface EditorState { + tabs: EditorTab[]; + activeTabId: string | null; + isFileBrowserOpen: boolean; + currentDirectory: string; + fileTree: FileEntry[]; + isLoadingTree: boolean; +} diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 0a35b7d..cdfe16d 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -7,6 +7,7 @@ import { initNotificationSync, cleanupNotificationSync } from "$lib/stores/notifications"; import { conversationsStore } from "$lib/stores/conversations"; import { claudeStore, isClaudeProcessing } from "$lib/stores/claude"; + import { editorStore } from "$lib/stores/editor"; import { getCurrentWindow, LogicalSize } from "@tauri-apps/api/window"; import "$lib/notifications/testNotifications"; import Terminal from "$lib/components/Terminal.svelte"; @@ -14,6 +15,7 @@ import StatusBar from "$lib/components/StatusBar.svelte"; import AnimeGirl from "$lib/components/AnimeGirl.svelte"; import CompactMode from "$lib/components/CompactMode.svelte"; + import EditorPanel from "$lib/components/editor/EditorPanel.svelte"; import { characterState } from "$lib/stores/character"; import type { CharacterState } from "$lib/types/states"; import PermissionModal from "$lib/components/PermissionModal.svelte"; @@ -29,6 +31,32 @@ let currentCharacterState: CharacterState = $state("idle"); let compactModeActive = $state(false); + // Editor state + const isEditorVisible = editorStore.isEditorVisible; + let lastInitializedCwd = ""; + + // Track connection status and CWD for the editor + const connectionStatus = claudeStore.connectionStatus; + const currentWorkingDirectory = claudeStore.currentWorkingDirectory; + + // Initialize/update editor file tree when CWD changes while editor is visible + $effect(() => { + const visible = $isEditorVisible; + const cwd = $currentWorkingDirectory; + const connected = $connectionStatus === "connected"; + + // Only initialize when editor is visible, connected, and CWD is set + if (visible && connected && cwd && cwd !== lastInitializedCwd) { + lastInitializedCwd = cwd; + editorStore.initializeFileTree(cwd); + } + + // Hide editor if disconnected + if (!connected && visible) { + editorStore.hideEditor(); + } + }); + // Window size constants const COMPACT_WIDTH = 280; const COMPACT_HEIGHT = 400; @@ -176,6 +204,40 @@ toggleCompactMode(); return; } + + // Ctrl+E - Toggle editor panel (only when connected) + if (event.ctrlKey && event.key === "e") { + event.preventDefault(); + // Only allow opening the editor when connected + if (get(claudeStore.connectionStatus) === "connected") { + editorStore.toggleEditor(); + } + return; + } + + // Ctrl+B - Toggle file browser (when editor is visible) + if (event.ctrlKey && event.key === "b" && get(editorStore.isEditorVisible)) { + event.preventDefault(); + editorStore.toggleFileBrowser(); + return; + } + + // Ctrl+S - Save current file (when editor is visible) + if (event.ctrlKey && event.key === "s" && get(editorStore.isEditorVisible)) { + event.preventDefault(); + editorStore.saveFile(); + return; + } + + // Ctrl+W - Close current tab (when editor is visible) + if (event.ctrlKey && event.key === "w" && get(editorStore.isEditorVisible)) { + event.preventDefault(); + const activeTabId = get(editorStore.activeTabId); + if (activeTabId) { + editorStore.closeTab(activeTabId); + } + return; + } } async function handleInterrupt() { @@ -330,10 +392,14 @@ onmousedown={startResize} > - +
- - + {#if $isEditorVisible} + + {:else} + + + {/if}