From e45a1a1c98cf13a380dda1321966af5a58d41235 Mon Sep 17 00:00:00 2001 From: Hikari Date: Wed, 28 Jan 2026 18:20:02 -0800 Subject: [PATCH] feat: add built-in file editor with syntax highlighting (#79) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Add CodeMirror 6 editor with syntax highlighting for 40+ languages - Add file browser sidebar with collapsible directory tree navigation - Add multi-tab support with dirty state indicators and close buttons - Add keyboard shortcuts (Ctrl+E toggle, Ctrl+B file browser, Ctrl+S save, Ctrl+W close tab) - Add editor toggle button to status bar (disabled when not connected) - Editor automatically uses current session's working directory - Add Tauri backend commands for file operations (list_directory, read_file_content, write_file_content) ## Test Plan - [ ] Connect to a session and verify the editor toggle button becomes enabled - [ ] Press Ctrl+E to open the editor and verify file tree shows the session's CWD - [ ] Navigate directories and open files to verify syntax highlighting works - [ ] Edit a file and verify the dirty indicator (*) appears - [ ] Save with Ctrl+S and verify the dirty indicator disappears - [ ] Open multiple files and verify tab switching works - [ ] Close tabs with Ctrl+W or the X button - [ ] Disconnect and verify the editor automatically closes - [ ] Verify keyboard shortcuts are documented in the shortcuts modal Closes #72 ✨ This PR was created with help from Hikari~ 🌸 Reviewed-on: https://git.nhcarrigan.com/nhcarrigan/hikari-desktop/pulls/79 Co-authored-by: Hikari Co-committed-by: Hikari --- package.json | 27 + pnpm-lock.yaml | 552 ++++++++++++++++++ src-tauri/src/commands.rs | 162 +++++ src-tauri/src/lib.rs | 8 + src/lib/components/InputBar.svelte | 29 + .../components/KeyboardShortcutsModal.svelte | 12 + src/lib/components/StatusBar.svelte | 29 + .../components/TextInputContextMenu.svelte | 228 ++++++++ src/lib/components/editor/CodeEditor.svelte | 482 +++++++++++++++ .../components/editor/ConfirmDialog.svelte | 131 +++++ .../editor/EditorContextMenu.svelte | 238 ++++++++ src/lib/components/editor/EditorPanel.svelte | 253 ++++++++ src/lib/components/editor/EditorTabs.svelte | 170 ++++++ src/lib/components/editor/FileBrowser.svelte | 358 ++++++++++++ .../components/editor/FileContextMenu.svelte | 203 +++++++ src/lib/components/editor/FileTreeItem.svelte | 208 +++++++ src/lib/components/editor/InputDialog.svelte | 173 ++++++ src/lib/stores/editor.ts | 426 ++++++++++++++ src/lib/types/editor.ts | 27 + src/routes/+layout.svelte | 10 +- src/routes/+page.svelte | 81 ++- 21 files changed, 3803 insertions(+), 4 deletions(-) create mode 100644 src/lib/components/TextInputContextMenu.svelte create mode 100644 src/lib/components/editor/CodeEditor.svelte create mode 100644 src/lib/components/editor/ConfirmDialog.svelte create mode 100644 src/lib/components/editor/EditorContextMenu.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/FileContextMenu.svelte create mode 100644 src/lib/components/editor/FileTreeItem.svelte create mode 100644 src/lib/components/editor/InputDialog.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..d31aa2a 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,32 @@ }, "license": "MIT", "dependencies": { + "@codemirror/commands": "6.8.1", + "@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 +62,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..770b6a4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,84 @@ importers: .: dependencies: + '@codemirror/commands': + specifier: 6.8.1 + version: 6.8.1 + '@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 +113,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 +239,90 @@ packages: resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} + '@codemirror/autocomplete@6.20.0': + resolution: {integrity: sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg==} + + '@codemirror/commands@6.8.1': + resolution: {integrity: sha512-KlGVYufHMQzxbdQONiLyGQDUW0itrLZwq3CcY7xpv9ZLRHqzkBSoteocBHtMCoY7/Ci4xhzSrToIeLg7FxHuaw==} + + '@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 +590,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 +1234,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 +1251,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 +1888,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 +2070,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 +2180,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.8.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 +2568,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 +3214,16 @@ snapshots: clsx@2.1.1: {} + codemirror@6.0.2: + dependencies: + '@codemirror/autocomplete': 6.20.0 + '@codemirror/commands': 6.8.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 +3234,8 @@ snapshots: cookie@0.6.0: {} + crelt@1.0.6: {} + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -3304,6 +3852,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 +4013,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..f71b265 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -394,6 +394,168 @@ 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)) +} + +#[tauri::command] +pub async fn create_file(path: String) -> Result<(), String> { + use std::fs::File; + use std::path::Path; + + let file_path = Path::new(&path); + + if file_path.exists() { + return Err("File already exists".to_string()); + } + + File::create(file_path).map_err(|e| format!("Failed to create file: {}", e))?; + + Ok(()) +} + +#[tauri::command] +pub async fn create_directory(path: String) -> Result<(), String> { + use std::fs; + use std::path::Path; + + let dir_path = Path::new(&path); + + if dir_path.exists() { + return Err("Directory already exists".to_string()); + } + + fs::create_dir_all(dir_path).map_err(|e| format!("Failed to create directory: {}", e))?; + + Ok(()) +} + +#[tauri::command] +pub async fn delete_file(path: String) -> Result<(), String> { + use std::fs; + use std::path::Path; + + let file_path = Path::new(&path); + + if !file_path.exists() { + return Err("File does not exist".to_string()); + } + + if file_path.is_dir() { + return Err("Path is a directory, use delete_directory instead".to_string()); + } + + fs::remove_file(file_path).map_err(|e| format!("Failed to delete file: {}", e))?; + + Ok(()) +} + +#[tauri::command] +pub async fn delete_directory(path: String) -> Result<(), String> { + use std::fs; + use std::path::Path; + + let dir_path = Path::new(&path); + + if !dir_path.exists() { + return Err("Directory does not exist".to_string()); + } + + if !dir_path.is_dir() { + return Err("Path is not a directory".to_string()); + } + + fs::remove_dir_all(dir_path).map_err(|e| format!("Failed to delete directory: {}", e))?; + + Ok(()) +} + +#[tauri::command] +pub async fn rename_path(old_path: String, new_path: String) -> Result<(), String> { + use std::fs; + use std::path::Path; + + let old = Path::new(&old_path); + let new = Path::new(&new_path); + + if !old.exists() { + return Err("Path does not exist".to_string()); + } + + if new.exists() { + return Err("Destination already exists".to_string()); + } + + fs::rename(old, new).map_err(|e| format!("Failed to rename: {}", e))?; + + Ok(()) +} + #[cfg(test)] mod tests { use super::*; diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 0dc2487..6db2820 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -151,6 +151,14 @@ pub fn run() { search_clipboard_entries, get_clipboard_languages, update_clipboard_language, + list_directory, + read_file_content, + write_file_content, + create_file, + create_directory, + delete_file, + delete_directory, + rename_path, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src/lib/components/InputBar.svelte b/src/lib/components/InputBar.svelte index d798985..a05da28 100644 --- a/src/lib/components/InputBar.svelte +++ b/src/lib/components/InputBar.svelte @@ -30,6 +30,7 @@ import SnippetLibraryPanel from "$lib/components/SnippetLibraryPanel.svelte"; import QuickActionsPanel from "$lib/components/QuickActionsPanel.svelte"; import ClipboardHistoryPanel from "$lib/components/ClipboardHistoryPanel.svelte"; + import TextInputContextMenu from "$lib/components/TextInputContextMenu.svelte"; import type { Attachment } from "$lib/types/messages"; const INPUT_HISTORY_KEY = "hikari-input-history"; @@ -49,6 +50,23 @@ let showClipboardHistory = $state(false); let streamerModeActive = $state(false); + // Context menu state + let textareaElement: HTMLTextAreaElement | undefined = $state(); + let contextMenuShow = $state(false); + let contextMenuX = $state(0); + let contextMenuY = $state(0); + + function handleContextMenu(event: MouseEvent) { + event.preventDefault(); + contextMenuShow = true; + contextMenuX = event.clientX; + contextMenuY = event.clientY; + } + + function closeContextMenu() { + contextMenuShow = false; + } + isStreamerMode.subscribe((value) => { streamerModeActive = value; }); @@ -876,10 +894,12 @@ User: ${formattedMessage}`;