diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index be949d60..00000000 --- a/.editorconfig +++ /dev/null @@ -1,43 +0,0 @@ -# EditorConfig is awesome: https://EditorConfig.org - -# top-most EditorConfig file -root = true - -# All files -[*] -indent_style = space -indent_size = 2 -end_of_line = lf -charset = utf-8 -trim_trailing_whitespace = true -insert_final_newline = true - -# TypeScript and JavaScript files -[*.{js,ts,jsx,tsx}] -indent_size = 2 -max_line_length = 100 - -# JSON files -[*.json] -indent_size = 2 - -# YAML files -[*.{yml,yaml}] -indent_size = 2 - -# Markdown files -[*.md] -trim_trailing_whitespace = false -max_line_length = off - -# Rust files -[*.rs] -indent_size = 3 - -# TOML files (Cargo.toml, etc.) -[*.toml] -indent_size = 2 - -# Config files -[*.{toml,conf,config}] -indent_size = 2 diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 7b05aecc..50833d5a 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -22,4 +22,9 @@ Closes # Fixes # +## Checklist + +- [ ] I have read and will follow the [Code of Conduct](../CODE_OF_CONDUCT.md). +- [ ] I have read and agree to the + [Contributor License and Feedback Agreement](../CONTRIBUTOR_LICENSE_AND_FEEDBACK_AGREEMENT.md). diff --git a/.github/workflows/bun.yml b/.github/workflows/bun.yml deleted file mode 100644 index 9568ed09..00000000 --- a/.github/workflows/bun.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: Bun CI - -on: - push: - branches: [main, master] - pull_request: - branches: [main, master] - workflow_dispatch: - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - -jobs: - check: - # Don't run on forks - if: github.repository == 'athasdev/athas' - name: check format, types, and lint - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Setup Bun - uses: oven-sh/setup-bun@v2 - with: - bun-version: latest - - - name: Cache Bun dependencies - uses: actions/cache@v4 - with: - path: ~/.bun/install/cache - key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }} - restore-keys: | - ${{ runner.os }}-bun- - - - name: Install dependencies - run: bun install - - - name: Run typecheck - run: bun typecheck - - - name: Run biome check - run: bun check diff --git a/.github/workflows/rust.yml b/.github/workflows/ci.yml similarity index 50% rename from .github/workflows/rust.yml rename to .github/workflows/ci.yml index a4cf5d19..ff6fda8e 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/ci.yml @@ -1,25 +1,52 @@ -name: Rust CI +name: CI on: push: - branches: [main, master] + branches: [master] pull_request: - branches: [main, master] + branches: [master] workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true -env: - CARGO_TERM_COLOR: always - jobs: - check: - # Don't run on forks + bun: + if: github.repository == 'athasdev/athas' + name: Bun — typecheck, lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Cache Bun dependencies + uses: actions/cache@v4 + with: + path: ~/.bun/install/cache + key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }} + restore-keys: | + ${{ runner.os }}-bun- + + - name: Install dependencies + run: bun install + + - name: Run typecheck + run: bun typecheck + + - name: Run biome check + run: bun check + + rust: if: github.repository == 'athasdev/athas' - name: check format and cargo check + name: Rust — fmt, cargo check runs-on: ubuntu-latest + env: + CARGO_TERM_COLOR: always steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml deleted file mode 100644 index 68c91be9..00000000 --- a/.github/workflows/deploy.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: Deploy Docs - -on: - push: - branches: [main, master] - workflow_dispatch: - -jobs: - deploy: - runs-on: ubuntu-latest - steps: - - name: Deploy via SSH - uses: appleboy/ssh-action@v1.0.3 - with: - host: ${{ secrets.VPS_HOST }} - username: ${{ secrets.VPS_USER }} - key: ${{ secrets.VPS_SSH_KEY }} - port: ${{ secrets.VPS_PORT || 22 }} - script: | - set -e - cd /srv/athas - git pull - /root/.bun/bin/bun scripts/build-extensions-index.ts - cd /srv/athas/docs - /root/.bun/bin/bun install - /root/.bun/bin/bun run build - systemctl restart athas-docs diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml deleted file mode 100644 index 52ffc8cb..00000000 --- a/.github/workflows/docs.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: Docs Build - -on: - push: - branches: [main, master] - paths: - - "docs/**" - - ".github/workflows/docs.yml" - pull_request: - branches: [main, master] - paths: - - "docs/**" - - ".github/workflows/docs.yml" - workflow_dispatch: - -jobs: - build: - if: github.repository == 'athasdev/athas' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Setup Bun - uses: oven-sh/setup-bun@v2 - with: - bun-version: latest - - - name: Install docs dependencies - run: bun --cwd docs install - - - name: Build docs - run: bun --cwd docs build diff --git a/.github/workflows/extensions-index.yml b/.github/workflows/extensions-index.yml deleted file mode 100644 index 8d28f10f..00000000 --- a/.github/workflows/extensions-index.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: Build Extensions Index - -on: - push: - branches: [main, master] - paths: - - "extensions/registry.json" - - "scripts/build-extensions-index.ts" - - ".github/workflows/extensions-index.yml" - workflow_dispatch: - -permissions: - contents: write - -jobs: - build-index: - if: github.repository == 'athasdev/athas' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Setup Bun - uses: oven-sh/setup-bun@v2 - with: - bun-version: latest - - - name: Build extensions index - run: bun scripts/build-extensions-index.ts - - - name: Commit changes - run: | - if git diff --quiet; then - echo "No changes to commit" - exit 0 - fi - - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - - git add extensions/index.json - git commit -m "Update extensions index" - git push diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9cf6e889..0bce2275 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,22 +19,46 @@ jobs: matrix: include: - platform: 'macos-latest' + os: 'macos' args: '--target aarch64-apple-darwin' target: 'aarch64-apple-darwin' - platform: 'macos-latest' + os: 'macos' args: '--target x86_64-apple-darwin' target: 'x86_64-apple-darwin' - platform: 'ubuntu-22.04' + os: 'linux' + args: '' + target: '' + - platform: 'ubuntu-22.04-arm' + os: 'linux' args: '' target: '' - platform: 'windows-latest' + os: 'windows' args: '' target: '' + - platform: 'windows-latest' + os: 'windows' + args: '--target aarch64-pc-windows-msvc' + target: 'aarch64-pc-windows-msvc' runs-on: ${{ matrix.platform }} + timeout-minutes: 40 steps: - uses: actions/checkout@v4 + - name: Free disk space (Linux only) + if: matrix.os == 'linux' + run: | + echo "Disk usage before cleanup:" + df -h + sudo rm -rf /usr/share/dotnet /opt/ghc /usr/local/lib/android /opt/hostedtoolcache/CodeQL || true + sudo docker system prune -af || true + sudo apt-get clean + echo "Disk usage after cleanup:" + df -h + - name: Setup Bun uses: oven-sh/setup-bun@v2 with: @@ -45,13 +69,10 @@ jobs: with: targets: ${{ matrix.target }} - - name: Add additional Rust targets - if: matrix.target != '' - run: rustup target add ${{ matrix.target }} - - - name: Cache Rust dependencies uses: Swatinem/rust-cache@v2 + with: + cache-on-failure: true - name: Cache Bun dependencies uses: actions/cache@v4 @@ -61,17 +82,21 @@ jobs: restore-keys: | ${{ runner.os }}-bun- - - name: Install dependencies (ubuntu only) - if: matrix.platform == 'ubuntu-22.04' + - name: Install dependencies (Linux only) + if: matrix.os == 'linux' run: | sudo apt-get update - sudo apt-get install -y libwebkit2gtk-4.0-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf + sudo apt-get install -y libwebkit2gtk-4.0-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libfuse2 + + - name: Configure AppImage runtime (Linux only) + if: matrix.os == 'linux' + run: echo "APPIMAGE_EXTRACT_AND_RUN=1" >> "$GITHUB_ENV" - name: Install frontend dependencies run: bun install - name: Import Apple Certificate (macOS only) - if: matrix.platform == 'macos-latest' + if: matrix.os == 'macos' env: APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} APPLE_CERT_PASSWORD: ${{ secrets.APPLE_CERT_PASSWORD }} @@ -102,9 +127,9 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} - APPLE_ID: ${{ secrets.APPLE_ID }} - APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} - APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + APPLE_ID: ${{ matrix.os == 'macos' && secrets.APPLE_ID || '' }} + APPLE_PASSWORD: ${{ matrix.os == 'macos' && secrets.APPLE_PASSWORD || '' }} + APPLE_TEAM_ID: ${{ matrix.os == 'macos' && secrets.APPLE_TEAM_ID || '' }} with: tagName: v__VERSION__ releaseName: 'Athas v__VERSION__' @@ -123,3 +148,13 @@ jobs: prerelease: false includeUpdaterJson: true args: ${{ matrix.args }} + + deploy-docs: + runs-on: ubuntu-latest + steps: + - name: Trigger docs rebuild on www + uses: peter-evans/repository-dispatch@v3 + with: + token: ${{ secrets.WWW_DEPLOY_TOKEN }} + repository: athasdev/www + event-type: docs-updated diff --git a/.github/workflows/sync-docs.yml b/.github/workflows/sync-docs.yml deleted file mode 100644 index 1e03fa39..00000000 --- a/.github/workflows/sync-docs.yml +++ /dev/null @@ -1,46 +0,0 @@ -name: Sync Website Content - -on: - push: - branches: [main, master] - paths: - - "CHANGELOG.md" - - "ROADMAP.md" - - ".github/workflows/sync-docs.yml" - workflow_dispatch: - -jobs: - sync-website-content: - runs-on: ubuntu-latest - steps: - - name: Checkout athas repo - uses: actions/checkout@v4 - with: - path: athas - - - name: Checkout www repo - uses: actions/checkout@v4 - with: - repository: athasdev/www - token: ${{ secrets.WWW_REPO_TOKEN }} - path: www - - - name: Sync changelog and roadmap - run: | - mkdir -p www/src/content - cp athas/CHANGELOG.md www/src/content/changelog.md - cp athas/ROADMAP.md www/src/content/roadmap.md - - - name: Commit and push changes - working-directory: www - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - - git add -A - if git diff --staged --quiet; then - echo "No changes to commit" - else - git commit -m "Sync changelog and roadmap from athas" - git push - fi diff --git a/.gitignore b/.gitignore index d0547755..6305b5d4 100644 --- a/.gitignore +++ b/.gitignore @@ -59,6 +59,7 @@ TODO.md # claude local settings .claude/settings.local.json +src-tauri/.claude/settings.local.json # Tauri gen files src-tauri/gen/ @@ -77,3 +78,6 @@ src/extensions/bundled/*/grammars/*.wasm # Benchmark files benchmark_*.md + +# local codex settings +src-tauri/.claude/settings.local.json diff --git a/.rules b/.rules index 1c2aed9b..fbc618c8 100644 --- a/.rules +++ b/.rules @@ -6,6 +6,7 @@ - Never change the .rules file unless the user specifically asks for it - Avoid unnecessary comments in UI components (keep code self-explanatory) - Avoid unnecessary `cn(...)` calls: use it only for conditional or merged class names; do not wrap static strings +- Always use bun. ## Zustand @@ -59,32 +60,6 @@ src/ └── types.ts # Theme interfaces ``` -### Available CSS Variables - -**UI Colors:** -- `--primary-bg`, `--secondary-bg` -- `--text`, `--text-light`, `--text-lighter` -- `--border`, `--hover`, `--selected` -- `--accent` - -**Editor:** -- `--cursor`, `--cursor-vim-normal`, `--cursor-vim-insert` - -**Semantic Colors:** -- `--error`, `--warning`, `--success`, `--info` - -**Git Status:** -- `--git-modified`, `--git-added`, `--git-deleted`, `--git-untracked`, `--git-renamed` - -**Syntax Highlighting:** -- `--syntax-comment`, `--syntax-keyword`, `--syntax-string`, `--syntax-number` -- `--syntax-function`, `--syntax-variable`, `--syntax-tag`, `--syntax-attribute` -- `--syntax-punctuation`, `--syntax-constant`, `--syntax-property`, `--syntax-type` - -**Terminal Colors:** -- `--terminal-{color}` for black, red, green, yellow, blue, magenta, cyan, white -- `--terminal-bright-{color}` for bright variants - ### Best Practices 1. **Consistency**: Use Tailwind utilities for all standard component styling diff --git a/.tool-versions b/.tool-versions deleted file mode 100644 index ec5bbc9f..00000000 --- a/.tool-versions +++ /dev/null @@ -1,2 +0,0 @@ -bun 1.1.29 -nodejs 22.18.0 diff --git a/.windsurfrules b/.windsurfrules deleted file mode 120000 index 8a63b64b..00000000 --- a/.windsurfrules +++ /dev/null @@ -1 +0,0 @@ -.rules \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index b873565a..00000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,246 +0,0 @@ -# Changelog - -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -## [0.3.2] - 2026-01-13 - -### Added -- Native PDF viewer with external open support -- CSV viewer as built-in extension -- HTML/CSS live preview with asset protocol support -- Preview mode for file tabs -- Auto-refresh for externally created/deleted files -- In-app report a bug button -- Drag and drop to open folders as projects and files as buffers -- Test connection button and feedback for SSH connections -- Configurable command bar file limit and improved fuzzy search -- Cmd+hover definition link highlighting -- Jump list navigation -- Performance monitoring system - -### Changed -- Virtualize file tree for better performance -- Optimize editor rendering -- Improve PDF and image zoom controls -- Improve autocompletion behavior and LSP completion -- Remove Rust from bundled registry - -### Fixed -- Fix scroll position bugs in editor -- Fix file tree git status not updating on save -- Fix nvim cursor in normal and insert mode -- Fix terminal corruption with multi-byte UTF-8 characters -- Fix search highlighting in editor -- Fix command palette not finding all files -- Prevent command injection in SSH fallback commands -- Fix git blame not appearing after file switch -- Fix duplicate LSP server creation due to race condition -- Fix gap between sticky headers in file tree -- Preserve scroll position when switching between buffers -- Fix window resize on Linux -- Disable problematic DMABUF renderer on Linux -- Fix stored XSS in markdown parser by sanitizing HTML output -- Bundle FreeType in Linux AppImage for older distro compatibility -- Fix incorrect bundled LSP path on Linux -- Fix git blame not showing after scrolling -- Fix tokenization lag and visual flashing during typing - -## [0.3.1] - 2025-12-22 - -### Fixed -- Fix Windows build by correcting CLI module imports - -## [0.3.0] - 2025-12-22 - -### Added -- Ollama support for local AI models -- ACP (Agent Client Protocol) integration with session modes -- GitHub pull request integration with checklist rendering -- Project picker dialog for workspace management -- Web viewer for browsing URLs in editor -- Extension system core architecture with bundled extensions -- Syntax highlighting in diff viewer -- Markdown syntax highlighting -- Terminal font settings customization -- Advanced model selector dropdown in AI chat -- ARIA accessibility descriptors for menus and tabs -- Persistent commands feature -- SSH key authentication with multiple key fallback -- Open File in Editor feature for AI agent -- Slider component and Christmas theme -- Pre-release check script for validating releases - -### Changed -- Replace expandable commits with hover previews in source control -- Improve LSP integration with diagnostics -- Improve scrollbar design -- Auto-refresh Source Control when view becomes active -- Add 1 second delay before showing git blame popover -- Refactor theme system -- Extend Gemini API support - -### Fixed -- Fix LSP popup position -- Fix editor viewport and line alignment bugs -- Fix terminal font rendering for Nerd Fonts -- Fix context menu positioning -- Fix sticky folder background transparency in file tree -- Fix workspace reset and terminal persistence -- Fix drag region for window on macOS -- Fix autosave functionality and dirty state logic -- Fix traffic lights for macOS 26 -- Fix LSP for JavaScript/TypeScript files - -## [0.2.6] - 2025-12-04 - -### Added -- Auto-update system with GitHub Releases integration -- Markdown preview button in editor toolbar -- Deep link support (`athas://` protocol) -- Linting service with Tauri backend support -- Symlink support for file explorer and icon themes -- Keymaps feature with customizable keyboard shortcuts -- External editor support -- Right-click context menu in editor -- Code folding in gutter -- xAI Grok models support -- Gemini 3 Pro Preview support -- Settings search functionality -- Storybook for UI development and testing - -### Changed -- Migrate to tree-sitter-web for syntax highlighting -- Migrate AI chat history to SQLite -- Refactor terminal module and fix auto-create behavior -- Refactor AI chat UI -- Improve LSP client and configuration -- Improve formatter service -- Improve extension system with on-demand architecture - -### Fixed -- Fix editor rendering and extension install UX -- Fix command bar not triggering -- Fix scrolling issues -- Fix Git Blame and other git issues -- Fix line numbers not showing up on big files -- Fix highlighter initialization after extension installation -- Fix HighlightLayer memo bug -- Fix editor selections -- Fix cursor position restoration and tab switching conflicts - -## [0.2.4] - 2025-11-08 - -### Added -- Project tabs for multi-workspace support - -### Changed -- Organize files by feature (vertical slice architecture) -- Refactor editor and fix overall issues - -### Fixed -- Fix Windows build errors in CLI and search commands -- Fix git status rows for nested paths and stage directories correctly - -### Removed -- Remove welcome screen and CLI install prompt - -## [0.2.2] - 2025-10-30 - -### Added -- Release automation system -- Certificate import and notarization for macOS in GitHub Actions - -### Changed -- Vim enhancements - -## [0.2.1] - 2025-10-29 - -### Added -- Code signing for macOS -- New AI models and BYOK (Bring Your Own Key) settings -- Image preview and toolbar -- Extension support for syntax highlighting -- CLI command installation feature -- Setting toggles in command palette - -### Changed -- Improve UX in switching projects -- Make git inline blame popup more compact - -### Fixed -- Fix welcome screen theme responsiveness for light mode -- Fix viewport ref for scrolling -- Fix syntax highlighting initialization -- Fix intrusive UI on the code editor -- Fix traffic light position for macOS - -## [0.1.2] - 2025-10-19 - -### Added -- Markdown preview -- Icon themes -- Multiple AI agents -- Individual zoom for editor, terminal, and window -- Vim commands -- Shortcut for color theme selector -- Shortcut to kill terminal process - -### Changed -- Improve SQLite viewer -- Refactor cursor styles for Vim mode - -### Fixed -- Fix editor syntax and search highlighting -- Fix Vim mode issues -- Fix agent dropdowns -- Fix editor scrolling - -## [0.1.1] - 2025-08-27 - -### Added -- Git blame functionality with inline display and hover details -- Password prompt dialog for SSH connections -- Lines and columns information in footer -- Integrated menu bar with toggle option -- Global search shortcuts -- Keyboard shortcuts to text editor -- Code formatting feature -- Move line up/down functionality -- Cursor position restoration when switching files -- Highlight search results and click-to-jump -- Shell switching support -- Close editor tabs with middle click - -### Changed -- Enhance remote connection handling -- Improve text selection handling -- Improve command bar performance -- Refactor clipboard utilities to use Tauri clipboard manager - -### Fixed -- Fix Linux menu bar UI -- Fix zooming issues -- Fix context menu positioning when zoomed -- Fix completion dropdown not hiding after acceptance -- Fix auto-pairing logic - -## [0.1.0] - 2025-08-12 - -Initial release - ---- - -[0.3.2]: https://github.com/athasdev/athas/compare/v0.3.1...v0.3.2 -[0.3.1]: https://github.com/athasdev/athas/compare/v0.3.0...v0.3.1 -[0.3.0]: https://github.com/athasdev/athas/compare/v0.2.6...v0.3.0 -[0.2.6]: https://github.com/athasdev/athas/compare/v0.2.5...v0.2.6 -[0.2.5]: https://github.com/athasdev/athas/compare/v0.2.4...v0.2.5 -[0.2.4]: https://github.com/athasdev/athas/compare/v0.2.2...v0.2.4 -[0.2.2]: https://github.com/athasdev/athas/compare/v0.2.1...v0.2.2 -[0.2.1]: https://github.com/athasdev/athas/compare/v0.1.2...v0.2.1 -[0.1.2]: https://github.com/athasdev/athas/compare/v0.1.1...v0.1.2 -[0.1.1]: https://github.com/athasdev/athas/compare/v0.1.0...v0.1.1 -[0.1.0]: https://github.com/athasdev/athas/releases/tag/v0.1.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 71b43d0e..06442386 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,14 +1,6 @@ # Contributing to Athas -Thank you for contributing to Athas! - -Please check existing issues and pull requests before creating new ones. - -## Getting Started - -**Small changes** (bug fixes, typos): Submit a PR directly. - -**Large changes** (new features, major refactors): Open an issue first to discuss. +Thank you for contributing to Athas! Please check existing issues and pull requests before creating new ones. ## Setup @@ -21,26 +13,26 @@ Prerequisites: ```bash bun install -bun tauri dev +bun dev ``` ## Before Submitting 1. Code passes checks: `bun check` 2. Auto-fix issues: `bun fix` -3. App runs: `bun tauri dev` +3. App runs: `bun dev` 4. Rebase on master: `git rebase origin/master` 5. Squash commits into logical units +6. Review and agree to the + [Contributor License and Feedback Agreement](CONTRIBUTOR_LICENSE_AND_FEEDBACK_AGREEMENT.md) ## Guidelines -- Follow [code style](docs/contributing/code-style.md) -- Use descriptive commit messages (present tense, capitalized) +- Follow the existing code style +- Use descriptive commit messages (i.e., "Add autocompletion") - One logical change per commit - Update documentation if needed ## Documentation -- [Code Style](docs/contributing/code-style.md) -- [Architecture](docs/contributing/architecture.md) - [Releasing](docs/contributing/releasing.md) diff --git a/CONTRIBUTOR_LICENSE_AND_FEEDBACK_AGREEMENT.md b/CONTRIBUTOR_LICENSE_AND_FEEDBACK_AGREEMENT.md new file mode 100644 index 00000000..83b476c9 --- /dev/null +++ b/CONTRIBUTOR_LICENSE_AND_FEEDBACK_AGREEMENT.md @@ -0,0 +1,64 @@ +# Contributor License and Feedback Agreement + +Effective date: 2026-02-18 + +This Contributor License and Feedback Agreement ("Agreement") applies to +contributions you submit to the Athas project repository. + +By submitting a contribution (including code, documentation, tests, design +assets, issue content, or any other material), you agree to the terms below. + +## 1. Definitions + +- "Project" means the Athas repository and related project resources. +- "Contribution" means any work you submit to the Project. +- "Feedback" means suggestions, ideas, bug reports, or improvement proposals. + +## 2. Your Representations + +You represent that: + +- You have the legal right to submit your Contribution. +- Your Contribution is your original work, or you have sufficient rights to + submit it. +- You are not knowingly submitting material that infringes third-party rights. + +## 3. License for Contributions + +You agree that each Contribution you submit is licensed under the same license +as the Project at the time of submission (currently AGPL-3.0, or any later +version if the Project uses "or later" terms). + +To the extent needed for maintainers and users to use your Contribution in the +Project, you grant Athas maintainers and all recipients of the Project a +worldwide, perpetual, non-exclusive, royalty-free, irrevocable copyright +license to use, reproduce, modify, distribute, publicly perform, and publicly +display your Contribution under the Project's license terms. + +## 4. Patent Grant + +If your Contribution includes patentable subject matter, you grant a worldwide, +perpetual, non-exclusive, royalty-free, irrevocable patent license to make, +have made, use, sell, offer for sale, import, and otherwise transfer your +Contribution as part of the Project under the Project's license terms. + +## 5. Feedback License + +You grant Athas maintainers a worldwide, perpetual, non-exclusive, royalty-free, +irrevocable license to use, copy, modify, publish, and otherwise exploit any +Feedback you provide, without restriction and without compensation to you. + +## 6. No Obligation + +Project maintainers are not required to accept, use, or maintain any +Contribution or Feedback. + +## 7. Disclaimer + +Except where required by applicable law, Contributions and Feedback are provided +"as is", without warranties or conditions of any kind. + +## 8. Acceptance + +Submitting a pull request, commit, patch, issue, discussion content, or other +Contribution to the Project constitutes your acceptance of this Agreement. diff --git a/Cargo.lock b/Cargo.lock index fed2adfe..15f4ddf6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -392,13 +392,16 @@ dependencies = [ "futures-util", "git2", "interceptor", + "keyring", "lazy_static", "log", "lsp-types", + "mimalloc", "notify", "notify-debouncer-mini", "nucleo", "nucleo-matcher", + "objc", "portable-pty", "rand 0.8.5", "regex", @@ -2969,6 +2972,16 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "keyring" +version = "3.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eebcc3aff044e5944a8fbaf69eb277d11986064cba30c468730e8b9909fb551c" +dependencies = [ + "log", + "zeroize", +] + [[package]] name = "kqueue" version = "1.1.1" @@ -3077,6 +3090,16 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" +[[package]] +name = "libmimalloc-sys" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "667f4fec20f29dfc6bc7357c582d91796c169ad7e2fce709468aefeb2c099870" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "libredox" version = "0.1.10" @@ -3213,6 +3236,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + [[package]] name = "markup5ever" version = "0.14.1" @@ -3274,6 +3306,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "mimalloc" +version = "0.1.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1ee66a4b64c74f4ef288bcbb9192ad9c3feaad75193129ac8509af543894fd8" +dependencies = [ + "libmimalloc-sys", +] + [[package]] name = "mime" version = "0.3.17" @@ -3535,6 +3576,15 @@ dependencies = [ "libc", ] +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + [[package]] name = "objc-sys" version = "0.3.5" @@ -4892,9 +4942,9 @@ dependencies = [ [[package]] name = "rkyv" -version = "0.7.45" +version = "0.7.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b" +checksum = "2297bf9c81a3f0dc96bc9521370b88f054168c29826a75e89c55ff196e7ed6a1" dependencies = [ "bitvec", "bytecheck", @@ -4910,9 +4960,9 @@ dependencies = [ [[package]] name = "rkyv_derive" -version = "0.7.45" +version = "0.7.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0" +checksum = "84d7b42d4b8d06048d3ac8db0eb31bcb942cbeb709f0b5f2b2ebde398d3038f5" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 0e786723..094fa885 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,3 +5,7 @@ resolver = "2" [workspace.lints.clippy] uninlined_format_args = "allow" new-without-default = "allow" + +[profile.release] +lto = "thin" +codegen-units = 1 diff --git a/README.md b/README.md index fc52a1fd..f15fee18 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,14 @@ ## Features -- External editor support (Neovim, Helix, etc.) -- Git integration - AI agents -- Terminal +- Git integration +- Syntax highlighting +- LSP support +- Vim keybindings +- Integrated terminal +- SQLite viewer +- External editor support ## Download @@ -23,6 +27,8 @@ See the [documentation](https://athas.dev/docs). ## Contributing Contributions are welcome! See the [contributing guide](CONTRIBUTING.md). +Please also review our [Code of Conduct](CODE_OF_CONDUCT.md) and +[Contributor License and Feedback Agreement](CONTRIBUTOR_LICENSE_AND_FEEDBACK_AGREEMENT.md). ## Support diff --git a/ROADMAP.md b/ROADMAP.md deleted file mode 100644 index 664f5faa..00000000 --- a/ROADMAP.md +++ /dev/null @@ -1,59 +0,0 @@ -# Athas Roadmap - -This roadmap outlines the planned features and improvements for Athas Code Editor. - -## In Progress - -### External Editors -Support for opening files in external editors like Neovim, Helix, etc. - -### Editor -Core editing features, performance optimizations, and bug fixes - -### Extensions -Extension API development and extension marketplace setup - -## Planned - -### Vim Keybindings -Visual block mode, command-line improvements, custom keybindings - -### Agents -Code completion, chat interface, and intelligent suggestions - -### Debugger -Breakpoints, call stack, variable inspection, and debugging UI - -## Future Considerations - -### Collaboration -Live share, pair programming, and code review tools - -## Completed - -### File Explorer -Basic file navigation and management within the editor - -### Command Palette -Quick access to commands and settings - -### Quick Open -Fast file opening with fuzzy search - -### Global Search -Search across files in the workspace - -### Tabs -Multi-file editing with tabbed interface - -### Settings -User preferences and configuration options - -### Themes -Customizable editor themes and color schemes - -### Terminal -Integrated terminal for command-line access - -### Git -Diff view, commit, push, pull, and branch management diff --git a/bun.lock b/bun.lock index 90110cb4..34746138 100644 --- a/bun.lock +++ b/bun.lock @@ -5,6 +5,8 @@ "": { "name": "athas-monorepo", "dependencies": { + "@fontsource-variable/geist": "^5.2.8", + "@fontsource-variable/geist-mono": "^5.2.7", "@tanstack/react-virtual": "^3.13.12", "@tauri-apps/api": "^2.9.1", "@tauri-apps/plugin-clipboard-manager": "~2.3.2", @@ -230,6 +232,10 @@ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.8", "", { "os": "win32", "cpu": "x64" }, "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw=="], + "@fontsource-variable/geist": ["@fontsource-variable/geist@5.2.8", "", {}, "sha512-cJ6m9e+8MQ5dCYJsLylfZrgBh6KkG4bOLckB35Tr9J/EqdkEM6QllH5PxqP1dhTvFup+HtMRPuz9xOjxXJggxw=="], + + "@fontsource-variable/geist-mono": ["@fontsource-variable/geist-mono@5.2.7", "", {}, "sha512-ZKlZ5sjtalb2TwXKs400mAGDlt/+2ENLNySPx0wTz3bP3mWARCsUW+rpxzZc7e05d2qGch70pItt3K4qttbIYA=="], + "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.3", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.0" }, "os": "darwin", "cpu": "arm64" }, "sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg=="], "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.3", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.0" }, "os": "darwin", "cpu": "x64" }, "sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA=="], diff --git a/docs/.gitignore b/docs/.gitignore deleted file mode 100644 index 318b18cc..00000000 --- a/docs/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -.next/ -.source/ -node_modules/ -*.log -.DS_Store diff --git a/docs/ai-agents.mdx b/docs/ai-agents.mdx new file mode 100644 index 00000000..df87f8ce --- /dev/null +++ b/docs/ai-agents.mdx @@ -0,0 +1,87 @@ +--- +title: AI Agents +description: Use CLI-based AI coding agents like Claude Code and Codex +--- + +import { Callout } from "fumadocs-ui/components/callout"; +import { Steps, Step } from "fumadocs-ui/components/steps"; + +AI Agents are powerful coding assistants that can read and write files, run commands, and help you build software. + +## Supported Agents + +Athas supports several AI coding agents: + +| Agent | Description | +|-------|-------------| +| **Claude Code** | Anthropic's coding agent | +| **Gemini CLI** | Google's coding agent | +| **Codex CLI** | OpenAI's coding agent | +| **OpenCode** | SST's open-source agent | +| **Kimi CLI** | Moonshot's coding agent | +| **Qwen Code** | Alibaba's coding agent | + +## Getting Started + + + + Install your preferred agent: + + | Agent | Install command | + |-------|-----------------| + | Claude Code | `npm install -g @anthropic-ai/claude-code` | + | Gemini CLI | `npm install -g @anthropic-ai/gemini-cli` | + | Codex CLI | `npm install -g @openai/codex` | + | OpenCode | `go install github.com/sst/opencode@latest` | + | Kimi CLI | `npm install -g @anthropic-ai/kimi-cli` | + | Qwen Code | `pip install qwen-code` | + + + Open the AI panel and select the agent from the dropdown + + + Start chatting - the agent will automatically start when you send a message + + + + + Agents are detected automatically. If an agent appears grayed out, it means it's not installed on your system. + + +## What Agents Can Do + +Unlike regular AI chat, agents can: + +- **Read and write files** in your project +- **Run terminal commands** to build, test, or install packages +- **Search your codebase** to understand context +- **Make multi-file changes** in a single conversation + +## Permissions + +When an agent wants to perform an action (like editing a file or running a command), you'll see a permission prompt: + +- Click **Allow** to approve the action +- Click **Deny** to reject it + +This keeps you in control of what changes are made to your project. + +## Slash Commands + +Some agents support slash commands for quick actions. Type `/` in the chat input to see available commands like: + +- `/web` - Search the web +- `/compact` - Get concise responses +- `/init` - Initialize a project + +Available commands depend on which agent you're using. + +## Switching Modes + +Some agents (like Claude Code) support different modes: + +- **Code Mode** - Full access to read, write, and execute +- **Plan Mode** - Planning only, no file changes +- **Ask Mode** - Questions only, requires approval for actions + +Switch modes using the mode selector in the chat header when available. diff --git a/docs/content/docs/ai-assistant.mdx b/docs/ai-assistant.mdx similarity index 100% rename from docs/content/docs/ai-assistant.mdx rename to docs/ai-assistant.mdx diff --git a/docs/content/docs/ai-providers.mdx b/docs/ai-providers.mdx similarity index 100% rename from docs/content/docs/ai-providers.mdx rename to docs/ai-providers.mdx diff --git a/docs/app/(docs)/[[...slug]]/page.tsx b/docs/app/(docs)/[[...slug]]/page.tsx deleted file mode 100644 index 9e60d9be..00000000 --- a/docs/app/(docs)/[[...slug]]/page.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { source, getOgImageUrl } from "@/lib/source"; -import { DocsPage, DocsBody, DocsTitle, DocsDescription } from "fumadocs-ui/page"; -import { notFound } from "next/navigation"; -import defaultMdxComponents, { createRelativeLink } from "fumadocs-ui/mdx"; -import type { Metadata } from "next"; - -export default async function Page(props: { params: Promise<{ slug?: string[] }> }) { - const params = await props.params; - const page = source.getPage(params.slug); - if (!page) notFound(); - - const MDX = page.data.body; - const slug = params.slug ?? []; - const filePath = slug.length === 0 ? "index.mdx" : `${slug.join("/")}.mdx`; - const components = { - ...defaultMdxComponents, - a: createRelativeLink(source, page), - }; - - return ( - - {page.data.title} - {page.data.description} - - - - - ); -} - -export async function generateStaticParams() { - return source.generateParams(); -} - -export async function generateMetadata(props: { params: Promise<{ slug?: string[] }> }): Promise { - const params = await props.params; - const page = source.getPage(params.slug); - if (!page) notFound(); - - const ogImage = getOgImageUrl(page); - - return { - title: page.data.title, - description: page.data.description, - openGraph: { - title: page.data.title, - description: page.data.description, - type: "article", - images: [ogImage], - }, - twitter: { - card: "summary_large_image", - title: page.data.title, - description: page.data.description, - images: [ogImage], - }, - }; -} diff --git a/docs/app/(docs)/layout.tsx b/docs/app/(docs)/layout.tsx deleted file mode 100644 index 0270317f..00000000 --- a/docs/app/(docs)/layout.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { DocsLayout } from "fumadocs-ui/layouts/docs"; -import type { ReactNode } from "react"; -import { source } from "@/lib/source"; -import Image from "next/image"; -import { Heart } from "lucide-react"; - -function DiscordIcon() { - return ( - - - - ); -} - -function XIcon() { - return ( - - - - ); -} - -export default function Layout({ children }: { children: ReactNode }) { - const basePath = "/docs"; - - return ( - - Athas - Athas - - ), - url: "/", - transparentMode: "top", - }} - sidebar={{ - defaultOpenLevel: 1, - collapsible: true, - banner: ( - - - Sponsor Athas - - ), - }} - githubUrl="https://github.com/athasdev/athas" - links={[ - { - text: "Changelog", - url: "https://github.com/athasdev/athas/blob/master/CHANGELOG.md", - active: "none", - }, - { - type: "icon", - icon: , - text: "Discord", - url: "https://discord.gg/athas", - }, - { - type: "icon", - icon: , - text: "X", - url: "https://x.com/athasindustries", - }, - ]} - > - {children} - - ); -} diff --git a/docs/app/api/search/route.ts b/docs/app/api/search/route.ts deleted file mode 100644 index f7ada4d1..00000000 --- a/docs/app/api/search/route.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { source } from "@/lib/source"; -import { createFromSource } from "fumadocs-core/search/server"; - -export const { GET } = createFromSource(source, { - language: "english", -}); diff --git a/docs/app/global.css b/docs/app/global.css deleted file mode 100644 index 881644eb..00000000 --- a/docs/app/global.css +++ /dev/null @@ -1,58 +0,0 @@ -@import "tailwindcss"; -@import "fumadocs-ui/css/neutral.css"; -@import "fumadocs-ui/css/preset.css"; - -@font-face { - font-family: "Space Grotesk"; - src: url("https://fonts.gstatic.com/s/spacegrotesk/v16/V8mDoQDjQSkFtoMM3T6r8E7mPbF4Cw.woff2") - format("woff2"); - font-weight: 300 700; - font-style: normal; - font-display: swap; -} - -@theme { - --font-sans: "Space Grotesk", ui-sans-serif, system-ui, sans-serif; - - /* Light mode - clean neutral with Athas blue accent (#3b82f6) */ - --color-fd-background: hsl(0, 0%, 100%); - --color-fd-foreground: hsl(0, 0%, 13%); - --color-fd-muted: hsl(0, 0%, 97%); - --color-fd-muted-foreground: hsl(0, 0%, 40%); - --color-fd-border: hsl(0, 0%, 88%); - --color-fd-primary: hsl(217, 91%, 60%); - --color-fd-primary-foreground: hsl(0, 0%, 100%); - --color-fd-accent: hsl(217, 91%, 97%); - --color-fd-accent-foreground: hsl(217, 91%, 45%); - --color-fd-ring: hsl(217, 91%, 60%); - --color-fd-card: hsl(0, 0%, 100%); - --color-fd-card-foreground: hsl(0, 0%, 13%); - --color-fd-popover: hsl(0, 0%, 100%); - --color-fd-popover-foreground: hsl(0, 0%, 13%); - --color-fd-secondary: hsl(0, 0%, 96%); - --color-fd-secondary-foreground: hsl(0, 0%, 13%); -} - -.dark { - /* Dark mode - Athas dark with blue accent (#60a5fa) */ - --color-fd-background: hsl(0, 0%, 10%); - --color-fd-foreground: hsl(0, 0%, 90%); - --color-fd-muted: hsl(0, 0%, 16%); - --color-fd-muted-foreground: hsl(0, 0%, 50%); - --color-fd-border: hsl(0, 0%, 25%); - --color-fd-primary: hsl(213, 94%, 68%); - --color-fd-primary-foreground: hsl(0, 0%, 10%); - --color-fd-accent: hsl(213, 50%, 15%); - --color-fd-accent-foreground: hsl(213, 94%, 75%); - --color-fd-ring: hsl(213, 94%, 68%); - --color-fd-card: hsl(0, 0%, 13%); - --color-fd-card-foreground: hsl(0, 0%, 90%); - --color-fd-popover: hsl(0, 0%, 13%); - --color-fd-popover-foreground: hsl(0, 0%, 90%); - --color-fd-secondary: hsl(0, 0%, 18%); - --color-fd-secondary-foreground: hsl(0, 0%, 90%); -} - -body { - font-family: var(--font-sans); -} diff --git a/docs/app/layout.tsx b/docs/app/layout.tsx deleted file mode 100644 index 17fc2edc..00000000 --- a/docs/app/layout.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { RootProvider } from "fumadocs-ui/provider/next"; -import "fumadocs-ui/style.css"; -import "./global.css"; -import type { Metadata } from "next"; -import type { ReactNode } from "react"; - -const siteUrl = process.env.NEXT_PUBLIC_BASE_URL || "https://athas.dev"; -const basePath = "/docs"; -const docsUrl = `${siteUrl}${basePath}`; - -export const metadata: Metadata = { - title: { - default: "Athas Documentation", - template: "%s | Athas Docs", - }, - description: "Documentation for Athas - A lightweight, cross-platform code editor built with Tauri", - metadataBase: new URL(siteUrl), - icons: { - icon: `${basePath}/icon.png`, - apple: `${basePath}/icon.png`, - }, - openGraph: { - title: "Athas Documentation", - description: "Documentation for Athas - A lightweight, cross-platform code editor built with Tauri", - url: docsUrl, - siteName: "Athas Docs", - type: "website", - }, - twitter: { - card: "summary_large_image", - title: "Athas Documentation", - description: "Documentation for Athas - A lightweight, cross-platform code editor built with Tauri", - creator: "@athasindustries", - }, -}; - -export default function RootLayout({ children }: { children: ReactNode }) { - return ( - - - {children} - - - ); -} diff --git a/docs/app/llms-full.txt/route.ts b/docs/app/llms-full.txt/route.ts deleted file mode 100644 index 508d56bf..00000000 --- a/docs/app/llms-full.txt/route.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { source } from "@/lib/source"; - -export function GET() { - const pages = source.getPages(); - const content = pages - .map((page) => `# ${page.data.title}\n\nURL: ${page.url}\n\n${page.data.description || ""}`) - .join("\n\n---\n\n"); - - return new Response(content, { - headers: { "Content-Type": "text/plain; charset=utf-8" }, - }); -} diff --git a/docs/app/llms.txt/route.ts b/docs/app/llms.txt/route.ts deleted file mode 100644 index 7f9f414b..00000000 --- a/docs/app/llms.txt/route.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { source } from "@/lib/source"; - -export function GET() { - const pages = source.getPages(); - const content = pages - .map((page) => `- [${page.data.title}](${page.url}): ${page.data.description || ""}`) - .join("\n"); - - return new Response( - `# Athas Documentation\n\n${content}`, - { headers: { "Content-Type": "text/plain; charset=utf-8" } } - ); -} diff --git a/docs/app/llms/docs/[[...slug]]/route.ts b/docs/app/llms/docs/[[...slug]]/route.ts deleted file mode 100644 index 9e3ad48e..00000000 --- a/docs/app/llms/docs/[[...slug]]/route.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { source } from "@/lib/source"; -import { notFound } from "next/navigation"; - -export async function GET( - _request: Request, - props: { params: Promise<{ slug?: string[] }> } -) { - const { slug } = await props.params; - const page = source.getPage(slug); - if (!page) notFound(); - - const content = `# ${page.data.title}\n\nURL: ${page.url}\n\n${page.data.description || ""}`; - - return new Response(content, { - headers: { "Content-Type": "text/markdown; charset=utf-8" }, - }); -} - -export function generateStaticParams() { - return source.generateParams(); -} diff --git a/docs/app/og/docs/[...slug]/route.tsx b/docs/app/og/docs/[...slug]/route.tsx deleted file mode 100644 index fa242353..00000000 --- a/docs/app/og/docs/[...slug]/route.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import { source } from "@/lib/source"; -import { ImageResponse } from "next/og"; -import { notFound } from "next/navigation"; - -export async function GET( - _req: Request, - { params }: { params: Promise<{ slug: string[] }> } -) { - const { slug } = await params; - const pageSlug = slug.slice(0, -1); - const page = source.getPage(pageSlug.length === 0 ? undefined : pageSlug); - - if (!page) notFound(); - - return new ImageResponse( - ( -
-
-
-
- A -
- - Athas Docs - -
-
- {page.data.title} -
- {page.data.description && ( -
- {page.data.description} -
- )} -
- athas.dev/docs -
-
- ), - { width: 1200, height: 630 } - ); -} - -export function generateStaticParams() { - return source.generateParams().map((params) => ({ - slug: [...(params.slug ?? []), "image.png"], - })); -} diff --git a/docs/content/docs/command-palette.mdx b/docs/command-palette.mdx similarity index 100% rename from docs/content/docs/command-palette.mdx rename to docs/command-palette.mdx diff --git a/docs/content/docs/extension-development.mdx b/docs/extension-development.mdx similarity index 100% rename from docs/content/docs/extension-development.mdx rename to docs/extension-development.mdx diff --git a/docs/content/docs/git-branches.mdx b/docs/git-branches.mdx similarity index 100% rename from docs/content/docs/git-branches.mdx rename to docs/git-branches.mdx diff --git a/docs/content/docs/git-diff.mdx b/docs/git-diff.mdx similarity index 100% rename from docs/content/docs/git-diff.mdx rename to docs/git-diff.mdx diff --git a/docs/content/docs/git-integration.mdx b/docs/git-integration.mdx similarity index 100% rename from docs/content/docs/git-integration.mdx rename to docs/git-integration.mdx diff --git a/docs/content/docs/index.mdx b/docs/index.mdx similarity index 80% rename from docs/content/docs/index.mdx rename to docs/index.mdx index 41d97176..9f170c53 100644 --- a/docs/content/docs/index.mdx +++ b/docs/index.mdx @@ -34,22 +34,22 @@ Athas is a lightweight, cross-platform code editor focused on speed and modern w ## Where to Go Next - + Get Athas installed on your OS. - + Run commands without leaving the editor. - + Work across multiple projects in one window. - + Customize shortcuts to match your workflow. - + Set up the built-in AI chat. - + Stage, commit, and review diffs. diff --git a/docs/content/docs/installation.mdx b/docs/installation.mdx similarity index 100% rename from docs/content/docs/installation.mdx rename to docs/installation.mdx diff --git a/docs/content/docs/keymaps.mdx b/docs/keymaps.mdx similarity index 100% rename from docs/content/docs/keymaps.mdx rename to docs/keymaps.mdx diff --git a/docs/lib/cn.ts b/docs/lib/cn.ts deleted file mode 100644 index 7fda2964..00000000 --- a/docs/lib/cn.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { twMerge } from "tailwind-merge"; -import { clsx, type ClassValue } from "clsx"; - -export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)); -} diff --git a/docs/lib/source.ts b/docs/lib/source.ts deleted file mode 100644 index 5010870e..00000000 --- a/docs/lib/source.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { docs } from "@/.source/server"; -import { loader, type InferPageType } from "fumadocs-core/source"; -import { icons } from "lucide-react"; -import { createElement } from "react"; - -export const source = loader({ - baseUrl: "/", - source: docs.toFumadocsSource(), - icon(icon) { - if (!icon) return undefined; - if (icon in icons) { - return createElement(icons[icon as keyof typeof icons]); - } - return undefined; - }, -}); - -export function getOgImageUrl(page: InferPageType) { - const segments = [...page.slugs, "image.png"]; - return `/docs/og/docs/${segments.join("/")}`; -} diff --git a/docs/content/docs/meta.json b/docs/meta.json similarity index 96% rename from docs/content/docs/meta.json rename to docs/meta.json index 7aa6e3c7..0544652c 100644 --- a/docs/content/docs/meta.json +++ b/docs/meta.json @@ -17,6 +17,7 @@ "extension-development", "---AI---", "ai-assistant", + "ai-agents", "ai-providers", "---Git---", "git-integration", diff --git a/docs/content/docs/minimap.mdx b/docs/minimap.mdx similarity index 100% rename from docs/content/docs/minimap.mdx rename to docs/minimap.mdx diff --git a/docs/next-env.d.ts b/docs/next-env.d.ts deleted file mode 100644 index c4b7818f..00000000 --- a/docs/next-env.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -/// -/// -import "./.next/dev/types/routes.d.ts"; - -// NOTE: This file should not be edited -// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/docs/next.config.ts b/docs/next.config.ts deleted file mode 100644 index 4ba7a638..00000000 --- a/docs/next.config.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { createMDX } from "fumadocs-mdx/next"; - -const withMDX = createMDX(); - -export default withMDX({ - reactStrictMode: true, - basePath: "/docs", -}); diff --git a/docs/package.json b/docs/package.json deleted file mode 100644 index 0f510481..00000000 --- a/docs/package.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "name": "athas-docs", - "version": "0.1.0", - "private": true, - "scripts": { - "dev": "next dev -p 3001", - "build": "next build", - "start": "next start", - "postinstall": "fumadocs-mdx" - }, - "dependencies": { - "@radix-ui/react-collapsible": "^1.1.12", - "@radix-ui/react-popover": "^1.1.15", - "class-variance-authority": "^0.7.1", - "fumadocs-core": "^16.5.0", - "fumadocs-mdx": "^14.2.6", - "fumadocs-ui": "^16.5.0", - "lucide-react": "^0.563.0", - "next": "^16.1.6", - "react": "^19.2.4", - "react-dom": "^19.2.4", - "tailwind-merge": "^3.4.0", - "zod": "^4.3.6" - }, - "devDependencies": { - "@tailwindcss/postcss": "^4.1.18", - "@types/react": "^19.2.10", - "@types/react-dom": "^19.2.3", - "autoprefixer": "^10.4.24", - "postcss": "^8.5.6", - "tailwindcss": "^4.1.18", - "typescript": "^5.9.3" - } -} diff --git a/docs/postcss.config.mjs b/docs/postcss.config.mjs deleted file mode 100644 index f69c5d41..00000000 --- a/docs/postcss.config.mjs +++ /dev/null @@ -1,6 +0,0 @@ -export default { - plugins: { - "@tailwindcss/postcss": {}, - autoprefixer: {}, - }, -}; diff --git a/docs/content/docs/project-tabs.mdx b/docs/project-tabs.mdx similarity index 100% rename from docs/content/docs/project-tabs.mdx rename to docs/project-tabs.mdx diff --git a/docs/public/icon.png b/docs/public/icon.png deleted file mode 100644 index 0fd111d3..00000000 Binary files a/docs/public/icon.png and /dev/null differ diff --git a/docs/content/docs/releasing.mdx b/docs/releasing.mdx similarity index 80% rename from docs/content/docs/releasing.mdx rename to docs/releasing.mdx index 1c968d8a..3d8b8a04 100644 --- a/docs/content/docs/releasing.mdx +++ b/docs/releasing.mdx @@ -6,9 +6,8 @@ description: How to create releases for Athas ## Release Process 1. Update version in `package.json` and `src-tauri/Cargo.toml` -2. Update `CHANGELOG.md` with changes -3. Create a git tag: `git tag v1.0.0` -4. Push the tag: `git push origin v1.0.0` +2. Create a git tag: `git tag v1.0.0` +3. Push the tag: `git push origin v1.0.0` ## GitHub Actions diff --git a/docs/content/docs/setup-contributing/linux.mdx b/docs/setup-contributing/linux.mdx similarity index 100% rename from docs/content/docs/setup-contributing/linux.mdx rename to docs/setup-contributing/linux.mdx diff --git a/docs/content/docs/setup-contributing/macos.mdx b/docs/setup-contributing/macos.mdx similarity index 100% rename from docs/content/docs/setup-contributing/macos.mdx rename to docs/setup-contributing/macos.mdx diff --git a/docs/content/docs/setup-contributing/meta.json b/docs/setup-contributing/meta.json similarity index 100% rename from docs/content/docs/setup-contributing/meta.json rename to docs/setup-contributing/meta.json diff --git a/docs/content/docs/setup-contributing/windows.mdx b/docs/setup-contributing/windows.mdx similarity index 100% rename from docs/content/docs/setup-contributing/windows.mdx rename to docs/setup-contributing/windows.mdx diff --git a/docs/source.config.ts b/docs/source.config.ts deleted file mode 100644 index 1c4e035b..00000000 --- a/docs/source.config.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { defineDocs, defineConfig } from "fumadocs-mdx/config"; - -export const docs = defineDocs({ - dir: "content/docs", -}); - -export default defineConfig({ - mdxOptions: { - rehypeCodeOptions: { - themes: { - light: "github-light", - dark: "github-dark", - }, - }, - }, -}); diff --git a/docs/content/docs/terminal.mdx b/docs/terminal.mdx similarity index 100% rename from docs/content/docs/terminal.mdx rename to docs/terminal.mdx diff --git a/docs/content/docs/themes.mdx b/docs/themes.mdx similarity index 100% rename from docs/content/docs/themes.mdx rename to docs/themes.mdx diff --git a/docs/tsconfig.json b/docs/tsconfig.json deleted file mode 100644 index 577fd250..00000000 --- a/docs/tsconfig.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "compilerOptions": { - "baseUrl": ".", - "target": "ESNext", - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], - "allowJs": true, - "skipLibCheck": true, - "strict": true, - "noEmit": true, - "esModuleInterop": true, - "module": "esnext", - "moduleResolution": "bundler", - "resolveJsonModule": true, - "isolatedModules": true, - "jsx": "react-jsx", - "incremental": true, - "plugins": [ - { - "name": "next" - } - ], - "paths": { - "@/*": [ - "./*" - ], - "@/.source/*": [ - "./.source/*" - ] - } - }, - "include": [ - "next-env.d.ts", - "**/*.ts", - "**/*.tsx", - ".next/types/**/*.ts", - ".source/**/*.ts", - ".next/dev/types/**/*.ts" - ], - "exclude": [ - "node_modules" - ] -} diff --git a/docs/content/docs/workspaces.mdx b/docs/workspaces.mdx similarity index 100% rename from docs/content/docs/workspaces.mdx rename to docs/workspaces.mdx diff --git a/extensions/bash/extension.json b/extensions/bash/extension.json deleted file mode 100644 index 791101e3..00000000 --- a/extensions/bash/extension.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "$schema": "https://athas.dev/schemas/extension.json", - "id": "athas.bash", - "name": "Bash", - "displayName": "Bash", - "version": "1.0.0", - "description": "Bash/Shell script support with syntax highlighting and LSP", - "publisher": "Athas", - "categories": ["Language"], - "languages": [ - { - "id": "bash", - "extensions": [".sh", ".bash", ".zsh"], - "aliases": ["Bash", "Shell", "sh"], - "filenames": [".bashrc", ".zshrc", ".bash_profile", ".profile"] - } - ], - "capabilities": { - "grammar": { - "wasmPath": "parser.wasm", - "highlightQuery": "highlights.scm" - }, - "lsp": { - "name": "bash-language-server", - "runtime": "bun", - "package": "bash-language-server", - "args": ["start"] - } - } -} diff --git a/extensions/bash/highlights.scm b/extensions/bash/highlights.scm deleted file mode 100644 index f33a7c2d..00000000 --- a/extensions/bash/highlights.scm +++ /dev/null @@ -1,56 +0,0 @@ -[ - (string) - (raw_string) - (heredoc_body) - (heredoc_start) -] @string - -(command_name) @function - -(variable_name) @property - -[ - "case" - "do" - "done" - "elif" - "else" - "esac" - "export" - "fi" - "for" - "function" - "if" - "in" - "select" - "then" - "unset" - "until" - "while" -] @keyword - -(comment) @comment - -(function_definition name: (word) @function) - -(file_descriptor) @number - -[ - (command_substitution) - (process_substitution) - (expansion) -]@embedded - -[ - "$" - "&&" - ">" - ">>" - "<" - "|" -] @operator - -( - (command (_) @constant) - (#match? @constant "^-") -) diff --git a/extensions/bash/parser.wasm b/extensions/bash/parser.wasm deleted file mode 100755 index 214d0a73..00000000 Binary files a/extensions/bash/parser.wasm and /dev/null differ diff --git a/extensions/c/extension.json b/extensions/c/extension.json deleted file mode 100644 index 71adc4af..00000000 --- a/extensions/c/extension.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "$schema": "https://athas.dev/schemas/extension.json", - "id": "athas.c", - "name": "C", - "displayName": "C", - "version": "1.0.0", - "description": "C language support with syntax highlighting", - "publisher": "Athas", - "categories": ["Language"], - "languages": [ - { - "id": "c", - "extensions": [".c", ".h"], - "aliases": ["C"] - } - ], - "capabilities": { - "grammar": { - "wasmPath": "parser.wasm", - "highlightQuery": "highlights.scm" - } - } -} diff --git a/extensions/c/highlights.scm b/extensions/c/highlights.scm deleted file mode 100644 index 8ee11890..00000000 --- a/extensions/c/highlights.scm +++ /dev/null @@ -1,81 +0,0 @@ -(identifier) @variable - -((identifier) @constant - (#match? @constant "^[A-Z][A-Z\\d_]*$")) - -"break" @keyword -"case" @keyword -"const" @keyword -"continue" @keyword -"default" @keyword -"do" @keyword -"else" @keyword -"enum" @keyword -"extern" @keyword -"for" @keyword -"if" @keyword -"inline" @keyword -"return" @keyword -"sizeof" @keyword -"static" @keyword -"struct" @keyword -"switch" @keyword -"typedef" @keyword -"union" @keyword -"volatile" @keyword -"while" @keyword - -"#define" @keyword -"#elif" @keyword -"#else" @keyword -"#endif" @keyword -"#if" @keyword -"#ifdef" @keyword -"#ifndef" @keyword -"#include" @keyword -(preproc_directive) @keyword - -"--" @operator -"-" @operator -"-=" @operator -"->" @operator -"=" @operator -"!=" @operator -"*" @operator -"&" @operator -"&&" @operator -"+" @operator -"++" @operator -"+=" @operator -"<" @operator -"==" @operator -">" @operator -"||" @operator - -"." @delimiter -";" @delimiter - -(string_literal) @string -(system_lib_string) @string - -(null) @constant -(number_literal) @number -(char_literal) @number - -(field_identifier) @property -(statement_identifier) @label -(type_identifier) @type -(primitive_type) @type -(sized_type_specifier) @type - -(call_expression - function: (identifier) @function) -(call_expression - function: (field_expression - field: (field_identifier) @function)) -(function_declarator - declarator: (identifier) @function) -(preproc_function_def - name: (identifier) @function.special) - -(comment) @comment diff --git a/extensions/c/parser.wasm b/extensions/c/parser.wasm deleted file mode 100755 index ceda238d..00000000 Binary files a/extensions/c/parser.wasm and /dev/null differ diff --git a/extensions/c_sharp/extension.json b/extensions/c_sharp/extension.json deleted file mode 100644 index ecbb1ad7..00000000 --- a/extensions/c_sharp/extension.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "$schema": "https://athas.dev/schemas/extension.json", - "id": "athas.csharp", - "name": "CSharp", - "displayName": "C#", - "version": "1.0.0", - "description": "C# language support with syntax highlighting", - "publisher": "Athas", - "categories": ["Language"], - "languages": [ - { - "id": "csharp", - "extensions": [".cs"], - "aliases": ["C#", "CSharp", "csharp"] - } - ], - "capabilities": { - "grammar": { - "wasmPath": "parser.wasm", - "highlightQuery": "highlights.scm" - } - } -} diff --git a/extensions/c_sharp/highlights.scm b/extensions/c_sharp/highlights.scm deleted file mode 100644 index dbfc6190..00000000 --- a/extensions/c_sharp/highlights.scm +++ /dev/null @@ -1,212 +0,0 @@ -(identifier) @variable - -;; Methods - -(method_declaration name: (identifier) @function) -(local_function_statement name: (identifier) @function) - -;; Types - -(interface_declaration name: (identifier) @type) -(class_declaration name: (identifier) @type) -(enum_declaration name: (identifier) @type) -(struct_declaration (identifier) @type) -(record_declaration (identifier) @type) -(namespace_declaration name: (identifier) @module) - -(generic_name (identifier) @type) -(type_parameter (identifier) @property.definition) -(parameter type: (identifier) @type) -(type_argument_list (identifier) @type) -(as_expression right: (identifier) @type) -(is_expression right: (identifier) @type) - -(constructor_declaration name: (identifier) @constructor) -(destructor_declaration name: (identifier) @constructor) - -(_ type: (identifier) @type) - -(base_list (identifier) @type) - -(predefined_type) @type.builtin - -;; Enum -(enum_member_declaration (identifier) @property.definition) - -;; Literals - -[ - (real_literal) - (integer_literal) -] @number - -[ - (character_literal) - (string_literal) - (raw_string_literal) - (verbatim_string_literal) - (interpolated_string_expression) - (interpolation_start) - (interpolation_quote) - ] @string - -(escape_sequence) @string.escape - -[ - (boolean_literal) - (null_literal) -] @constant.builtin - -;; Comments - -(comment) @comment - -;; Tokens - -[ - ";" - "." - "," -] @punctuation.delimiter - -[ - "--" - "-" - "-=" - "&" - "&=" - "&&" - "+" - "++" - "+=" - "<" - "<=" - "<<" - "<<=" - "=" - "==" - "!" - "!=" - "=>" - ">" - ">=" - ">>" - ">>=" - ">>>" - ">>>=" - "|" - "|=" - "||" - "?" - "??" - "??=" - "^" - "^=" - "~" - "*" - "*=" - "/" - "/=" - "%" - "%=" - ":" -] @operator - -[ - "(" - ")" - "[" - "]" - "{" - "}" - (interpolation_brace) -] @punctuation.bracket - -;; Keywords - -[ - (modifier) - "this" - (implicit_type) -] @keyword - -[ - "add" - "alias" - "as" - "base" - "break" - "case" - "catch" - "checked" - "class" - "continue" - "default" - "delegate" - "do" - "else" - "enum" - "event" - "explicit" - "extern" - "finally" - "for" - "foreach" - "global" - "goto" - "if" - "implicit" - "interface" - "is" - "lock" - "namespace" - "notnull" - "operator" - "params" - "return" - "remove" - "sizeof" - "stackalloc" - "static" - "struct" - "switch" - "throw" - "try" - "typeof" - "unchecked" - "using" - "while" - "new" - "await" - "in" - "yield" - "get" - "set" - "when" - "out" - "ref" - "from" - "where" - "select" - "record" - "init" - "with" - "let" -] @keyword - -;; Attribute - -(attribute name: (identifier) @attribute) - -;; Parameters - -(parameter - name: (identifier) @variable.parameter) - -;; Type constraints - -(type_parameter_constraints_clause (identifier) @property.definition) - -;; Method calls - -(invocation_expression (member_access_expression name: (identifier) @function)) diff --git a/extensions/c_sharp/parser.wasm b/extensions/c_sharp/parser.wasm deleted file mode 100755 index 5c11b4f3..00000000 Binary files a/extensions/c_sharp/parser.wasm and /dev/null differ diff --git a/extensions/cpp/extension.json b/extensions/cpp/extension.json deleted file mode 100644 index c689b718..00000000 --- a/extensions/cpp/extension.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "$schema": "https://athas.dev/schemas/extension.json", - "id": "athas.cpp", - "name": "C++", - "displayName": "C++", - "version": "1.0.0", - "description": "C++ language support with syntax highlighting", - "publisher": "Athas", - "categories": ["Language"], - "languages": [ - { - "id": "cpp", - "extensions": [".cpp", ".cc", ".cxx", ".hpp", ".hh", ".hxx"], - "aliases": ["C++", "cpp"] - } - ], - "capabilities": { - "grammar": { - "wasmPath": "parser.wasm", - "highlightQuery": "highlights.scm" - } - } -} diff --git a/extensions/cpp/highlights.scm b/extensions/cpp/highlights.scm deleted file mode 100644 index 5690b5e3..00000000 --- a/extensions/cpp/highlights.scm +++ /dev/null @@ -1,77 +0,0 @@ -; Functions - -(call_expression - function: (qualified_identifier - name: (identifier) @function)) - -(template_function - name: (identifier) @function) - -(template_method - name: (field_identifier) @function) - -(template_function - name: (identifier) @function) - -(function_declarator - declarator: (qualified_identifier - name: (identifier) @function)) - -(function_declarator - declarator: (field_identifier) @function) - -; Types - -((namespace_identifier) @type - (#match? @type "^[A-Z]")) - -(auto) @type - -; Constants - -(this) @variable.builtin -(null "nullptr" @constant) - -; Modules -(module_name - (identifier) @module) - -; Keywords - -[ - "catch" - "class" - "co_await" - "co_return" - "co_yield" - "constexpr" - "constinit" - "consteval" - "delete" - "explicit" - "final" - "friend" - "mutable" - "namespace" - "noexcept" - "new" - "override" - "private" - "protected" - "public" - "template" - "throw" - "try" - "typename" - "using" - "concept" - "requires" - "virtual" - "import" - "export" - "module" -] @keyword - -; Strings - -(raw_string_literal) @string diff --git a/extensions/cpp/parser.wasm b/extensions/cpp/parser.wasm deleted file mode 100755 index 2d453db9..00000000 Binary files a/extensions/cpp/parser.wasm and /dev/null differ diff --git a/extensions/css/extension.json b/extensions/css/extension.json deleted file mode 100644 index b3e66d3f..00000000 --- a/extensions/css/extension.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "$schema": "https://athas.dev/schemas/extension.json", - "id": "athas.css", - "name": "CSS", - "displayName": "CSS", - "version": "1.0.0", - "description": "CSS language support with syntax highlighting, LSP, and formatting", - "publisher": "Athas", - "categories": ["Language"], - "languages": [ - { - "id": "css", - "extensions": [".css"], - "aliases": ["CSS"] - } - ], - "capabilities": { - "grammar": { - "wasmPath": "parser.wasm", - "highlightQuery": "highlights.scm" - }, - "lsp": { - "name": "vscode-css-language-server", - "runtime": "bun", - "package": "vscode-langservers-extracted", - "args": ["--stdio"] - }, - "formatter": { - "name": "prettier", - "runtime": "bun", - "package": "prettier", - "args": ["--stdin-filepath", "${file}"] - } - } -} diff --git a/extensions/css/highlights.scm b/extensions/css/highlights.scm deleted file mode 100644 index 40c65861..00000000 --- a/extensions/css/highlights.scm +++ /dev/null @@ -1,76 +0,0 @@ -(comment) @comment - -(tag_name) @tag -(nesting_selector) @tag -(universal_selector) @tag - -"~" @operator -">" @operator -"+" @operator -"-" @operator -"*" @operator -"/" @operator -"=" @operator -"^=" @operator -"|=" @operator -"~=" @operator -"$=" @operator -"*=" @operator - -"and" @operator -"or" @operator -"not" @operator -"only" @operator - -(attribute_selector (plain_value) @string) - -((property_name) @variable - (#match? @variable "^--")) -((plain_value) @variable - (#match? @variable "^--")) - -(class_name) @property -(id_name) @property -(namespace_name) @property -(property_name) @property -(feature_name) @property - -(pseudo_element_selector (tag_name) @attribute) -(pseudo_class_selector (class_name) @attribute) -(attribute_name) @attribute - -(function_name) @function - -"@media" @keyword -"@import" @keyword -"@charset" @keyword -"@namespace" @keyword -"@supports" @keyword -"@keyframes" @keyword -(at_keyword) @keyword -(to) @keyword -(from) @keyword -(important) @keyword - -(string_value) @string -(color_value) @string.special - -(integer_value) @number -(float_value) @number -(unit) @type - -[ - "#" - "," - "." - ":" - "::" - ";" -] @punctuation.delimiter - -[ - "{" - ")" - "(" - "}" -] @punctuation.bracket diff --git a/extensions/css/parser.wasm b/extensions/css/parser.wasm deleted file mode 100755 index 24f8a269..00000000 Binary files a/extensions/css/parser.wasm and /dev/null differ diff --git a/extensions/dart/extension.json b/extensions/dart/extension.json deleted file mode 100644 index 29c8e6ae..00000000 --- a/extensions/dart/extension.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "$schema": "https://athas.dev/schemas/extension.json", - "id": "athas.dart", - "name": "Dart", - "displayName": "Dart", - "version": "1.0.0", - "description": "Dart language support with syntax highlighting", - "publisher": "Athas", - "categories": ["Language"], - "languages": [ - { - "id": "dart", - "extensions": [".dart"], - "aliases": ["Dart"] - } - ], - "capabilities": { - "grammar": { - "wasmPath": "parser.wasm", - "highlightQuery": "highlights.scm" - } - } -} diff --git a/extensions/dart/highlights.scm b/extensions/dart/highlights.scm deleted file mode 100644 index 072a10d2..00000000 --- a/extensions/dart/highlights.scm +++ /dev/null @@ -1,303 +0,0 @@ -(identifier) @variable - -(dotted_identifier_list) @string - -; Methods -; -------------------- -; TODO: add method/call_expression to grammar and -; distinguish method call from variable access -(function_expression_body - (identifier) @function.call) - -; ((identifier)(selector (argument_part)) @function) -; NOTE: This query is a bit of a work around for the fact that the dart grammar doesn't -; specifically identify a node as a function call -(((identifier) @function.call - (#lua-match? @function.call "^_?[%l]")) - . - (selector - . - (argument_part))) @function.call - -; Annotations -; -------------------- -(annotation - "@" @attribute - name: (identifier) @attribute) - -; Operators and Tokens -; -------------------- -(template_substitution - "$" @punctuation.special - "{" @punctuation.special - "}" @punctuation.special) @none - -(template_substitution - "$" @punctuation.special - (identifier_dollar_escaped) @variable) @none - -(escape_sequence) @string.escape - -[ - "=>" - ".." - "??" - "==" - "!" - "?" - "&&" - "%" - "<" - ">" - "=" - ">=" - "<=" - "||" - ">>>=" - ">>=" - "<<=" - "&=" - "|=" - "??=" - "%=" - "+=" - "-=" - "*=" - "/=" - "^=" - "~/=" - (shift_operator) - (multiplicative_operator) - (increment_operator) - (is_operator) - (prefix_operator) - (equality_operator) - (additive_operator) -] @operator - -[ - "(" - ")" - "[" - "]" - "{" - "}" -] @punctuation.bracket - -; Delimiters -; -------------------- -[ - ";" - "." - "," - ":" - "?." - "?" -] @punctuation.delimiter - -; Types -; -------------------- -(class_definition - name: (identifier) @type) - -(constructor_signature - name: (identifier) @type) - -(scoped_identifier - scope: (identifier) @type) - -(function_signature - name: (identifier) @function.method) - -(getter_signature - (identifier) @function.method) - -(setter_signature - name: (identifier) @function.method) - -(enum_declaration - name: (identifier) @type) - -(enum_constant - name: (identifier) @type) - -(void_type) @type - -((scoped_identifier - scope: (identifier) @type - name: (identifier) @type) - (#lua-match? @type "^[%u%l]")) - -(type_identifier) @type - -(type_alias - (type_identifier) @type.definition) - -(type_arguments - [ - "<" - ">" - ] @punctuation.bracket) - -; Variables -; -------------------- -; var keyword -(inferred_type) @keyword - -((identifier) @type - (#lua-match? @type "^_?[%u].*[%l]")) ; catch Classes or IClasses not CLASSES - -"Function" @type - -; properties -(unconditional_assignable_selector - (identifier) @property) - -(conditional_assignable_selector - (identifier) @property) - -(this) @variable.builtin - -; Parameters -; -------------------- -(formal_parameter - (identifier) @variable.parameter) - -(named_argument - (label - (identifier) @variable.parameter)) - -; Literals -; -------------------- -[ - (hex_integer_literal) - (decimal_integer_literal) - (decimal_floating_point_literal) - ; TODO: inaccessible nodes - ; (octal_integer_literal) - ; (hex_floating_point_literal) -] @number - -(symbol_literal) @string.special.symbol - -(string_literal) @string - -(true) @boolean - -(false) @boolean - -(null_literal) @constant.builtin - -(comment) @comment @spell - -(documentation_comment) @comment.documentation @spell - -; Keywords -; -------------------- -[ - "import" - "library" - "export" - "as" - "show" - "hide" -] @keyword.import - -; Reserved words (cannot be used as identifiers) -[ - ; TODO: - ; "rethrow" cannot be targeted at all and seems to be an invisible node - ; TODO: - ; the assert keyword cannot be specifically targeted - ; because the grammar selects the whole node or the content - ; of the assertion not just the keyword - ; assert - (case_builtin) - "late" - "required" - "on" - "extends" - "in" - "is" - "new" - "super" - "with" -] @keyword - -[ - "class" - "enum" - "extension" -] @keyword.type - -"return" @keyword.return - -; Built in identifiers: -; alone these are marked as keywords -[ - "deferred" - "factory" - "get" - "implements" - "interface" - "library" - "operator" - "mixin" - "part" - "set" - "typedef" -] @keyword - -[ - "async" - "async*" - "sync*" - "await" - "yield" -] @keyword.coroutine - -[ - (const_builtin) - (final_builtin) - "abstract" - "covariant" - "external" - "static" - "final" - "base" - "sealed" -] @keyword.modifier - -; when used as an identifier: -((identifier) @variable.builtin - (#any-of? @variable.builtin - "abstract" "as" "covariant" "deferred" "dynamic" "export" "external" "factory" "Function" "get" - "implements" "import" "interface" "library" "operator" "mixin" "part" "set" "static" "typedef")) - -[ - "if" - "else" - "switch" - "default" -] @keyword.conditional - -(conditional_expression - [ - "?" - ":" - ] @keyword.conditional.ternary) - -[ - "try" - "throw" - "catch" - "finally" - (break_statement) -] @keyword.exception - -[ - "do" - "while" - "continue" - "for" -] @keyword.repeat diff --git a/extensions/dart/parser.wasm b/extensions/dart/parser.wasm deleted file mode 100755 index 17007b4f..00000000 Binary files a/extensions/dart/parser.wasm and /dev/null differ diff --git a/extensions/elisp/extension.json b/extensions/elisp/extension.json deleted file mode 100644 index 20d0650f..00000000 --- a/extensions/elisp/extension.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "$schema": "https://athas.dev/schemas/extension.json", - "id": "athas.elisp", - "name": "EmacsLisp", - "displayName": "Emacs Lisp", - "version": "1.0.0", - "description": "Emacs Lisp language support with syntax highlighting", - "publisher": "Athas", - "categories": ["Language"], - "languages": [ - { - "id": "elisp", - "extensions": [".el", ".emacs"], - "aliases": ["Emacs Lisp", "elisp"] - } - ], - "capabilities": { - "grammar": { - "wasmPath": "parser.wasm", - "highlightQuery": "highlights.scm" - } - } -} diff --git a/extensions/elisp/highlights.scm b/extensions/elisp/highlights.scm deleted file mode 100644 index d78b960f..00000000 --- a/extensions/elisp/highlights.scm +++ /dev/null @@ -1,72 +0,0 @@ -;; Special forms -[ - "and" - "catch" - "cond" - "condition-case" - "defconst" - "defvar" - "function" - "if" - "interactive" - "lambda" - "let" - "let*" - "or" - "prog1" - "prog2" - "progn" - "quote" - "save-current-buffer" - "save-excursion" - "save-restriction" - "setq" - "setq-default" - "unwind-protect" - "while" -] @keyword - -;; Function definitions -[ - "defun" - "defsubst" - ] @keyword -(function_definition name: (symbol) @function) -(function_definition parameters: (list (symbol) @variable.parameter)) -(function_definition docstring: (string) @comment) - -;; Highlight macro definitions the same way as function definitions. -"defmacro" @keyword -(macro_definition name: (symbol) @function) -(macro_definition parameters: (list (symbol) @variable.parameter)) -(macro_definition docstring: (string) @comment) - -(comment) @comment - -(integer) @number -(float) @number -(char) @number - -(string) @string - -[ - "(" - ")" - "#[" - "[" - "]" -] @punctuation.bracket - -[ - "`" - "#'" - "'" - "," - ",@" -] @operator - -;; Highlight nil and t as constants, unlike other symbols -[ - "nil" - "t" -] @constant.builtin diff --git a/extensions/elisp/parser.wasm b/extensions/elisp/parser.wasm deleted file mode 100755 index 98a7243e..00000000 Binary files a/extensions/elisp/parser.wasm and /dev/null differ diff --git a/extensions/elixir/extension.json b/extensions/elixir/extension.json deleted file mode 100644 index cc869e54..00000000 --- a/extensions/elixir/extension.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "$schema": "https://athas.dev/schemas/extension.json", - "id": "athas.elixir", - "name": "Elixir", - "displayName": "Elixir", - "version": "1.0.0", - "description": "Elixir language support with syntax highlighting", - "publisher": "Athas", - "categories": ["Language"], - "languages": [ - { - "id": "elixir", - "extensions": [".ex", ".exs"], - "aliases": ["Elixir"] - } - ], - "capabilities": { - "grammar": { - "wasmPath": "parser.wasm", - "highlightQuery": "highlights.scm" - } - } -} diff --git a/extensions/elixir/highlights.scm b/extensions/elixir/highlights.scm deleted file mode 100644 index d49f0934..00000000 --- a/extensions/elixir/highlights.scm +++ /dev/null @@ -1,223 +0,0 @@ -; Punctuation - -[ - "%" -] @punctuation - -[ - "," - ";" -] @punctuation.delimiter - -[ - "(" - ")" - "[" - "]" - "{" - "}" - "<<" - ">>" -] @punctuation.bracket - -; Literals - -[ - (boolean) - (nil) -] @constant - -[ - (integer) - (float) -] @number - -(char) @constant - -; Identifiers - -; * regular -(identifier) @variable - -; * unused -( - (identifier) @comment.unused - (#match? @comment.unused "^_") -) - -; * special -( - (identifier) @constant.builtin - (#any-of? @constant.builtin "__MODULE__" "__DIR__" "__ENV__" "__CALLER__" "__STACKTRACE__") -) - -; Comment - -(comment) @comment - -; Quoted content - -(interpolation "#{" @punctuation.special "}" @punctuation.special) @embedded - -(escape_sequence) @string.escape - -[ - (string) - (charlist) -] @string - -[ - (atom) - (quoted_atom) - (keyword) - (quoted_keyword) -] @string.special.symbol - -; Note that we explicitly target sigil quoted start/end, so they are not overridden by delimiters - -(sigil - (sigil_name) @__name__ - quoted_start: _ @string.special - quoted_end: _ @string.special) @string.special - -(sigil - (sigil_name) @__name__ - quoted_start: _ @string - quoted_end: _ @string - (#match? @__name__ "^[sS]$")) @string - -(sigil - (sigil_name) @__name__ - quoted_start: _ @string.regex - quoted_end: _ @string.regex - (#match? @__name__ "^[rR]$")) @string.regex - -; Calls - -; * local function call -(call - target: (identifier) @function) - -; * remote function call -(call - target: (dot - right: (identifier) @function)) - -; * field without parentheses or block -(call - target: (dot - right: (identifier) @property) - .) - -; * remote call without parentheses or block (overrides above) -(call - target: (dot - left: [ - (alias) - (atom) - ] - right: (identifier) @function) - .) - -; * definition keyword -(call - target: (identifier) @keyword - (#any-of? @keyword "def" "defdelegate" "defexception" "defguard" "defguardp" "defimpl" "defmacro" "defmacrop" "defmodule" "defn" "defnp" "defoverridable" "defp" "defprotocol" "defstruct")) - -; * kernel or special forms keyword -(call - target: (identifier) @keyword - (#any-of? @keyword "alias" "case" "cond" "for" "if" "import" "quote" "raise" "receive" "require" "reraise" "super" "throw" "try" "unless" "unquote" "unquote_splicing" "use" "with")) - -; * just identifier in function definition -(call - target: (identifier) @keyword - (arguments - [ - (identifier) @function - (binary_operator - left: (identifier) @function - operator: "when") - ]) - (#any-of? @keyword "def" "defdelegate" "defguard" "defguardp" "defmacro" "defmacrop" "defn" "defnp" "defp")) - -; * pipe into identifier (function call) -(binary_operator - operator: "|>" - right: (identifier) @function) - -; * pipe into identifier (definition) -(call - target: (identifier) @keyword - (arguments - (binary_operator - operator: "|>" - right: (identifier) @variable)) - (#any-of? @keyword "def" "defdelegate" "defguard" "defguardp" "defmacro" "defmacrop" "defn" "defnp" "defp")) - -; * pipe into field without parentheses (function call) -(binary_operator - operator: "|>" - right: (call - target: (dot - right: (identifier) @function))) - -; Operators - -; * capture operand -(unary_operator - operator: "&" - operand: (integer) @operator) - -(operator_identifier) @operator - -(unary_operator - operator: _ @operator) - -(binary_operator - operator: _ @operator) - -(dot - operator: _ @operator) - -(stab_clause - operator: _ @operator) - -; * module attribute -(unary_operator - operator: "@" @attribute - operand: [ - (identifier) @attribute - (call - target: (identifier) @attribute) - (boolean) @attribute - (nil) @attribute - ]) - -; * doc string -(unary_operator - operator: "@" @comment.doc - operand: (call - target: (identifier) @comment.doc.__attribute__ - (arguments - [ - (string) @comment.doc - (charlist) @comment.doc - (sigil - quoted_start: _ @comment.doc - quoted_end: _ @comment.doc) @comment.doc - (boolean) @comment.doc - ])) - (#any-of? @comment.doc.__attribute__ "moduledoc" "typedoc" "doc")) - -; Module - -(alias) @module - -(call - target: (dot - left: (atom) @module)) - -; Reserved keywords - -["when" "and" "or" "not" "in" "not in" "fn" "do" "end" "catch" "rescue" "after" "else"] @keyword diff --git a/extensions/elixir/parser.wasm b/extensions/elixir/parser.wasm deleted file mode 100755 index e4537eb5..00000000 Binary files a/extensions/elixir/parser.wasm and /dev/null differ diff --git a/extensions/elm/extension.json b/extensions/elm/extension.json deleted file mode 100644 index 727d2fee..00000000 --- a/extensions/elm/extension.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "$schema": "https://athas.dev/schemas/extension.json", - "id": "athas.elm", - "name": "Elm", - "displayName": "Elm", - "version": "1.0.0", - "description": "Elm language support with syntax highlighting", - "publisher": "Athas", - "categories": ["Language"], - "languages": [ - { - "id": "elm", - "extensions": [".elm"], - "aliases": ["Elm"] - } - ], - "capabilities": { - "grammar": { - "wasmPath": "parser.wasm", - "highlightQuery": "highlights.scm" - } - } -} diff --git a/extensions/elm/highlights.scm b/extensions/elm/highlights.scm deleted file mode 100644 index 8cd68257..00000000 --- a/extensions/elm/highlights.scm +++ /dev/null @@ -1,76 +0,0 @@ -; Keywords -[ - "if" - "then" - "else" - "let" - "in" - ] @keyword.control.elm -(case) @keyword.control.elm -(of) @keyword.control.elm - -(colon) @keyword.other.elm -(backslash) @keyword.other.elm -(as) @keyword.other.elm -(port) @keyword.other.elm -(exposing) @keyword.other.elm -(alias) @keyword.other.elm -(infix) @keyword.other.elm - -(arrow) @keyword.operator.arrow.elm - -(port) @keyword.other.port.elm - -(type_annotation(lower_case_identifier) @function.elm) -(port_annotation(lower_case_identifier) @function.elm) -(function_declaration_left(lower_case_identifier) @function.elm) -(function_call_expr target: (value_expr) @function.elm) - -(field_access_expr(value_expr(value_qid)) @local.function.elm) -(lower_pattern) @local.function.elm -(record_base_identifier) @local.function.elm - - -(operator_identifier) @keyword.operator.elm -(eq) @keyword.operator.assignment.elm - - -"(" @punctuation.section.braces -")" @punctuation.section.braces - -"|" @keyword.other.elm -"," @punctuation.separator.comma.elm - -(import) @meta.import.elm -(module) @keyword.other.elm - -(number_constant_expr) @constant.numeric.elm - - -(type) @keyword.type.elm - -(type_declaration(upper_case_identifier) @storage.type.elm) -(type_ref) @storage.type.elm -(type_alias_declaration name: (upper_case_identifier) @storage.type.elm) - -(union_variant(upper_case_identifier) @union.elm) -(union_pattern) @union.elm -(value_expr(upper_case_qid(upper_case_identifier)) @union.elm) - -; comments -(line_comment) @comment.elm -(block_comment) @comment.elm - -; strings -(string_escape) @character.escape.elm - -(open_quote) @string.elm -(close_quote) @string.elm -(regular_string_part) @string.elm - -(open_char) @char.elm -(close_char) @char.elm - - -; glsl -(glsl_content) @source.glsl diff --git a/extensions/elm/parser.wasm b/extensions/elm/parser.wasm deleted file mode 100755 index 97c6a306..00000000 Binary files a/extensions/elm/parser.wasm and /dev/null differ diff --git a/extensions/embedded_template/parser.wasm b/extensions/embedded_template/parser.wasm deleted file mode 100755 index 8b617934..00000000 Binary files a/extensions/embedded_template/parser.wasm and /dev/null differ diff --git a/extensions/go/extension.json b/extensions/go/extension.json deleted file mode 100644 index 285e7807..00000000 --- a/extensions/go/extension.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "$schema": "https://athas.dev/schemas/extension.json", - "id": "athas.go", - "name": "Go", - "displayName": "Go", - "version": "1.0.0", - "description": "Go language support with syntax highlighting, LSP, and tooling", - "publisher": "Athas", - "categories": ["Language"], - "languages": [ - { - "id": "go", - "extensions": [".go"], - "aliases": ["Go", "golang"] - } - ], - "capabilities": { - "grammar": { - "wasmPath": "parser.wasm", - "highlightQuery": "highlights.scm" - }, - "lsp": { - "name": "gopls", - "runtime": "go", - "package": "golang.org/x/tools/gopls", - "args": ["serve"] - }, - "linter": { - "name": "golangci-lint", - "runtime": "go", - "package": "github.com/golangci/golangci-lint/cmd/golangci-lint", - "args": ["run", "--out-format", "json"] - } - } -} diff --git a/extensions/go/highlights.scm b/extensions/go/highlights.scm deleted file mode 100644 index 6a3c0ac8..00000000 --- a/extensions/go/highlights.scm +++ /dev/null @@ -1,123 +0,0 @@ -; Function calls - -(call_expression - function: (identifier) @function) - -(call_expression - function: (identifier) @function.builtin - (#match? @function.builtin "^(append|cap|close|complex|copy|delete|imag|len|make|new|panic|print|println|real|recover)$")) - -(call_expression - function: (selector_expression - field: (field_identifier) @function.method)) - -; Function definitions - -(function_declaration - name: (identifier) @function) - -(method_declaration - name: (field_identifier) @function.method) - -; Identifiers - -(type_identifier) @type -(field_identifier) @property -(identifier) @variable - -; Operators - -[ - "--" - "-" - "-=" - ":=" - "!" - "!=" - "..." - "*" - "*" - "*=" - "/" - "/=" - "&" - "&&" - "&=" - "%" - "%=" - "^" - "^=" - "+" - "++" - "+=" - "<-" - "<" - "<<" - "<<=" - "<=" - "=" - "==" - ">" - ">=" - ">>" - ">>=" - "|" - "|=" - "||" - "~" -] @operator - -; Keywords - -[ - "break" - "case" - "chan" - "const" - "continue" - "default" - "defer" - "else" - "fallthrough" - "for" - "func" - "go" - "goto" - "if" - "import" - "interface" - "map" - "package" - "range" - "return" - "select" - "struct" - "switch" - "type" - "var" -] @keyword - -; Literals - -[ - (interpreted_string_literal) - (raw_string_literal) - (rune_literal) -] @string - -(escape_sequence) @escape - -[ - (int_literal) - (float_literal) - (imaginary_literal) -] @number - -[ - (true) - (false) - (nil) - (iota) -] @constant.builtin - -(comment) @comment diff --git a/extensions/go/parser.wasm b/extensions/go/parser.wasm deleted file mode 100755 index a20aba8a..00000000 Binary files a/extensions/go/parser.wasm and /dev/null differ diff --git a/extensions/html/extension.json b/extensions/html/extension.json deleted file mode 100644 index d7cc2ae2..00000000 --- a/extensions/html/extension.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "$schema": "https://athas.dev/schemas/extension.json", - "id": "athas.html", - "name": "HTML", - "displayName": "HTML", - "version": "1.0.0", - "description": "HTML language support with syntax highlighting, LSP, and formatting", - "publisher": "Athas", - "categories": ["Language"], - "languages": [ - { - "id": "html", - "extensions": [".html", ".htm", ".xhtml"], - "aliases": ["HTML"] - } - ], - "capabilities": { - "grammar": { - "wasmPath": "parser.wasm", - "highlightQuery": "highlights.scm" - }, - "lsp": { - "name": "vscode-html-language-server", - "runtime": "bun", - "package": "vscode-langservers-extracted", - "args": ["--stdio"] - }, - "formatter": { - "name": "prettier", - "runtime": "bun", - "package": "prettier", - "args": ["--stdin-filepath", "${file}"] - } - } -} diff --git a/extensions/html/highlights.scm b/extensions/html/highlights.scm deleted file mode 100644 index ea0ff4e3..00000000 --- a/extensions/html/highlights.scm +++ /dev/null @@ -1,13 +0,0 @@ -(tag_name) @tag -(erroneous_end_tag_name) @tag.error -(doctype) @constant -(attribute_name) @attribute -(attribute_value) @string -(comment) @comment - -[ - "<" - ">" - "" -] @punctuation.bracket diff --git a/extensions/html/parser.wasm b/extensions/html/parser.wasm deleted file mode 100755 index 50a00921..00000000 Binary files a/extensions/html/parser.wasm and /dev/null differ diff --git a/extensions/java/extension.json b/extensions/java/extension.json deleted file mode 100644 index 4ec3fbf3..00000000 --- a/extensions/java/extension.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "$schema": "https://athas.dev/schemas/extension.json", - "id": "athas.java", - "name": "Java", - "displayName": "Java", - "version": "1.0.0", - "description": "Java language support with syntax highlighting", - "publisher": "Athas", - "categories": ["Language"], - "languages": [ - { - "id": "java", - "extensions": [".java"], - "aliases": ["Java"] - } - ], - "capabilities": { - "grammar": { - "wasmPath": "parser.wasm", - "highlightQuery": "highlights.scm" - } - } -} diff --git a/extensions/java/highlights.scm b/extensions/java/highlights.scm deleted file mode 100644 index b13b4f46..00000000 --- a/extensions/java/highlights.scm +++ /dev/null @@ -1,149 +0,0 @@ -; Variables - -(identifier) @variable - -; Methods - -(method_declaration - name: (identifier) @function.method) -(method_invocation - name: (identifier) @function.method) -(super) @function.builtin - -; Annotations - -(annotation - name: (identifier) @attribute) -(marker_annotation - name: (identifier) @attribute) - -"@" @operator - -; Types - -(type_identifier) @type - -(interface_declaration - name: (identifier) @type) -(class_declaration - name: (identifier) @type) -(enum_declaration - name: (identifier) @type) - -((field_access - object: (identifier) @type) - (#match? @type "^[A-Z]")) -((scoped_identifier - scope: (identifier) @type) - (#match? @type "^[A-Z]")) -((method_invocation - object: (identifier) @type) - (#match? @type "^[A-Z]")) -((method_reference - . (identifier) @type) - (#match? @type "^[A-Z]")) - -(constructor_declaration - name: (identifier) @type) - -[ - (boolean_type) - (integral_type) - (floating_point_type) - (floating_point_type) - (void_type) -] @type.builtin - -; Constants - -((identifier) @constant - (#match? @constant "^_*[A-Z][A-Z\\d_]+$")) - -; Builtins - -(this) @variable.builtin - -; Literals - -[ - (hex_integer_literal) - (decimal_integer_literal) - (octal_integer_literal) - (decimal_floating_point_literal) - (hex_floating_point_literal) -] @number - -[ - (character_literal) - (string_literal) -] @string -(escape_sequence) @string.escape - -[ - (true) - (false) - (null_literal) -] @constant.builtin - -[ - (line_comment) - (block_comment) -] @comment - -; Keywords - -[ - "abstract" - "assert" - "break" - "case" - "catch" - "class" - "continue" - "default" - "do" - "else" - "enum" - "exports" - "extends" - "final" - "finally" - "for" - "if" - "implements" - "import" - "instanceof" - "interface" - "module" - "native" - "new" - "non-sealed" - "open" - "opens" - "package" - "permits" - "private" - "protected" - "provides" - "public" - "requires" - "record" - "return" - "sealed" - "static" - "strictfp" - "switch" - "synchronized" - "throw" - "throws" - "to" - "transient" - "transitive" - "try" - "uses" - "volatile" - "when" - "while" - "with" - "yield" -] @keyword diff --git a/extensions/java/parser.wasm b/extensions/java/parser.wasm deleted file mode 100755 index 45022a96..00000000 Binary files a/extensions/java/parser.wasm and /dev/null differ diff --git a/extensions/javascript/extension.json b/extensions/javascript/extension.json deleted file mode 100644 index ae39cf2a..00000000 --- a/extensions/javascript/extension.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "$schema": "https://athas.dev/schemas/extension.json", - "id": "athas.javascript", - "name": "JavaScript", - "displayName": "JavaScript", - "version": "1.0.0", - "description": "JavaScript language support with syntax highlighting, LSP, and tooling", - "publisher": "Athas", - "categories": ["Language"], - "languages": [ - { - "id": "javascript", - "extensions": [".js", ".mjs", ".cjs"], - "aliases": ["JavaScript", "js"] - } - ], - "capabilities": { - "grammar": { - "wasmPath": "parser.wasm", - "highlightQuery": "highlights.scm" - }, - "lsp": { - "name": "typescript-language-server", - "runtime": "bun", - "package": "typescript-language-server", - "args": ["--stdio"] - }, - "formatter": { - "name": "prettier", - "runtime": "bun", - "package": "prettier", - "args": ["--stdin-filepath", "${file}"] - }, - "linter": { - "name": "eslint", - "runtime": "bun", - "package": "eslint", - "args": ["--stdin", "--stdin-filename", "${file}", "--format", "json"] - } - } -} diff --git a/extensions/javascript/highlights.scm b/extensions/javascript/highlights.scm deleted file mode 100644 index 9312d682..00000000 --- a/extensions/javascript/highlights.scm +++ /dev/null @@ -1,204 +0,0 @@ -; Variables -;---------- - -(identifier) @variable - -; Properties -;----------- - -(property_identifier) @property - -; Function and method definitions -;-------------------------------- - -(function_expression - name: (identifier) @function) -(function_declaration - name: (identifier) @function) -(method_definition - name: (property_identifier) @function.method) - -(pair - key: (property_identifier) @function.method - value: [(function_expression) (arrow_function)]) - -(assignment_expression - left: (member_expression - property: (property_identifier) @function.method) - right: [(function_expression) (arrow_function)]) - -(variable_declarator - name: (identifier) @function - value: [(function_expression) (arrow_function)]) - -(assignment_expression - left: (identifier) @function - right: [(function_expression) (arrow_function)]) - -; Function and method calls -;-------------------------- - -(call_expression - function: (identifier) @function) - -(call_expression - function: (member_expression - property: (property_identifier) @function.method)) - -; Special identifiers -;-------------------- - -((identifier) @constructor - (#match? @constructor "^[A-Z]")) - -([ - (identifier) - (shorthand_property_identifier) - (shorthand_property_identifier_pattern) - ] @constant - (#match? @constant "^[A-Z_][A-Z\\d_]+$")) - -((identifier) @variable.builtin - (#match? @variable.builtin "^(arguments|module|console|window|document)$") - (#is-not? local)) - -((identifier) @function.builtin - (#eq? @function.builtin "require") - (#is-not? local)) - -; Literals -;--------- - -(this) @variable.builtin -(super) @variable.builtin - -[ - (true) - (false) - (null) - (undefined) -] @constant.builtin - -(comment) @comment - -[ - (string) - (template_string) -] @string - -(regex) @string.special -(number) @number - -; Tokens -;------- - -[ - ";" - (optional_chain) - "." - "," -] @punctuation.delimiter - -[ - "-" - "--" - "-=" - "+" - "++" - "+=" - "*" - "*=" - "**" - "**=" - "/" - "/=" - "%" - "%=" - "<" - "<=" - "<<" - "<<=" - "=" - "==" - "===" - "!" - "!=" - "!==" - "=>" - ">" - ">=" - ">>" - ">>=" - ">>>" - ">>>=" - "~" - "^" - "&" - "|" - "^=" - "&=" - "|=" - "&&" - "||" - "??" - "&&=" - "||=" - "??=" -] @operator - -[ - "(" - ")" - "[" - "]" - "{" - "}" -] @punctuation.bracket - -(template_substitution - "${" @punctuation.special - "}" @punctuation.special) @embedded - -[ - "as" - "async" - "await" - "break" - "case" - "catch" - "class" - "const" - "continue" - "debugger" - "default" - "delete" - "do" - "else" - "export" - "extends" - "finally" - "for" - "from" - "function" - "get" - "if" - "import" - "in" - "instanceof" - "let" - "new" - "of" - "return" - "set" - "static" - "switch" - "target" - "throw" - "try" - "typeof" - "var" - "void" - "while" - "with" - "yield" -] @keyword diff --git a/extensions/javascript/parser.wasm b/extensions/javascript/parser.wasm deleted file mode 100755 index edaeba97..00000000 Binary files a/extensions/javascript/parser.wasm and /dev/null differ diff --git a/extensions/json/extension.json b/extensions/json/extension.json deleted file mode 100644 index 24ef80af..00000000 --- a/extensions/json/extension.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "$schema": "https://athas.dev/schemas/extension.json", - "id": "athas.json", - "name": "JSON", - "displayName": "JSON", - "version": "1.0.0", - "description": "JSON language support with syntax highlighting, LSP, and formatting", - "publisher": "Athas", - "categories": ["Language"], - "languages": [ - { - "id": "json", - "extensions": [".json", ".jsonc"], - "aliases": ["JSON"] - } - ], - "capabilities": { - "grammar": { - "wasmPath": "parser.wasm", - "highlightQuery": "highlights.scm" - }, - "lsp": { - "name": "vscode-json-language-server", - "runtime": "bun", - "package": "vscode-langservers-extracted", - "args": ["--stdio"] - }, - "formatter": { - "name": "prettier", - "runtime": "bun", - "package": "prettier", - "args": ["--stdin-filepath", "${file}"] - } - } -} diff --git a/extensions/json/highlights.scm b/extensions/json/highlights.scm deleted file mode 100644 index ece8392f..00000000 --- a/extensions/json/highlights.scm +++ /dev/null @@ -1,16 +0,0 @@ -(pair - key: (_) @string.special.key) - -(string) @string - -(number) @number - -[ - (null) - (true) - (false) -] @constant.builtin - -(escape_sequence) @escape - -(comment) @comment diff --git a/extensions/json/parser.wasm b/extensions/json/parser.wasm deleted file mode 100755 index 7abe88c4..00000000 Binary files a/extensions/json/parser.wasm and /dev/null differ diff --git a/extensions/kotlin/extension.json b/extensions/kotlin/extension.json deleted file mode 100644 index d0f882e2..00000000 --- a/extensions/kotlin/extension.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "$schema": "https://athas.dev/schemas/extension.json", - "id": "athas.kotlin", - "name": "Kotlin", - "displayName": "Kotlin", - "version": "1.0.0", - "description": "Kotlin language support with syntax highlighting", - "publisher": "Athas", - "categories": ["Language"], - "languages": [ - { - "id": "kotlin", - "extensions": [".kt", ".kts"], - "aliases": ["Kotlin"] - } - ], - "capabilities": { - "grammar": { - "wasmPath": "parser.wasm", - "highlightQuery": "highlights.scm" - } - } -} diff --git a/extensions/kotlin/highlights.scm b/extensions/kotlin/highlights.scm deleted file mode 100644 index 8eda6ef6..00000000 --- a/extensions/kotlin/highlights.scm +++ /dev/null @@ -1,398 +0,0 @@ -; Identifiers -(simple_identifier) @variable - -; `it` keyword inside lambdas -; FIXME: This will highlight the keyword outside of lambdas since tree-sitter -; does not allow us to check for arbitrary nestation -((simple_identifier) @variable.builtin - (#eq? @variable.builtin "it")) - -; `field` keyword inside property getter/setter -; FIXME: This will highlight the keyword outside of getters and setters -; since tree-sitter does not allow us to check for arbitrary nestation -((simple_identifier) @variable.builtin - (#eq? @variable.builtin "field")) - -[ - "this" - "super" - "this@" - "super@" -] @variable.builtin - -; NOTE: for consistency with "super@" -(super_expression - "@" @variable.builtin) - -(class_parameter - (simple_identifier) @variable.member) - -; NOTE: temporary fix for treesitter bug that causes delay in file opening -;(class_body -; (property_declaration -; (variable_declaration -; (simple_identifier) @variable.member))) -; id_1.id_2.id_3: `id_2` and `id_3` are assumed as object properties -(_ - (navigation_suffix - (simple_identifier) @variable.member)) - -; SCREAMING CASE identifiers are assumed to be constants -((simple_identifier) @constant - (#lua-match? @constant "^[A-Z][A-Z0-9_]*$")) - -(_ - (navigation_suffix - (simple_identifier) @constant - (#lua-match? @constant "^[A-Z][A-Z0-9_]*$"))) - -(enum_entry - (simple_identifier) @constant) - -(type_identifier) @type - -; '?' operator, replacement for Java @Nullable -(nullable_type) @punctuation.special - -(type_alias - (type_identifier) @type.definition) - -((type_identifier) @type.builtin - (#any-of? @type.builtin - "Byte" "Short" "Int" "Long" "UByte" "UShort" "UInt" "ULong" "Float" "Double" "Boolean" "Char" - "String" "Array" "ByteArray" "ShortArray" "IntArray" "LongArray" "UByteArray" "UShortArray" - "UIntArray" "ULongArray" "FloatArray" "DoubleArray" "BooleanArray" "CharArray" "Map" "Set" - "List" "EmptyMap" "EmptySet" "EmptyList" "MutableMap" "MutableSet" "MutableList")) - -(package_header - "package" @keyword - . - (identifier - (simple_identifier) @module)) - -(import_header - "import" @keyword.import) - -(wildcard_import) @character.special - -; The last `simple_identifier` in a `import_header` will always either be a function -; or a type. Classes can appear anywhere in the import path, unlike functions -(import_header - (identifier - (simple_identifier) @type @_import) - (import_alias - (type_identifier) @type.definition)? - (#lua-match? @_import "^[A-Z]")) - -(import_header - (identifier - (simple_identifier) @function @_import .) - (import_alias - (type_identifier) @function)? - (#lua-match? @_import "^[a-z]")) - -(label) @label - -; Function definitions -(function_declaration - (simple_identifier) @function) - -(getter - "get" @function.builtin) - -(setter - "set" @function.builtin) - -(primary_constructor) @constructor - -(secondary_constructor - "constructor" @constructor) - -(constructor_invocation - (user_type - (type_identifier) @constructor)) - -(anonymous_initializer - "init" @constructor) - -(parameter - (simple_identifier) @variable.parameter) - -(parameter_with_optional_type - (simple_identifier) @variable.parameter) - -; lambda parameters -(lambda_literal - (lambda_parameters - (variable_declaration - (simple_identifier) @variable.parameter))) - -; Function calls -; function() -(call_expression - . - (simple_identifier) @function.call) - -; ::function -(callable_reference - . - (simple_identifier) @function.call) - -; object.function() or object.property.function() -(call_expression - (navigation_expression - (navigation_suffix - (simple_identifier) @function.call) .)) - -(call_expression - . - (simple_identifier) @function.builtin - (#any-of? @function.builtin - "arrayOf" "arrayOfNulls" "byteArrayOf" "shortArrayOf" "intArrayOf" "longArrayOf" "ubyteArrayOf" - "ushortArrayOf" "uintArrayOf" "ulongArrayOf" "floatArrayOf" "doubleArrayOf" "booleanArrayOf" - "charArrayOf" "emptyArray" "mapOf" "setOf" "listOf" "emptyMap" "emptySet" "emptyList" - "mutableMapOf" "mutableSetOf" "mutableListOf" "print" "println" "error" "TODO" "run" - "runCatching" "repeat" "lazy" "lazyOf" "enumValues" "enumValueOf" "assert" "check" - "checkNotNull" "require" "requireNotNull" "with" "suspend" "synchronized")) - -; Literals -[ - (line_comment) - (multiline_comment) -] @comment @spell - -((multiline_comment) @comment.documentation - (#lua-match? @comment.documentation "^/[*][*][^*].*[*]/$")) - -(shebang_line) @keyword.directive - -(real_literal) @number.float - -[ - (integer_literal) - (long_literal) - (hex_literal) - (bin_literal) - (unsigned_literal) -] @number - -[ - (null_literal) - ; should be highlighted the same as booleans - (boolean_literal) -] @boolean - -(character_literal) @character - -(string_literal) @string - -; NOTE: Escapes not allowed in multi-line strings -(character_literal - (character_escape_seq) @string.escape) - -; There are 3 ways to define a regex -; - "[abc]?".toRegex() -(call_expression - (navigation_expression - (string_literal) @string.regexp - (navigation_suffix - ((simple_identifier) @_function - (#eq? @_function "toRegex"))))) - -; - Regex("[abc]?") -(call_expression - ((simple_identifier) @_function - (#eq? @_function "Regex")) - (call_suffix - (value_arguments - (value_argument - (string_literal) @string.regexp)))) - -; - Regex.fromLiteral("[abc]?") -(call_expression - (navigation_expression - ((simple_identifier) @_class - (#eq? @_class "Regex")) - (navigation_suffix - ((simple_identifier) @_function - (#eq? @_function "fromLiteral")))) - (call_suffix - (value_arguments - (value_argument - (string_literal) @string.regexp)))) - -; Keywords -(type_alias - "typealias" @keyword) - -(companion_object - "companion" @keyword) - -[ - (class_modifier) - (member_modifier) - (function_modifier) - (property_modifier) - (platform_modifier) - (variance_modifier) - (parameter_modifier) - (visibility_modifier) - (reification_modifier) - (inheritance_modifier) -] @keyword.modifier - -[ - "val" - "var" - ; "typeof" ; NOTE: It is reserved for future use -] @keyword - -[ - "enum" - "class" - "object" - "interface" -] @keyword.type - -[ - "return" - "return@" -] @keyword.return - -"suspend" @keyword.coroutine - -"fun" @keyword.function - -[ - "if" - "else" - "when" -] @keyword.conditional - -[ - "for" - "do" - "while" - "continue" - "continue@" - "break" - "break@" -] @keyword.repeat - -[ - "try" - "catch" - "throw" - "finally" -] @keyword.exception - -(annotation - "@" @attribute - (use_site_target)? @attribute) - -(annotation - (user_type - (type_identifier) @attribute)) - -(annotation - (constructor_invocation - (user_type - (type_identifier) @attribute))) - -(file_annotation - "@" @attribute - "file" @attribute - ":" @attribute) - -(file_annotation - (user_type - (type_identifier) @attribute)) - -(file_annotation - (constructor_invocation - (user_type - (type_identifier) @attribute))) - -; Operators & Punctuation -[ - "!" - "!=" - "!==" - "=" - "==" - "===" - ">" - ">=" - "<" - "<=" - "||" - "&&" - "+" - "++" - "+=" - "-" - "--" - "-=" - "*" - "*=" - "/" - "/=" - "%" - "%=" - "?." - "?:" - "!!" - "is" - "!is" - "in" - "!in" - "as" - "as?" - ".." - "->" -] @operator - -[ - "(" - ")" - "[" - "]" - "{" - "}" -] @punctuation.bracket - -[ - "." - "," - ";" - ":" - "::" -] @punctuation.delimiter - -(super_expression - [ - "<" - ">" - ] @punctuation.delimiter) - -(type_arguments - [ - "<" - ">" - ] @punctuation.delimiter) - -(type_parameters - [ - "<" - ">" - ] @punctuation.delimiter) - -; NOTE: `interpolated_identifier`s can be highlighted in any way -(string_literal - "$" @punctuation.special - (interpolated_identifier) @none @variable) - -(string_literal - "${" @punctuation.special - (interpolated_expression) @none - "}" @punctuation.special) diff --git a/extensions/kotlin/parser.wasm b/extensions/kotlin/parser.wasm deleted file mode 100755 index b0e4db68..00000000 Binary files a/extensions/kotlin/parser.wasm and /dev/null differ diff --git a/extensions/lua/extension.json b/extensions/lua/extension.json deleted file mode 100644 index ccba4362..00000000 --- a/extensions/lua/extension.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "$schema": "https://athas.dev/schemas/extension.json", - "id": "athas.lua", - "name": "Lua", - "displayName": "Lua", - "version": "1.0.0", - "description": "Lua language support with syntax highlighting and LSP", - "publisher": "Athas", - "categories": ["Language"], - "languages": [ - { - "id": "lua", - "extensions": [".lua"], - "aliases": ["Lua"] - } - ], - "capabilities": { - "grammar": { - "wasmPath": "parser.wasm", - "highlightQuery": "highlights.scm" - }, - "lsp": { - "name": "lua-language-server", - "runtime": "binary", - "downloadUrl": "https://github.com/LuaLS/lua-language-server/releases/latest/download/lua-language-server-${version}-${os}-${arch}.tar.gz" - } - } -} diff --git a/extensions/lua/highlights.scm b/extensions/lua/highlights.scm deleted file mode 100644 index 5bdfcab5..00000000 --- a/extensions/lua/highlights.scm +++ /dev/null @@ -1,224 +0,0 @@ -;; Keywords - -"return" @keyword.return - -[ - "goto" - "in" - "local" -] @keyword - -(label_statement) @label - -(break_statement) @keyword - -(do_statement -[ - "do" - "end" -] @keyword) - -(while_statement -[ - "while" - "do" - "end" -] @repeat) - -(repeat_statement -[ - "repeat" - "until" -] @repeat) - -(if_statement -[ - "if" - "elseif" - "else" - "then" - "end" -] @conditional) - -(elseif_statement -[ - "elseif" - "then" - "end" -] @conditional) - -(else_statement -[ - "else" - "end" -] @conditional) - -(for_statement -[ - "for" - "do" - "end" -] @repeat) - -(function_declaration -[ - "function" - "end" -] @keyword.function) - -(function_definition -[ - "function" - "end" -] @keyword.function) - -;; Operators - -[ - "and" - "not" - "or" -] @keyword.operator - -[ - "+" - "-" - "*" - "/" - "%" - "^" - "#" - "==" - "~=" - "<=" - ">=" - "<" - ">" - "=" - "&" - "~" - "|" - "<<" - ">>" - "//" - ".." -] @operator - -;; Punctuations - -[ - ";" - ":" - "," - "." -] @punctuation.delimiter - -;; Brackets - -[ - "(" - ")" - "[" - "]" - "{" - "}" -] @punctuation.bracket - -;; Variables - -(identifier) @variable - -((identifier) @variable.builtin - (#eq? @variable.builtin "self")) - -(variable_list - (attribute - "<" @punctuation.bracket - (identifier) @attribute - ">" @punctuation.bracket)) - -;; Constants - -((identifier) @constant - (#match? @constant "^[A-Z][A-Z_0-9]*$")) - -(vararg_expression) @constant - -(nil) @constant.builtin - -[ - (false) - (true) -] @boolean - -;; Tables - -(field name: (identifier) @field) - -(dot_index_expression field: (identifier) @field) - -(table_constructor -[ - "{" - "}" -] @constructor) - -;; Functions - -(parameters (identifier) @parameter) - -(function_declaration - name: [ - (identifier) @function - (dot_index_expression - field: (identifier) @function) - ]) - -(function_declaration - name: (method_index_expression - method: (identifier) @method)) - -(assignment_statement - (variable_list . - name: [ - (identifier) @function - (dot_index_expression - field: (identifier) @function) - ]) - (expression_list . - value: (function_definition))) - -(table_constructor - (field - name: (identifier) @function - value: (function_definition))) - -(function_call - name: [ - (identifier) @function.call - (dot_index_expression - field: (identifier) @function.call) - (method_index_expression - method: (identifier) @method.call) - ]) - -(function_call - (identifier) @function.builtin - (#any-of? @function.builtin - ;; built-in functions in Lua 5.1 - "assert" "collectgarbage" "dofile" "error" "getfenv" "getmetatable" "ipairs" - "load" "loadfile" "loadstring" "module" "next" "pairs" "pcall" "print" - "rawequal" "rawget" "rawset" "require" "select" "setfenv" "setmetatable" - "tonumber" "tostring" "type" "unpack" "xpcall")) - -;; Others - -(comment) @comment - -(hash_bang_line) @preproc - -(number) @number - -(string) @string - -(escape_sequence) @string.escape diff --git a/extensions/lua/parser.wasm b/extensions/lua/parser.wasm deleted file mode 100755 index 6783ea0c..00000000 Binary files a/extensions/lua/parser.wasm and /dev/null differ diff --git a/extensions/objc/parser.wasm b/extensions/objc/parser.wasm deleted file mode 100755 index 8a347a6a..00000000 Binary files a/extensions/objc/parser.wasm and /dev/null differ diff --git a/extensions/ocaml/extension.json b/extensions/ocaml/extension.json deleted file mode 100644 index 44ccf004..00000000 --- a/extensions/ocaml/extension.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "$schema": "https://athas.dev/schemas/extension.json", - "id": "athas.ocaml", - "name": "OCaml", - "displayName": "OCaml", - "version": "1.0.0", - "description": "OCaml language support with syntax highlighting", - "publisher": "Athas", - "categories": ["Language"], - "languages": [ - { - "id": "ocaml", - "extensions": [".ml", ".mli"], - "aliases": ["OCaml"] - } - ], - "capabilities": { - "grammar": { - "wasmPath": "parser.wasm", - "highlightQuery": "highlights.scm" - } - } -} diff --git a/extensions/ocaml/highlights.scm b/extensions/ocaml/highlights.scm deleted file mode 100644 index 8f73a9fb..00000000 --- a/extensions/ocaml/highlights.scm +++ /dev/null @@ -1,148 +0,0 @@ -; Punctuation -;------------ - -[ - "," "." ";" ":" "=" "|" "~" "?" "+" "-" "!" ">" "&" - "->" ";;" ":>" "+=" ":=" ".." -] @punctuation.delimiter - -["(" ")" "[" "]" "{" "}" "[|" "|]" "[<" "[>"] @punctuation.bracket - -(object_type ["<" ">"] @punctuation.bracket) - -"%" @punctuation.special - -(attribute ["[@" "]"] @punctuation.special) -(item_attribute ["[@@" "]"] @punctuation.special) -(floating_attribute ["[@@@" "]"] @punctuation.special) -(extension ["[%" "]"] @punctuation.special) -(item_extension ["[%%" "]"] @punctuation.special) -(quoted_extension ["{%" "}"] @punctuation.special) -(quoted_item_extension ["{%%" "}"] @punctuation.special) - -; Keywords -;--------- - -[ - "and" "as" "assert" "begin" "class" "constraint" "do" "done" "downto" "effect" - "else" "end" "exception" "external" "for" "fun" "function" "functor" "if" "in" - "include" "inherit" "initializer" "lazy" "let" "match" "method" "module" - "mutable" "new" "nonrec" "object" "of" "open" "private" "rec" "sig" "struct" - "then" "to" "try" "type" "val" "virtual" "when" "while" "with" -] @keyword - -; Operators -;---------- - -[ - (prefix_operator) - (sign_operator) - (pow_operator) - (mult_operator) - (add_operator) - (concat_operator) - (rel_operator) - (and_operator) - (or_operator) - (assign_operator) - (hash_operator) - (indexing_operator) - (let_operator) - (let_and_operator) - (match_operator) -] @operator - -(match_expression (match_operator) @keyword) - -(value_definition [(let_operator) (let_and_operator)] @keyword) - -["*" "#" "::" "<-"] @operator - -; Constants -;---------- - -(boolean) @constant - -[(number) (signed_number)] @number - -[(string) (character)] @string - -(quoted_string "{" @string "}" @string) @string - -(escape_sequence) @escape - -(conversion_specification) @string.special - -; Variables -;---------- - -[(value_name) (type_variable)] @variable - -(value_pattern) @variable.parameter - -; Properties -;----------- - -[(label_name) (field_name) (instance_variable_name)] @property - -; Functions -;---------- - -(let_binding - pattern: (value_name) @function - (parameter)) - -(let_binding - pattern: (value_name) @function - body: [(fun_expression) (function_expression)]) - -(value_specification (value_name) @function) - -(external (value_name) @function) - -(method_name) @function.method - -(application_expression - function: (value_path (value_name) @function)) - -(infix_expression - left: (value_path (value_name) @function) - operator: (concat_operator) @operator - (#eq? @operator "@@")) - -(infix_expression - operator: (rel_operator) @operator - right: (value_path (value_name) @function) - (#eq? @operator "|>")) - -( - (value_name) @function.builtin - (#match? @function.builtin "^(raise(_notrace)?|failwith|invalid_arg)$") -) - -; Types -;------ - -[(class_name) (class_type_name) (type_constructor)] @type - -( - (type_constructor) @type.builtin - (#match? @type.builtin "^(int|char|bytes|string|float|bool|unit|exn|array|list|option|int32|int64|nativeint|format6|lazy_t)$") -) - -[(constructor_name) (tag)] @constructor - -; Modules -;-------- - -[(module_name) (module_type_name)] @module - -; Attributes -;----------- - -(attribute_id) @tag - -; Comments -;--------- - -[(comment) (line_number_directive) (directive) (shebang)] @comment diff --git a/extensions/ocaml/parser.wasm b/extensions/ocaml/parser.wasm deleted file mode 100755 index 6105e8ef..00000000 Binary files a/extensions/ocaml/parser.wasm and /dev/null differ diff --git a/extensions/packages/css/extension.json b/extensions/packages/css/extension.json deleted file mode 100644 index 8af326e8..00000000 --- a/extensions/packages/css/extension.json +++ /dev/null @@ -1,64 +0,0 @@ -{ - "id": "athas.css", - "name": "CSS", - "displayName": "CSS Language Support", - "description": "CSS language support with IntelliSense, formatting, and snippets", - "version": "1.0.0", - "publisher": "Athas", - "license": "MIT", - "category": "language", - "icon": "https://athas.dev/extensions/packages/css/icon.svg", - "capabilities": { - "type": "language", - "languageId": "css", - "fileExtensions": ["css"], - "aliases": ["CSS", "css"], - "grammar": { - "wasmPath": "parsers/tree-sitter-css.wasm", - "highlightQuery": "queries/css/highlights.scm", - "scopeName": "source.css" - }, - "lsp": { - "server": { - "darwin-arm64": "lsp/node_modules/vscode-css-languageserver-bin/cssServerMain.js", - "darwin-x64": "lsp/node_modules/vscode-css-languageserver-bin/cssServerMain.js", - "linux-x64": "lsp/node_modules/vscode-css-languageserver-bin/cssServerMain.js", - "linux-arm64": "lsp/node_modules/vscode-css-languageserver-bin/cssServerMain.js", - "win32-x64": "lsp/node_modules/vscode-css-languageserver-bin/cssServerMain.js" - }, - "args": ["--stdio"], - "fileExtensions": [".css"], - "languageIds": ["css"] - } - }, - "installation": { - "minVersion": "0.3.0", - "platforms": { - "darwin-arm64": { - "downloadUrl": "https://athas.dev/extensions/packages/css/css-darwin-arm64.tar.gz", - "size": 0, - "checksum": "" - }, - "darwin-x64": { - "downloadUrl": "https://athas.dev/extensions/packages/css/css-darwin-x64.tar.gz", - "size": 0, - "checksum": "" - }, - "linux-x64": { - "downloadUrl": "https://athas.dev/extensions/packages/css/css-linux-x64.tar.gz", - "size": 0, - "checksum": "" - }, - "linux-arm64": { - "downloadUrl": "https://athas.dev/extensions/packages/css/css-linux-arm64.tar.gz", - "size": 0, - "checksum": "" - }, - "win32-x64": { - "downloadUrl": "https://athas.dev/extensions/packages/css/css-win32-x64.tar.gz", - "size": 0, - "checksum": "" - } - } - } -} diff --git a/extensions/packages/go/build.sh b/extensions/packages/go/build.sh deleted file mode 100644 index f638e8cf..00000000 --- a/extensions/packages/go/build.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -PKG_DIR="$(cd "$(dirname "$0")" && pwd)" -ROOT_DIR="$(cd "$(dirname "$0")/../../../.." && pwd)" -echo "Building go tools tarballs..." - -for platform in darwin-arm64 darwin-x64 linux-x64 win32-x64; do - name="go-$platform.tar.gz" - tmpdir="$(mktemp -d)" - mkdir -p "$tmpdir/lsp" - for bin in gopls gofumpt golangci-lint; do - src="$PKG_DIR/lsp/${bin}-$platform" - [ "$platform" = "win32-x64" ] && src+=".exe" || true - if [ -f "$src" ]; then - cp "$src" "$tmpdir/lsp/" || true - chmod +x "$tmpdir/lsp/"* || true - else - echo "[WARN] Missing $src" - fi - done - (cd "$tmpdir" && tar -czf "$PKG_DIR/$name" .) - rm -rf "$tmpdir" - echo "Created $name" -done - -echo "Updating checksums in extension.json..." -bun run "$ROOT_DIR/www/scripts/update-checksums.ts" -echo "Done." - diff --git a/extensions/packages/go/extension.json b/extensions/packages/go/extension.json deleted file mode 100644 index 75fd644b..00000000 --- a/extensions/packages/go/extension.json +++ /dev/null @@ -1,74 +0,0 @@ -{ - "id": "athas.go-full", - "name": "Go Tools", - "displayName": "Go Tools", - "description": "gopls, gofumpt, and golangci-lint packaged for Athas", - "version": "1.0.0", - "publisher": "Athas", - "license": "MIT", - "category": "language", - "capabilities": { - "type": "language", - "languageId": "go", - "fileExtensions": ["go"], - "aliases": ["Go", "golang"], - "lsp": { - "server": { - "darwin-arm64": "lsp/gopls-darwin", - "darwin-x64": "lsp/gopls-darwin", - "linux-x64": "lsp/gopls-linux", - "win32-x64": "lsp/gopls.exe" - }, - "args": [], - "fileExtensions": [".go"], - "languageIds": ["go"] - }, - "formatter": { - "command": { - "darwin-arm64": "lsp/gofumpt-darwin", - "darwin-x64": "lsp/gofumpt-darwin", - "linux-x64": "lsp/gofumpt-linux", - "win32-x64": "lsp/gofumpt.exe" - }, - "args": [], - "inputMethod": "stdin", - "outputMethod": "stdout" - }, - "linter": { - "command": { - "darwin-arm64": "lsp/golangci-lint-darwin", - "darwin-x64": "lsp/golangci-lint-darwin", - "linux-x64": "lsp/golangci-lint-linux", - "win32-x64": "lsp/golangci-lint.exe" - }, - "args": ["run", "--out-format", "json"], - "diagnosticFormat": "regex" - } - }, - "installation": { - "minVersion": "0.3.0", - "platforms": { - "darwin-arm64": { - "downloadUrl": "https://athas.dev/extensions/packages/go/go-darwin-arm64.tar.gz", - "size": 0, - "checksum": "" - }, - "darwin-x64": { - "downloadUrl": "https://athas.dev/extensions/packages/go/go-darwin-x64.tar.gz", - "size": 0, - "checksum": "" - }, - "linux-x64": { - "downloadUrl": "https://athas.dev/extensions/packages/go/go-linux-x64.tar.gz", - "size": 0, - "checksum": "" - }, - "win32-x64": { - "downloadUrl": "https://athas.dev/extensions/packages/go/go-win32-x64.tar.gz", - "size": 0, - "checksum": "" - } - } - } -} - diff --git a/extensions/packages/html/extension.json b/extensions/packages/html/extension.json deleted file mode 100644 index 65a69b08..00000000 --- a/extensions/packages/html/extension.json +++ /dev/null @@ -1,64 +0,0 @@ -{ - "id": "athas.html", - "name": "HTML", - "displayName": "HTML Language Support", - "description": "HTML language support with IntelliSense, formatting, and snippets", - "version": "1.0.0", - "publisher": "Athas", - "license": "MIT", - "category": "language", - "icon": "https://athas.dev/extensions/packages/html/icon.svg", - "capabilities": { - "type": "language", - "languageId": "html", - "fileExtensions": ["html", "htm"], - "aliases": ["HTML", "html"], - "grammar": { - "wasmPath": "parsers/tree-sitter-html.wasm", - "highlightQuery": "queries/html/highlights.scm", - "scopeName": "text.html.basic" - }, - "lsp": { - "server": { - "darwin-arm64": "lsp/node_modules/vscode-html-languageserver-bin/htmlServerMain.js", - "darwin-x64": "lsp/node_modules/vscode-html-languageserver-bin/htmlServerMain.js", - "linux-x64": "lsp/node_modules/vscode-html-languageserver-bin/htmlServerMain.js", - "linux-arm64": "lsp/node_modules/vscode-html-languageserver-bin/htmlServerMain.js", - "win32-x64": "lsp/node_modules/vscode-html-languageserver-bin/htmlServerMain.js" - }, - "args": ["--stdio"], - "fileExtensions": [".html", ".htm"], - "languageIds": ["html"] - } - }, - "installation": { - "minVersion": "0.3.0", - "platforms": { - "darwin-arm64": { - "downloadUrl": "https://athas.dev/extensions/packages/html/html-darwin-arm64.tar.gz", - "size": 0, - "checksum": "" - }, - "darwin-x64": { - "downloadUrl": "https://athas.dev/extensions/packages/html/html-darwin-x64.tar.gz", - "size": 0, - "checksum": "" - }, - "linux-x64": { - "downloadUrl": "https://athas.dev/extensions/packages/html/html-linux-x64.tar.gz", - "size": 0, - "checksum": "" - }, - "linux-arm64": { - "downloadUrl": "https://athas.dev/extensions/packages/html/html-linux-arm64.tar.gz", - "size": 0, - "checksum": "" - }, - "win32-x64": { - "downloadUrl": "https://athas.dev/extensions/packages/html/html-win32-x64.tar.gz", - "size": 0, - "checksum": "" - } - } - } -} diff --git a/extensions/packages/lua/build.sh b/extensions/packages/lua/build.sh deleted file mode 100644 index 86120b2e..00000000 --- a/extensions/packages/lua/build.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -PKG_DIR="$(cd "$(dirname "$0")" && pwd)" -ROOT_DIR="$(cd "$(dirname "$0")/../../../.." && pwd)" -echo "Building lua tools tarballs..." - -for platform in darwin-arm64 darwin-x64 linux-x64 win32-x64; do - name="lua-$platform.tar.gz" - tmpdir="$(mktemp -d)" - mkdir -p "$tmpdir/lsp" - for bin in lua-language-server stylua luacheck; do - src="$PKG_DIR/lsp/${bin}-$platform" - [ "$platform" = "win32-x64" ] && src+=".exe" || true - if [ -f "$src" ]; then - cp "$src" "$tmpdir/lsp/" || true - chmod +x "$tmpdir/lsp/"* || true - else - echo "[WARN] Missing $src" - fi - done - (cd "$tmpdir" && tar -czf "$PKG_DIR/$name" .) - rm -rf "$tmpdir" - echo "Created $name" -done - -echo "Updating checksums in extension.json..." -bun run "$ROOT_DIR/www/scripts/update-checksums.ts" -echo "Done." - diff --git a/extensions/packages/lua/extension.json b/extensions/packages/lua/extension.json deleted file mode 100644 index 2275bd0c..00000000 --- a/extensions/packages/lua/extension.json +++ /dev/null @@ -1,74 +0,0 @@ -{ - "id": "athas.lua-full", - "name": "Lua Tools", - "displayName": "Lua Tools", - "description": "lua-language-server, stylua, and luacheck packaged for Athas", - "version": "1.0.0", - "publisher": "Athas", - "license": "MIT", - "category": "language", - "capabilities": { - "type": "language", - "languageId": "lua", - "fileExtensions": ["lua"], - "aliases": ["Lua"], - "lsp": { - "server": { - "darwin-arm64": "lsp/lua-language-server-darwin", - "darwin-x64": "lsp/lua-language-server-darwin", - "linux-x64": "lsp/lua-language-server-linux", - "win32-x64": "lsp/lua-language-server.exe" - }, - "args": [], - "fileExtensions": [".lua"], - "languageIds": ["lua"] - }, - "formatter": { - "command": { - "darwin-arm64": "lsp/stylua-darwin", - "darwin-x64": "lsp/stylua-darwin", - "linux-x64": "lsp/stylua-linux", - "win32-x64": "lsp/stylua.exe" - }, - "args": ["-"], - "inputMethod": "stdin", - "outputMethod": "stdout" - }, - "linter": { - "command": { - "darwin-arm64": "lsp/luacheck-darwin", - "darwin-x64": "lsp/luacheck-darwin", - "linux-x64": "lsp/luacheck-linux", - "win32-x64": "lsp/luacheck.exe" - }, - "args": ["--formatter", "plain"], - "diagnosticFormat": "regex" - } - }, - "installation": { - "minVersion": "0.3.0", - "platforms": { - "darwin-arm64": { - "downloadUrl": "https://athas.dev/extensions/packages/lua/lua-darwin-arm64.tar.gz", - "size": 0, - "checksum": "" - }, - "darwin-x64": { - "downloadUrl": "https://athas.dev/extensions/packages/lua/lua-darwin-x64.tar.gz", - "size": 0, - "checksum": "" - }, - "linux-x64": { - "downloadUrl": "https://athas.dev/extensions/packages/lua/lua-linux-x64.tar.gz", - "size": 0, - "checksum": "" - }, - "win32-x64": { - "downloadUrl": "https://athas.dev/extensions/packages/lua/lua-win32-x64.tar.gz", - "size": 0, - "checksum": "" - } - } - } -} - diff --git a/extensions/packages/markdown/build.sh b/extensions/packages/markdown/build.sh deleted file mode 100644 index be719f57..00000000 --- a/extensions/packages/markdown/build.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -PKG_DIR="$(cd "$(dirname "$0")" && pwd)" -ROOT_DIR="$(cd "$(dirname "$0")/../../../.." && pwd)" -echo "Building markdown tools tarballs..." - -for platform in darwin-arm64 darwin-x64 linux-x64 win32-x64; do - name="markdown-$platform.tar.gz" - tmpdir="$(mktemp -d)" - mkdir -p "$tmpdir/lsp" - src="$PKG_DIR/lsp/marksman-$platform" - [ "$platform" = "win32-x64" ] && src+=".exe" || true - if [ -f "$src" ]; then - cp "$src" "$tmpdir/lsp/" || true - chmod +x "$tmpdir/lsp/"* || true - else - echo "[WARN] Missing $src" - fi - (cd "$tmpdir" && tar -czf "$PKG_DIR/$name" .) - rm -rf "$tmpdir" - echo "Created $name" -done - -echo "Updating checksums in extension.json..." -bun run "$ROOT_DIR/www/scripts/update-checksums.ts" -echo "Done." - diff --git a/extensions/packages/markdown/extension.json b/extensions/packages/markdown/extension.json deleted file mode 100644 index 15acee94..00000000 --- a/extensions/packages/markdown/extension.json +++ /dev/null @@ -1,64 +0,0 @@ -{ - "id": "athas.markdown", - "name": "Markdown", - "displayName": "Markdown Language Support", - "description": "Markdown language support with IntelliSense and snippets", - "version": "1.0.0", - "publisher": "Athas", - "license": "MIT", - "category": "language", - "icon": "https://athas.dev/extensions/packages/markdown/icon.svg", - "capabilities": { - "type": "language", - "languageId": "markdown", - "fileExtensions": ["md", "markdown", "mdx"], - "aliases": ["Markdown", "md"], - "grammar": { - "wasmPath": "parsers/tree-sitter-markdown.wasm", - "highlightQuery": "queries/markdown/highlights.scm", - "scopeName": "text.md" - }, - "lsp": { - "server": { - "darwin-arm64": "lsp/marksman-darwin-arm64", - "darwin-x64": "lsp/marksman-darwin-x64", - "linux-x64": "lsp/marksman-linux-x64", - "linux-arm64": "lsp/marksman-linux-arm64", - "win32-x64": "lsp/marksman-win32-x64.exe" - }, - "args": ["server"], - "fileExtensions": [".md", ".markdown", ".mdx"], - "languageIds": ["markdown"] - } - }, - "installation": { - "minVersion": "0.3.0", - "platforms": { - "darwin-arm64": { - "downloadUrl": "https://athas.dev/extensions/packages/markdown/markdown-darwin-arm64.tar.gz", - "size": 0, - "checksum": "" - }, - "darwin-x64": { - "downloadUrl": "https://athas.dev/extensions/packages/markdown/markdown-darwin-x64.tar.gz", - "size": 0, - "checksum": "" - }, - "linux-x64": { - "downloadUrl": "https://athas.dev/extensions/packages/markdown/markdown-linux-x64.tar.gz", - "size": 0, - "checksum": "" - }, - "linux-arm64": { - "downloadUrl": "https://athas.dev/extensions/packages/markdown/markdown-linux-arm64.tar.gz", - "size": 0, - "checksum": "" - }, - "win32-x64": { - "downloadUrl": "https://athas.dev/extensions/packages/markdown/markdown-win32-x64.tar.gz", - "size": 0, - "checksum": "" - } - } - } -} diff --git a/extensions/packages/php/build.sh b/extensions/packages/php/build.sh deleted file mode 100755 index 8e8c3808..00000000 --- a/extensions/packages/php/build.sh +++ /dev/null @@ -1,62 +0,0 @@ -#!/bin/bash -set -e - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PACKAGE_DIR="$SCRIPT_DIR" - -echo "Building PHP extension package..." - -# Install build dependencies -echo "Installing build dependencies..." -cd "$PACKAGE_DIR/build" -npm install - -# Build standalone binaries for all platforms -echo "Building standalone Intelephense binaries..." -npm run build:all - -# Download tree-sitter-php.wasm if not exists -if [ ! -f "$PACKAGE_DIR/parsers/tree-sitter-php.wasm" ]; then - echo "Downloading tree-sitter-php.wasm..." - curl -L "https://github.com/nickvdyck/nickvdyck.github.io/raw/master/tree-sitter/tree-sitter-php.wasm" \ - -o "$PACKAGE_DIR/parsers/tree-sitter-php.wasm" -fi - -# Create platform-specific tar.gz packages -echo "Creating platform-specific packages..." - -for platform in darwin-arm64 darwin-x64 linux-x64 win32-x64; do - echo "Creating php-$platform.tar.gz..." - - TEMP_DIR=$(mktemp -d) - - # Copy common files - cp "$PACKAGE_DIR/extension.json" "$TEMP_DIR/" - cp "$PACKAGE_DIR/snippets.json" "$TEMP_DIR/" - cp -r "$PACKAGE_DIR/queries" "$TEMP_DIR/" - cp -r "$PACKAGE_DIR/parsers" "$TEMP_DIR/" - - # Copy platform-specific LSP binary - mkdir -p "$TEMP_DIR/lsp" - if [ "$platform" = "win32-x64" ]; then - cp "$PACKAGE_DIR/lsp/intelephense-$platform.exe" "$TEMP_DIR/lsp/" - else - cp "$PACKAGE_DIR/lsp/intelephense-$platform" "$TEMP_DIR/lsp/" - # Make LSP binary executable - chmod +x "$TEMP_DIR/lsp/intelephense-$platform" - fi - - # Create tar.gz - cd "$TEMP_DIR" - tar -czvf "$PACKAGE_DIR/php-$platform.tar.gz" . - - # Cleanup - rm -rf "$TEMP_DIR" -done - -echo "" -echo "Build complete! Package files created:" -ls -lh "$PACKAGE_DIR"/*.tar.gz - -echo "" -echo "Upload these to athas.dev/extensions/packages/php/" diff --git a/extensions/packages/php/build/index.js b/extensions/packages/php/build/index.js deleted file mode 100644 index b7598ef6..00000000 --- a/extensions/packages/php/build/index.js +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env node -/** - * Standalone Intelephense PHP Language Server - * Entry point for pkg-compiled binary - */ -require('intelephense/lib/intelephense'); diff --git a/extensions/packages/php/build/package.json b/extensions/packages/php/build/package.json deleted file mode 100644 index d88e1aac..00000000 --- a/extensions/packages/php/build/package.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "name": "intelephense-standalone", - "version": "1.0.0", - "description": "Standalone Intelephense PHP Language Server", - "main": "index.js", - "bin": "index.js", - "scripts": { - "build": "npm run build:all", - "build:all": "npm run build:darwin-arm64 && npm run build:darwin-x64 && npm run build:linux-x64 && npm run build:win32-x64", - "build:darwin-arm64": "pkg . --target node18-macos-arm64 --output ../lsp/intelephense-darwin-arm64", - "build:darwin-x64": "pkg . --target node18-macos-x64 --output ../lsp/intelephense-darwin-x64", - "build:linux-x64": "pkg . --target node18-linux-x64 --output ../lsp/intelephense-linux-x64", - "build:win32-x64": "pkg . --target node18-win-x64 --output ../lsp/intelephense-win32-x64.exe" - }, - "pkg": { - "scripts": [], - "assets": [ - "node_modules/intelephense/**/*" - ], - "outputPath": "../lsp" - }, - "dependencies": { - "intelephense": "^1.10.4" - }, - "devDependencies": { - "@yao-pkg/pkg": "^5.12.0" - } -} diff --git a/extensions/packages/php/extension.json b/extensions/packages/php/extension.json deleted file mode 100644 index 4fd3b348..00000000 --- a/extensions/packages/php/extension.json +++ /dev/null @@ -1,70 +0,0 @@ -{ - "id": "athas.php", - "name": "PHP", - "displayName": "PHP Language Support", - "description": "Full PHP language support with IntelliSense, diagnostics, formatting, and snippets via Intelephense", - "version": "1.0.0", - "publisher": "Athas", - "license": "MIT", - "category": "language", - "capabilities": { - "type": "language", - "languageId": "php", - "fileExtensions": ["php", "phtml", "php3", "php4", "php5", "php7", "php8", "phar", "phps"], - "aliases": ["PHP", "php"], - "grammar": { - "wasmPath": "parsers/tree-sitter-php.wasm", - "highlightQuery": "queries/highlights.scm", - "scopeName": "source.php" - }, - "lsp": { - "server": { - "darwin-arm64": "lsp/intelephense-darwin-arm64", - "darwin-x64": "lsp/intelephense-darwin-x64", - "linux-x64": "lsp/intelephense-linux-x64", - "win32-x64": "lsp/intelephense-win32-x64.exe" - }, - "args": ["--stdio"], - "fileExtensions": [".php", ".phtml", ".php3", ".php4", ".php5", ".php7", ".php8", ".phar", ".phps"], - "languageIds": ["php"] - }, - "snippets": "snippets.json", - "commands": [ - { - "command": "php.restartServer", - "title": "Restart PHP Language Server", - "category": "PHP" - }, - { - "command": "php.formatDocument", - "title": "Format PHP Document", - "category": "PHP" - } - ] - }, - "installation": { - "minVersion": "0.2.0", - "platforms": { - "darwin-arm64": { - "downloadUrl": "https://athas.dev/extensions/packages/php/php-darwin-arm64.tar.gz", - "size": 52681335, - "checksum": "5c21da47f7c17cfa798fa2cfd0df905992824f520e8d9930640fcfa5e44ece4d" - }, - "darwin-x64": { - "downloadUrl": "https://athas.dev/extensions/packages/php/php-darwin-x64.tar.gz", - "size": 56850520, - "checksum": "6fa06325af8518b346235f7c86d887a88d04c970398657ac8c8c21482fcb180c" - }, - "linux-x64": { - "downloadUrl": "https://athas.dev/extensions/packages/php/php-linux-x64.tar.gz", - "size": 55510926, - "checksum": "a29aa4bbb04f623bc22826a38d86ccb9590d1f9bf3ad7ddbc05f79522d8f835a" - }, - "win32-x64": { - "downloadUrl": "https://athas.dev/extensions/packages/php/php-win32-x64.tar.gz", - "size": 52036166, - "checksum": "40f2d64fb15330bb950fbc59b44c74dcc74368abafcd8ff502e18b956a478cc5" - } - } - } -} diff --git a/extensions/packages/php/parsers/tree-sitter-php.wasm b/extensions/packages/php/parsers/tree-sitter-php.wasm deleted file mode 100644 index 87693f1d..00000000 --- a/extensions/packages/php/parsers/tree-sitter-php.wasm +++ /dev/null @@ -1,1438 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Page not found · GitHub · GitHub - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
- - - - -
- Skip to content - - - - - - - - - - - -
-
- - - - - - - - - - - - - - - - - -
- -
- - - - - - - - -
- - - - - -
- - - - - - - - - -
-
- - - -
-
- -
-
- 404 “This is not the web page you are looking for” - - - - - - - - - - - - -
-
- -
-
- -
- - -
-
- -
- -
- -
- - - - - - - - - - - - - - - - - - - - - - -
-
-
- - - diff --git a/extensions/packages/php/queries/highlights.scm b/extensions/packages/php/queries/highlights.scm deleted file mode 100644 index 197de168..00000000 --- a/extensions/packages/php/queries/highlights.scm +++ /dev/null @@ -1,111 +0,0 @@ -; PHP highlight query compatible with php_only grammar - -; Comments -(comment) @comment - -; Strings -[ - (string) - (string_content) - (encapsed_string) - (heredoc) - (heredoc_body) - (nowdoc_body) -] @string - -; Numbers -(integer) @number -(float) @number - -; Boolean and null -(boolean) @constant.builtin -(null) @constant.builtin - -; Variables -(variable_name) @variable - -((name) @variable.builtin - (#eq? @variable.builtin "this")) - -; Function definitions and calls -(function_definition - name: (name) @function) - -(method_declaration - name: (name) @function.method) - -(function_call_expression - function: [ - (qualified_name (name)) - (relative_name (name)) - (name) - ] @function) - -(scoped_call_expression - name: (name) @function) - -(member_call_expression - name: (name) @function.method) - -(array_creation_expression "array" @function.builtin) -(list_literal "list" @function.builtin) - -; Class, interface, trait declarations -(class_declaration - name: (name) @type) - -(interface_declaration - name: (name) @type) - -(trait_declaration - name: (name) @type) - -; Types -(primitive_type) @type.builtin -(cast_type) @type.builtin -(named_type [ - (name) @type - (qualified_name (name) @type) - (relative_name (name) @type) -]) - -(scoped_call_expression - scope: [ - (name) @type - (qualified_name (name) @type) - (relative_name (name) @type) - ]) - -; Object creation -(object_creation_expression [ - (name) @constructor - (qualified_name (name) @constructor) - (relative_name (name) @constructor) -]) - -(method_declaration name: (name) @constructor - (#eq? @constructor "__construct")) - -; Properties -(property_element - (variable_name) @property) - -(member_access_expression - name: (variable_name (name)) @property) -(member_access_expression - name: (name) @property) - -; Namespace -(namespace_definition - name: (namespace_name) @module) - -(namespace_name (name) @module) - -; Constants (UPPER_CASE names) -((name) @constant - (#match? @constant "^_?[A-Z][A-Z0-9_]+$")) - -(const_declaration (const_element (name) @constant)) - -; Operators -"$" @operator diff --git a/extensions/packages/php/snippets.json b/extensions/packages/php/snippets.json deleted file mode 100644 index c533d5c6..00000000 --- a/extensions/packages/php/snippets.json +++ /dev/null @@ -1,390 +0,0 @@ -{ - "PHP Opening Tag": { - "prefix": "php", - "body": ["${3:property};", - "}" - ], - "description": "PHP getter method" - }, - "Setter": { - "prefix": "setter", - "body": [ - "public function set${1:Property}(${2:mixed} \\$${3:value}): void", - "{", - "\t\\$this->${4:property} = \\$${3:value};", - "}" - ], - "description": "PHP setter method" - }, - "Try Catch": { - "prefix": "try", - "body": [ - "try {", - "\t$1", - "} catch (${2:\\Exception} \\$e) {", - "\t$3", - "}" - ], - "description": "Try-catch block" - }, - "Try Catch Finally": { - "prefix": "trycf", - "body": [ - "try {", - "\t$1", - "} catch (${2:\\Exception} \\$e) {", - "\t$3", - "} finally {", - "\t$4", - "}" - ], - "description": "Try-catch-finally block" - }, - "For Loop": { - "prefix": "for", - "body": [ - "for (\\$${1:i} = 0; \\$${1:i} < ${2:count}; \\$${1:i}++) {", - "\t$3", - "}" - ], - "description": "For loop" - }, - "Foreach": { - "prefix": "foreach", - "body": [ - "foreach (${1:\\$array} as ${2:\\$item}) {", - "\t$3", - "}" - ], - "description": "Foreach loop" - }, - "Foreach Key Value": { - "prefix": "forek", - "body": [ - "foreach (${1:\\$array} as ${2:\\$key} => ${3:\\$value}) {", - "\t$4", - "}" - ], - "description": "Foreach loop with key" - }, - "While": { - "prefix": "while", - "body": [ - "while (${1:condition}) {", - "\t$2", - "}" - ], - "description": "While loop" - }, - "Do While": { - "prefix": "dowhile", - "body": [ - "do {", - "\t$1", - "} while (${2:condition});" - ], - "description": "Do-while loop" - }, - "If": { - "prefix": "if", - "body": [ - "if (${1:condition}) {", - "\t$2", - "}" - ], - "description": "If statement" - }, - "If Else": { - "prefix": "ife", - "body": [ - "if (${1:condition}) {", - "\t$2", - "} else {", - "\t$3", - "}" - ], - "description": "If-else statement" - }, - "If Elseif Else": { - "prefix": "ifeif", - "body": [ - "if (${1:condition}) {", - "\t$2", - "} elseif (${3:condition}) {", - "\t$4", - "} else {", - "\t$5", - "}" - ], - "description": "If-elseif-else statement" - }, - "Switch": { - "prefix": "switch", - "body": [ - "switch (${1:\\$variable}) {", - "\tcase ${2:value}:", - "\t\t$3", - "\t\tbreak;", - "\tdefault:", - "\t\t$4", - "\t\tbreak;", - "}" - ], - "description": "Switch statement" - }, - "Match": { - "prefix": "match", - "body": [ - "\\$result = match (${1:\\$value}) {", - "\t${2:'case1'} => ${3:'result1'},", - "\t${4:'case2'} => ${5:'result2'},", - "\tdefault => ${6:'default'},", - "};" - ], - "description": "Match expression (PHP 8.0+)" - }, - "Namespace": { - "prefix": "namespace", - "body": ["namespace ${1:App\\\\${2:Namespace}};", ""], - "description": "PHP namespace declaration" - }, - "Use": { - "prefix": "use", - "body": "use ${1:App\\\\${2:Class}};", - "description": "PHP use statement" - }, - "Arrow Function": { - "prefix": "fn", - "body": "fn(${1}) => ${2}", - "description": "Arrow function (PHP 7.4+)" - }, - "Closure": { - "prefix": "closure", - "body": [ - "function (${1}) use (${2}) {", - "\t$3", - "}" - ], - "description": "Closure/anonymous function" - }, - "Array": { - "prefix": "arr", - "body": "[${1}]", - "description": "Array literal" - }, - "Array Map": { - "prefix": "arrmap", - "body": "array_map(fn(\\$${1:item}) => ${2}, ${3:\\$array})", - "description": "Array map with arrow function" - }, - "Array Filter": { - "prefix": "arrfilter", - "body": "array_filter(${1:\\$array}, fn(\\$${2:item}) => ${3})", - "description": "Array filter with arrow function" - }, - "Array Reduce": { - "prefix": "arrreduce", - "body": "array_reduce(${1:\\$array}, fn(\\$${2:carry}, \\$${3:item}) => ${4}, ${5:initial})", - "description": "Array reduce with arrow function" - }, - "Error Log": { - "prefix": "log", - "body": "error_log(${1});", - "description": "Log to error log" - }, - "Var Dump": { - "prefix": "dump", - "body": "var_dump(${1});", - "description": "Dump variable" - }, - "Dump and Die": { - "prefix": "dd", - "body": ["var_dump(${1});", "die();"], - "description": "Dump and die" - }, - "Print R": { - "prefix": "pr", - "body": "print_r(${1}, true);", - "description": "Print readable" - }, - "Echo": { - "prefix": "echo", - "body": "echo ${1};", - "description": "Echo statement" - }, - "Return": { - "prefix": "ret", - "body": "return ${1};", - "description": "Return statement" - }, - "Throw Exception": { - "prefix": "throw", - "body": "throw new ${1:\\Exception}('${2:message}');", - "description": "Throw exception" - }, - "PHPDoc Class": { - "prefix": "docclass", - "body": [ - "/**", - " * ${1:Class description}", - " *", - " * @package ${2:Package}", - " * @author ${3:Author}", - " */" - ], - "description": "PHPDoc for class" - }, - "PHPDoc Method": { - "prefix": "docmethod", - "body": [ - "/**", - " * ${1:Method description}", - " *", - " * @param ${2:type} \\$${3:param}", - " * @return ${4:type}", - " */" - ], - "description": "PHPDoc for method" - }, - "PHPDoc Property": { - "prefix": "docprop", - "body": [ - "/**", - " * @var ${1:type}", - " */" - ], - "description": "PHPDoc for property" - } -} diff --git a/extensions/packages/rust/build.sh b/extensions/packages/rust/build.sh deleted file mode 100644 index 3c479c88..00000000 --- a/extensions/packages/rust/build.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -PKG_DIR="$(cd "$(dirname "$0")" && pwd)" -ROOT_DIR="$(cd "$(dirname "$0")/../../../.." && pwd)" -echo "Building rust tools tarballs..." - -for platform in darwin-arm64 darwin-x64 linux-x64 win32-x64; do - name="rust-$platform.tar.gz" - tmpdir="$(mktemp -d)" - mkdir -p "$tmpdir/lsp" - # Copy expected binaries if present - for bin in rust-analyzer rustfmt cargo; do - src="$PKG_DIR/lsp/${bin}-$platform" - [ "$bin" = "cargo" ] && [ "$platform" = "win32-x64" ] && src="$PKG_DIR/lsp/cargo.exe" || true - [ "$platform" = "win32-x64" ] && [[ "$bin" != "cargo" ]] && src="$src" && src+=".exe" || true - if [ -f "$src" ]; then - cp "$src" "$tmpdir/lsp/" || true - chmod +x "$tmpdir/lsp/"* || true - else - echo "[WARN] Missing $src" - fi - done - (cd "$tmpdir" && tar -czf "$PKG_DIR/$name" .) - rm -rf "$tmpdir" - echo "Created $name" -done - -echo "Updating checksums in extension.json..." -bun run "$ROOT_DIR/www/scripts/update-checksums.ts" -echo "Done." - diff --git a/extensions/packages/rust/extension.json b/extensions/packages/rust/extension.json deleted file mode 100644 index 9e2447fb..00000000 --- a/extensions/packages/rust/extension.json +++ /dev/null @@ -1,74 +0,0 @@ -{ - "id": "athas.rust-full", - "name": "Rust Tools", - "displayName": "Rust Tools", - "description": "rust-analyzer, rustfmt, and clippy packaged for Athas", - "version": "1.0.0", - "publisher": "Athas", - "license": "MIT", - "category": "language", - "icon": "https://athas.dev/extensions/packages/rust/icon.svg", - "capabilities": { - "type": "language", - "languageId": "rust", - "fileExtensions": ["rs"], - "aliases": ["Rust", "rs"], - "lsp": { - "server": { - "darwin-arm64": "lsp/rust-analyzer-darwin", - "darwin-x64": "lsp/rust-analyzer-darwin", - "linux-x64": "lsp/rust-analyzer-linux", - "win32-x64": "lsp/rust-analyzer.exe" - }, - "args": [], - "fileExtensions": [".rs"], - "languageIds": ["rust"] - }, - "formatter": { - "command": { - "darwin-arm64": "lsp/rustfmt-darwin", - "darwin-x64": "lsp/rustfmt-darwin", - "linux-x64": "lsp/rustfmt-linux", - "win32-x64": "lsp/rustfmt.exe" - }, - "args": [], - "inputMethod": "stdin", - "outputMethod": "stdout" - }, - "linter": { - "command": { - "darwin-arm64": "lsp/cargo-darwin", - "darwin-x64": "lsp/cargo-darwin", - "linux-x64": "lsp/cargo-linux", - "win32-x64": "lsp/cargo.exe" - }, - "args": ["clippy", "--message-format", "json"], - "diagnosticFormat": "regex" - } - }, - "installation": { - "minVersion": "0.3.0", - "platforms": { - "darwin-arm64": { - "downloadUrl": "https://athas.dev/extensions/packages/rust/rust-darwin-arm64.tar.gz", - "size": 0, - "checksum": "" - }, - "darwin-x64": { - "downloadUrl": "https://athas.dev/extensions/packages/rust/rust-darwin-x64.tar.gz", - "size": 0, - "checksum": "" - }, - "linux-x64": { - "downloadUrl": "https://athas.dev/extensions/packages/rust/rust-linux-x64.tar.gz", - "size": 0, - "checksum": "" - }, - "win32-x64": { - "downloadUrl": "https://athas.dev/extensions/packages/rust/rust-win32-x64.tar.gz", - "size": 0, - "checksum": "" - } - } - } -} diff --git a/extensions/packages/toml/build.sh b/extensions/packages/toml/build.sh deleted file mode 100644 index 8ca04032..00000000 --- a/extensions/packages/toml/build.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -PKG_DIR="$(cd "$(dirname "$0")" && pwd)" -ROOT_DIR="$(cd "$(dirname "$0")/../../../.." && pwd)" -echo "Building toml tools tarballs..." - -for platform in darwin-arm64 darwin-x64 linux-x64 win32-x64; do - name="toml-$platform.tar.gz" - tmpdir="$(mktemp -d)" - mkdir -p "$tmpdir/lsp" - src="$PKG_DIR/lsp/taplo-$platform" - [ "$platform" = "win32-x64" ] && src+=".exe" || true - if [ -f "$src" ]; then - cp "$src" "$tmpdir/lsp/" || true - chmod +x "$tmpdir/lsp/"* || true - else - echo "[WARN] Missing $src" - fi - (cd "$tmpdir" && tar -czf "$PKG_DIR/$name" .) - rm -rf "$tmpdir" - echo "Created $name" -done - -echo "Updating checksums in extension.json..." -bun run "$ROOT_DIR/www/scripts/update-checksums.ts" -echo "Done." - diff --git a/extensions/packages/toml/extension.json b/extensions/packages/toml/extension.json deleted file mode 100644 index c078de29..00000000 --- a/extensions/packages/toml/extension.json +++ /dev/null @@ -1,64 +0,0 @@ -{ - "id": "athas.toml-full", - "name": "TOML Tools", - "displayName": "TOML Tools", - "description": "Taplo LSP and formatter packaged for Athas", - "version": "1.0.0", - "publisher": "Athas", - "license": "MIT", - "category": "language", - "capabilities": { - "type": "language", - "languageId": "toml", - "fileExtensions": ["toml"], - "aliases": ["TOML", "toml"], - "lsp": { - "server": { - "darwin-arm64": "lsp/taplo-darwin", - "darwin-x64": "lsp/taplo-darwin", - "linux-x64": "lsp/taplo-linux", - "win32-x64": "lsp/taplo.exe" - }, - "args": ["lsp", "stdio"], - "fileExtensions": [".toml"], - "languageIds": ["toml"] - }, - "formatter": { - "command": { - "darwin-arm64": "lsp/taplo-darwin", - "darwin-x64": "lsp/taplo-darwin", - "linux-x64": "lsp/taplo-linux", - "win32-x64": "lsp/taplo.exe" - }, - "args": ["fmt", "-"], - "inputMethod": "stdin", - "outputMethod": "stdout" - } - }, - "installation": { - "minVersion": "0.3.0", - "platforms": { - "darwin-arm64": { - "downloadUrl": "https://athas.dev/extensions/packages/toml/toml-darwin-arm64.tar.gz", - "size": 0, - "checksum": "" - }, - "darwin-x64": { - "downloadUrl": "https://athas.dev/extensions/packages/toml/toml-darwin-x64.tar.gz", - "size": 0, - "checksum": "" - }, - "linux-x64": { - "downloadUrl": "https://athas.dev/extensions/packages/toml/toml-linux-x64.tar.gz", - "size": 0, - "checksum": "" - }, - "win32-x64": { - "downloadUrl": "https://athas.dev/extensions/packages/toml/toml-win32-x64.tar.gz", - "size": 0, - "checksum": "" - } - } - } -} - diff --git a/extensions/packages/web-core/BUILD.md b/extensions/packages/web-core/BUILD.md deleted file mode 100644 index 7d3585c7..00000000 --- a/extensions/packages/web-core/BUILD.md +++ /dev/null @@ -1,19 +0,0 @@ -Web Core Package -================ - -Contents: lsp/node_modules for these packages: - - vscode-html-languageserver-bin - - vscode-css-languageserver-bin - - vscode-json-languageserver-bin - - yaml-language-server - -Build steps: -1. Install packages into `lsp/` (Bun or npm). -2. Create per-platform archives (the content is identical, but we ship per-platform tarballs for consistency): - - web-core-darwin-arm64.tar.gz - - web-core-darwin-x64.tar.gz - - web-core-linux-x64.tar.gz - - web-core-win32-x64.tar.gz -3. Compute SHA-256 for each and update extension.json installation.platforms.*.checksum/size. -4. Upload tarballs to https://athas.dev/extensions/packages/web-core/ and update registry.json. - diff --git a/extensions/packages/web-core/build.sh b/extensions/packages/web-core/build.sh deleted file mode 100644 index 4519f1b3..00000000 --- a/extensions/packages/web-core/build.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/../../../.." && pwd)" -PKG_DIR="$(cd "$(dirname "$0")" && pwd)" - -echo "Building web-core package tarballs..." - -# Expect lsp/node_modules to be present (install with Bun beforehand) -if [ ! -d "$PKG_DIR/lsp/node_modules" ]; then - echo "[WARN] lsp/node_modules not found. Run 'bun init -y && bun add ' under lsp/ first." >&2 -fi - -for platform in darwin-arm64 darwin-x64 linux-x64 win32-x64; do - name="web-core-$platform.tar.gz" - tmpdir="$(mktemp -d)" - mkdir -p "$tmpdir/lsp" - cp -R "$PKG_DIR/lsp/node_modules" "$tmpdir/lsp/" 2>/dev/null || true - (cd "$tmpdir" && tar -czf "$PKG_DIR/$name" .) - rm -rf "$tmpdir" - echo "Created $name" -done - -echo "Updating checksums in extension.json..." -bun run "$ROOT_DIR/www/scripts/update-checksums.ts" -echo "Done." - diff --git a/extensions/packages/web-core/extension.json b/extensions/packages/web-core/extension.json deleted file mode 100644 index 6cf3b979..00000000 --- a/extensions/packages/web-core/extension.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "id": "athas.web-core", - "name": "Web Core", - "displayName": "Web Core Language Servers", - "description": "Shared HTML/CSS/JSON/YAML language servers packaged for Athas", - "version": "1.0.0", - "publisher": "Athas", - "license": "MIT", - "category": "language", - "capabilities": { - "type": "language", - "languageId": "web-core", - "fileExtensions": [], - "aliases": ["web-core"] - }, - "installation": { - "minVersion": "0.3.0", - "platforms": { - "darwin-arm64": { - "downloadUrl": "https://athas.dev/extensions/packages/web-core/web-core-darwin-arm64.tar.gz", - "size": 0, - "checksum": "" - }, - "darwin-x64": { - "downloadUrl": "https://athas.dev/extensions/packages/web-core/web-core-darwin-x64.tar.gz", - "size": 0, - "checksum": "" - }, - "linux-x64": { - "downloadUrl": "https://athas.dev/extensions/packages/web-core/web-core-linux-x64.tar.gz", - "size": 0, - "checksum": "" - }, - "win32-x64": { - "downloadUrl": "https://athas.dev/extensions/packages/web-core/web-core-win32-x64.tar.gz", - "size": 0, - "checksum": "" - } - } - } -} - diff --git a/extensions/php/extension.json b/extensions/php/extension.json deleted file mode 100644 index 8b7075a1..00000000 --- a/extensions/php/extension.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "$schema": "https://athas.dev/schemas/extension.json", - "id": "athas.php", - "name": "PHP", - "displayName": "PHP", - "version": "1.0.0", - "description": "PHP language support with syntax highlighting and LSP", - "publisher": "Athas", - "categories": ["Language"], - "languages": [ - { - "id": "php", - "extensions": [".php", ".phtml", ".php3", ".php4", ".php5"], - "aliases": ["PHP"] - } - ], - "capabilities": { - "grammar": { - "wasmPath": "parser.wasm", - "highlightQuery": "highlights.scm" - }, - "lsp": { - "name": "intelephense", - "runtime": "bun", - "package": "intelephense", - "args": ["--stdio"] - } - } -} diff --git a/extensions/php/highlights.scm b/extensions/php/highlights.scm deleted file mode 100644 index 197de168..00000000 --- a/extensions/php/highlights.scm +++ /dev/null @@ -1,111 +0,0 @@ -; PHP highlight query compatible with php_only grammar - -; Comments -(comment) @comment - -; Strings -[ - (string) - (string_content) - (encapsed_string) - (heredoc) - (heredoc_body) - (nowdoc_body) -] @string - -; Numbers -(integer) @number -(float) @number - -; Boolean and null -(boolean) @constant.builtin -(null) @constant.builtin - -; Variables -(variable_name) @variable - -((name) @variable.builtin - (#eq? @variable.builtin "this")) - -; Function definitions and calls -(function_definition - name: (name) @function) - -(method_declaration - name: (name) @function.method) - -(function_call_expression - function: [ - (qualified_name (name)) - (relative_name (name)) - (name) - ] @function) - -(scoped_call_expression - name: (name) @function) - -(member_call_expression - name: (name) @function.method) - -(array_creation_expression "array" @function.builtin) -(list_literal "list" @function.builtin) - -; Class, interface, trait declarations -(class_declaration - name: (name) @type) - -(interface_declaration - name: (name) @type) - -(trait_declaration - name: (name) @type) - -; Types -(primitive_type) @type.builtin -(cast_type) @type.builtin -(named_type [ - (name) @type - (qualified_name (name) @type) - (relative_name (name) @type) -]) - -(scoped_call_expression - scope: [ - (name) @type - (qualified_name (name) @type) - (relative_name (name) @type) - ]) - -; Object creation -(object_creation_expression [ - (name) @constructor - (qualified_name (name) @constructor) - (relative_name (name) @constructor) -]) - -(method_declaration name: (name) @constructor - (#eq? @constructor "__construct")) - -; Properties -(property_element - (variable_name) @property) - -(member_access_expression - name: (variable_name (name)) @property) -(member_access_expression - name: (name) @property) - -; Namespace -(namespace_definition - name: (namespace_name) @module) - -(namespace_name (name) @module) - -; Constants (UPPER_CASE names) -((name) @constant - (#match? @constant "^_?[A-Z][A-Z0-9_]+$")) - -(const_declaration (const_element (name) @constant)) - -; Operators -"$" @operator diff --git a/extensions/php/parser.wasm b/extensions/php/parser.wasm deleted file mode 100755 index 505fe869..00000000 Binary files a/extensions/php/parser.wasm and /dev/null differ diff --git a/extensions/python/extension.json b/extensions/python/extension.json deleted file mode 100644 index 4a9576fd..00000000 --- a/extensions/python/extension.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "$schema": "https://athas.dev/schemas/extension.json", - "id": "athas.python", - "name": "Python", - "displayName": "Python", - "version": "1.0.0", - "description": "Python language support with syntax highlighting, LSP, and tooling", - "publisher": "Athas", - "categories": ["Language"], - "languages": [ - { - "id": "python", - "extensions": [".py", ".pyw", ".pyi"], - "aliases": ["Python", "python3"] - } - ], - "capabilities": { - "grammar": { - "wasmPath": "parser.wasm", - "highlightQuery": "highlights.scm" - }, - "lsp": { - "name": "pyright", - "runtime": "bun", - "package": "pyright", - "args": ["--stdio"] - }, - "formatter": { - "name": "black", - "runtime": "python", - "package": "black", - "args": ["--stdin-filename", "${file}", "-"] - }, - "linter": { - "name": "ruff", - "runtime": "python", - "package": "ruff", - "args": ["check", "--stdin-filename", "${file}", "--output-format", "json", "-"] - } - } -} diff --git a/extensions/python/highlights.scm b/extensions/python/highlights.scm deleted file mode 100644 index af744484..00000000 --- a/extensions/python/highlights.scm +++ /dev/null @@ -1,137 +0,0 @@ -; Identifier naming conventions - -(identifier) @variable - -((identifier) @constructor - (#match? @constructor "^[A-Z]")) - -((identifier) @constant - (#match? @constant "^[A-Z][A-Z_]*$")) - -; Function calls - -(decorator) @function -(decorator - (identifier) @function) - -(call - function: (attribute attribute: (identifier) @function.method)) -(call - function: (identifier) @function) - -; Builtin functions - -((call - function: (identifier) @function.builtin) - (#match? - @function.builtin - "^(abs|all|any|ascii|bin|bool|breakpoint|bytearray|bytes|callable|chr|classmethod|compile|complex|delattr|dict|dir|divmod|enumerate|eval|exec|filter|float|format|frozenset|getattr|globals|hasattr|hash|help|hex|id|input|int|isinstance|issubclass|iter|len|list|locals|map|max|memoryview|min|next|object|oct|open|ord|pow|print|property|range|repr|reversed|round|set|setattr|slice|sorted|staticmethod|str|sum|super|tuple|type|vars|zip|__import__)$")) - -; Function definitions - -(function_definition - name: (identifier) @function) - -(attribute attribute: (identifier) @property) -(type (identifier) @type) - -; Literals - -[ - (none) - (true) - (false) -] @constant.builtin - -[ - (integer) - (float) -] @number - -(comment) @comment -(string) @string -(escape_sequence) @escape - -(interpolation - "{" @punctuation.special - "}" @punctuation.special) @embedded - -[ - "-" - "-=" - "!=" - "*" - "**" - "**=" - "*=" - "/" - "//" - "//=" - "/=" - "&" - "&=" - "%" - "%=" - "^" - "^=" - "+" - "->" - "+=" - "<" - "<<" - "<<=" - "<=" - "<>" - "=" - ":=" - "==" - ">" - ">=" - ">>" - ">>=" - "|" - "|=" - "~" - "@=" - "and" - "in" - "is" - "not" - "or" - "is not" - "not in" -] @operator - -[ - "as" - "assert" - "async" - "await" - "break" - "class" - "continue" - "def" - "del" - "elif" - "else" - "except" - "exec" - "finally" - "for" - "from" - "global" - "if" - "import" - "lambda" - "nonlocal" - "pass" - "print" - "raise" - "return" - "try" - "while" - "with" - "yield" - "match" - "case" -] @keyword diff --git a/extensions/python/parser.wasm b/extensions/python/parser.wasm deleted file mode 100755 index 14237633..00000000 Binary files a/extensions/python/parser.wasm and /dev/null differ diff --git a/extensions/ql/parser.wasm b/extensions/ql/parser.wasm deleted file mode 100755 index ffe8224a..00000000 Binary files a/extensions/ql/parser.wasm and /dev/null differ diff --git a/extensions/registry.json b/extensions/registry.json deleted file mode 100644 index 365d749c..00000000 --- a/extensions/registry.json +++ /dev/null @@ -1,149 +0,0 @@ -{ - "version": "1.0.0", - "lastUpdated": "2025-01-15T00:00:00Z", - "extensions": [ - { - "id": "athas.html", - "name": "HTML", - "displayName": "HTML Language Support", - "description": "HTML language support with IntelliSense, formatting, and snippets", - "version": "1.0.0", - "publisher": "Athas", - "category": "language", - "icon": "https://athas.dev/extensions/html/icon.svg", - "downloads": 0, - "rating": 0, - "manifestUrl": "https://athas.dev/extensions/html/extension.json" - }, - { - "id": "athas.css", - "name": "CSS", - "displayName": "CSS Language Support", - "description": "CSS language support with IntelliSense, formatting, and snippets", - "version": "1.0.0", - "publisher": "Athas", - "category": "language", - "icon": "https://athas.dev/extensions/css/icon.svg", - "downloads": 0, - "rating": 0, - "manifestUrl": "https://athas.dev/extensions/css/extension.json" - }, - { - "id": "athas.json", - "name": "JSON", - "displayName": "JSON Language Support", - "description": "JSON language support with IntelliSense, formatting, and snippets", - "version": "1.0.0", - "publisher": "Athas", - "category": "language", - "icon": "https://athas.dev/extensions/json/icon.svg", - "downloads": 0, - "rating": 0, - "manifestUrl": "https://athas.dev/extensions/json/extension.json" - }, - { - "id": "athas.yaml", - "name": "YAML", - "displayName": "YAML Language Support", - "description": "YAML language support with IntelliSense, formatting, and snippets", - "version": "1.0.0", - "publisher": "Athas", - "category": "language", - "icon": "https://athas.dev/extensions/yaml/icon.svg", - "downloads": 0, - "rating": 0, - "manifestUrl": "https://athas.dev/extensions/yaml/extension.json" - }, - { - "id": "athas.markdown", - "name": "Markdown", - "displayName": "Markdown Language Support", - "description": "Markdown language support with IntelliSense and snippets", - "version": "1.0.0", - "publisher": "Athas", - "category": "language", - "icon": "https://athas.dev/extensions/markdown/icon.svg", - "downloads": 0, - "rating": 0, - "manifestUrl": "https://athas.dev/extensions/markdown/extension.json" - }, - { - "id": "athas.typescript", - "name": "TypeScript", - "displayName": "TypeScript Language Support", - "description": "TypeScript and JavaScript language support with IntelliSense, formatting, and snippets", - "version": "1.0.0", - "publisher": "Athas", - "category": "language", - "icon": "https://athas.dev/extensions/typescript/icon.svg", - "downloads": 0, - "rating": 0, - "manifestUrl": "https://athas.dev/extensions/typescript/extension.json" - }, - { - "id": "athas.php", - "name": "PHP", - "displayName": "PHP Language Support", - "description": "Full PHP language support with IntelliSense, diagnostics, formatting, and snippets via Intelephense", - "version": "1.0.0", - "publisher": "Athas", - "category": "language", - "icon": "https://athas.dev/extensions/php/icon.svg", - "downloads": 0, - "rating": 0, - "manifestUrl": "https://athas.dev/extensions/php/extension.json" - }, - { - "id": "athas.rust-full", - "name": "Rust Tools", - "displayName": "Rust Tools", - "description": "rust-analyzer, rustfmt, and clippy packaged for Athas", - "version": "1.0.0", - "publisher": "Athas", - "category": "language", - "icon": "https://athas.dev/extensions/rust/icon.svg", - "downloads": 0, - "rating": 0, - "manifestUrl": "https://athas.dev/extensions/rust/extension.json" - }, - { - "id": "athas.go-full", - "name": "Go Tools", - "displayName": "Go Tools", - "description": "gopls, gofumpt, and golangci-lint packaged for Athas", - "version": "1.0.0", - "publisher": "Athas", - "category": "language", - "icon": "https://athas.dev/extensions/go/icon.svg", - "downloads": 0, - "rating": 0, - "manifestUrl": "https://athas.dev/extensions/go/extension.json" - }, - { - "id": "athas.lua-full", - "name": "Lua Tools", - "displayName": "Lua Tools", - "description": "lua-language-server, stylua, and luacheck packaged for Athas", - "version": "1.0.0", - "publisher": "Athas", - "category": "language", - "icon": "https://athas.dev/extensions/lua/icon.svg", - "downloads": 0, - "rating": 0, - "manifestUrl": "https://athas.dev/extensions/lua/extension.json" - }, - { - "id": "athas.toml-full", - "name": "TOML Tools", - "displayName": "TOML Tools", - "description": "Taplo LSP and formatter packaged for Athas", - "version": "1.0.0", - "publisher": "Athas", - "category": "language", - "icon": "https://athas.dev/extensions/toml/icon.svg", - "downloads": 0, - "rating": 0, - "manifestUrl": "https://athas.dev/extensions/toml/extension.json" - } - ] -} diff --git a/extensions/rescript/parser.wasm b/extensions/rescript/parser.wasm deleted file mode 100755 index 5c17ef71..00000000 Binary files a/extensions/rescript/parser.wasm and /dev/null differ diff --git a/extensions/ruby/extension.json b/extensions/ruby/extension.json deleted file mode 100644 index d710a4b2..00000000 --- a/extensions/ruby/extension.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "$schema": "https://athas.dev/schemas/extension.json", - "id": "athas.ruby", - "name": "Ruby", - "displayName": "Ruby", - "version": "1.0.0", - "description": "Ruby language support with syntax highlighting", - "publisher": "Athas", - "categories": ["Language"], - "languages": [ - { - "id": "ruby", - "extensions": [".rb", ".rake", ".gemspec"], - "aliases": ["Ruby", "rb"], - "filenames": ["Rakefile", "Gemfile"] - } - ], - "capabilities": { - "grammar": { - "wasmPath": "parser.wasm", - "highlightQuery": "highlights.scm" - } - } -} diff --git a/extensions/ruby/highlights.scm b/extensions/ruby/highlights.scm deleted file mode 100644 index dd1c9139..00000000 --- a/extensions/ruby/highlights.scm +++ /dev/null @@ -1,154 +0,0 @@ -(identifier) @variable - -((identifier) @function.method - (#is-not? local)) - -[ - "alias" - "and" - "begin" - "break" - "case" - "class" - "def" - "do" - "else" - "elsif" - "end" - "ensure" - "for" - "if" - "in" - "module" - "next" - "or" - "rescue" - "retry" - "return" - "then" - "unless" - "until" - "when" - "while" - "yield" -] @keyword - -((identifier) @keyword - (#match? @keyword "^(private|protected|public)$")) - -(constant) @constructor - -; Function calls - -"defined?" @function.method.builtin - -(call - method: [(identifier) (constant)] @function.method) - -((identifier) @function.method.builtin - (#eq? @function.method.builtin "require")) - -; Function definitions - -(alias (identifier) @function.method) -(setter (identifier) @function.method) -(method name: [(identifier) (constant)] @function.method) -(singleton_method name: [(identifier) (constant)] @function.method) - -; Identifiers - -[ - (class_variable) - (instance_variable) -] @property - -((identifier) @constant.builtin - (#match? @constant.builtin "^__(FILE|LINE|ENCODING)__$")) - -(file) @constant.builtin -(line) @constant.builtin -(encoding) @constant.builtin - -(hash_splat_nil - "**" @operator) @constant.builtin - -((constant) @constant - (#match? @constant "^[A-Z\\d_]+$")) - -[ - (self) - (super) -] @variable.builtin - -(block_parameter (identifier) @variable.parameter) -(block_parameters (identifier) @variable.parameter) -(destructured_parameter (identifier) @variable.parameter) -(hash_splat_parameter (identifier) @variable.parameter) -(lambda_parameters (identifier) @variable.parameter) -(method_parameters (identifier) @variable.parameter) -(splat_parameter (identifier) @variable.parameter) - -(keyword_parameter name: (identifier) @variable.parameter) -(optional_parameter name: (identifier) @variable.parameter) - -; Literals - -[ - (string) - (bare_string) - (subshell) - (heredoc_body) - (heredoc_beginning) -] @string - -[ - (simple_symbol) - (delimited_symbol) - (hash_key_symbol) - (bare_symbol) -] @string.special.symbol - -(regex) @string.special.regex -(escape_sequence) @escape - -[ - (integer) - (float) -] @number - -[ - (nil) - (true) - (false) -] @constant.builtin - -(interpolation - "#{" @punctuation.special - "}" @punctuation.special) @embedded - -(comment) @comment - -; Operators - -[ -"=" -"=>" -"->" -] @operator - -[ - "," - ";" - "." -] @punctuation.delimiter - -[ - "(" - ")" - "[" - "]" - "{" - "}" - "%w(" - "%i(" -] @punctuation.bracket diff --git a/extensions/ruby/parser.wasm b/extensions/ruby/parser.wasm deleted file mode 100755 index 2713e111..00000000 Binary files a/extensions/ruby/parser.wasm and /dev/null differ diff --git a/extensions/rust/extension.json b/extensions/rust/extension.json deleted file mode 100644 index b4c25a1c..00000000 --- a/extensions/rust/extension.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "$schema": "https://athas.dev/schemas/extension.json", - "id": "athas.rust", - "name": "Rust", - "displayName": "Rust", - "version": "1.0.0", - "description": "Rust language support with syntax highlighting and LSP", - "publisher": "Athas", - "categories": ["Language"], - "languages": [ - { - "id": "rust", - "extensions": [".rs"], - "aliases": ["Rust", "rs"] - } - ], - "capabilities": { - "grammar": { - "wasmPath": "parser.wasm", - "highlightQuery": "highlights.scm" - }, - "lsp": { - "name": "rust-analyzer", - "runtime": "binary", - "downloadUrl": "https://github.com/rust-lang/rust-analyzer/releases/latest/download/rust-analyzer-${arch}-${os}.gz" - } - } -} diff --git a/extensions/rust/highlights.scm b/extensions/rust/highlights.scm deleted file mode 100644 index 48c7284e..00000000 --- a/extensions/rust/highlights.scm +++ /dev/null @@ -1,161 +0,0 @@ -; Identifiers - -(type_identifier) @type -(primitive_type) @type.builtin -(field_identifier) @property - -; Identifier conventions - -; Assume all-caps names are constants -((identifier) @constant - (#match? @constant "^[A-Z][A-Z\\d_]+$'")) - -; Assume uppercase names are enum constructors -((identifier) @constructor - (#match? @constructor "^[A-Z]")) - -; Assume that uppercase names in paths are types -((scoped_identifier - path: (identifier) @type) - (#match? @type "^[A-Z]")) -((scoped_identifier - path: (scoped_identifier - name: (identifier) @type)) - (#match? @type "^[A-Z]")) -((scoped_type_identifier - path: (identifier) @type) - (#match? @type "^[A-Z]")) -((scoped_type_identifier - path: (scoped_identifier - name: (identifier) @type)) - (#match? @type "^[A-Z]")) - -; Assume all qualified names in struct patterns are enum constructors. (They're -; either that, or struct names; highlighting both as constructors seems to be -; the less glaring choice of error, visually.) -(struct_pattern - type: (scoped_type_identifier - name: (type_identifier) @constructor)) - -; Function calls - -(call_expression - function: (identifier) @function) -(call_expression - function: (field_expression - field: (field_identifier) @function.method)) -(call_expression - function: (scoped_identifier - "::" - name: (identifier) @function)) - -(generic_function - function: (identifier) @function) -(generic_function - function: (scoped_identifier - name: (identifier) @function)) -(generic_function - function: (field_expression - field: (field_identifier) @function.method)) - -(macro_invocation - macro: (identifier) @function.macro - "!" @function.macro) - -; Function definitions - -(function_item (identifier) @function) -(function_signature_item (identifier) @function) - -(line_comment) @comment -(block_comment) @comment - -(line_comment (doc_comment)) @comment.documentation -(block_comment (doc_comment)) @comment.documentation - -"(" @punctuation.bracket -")" @punctuation.bracket -"[" @punctuation.bracket -"]" @punctuation.bracket -"{" @punctuation.bracket -"}" @punctuation.bracket - -(type_arguments - "<" @punctuation.bracket - ">" @punctuation.bracket) -(type_parameters - "<" @punctuation.bracket - ">" @punctuation.bracket) - -"::" @punctuation.delimiter -":" @punctuation.delimiter -"." @punctuation.delimiter -"," @punctuation.delimiter -";" @punctuation.delimiter - -(parameter (identifier) @variable.parameter) - -(lifetime (identifier) @label) - -"as" @keyword -"async" @keyword -"await" @keyword -"break" @keyword -"const" @keyword -"continue" @keyword -"default" @keyword -"dyn" @keyword -"else" @keyword -"enum" @keyword -"extern" @keyword -"fn" @keyword -"for" @keyword -"gen" @keyword -"if" @keyword -"impl" @keyword -"in" @keyword -"let" @keyword -"loop" @keyword -"macro_rules!" @keyword -"match" @keyword -"mod" @keyword -"move" @keyword -"pub" @keyword -"raw" @keyword -"ref" @keyword -"return" @keyword -"static" @keyword -"struct" @keyword -"trait" @keyword -"type" @keyword -"union" @keyword -"unsafe" @keyword -"use" @keyword -"where" @keyword -"while" @keyword -"yield" @keyword -(crate) @keyword -(mutable_specifier) @keyword -(use_list (self) @keyword) -(scoped_use_list (self) @keyword) -(scoped_identifier (self) @keyword) -(super) @keyword - -(self) @variable.builtin - -(char_literal) @string -(string_literal) @string -(raw_string_literal) @string - -(boolean_literal) @constant.builtin -(integer_literal) @constant.builtin -(float_literal) @constant.builtin - -(escape_sequence) @escape - -(attribute_item) @attribute -(inner_attribute_item) @attribute - -"*" @operator -"&" @operator -"'" @operator diff --git a/extensions/rust/parser.wasm b/extensions/rust/parser.wasm deleted file mode 100755 index 5b3b213d..00000000 Binary files a/extensions/rust/parser.wasm and /dev/null differ diff --git a/extensions/scala/extension.json b/extensions/scala/extension.json deleted file mode 100644 index 092d805d..00000000 --- a/extensions/scala/extension.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "$schema": "https://athas.dev/schemas/extension.json", - "id": "athas.scala", - "name": "Scala", - "displayName": "Scala", - "version": "1.0.0", - "description": "Scala language support with syntax highlighting", - "publisher": "Athas", - "categories": ["Language"], - "languages": [ - { - "id": "scala", - "extensions": [".scala", ".sc"], - "aliases": ["Scala"] - } - ], - "capabilities": { - "grammar": { - "wasmPath": "parser.wasm", - "highlightQuery": "highlights.scm" - } - } -} diff --git a/extensions/scala/highlights.scm b/extensions/scala/highlights.scm deleted file mode 100644 index 61637b0e..00000000 --- a/extensions/scala/highlights.scm +++ /dev/null @@ -1,260 +0,0 @@ -; CREDITS @stumash (stuart.mashaal@gmail.com) - -(field_expression field: (identifier) @property) -(field_expression value: (identifier) @type - (#match? @type "^[A-Z]")) - -(type_identifier) @type - -(class_definition - name: (identifier) @type) - -(enum_definition - name: (identifier) @type) - -(object_definition - name: (identifier) @type) - -(trait_definition - name: (identifier) @type) - -(full_enum_case - name: (identifier) @type) - -(simple_enum_case - name: (identifier) @type) - -;; variables - -(class_parameter - name: (identifier) @parameter) - -(self_type (identifier) @parameter) - -(interpolation (identifier) @none) -(interpolation (block) @none) - -;; types - -(type_definition - name: (type_identifier) @type.definition) - -;; val/var definitions/declarations - -(val_definition - pattern: (identifier) @variable) - -(var_definition - pattern: (identifier) @variable) - -(val_declaration - name: (identifier) @variable) - -(var_declaration - name: (identifier) @variable) - -; imports/exports - -(import_declaration - path: (identifier) @namespace) -((stable_identifier (identifier) @namespace)) - -((import_declaration - path: (identifier) @type) (#match? @type "^[A-Z]")) -((stable_identifier (identifier) @type) (#match? @type "^[A-Z]")) - -(export_declaration - path: (identifier) @namespace) -((stable_identifier (identifier) @namespace)) - -((export_declaration - path: (identifier) @type) (#match? @type "^[A-Z]")) -((stable_identifier (identifier) @type) (#match? @type "^[A-Z]")) - -((namespace_selectors (identifier) @type) (#match? @type "^[A-Z]")) - -; method invocation - -(call_expression - function: (identifier) @function.call) - -(call_expression - function: (operator_identifier) @function.call) - -(call_expression - function: (field_expression - field: (identifier) @method.call)) - -((call_expression - function: (identifier) @constructor) - (#match? @constructor "^[A-Z]")) - -(generic_function - function: (identifier) @function.call) - -(interpolated_string_expression - interpolator: (identifier) @function.call) - -; function definitions - -(function_definition - name: (identifier) @function) - -(parameter - name: (identifier) @parameter) - -(binding - name: (identifier) @parameter) - -; method definition - -(function_declaration - name: (identifier) @method) - -(function_definition - name: (identifier) @method) - -; expressions - -(infix_expression operator: (identifier) @operator) -(infix_expression operator: (operator_identifier) @operator) -(infix_type operator: (operator_identifier) @operator) -(infix_type operator: (operator_identifier) @operator) - -; literals - -(boolean_literal) @boolean -(integer_literal) @number -(floating_point_literal) @float - -[ - (string) - (character_literal) - (interpolated_string_expression) -] @string - -(interpolation "$" @punctuation.special) - -;; keywords - -(opaque_modifier) @type.qualifier -(infix_modifier) @keyword -(transparent_modifier) @type.qualifier -(open_modifier) @type.qualifier - -[ - "case" - "class" - "enum" - "extends" - "derives" - "finally" -;; `forSome` existential types not implemented yet -;; `macro` not implemented yet - "object" - "override" - "package" - "trait" - "type" - "val" - "var" - "with" - "given" - "using" - "end" - "implicit" - "extension" - "with" -] @keyword - -[ - "abstract" - "final" - "lazy" - "sealed" - "private" - "protected" -] @type.qualifier - -(inline_modifier) @storageclass - -(null_literal) @constant.builtin - -(wildcard) @parameter - -(annotation) @attribute - -;; special keywords - -"new" @keyword.operator - -[ - "else" - "if" - "match" - "then" -] @conditional - -[ - "(" - ")" - "[" - "]" - "{" - "}" -] @punctuation.bracket - -[ - "." - "," -] @punctuation.delimiter - -[ - "do" - "for" - "while" - "yield" -] @repeat - -"def" @keyword.function - -[ - "=>" - "<-" - "@" -] @operator - -["import" "export"] @include - -[ - "try" - "catch" - "throw" -] @exception - -"return" @keyword.return - -(comment) @spell @comment -(block_comment) @spell @comment - -;; `case` is a conditional keyword in case_block - -(case_block - (case_clause ("case") @conditional)) -(indented_cases - (case_clause ("case") @conditional)) - -(operator_identifier) @operator - -((identifier) @type (#match? @type "^[A-Z]")) -((identifier) @variable.builtin - (#match? @variable.builtin "^this$")) - -( - (identifier) @function.builtin - (#match? @function.builtin "^super$") -) - -;; Scala CLI using directives -(using_directive_key) @parameter -(using_directive_value) @string diff --git a/extensions/scala/parser.wasm b/extensions/scala/parser.wasm deleted file mode 100755 index e23c24fe..00000000 Binary files a/extensions/scala/parser.wasm and /dev/null differ diff --git a/extensions/solidity/parser.wasm b/extensions/solidity/parser.wasm deleted file mode 100755 index 42c1ceb6..00000000 Binary files a/extensions/solidity/parser.wasm and /dev/null differ diff --git a/extensions/swift/extension.json b/extensions/swift/extension.json deleted file mode 100644 index d502a10e..00000000 --- a/extensions/swift/extension.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "$schema": "https://athas.dev/schemas/extension.json", - "id": "athas.swift", - "name": "Swift", - "displayName": "Swift", - "version": "1.0.0", - "description": "Swift language support with syntax highlighting", - "publisher": "Athas", - "categories": ["Language"], - "languages": [ - { - "id": "swift", - "extensions": [".swift"], - "aliases": ["Swift"] - } - ], - "capabilities": { - "grammar": { - "wasmPath": "parser.wasm", - "highlightQuery": "highlights.scm" - } - } -} diff --git a/extensions/swift/highlights.scm b/extensions/swift/highlights.scm deleted file mode 100644 index 5c52ee9d..00000000 --- a/extensions/swift/highlights.scm +++ /dev/null @@ -1,347 +0,0 @@ -[ - "." - ";" - ":" - "," -] @punctuation.delimiter - -[ - "(" - ")" - "[" - "]" - "{" - "}" -] @punctuation.bracket - -; Identifiers -(type_identifier) @type - -[ - (self_expression) - (super_expression) -] @variable.builtin - -; Declarations -[ - "func" - "deinit" -] @keyword.function - -[ - (visibility_modifier) - (member_modifier) - (function_modifier) - (property_modifier) - (parameter_modifier) - (inheritance_modifier) - (mutation_modifier) -] @keyword.modifier - -(simple_identifier) @variable - -(function_declaration - (simple_identifier) @function.method) - -(protocol_function_declaration - name: (simple_identifier) @function.method) - -(init_declaration - "init" @constructor) - -(parameter - external_name: (simple_identifier) @variable.parameter) - -(parameter - name: (simple_identifier) @variable.parameter) - -(type_parameter - (type_identifier) @variable.parameter) - -(inheritance_constraint - (identifier - (simple_identifier) @variable.parameter)) - -(equality_constraint - (identifier - (simple_identifier) @variable.parameter)) - -[ - "protocol" - "extension" - "indirect" - "nonisolated" - "override" - "convenience" - "required" - "some" - "any" - "weak" - "unowned" - "didSet" - "willSet" - "subscript" - "let" - "var" - (throws) - (where_keyword) - (getter_specifier) - (setter_specifier) - (modify_specifier) - (else) - (as_operator) -] @keyword - -[ - "enum" - "struct" - "class" - "typealias" -] @keyword.type - -[ - "async" - "await" -] @keyword.coroutine - -(shebang_line) @keyword.directive - -(class_body - (property_declaration - (pattern - (simple_identifier) @variable.member))) - -(protocol_property_declaration - (pattern - (simple_identifier) @variable.member)) - -(navigation_expression - (navigation_suffix - (simple_identifier) @variable.member)) - -(value_argument - name: (value_argument_label - (simple_identifier) @variable.member)) - -(import_declaration - "import" @keyword.import) - -(enum_entry - "case" @keyword) - -(modifiers - (attribute - "@" @attribute - (user_type - (type_identifier) @attribute))) - -; Function calls -(call_expression - (simple_identifier) @function.call) ; foo() - -(call_expression - ; foo.bar.baz(): highlight the baz() - (navigation_expression - (navigation_suffix - (simple_identifier) @function.call))) - -(call_expression - (prefix_expression - (simple_identifier) @function.call)) ; .foo() - -((navigation_expression - (simple_identifier) @type) ; SomeType.method(): highlight SomeType as a type - (#lua-match? @type "^[A-Z]")) - -(directive) @keyword.directive - -; See https://docs.swift.org/swift-book/documentation/the-swift-programming-language/lexicalstructure/#Keywords-and-Punctuation -[ - (diagnostic) - "#available" - "#unavailable" - "#fileLiteral" - "#colorLiteral" - "#imageLiteral" - "#keyPath" - "#selector" - "#externalMacro" -] @function.macro - -[ - "#column" - "#dsohandle" - "#fileID" - "#filePath" - "#file" - "#function" - "#line" -] @constant.macro - -; Statements -(for_statement - "for" @keyword.repeat) - -(for_statement - "in" @keyword.repeat) - -[ - "while" - "repeat" - "continue" - "break" -] @keyword.repeat - -(guard_statement - "guard" @keyword.conditional) - -(if_statement - "if" @keyword.conditional) - -(switch_statement - "switch" @keyword.conditional) - -(switch_entry - "case" @keyword) - -(switch_entry - "fallthrough" @keyword) - -(switch_entry - (default_keyword) @keyword) - -"return" @keyword.return - -(ternary_expression - [ - "?" - ":" - ] @keyword.conditional.ternary) - -[ - (try_operator) - "do" - (throw_keyword) - (catch_keyword) -] @keyword.exception - -(statement_label) @label - -; Comments -[ - (comment) - (multiline_comment) -] @comment @spell - -((comment) @comment.documentation - (#lua-match? @comment.documentation "^///[^/]")) - -((comment) @comment.documentation - (#lua-match? @comment.documentation "^///$")) - -((multiline_comment) @comment.documentation - (#lua-match? @comment.documentation "^/[*][*][^*].*[*]/$")) - -; String literals -(line_str_text) @string - -(str_escaped_char) @string.escape - -(multi_line_str_text) @string - -(raw_str_part) @string - -(raw_str_end_part) @string - -(line_string_literal - [ - "\\(" - ")" - ] @punctuation.special) - -(multi_line_string_literal - [ - "\\(" - ")" - ] @punctuation.special) - -(raw_str_interpolation - [ - (raw_str_interpolation_start) - ")" - ] @punctuation.special) - -[ - "\"" - "\"\"\"" -] @string - -; Lambda literals -(lambda_literal - "in" @keyword.operator) - -; Basic literals -[ - (integer_literal) - (hex_literal) - (oct_literal) - (bin_literal) -] @number - -(real_literal) @number.float - -(boolean_literal) @boolean - -"nil" @constant.builtin - -(wildcard_pattern) @character.special - -; Regex literals -(regex_literal) @string.regexp - -; Operators -(custom_operator) @operator - -[ - "+" - "-" - "*" - "/" - "%" - "=" - "+=" - "-=" - "*=" - "/=" - "<" - ">" - "<<" - ">>" - "<=" - ">=" - "++" - "--" - "^" - "&" - "&&" - "|" - "||" - "~" - "%=" - "!=" - "!==" - "==" - "===" - "?" - "??" - "->" - "..<" - "..." - (bang) -] @operator - -(type_arguments - [ - "<" - ">" - ] @punctuation.bracket) diff --git a/extensions/swift/parser.wasm b/extensions/swift/parser.wasm deleted file mode 100755 index 87282f21..00000000 Binary files a/extensions/swift/parser.wasm and /dev/null differ diff --git a/extensions/systemrdl/parser.wasm b/extensions/systemrdl/parser.wasm deleted file mode 100755 index a90cf497..00000000 Binary files a/extensions/systemrdl/parser.wasm and /dev/null differ diff --git a/extensions/tlaplus/parser.wasm b/extensions/tlaplus/parser.wasm deleted file mode 100755 index 914aac41..00000000 Binary files a/extensions/tlaplus/parser.wasm and /dev/null differ diff --git a/extensions/toml/extension.json b/extensions/toml/extension.json deleted file mode 100644 index 412c11c7..00000000 --- a/extensions/toml/extension.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "$schema": "https://athas.dev/schemas/extension.json", - "id": "athas.toml", - "name": "TOML", - "displayName": "TOML", - "version": "1.0.0", - "description": "TOML language support with syntax highlighting, LSP, and formatting", - "publisher": "Athas", - "categories": ["Language"], - "languages": [ - { - "id": "toml", - "extensions": [".toml"], - "aliases": ["TOML"] - } - ], - "capabilities": { - "grammar": { - "wasmPath": "parser.wasm", - "highlightQuery": "highlights.scm" - }, - "lsp": { - "name": "taplo", - "runtime": "rust", - "package": "taplo-cli", - "args": ["lsp", "stdio"] - }, - "formatter": { - "name": "taplo", - "runtime": "rust", - "package": "taplo-cli", - "args": ["format", "-"] - } - } -} diff --git a/extensions/toml/highlights.scm b/extensions/toml/highlights.scm deleted file mode 100644 index e4d6966f..00000000 --- a/extensions/toml/highlights.scm +++ /dev/null @@ -1,33 +0,0 @@ -; Properties -;----------- - -(bare_key) @property -(quoted_key) @string - -; Literals -;--------- - -(boolean) @constant.builtin -(comment) @comment -(string) @string -(integer) @number -(float) @number -(offset_date_time) @string.special -(local_date_time) @string.special -(local_date) @string.special -(local_time) @string.special - -; Punctuation -;------------ - -"." @punctuation.delimiter -"," @punctuation.delimiter - -"=" @operator - -"[" @punctuation.bracket -"]" @punctuation.bracket -"[[" @punctuation.bracket -"]]" @punctuation.bracket -"{" @punctuation.bracket -"}" @punctuation.bracket diff --git a/extensions/toml/parser.wasm b/extensions/toml/parser.wasm deleted file mode 100755 index f5d6e44e..00000000 Binary files a/extensions/toml/parser.wasm and /dev/null differ diff --git a/extensions/tsx/extension.json b/extensions/tsx/extension.json deleted file mode 100644 index 1fd5e1be..00000000 --- a/extensions/tsx/extension.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "$schema": "https://athas.dev/schemas/extension.json", - "id": "athas.tsx", - "name": "TSX", - "displayName": "TypeScript JSX", - "version": "1.0.0", - "description": "TypeScript JSX/React support with syntax highlighting, LSP, and tooling", - "publisher": "Athas", - "categories": ["Language"], - "languages": [ - { - "id": "typescriptreact", - "extensions": [".tsx"], - "aliases": ["TSX", "TypeScript React"] - } - ], - "capabilities": { - "grammar": { - "wasmPath": "parser.wasm", - "highlightQuery": "highlights.scm" - }, - "lsp": { - "name": "typescript-language-server", - "runtime": "bun", - "package": "typescript-language-server", - "args": ["--stdio"] - }, - "formatter": { - "name": "prettier", - "runtime": "bun", - "package": "prettier", - "args": ["--stdin-filepath", "${file}"] - }, - "linter": { - "name": "eslint", - "runtime": "bun", - "package": "eslint", - "args": ["--stdin", "--stdin-filename", "${file}", "--format", "json"] - } - } -} diff --git a/extensions/tsx/highlights.scm b/extensions/tsx/highlights.scm deleted file mode 100644 index 0ea34f0e..00000000 --- a/extensions/tsx/highlights.scm +++ /dev/null @@ -1,756 +0,0 @@ -; Types -; Javascript -; Variables -;----------- -(identifier) @variable - -; Properties -;----------- -(property_identifier) @variable.member - -(shorthand_property_identifier) @variable.member - -(private_property_identifier) @variable.member - -(object_pattern - (shorthand_property_identifier_pattern) @variable) - -(object_pattern - (object_assignment_pattern - (shorthand_property_identifier_pattern) @variable)) - -; Special identifiers -;-------------------- -((identifier) @type - (#lua-match? @type "^[A-Z]")) - -((identifier) @constant - (#lua-match? @constant "^_*[A-Z][A-Z%d_]*$")) - -((shorthand_property_identifier) @constant - (#lua-match? @constant "^_*[A-Z][A-Z%d_]*$")) - -((identifier) @variable.builtin - (#any-of? @variable.builtin "arguments" "module" "console" "window" "document")) - -((identifier) @type.builtin - (#any-of? @type.builtin - "Object" "Function" "Boolean" "Symbol" "Number" "Math" "Date" "String" "RegExp" "Map" "Set" - "WeakMap" "WeakSet" "Promise" "Array" "Int8Array" "Uint8Array" "Uint8ClampedArray" "Int16Array" - "Uint16Array" "Int32Array" "Uint32Array" "Float32Array" "Float64Array" "ArrayBuffer" "DataView" - "Error" "EvalError" "InternalError" "RangeError" "ReferenceError" "SyntaxError" "TypeError" - "URIError")) - -(statement_identifier) @label - -; Function and method definitions -;-------------------------------- -(function_expression - name: (identifier) @function) - -(function_declaration - name: (identifier) @function) - -(generator_function - name: (identifier) @function) - -(generator_function_declaration - name: (identifier) @function) - -(method_definition - name: [ - (property_identifier) - (private_property_identifier) - ] @function.method) - -(method_definition - name: (property_identifier) @constructor - (#eq? @constructor "constructor")) - -(pair - key: (property_identifier) @function.method - value: (function_expression)) - -(pair - key: (property_identifier) @function.method - value: (arrow_function)) - -(assignment_expression - left: (member_expression - property: (property_identifier) @function.method) - right: (arrow_function)) - -(assignment_expression - left: (member_expression - property: (property_identifier) @function.method) - right: (function_expression)) - -(variable_declarator - name: (identifier) @function - value: (arrow_function)) - -(variable_declarator - name: (identifier) @function - value: (function_expression)) - -(assignment_expression - left: (identifier) @function - right: (arrow_function)) - -(assignment_expression - left: (identifier) @function - right: (function_expression)) - -; Function and method calls -;-------------------------- -(call_expression - function: (identifier) @function.call) - -(call_expression - function: (member_expression - property: [ - (property_identifier) - (private_property_identifier) - ] @function.method.call)) - -(call_expression - function: (await_expression - (identifier) @function.call)) - -(call_expression - function: (await_expression - (member_expression - property: [ - (property_identifier) - (private_property_identifier) - ] @function.method.call))) - -; Builtins -;--------- -((identifier) @module.builtin - (#eq? @module.builtin "Intl")) - -((identifier) @function.builtin - (#any-of? @function.builtin - "eval" "isFinite" "isNaN" "parseFloat" "parseInt" "decodeURI" "decodeURIComponent" "encodeURI" - "encodeURIComponent" "require")) - -; Constructor -;------------ -(new_expression - constructor: (identifier) @constructor) - -; Decorators -;---------- -(decorator - "@" @attribute - (identifier) @attribute) - -(decorator - "@" @attribute - (call_expression - (identifier) @attribute)) - -(decorator - "@" @attribute - (member_expression - (property_identifier) @attribute)) - -(decorator - "@" @attribute - (call_expression - (member_expression - (property_identifier) @attribute))) - -; Literals -;--------- -[ - (this) - (super) -] @variable.builtin - -((identifier) @variable.builtin - (#eq? @variable.builtin "self")) - -[ - (true) - (false) -] @boolean - -[ - (null) - (undefined) -] @constant.builtin - -[ - (comment) - (html_comment) -] @comment @spell - -((comment) @comment.documentation - (#lua-match? @comment.documentation "^/[*][*][^*].*[*]/$")) - -(hash_bang_line) @keyword.directive - -((string_fragment) @keyword.directive - (#eq? @keyword.directive "use strict")) - -(string) @string - -(template_string) @string - -(escape_sequence) @string.escape - -(regex_pattern) @string.regexp - -(regex_flags) @character.special - -(regex - "/" @punctuation.bracket) ; Regex delimiters - -(number) @number - -((identifier) @number - (#any-of? @number "NaN" "Infinity")) - -; Punctuation -;------------ -[ - ";" - "." - "," - ":" -] @punctuation.delimiter - -[ - "--" - "-" - "-=" - "&&" - "+" - "++" - "+=" - "&=" - "/=" - "**=" - "<<=" - "<" - "<=" - "<<" - "=" - "==" - "===" - "!=" - "!==" - "=>" - ">" - ">=" - ">>" - "||" - "%" - "%=" - "*" - "**" - ">>>" - "&" - "|" - "^" - "??" - "*=" - ">>=" - ">>>=" - "^=" - "|=" - "&&=" - "||=" - "??=" - "..." -] @operator - -(binary_expression - "/" @operator) - -(ternary_expression - [ - "?" - ":" - ] @keyword.conditional.ternary) - -(unary_expression - [ - "!" - "~" - "-" - "+" - ] @operator) - -(unary_expression - [ - "delete" - "void" - ] @keyword.operator) - -[ - "(" - ")" - "[" - "]" - "{" - "}" -] @punctuation.bracket - -(template_substitution - [ - "${" - "}" - ] @punctuation.special) @none - -; Imports -;---------- -(namespace_import - "*" @character.special - (identifier) @module) - -(namespace_export - "*" @character.special - (identifier) @module) - -(export_statement - "*" @character.special) - -; Keywords -;---------- -[ - "if" - "else" - "switch" - "case" -] @keyword.conditional - -[ - "import" - "from" - "as" - "export" -] @keyword.import - -[ - "for" - "of" - "do" - "while" - "continue" -] @keyword.repeat - -[ - "break" - "const" - "debugger" - "extends" - "get" - "let" - "set" - "static" - "target" - "var" - "with" -] @keyword - -"class" @keyword.type - -[ - "async" - "await" -] @keyword.coroutine - -[ - "return" - "yield" -] @keyword.return - -"function" @keyword.function - -[ - "new" - "delete" - "in" - "instanceof" - "typeof" -] @keyword.operator - -[ - "throw" - "try" - "catch" - "finally" -] @keyword.exception - -(export_statement - "default" @keyword) - -(switch_default - "default" @keyword.conditional) - -"require" @keyword.import - -(import_require_clause - source: (string) @string.special.url) - -[ - "declare" - "implements" - "type" - "override" - "module" - "asserts" - "infer" - "is" - "using" -] @keyword - -[ - "namespace" - "interface" - "enum" -] @keyword.type - -[ - "keyof" - "satisfies" -] @keyword.operator - -(as_expression - "as" @keyword.operator) - -(mapped_type_clause - "as" @keyword.operator) - -[ - "abstract" - "private" - "protected" - "public" - "readonly" -] @keyword.modifier - -; types -(type_identifier) @type - -(predefined_type) @type.builtin - -(import_statement - "type" - (import_clause - (named_imports - (import_specifier - name: (identifier) @type)))) - -(template_literal_type) @string - -(non_null_expression - "!" @operator) - -; punctuation -(type_arguments - [ - "<" - ">" - ] @punctuation.bracket) - -(type_parameters - [ - "<" - ">" - ] @punctuation.bracket) - -(object_type - [ - "{|" - "|}" - ] @punctuation.bracket) - -(union_type - "|" @punctuation.delimiter) - -(intersection_type - "&" @punctuation.delimiter) - -(type_annotation - ":" @punctuation.delimiter) - -(type_predicate_annotation - ":" @punctuation.delimiter) - -(index_signature - ":" @punctuation.delimiter) - -(omitting_type_annotation - "-?:" @punctuation.delimiter) - -(adding_type_annotation - "+?:" @punctuation.delimiter) - -(opting_type_annotation - "?:" @punctuation.delimiter) - -"?." @punctuation.delimiter - -(abstract_method_signature - "?" @punctuation.special) - -(method_signature - "?" @punctuation.special) - -(method_definition - "?" @punctuation.special) - -(property_signature - "?" @punctuation.special) - -(optional_parameter - "?" @punctuation.special) - -(optional_type - "?" @punctuation.special) - -(public_field_definition - [ - "?" - "!" - ] @punctuation.special) - -(flow_maybe_type - "?" @punctuation.special) - -(template_type - [ - "${" - "}" - ] @punctuation.special) - -(conditional_type - [ - "?" - ":" - ] @keyword.conditional.ternary) - -; Parameters -(required_parameter - pattern: (identifier) @variable.parameter) - -(optional_parameter - pattern: (identifier) @variable.parameter) - -(required_parameter - (rest_pattern - (identifier) @variable.parameter)) - -; ({ a }) => null -(required_parameter - (object_pattern - (shorthand_property_identifier_pattern) @variable.parameter)) - -; ({ a = b }) => null -(required_parameter - (object_pattern - (object_assignment_pattern - (shorthand_property_identifier_pattern) @variable.parameter))) - -; ({ a: b }) => null -(required_parameter - (object_pattern - (pair_pattern - value: (identifier) @variable.parameter))) - -; ([ a ]) => null -(required_parameter - (array_pattern - (identifier) @variable.parameter)) - -; a => null -(arrow_function - parameter: (identifier) @variable.parameter) - -; global declaration -(ambient_declaration - "global" @module) - -; function signatures -(ambient_declaration - (function_signature - name: (identifier) @function)) - -; method signatures -(method_signature - name: (_) @function.method) - -(abstract_method_signature - name: (property_identifier) @function.method) - -; property signatures -(property_signature - name: (property_identifier) @function.method - type: (type_annotation - [ - (union_type - (parenthesized_type - (function_type))) - (function_type) - ])) -(jsx_element - open_tag: (jsx_opening_element - [ - "<" - ">" - ] @tag.delimiter)) - -(jsx_element - close_tag: (jsx_closing_element - [ - "" - ] @tag.delimiter)) - -(jsx_self_closing_element - [ - "<" - "/>" - ] @tag.delimiter) - -(jsx_attribute - (property_identifier) @tag.attribute) - -(jsx_opening_element - name: (identifier) @tag.builtin) - -(jsx_closing_element - name: (identifier) @tag.builtin) - -(jsx_self_closing_element - name: (identifier) @tag.builtin) - -(jsx_opening_element - ((identifier) @tag - (#lua-match? @tag "^[A-Z]"))) - -; Handle the dot operator effectively - -(jsx_opening_element - (member_expression - (identifier) @tag.builtin - (property_identifier) @tag)) - -(jsx_closing_element - ((identifier) @tag - (#lua-match? @tag "^[A-Z]"))) - -; Handle the dot operator effectively - -(jsx_closing_element - (member_expression - (identifier) @tag.builtin - (property_identifier) @tag)) - -(jsx_self_closing_element - ((identifier) @tag - (#lua-match? @tag "^[A-Z]"))) - -; Handle the dot operator effectively - -(jsx_self_closing_element - (member_expression - (identifier) @tag.builtin - (property_identifier) @tag)) - -(html_character_reference) @tag - -(jsx_text) @none @spell - -(html_character_reference) @character.special - -((jsx_element - (jsx_opening_element - name: (identifier) @_tag) - (jsx_text) @markup.heading) - (#eq? @_tag "title")) - -((jsx_element - (jsx_opening_element - name: (identifier) @_tag) - (jsx_text) @markup.heading.1) - (#eq? @_tag "h1")) - -((jsx_element - (jsx_opening_element - name: (identifier) @_tag) - (jsx_text) @markup.heading.2) - (#eq? @_tag "h2")) - -((jsx_element - (jsx_opening_element - name: (identifier) @_tag) - (jsx_text) @markup.heading.3) - (#eq? @_tag "h3")) - -((jsx_element - (jsx_opening_element - name: (identifier) @_tag) - (jsx_text) @markup.heading.4) - (#eq? @_tag "h4")) - -((jsx_element - (jsx_opening_element - name: (identifier) @_tag) - (jsx_text) @markup.heading.5) - (#eq? @_tag "h5")) - -((jsx_element - (jsx_opening_element - name: (identifier) @_tag) - (jsx_text) @markup.heading.6) - (#eq? @_tag "h6")) - -((jsx_element - (jsx_opening_element - name: (identifier) @_tag) - (jsx_text) @markup.strong) - (#any-of? @_tag "strong" "b")) - -((jsx_element - (jsx_opening_element - name: (identifier) @_tag) - (jsx_text) @markup.italic) - (#any-of? @_tag "em" "i")) - -((jsx_element - (jsx_opening_element - name: (identifier) @_tag) - (jsx_text) @markup.strikethrough) - (#any-of? @_tag "s" "del")) - -((jsx_element - (jsx_opening_element - name: (identifier) @_tag) - (jsx_text) @markup.underline) - (#eq? @_tag "u")) - -((jsx_element - (jsx_opening_element - name: (identifier) @_tag) - (jsx_text) @markup.raw) - (#any-of? @_tag "code" "kbd")) - -((jsx_element - (jsx_opening_element - name: (identifier) @_tag) - (jsx_text) @markup.link.label) - (#eq? @_tag "a")) - -((jsx_attribute - (property_identifier) @_attr - (string - (string_fragment) @string.special.url)) - (#any-of? @_attr "href" "src")) - -((jsx_element) @_jsx_element - (#set! @_jsx_element bo.commentstring "{/* %s */}")) - -((jsx_attribute) @_jsx_attribute - (#set! @_jsx_attribute bo.commentstring "// %s")) diff --git a/extensions/tsx/parser.wasm b/extensions/tsx/parser.wasm deleted file mode 100755 index 1e11febb..00000000 Binary files a/extensions/tsx/parser.wasm and /dev/null differ diff --git a/extensions/typescript/extension.json b/extensions/typescript/extension.json deleted file mode 100644 index d726abc4..00000000 --- a/extensions/typescript/extension.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "$schema": "https://athas.dev/schemas/extension.json", - "id": "athas.typescript", - "name": "TypeScript", - "displayName": "TypeScript", - "version": "1.0.0", - "description": "TypeScript language support with syntax highlighting, LSP, and tooling", - "publisher": "Athas", - "categories": ["Language"], - "languages": [ - { - "id": "typescript", - "extensions": [".ts", ".mts", ".cts"], - "aliases": ["TypeScript", "ts"] - } - ], - "capabilities": { - "grammar": { - "wasmPath": "parser.wasm", - "highlightQuery": "highlights.scm" - }, - "lsp": { - "name": "typescript-language-server", - "runtime": "bun", - "package": "typescript-language-server", - "args": ["--stdio"] - }, - "formatter": { - "name": "prettier", - "runtime": "bun", - "package": "prettier", - "args": ["--stdin-filepath", "${file}"] - }, - "linter": { - "name": "eslint", - "runtime": "bun", - "package": "eslint", - "args": ["--stdin", "--stdin-filename", "${file}", "--format", "json"] - } - } -} diff --git a/extensions/typescript/highlights.scm b/extensions/typescript/highlights.scm deleted file mode 100644 index c758b516..00000000 --- a/extensions/typescript/highlights.scm +++ /dev/null @@ -1,35 +0,0 @@ -; Types - -(type_identifier) @type -(predefined_type) @type.builtin - -((identifier) @type - (#match? @type "^[A-Z]")) - -(type_arguments - "<" @punctuation.bracket - ">" @punctuation.bracket) - -; Variables - -(required_parameter (identifier) @variable.parameter) -(optional_parameter (identifier) @variable.parameter) - -; Keywords - -[ "abstract" - "declare" - "enum" - "export" - "implements" - "interface" - "keyof" - "namespace" - "private" - "protected" - "public" - "type" - "readonly" - "override" - "satisfies" -] @keyword diff --git a/extensions/typescript/parser.wasm b/extensions/typescript/parser.wasm deleted file mode 100755 index 36c7ae0e..00000000 Binary files a/extensions/typescript/parser.wasm and /dev/null differ diff --git a/extensions/vue/extension.json b/extensions/vue/extension.json deleted file mode 100644 index bcb5a169..00000000 --- a/extensions/vue/extension.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "$schema": "https://athas.dev/schemas/extension.json", - "id": "athas.vue", - "name": "Vue", - "displayName": "Vue", - "version": "1.0.0", - "description": "Vue language support with syntax highlighting", - "publisher": "Athas", - "categories": ["Language"], - "languages": [ - { - "id": "vue", - "extensions": [".vue"], - "aliases": ["Vue"] - } - ], - "capabilities": { - "grammar": { - "wasmPath": "parser.wasm", - "highlightQuery": "highlights.scm" - } - } -} diff --git a/extensions/vue/highlights.scm b/extensions/vue/highlights.scm deleted file mode 100644 index 64195c34..00000000 --- a/extensions/vue/highlights.scm +++ /dev/null @@ -1,43 +0,0 @@ -; inherits: html_tags - -[ - "[" - "]" -] @punctuation.bracket - -(interpolation) @punctuation.special - -(interpolation - (raw_text) @none) - -(dynamic_directive_inner_value) @variable - -(directive_name) @tag.attribute - -; Accessing a component object's field -(":" - . - (directive_value) @variable.member) - -("." - . - (directive_value) @property) - -; @click is like onclick for HTML -("@" - . - (directive_value) @function.method) - -; Used in v-slot, declaring position the element should be put in -("#" - . - (directive_value) @variable) - -(directive_attribute - (quoted_attribute_value) @punctuation.special) - -(directive_attribute - (quoted_attribute_value - (attribute_value) @none)) - -(directive_modifier) @function.method diff --git a/extensions/vue/parser.wasm b/extensions/vue/parser.wasm deleted file mode 100755 index 32fa5689..00000000 Binary files a/extensions/vue/parser.wasm and /dev/null differ diff --git a/extensions/yaml/extension.json b/extensions/yaml/extension.json deleted file mode 100644 index b4e0e294..00000000 --- a/extensions/yaml/extension.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "$schema": "https://athas.dev/schemas/extension.json", - "id": "athas.yaml", - "name": "YAML", - "displayName": "YAML", - "version": "1.0.0", - "description": "YAML language support with syntax highlighting, LSP, and formatting", - "publisher": "Athas", - "categories": ["Language"], - "languages": [ - { - "id": "yaml", - "extensions": [".yaml", ".yml"], - "aliases": ["YAML"] - } - ], - "capabilities": { - "grammar": { - "wasmPath": "parser.wasm", - "highlightQuery": "highlights.scm" - }, - "lsp": { - "name": "yaml-language-server", - "runtime": "bun", - "package": "yaml-language-server", - "args": ["--stdio"] - }, - "formatter": { - "name": "prettier", - "runtime": "bun", - "package": "prettier", - "args": ["--stdin-filepath", "${file}"] - } - } -} diff --git a/extensions/yaml/highlights.scm b/extensions/yaml/highlights.scm deleted file mode 100644 index 9aed59ce..00000000 --- a/extensions/yaml/highlights.scm +++ /dev/null @@ -1,99 +0,0 @@ -(boolean_scalar) @boolean - -(null_scalar) @constant.builtin - -(double_quote_scalar) @string - -(single_quote_scalar) @string - -((block_scalar) @string - (#set! priority 99)) - -(string_scalar) @string - -(escape_sequence) @string.escape - -(integer_scalar) @number - -(float_scalar) @number - -(comment) @comment @spell - -[ - (anchor_name) - (alias_name) -] @label - -(tag) @type - -[ - (yaml_directive) - (tag_directive) - (reserved_directive) -] @keyword.directive - -(block_mapping_pair - key: (flow_node - [ - (double_quote_scalar) - (single_quote_scalar) - ] @property)) - -(block_mapping_pair - key: (flow_node - (plain_scalar - (string_scalar) @property))) - -(flow_mapping - (_ - key: (flow_node - [ - (double_quote_scalar) - (single_quote_scalar) - ] @property))) - -(flow_mapping - (_ - key: (flow_node - (plain_scalar - (string_scalar) @property)))) - -[ - "," - "-" - ":" - ">" - "?" - "|" -] @punctuation.delimiter - -[ - "[" - "]" - "{" - "}" -] @punctuation.bracket - -[ - "*" - "&" - "---" - "..." -] @punctuation.special - -; help deal with for yaml's norway problem https://www.bram.us/2022/01/11/yaml-the-norway-problem/ -; only using `true` and `false`, since Treesitter parser targets YAML spec 1.2 https://github.com/nvim-treesitter/nvim-treesitter/pull/7512#issuecomment-2565397302 -(block_mapping_pair - value: (block_node - (block_sequence - (block_sequence_item - (flow_node - (plain_scalar - (string_scalar) @boolean - (#any-of? @boolean "TRUE" "FALSE" "True" "False"))))))) - -(block_mapping_pair - value: (flow_node - (plain_scalar - (string_scalar) @boolean - (#any-of? @boolean "TRUE" "FALSE" "True" "False")))) diff --git a/extensions/yaml/parser.wasm b/extensions/yaml/parser.wasm deleted file mode 100755 index d9a609a2..00000000 Binary files a/extensions/yaml/parser.wasm and /dev/null differ diff --git a/extensions/zig/extension.json b/extensions/zig/extension.json deleted file mode 100644 index b669f7c0..00000000 --- a/extensions/zig/extension.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "$schema": "https://athas.dev/schemas/extension.json", - "id": "athas.zig", - "name": "Zig", - "displayName": "Zig", - "version": "1.0.0", - "description": "Zig language support with syntax highlighting", - "publisher": "Athas", - "categories": ["Language"], - "languages": [ - { - "id": "zig", - "extensions": [".zig"], - "aliases": ["Zig"] - } - ], - "capabilities": { - "grammar": { - "wasmPath": "parser.wasm", - "highlightQuery": "highlights.scm" - } - } -} diff --git a/extensions/zig/highlights.scm b/extensions/zig/highlights.scm deleted file mode 100644 index 0989704c..00000000 --- a/extensions/zig/highlights.scm +++ /dev/null @@ -1,233 +0,0 @@ -[ - (container_doc_comment) - (doc_comment) - (line_comment) -] @comment @spell - -[ - variable: (IDENTIFIER) - variable_type_function: (IDENTIFIER) -] @variable - -parameter: (IDENTIFIER) @parameter - -[ - field_member: (IDENTIFIER) - field_access: (IDENTIFIER) -] @field - -;; assume TitleCase is a type -( - [ - variable_type_function: (IDENTIFIER) - field_access: (IDENTIFIER) - parameter: (IDENTIFIER) - ] @type - (#match? @type "^[A-Z]([a-z]+[A-Za-z0-9]*)*$") -) -;; assume camelCase is a function -( - [ - variable_type_function: (IDENTIFIER) - field_access: (IDENTIFIER) - parameter: (IDENTIFIER) - ] @function - (#match? @function "^[a-z]+([A-Z][a-z0-9]*)+$") -) - -;; assume all CAPS_1 is a constant -( - [ - variable_type_function: (IDENTIFIER) - field_access: (IDENTIFIER) - ] @constant - (#match? @constant "^[A-Z][A-Z_0-9]+$") -) - -[ - function_call: (IDENTIFIER) - function: (IDENTIFIER) -] @function - -exception: "!" @exception - -( - (IDENTIFIER) @variable.builtin - (#eq? @variable.builtin "_") -) - -(PtrTypeStart "c" @variable.builtin) - -( - (ContainerDeclType - [ - (ErrorUnionExpr) - "enum" - ] - ) - (ContainerField (IDENTIFIER) @constant) -) - -field_constant: (IDENTIFIER) @constant - -(BUILTINIDENTIFIER) @function.builtin - -((BUILTINIDENTIFIER) @include - (#any-of? @include "@import" "@cImport")) - -(INTEGER) @number - -(FLOAT) @float - -[ - "true" - "false" -] @boolean - -[ - (LINESTRING) - (STRINGLITERALSINGLE) -] @string @spell - -(CHAR_LITERAL) @character -(EscapeSequence) @string.escape -(FormatSequence) @string.special - -(BreakLabel (IDENTIFIER) @label) -(BlockLabel (IDENTIFIER) @label) - -[ - "asm" - "defer" - "errdefer" - "test" - "struct" - "union" - "enum" - "opaque" - "error" -] @keyword - -[ - "async" - "await" - "suspend" - "nosuspend" - "resume" -] @keyword.coroutine - -[ - "fn" -] @keyword.function - -[ - "and" - "or" - "orelse" -] @keyword.operator - -[ - "return" -] @keyword.return - -[ - "if" - "else" - "switch" -] @conditional - -[ - "for" - "while" - "break" - "continue" -] @repeat - -[ - "usingnamespace" -] @include - -[ - "try" - "catch" -] @exception - -[ - "anytype" - (BuildinTypeExpr) -] @type.builtin - -[ - "const" - "var" - "volatile" - "allowzero" - "noalias" -] @type.qualifier - -[ - "addrspace" - "align" - "callconv" - "linksection" -] @storageclass - -[ - "comptime" - "export" - "extern" - "inline" - "noinline" - "packed" - "pub" - "threadlocal" -] @attribute - -[ - "null" - "unreachable" - "undefined" -] @constant.builtin - -[ - (CompareOp) - (BitwiseOp) - (BitShiftOp) - (AdditionOp) - (AssignOp) - (MultiplyOp) - (PrefixOp) - "*" - "**" - "->" - ".?" - ".*" - "?" -] @operator - -[ - ";" - "." - "," - ":" -] @punctuation.delimiter - -[ - ".." - "..." -] @punctuation.special - -[ - "[" - "]" - "(" - ")" - "{" - "}" - (Payload "|") - (PtrPayload "|") - (PtrIndexPayload "|") -] @punctuation.bracket - -; Error -(ERROR) @error diff --git a/extensions/zig/parser.wasm b/extensions/zig/parser.wasm deleted file mode 100755 index 5a8f3814..00000000 Binary files a/extensions/zig/parser.wasm and /dev/null differ diff --git a/package.json b/package.json index b299f0a0..ed5f351c 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,11 @@ { "name": "athas-code", "private": true, - "version": "0.3.2", + "version": "0.4.0", "type": "module", "scripts": { "tauri": "tauri", + "test": "bun test", "type-check": "tsc --noEmit", "typecheck": "tsc --noEmit", "check": "biome check", @@ -18,11 +19,9 @@ "prepare": "simple-git-hooks", "commitlint": "commitlint", "setup": "bun scripts/setup.ts", - "dev": "concurrently --names \"app,docs\" --prefix-colors \"cyan,magenta\" \"bun dev:app\" \"bun dev:docs\"", + "dev": "bun dev:app", "dev:app": "WEBKIT_DISABLE_DMABUF_RENDERER=1 tauri dev", "dev:scan": "WEBKIT_DISABLE_DMABUF_RENDERER=1 VITE_REACT_SCAN=true tauri dev", - "dev:docs": "bun --cwd docs dev", - "extensions:index": "bun scripts/build-extensions-index.ts", "pre-release": "bun scripts/pre-release-check.ts", "release": "bun scripts/release.ts", "release:patch": "bun scripts/release.ts patch", @@ -30,6 +29,8 @@ "release:major": "bun scripts/release.ts major" }, "dependencies": { + "@fontsource-variable/geist": "^5.2.8", + "@fontsource-variable/geist-mono": "^5.2.7", "@tanstack/react-virtual": "^3.13.12", "@tauri-apps/api": "^2.9.1", "@tauri-apps/plugin-clipboard-manager": "~2.3.2", diff --git a/scripts/build-extensions-index.ts b/scripts/build-extensions-index.ts deleted file mode 100644 index 9e981316..00000000 --- a/scripts/build-extensions-index.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { readFile, writeFile } from "node:fs/promises"; -import { join } from "node:path"; - -type RegistryEntry = { - id?: string; - name?: string; - displayName?: string; - description?: string; - version?: string; - publisher?: string; - category?: string; - icon?: string; - manifestUrl?: string; - downloads?: number; - rating?: number; -}; - -type Registry = { - extensions?: RegistryEntry[]; -}; - -function normalizeCategory(raw?: string) { - const value = (raw ?? "").toLowerCase().replace(/[_-]+/g, " ").trim(); - - if (value.includes("icon") && value.includes("theme")) return "Icon Themes"; - if (value === "icon" || value === "icon theme" || value === "icon themes") return "Icon Themes"; - if (value === "theme" || value === "themes") return "Themes"; - if (value === "language" || value === "languages" || value === "lang") return "Languages"; - - return "Languages"; -} - -const registryPath = join(process.cwd(), "extensions", "registry.json"); -const outputPath = join(process.cwd(), "extensions", "index.json"); - -const registryRaw = await readFile(registryPath, "utf8"); -const registry = JSON.parse(registryRaw) as Registry; - -const extensions = (registry.extensions ?? []).map((entry) => ({ - id: entry.id ?? "", - name: entry.displayName ?? entry.name ?? entry.id ?? "Untitled", - description: entry.description ?? "", - version: entry.version ?? "0.0.0", - author: entry.publisher ?? "Athas", - category: normalizeCategory(entry.category), - icon: entry.icon ?? "", - manifestUrl: entry.manifestUrl ?? "", - downloads: entry.downloads ?? 0, - rating: entry.rating ?? 0, -})); - -await writeFile(outputPath, JSON.stringify(extensions, null, 2) + "\n"); -console.log(`Wrote ${extensions.length} extensions to ${outputPath}`); diff --git a/scripts/pre-release-check.ts b/scripts/pre-release-check.ts index 2aab9ef7..5bb4acfb 100644 --- a/scripts/pre-release-check.ts +++ b/scripts/pre-release-check.ts @@ -175,28 +175,6 @@ async function main() { return { passed: true }; }); - header("Changelog"); - - // Check: Changelog has entry for current version - await runCheck("Changelog has version entry", async () => { - const changelogPath = join(process.cwd(), "CHANGELOG.md"); - if (!existsSync(changelogPath)) { - return { passed: true, warning: true, message: "CHANGELOG.md not found" }; - } - - const changelog = readFileSync(changelogPath, "utf-8"); - const versionPattern = new RegExp(`## \\[${currentVersion}\\]`); - - if (!versionPattern.test(changelog)) { - return { - passed: true, - warning: true, - message: `No entry for v${currentVersion} in CHANGELOG.md`, - }; - } - return { passed: true }; - }); - header("Frontend Checks"); // Check: TypeScript diff --git a/scripts/release.ts b/scripts/release.ts index db28bafe..bb9e337d 100755 --- a/scripts/release.ts +++ b/scripts/release.ts @@ -144,8 +144,8 @@ async function release() { const newVersion = bumpVersion(currentVersion, bumpType); info(`New version: ${newVersion}`); - // Get commits for changelog - log("\n📝 Generating changelog...\n", "yellow"); + // Get commits since last release + log("\nFetching recent commits...\n", "yellow"); const commits = await getCommitsSinceLastTag(); if (commits.length === 0) { diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 463e8331..17ca209b 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -25,9 +25,10 @@ dirs = "5.0" ec4rs = "1.2" env_logger = "0.11.8" futures-util = "0.3" -git2 = { version = "0.20", features = ["vendored-openssl"] } +git2 = { version = "0.20", features = ["vendored-libgit2", "vendored-openssl"] } interceptor = { path = "./packages/interceptor" } lazy_static = "1.4" +mimalloc = { version = "0.1", default-features = false } log = "0.4.27" lsp-types = { version = "0.95", features = ["proposed"] } notify = "8.1.0" @@ -42,6 +43,7 @@ flate2 = "1.0" serde = { version = "1.0", features = ["derive"] } sha2 = "0.10" serde_json = "1.0" +keyring = "3.6.3" ssh2 = { version = "0.9", features = ["vendored-openssl"] } tauri = { version = "2", features = ["protocol-asset", "macos-private-api", "unstable", "test"] } tauri-plugin-dialog = "2" @@ -81,3 +83,4 @@ tauri-plugin-window-state = "2" [target.'cfg(target_os = "macos")'.dependencies] rand = "0.8.5" +objc = "0.2" diff --git a/src-tauri/capabilities/main.json b/src-tauri/capabilities/main.json index c266c356..f4467219 100644 --- a/src-tauri/capabilities/main.json +++ b/src-tauri/capabilities/main.json @@ -34,9 +34,6 @@ "allow": [ { "path": "$HOME/**" - }, - { - "path": "/**" } ], "requireLiteralLeadingDot": false @@ -55,6 +52,12 @@ { "url": "https://athas.dev/**" }, + { + "url": "http://localhost:3000/**" + }, + { + "url": "http://127.0.0.1:3000/**" + }, { "url": "https://generativelanguage.googleapis.com/**" }, @@ -77,59 +80,6 @@ } ] }, - { - "identifier": "shell:allow-execute", - "allow": [ - { - "name": "sh", - "cmd": "sh", - "args": [ - "-c", - { - "validator": ".*" - } - ] - }, - { - "name": "gsettings", - "cmd": "gsettings", - "args": [ - "get", - "org.gnome.desktop.interface", - { - "validator": "(gtk-theme|color-scheme)" - } - ] - }, - { - "name": "kreadconfig5", - "cmd": "kreadconfig5", - "args": [ - "--file", - "kdeglobals", - "--group", - "General", - "--key", - "ColorScheme" - ] - }, - { - "name": "defaults", - "cmd": "defaults", - "args": ["read", "-g", "AppleInterfaceStyle"] - }, - { - "name": "reg", - "cmd": "reg", - "args": [ - "query", - "HKCU\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize", - "/v", - "AppsUseLightTheme" - ] - } - ] - }, "store:default", "deep-link:default", "updater:default" diff --git a/src-tauri/src/commands/ai/acp.rs b/src-tauri/src/commands/ai/acp.rs index 14ac7772..8795bcb6 100644 --- a/src-tauri/src/commands/ai/acp.rs +++ b/src-tauri/src/commands/ai/acp.rs @@ -30,7 +30,7 @@ pub async fn start_acp_agent( workspace_path: Option, env_vars: Option>, ) -> Result { - let mut bridge = bridge.lock().await; + let bridge = { bridge.lock().await.clone() }; bridge .start_agent(&agent_id, workspace_path, env_vars.unwrap_or_default()) .await @@ -39,7 +39,7 @@ pub async fn start_acp_agent( #[tauri::command] pub async fn stop_acp_agent(bridge: State<'_, AcpBridgeState>) -> Result { - let mut bridge = bridge.lock().await; + let bridge = { bridge.lock().await.clone() }; bridge.stop_agent().await.map_err(|e| e.to_string())?; Ok(bridge.get_status().await) } @@ -49,13 +49,13 @@ pub async fn send_acp_prompt( bridge: State<'_, AcpBridgeState>, prompt: String, ) -> Result<(), String> { - let bridge = bridge.lock().await; + let bridge = { bridge.lock().await.clone() }; bridge.send_prompt(&prompt).await.map_err(|e| e.to_string()) } #[tauri::command] pub async fn get_acp_status(bridge: State<'_, AcpBridgeState>) -> Result { - let bridge = bridge.lock().await; + let bridge = { bridge.lock().await.clone() }; Ok(bridge.get_status().await) } @@ -64,7 +64,7 @@ pub async fn respond_acp_permission( bridge: State<'_, AcpBridgeState>, args: PermissionResponseArgs, ) -> Result<(), String> { - let bridge = bridge.lock().await; + let bridge = { bridge.lock().await.clone() }; bridge .respond_to_permission(args.request_id, args.approved, args.cancelled) .await @@ -76,7 +76,7 @@ pub async fn set_acp_session_mode( bridge: State<'_, AcpBridgeState>, mode_id: String, ) -> Result<(), String> { - let bridge = bridge.lock().await; + let bridge = { bridge.lock().await.clone() }; bridge .set_session_mode(&mode_id) .await @@ -85,6 +85,6 @@ pub async fn set_acp_session_mode( #[tauri::command] pub async fn cancel_acp_prompt(bridge: State<'_, AcpBridgeState>) -> Result<(), String> { - let bridge = bridge.lock().await; + let bridge = { bridge.lock().await.clone() }; bridge.cancel_prompt().await.map_err(|e| e.to_string()) } diff --git a/src-tauri/src/commands/ai/auth.rs b/src-tauri/src/commands/ai/auth.rs index 7250a052..cfa2b00b 100644 --- a/src-tauri/src/commands/ai/auth.rs +++ b/src-tauri/src/commands/ai/auth.rs @@ -1,61 +1,22 @@ +use crate::secure_storage::{get_secret, remove_secret, store_secret}; use tauri::command; -/// Store the auth token securely +const AUTH_TOKEN_KEY: &str = "athas_auth_token"; + +/// Store the auth token using OS keychain when available. #[command] pub async fn store_auth_token(app: tauri::AppHandle, token: String) -> Result<(), String> { - use tauri_plugin_store::StoreExt; - - let store = app - .store("secure.json") - .map_err(|e| format!("Failed to access store: {e}"))?; - - store.set( - "athas_auth_token".to_string(), - serde_json::Value::String(token), - ); - - store - .save() - .map_err(|e| format!("Failed to save store: {e}"))?; - - Ok(()) + store_secret(&app, AUTH_TOKEN_KEY, &token) } /// Get the stored auth token #[command] pub async fn get_auth_token(app: tauri::AppHandle) -> Result, String> { - use tauri_plugin_store::StoreExt; - - let store = app - .store("secure.json") - .map_err(|e| format!("Failed to access store: {e}"))?; - - match store.get("athas_auth_token") { - Some(token) => { - if let Some(token_str) = token.as_str() { - Ok(Some(token_str.to_string())) - } else { - Ok(None) - } - } - None => Ok(None), - } + get_secret(&app, AUTH_TOKEN_KEY) } /// Remove the auth token #[command] pub async fn remove_auth_token(app: tauri::AppHandle) -> Result<(), String> { - use tauri_plugin_store::StoreExt; - - let store = app - .store("secure.json") - .map_err(|e| format!("Failed to access store: {e}"))?; - - let _removed = store.delete("athas_auth_token"); - - store - .save() - .map_err(|e| format!("Failed to save store: {e}"))?; - - Ok(()) + remove_secret(&app, AUTH_TOKEN_KEY) } diff --git a/src-tauri/src/commands/ai/tokens.rs b/src-tauri/src/commands/ai/tokens.rs index 421d8fef..c5a32ae6 100644 --- a/src-tauri/src/commands/ai/tokens.rs +++ b/src-tauri/src/commands/ai/tokens.rs @@ -1,26 +1,18 @@ +use crate::secure_storage::{get_secret, remove_secret, store_secret}; use tauri::command; -/// Store an AI provider token securely +fn provider_key(provider_id: &str) -> String { + format!("ai_token_{}", provider_id) +} + +/// Store an AI provider token using OS keychain when available. #[command] pub async fn store_ai_provider_token( app: tauri::AppHandle, provider_id: String, token: String, ) -> Result<(), String> { - use tauri_plugin_store::StoreExt; - - let store = app - .store("secure.json") - .map_err(|e| format!("Failed to access store: {e}"))?; - - let key = format!("ai_token_{}", provider_id); - store.set(key, serde_json::Value::String(token)); - - store - .save() - .map_err(|e| format!("Failed to save store: {e}"))?; - - Ok(()) + store_secret(&app, &provider_key(&provider_id), &token) } /// Get an AI provider token @@ -29,23 +21,7 @@ pub async fn get_ai_provider_token( app: tauri::AppHandle, provider_id: String, ) -> Result, String> { - use tauri_plugin_store::StoreExt; - - let store = app - .store("secure.json") - .map_err(|e| format!("Failed to access store: {e}"))?; - - let key = format!("ai_token_{}", provider_id); - match store.get(&key) { - Some(token) => { - if let Some(token_str) = token.as_str() { - Ok(Some(token_str.to_string())) - } else { - Ok(None) - } - } - None => Ok(None), - } + get_secret(&app, &provider_key(&provider_id)) } /// Remove an AI provider token @@ -54,18 +30,5 @@ pub async fn remove_ai_provider_token( app: tauri::AppHandle, provider_id: String, ) -> Result<(), String> { - use tauri_plugin_store::StoreExt; - - let store = app - .store("secure.json") - .map_err(|e| format!("Failed to access store: {e}"))?; - - let key = format!("ai_token_{}", provider_id); - let _removed = store.delete(&key); - - store - .save() - .map_err(|e| format!("Failed to save store: {e}"))?; - - Ok(()) + remove_secret(&app, &provider_key(&provider_id)) } diff --git a/src-tauri/src/commands/development/lsp.rs b/src-tauri/src/commands/development/lsp.rs index f2498627..462db943 100644 --- a/src-tauri/src/commands/development/lsp.rs +++ b/src-tauri/src/commands/development/lsp.rs @@ -137,9 +137,10 @@ pub fn lsp_document_open( lsp_manager: State<'_, LspManager>, file_path: String, content: String, + language_id: Option, ) -> LspResult<()> { lsp_manager - .notify_document_open(&file_path, content) + .notify_document_open(&file_path, content, language_id) .map_err(Into::into) } diff --git a/src-tauri/src/commands/development/tools.rs b/src-tauri/src/commands/development/tools.rs index c7ebd4a7..f5f14370 100644 --- a/src-tauri/src/commands/development/tools.rs +++ b/src-tauri/src/commands/development/tools.rs @@ -143,7 +143,12 @@ pub async fn get_tool_path( None => return Ok(None), }; - let path = ToolInstaller::get_tool_path(&app_handle, &config).map_err(|e| e.to_string())?; + let path = match tool_type { + ToolType::Lsp => { + ToolInstaller::get_lsp_launch_path(&app_handle, &config).map_err(|e| e.to_string())? + } + _ => ToolInstaller::get_tool_path(&app_handle, &config).map_err(|e| e.to_string())?, + }; if path.exists() { Ok(Some(path.to_string_lossy().to_string())) diff --git a/src-tauri/src/commands/extensions.rs b/src-tauri/src/commands/extensions.rs index 2b5cdf22..295763ac 100644 --- a/src-tauri/src/commands/extensions.rs +++ b/src-tauri/src/commands/extensions.rs @@ -7,6 +7,42 @@ use std::{ path::{Path, PathBuf}, }; use tauri::{AppHandle, Runtime, command}; +use url::Url; + +fn validate_extension_id(extension_id: &str) -> Result<(), String> { + if extension_id.is_empty() || extension_id.len() > 128 { + return Err("Invalid extension id length".to_string()); + } + if extension_id.contains("..") || extension_id.contains('/') || extension_id.contains('\\') { + return Err("Invalid extension id path characters".to_string()); + } + if !extension_id + .chars() + .all(|ch| ch.is_ascii_alphanumeric() || ch == '.' || ch == '_' || ch == '-') + { + return Err("Invalid extension id characters".to_string()); + } + Ok(()) +} + +fn validate_extension_download_url(input: &str) -> Result<(), String> { + let parsed = Url::parse(input).map_err(|_| "Invalid extension download URL".to_string())?; + let host = parsed.host_str().unwrap_or_default(); + match parsed.scheme() { + "https" => { + if !cfg!(debug_assertions) && !host.ends_with("athas.dev") { + return Err("Extension download host is not allowed".to_string()); + } + } + "http" if cfg!(debug_assertions) => { + if host != "localhost" && host != "127.0.0.1" { + return Err("Insecure extension download URL is not allowed".to_string()); + } + } + _ => return Err("Extension download URL must use HTTPS".to_string()), + } + Ok(()) +} #[command] pub async fn download_extension( @@ -14,6 +50,9 @@ pub async fn download_extension( extension_id: String, checksum: String, ) -> Result { + validate_extension_id(&extension_id)?; + validate_extension_download_url(&url)?; + // Get extensions directory let extensions_dir = get_extensions_dir()?; let download_dir = extensions_dir.join("downloads"); @@ -68,13 +107,18 @@ pub async fn download_extension( #[command] pub fn install_extension(extension_id: String, package_path: String) -> Result<(), String> { + validate_extension_id(&extension_id)?; + // Get extensions directory let extensions_dir = get_extensions_dir()?; let installed_dir = extensions_dir.join("installed"); + let download_dir = extensions_dir.join("downloads"); // Create installed directory if it doesn't exist fs::create_dir_all(&installed_dir) .map_err(|e| format!("Failed to create installed directory: {}", e))?; + fs::create_dir_all(&download_dir) + .map_err(|e| format!("Failed to create downloads directory: {}", e))?; // Create extension directory let extension_dir = installed_dir.join(&extension_id); @@ -83,19 +127,28 @@ pub fn install_extension(extension_id: String, package_path: String) -> Result<( // Copy WASM file to installed directory let source_path = Path::new(&package_path); + let canonical_source = fs::canonicalize(source_path) + .map_err(|e| format!("Failed to resolve extension package path: {}", e))?; + let canonical_download_dir = fs::canonicalize(&download_dir) + .map_err(|e| format!("Failed to resolve downloads directory: {}", e))?; + if !canonical_source.starts_with(&canonical_download_dir) { + return Err("Extension package path is outside the downloads directory".to_string()); + } let target_path = extension_dir.join("extension.wasm"); - fs::copy(source_path, &target_path) + fs::copy(&canonical_source, &target_path) .map_err(|e| format!("Failed to copy extension file: {}", e))?; // Clean up download - fs::remove_file(source_path).ok(); + fs::remove_file(&canonical_source).ok(); Ok(()) } #[command] pub fn uninstall_extension(extension_id: String) -> Result<(), String> { + validate_extension_id(&extension_id)?; + // Get extensions directory let extensions_dir = get_extensions_dir()?; let installed_dir = extensions_dir.join("installed"); @@ -206,6 +259,9 @@ pub async fn install_extension_from_url( checksum: String, size: u64, ) -> Result<(), String> { + validate_extension_id(&extension_id)?; + validate_extension_download_url(&url)?; + log::info!("Installing extension {} from {}", extension_id, url); let installer = ExtensionInstaller::new(app_handle) @@ -225,6 +281,8 @@ pub async fn install_extension_from_url( #[command] pub fn uninstall_extension_new(app_handle: AppHandle, extension_id: String) -> Result<(), String> { + validate_extension_id(&extension_id)?; + log::info!("Uninstalling extension {}", extension_id); let installer = ExtensionInstaller::new(app_handle) @@ -249,6 +307,8 @@ pub fn list_installed_extensions_new( #[command] pub fn get_extension_path(app_handle: AppHandle, extension_id: String) -> Result { + validate_extension_id(&extension_id)?; + log::info!("Getting path for extension {}", extension_id); let installer = ExtensionInstaller::new(app_handle) @@ -337,4 +397,39 @@ mod tests { path_str ); } + + #[test] + fn test_validate_extension_id_accepts_safe_ids() { + assert!(validate_extension_id("language.typescript").is_ok()); + assert!(validate_extension_id("icon-theme_material").is_ok()); + assert!(validate_extension_id("theme-1").is_ok()); + } + + #[test] + fn test_validate_extension_id_rejects_unsafe_ids() { + assert!(validate_extension_id("../evil").is_err()); + assert!(validate_extension_id("evil/path").is_err()); + assert!(validate_extension_id("evil\\path").is_err()); + assert!(validate_extension_id("evil*id").is_err()); + assert!(validate_extension_id("").is_err()); + } + + #[test] + fn test_validate_extension_download_url_rejects_unsafe_schemes() { + assert!(validate_extension_download_url("file:///tmp/evil.tar.gz").is_err()); + assert!(validate_extension_download_url("javascript:alert(1)").is_err()); + assert!(validate_extension_download_url("ftp://example.com/ext.tar.gz").is_err()); + } + + #[test] + fn test_validate_extension_download_url_accepts_expected_hosts() { + assert!(validate_extension_download_url("https://athas.dev/extensions/test.tar.gz").is_ok()); + assert!( + validate_extension_download_url("https://cdn.athas.dev/extensions/test.tar.gz").is_ok() + ); + + if cfg!(debug_assertions) { + assert!(validate_extension_download_url("http://localhost:3000/test.tar.gz").is_ok()); + } + } } diff --git a/src-tauri/src/commands/ui/window.rs b/src-tauri/src/commands/ui/window.rs index 86a92cd7..a2036bfd 100644 --- a/src-tauri/src/commands/ui/window.rs +++ b/src-tauri/src/commands/ui/window.rs @@ -138,46 +138,6 @@ const SHORTCUT_INTERCEPTOR_SCRIPT: &str = r#" })(); "#; -/// React Grab script loader for localhost development URLs -/// Includes both the main react-grab script and the Claude Code client -const REACT_GRAB_SCRIPT: &str = r#" -(function() { - if (window.__REACT_GRAB_LOADED__) return; - window.__REACT_GRAB_LOADED__ = true; - - function loadScripts() { - var script1 = document.createElement('script'); - script1.src = 'https://unpkg.com/react-grab/dist/index.global.js'; - script1.crossOrigin = 'anonymous'; - - var script2 = document.createElement('script'); - script2.src = 'https://unpkg.com/@react-grab/claude-code/dist/client.global.js'; - script2.crossOrigin = 'anonymous'; - - // Load Claude Code client after main script loads - script1.onload = function() { - document.head.appendChild(script2); - }; - - document.head.appendChild(script1); - } - - // Inject as early as possible - if (document.head) { - loadScripts(); - } else { - // Wait for head to be available - var observer = new MutationObserver(function(mutations, obs) { - if (document.head) { - obs.disconnect(); - loadScripts(); - } - }); - observer.observe(document.documentElement || document, { childList: true, subtree: true }); - } -})(); -"#; - #[command] pub async fn create_embedded_webview( app: tauri::AppHandle, @@ -226,9 +186,6 @@ pub async fn create_embedded_webview( // Inject shortcut interceptor script webview_builder = webview_builder.initialization_script(SHORTCUT_INTERCEPTOR_SCRIPT); - // Always inject react-grab script for web viewer - webview_builder = webview_builder.initialization_script(REACT_GRAB_SCRIPT); - // Create embedded webview within the main window let webview = main_window .add_child( diff --git a/src-tauri/src/commands/vcs/git/stash.rs b/src-tauri/src/commands/vcs/git/stash.rs index 2ed345aa..8af7efdf 100644 --- a/src-tauri/src/commands/vcs/git/stash.rs +++ b/src-tauri/src/commands/vcs/git/stash.rs @@ -73,12 +73,12 @@ fn _git_create_stash( args.push(msg); } - if let Some(ref file_list) = files { - if !file_list.is_empty() { - args.push("--"); - for file in file_list { - args.push(file); - } + if let Some(ref file_list) = files + && !file_list.is_empty() + { + args.push("--"); + for file in file_list { + args.push(file); } } diff --git a/src-tauri/src/commands/vcs/git/status.rs b/src-tauri/src/commands/vcs/git/status.rs index 510ee8c3..e9c2c603 100644 --- a/src-tauri/src/commands/vcs/git/status.rs +++ b/src-tauri/src/commands/vcs/git/status.rs @@ -119,3 +119,22 @@ fn _git_init(repo_path: String) -> Result<()> { Repository::init(&repo_path).context("Failed to initialize repository")?; Ok(()) } + +#[command] +pub fn git_discover_repo(path: String) -> Result, String> { + let discovered = match Repository::discover(&path) { + Ok(repo) => { + if let Some(workdir) = repo.workdir() { + Some(workdir.to_string_lossy().to_string()) + } else { + repo + .path() + .parent() + .map(|parent| parent.to_string_lossy().to_string()) + } + } + Err(_) => None, + }; + + Ok(discovered) +} diff --git a/src-tauri/src/commands/vcs/github.rs b/src-tauri/src/commands/vcs/github.rs index 2b944fac..289cad5a 100644 --- a/src-tauri/src/commands/vcs/github.rs +++ b/src-tauri/src/commands/vcs/github.rs @@ -1,3 +1,4 @@ +use crate::secure_storage::{get_secret, remove_secret, store_secret}; use serde::{Deserialize, Serialize}; use std::{path::Path, process::Command}; use tauri::command; @@ -342,54 +343,15 @@ pub fn github_get_pr_comments( #[command] pub async fn store_github_token(app: tauri::AppHandle, token: String) -> Result<(), String> { - use tauri_plugin_store::StoreExt; - - let store = app - .store("secure.json") - .map_err(|e| format!("Failed to access store: {e}"))?; - - store.set("github_token", serde_json::Value::String(token)); - - store - .save() - .map_err(|e| format!("Failed to save store: {e}"))?; - - Ok(()) + store_secret(&app, "github_token", &token) } #[command] pub async fn get_github_token(app: tauri::AppHandle) -> Result, String> { - use tauri_plugin_store::StoreExt; - - let store = app - .store("secure.json") - .map_err(|e| format!("Failed to access store: {e}"))?; - - match store.get("github_token") { - Some(token) => { - if let Some(token_str) = token.as_str() { - Ok(Some(token_str.to_string())) - } else { - Ok(None) - } - } - None => Ok(None), - } + get_secret(&app, "github_token") } #[command] pub async fn remove_github_token(app: tauri::AppHandle) -> Result<(), String> { - use tauri_plugin_store::StoreExt; - - let store = app - .store("secure.json") - .map_err(|e| format!("Failed to access store: {e}"))?; - - let _removed = store.delete("github_token"); - - store - .save() - .map_err(|e| format!("Failed to save store: {e}"))?; - - Ok(()) + remove_secret(&app, "github_token") } diff --git a/src-tauri/src/extensions/installer.rs b/src-tauri/src/extensions/installer.rs index 241a550d..7b8374c4 100644 --- a/src-tauri/src/extensions/installer.rs +++ b/src-tauri/src/extensions/installer.rs @@ -11,6 +11,22 @@ pub struct ExtensionInstaller { extensions_dir: PathBuf, } +fn validate_extension_id(extension_id: &str) -> Result<()> { + if extension_id.is_empty() || extension_id.len() > 128 { + anyhow::bail!("Invalid extension id length"); + } + if extension_id.contains("..") || extension_id.contains('/') || extension_id.contains('\\') { + anyhow::bail!("Invalid extension id path characters"); + } + if !extension_id + .chars() + .all(|ch| ch.is_ascii_alphanumeric() || ch == '.' || ch == '_' || ch == '-') + { + anyhow::bail!("Invalid extension id characters"); + } + Ok(()) +} + impl ExtensionInstaller { pub fn new(app_handle: AppHandle) -> Result { let app_data_dir = app_handle @@ -35,6 +51,8 @@ impl ExtensionInstaller { extension_id: &str, download_info: &DownloadInfo, ) -> Result { + validate_extension_id(extension_id)?; + log::info!( "Downloading extension {} from {}", extension_id, @@ -54,8 +72,24 @@ impl ExtensionInstaller { // Download the file let response = reqwest::get(&download_info.url).await?; + if !response.status().is_success() { + anyhow::bail!( + "Failed to download extension {}: HTTP {}", + extension_id, + response.status() + ); + } let bytes = response.bytes().await?; + if download_info.size > 0 && bytes.len() as u64 != download_info.size { + anyhow::bail!( + "Downloaded extension size mismatch for {}: expected {}, got {}", + extension_id, + download_info.size, + bytes.len() + ); + } + log::info!( "Downloaded {} bytes for extension {}", bytes.len(), @@ -95,6 +129,8 @@ impl ExtensionInstaller { /// Extract extension archive async fn extract_extension(&self, extension_id: &str, archive_path: &Path) -> Result { + validate_extension_id(extension_id)?; + log::info!( "Extracting extension {} from {:?}", extension_id, @@ -124,7 +160,13 @@ impl ExtensionInstaller { let tar_gz = fs::File::open(archive_path)?; let tar = flate2::read::GzDecoder::new(tar_gz); let mut archive = tar::Archive::new(tar); - archive.unpack(&extension_dir)?; + for entry in archive.entries()? { + let mut entry = entry?; + let unpacked = entry.unpack_in(&extension_dir)?; + if !unpacked { + anyhow::bail!("Archive entry attempted to escape extension directory"); + } + } log::info!( "Extension {} extracted to {:?}", @@ -144,6 +186,8 @@ impl ExtensionInstaller { extension_id: String, download_info: DownloadInfo, ) -> Result<()> { + validate_extension_id(&extension_id)?; + log::info!("Installing extension {}", extension_id); // Emit initial progress @@ -193,6 +237,8 @@ impl ExtensionInstaller { /// Uninstall extension pub fn uninstall_extension(&self, extension_id: &str) -> Result<()> { + validate_extension_id(extension_id)?; + log::info!("Uninstalling extension {}", extension_id); let extension_dir = self.extensions_dir.join(extension_id); @@ -256,3 +302,28 @@ impl ExtensionInstaller { self.extensions_dir.join(extension_id) } } + +#[cfg(test)] +mod tests { + use super::validate_extension_id; + + #[test] + fn validate_extension_id_accepts_safe_values() { + assert!(validate_extension_id("language.typescript").is_ok()); + assert!(validate_extension_id("theme-dark_01").is_ok()); + } + + #[test] + fn validate_extension_id_rejects_path_traversal() { + assert!(validate_extension_id("../evil").is_err()); + assert!(validate_extension_id("evil/dir").is_err()); + assert!(validate_extension_id("evil\\dir").is_err()); + } + + #[test] + fn validate_extension_id_rejects_invalid_characters() { + assert!(validate_extension_id("evil$id").is_err()); + assert!(validate_extension_id("").is_err()); + assert!(validate_extension_id(&"a".repeat(129)).is_err()); + } +} diff --git a/src-tauri/src/features/ai/acp/bridge.rs b/src-tauri/src/features/ai/acp/bridge.rs index ed23c787..1fccd61f 100644 --- a/src-tauri/src/features/ai/acp/bridge.rs +++ b/src-tauri/src/features/ai/acp/bridge.rs @@ -3,9 +3,11 @@ use super::{ config::AgentRegistry, types::{AcpAgentStatus, AcpEvent, AgentConfig, SessionMode, SessionModeState, StopReason}, }; +use crate::terminal::TerminalManager; use acp::Agent; use agent_client_protocol as acp; use anyhow::{Context, Result, bail}; +use serde_json::json; use std::{process::Stdio, sync::Arc, thread}; use tauri::{AppHandle, Emitter}; use tokio::{ @@ -24,6 +26,7 @@ enum AcpCommand { config: Box, env_vars: std::collections::HashMap, app_handle: AppHandle, + terminal_manager: Arc, response_tx: oneshot::Sender)>>, }, SendPrompt { @@ -72,6 +75,7 @@ impl AcpWorker { workspace_path: Option, config: AgentConfig, app_handle: AppHandle, + terminal_manager: Arc, ) -> Result<(AcpAgentStatus, mpsc::Sender)> { // Stop any existing agent first self.stop().await?; @@ -130,8 +134,8 @@ impl AcpWorker { // Create ACP client let client = Arc::new(AthasAcpClient::new( app_handle.clone(), - agent_id.clone(), workspace_path.clone(), + terminal_manager, )); let permission_sender = client.permission_sender(); @@ -155,8 +159,37 @@ impl AcpWorker { }); // Initialize connection with timeout + let mut client_meta = acp::Meta::new(); + client_meta.insert( + "athas".to_string(), + json!({ + "extensionMethods": [ + { + "name": "athas.openWebViewer", + "description": "Open a URL in Athas web viewer", + "params": { "url": "string" } + }, + { + "name": "athas.openTerminal", + "description": "Open a terminal tab in Athas", + "params": { "command": "string|null" } + } + ], + "notes": "Call these via ACP extension methods, not shell commands." + }), + ); + + let client_capabilities = acp::ClientCapabilities::new() + .fs( + acp::FileSystemCapability::new() + .read_text_file(true) + .write_text_file(true), + ) + .terminal(true) + .meta(client_meta); + let init_request = acp::InitializeRequest::new(acp::ProtocolVersion::LATEST) - .client_capabilities(acp::ClientCapabilities::default()) + .client_capabilities(client_capabilities) .client_info(acp::Implementation::new("Athas", env!("CARGO_PKG_VERSION"))); log::info!("Sending ACP initialize request..."); @@ -268,18 +301,50 @@ impl AcpWorker { } async fn send_prompt(&self, prompt: &str) -> Result<()> { - let connection = self.connection.as_ref().context("No active connection")?; - let session_id = self.session_id.as_ref().context("No active session")?; + let connection = self + .connection + .as_ref() + .context("No active connection")? + .clone(); + let session_id = self + .session_id + .as_ref() + .context("No active session")? + .clone(); let app_handle = self .app_handle .as_ref() - .context("No app handle available")?; + .context("No app handle available")? + .clone(); + let prompt = prompt.to_string(); + + tokio::task::spawn_local(async move { + if let Err(err) = + Self::run_prompt(connection, session_id.clone(), app_handle.clone(), prompt).await + { + log::error!("Failed to run ACP prompt: {}", err); + let _ = app_handle.emit( + "acp-event", + AcpEvent::Error { + session_id: Some(session_id.to_string()), + error: format!("Failed to run prompt: {}", err), + }, + ); + } + }); + Ok(()) + } + + async fn run_prompt( + connection: Arc, + session_id: acp::SessionId, + app_handle: AppHandle, + prompt: String, + ) -> Result<()> { let prompt_request = acp::PromptRequest::new( session_id.clone(), - vec![acp::ContentBlock::Text(acp::TextContent::new( - prompt.to_string(), - ))], + vec![acp::ContentBlock::Text(acp::TextContent::new(prompt))], ); let response = connection @@ -363,16 +428,18 @@ impl AcpWorker { } /// Manages ACP agent connections via a dedicated worker thread +#[derive(Clone)] pub struct AcpAgentBridge { app_handle: AppHandle, registry: AgentRegistry, command_tx: mpsc::Sender, status: Arc>, permission_tx: Arc>>>, + terminal_manager: Arc, } impl AcpAgentBridge { - pub fn new(app_handle: AppHandle) -> Self { + pub fn new(app_handle: AppHandle, terminal_manager: Arc) -> Self { let mut registry = AgentRegistry::new(); registry.detect_installed(); @@ -396,6 +463,7 @@ impl AcpAgentBridge { command_tx, status, permission_tx: Arc::new(Mutex::new(None)), + terminal_manager, } } @@ -413,6 +481,7 @@ impl AcpAgentBridge { config, env_vars, app_handle, + terminal_manager, response_tx, } => { let mut config = *config; @@ -420,7 +489,13 @@ impl AcpAgentBridge { config.env_vars.insert(key, value); } let result = worker - .initialize(agent_id, workspace_path, config, app_handle) + .initialize( + agent_id, + workspace_path, + config, + app_handle, + terminal_manager, + ) .await; // Update shared status @@ -472,7 +547,7 @@ impl AcpAgentBridge { /// Start an ACP agent by ID pub async fn start_agent( - &mut self, + &self, agent_id: &str, workspace_path: Option, env_vars: std::collections::HashMap, @@ -498,6 +573,7 @@ impl AcpAgentBridge { config: Box::new(config), env_vars, app_handle: self.app_handle.clone(), + terminal_manager: self.terminal_manager.clone(), response_tx, }) .await @@ -555,7 +631,15 @@ impl AcpAgentBridge { } /// Stop the active agent - pub async fn stop_agent(&mut self) -> Result<()> { + pub async fn stop_agent(&self) -> Result<()> { + // Get current session ID before stopping + let current_status = self.status.lock().await.clone(); + let session_id = if current_status.running { + Some(current_status.agent_id.clone()) + } else { + None + }; + let (response_tx, response_rx) = oneshot::channel(); self @@ -572,6 +656,13 @@ impl AcpAgentBridge { *tx = None; } + // Emit SessionComplete before StatusChanged + if let Some(sid) = session_id { + let _ = self + .app_handle + .emit("acp-event", AcpEvent::SessionComplete { session_id: sid }); + } + // Emit status change self.emit_status_change(&AcpAgentStatus::default()); diff --git a/src-tauri/src/features/ai/acp/client.rs b/src-tauri/src/features/ai/acp/client.rs index 439e8b58..db6ebeb1 100644 --- a/src-tauri/src/features/ai/acp/client.rs +++ b/src-tauri/src/features/ai/acp/client.rs @@ -1,9 +1,13 @@ -use super::types::{AcpContentBlock, AcpEvent, AcpToolStatus}; +use super::types::{AcpContentBlock, AcpEvent, UiAction}; +use crate::terminal::{TerminalManager, config::TerminalConfig}; use agent_client_protocol as acp; use async_trait::async_trait; -use std::{collections::HashMap, sync::Arc}; -use tauri::{AppHandle, Emitter}; -use tokio::sync::{Mutex, mpsc}; +use std::{ + collections::HashMap, + sync::{Arc, Mutex as StdMutex}, +}; +use tauri::{AppHandle, Emitter, Listener}; +use tokio::sync::{Mutex, mpsc, oneshot}; /// Response for permission requests pub struct PermissionResponse { @@ -12,36 +16,85 @@ pub struct PermissionResponse { pub cancelled: bool, } -#[derive(Clone)] -struct ActiveToolState { - name: String, - input: serde_json::Value, - kind: Option, +/// Tracks state for an ACP terminal session +struct AcpTerminalState { + athas_terminal_id: String, + output_buffer: String, + max_output_bytes: usize, + truncated: bool, + exit_status: Option, + exit_waiters: Vec>, +} + +impl AcpTerminalState { + fn new(athas_terminal_id: String, max_output_bytes: Option) -> Self { + Self { + athas_terminal_id, + output_buffer: String::new(), + max_output_bytes: max_output_bytes.unwrap_or(1_000_000) as usize, + truncated: false, + exit_status: None, + exit_waiters: Vec::new(), + } + } + + fn append_output(&mut self, data: &str) { + if self.output_buffer.len() + data.len() > self.max_output_bytes { + let remaining = self + .max_output_bytes + .saturating_sub(self.output_buffer.len()); + if remaining > 0 { + self + .output_buffer + .push_str(&data[..remaining.min(data.len())]); + } + self.truncated = true; + } else { + self.output_buffer.push_str(data); + } + } + + fn set_exit_status(&mut self, exit_code: Option, signal: Option) { + let status = acp::TerminalExitStatus::new() + .exit_code(exit_code.unwrap_or(0)) + .signal(signal); + self.exit_status = Some(status.clone()); + + // Notify all waiters + for waiter in self.exit_waiters.drain(..) { + let _ = waiter.send(status.clone()); + } + } } /// Athas ACP Client implementation /// Handles requests from the agent (file access, terminals, permissions) pub struct AthasAcpClient { app_handle: AppHandle, - agent_id: String, workspace_path: Option, permission_tx: mpsc::Sender, permission_rx: Arc>>, current_session_id: Arc>>, - active_tools: Arc>>, + terminal_manager: Arc, + /// Maps ACP terminal IDs to terminal state (uses StdMutex for sync access from event listeners) + terminal_states: Arc>>, } impl AthasAcpClient { - pub fn new(app_handle: AppHandle, agent_id: String, workspace_path: Option) -> Self { + pub fn new( + app_handle: AppHandle, + workspace_path: Option, + terminal_manager: Arc, + ) -> Self { let (permission_tx, permission_rx) = mpsc::channel(32); Self { app_handle, - agent_id, workspace_path, permission_tx, permission_rx: Arc::new(Mutex::new(permission_rx)), current_session_id: Arc::new(Mutex::new(None)), - active_tools: Arc::new(Mutex::new(HashMap::new())), + terminal_manager, + terminal_states: Arc::new(StdMutex::new(HashMap::new())), } } @@ -70,46 +123,148 @@ impl AthasAcpClient { path.to_string() } - fn map_tool_status(status: acp::ToolCallStatus) -> AcpToolStatus { - match status { - acp::ToolCallStatus::Pending => AcpToolStatus::Pending, - acp::ToolCallStatus::InProgress => AcpToolStatus::InProgress, - acp::ToolCallStatus::Completed => AcpToolStatus::Completed, - acp::ToolCallStatus::Failed => AcpToolStatus::Failed, - _ => AcpToolStatus::InProgress, + fn extract_first_url(text: &str) -> Option { + for scheme in ["https://", "http://"] { + if let Some(start) = text.find(scheme) { + let rest = &text[start..]; + let end = rest + .find(|c: char| { + c.is_whitespace() + || matches!(c, '"' | '\'' | '`' | ')' | '}' | ']' | '|' | '<' | '>') + }) + .unwrap_or(rest.len()); + let url = rest[..end].trim_end_matches(['.', ',', ';']); + if !url.is_empty() { + return Some(url.to_string()); + } + } } + None } - fn map_tool_kind(kind: acp::ToolKind) -> String { - match kind { - acp::ToolKind::Read => "read".to_string(), - acp::ToolKind::Edit => "edit".to_string(), - acp::ToolKind::Delete => "delete".to_string(), - acp::ToolKind::Move => "move".to_string(), - acp::ToolKind::Search => "search".to_string(), - acp::ToolKind::Execute => "execute".to_string(), - acp::ToolKind::Think => "think".to_string(), - acp::ToolKind::Fetch => "fetch".to_string(), - acp::ToolKind::SwitchMode => "switch_mode".to_string(), - _ => "other".to_string(), + fn extract_json_string_fields(text: &str, field: &str) -> Vec { + let mut values = Vec::new(); + let needle = format!("\"{}\"", field); + let mut offset = 0usize; + + while let Some(rel_idx) = text[offset..].find(&needle) { + let start = offset + rel_idx + needle.len(); + let Some(colon_rel) = text[start..].find(':') else { + break; + }; + let after_colon = start + colon_rel + 1; + let rest = &text[after_colon..]; + let trimmed = rest.trim_start(); + let ws = rest.len().saturating_sub(trimmed.len()); + if !trimmed.starts_with('"') { + offset = after_colon + ws + 1; + continue; + } + + let mut escaped = false; + let mut end = None; + for (i, ch) in trimmed[1..].char_indices() { + if escaped { + escaped = false; + continue; + } + if ch == '\\' { + escaped = true; + continue; + } + if ch == '"' { + end = Some(1 + i); + break; + } + } + + if let Some(end_idx) = end { + let value = &trimmed[1..end_idx]; + values.push(value.to_string()); + offset = after_colon + ws + end_idx + 1; + } else { + break; + } } + + values } - fn extract_error(raw_output: &Option) -> Option { - let Some(raw) = raw_output else { + fn extract_webviewer_fallback_url( + tool_title: &str, + raw_input: Option<&serde_json::Value>, + ) -> Option { + let raw_input_text = raw_input + .and_then(|value| serde_json::to_string(value).ok()) + .unwrap_or_default(); + + let references_webviewer = tool_title.contains("athas.openWebViewer") + || raw_input_text.contains("athas.openWebViewer") + || (raw_input_text.contains("openWebViewer") && raw_input_text.contains("ext_method")); + + if !references_webviewer { return None; - }; + } + + Self::extract_first_url(tool_title).or_else(|| Self::extract_first_url(&raw_input_text)) + } + + fn extract_terminal_fallback_command( + tool_title: &str, + raw_input: Option<&serde_json::Value>, + ) -> Option { + let raw_input_text = raw_input + .and_then(|value| serde_json::to_string(value).ok()) + .unwrap_or_default(); + + let references_terminal = tool_title.contains("athas.openTerminal") + || raw_input_text.contains("athas.openTerminal") + || (raw_input_text.contains("openTerminal") && raw_input_text.contains("ext_method")); - if let Some(error) = raw.get("error").and_then(|v| v.as_str()) { - return Some(error.to_string()); + if !references_terminal { + return None; + } + + let candidates = Self::extract_json_string_fields(&raw_input_text, "command"); + for candidate in candidates { + let candidate = candidate.trim(); + if candidate.is_empty() { + continue; + } + if candidate.contains("ext_method") || candidate.contains("athas.openTerminal") { + continue; + } + return Some(candidate.to_string()); } - if let Some(message) = raw.get("message").and_then(|v| v.as_str()) { - return Some(message.to_string()); + if raw_input_text.contains("lazygit") || tool_title.contains("lazygit") { + return Some("lazygit".to_string()); } None } + + fn fallback_permission_response( + args: &acp::RequestPermissionRequest, + ) -> acp::RequestPermissionResponse { + let selected_option = args + .options + .iter() + .find(|opt| { + matches!( + opt.kind, + acp::PermissionOptionKind::RejectOnce | acp::PermissionOptionKind::RejectAlways + ) + }) + .or_else(|| args.options.first()) + .map(|opt| acp::SelectedPermissionOutcome::new(opt.option_id.clone())); + + if let Some(selected) = selected_option { + acp::RequestPermissionResponse::new(acp::RequestPermissionOutcome::Selected(selected)) + } else { + acp::RequestPermissionResponse::new(acp::RequestPermissionOutcome::Cancelled) + } + } } #[async_trait(?Send)] @@ -119,16 +274,29 @@ impl acp::Client for AthasAcpClient { args: acp::RequestPermissionRequest, ) -> acp::Result { let request_id = uuid::Uuid::new_v4().to_string(); + let session_id = args.session_id.to_string(); // Extract tool call info for the permission request let tool_call_id = args.tool_call.tool_call_id.clone(); + let tool_title = args + .tool_call + .fields + .title + .as_deref() + .unwrap_or("Tool call"); + let fallback_webviewer_url = + Self::extract_webviewer_fallback_url(tool_title, args.tool_call.fields.raw_input.as_ref()); + let fallback_terminal_command = Self::extract_terminal_fallback_command( + tool_title, + args.tool_call.fields.raw_input.as_ref(), + ); // Emit permission request to frontend self.emit_event(AcpEvent::PermissionRequest { request_id: request_id.clone(), permission_type: "tool_call".to_string(), resource: tool_call_id.to_string(), - description: format!("Tool call: {}", tool_call_id), + description: format!("{} ({})", tool_title, tool_call_id), }); // Wait for user response with timeout @@ -151,6 +319,27 @@ impl acp::Client for AthasAcpClient { } if response.approved { + if let Some(url) = fallback_webviewer_url.clone() { + // Claude Code adapters may try to invoke ext_method via shell command. + // Execute the equivalent Athas UI action directly and reject the shell tool call. + self.emit_event(AcpEvent::UiAction { + session_id: session_id.clone(), + action: UiAction::OpenWebViewer { url }, + }); + return Ok(Self::fallback_permission_response(&args)); + } + + if let Some(command) = fallback_terminal_command.clone() { + // Same fallback for athas.openTerminal misuse through shell commands. + self.emit_event(AcpEvent::UiAction { + session_id: session_id.clone(), + action: UiAction::OpenTerminal { + command: Some(command), + }, + }); + return Ok(Self::fallback_permission_response(&args)); + } + // Prefer allow-once/allow-always options if available let selected_option = args .options @@ -231,108 +420,50 @@ impl acp::Client for AthasAcpClient { }); } acp::SessionUpdate::ToolCall(tool_call) => { - let tool_id = tool_call.tool_call_id.to_string(); - let input = tool_call - .raw_input - .clone() - .unwrap_or(serde_json::Value::Null); - let kind = Some(Self::map_tool_kind(tool_call.kind)); - let status = Some(Self::map_tool_status(tool_call.status)); - let content = serde_json::to_value(&tool_call.content).ok(); - let locations = serde_json::to_value(&tool_call.locations).ok(); - { - let mut active_tools = self.active_tools.lock().await; - active_tools.insert( - tool_id.clone(), - ActiveToolState { - name: tool_call.title.clone(), - input: input.clone(), - kind: kind.clone(), - }, - ); - } + // ToolCall has: tool_call_id, title, kind, status, content, etc. + // Content may contain the input; we serialize the whole content for display + let input = if tool_call.content.is_empty() { + serde_json::Value::Null + } else { + serde_json::to_value(&tool_call.content).unwrap_or(serde_json::Value::Null) + }; + self.emit_event(AcpEvent::ToolStart { session_id, tool_name: tool_call.title.clone(), - tool_id: tool_id.clone(), + tool_id: tool_call.tool_call_id.to_string(), input, - status, - kind, - content, - locations, + status: None, + kind: None, + content: None, + locations: None, }); } acp::SessionUpdate::ToolCallUpdate(update) => { - let tool_id = update.tool_call_id.to_string(); - let fields = update.fields.clone(); - let status_raw = fields.status.unwrap_or(acp::ToolCallStatus::Completed); - let status = Some(Self::map_tool_status(status_raw)); - let success = !matches!(status_raw, acp::ToolCallStatus::Failed); - let kind = fields.kind.map(Self::map_tool_kind); - let content = fields - .content - .as_ref() - .and_then(|items| serde_json::to_value(items).ok()); - let locations = fields - .locations - .as_ref() - .and_then(|items| serde_json::to_value(items).ok()); - - let (tool_name, input, fallback_kind) = { - let mut active_tools = self.active_tools.lock().await; - let previous = active_tools.get(&tool_id).cloned(); - - let tool_name = fields - .title - .clone() - .or_else(|| previous.as_ref().map(|t| t.name.clone())) - .unwrap_or_else(|| "unknown".to_string()); - - let input = fields - .raw_input - .clone() - .or_else(|| previous.as_ref().map(|t| t.input.clone())); - - let fallback_kind = previous.and_then(|t| t.kind); - - if matches!( - status_raw, - acp::ToolCallStatus::Completed | acp::ToolCallStatus::Failed - ) { - active_tools.remove(&tool_id); - } else { - active_tools.insert( - tool_id.clone(), - ActiveToolState { - name: tool_name.clone(), - input: input.clone().unwrap_or(serde_json::Value::Null), - kind: kind.clone().or(fallback_kind.clone()), - }, - ); - } - - (tool_name, input, fallback_kind) - }; - - let output = fields.raw_output.clone(); - let error = if success { + // Check tool status to determine success + // ToolCallUpdate has fields: kind, status, title, content, etc. + let success = matches!( + update.fields.status, None - } else { - Self::extract_error(&output).or_else(|| Some("Tool execution failed".to_string())) - }; + | Some( + acp::ToolCallStatus::Pending + | acp::ToolCallStatus::InProgress + | acp::ToolCallStatus::Completed + ) + ); self.emit_event(AcpEvent::ToolComplete { session_id, - tool_id, + tool_id: update.tool_call_id.to_string(), success, - tool_name: Some(tool_name), - input, - output, - error, - status, - kind: kind.or(fallback_kind), - content, - locations, + tool_name: None, + input: None, + output: None, + error: None, + status: None, + kind: None, + content: None, + locations: None, }); } acp::SessionUpdate::CurrentModeUpdate(update) => { @@ -342,6 +473,29 @@ impl acp::Client for AthasAcpClient { current_mode_id: update.current_mode_id.to_string(), }); } + acp::SessionUpdate::AvailableCommandsUpdate(commands_update) => { + self.emit_event(AcpEvent::SlashCommandsUpdate { + session_id, + commands: commands_update + .available_commands + .iter() + .map(|c| super::types::SlashCommand { + name: c.name.clone(), + description: c.description.clone(), + input: c.input.as_ref().and_then(|input| { + // Extract hint from unstructured command input + if let acp::AvailableCommandInput::Unstructured(unstructured) = input { + Some(super::types::SlashCommandInput { + hint: unstructured.hint.clone(), + }) + } else { + None + } + }), + }) + .collect(), + }); + } _ => { // Handle other session updates as needed } @@ -356,7 +510,25 @@ impl acp::Client for AthasAcpClient { let path_str = args.path.to_string_lossy(); let path = self.resolve_path(&path_str); match tokio::fs::read_to_string(&path).await { - Ok(content) => Ok(acp::ReadTextFileResponse::new(content)), + Ok(content) => { + // Handle line and limit parameters for partial file reading + let result = if args.line.is_some() || args.limit.is_some() { + let lines: Vec<&str> = content.lines().collect(); + let start_line = args.line.unwrap_or(1).saturating_sub(1) as usize; + let limit = args.limit.map(|l| l as usize).unwrap_or(lines.len()); + + lines + .iter() + .skip(start_line) + .take(limit) + .copied() + .collect::>() + .join("\n") + } else { + content + }; + Ok(acp::ReadTextFileResponse::new(result)) + } Err(e) => Err(acp::Error::new( -32603, format!("Failed to read file: {}", e), @@ -393,45 +565,267 @@ impl acp::Client for AthasAcpClient { async fn create_terminal( &self, - _args: acp::CreateTerminalRequest, + args: acp::CreateTerminalRequest, ) -> acp::Result { - // TODO: Integrate with existing TerminalManager - Err(acp::Error::method_not_found()) + let working_dir = args + .cwd + .map(|p| p.to_string_lossy().to_string()) + .or_else(|| self.workspace_path.clone()); + + let env_map: Option> = if args.env.is_empty() { + None + } else { + Some(args.env.into_iter().map(|e| (e.name, e.value)).collect()) + }; + + // Build the full command with arguments + let full_command = if args.args.is_empty() { + args.command.clone() + } else { + // Quote arguments that contain spaces or special characters + let quoted_args: Vec = args + .args + .iter() + .map(|arg| { + if arg.contains(' ') || arg.contains('"') || arg.contains('\'') { + format!("'{}'", arg.replace('\'', "'\\''")) + } else { + arg.clone() + } + }) + .collect(); + format!("{} {}", args.command, quoted_args.join(" ")) + }; + + let config = TerminalConfig { + working_directory: working_dir, + shell: None, + environment: env_map, + rows: 24, + cols: 80, + }; + + match self + .terminal_manager + .create_terminal(config, self.app_handle.clone()) + { + Ok(athas_terminal_id) => { + let terminal_id = athas_terminal_id.clone(); + let output_limit = args.output_byte_limit.map(|l| l as u32); + let state = AcpTerminalState::new(athas_terminal_id.clone(), output_limit); + + // Execute the command in the terminal + if let Err(e) = self + .terminal_manager + .write_to_terminal(&athas_terminal_id, &format!("{}\n", full_command)) + { + log::warn!("Failed to write command to terminal: {}", e); + } + { + let mut states = self.terminal_states.lock().unwrap(); + states.insert(terminal_id.clone(), state); + } + + // Set up output listener + let output_event = format!("pty-output-{}", athas_terminal_id); + let states_clone = self.terminal_states.clone(); + let terminal_id_clone = terminal_id.clone(); + self.app_handle.listen(output_event, move |event| { + let payload = event.payload(); + if let Ok(parsed) = serde_json::from_str::(payload) + && let Some(data) = parsed.get("data").and_then(|d| d.as_str()) + && let Ok(mut states) = states_clone.lock() + && let Some(state) = states.get_mut(&terminal_id_clone) + { + state.append_output(data); + } + }); + + // Set up close listener + let close_event = format!("pty-closed-{}", athas_terminal_id); + let states_clone = self.terminal_states.clone(); + let terminal_id_clone = terminal_id.clone(); + self.app_handle.listen(close_event, move |_| { + if let Ok(mut states) = states_clone.lock() + && let Some(state) = states.get_mut(&terminal_id_clone) + { + state.set_exit_status(Some(0), None); + } + }); + + log::info!("ACP terminal created: {}", terminal_id); + Ok(acp::CreateTerminalResponse::new(terminal_id)) + } + Err(e) => { + log::error!("Failed to create ACP terminal: {}", e); + Err(acp::Error::new( + -32603, + format!("Failed to create terminal: {}", e), + )) + } + } } async fn terminal_output( &self, - _args: acp::TerminalOutputRequest, + args: acp::TerminalOutputRequest, ) -> acp::Result { - Err(acp::Error::method_not_found()) + let terminal_id = args.terminal_id.to_string(); + let mut states = self + .terminal_states + .lock() + .map_err(|_| acp::Error::new(-32603, "Lock poisoned".to_string()))?; + + let state = states + .get_mut(&terminal_id) + .ok_or_else(|| acp::Error::new(-32603, "Terminal not found".to_string()))?; + + let output = std::mem::take(&mut state.output_buffer); + let truncated = state.truncated; + state.truncated = false; + + Ok(acp::TerminalOutputResponse::new(output, truncated)) } async fn release_terminal( &self, - _args: acp::ReleaseTerminalRequest, + args: acp::ReleaseTerminalRequest, ) -> acp::Result { - Err(acp::Error::method_not_found()) + let terminal_id = args.terminal_id.to_string(); + let removed_state = { + let mut states = self + .terminal_states + .lock() + .map_err(|_| acp::Error::new(-32603, "Lock poisoned".to_string()))?; + states.remove(&terminal_id) + }; + + if let Some(state) = removed_state + && let Err(e) = self + .terminal_manager + .close_terminal(&state.athas_terminal_id) + { + log::warn!("Failed to close terminal {}: {}", terminal_id, e); + } + + Ok(acp::ReleaseTerminalResponse::new()) } async fn wait_for_terminal_exit( &self, - _args: acp::WaitForTerminalExitRequest, + args: acp::WaitForTerminalExitRequest, ) -> acp::Result { - Err(acp::Error::method_not_found()) + let terminal_id = args.terminal_id.to_string(); + + let receiver = { + let mut states = self + .terminal_states + .lock() + .map_err(|_| acp::Error::new(-32603, "Lock poisoned".to_string()))?; + + let state = states + .get_mut(&terminal_id) + .ok_or_else(|| acp::Error::new(-32603, "Terminal not found".to_string()))?; + + if let Some(status) = state.exit_status.clone() { + return Ok(acp::WaitForTerminalExitResponse::new(status)); + } + + let (tx, rx) = oneshot::channel(); + state.exit_waiters.push(tx); + rx + }; + + match receiver.await { + Ok(status) => Ok(acp::WaitForTerminalExitResponse::new(status)), + Err(_) => { + let exit_status = acp::TerminalExitStatus::new().exit_code(1); + Ok(acp::WaitForTerminalExitResponse::new(exit_status)) + } + } } async fn kill_terminal_command( &self, - _args: acp::KillTerminalCommandRequest, + args: acp::KillTerminalCommandRequest, ) -> acp::Result { - Err(acp::Error::method_not_found()) + let terminal_id = args.terminal_id.to_string(); + let athas_id = { + let states = self + .terminal_states + .lock() + .map_err(|_| acp::Error::new(-32603, "Lock poisoned".to_string()))?; + states + .get(&terminal_id) + .map(|s| s.athas_terminal_id.clone()) + }; + + if let Some(athas_terminal_id) = athas_id + && let Err(e) = self.terminal_manager.close_terminal(&athas_terminal_id) + { + log::warn!("Failed to kill terminal {}: {}", terminal_id, e); + } + + Ok(acp::KillTerminalCommandResponse::new()) } - async fn ext_method(&self, _args: acp::ExtRequest) -> acp::Result { - Err(acp::Error::method_not_found()) + async fn ext_method(&self, args: acp::ExtRequest) -> acp::Result { + let session_id = self + .current_session_id + .lock() + .await + .clone() + .unwrap_or_default(); + + // Parse params from RawValue to Value for easier access + let params: serde_json::Value = + serde_json::from_str(args.params.get()).unwrap_or(serde_json::Value::Null); + + match &*args.method { + "athas.openWebViewer" => { + let url = params + .get("url") + .and_then(|v| v.as_str()) + .unwrap_or("about:blank") + .to_string(); + + self.emit_event(AcpEvent::UiAction { + session_id, + action: UiAction::OpenWebViewer { url }, + }); + + let response = serde_json::json!({ "success": true }); + Ok(acp::ExtResponse::new( + serde_json::value::to_raw_value(&response).unwrap().into(), + )) + } + "athas.openTerminal" => { + let command = params + .get("command") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + + self.emit_event(AcpEvent::UiAction { + session_id, + action: UiAction::OpenTerminal { command }, + }); + + let response = serde_json::json!({ "success": true }); + Ok(acp::ExtResponse::new( + serde_json::value::to_raw_value(&response).unwrap().into(), + )) + } + _ => Err(acp::Error::method_not_found()), + } } - async fn ext_notification(&self, _args: acp::ExtNotification) -> acp::Result<()> { + async fn ext_notification(&self, args: acp::ExtNotification) -> acp::Result<()> { + // Log extension notifications for debugging + log::debug!( + "ACP extension notification: method={}, params={}", + args.method, + args.params.get() + ); Ok(()) } } diff --git a/src-tauri/src/features/ai/acp/config.rs b/src-tauri/src/features/ai/acp/config.rs index f605d9ac..8965c4c9 100644 --- a/src-tauri/src/features/ai/acp/config.rs +++ b/src-tauri/src/features/ai/acp/config.rs @@ -3,11 +3,17 @@ use std::{ collections::HashMap, env, fs, path::{Path, PathBuf}, + time::Instant, }; +/// Cache duration for binary detection (60 seconds) +const DETECTION_CACHE_SECONDS: u64 = 60; + /// Registry of known ACP-compatible agents +#[derive(Clone)] pub struct AgentRegistry { agents: HashMap, + last_detection: Option, } impl AgentRegistry { @@ -65,14 +71,17 @@ impl AgentRegistry { ); // Kairo Code - native ACP adapter - // Install: bun add -g --no-cache @colineapp/kairo-code-acp + // Install: pnpm add -g @colineapp/kairo-code-acp agents.insert( "kairo-code".to_string(), AgentConfig::new("kairo-code", "Kairo Code", "kairo-code-acp") .with_description("Coline Kairo Code (ACP adapter)"), ); - Self { agents } + Self { + agents, + last_detection: None, + } } pub fn get(&self, id: &str) -> Option<&AgentConfig> { @@ -84,6 +93,19 @@ impl AgentRegistry { } pub fn detect_installed(&mut self) { + // Check if we should skip detection due to caching + if let Some(last) = self.last_detection { + let elapsed = last.elapsed().as_secs(); + if elapsed < DETECTION_CACHE_SECONDS { + log::debug!( + "Skipping binary detection, cached for {}s more", + DETECTION_CACHE_SECONDS - elapsed + ); + return; + } + } + + log::debug!("Running binary detection for ACP agents"); for config in self.agents.values_mut() { if let Some(path) = find_binary(&config.binary_name) { config.installed = true; @@ -93,6 +115,8 @@ impl AgentRegistry { config.binary_path = None; } } + + self.last_detection = Some(Instant::now()); } } @@ -125,6 +149,8 @@ fn find_binary(binary_name: &str) -> Option { candidates.push(home.join(".pnpm")); candidates.push(home.join("Library/pnpm")); candidates.push(home.join("Library/pnpm/bin")); + candidates.push(home.join(".cargo/bin")); + candidates.push(home.join("go/bin")); candidates.push(home.join(".asdf/shims")); candidates.push(home.join(".local/share/mise/shims")); @@ -193,6 +219,15 @@ fn find_binary(binary_name: &str) -> Option { } } } + if let Some(dir) = env::var_os("GOPATH") { + candidates.push(PathBuf::from(dir).join("bin")); + } + if let Some(dir) = env::var_os("GOBIN") { + candidates.push(PathBuf::from(dir)); + } + if let Some(dir) = env::var_os("CARGO_HOME") { + candidates.push(PathBuf::from(dir).join("bin")); + } for dir in candidates { if let Some(found) = check_dir_for_binary(&dir, binary_name) { diff --git a/src-tauri/src/features/ai/acp/types.rs b/src-tauri/src/features/ai/acp/types.rs index 127b2a0f..f6a8f20b 100644 --- a/src-tauri/src/features/ai/acp/types.rs +++ b/src-tauri/src/features/ai/acp/types.rs @@ -143,6 +143,18 @@ pub enum AcpContentBlock { }, } +/// UI action types that agents can request +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "action", rename_all = "snake_case")] +pub enum UiAction { + /// Open a URL in the web viewer + #[serde(rename_all = "camelCase")] + OpenWebViewer { url: String }, + /// Open a terminal with an optional command + #[serde(rename_all = "camelCase")] + OpenTerminal { command: Option }, +} + /// Events emitted to the frontend via Tauri #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] @@ -237,4 +249,10 @@ pub enum AcpEvent { session_id: String, stop_reason: StopReason, }, + /// UI action request from agent + #[serde(rename_all = "camelCase")] + UiAction { + session_id: String, + action: UiAction, + }, } diff --git a/src-tauri/src/features/mod.rs b/src-tauri/src/features/mod.rs index c0ba82e4..84b6a555 100644 --- a/src-tauri/src/features/mod.rs +++ b/src-tauri/src/features/mod.rs @@ -5,4 +5,3 @@ pub mod tools; pub use ai::*; pub use project::*; -pub use tools::*; diff --git a/src-tauri/src/features/tools/installer.rs b/src-tauri/src/features/tools/installer.rs index a29c416a..0353f9ee 100644 --- a/src-tauri/src/features/tools/installer.rs +++ b/src-tauri/src/features/tools/installer.rs @@ -1,12 +1,176 @@ use super::types::{ToolConfig, ToolError, ToolRuntime}; use crate::features::runtime::{RuntimeManager, RuntimeType}; -use std::{path::PathBuf, process::Command}; +use flate2::read::GzDecoder; +use serde_json::Value; +use std::{ + fs, + io::Cursor, + path::{Path, PathBuf}, + process::Command, +}; use tauri::Manager; +use walkdir::WalkDir; +use zip::ZipArchive; /// Handles installation of language tools pub struct ToolInstaller; impl ToolInstaller { + fn node_bin_name(name: &str) -> String { + if cfg!(windows) { + format!("{}.cmd", name) + } else { + name.to_string() + } + } + + fn bin_file_name(name: &str) -> String { + if cfg!(windows) { + format!("{}.exe", name) + } else { + name.to_string() + } + } + + fn resolve_node_package_entrypoint( + package_dir: &Path, + package: &str, + command_name: &str, + ) -> Option { + let package_root = package_dir.join("node_modules").join(package); + let package_json = package_root.join("package.json"); + let package_json_content = fs::read_to_string(package_json).ok()?; + let package_json_value: Value = serde_json::from_str(&package_json_content).ok()?; + let bin_field = package_json_value.get("bin")?; + + if let Some(single_bin) = bin_field.as_str() { + return Some(package_root.join(single_bin)); + } + + let bins = bin_field.as_object()?; + if let Some(command_bin) = bins.get(command_name).and_then(|value| value.as_str()) { + return Some(package_root.join(command_bin)); + } + + bins + .values() + .next() + .and_then(|value| value.as_str()) + .map(|first_bin| package_root.join(first_bin)) + } + + fn ensure_executable(path: &Path) -> Result<(), ToolError> { + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + fs::set_permissions(path, fs::Permissions::from_mode(0o755))?; + } + Ok(()) + } + + fn extract_archive(bytes: &[u8], url: &str, target_dir: &Path) -> Result<(), ToolError> { + if url.ends_with(".tar.gz") || url.ends_with(".tgz") { + let decoder = GzDecoder::new(Cursor::new(bytes)); + let mut archive = tar::Archive::new(decoder); + let entries = archive.entries().map_err(|e| { + ToolError::InstallationFailed(format!("Failed to read tar.gz entries: {}", e)) + })?; + for entry in entries { + let mut entry = entry.map_err(|e| { + ToolError::InstallationFailed(format!("Failed to read tar.gz entry: {}", e)) + })?; + let unpacked = entry.unpack_in(target_dir).map_err(|e| { + ToolError::InstallationFailed(format!("Failed to unpack tar.gz entry: {}", e)) + })?; + if !unpacked { + return Err(ToolError::InstallationFailed( + "Rejected archive entry with invalid path".to_string(), + )); + } + } + return Ok(()); + } + + if url.ends_with(".zip") { + let mut archive = ZipArchive::new(Cursor::new(bytes)).map_err(|e| { + ToolError::InstallationFailed(format!("Failed to read zip archive: {}", e)) + })?; + + for index in 0..archive.len() { + let mut file = archive.by_index(index).map_err(|e| { + ToolError::InstallationFailed(format!("Failed to read zip entry: {}", e)) + })?; + + let Some(relative_path) = file.enclosed_name().map(|p| p.to_path_buf()) else { + continue; + }; + + let output_path = target_dir.join(relative_path); + + if file.name().ends_with('/') { + fs::create_dir_all(&output_path)?; + continue; + } + + if let Some(parent) = output_path.parent() { + fs::create_dir_all(parent)?; + } + + let mut output_file = fs::File::create(&output_path)?; + std::io::copy(&mut file, &mut output_file)?; + } + + return Ok(()); + } + + if url.ends_with(".gz") { + let mut decoder = GzDecoder::new(Cursor::new(bytes)); + let output_path = target_dir.join("downloaded-binary"); + let mut output_file = fs::File::create(output_path)?; + std::io::copy(&mut decoder, &mut output_file)?; + return Ok(()); + } + + fs::write(target_dir.join("downloaded-binary"), bytes)?; + Ok(()) + } + + fn pick_binary(staging_dir: &Path, command_name: &str) -> Result { + let expected_name = Self::bin_file_name(command_name); + let mut prefix_matches: Vec = Vec::new(); + let mut fallback_files: Vec = Vec::new(); + + for entry in WalkDir::new(staging_dir) + .into_iter() + .filter_map(|entry| entry.ok()) + .filter(|entry| entry.file_type().is_file()) + { + let path = entry.into_path(); + let file_name = path + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or_default(); + + if file_name == expected_name || (!cfg!(windows) && file_name == command_name) { + return Ok(path); + } + + if file_name.starts_with(command_name) { + prefix_matches.push(path.clone()); + } + + fallback_files.push(path); + } + + if let Some(path) = prefix_matches.into_iter().next() { + return Ok(path); + } + + fallback_files.into_iter().next().ok_or_else(|| { + ToolError::InstallationFailed("No binary found in downloaded archive".to_string()) + }) + } + /// Install a tool based on its configuration pub async fn install( app_handle: &tauri::AppHandle, @@ -18,35 +182,35 @@ impl ToolInstaller { .package .as_ref() .ok_or_else(|| ToolError::ConfigError("No package specified".to_string()))?; - Self::install_via_bun(app_handle, package).await + Self::install_via_bun(app_handle, package, &config.name).await } ToolRuntime::Node => { let package = config .package .as_ref() .ok_or_else(|| ToolError::ConfigError("No package specified".to_string()))?; - Self::install_via_npm(app_handle, package).await + Self::install_via_npm(app_handle, package, &config.name).await } ToolRuntime::Python => { let package = config .package .as_ref() .ok_or_else(|| ToolError::ConfigError("No package specified".to_string()))?; - Self::install_via_pip(app_handle, package).await + Self::install_via_pip(app_handle, package, &config.name).await } ToolRuntime::Go => { let package = config .package .as_ref() .ok_or_else(|| ToolError::ConfigError("No package specified".to_string()))?; - Self::install_via_go(app_handle, package).await + Self::install_via_go(app_handle, package, &config.name).await } ToolRuntime::Rust => { let package = config .package .as_ref() .ok_or_else(|| ToolError::ConfigError("No package specified".to_string()))?; - Self::install_via_cargo(app_handle, package).await + Self::install_via_cargo(app_handle, package, &config.name).await } ToolRuntime::Binary => { let url = config @@ -71,6 +235,7 @@ impl ToolInstaller { async fn install_via_bun( app_handle: &tauri::AppHandle, package: &str, + command_name: &str, ) -> Result { let bun_path = RuntimeManager::get_runtime(app_handle, RuntimeType::Bun) .await @@ -96,8 +261,11 @@ impl ToolInstaller { ))); } - // Return the node_modules/.bin path - let bin_path = package_dir.join("node_modules").join(".bin").join(package); + // Return the node_modules/.bin path for the configured command. + let bin_path = package_dir + .join("node_modules") + .join(".bin") + .join(Self::node_bin_name(command_name)); if bin_path.exists() { Ok(bin_path) } else { @@ -110,6 +278,7 @@ impl ToolInstaller { async fn install_via_npm( app_handle: &tauri::AppHandle, package: &str, + command_name: &str, ) -> Result { let node_path = RuntimeManager::get_runtime(app_handle, RuntimeType::Node) .await @@ -141,7 +310,10 @@ impl ToolInstaller { ))); } - let bin_path = package_dir.join("node_modules").join(".bin").join(package); + let bin_path = package_dir + .join("node_modules") + .join(".bin") + .join(Self::node_bin_name(command_name)); if bin_path.exists() { Ok(bin_path) } else { @@ -153,6 +325,7 @@ impl ToolInstaller { async fn install_via_pip( app_handle: &tauri::AppHandle, package: &str, + command_name: &str, ) -> Result { let python_path = RuntimeManager::get_runtime(app_handle, RuntimeType::Python) .await @@ -204,9 +377,11 @@ impl ToolInstaller { // Return binary path let bin_path = if cfg!(windows) { - venv_dir.join("Scripts").join(format!("{}.exe", package)) + venv_dir + .join("Scripts") + .join(Self::bin_file_name(command_name)) } else { - venv_dir.join("bin").join(package) + venv_dir.join("bin").join(command_name) }; Ok(bin_path) @@ -216,6 +391,7 @@ impl ToolInstaller { async fn install_via_go( app_handle: &tauri::AppHandle, package: &str, + command_name: &str, ) -> Result { let go_path = RuntimeManager::get_runtime(app_handle, RuntimeType::Go) .await @@ -241,13 +417,10 @@ impl ToolInstaller { ))); } - // Extract binary name from package path - let binary_name = package.split('/').last().unwrap_or(package); - let bin_path = if cfg!(windows) { - gopath.join("bin").join(format!("{}.exe", binary_name)) + gopath.join("bin").join(Self::bin_file_name(command_name)) } else { - gopath.join("bin").join(binary_name) + gopath.join("bin").join(command_name) }; Ok(bin_path) @@ -257,6 +430,7 @@ impl ToolInstaller { async fn install_via_cargo( app_handle: &tauri::AppHandle, package: &str, + command_name: &str, ) -> Result { let cargo_path = RuntimeManager::get_runtime(app_handle, RuntimeType::Rust) .await @@ -283,9 +457,11 @@ impl ToolInstaller { } let bin_path = if cfg!(windows) { - cargo_home.join("bin").join(format!("{}.exe", package)) + cargo_home + .join("bin") + .join(Self::bin_file_name(command_name)) } else { - cargo_home.join("bin").join(package) + cargo_home.join("bin").join(command_name) }; Ok(bin_path) @@ -301,11 +477,7 @@ impl ToolInstaller { let bin_dir = tools_dir.join("bin"); std::fs::create_dir_all(&bin_dir)?; - let bin_name = if cfg!(windows) { - format!("{}.exe", name) - } else { - name.to_string() - }; + let bin_name = Self::bin_file_name(name); let bin_path = bin_dir.join(&bin_name); log::info!("Downloading {} from {}", name, url); @@ -327,14 +499,18 @@ impl ToolInstaller { .await .map_err(|e| ToolError::DownloadFailed(e.to_string()))?; - std::fs::write(&bin_path, &bytes)?; + let staging_dir = tempfile::tempdir() + .map_err(|e| ToolError::InstallationFailed(format!("Failed to create temp dir: {}", e)))?; + Self::extract_archive(&bytes, url, staging_dir.path())?; - // Make executable on Unix - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - std::fs::set_permissions(&bin_path, std::fs::Permissions::from_mode(0o755))?; - } + let source_binary = Self::pick_binary(staging_dir.path(), name)?; + fs::copy(&source_binary, &bin_path).map_err(|e| { + ToolError::InstallationFailed(format!( + "Failed to copy binary from {:?} to {:?}: {}", + source_binary, bin_path, e + )) + })?; + Self::ensure_executable(&bin_path)?; Ok(bin_path) } @@ -366,7 +542,7 @@ impl ToolInstaller { .join(package) .join("node_modules") .join(".bin") - .join(package)) + .join(Self::node_bin_name(&config.name))) } ToolRuntime::Node => { let package = config @@ -378,57 +554,83 @@ impl ToolInstaller { .join(package) .join("node_modules") .join(".bin") - .join(package)) + .join(Self::node_bin_name(&config.name))) } ToolRuntime::Python => { + let bin_name = Self::bin_file_name(&config.name); let package = config .package .as_ref() .ok_or_else(|| ToolError::ConfigError("No package specified".to_string()))?; - let bin_name = if cfg!(windows) { - format!("{}.exe", package) - } else { - package.clone() - }; + let scripts_dir = if cfg!(windows) { "Scripts" } else { "bin" }; Ok(tools_dir .join("python") .join(package) - .join("bin") + .join(scripts_dir) .join(bin_name)) } ToolRuntime::Go => { + let bin_name = Self::bin_file_name(&config.name); + Ok(tools_dir.join("go").join("bin").join(bin_name)) + } + ToolRuntime::Rust => { + let bin_name = Self::bin_file_name(&config.name); + Ok(tools_dir.join("cargo").join("bin").join(bin_name)) + } + ToolRuntime::Binary => { + let bin_name = Self::bin_file_name(&config.name); + Ok(tools_dir.join("bin").join(bin_name)) + } + } + } + + /// Get the preferred launch path for LSP servers. + /// For Node/Bun tools, this returns the package bin entrypoint (e.g. .js/.mjs) + /// so the LSP client can run it with managed Node runtime. + pub fn get_lsp_launch_path( + app_handle: &tauri::AppHandle, + config: &ToolConfig, + ) -> Result { + let tools_dir = Self::get_tools_dir(app_handle)?; + + match config.runtime { + ToolRuntime::Bun => { let package = config .package .as_ref() .ok_or_else(|| ToolError::ConfigError("No package specified".to_string()))?; - let binary_name = package.split('/').last().unwrap_or(package); - let bin_name = if cfg!(windows) { - format!("{}.exe", binary_name) - } else { - binary_name.to_string() - }; - Ok(tools_dir.join("go").join("bin").join(bin_name)) + let package_dir = tools_dir.join("bun").join(package); + + if let Some(entrypoint) = + Self::resolve_node_package_entrypoint(&package_dir, package, &config.name) + { + return Ok(entrypoint); + } + + Ok(package_dir + .join("node_modules") + .join(".bin") + .join(Self::node_bin_name(&config.name))) } - ToolRuntime::Rust => { + ToolRuntime::Node => { let package = config .package .as_ref() .ok_or_else(|| ToolError::ConfigError("No package specified".to_string()))?; - let bin_name = if cfg!(windows) { - format!("{}.exe", package) - } else { - package.clone() - }; - Ok(tools_dir.join("cargo").join("bin").join(bin_name)) - } - ToolRuntime::Binary => { - let bin_name = if cfg!(windows) { - format!("{}.exe", config.name) - } else { - config.name.clone() - }; - Ok(tools_dir.join("bin").join(bin_name)) + let package_dir = tools_dir.join("npm").join(package); + + if let Some(entrypoint) = + Self::resolve_node_package_entrypoint(&package_dir, package, &config.name) + { + return Ok(entrypoint); + } + + Ok(package_dir + .join("node_modules") + .join(".bin") + .join(Self::node_bin_name(&config.name))) } + _ => Self::get_tool_path(app_handle, config), } } } diff --git a/src-tauri/src/features/tools/mod.rs b/src-tauri/src/features/tools/mod.rs index 988017bf..3779b374 100644 --- a/src-tauri/src/features/tools/mod.rs +++ b/src-tauri/src/features/tools/mod.rs @@ -4,4 +4,4 @@ mod types; pub use installer::ToolInstaller; pub use registry::ToolRegistry; -pub use types::{LanguageToolStatus, ToolConfig, ToolError, ToolRuntime, ToolStatus, ToolType}; +pub use types::{LanguageToolStatus, ToolStatus, ToolType}; diff --git a/src-tauri/src/features/tools/registry.rs b/src-tauri/src/features/tools/registry.rs index 3dab1611..7dc8e862 100644 --- a/src-tauri/src/features/tools/registry.rs +++ b/src-tauri/src/features/tools/registry.rs @@ -15,7 +15,10 @@ impl ToolRegistry { "rust" => Some(Self::rust_tools()), "go" => Some(Self::go_tools()), "php" => Some(Self::php_tools()), - "html" | "css" | "scss" | "less" => Some(Self::web_tools()), + "bash" => Some(Self::bash_tools()), + "lua" => Some(Self::lua_tools()), + "html" => Some(Self::html_tools()), + "css" | "scss" | "less" => Some(Self::css_tools()), "json" | "jsonc" => Some(Self::json_tools()), "yaml" | "yml" => Some(Self::yaml_tools()), "toml" => Some(Self::toml_tools()), @@ -200,13 +203,79 @@ impl ToolRegistry { tools } - fn web_tools() -> HashMap { + fn bash_tools() -> HashMap { let mut tools = HashMap::new(); tools.insert( ToolType::Lsp, ToolConfig { - name: "vscode-langservers-extracted".to_string(), + name: "bash-language-server".to_string(), + runtime: ToolRuntime::Bun, + package: Some("bash-language-server".to_string()), + download_url: None, + args: vec!["start".to_string()], + env: HashMap::new(), + }, + ); + + tools + } + + fn lua_tools() -> HashMap { + let mut tools = HashMap::new(); + + tools.insert( + ToolType::Lsp, + ToolConfig { + name: "lua-language-server".to_string(), + runtime: ToolRuntime::Binary, + package: None, + download_url: Some(Self::lua_language_server_url()), + args: vec![], + env: HashMap::new(), + }, + ); + + tools + } + + fn html_tools() -> HashMap { + let mut tools = HashMap::new(); + + tools.insert( + ToolType::Lsp, + ToolConfig { + name: "vscode-html-language-server".to_string(), + runtime: ToolRuntime::Bun, + package: Some("vscode-langservers-extracted".to_string()), + download_url: None, + args: vec!["--stdio".to_string()], + env: HashMap::new(), + }, + ); + + tools.insert( + ToolType::Formatter, + ToolConfig { + name: "prettier".to_string(), + runtime: ToolRuntime::Bun, + package: Some("prettier".to_string()), + download_url: None, + args: vec!["--stdin-filepath".to_string(), "${file}".to_string()], + env: HashMap::new(), + }, + ); + + tools + } + + fn css_tools() -> HashMap { + let mut tools = HashMap::new(); + + tools.insert( + ToolType::Lsp, + ToolConfig { + name: "vscode-css-language-server".to_string(), runtime: ToolRuntime::Bun, package: Some("vscode-langservers-extracted".to_string()), download_url: None, @@ -236,7 +305,7 @@ impl ToolRegistry { tools.insert( ToolType::Lsp, ToolConfig { - name: "vscode-langservers-extracted".to_string(), + name: "vscode-json-language-server".to_string(), runtime: ToolRuntime::Bun, package: Some("vscode-langservers-extracted".to_string()), download_url: None, @@ -358,4 +427,20 @@ impl ToolRegistry { arch, os, ext ) } + + fn lua_language_server_url() -> String { + let platform = match (std::env::consts::OS, std::env::consts::ARCH) { + ("macos", "aarch64") => "darwin-arm64", + ("macos", "x86_64") => "darwin-x64", + ("linux", "x86_64") => "linux-x64", + ("windows", "x86_64") => "win32-x64", + // Fallback to linux-x64 package when platform-specific builds are unavailable. + _ => "linux-x64", + }; + + format!( + "https://athas.dev/extensions/packages/lua/lua-{}.tar.gz", + platform + ) + } } diff --git a/src-tauri/src/lsp/client.rs b/src-tauri/src/lsp/client.rs index 313c75d4..a5d1bf56 100644 --- a/src-tauri/src/lsp/client.rs +++ b/src-tauri/src/lsp/client.rs @@ -81,7 +81,12 @@ impl LspClient { .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn() - .context("Failed to spawn LSP server")?; + .with_context(|| { + format!( + "Failed to spawn LSP server: command={:?}, args={:?}", + command_path, final_args + ) + })?; log::info!("Language server process started with PID: {:?}", child.id()); diff --git a/src-tauri/src/lsp/manager.rs b/src-tauri/src/lsp/manager.rs index b0c69cb6..0318c395 100644 --- a/src-tauri/src/lsp/manager.rs +++ b/src-tauri/src/lsp/manager.rs @@ -351,19 +351,6 @@ impl LspManager { } } - // Third pass: fallback - find any server in the workspace - // This handles the case where a file is being opened for the first time - for ((workspace_path, server_name), instance) in clients.iter() { - if path.starts_with(workspace_path) { - log::warn!( - "get_client_for_file: fallback to server '{}' for {} (no extension match)", - server_name, - file_path - ); - return Some(instance.client.clone()); - } - } - log::warn!("get_client_for_file: no client found for {}", file_path); None } @@ -430,9 +417,9 @@ impl LspManager { line: u32, character: u32, ) -> Result> { - let client = self - .get_client_for_file(file_path) - .context("No LSP client for this file")?; + let Some(client) = self.get_client_for_file(file_path) else { + return Ok(None); + }; let text_document = TextDocumentIdentifier { uri: Url::from_file_path(file_path).map_err(|_| anyhow::anyhow!("Invalid file path"))?, @@ -446,7 +433,20 @@ impl LspManager { work_done_progress_params: Default::default(), }; - client.text_document_hover(params).await + match client.text_document_hover(params).await { + Ok(value) => Ok(value), + Err(error) => { + let message = error.to_string(); + if message.contains("-32601") + || message.contains("Method not found") + || message.contains("Unhandled method textDocument/hover") + { + log::debug!("Hover method is not supported by this language server"); + return Ok(None); + } + Err(error) + } + } } pub async fn get_definition( @@ -455,9 +455,9 @@ impl LspManager { line: u32, character: u32, ) -> Result> { - let client = self - .get_client_for_file(file_path) - .context("No LSP client for this file")?; + let Some(client) = self.get_client_for_file(file_path) else { + return Ok(None); + }; let text_document = TextDocumentIdentifier { uri: Url::from_file_path(file_path).map_err(|_| anyhow::anyhow!("Invalid file path"))?, @@ -472,10 +472,28 @@ impl LspManager { partial_result_params: Default::default(), }; - client.text_document_definition(params).await + match client.text_document_definition(params).await { + Ok(value) => Ok(value), + Err(error) => { + let message = error.to_string(); + if message.contains("-32601") + || message.contains("Method not found") + || message.contains("Unhandled method textDocument/definition") + { + log::debug!("Definition method is not supported by this language server"); + return Ok(None); + } + Err(error) + } + } } - pub fn notify_document_open(&self, file_path: &str, content: String) -> Result<()> { + pub fn notify_document_open( + &self, + file_path: &str, + content: String, + language_id: Option, + ) -> Result<()> { let path = PathBuf::from(file_path); let _extension = path.extension().and_then(|ext| ext.to_str()).unwrap_or(""); @@ -487,7 +505,7 @@ impl LspManager { text_document: TextDocumentItem { uri: Url::from_file_path(file_path) .map_err(|_| anyhow::anyhow!("Invalid file path"))?, - language_id: self.get_language_id_for_file(file_path), + language_id: language_id.unwrap_or_else(|| self.get_language_id_for_file(file_path)), version: 1, text: content, }, @@ -581,17 +599,57 @@ impl LspManager { fn get_language_id_for_file(&self, file_path: &str) -> String { let path = PathBuf::from(file_path); + let file_name = path + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or_default(); let extension = path.extension().and_then(|ext| ext.to_str()).unwrap_or(""); - match extension { + let mut language_id = match extension { + "sh" | "bash" | "zsh" => "bash", + "c" => "c", + "h" => "c", + "cpp" | "cc" | "cxx" | "hpp" | "hh" | "hxx" => "cpp", + "cs" => "csharp", + "css" => "css", + "dart" => "dart", + "el" => "elisp", + "ex" | "exs" => "elixir", + "elm" => "elm", + "go" => "go", + "html" | "htm" | "xhtml" => "html", + "java" => "java", "ts" => "typescript", "tsx" => "typescriptreact", "js" | "mjs" | "cjs" => "javascript", "jsx" => "javascriptreact", + "jsonc" => "jsonc", "json" => "json", + "kt" | "kts" => "kotlin", + "lua" => "lua", + "ml" | "mli" => "ocaml", + "php" | "phtml" | "php3" | "php4" | "php5" => "php", + "py" | "pyw" | "pyi" => "python", + "rb" | "rake" | "gemspec" => "ruby", + "rs" => "rust", + "scala" | "sc" => "scala", + "swift" => "swift", + "toml" => "toml", + "vue" => "vue", + "yaml" | "yml" => "yaml", + "zig" => "zig", _ => "plaintext", } - .to_string() + .to_string(); + + if language_id == "plaintext" { + language_id = match file_name { + ".bashrc" | ".zshrc" | ".bash_profile" | ".profile" => "bash".to_string(), + _ => language_id, + }; + } + + language_id } } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 8fe39384..ad71d619 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -1,6 +1,9 @@ // Prevents additional console window on Windows in release #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] +#[global_allocator] +static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; + use commands::*; use features::{AcpAgentBridge, ClaudeCodeBridge, FileWatcher}; use log::{debug, info}; @@ -24,9 +27,50 @@ mod features; mod logger; mod lsp; mod menu; +mod secure_storage; mod ssh; mod terminal; +#[cfg(target_os = "macos")] +#[allow(unexpected_cfgs)] +fn disable_macos_autofill_heuristics() { + use objc::{ + class, msg_send, + runtime::{NO, Object}, + sel, sel_impl, + }; + use std::ffi::CString; + + // Disables macOS AutoFill heuristics in the app webview process. + // This is known to reduce extra AutoFill subprocess activity. + unsafe { + let key_cstr = match CString::new("NSAutoFillHeuristicControllerEnabled") { + Ok(value) => value, + Err(_) => return, + }; + + let key: *mut Object = msg_send![class!(NSString), stringWithUTF8String: key_cstr.as_ptr()]; + if key.is_null() { + return; + } + + let user_defaults: *mut Object = msg_send![class!(NSUserDefaults), standardUserDefaults]; + if user_defaults.is_null() { + return; + } + + let existing_value: *mut Object = msg_send![user_defaults, objectForKey: key]; + if existing_value.is_null() { + let false_value: *mut Object = msg_send![class!(NSNumber), numberWithBool: NO]; + if false_value.is_null() { + return; + } + + let _: () = msg_send![user_defaults, setObject: false_value forKey: key]; + } + } +} + fn main() { #[cfg(target_os = "linux")] if std::env::var("WEBKIT_DISABLE_DMABUF_RENDERER").is_err() { @@ -36,6 +80,9 @@ fn main() { } } + #[cfg(target_os = "macos")] + disable_macos_autofill_heuristics(); + tauri::Builder::default() .plugin(tauri_plugin_store::Builder::default().build()) .plugin(tauri_plugin_clipboard_manager::init()) @@ -73,12 +120,19 @@ fn main() { // Set up the file watcher app.manage(Arc::new(FileWatcher::new(app.handle().clone()))); + // Set up terminal manager (shared between ACP and direct terminal usage) + let terminal_manager = Arc::new(TerminalManager::new()); + app.manage(terminal_manager.clone()); + // Set up Claude bridge (legacy, kept for rollback) let claude_bridge = Arc::new(Mutex::new(ClaudeCodeBridge::new(app.handle().clone()))); app.manage(claude_bridge.clone()); // Set up ACP agent bridge (new implementation) - let acp_bridge = Arc::new(Mutex::new(AcpAgentBridge::new(app.handle().clone()))); + let acp_bridge = Arc::new(Mutex::new(AcpAgentBridge::new( + app.handle().clone(), + terminal_manager, + ))); app.manage(acp_bridge); // Set up LSP manager @@ -272,7 +326,6 @@ fn main() { Ok(()) }) - .manage(Arc::new(TerminalManager::new())) .invoke_handler(tauri::generate_handler![ // File system commands open_file_external, @@ -286,6 +339,7 @@ fn main() { clipboard_paste, // Git commands git_status, + git_discover_repo, git_add, git_reset, git_commit, diff --git a/src-tauri/src/menu.rs b/src-tauri/src/menu.rs index 0b51676e..840da57a 100644 --- a/src-tauri/src/menu.rs +++ b/src-tauri/src/menu.rs @@ -200,7 +200,7 @@ pub fn create_menu_with_themes( "toggle_terminal", "Toggle Terminal", true, - Some("CmdOrCtrl+`"), + Some("CmdOrCtrl+J"), )?) .item(&MenuItem::with_id( app, diff --git a/src-tauri/src/secure_storage.rs b/src-tauri/src/secure_storage.rs new file mode 100644 index 00000000..5d05b15f --- /dev/null +++ b/src-tauri/src/secure_storage.rs @@ -0,0 +1,119 @@ +use tauri::AppHandle; +use tauri_plugin_store::StoreExt; + +const SECURE_STORE_FILE: &str = "secure.json"; +const KEYCHAIN_SERVICE: &str = "com.code.athas"; + +fn keyring_entry(key: &str) -> Result { + keyring::Entry::new(KEYCHAIN_SERVICE, key) + .map_err(|e| format!("Failed to initialize keychain entry: {e}")) +} + +fn store_set(app: &AppHandle, key: &str, value: &str) -> Result<(), String> { + let store = app + .store(SECURE_STORE_FILE) + .map_err(|e| format!("Failed to access secure store: {e}"))?; + + store.set( + key.to_string(), + serde_json::Value::String(value.to_string()), + ); + + store + .save() + .map_err(|e| format!("Failed to save secure store: {e}"))?; + + Ok(()) +} + +fn store_get(app: &AppHandle, key: &str) -> Result, String> { + let store = app + .store(SECURE_STORE_FILE) + .map_err(|e| format!("Failed to access secure store: {e}"))?; + + Ok(store + .get(key) + .and_then(|value| value.as_str().map(|s| s.to_string()))) +} + +fn store_delete(app: &AppHandle, key: &str) -> Result<(), String> { + let store = app + .store(SECURE_STORE_FILE) + .map_err(|e| format!("Failed to access secure store: {e}"))?; + + let _removed = store.delete(key); + store + .save() + .map_err(|e| format!("Failed to save secure store: {e}"))?; + + Ok(()) +} + +pub fn store_secret(app: &AppHandle, key: &str, value: &str) -> Result<(), String> { + match keyring_entry(key) { + Ok(entry) => match entry.set_password(value) { + Ok(()) => { + let _ = store_delete(app, key); + return Ok(()); + } + Err(error) => { + log::warn!( + "Keychain unavailable for key '{}', falling back to secure.json: {}", + key, + error + ); + } + }, + Err(error) => { + log::warn!( + "Keychain entry initialization failed for key '{}', falling back to secure.json: {}", + key, + error + ); + } + } + + store_set(app, key, value) +} + +pub fn get_secret(app: &AppHandle, key: &str) -> Result, String> { + match keyring_entry(key) { + Ok(entry) => match entry.get_password() { + Ok(value) => return Ok(Some(value)), + Err(keyring::Error::NoEntry) => {} + Err(error) => { + log::warn!( + "Failed to read key '{}' from keychain, falling back to secure.json: {}", + key, + error + ); + } + }, + Err(error) => { + log::warn!( + "Keychain entry initialization failed for key '{}', falling back to secure.json: {}", + key, + error + ); + } + } + + store_get(app, key) +} + +pub fn remove_secret(app: &AppHandle, key: &str) -> Result<(), String> { + if let Ok(entry) = keyring_entry(key) { + match entry.delete_credential() { + Ok(()) | Err(keyring::Error::NoEntry) => {} + Err(error) => { + log::warn!( + "Failed to remove key '{}' from keychain, continuing with secure.json cleanup: {}", + key, + error + ); + } + } + } + + store_delete(app, key) +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index d1d7ba62..c7206acc 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Athas", - "version": "0.3.2", + "version": "0.4.0", "identifier": "com.code.athas", "build": { "beforeDevCommand": "bun vite", @@ -33,7 +33,7 @@ } ], "security": { - "csp": "default-src 'self' ipc: http://ipc.localhost; connect-src 'self' https://athas.dev https://*.athas.dev https://api.anthropic.com https://api.openai.com https://generativelanguage.googleapis.com https://*.githubusercontent.com; img-src 'self' asset: http://asset.localhost data: blob:; font-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' 'unsafe-eval'", + "csp": "default-src 'self' ipc: http://ipc.localhost; connect-src 'self' http://localhost:3000 http://127.0.0.1:3000 https://athas.dev https://*.athas.dev https://api.anthropic.com https://api.openai.com https://generativelanguage.googleapis.com https://*.githubusercontent.com; img-src 'self' asset: http://asset.localhost data: blob:; font-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src 'self'", "capabilities": [ "main-capability" ], diff --git a/src/App.tsx b/src/App.tsx index 012fecc4..b7f830c5 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,13 +1,15 @@ import { enableMapSet } from "immer"; -import { useEffect } from "react"; +import { lazy, Suspense, useEffect } from "react"; import { useLspInitialization } from "@/features/editor/hooks/use-lsp-initialization"; import { useKeymapContext } from "@/features/keymaps/hooks/use-keymap-context"; import { useKeymaps } from "@/features/keymaps/hooks/use-keymaps"; import { useRemoteConnection } from "@/features/remote/hooks/use-remote-connection"; import { useRemoteWindowClose } from "@/features/remote/hooks/use-remote-window-close"; import { FontStyleInjector } from "@/features/settings/components/font-style-injector"; -import UpdateDialog from "@/features/settings/components/update-dialog"; import { useAutoUpdate } from "@/features/settings/hooks/use-auto-update"; + +const UpdateDialog = lazy(() => import("@/features/settings/components/update-dialog")); + import { useContextMenuPrevention } from "@/features/window/hooks/use-context-menu-prevention"; import { useFontLoading } from "@/features/window/hooks/use-font-loading"; import { usePlatformSetup } from "@/features/window/hooks/use-platform-setup"; @@ -122,17 +124,18 @@ function App() { - {/* Update Dialog */} {showUpdateDialog && updateInfo && ( - + + + )}
diff --git a/src/extensions/core/providers/icon-theme-provider.ts b/src/extensions/core/providers/icon-theme-provider.ts index ddf1573f..bd02fbb1 100644 --- a/src/extensions/core/providers/icon-theme-provider.ts +++ b/src/extensions/core/providers/icon-theme-provider.ts @@ -24,6 +24,14 @@ class IconThemeProvider { private iconCache = new Map(); // Cache loaded SVG content private changeListeners = new Set<() => void>(); + private isSafeRelativeIconPath(iconPath: string): boolean { + if (!iconPath) return false; + if (iconPath.includes("..")) return false; + if (iconPath.startsWith("/") || iconPath.startsWith("\\")) return false; + if (iconPath.includes("://")) return false; + return true; + } + /** * Set the active icon theme */ @@ -132,6 +140,11 @@ class IconThemeProvider { return null; } + if (!this.isSafeRelativeIconPath(iconPath)) { + logger.warn("IconThemeProvider", `Rejected unsafe icon path: ${iconPath}`); + return null; + } + try { // Resolve path relative to the icon theme extension // For bundled themes, the path is relative to the extension directory diff --git a/src/extensions/core/providers/theme-provider.ts b/src/extensions/core/providers/theme-provider.ts index e876be2b..234e90ae 100644 --- a/src/extensions/core/providers/theme-provider.ts +++ b/src/extensions/core/providers/theme-provider.ts @@ -97,7 +97,7 @@ export const themeProvider = new ThemeProvider(); /** * Apply default theme on app startup */ -export function initializeThemeProvider(defaultThemeId = "one-dark"): void { +export function initializeThemeProvider(defaultThemeId = "vitesse-dark"): void { // Wait for registry to be initialized extensionRegistry.ensureInitialized().then(() => { // Load persisted theme preference diff --git a/src/extensions/core/store.ts b/src/extensions/core/store.ts index 319ef5db..3d20b452 100644 --- a/src/extensions/core/store.ts +++ b/src/extensions/core/store.ts @@ -165,7 +165,7 @@ const useExtensionStoreBase = create()( // Load persisted preferences const storedPrefs = localStorage.getItem("extension-preferences"); let prefs: PersistedPreferences = { - activeThemeVariantId: "one-dark", // Default theme + activeThemeVariantId: "vitesse-dark", activeIconThemeId: "material", // Default icon theme }; diff --git a/src/extensions/languages/full-extensions.ts b/src/extensions/languages/full-extensions.ts index ffd78c7a..0109b230 100644 --- a/src/extensions/languages/full-extensions.ts +++ b/src/extensions/languages/full-extensions.ts @@ -75,28 +75,28 @@ export const fullExtensions: ExtensionManifest[] = [ }, ], installation: { - downloadUrl: `${CDN_BASE_URL}/packages/php/php-darwin-arm64.tar.gz`, + downloadUrl: `${CDN_BASE_URL}/php/php-darwin-arm64.tar.gz`, size: 52681335, checksum: "5c21da47f7c17cfa798fa2cfd0df905992824f520e8d9930640fcfa5e44ece4d", minEditorVersion: "0.2.0", platformArch: { "darwin-arm64": { - downloadUrl: `${CDN_BASE_URL}/packages/php/php-darwin-arm64.tar.gz`, + downloadUrl: `${CDN_BASE_URL}/php/php-darwin-arm64.tar.gz`, size: 52681335, checksum: "5c21da47f7c17cfa798fa2cfd0df905992824f520e8d9930640fcfa5e44ece4d", }, "darwin-x64": { - downloadUrl: `${CDN_BASE_URL}/packages/php/php-darwin-x64.tar.gz`, + downloadUrl: `${CDN_BASE_URL}/php/php-darwin-x64.tar.gz`, size: 56850520, checksum: "6fa06325af8518b346235f7c86d887a88d04c970398657ac8c8c21482fcb180c", }, "linux-x64": { - downloadUrl: `${CDN_BASE_URL}/packages/php/php-linux-x64.tar.gz`, + downloadUrl: `${CDN_BASE_URL}/php/php-linux-x64.tar.gz`, size: 55510926, checksum: "a29aa4bbb04f623bc22826a38d86ccb9590d1f9bf3ad7ddbc05f79522d8f835a", }, "win32-x64": { - downloadUrl: `${CDN_BASE_URL}/packages/php/php-win32-x64.tar.gz`, + downloadUrl: `${CDN_BASE_URL}/php/php-win32-x64.tar.gz`, size: 52036166, checksum: "40f2d64fb15330bb950fbc59b44c74dcc74368abafcd8ff502e18b956a478cc5", }, diff --git a/src/extensions/languages/language-packager.ts b/src/extensions/languages/language-packager.ts index 77e0c430..f364cdf3 100644 --- a/src/extensions/languages/language-packager.ts +++ b/src/extensions/languages/language-packager.ts @@ -1,125 +1,282 @@ /** * Language Extension Packager - * Converts language manifest files to ExtensionManifest format for the extension store + * Fetches extension manifests from the CDN and converts them to internal ExtensionManifest format. */ -import type { ExtensionManifest } from "../types/extension-manifest"; +import type { + ExtensionCategory, + ExtensionManifest, + FormatterConfiguration, + LinterConfiguration, + LspConfiguration, + PlatformExecutable, +} from "../types/extension-manifest"; -// CDN base URL for downloading WASM parsers and highlight queries -// Can be configured via environment variable const CDN_BASE_URL = import.meta.env.VITE_PARSER_CDN_URL || "https://athas.dev/extensions"; +const MANIFESTS_URL = `${CDN_BASE_URL}/manifests.json`; -// Old manifest format from JSON files -interface LanguageManifestFile { +interface ExternalLanguageContribution { + id: string; + extensions: string[]; + aliases?: string[]; + filenames?: string[]; +} + +interface ExternalToolConfig { + name?: string; + args?: string[]; + env?: Record; +} + +interface ExternalLanguageManifest { id: string; name: string; - version: string; - description: string; - category: string; - author: string; - capabilities: { - languageProvider: { - id: string; - extensions: string[]; - aliases: string[]; - wasmPath: string; - highlightQuery: string; + displayName?: string; + description?: string; + version?: string; + publisher?: string; + categories?: string[]; + languages?: ExternalLanguageContribution[]; + capabilities?: { + grammar?: { + wasmPath?: string; + highlightQuery?: string; + scopeName?: string; }; + lsp?: ExternalToolConfig; + formatter?: ExternalToolConfig; + linter?: ExternalToolConfig; }; } -/** - * Convert a language manifest file to ExtensionManifest format - */ -function convertLanguageManifest(manifest: LanguageManifestFile): ExtensionManifest { - const { capabilities } = manifest; - const { languageProvider } = capabilities; +type PackagedLanguageEntry = { + manifest: ExtensionManifest; + languageIds: string[]; + wasmUrl: string; + highlightQueryUrl: string; +}; - // Convert file extensions to include dots - const extensions = languageProvider.extensions.map((ext) => - ext.startsWith(".") ? ext : `.${ext}`, - ); +function toExtensionCategories(rawCategories: string[] | undefined): ExtensionCategory[] { + if (!rawCategories || rawCategories.length === 0) return ["Language"]; + + return rawCategories.map((category) => { + const normalized = category.trim().toLowerCase(); + if (normalized === "language") return "Language"; + if (normalized === "linter") return "Linter"; + if (normalized === "formatter") return "Formatter"; + if (normalized === "theme") return "Theme"; + if (normalized === "keymaps") return "Keymaps"; + if (normalized === "snippets") return "Snippets"; + return "Other"; + }); +} + +function normalizeExtensions(extensions: string[]): string[] { + return extensions.map((ext) => (ext.startsWith(".") ? ext : `.${ext}`)); +} + +function defaultCommand(name?: string): PlatformExecutable { + return { default: name || "" }; +} + +function createLspConfig(manifest: ExternalLanguageManifest): LspConfiguration | undefined { + const lsp = manifest.capabilities?.lsp; + const languages = manifest.languages || []; + if (!lsp?.name || languages.length === 0) return undefined; + + const fileExtensions = languages.flatMap((lang) => normalizeExtensions(lang.extensions || [])); + const languageIds = languages.map((lang) => lang.id); return { + server: defaultCommand(lsp.name), + args: lsp.args || [], + env: lsp.env, + fileExtensions, + languageIds, + }; +} + +function createFormatterConfig( + manifest: ExternalLanguageManifest, +): FormatterConfiguration | undefined { + const formatter = manifest.capabilities?.formatter; + const languageIds = (manifest.languages || []).map((lang) => lang.id); + if (!formatter?.name || languageIds.length === 0) return undefined; + + return { + command: defaultCommand(formatter.name), + args: formatter.args || [], + env: formatter.env, + inputMethod: "stdin", + outputMethod: "stdout", + languages: languageIds, + }; +} + +function createLinterConfig(manifest: ExternalLanguageManifest): LinterConfiguration | undefined { + const linter = manifest.capabilities?.linter; + const languageIds = (manifest.languages || []).map((lang) => lang.id); + if (!linter?.name || languageIds.length === 0) return undefined; + + return { + command: defaultCommand(linter.name), + args: linter.args || [], + env: linter.env, + inputMethod: "stdin", + languages: languageIds, + }; +} + +function convertLanguageManifest( + path: string, + manifest: ExternalLanguageManifest, +): PackagedLanguageEntry { + const folderMatch = path.match(/\/extensions\/([^/]+)\/extension\.json$/); + const folder = folderMatch?.[1]; + + if (!folder) { + throw new Error(`Could not resolve extension folder from path: ${path}`); + } + + const languages = (manifest.languages || []).map((language) => ({ + id: language.id, + extensions: normalizeExtensions(language.extensions || []), + aliases: language.aliases, + filenames: language.filenames, + })); + + if (languages.length === 0) { + throw new Error(`No language contributions found for ${manifest.id}`); + } + + const wasmUrl = `${CDN_BASE_URL}/${folder}/parser.wasm`; + const highlightQueryUrl = `${CDN_BASE_URL}/${folder}/highlights.scm`; + const primaryLanguageId = languages[0].id; + + const converted: ExtensionManifest = { id: manifest.id, name: manifest.name, - displayName: manifest.name, - description: manifest.description, - version: manifest.version, - publisher: manifest.author, - categories: ["Language"], - languages: [ - { - id: languageProvider.id, - extensions, - aliases: languageProvider.aliases, - }, - ], - activationEvents: [`onLanguage:${languageProvider.id}`], - // Extension is downloadable from CDN + displayName: manifest.displayName || manifest.name, + description: manifest.description || `${manifest.name} language support`, + version: manifest.version || "1.0.0", + publisher: manifest.publisher || "Athas", + categories: toExtensionCategories(manifest.categories), + languages, + grammar: { + wasmPath: wasmUrl, + scopeName: manifest.capabilities?.grammar?.scopeName || `source.${primaryLanguageId}`, + languageId: primaryLanguageId, + }, + lsp: createLspConfig(manifest), + formatter: createFormatterConfig(manifest), + linter: createLinterConfig(manifest), + activationEvents: languages.map((lang) => `onLanguage:${lang.id}`), installation: { - downloadUrl: `${CDN_BASE_URL}/${languageProvider.id}/parser.wasm`, - size: 0, // Will be determined during download - checksum: "", // Will be calculated after download + downloadUrl: wasmUrl, + size: 0, + checksum: "", minEditorVersion: "0.1.0", }, }; + + return { + manifest: converted, + languageIds: languages.map((lang) => lang.id), + wasmUrl, + highlightQueryUrl, + }; } -// Import all manifest files -const manifestModules = import.meta.glob("./manifests/*.json", { - eager: true, - import: "default", -}); +let packagedEntries: PackagedLanguageEntry[] = []; +const manifestByLanguageId = new Map(); +const wasmUrlByLanguageId = new Map(); +const highlightUrlByLanguageId = new Map(); +const highlightUrlByExtensionId = new Map(); +let packagedExtensions: ExtensionManifest[] = []; +let initialized = false; +let initPromise: Promise | null = null; -/** - * Get all packaged language extensions - */ -export function getPackagedLanguageExtensions(): ExtensionManifest[] { - const extensions: ExtensionManifest[] = []; +function processManifests(manifests: Record) { + packagedEntries = []; + manifestByLanguageId.clear(); + wasmUrlByLanguageId.clear(); + highlightUrlByLanguageId.clear(); + highlightUrlByExtensionId.clear(); - for (const [path, manifest] of Object.entries(manifestModules)) { + for (const [folder, manifest] of Object.entries(manifests)) { try { - const converted = convertLanguageManifest(manifest); - extensions.push(converted); + const syntheticPath = `/extensions/${folder}/extension.json`; + const entry = convertLanguageManifest(syntheticPath, manifest); + packagedEntries.push(entry); + + highlightUrlByExtensionId.set(entry.manifest.id, entry.highlightQueryUrl); + + for (const languageId of entry.languageIds) { + manifestByLanguageId.set(languageId, entry.manifest); + wasmUrlByLanguageId.set(languageId, entry.wasmUrl); + highlightUrlByLanguageId.set(languageId, entry.highlightQueryUrl); + } } catch (error) { - console.error(`Failed to convert language manifest at ${path}:`, error); + console.error(`Failed to convert language manifest for ${folder}:`, error); } } - return extensions; + packagedExtensions = packagedEntries.map((entry) => entry.manifest); + initialized = true; } /** - * Get language extension by language ID + * Initialize the language packager by fetching manifests from the CDN. + * Must be called before using any getter functions. */ +export async function initializeLanguagePackager(): Promise { + if (initialized) return; + if (initPromise) return initPromise; + + initPromise = (async () => { + try { + const response = await fetch(MANIFESTS_URL); + if (!response.ok) { + throw new Error(`Failed to fetch manifests: ${response.status} ${response.statusText}`); + } + const manifests: Record = await response.json(); + processManifests(manifests); + } catch (error) { + console.error("Failed to load extension manifests from CDN:", error); + // Initialize with empty state so the editor can still function + initialized = true; + } + })(); + + return initPromise; +} + +export function getPackagedLanguageExtensions(): ExtensionManifest[] { + return packagedExtensions; +} + export function getLanguageExtensionById(languageId: string): ExtensionManifest | undefined { - const extensions = getPackagedLanguageExtensions(); - return extensions.find((ext) => ext.languages?.some((lang) => lang.id === languageId)); + return manifestByLanguageId.get(languageId); } -/** - * Get language extension by file extension - */ export function getLanguageExtensionByFileExt(fileExt: string): ExtensionManifest | undefined { const ext = fileExt.startsWith(".") ? fileExt : `.${fileExt}`; - const extensions = getPackagedLanguageExtensions(); - - return extensions.find((extension) => - extension.languages?.some((lang) => lang.extensions.includes(ext)), + return packagedExtensions.find((extension) => + extension.languages?.some((language) => language.extensions.includes(ext)), ); } -/** - * Get WASM download URL for a language - */ export function getWasmUrlForLanguage(languageId: string): string { - return `${CDN_BASE_URL}/${languageId}/parser.wasm`; + return wasmUrlByLanguageId.get(languageId) || `${CDN_BASE_URL}/${languageId}/parser.wasm`; } -/** - * Get highlight query URL for a language - */ export function getHighlightQueryUrl(languageId: string): string { - return `${CDN_BASE_URL}/${languageId}/highlights.scm`; + return highlightUrlByLanguageId.get(languageId) || `${CDN_BASE_URL}/${languageId}/highlights.scm`; +} + +export function getHighlightQueryUrlForExtension(manifest: ExtensionManifest): string { + return ( + highlightUrlByExtensionId.get(manifest.id) || + (manifest.languages?.[0] ? getHighlightQueryUrl(manifest.languages[0].id) : "") + ); } diff --git a/src/extensions/languages/manifests/bash.json b/src/extensions/languages/manifests/bash.json deleted file mode 100644 index 8906fc50..00000000 --- a/src/extensions/languages/manifests/bash.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "id": "language.bash", - "name": "Bash", - "version": "1.0.0", - "description": "Bash language support with syntax highlighting", - "category": "language", - "author": "Athas Team", - "capabilities": { - "languageProvider": { - "id": "bash", - "extensions": ["bash"], - "aliases": ["bash"], - "wasmPath": "/extensions/bash/parser.wasm", - "highlightQuery": "/extensions/bash/highlights.scm" - } - } -} diff --git a/src/extensions/languages/manifests/c.json b/src/extensions/languages/manifests/c.json deleted file mode 100644 index 49d2a216..00000000 --- a/src/extensions/languages/manifests/c.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "id": "language.c", - "name": "C", - "version": "1.0.0", - "description": "C language support with syntax highlighting", - "category": "language", - "author": "Athas Team", - "capabilities": { - "languageProvider": { - "id": "c", - "extensions": ["c", "h"], - "aliases": ["c"], - "wasmPath": "/extensions/c/parser.wasm", - "highlightQuery": "/extensions/c/highlights.scm" - } - } -} diff --git a/src/extensions/languages/manifests/cpp.json b/src/extensions/languages/manifests/cpp.json deleted file mode 100644 index b2c1e19c..00000000 --- a/src/extensions/languages/manifests/cpp.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "id": "language.cpp", - "name": "C++", - "version": "1.0.0", - "description": "C++ language support with syntax highlighting", - "category": "language", - "author": "Athas Team", - "capabilities": { - "languageProvider": { - "id": "cpp", - "extensions": ["cpp", "hpp", "cc", "cxx", "hh"], - "aliases": ["cpp"], - "wasmPath": "/extensions/cpp/parser.wasm", - "highlightQuery": "/extensions/cpp/highlights.scm" - } - } -} diff --git a/src/extensions/languages/manifests/csharp.json b/src/extensions/languages/manifests/csharp.json deleted file mode 100644 index f9dcbbf5..00000000 --- a/src/extensions/languages/manifests/csharp.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "id": "language.csharp", - "name": "C#", - "version": "1.0.0", - "description": "C# language support with syntax highlighting", - "category": "language", - "author": "Athas Team", - "capabilities": { - "languageProvider": { - "id": "csharp", - "extensions": ["cs", "csx"], - "aliases": ["csharp", "cs"], - "wasmPath": "/extensions/c_sharp/parser.wasm", - "highlightQuery": "/extensions/c_sharp/highlights.scm" - } - } -} diff --git a/src/extensions/languages/manifests/css.json b/src/extensions/languages/manifests/css.json deleted file mode 100644 index d30d8bbf..00000000 --- a/src/extensions/languages/manifests/css.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "id": "language.css", - "name": "CSS", - "version": "1.0.0", - "description": "CSS language support with syntax highlighting", - "category": "language", - "author": "Athas Team", - "capabilities": { - "languageProvider": { - "id": "css", - "extensions": ["css"], - "aliases": ["css"], - "wasmPath": "/extensions/css/parser.wasm", - "highlightQuery": "/extensions/css/highlights.scm" - } - } -} diff --git a/src/extensions/languages/manifests/dart.json b/src/extensions/languages/manifests/dart.json deleted file mode 100644 index bd81ffd7..00000000 --- a/src/extensions/languages/manifests/dart.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "id": "language.dart", - "name": "Dart", - "version": "1.0.0", - "description": "Dart language support with syntax highlighting", - "category": "language", - "author": "Athas Team", - "capabilities": { - "languageProvider": { - "id": "dart", - "extensions": ["dart"], - "aliases": ["dart"], - "wasmPath": "/extensions/dart/parser.wasm", - "highlightQuery": "/extensions/dart/highlights.scm" - } - } -} diff --git a/src/extensions/languages/manifests/elixir.json b/src/extensions/languages/manifests/elixir.json deleted file mode 100644 index e90c6628..00000000 --- a/src/extensions/languages/manifests/elixir.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "id": "language.elixir", - "name": "Elixir", - "version": "1.0.0", - "description": "Elixir language support with syntax highlighting", - "category": "language", - "author": "Athas Team", - "capabilities": { - "languageProvider": { - "id": "elixir", - "extensions": ["ex", "exs"], - "aliases": ["elixir"], - "wasmPath": "/extensions/elixir/parser.wasm", - "highlightQuery": "/extensions/elixir/highlights.scm" - } - } -} diff --git a/src/extensions/languages/manifests/go.json b/src/extensions/languages/manifests/go.json deleted file mode 100644 index 76760286..00000000 --- a/src/extensions/languages/manifests/go.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "id": "language.go", - "name": "Go", - "version": "1.0.0", - "description": "Go language support with syntax highlighting", - "category": "language", - "author": "Athas Team", - "capabilities": { - "languageProvider": { - "id": "go", - "extensions": ["go"], - "aliases": ["go"], - "wasmPath": "/extensions/go/parser.wasm", - "highlightQuery": "/extensions/go/highlights.scm" - } - } -} diff --git a/src/extensions/languages/manifests/html.json b/src/extensions/languages/manifests/html.json deleted file mode 100644 index 39e2e9d5..00000000 --- a/src/extensions/languages/manifests/html.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "id": "language.html", - "name": "HTML", - "version": "1.0.0", - "description": "HTML language support with syntax highlighting", - "category": "language", - "author": "Athas Team", - "capabilities": { - "languageProvider": { - "id": "html", - "extensions": ["html", "htm"], - "aliases": ["html"], - "wasmPath": "/extensions/html/parser.wasm", - "highlightQuery": "/extensions/html/highlights.scm" - } - } -} diff --git a/src/extensions/languages/manifests/java.json b/src/extensions/languages/manifests/java.json deleted file mode 100644 index 71ca2128..00000000 --- a/src/extensions/languages/manifests/java.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "id": "language.java", - "name": "Java", - "version": "1.0.0", - "description": "Java language support with syntax highlighting", - "category": "language", - "author": "Athas Team", - "capabilities": { - "languageProvider": { - "id": "java", - "extensions": ["java"], - "aliases": ["java"], - "wasmPath": "/extensions/java/parser.wasm", - "highlightQuery": "/extensions/java/highlights.scm" - } - } -} diff --git a/src/extensions/languages/manifests/json.json b/src/extensions/languages/manifests/json.json deleted file mode 100644 index 84733b2b..00000000 --- a/src/extensions/languages/manifests/json.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "id": "language.json", - "name": "JSON", - "version": "1.0.0", - "description": "JSON language support with syntax highlighting", - "category": "language", - "author": "Athas Team", - "capabilities": { - "languageProvider": { - "id": "json", - "extensions": ["json", "jsonc"], - "aliases": ["json"], - "wasmPath": "/extensions/json/parser.wasm", - "highlightQuery": "/extensions/json/highlights.scm" - } - } -} diff --git a/src/extensions/languages/manifests/kotlin.json b/src/extensions/languages/manifests/kotlin.json deleted file mode 100644 index 8eff2a4c..00000000 --- a/src/extensions/languages/manifests/kotlin.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "id": "language.kotlin", - "name": "Kotlin", - "version": "1.0.0", - "description": "Kotlin language support with syntax highlighting", - "category": "language", - "author": "Athas Team", - "capabilities": { - "languageProvider": { - "id": "kotlin", - "extensions": ["kt", "kts"], - "aliases": ["kotlin"], - "wasmPath": "/extensions/kotlin/parser.wasm", - "highlightQuery": "/extensions/kotlin/highlights.scm" - } - } -} diff --git a/src/extensions/languages/manifests/lua.json b/src/extensions/languages/manifests/lua.json deleted file mode 100644 index 41bdac95..00000000 --- a/src/extensions/languages/manifests/lua.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "id": "language.lua", - "name": "Lua", - "version": "1.0.0", - "description": "Lua language support with syntax highlighting", - "category": "language", - "author": "Athas Team", - "capabilities": { - "languageProvider": { - "id": "lua", - "extensions": ["lua"], - "aliases": ["lua"], - "wasmPath": "/extensions/lua/parser.wasm", - "highlightQuery": "/extensions/lua/highlights.scm" - } - } -} diff --git a/src/extensions/languages/manifests/markdown.json b/src/extensions/languages/manifests/markdown.json deleted file mode 100644 index 43f6617e..00000000 --- a/src/extensions/languages/manifests/markdown.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "id": "language.markdown", - "name": "Markdown", - "version": "1.0.0", - "description": "Markdown language support with syntax highlighting", - "category": "language", - "author": "Athas Team", - "capabilities": { - "languageProvider": { - "id": "markdown", - "extensions": ["md", "markdown", "mdx"], - "aliases": ["markdown", "md"], - "wasmPath": "/extensions/markdown/parser.wasm", - "highlightQuery": "/extensions/markdown/highlights.scm" - } - } -} diff --git a/src/extensions/languages/manifests/ocaml.json b/src/extensions/languages/manifests/ocaml.json deleted file mode 100644 index 230ad379..00000000 --- a/src/extensions/languages/manifests/ocaml.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "id": "language.ocaml", - "name": "OCaml", - "version": "1.0.0", - "description": "OCaml language support with syntax highlighting", - "category": "language", - "author": "Athas Team", - "capabilities": { - "languageProvider": { - "id": "ocaml", - "extensions": ["ml", "mli"], - "aliases": ["ocaml"], - "wasmPath": "/extensions/ocaml/parser.wasm", - "highlightQuery": "/extensions/ocaml/highlights.scm" - } - } -} diff --git a/src/extensions/languages/manifests/python.json b/src/extensions/languages/manifests/python.json deleted file mode 100644 index a4bda368..00000000 --- a/src/extensions/languages/manifests/python.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "id": "language.python", - "name": "Python", - "version": "1.0.0", - "description": "Python language support with syntax highlighting", - "category": "language", - "author": "Athas Team", - "capabilities": { - "languageProvider": { - "id": "python", - "extensions": ["py", "pyw"], - "aliases": ["py", "python"], - "wasmPath": "/extensions/python/parser.wasm", - "highlightQuery": "/extensions/python/highlights.scm" - } - } -} diff --git a/src/extensions/languages/manifests/ruby.json b/src/extensions/languages/manifests/ruby.json deleted file mode 100644 index b8375372..00000000 --- a/src/extensions/languages/manifests/ruby.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "id": "language.ruby", - "name": "Ruby", - "version": "1.0.0", - "description": "Ruby language support with syntax highlighting (includes ERB)", - "category": "language", - "author": "Athas Team", - "capabilities": { - "languageProvider": { - "id": "ruby", - "extensions": ["rb", "erb", "rake", "gemspec"], - "aliases": ["ruby", "erb"], - "wasmPath": "/extensions/ruby/parser.wasm", - "highlightQuery": "/extensions/ruby/highlights.scm" - } - } -} diff --git a/src/extensions/languages/manifests/rust.json b/src/extensions/languages/manifests/rust.json deleted file mode 100644 index a160138f..00000000 --- a/src/extensions/languages/manifests/rust.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "id": "language.rust", - "name": "Rust", - "version": "1.0.0", - "description": "Rust language support with syntax highlighting", - "category": "language", - "author": "Athas Team", - "capabilities": { - "languageProvider": { - "id": "rust", - "extensions": ["rs"], - "aliases": ["rust"], - "wasmPath": "/extensions/rust/parser.wasm", - "highlightQuery": "/extensions/rust/highlights.scm" - } - } -} diff --git a/src/extensions/languages/manifests/scala.json b/src/extensions/languages/manifests/scala.json deleted file mode 100644 index b457ad52..00000000 --- a/src/extensions/languages/manifests/scala.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "id": "language.scala", - "name": "Scala", - "version": "1.0.0", - "description": "Scala language support with syntax highlighting", - "category": "language", - "author": "Athas Team", - "capabilities": { - "languageProvider": { - "id": "scala", - "extensions": ["scala", "sc"], - "aliases": ["scala"], - "wasmPath": "/extensions/scala/parser.wasm", - "highlightQuery": "/extensions/scala/highlights.scm" - } - } -} diff --git a/src/extensions/languages/manifests/solidity.json b/src/extensions/languages/manifests/solidity.json deleted file mode 100644 index ae749c7c..00000000 --- a/src/extensions/languages/manifests/solidity.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "id": "language.solidity", - "name": "Solidity", - "version": "1.0.0", - "description": "Solidity language support with syntax highlighting", - "category": "language", - "author": "Athas Team", - "capabilities": { - "languageProvider": { - "id": "solidity", - "extensions": ["sol"], - "aliases": ["solidity"], - "wasmPath": "/extensions/solidity/parser.wasm", - "highlightQuery": "/extensions/solidity/highlights.scm" - } - } -} diff --git a/src/extensions/languages/manifests/swift.json b/src/extensions/languages/manifests/swift.json deleted file mode 100644 index 274f39d1..00000000 --- a/src/extensions/languages/manifests/swift.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "id": "language.swift", - "name": "Swift", - "version": "1.0.0", - "description": "Swift language support with syntax highlighting", - "category": "language", - "author": "Athas Team", - "capabilities": { - "languageProvider": { - "id": "swift", - "extensions": ["swift"], - "aliases": ["swift"], - "wasmPath": "/extensions/swift/parser.wasm", - "highlightQuery": "/extensions/swift/highlights.scm" - } - } -} diff --git a/src/extensions/languages/manifests/toml.json b/src/extensions/languages/manifests/toml.json deleted file mode 100644 index fec80832..00000000 --- a/src/extensions/languages/manifests/toml.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "id": "language.toml", - "name": "TOML", - "version": "1.0.0", - "description": "TOML language support with syntax highlighting", - "category": "language", - "author": "Athas Team", - "capabilities": { - "languageProvider": { - "id": "toml", - "extensions": ["toml"], - "aliases": ["toml"], - "wasmPath": "/extensions/toml/parser.wasm", - "highlightQuery": "/extensions/toml/highlights.scm" - } - } -} diff --git a/src/extensions/languages/manifests/typescript.json b/src/extensions/languages/manifests/typescript.json deleted file mode 100644 index 14b06fe5..00000000 --- a/src/extensions/languages/manifests/typescript.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "id": "language.typescript", - "name": "TypeScript", - "version": "1.0.0", - "description": "TypeScript and JavaScript language support with syntax highlighting", - "category": "language", - "author": "Athas Team", - "capabilities": { - "languageProvider": { - "id": "tsx", - "extensions": ["ts", "tsx", "mts", "cts", "js", "jsx", "mjs", "cjs"], - "aliases": ["typescript", "javascript", "ts", "js"], - "wasmPath": "/extensions/tsx/parser.wasm", - "highlightQuery": "/extensions/tsx/highlights.scm" - } - } -} diff --git a/src/extensions/languages/manifests/vue.json b/src/extensions/languages/manifests/vue.json deleted file mode 100644 index a1a28acd..00000000 --- a/src/extensions/languages/manifests/vue.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "id": "language.vue", - "name": "Vue", - "version": "1.0.0", - "description": "Vue.js language support with syntax highlighting", - "category": "language", - "author": "Athas Team", - "capabilities": { - "languageProvider": { - "id": "vue", - "extensions": ["vue"], - "aliases": ["vue"], - "wasmPath": "/extensions/vue/parser.wasm", - "highlightQuery": "/extensions/vue/highlights.scm" - } - } -} diff --git a/src/extensions/languages/manifests/yaml.json b/src/extensions/languages/manifests/yaml.json deleted file mode 100644 index 1837e424..00000000 --- a/src/extensions/languages/manifests/yaml.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "id": "language.yaml", - "name": "YAML", - "version": "1.0.0", - "description": "YAML language support with syntax highlighting", - "category": "language", - "author": "Athas Team", - "capabilities": { - "languageProvider": { - "id": "yaml", - "extensions": ["yaml", "yml"], - "aliases": ["yaml", "yml"], - "wasmPath": "/extensions/yaml/parser.wasm", - "highlightQuery": "/extensions/yaml/highlights.scm" - } - } -} diff --git a/src/extensions/languages/manifests/zig.json b/src/extensions/languages/manifests/zig.json deleted file mode 100644 index f0f5e23f..00000000 --- a/src/extensions/languages/manifests/zig.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "id": "language.zig", - "name": "Zig", - "version": "1.0.0", - "description": "Zig language support with syntax highlighting", - "category": "language", - "author": "Athas Team", - "capabilities": { - "languageProvider": { - "id": "zig", - "extensions": ["zig"], - "aliases": ["zig"], - "wasmPath": "/extensions/zig/parser.wasm", - "highlightQuery": "/extensions/zig/highlights.scm" - } - } -} diff --git a/src/extensions/loader/extension-loader.ts b/src/extensions/loader/extension-loader.ts index 7cfed802..d00e3f3d 100644 --- a/src/extensions/loader/extension-loader.ts +++ b/src/extensions/loader/extension-loader.ts @@ -49,7 +49,7 @@ function createDummyEditorAPI(): EditorAPI { tabSize: 2, lineNumbers: true, wordWrap: false, - theme: "one-dark", + theme: "vitesse-dark", }), updateSettings: () => {}, on: () => () => {}, @@ -248,14 +248,15 @@ class ExtensionLoader { // Load all extensions from registry const extensions = extensionRegistry.getAllExtensions(); - for (const extension of extensions) { - try { - await this.loadExtension(extension); - } catch (error) { + const results = await Promise.allSettled(extensions.map((ext) => this.loadExtension(ext))); + + for (let i = 0; i < results.length; i++) { + const result = results[i]; + if (result.status === "rejected") { logger.error( "ExtensionLoader", - `Failed to load extension ${extension.manifest.displayName}:`, - error, + `Failed to load extension ${extensions[i].manifest.displayName}:`, + result.reason, ); } } diff --git a/src/extensions/registry/extension-registry.ts b/src/extensions/registry/extension-registry.ts index 338a84c5..cca30d06 100644 --- a/src/extensions/registry/extension-registry.ts +++ b/src/extensions/registry/extension-registry.ts @@ -67,6 +67,38 @@ class ExtensionRegistry { } } + /** + * Register or update an extension at runtime. + * Used for language extensions installed via the extension store. + */ + registerExtension( + manifest: ExtensionManifest, + options: { + path?: string; + isBundled?: boolean; + isEnabled?: boolean; + state?: ExtensionState; + } = {}, + ): void { + const existing = this.extensions.get(manifest.id); + + this.extensions.set(manifest.id, { + manifest, + path: options.path ?? existing?.path ?? "", + isBundled: options.isBundled ?? existing?.isBundled ?? false, + isEnabled: options.isEnabled ?? existing?.isEnabled ?? true, + state: options.state ?? existing?.state ?? "installed", + }); + } + + /** + * Unregister an extension at runtime. + */ + unregisterExtension(extensionId: string): void { + this.extensions.delete(extensionId); + this.activatedExtensions.delete(extensionId); + } + /** * Get all registered extensions */ @@ -116,13 +148,35 @@ class ExtensionRegistry { return undefined; } + /** + * Get extension by a full file path (checks filename and extension). + */ + getExtensionForFilePath(filePath: string): BundledExtension | undefined { + const fileName = filePath.split("/").pop() || filePath; + + for (const extension of this.extensions.values()) { + if (!extension.manifest.languages) continue; + + for (const language of extension.manifest.languages) { + if (language.filenames?.includes(fileName)) { + return extension; + } + } + } + + const lastDotIndex = fileName.lastIndexOf("."); + if (lastDotIndex === -1) { + return undefined; + } + + return this.getExtensionByFileExtension(fileName.substring(lastDotIndex)); + } + /** * Get LSP server path for a file */ getLspServerPath(filePath: string): string | null { - // Extract file extension - const ext = filePath.substring(filePath.lastIndexOf(".")); - const extension = this.getExtensionByFileExtension(ext); + const extension = this.getExtensionForFilePath(filePath); if (!extension?.manifest.lsp) { return null; @@ -154,8 +208,7 @@ class ExtensionRegistry { * Get LSP server arguments for a file */ getLspServerArgs(filePath: string): string[] { - const ext = filePath.substring(filePath.lastIndexOf(".")); - const extension = this.getExtensionByFileExtension(ext); + const extension = this.getExtensionForFilePath(filePath); if (!extension?.manifest.lsp) { return []; @@ -168,8 +221,7 @@ class ExtensionRegistry { * Get LSP initialization options for a file */ getLspInitializationOptions(filePath: string): Record | undefined { - const ext = filePath.substring(filePath.lastIndexOf(".")); - const extension = this.getExtensionByFileExtension(ext); + const extension = this.getExtensionForFilePath(filePath); if (!extension?.manifest.lsp) { return undefined; @@ -189,8 +241,10 @@ class ExtensionRegistry { * Get language ID for a file */ getLanguageId(filePath: string): string | null { - const ext = filePath.substring(filePath.lastIndexOf(".")); - const extension = this.getExtensionByFileExtension(ext); + const extension = this.getExtensionForFilePath(filePath); + const fileName = filePath.split("/").pop() || filePath; + const extIndex = fileName.lastIndexOf("."); + const ext = extIndex >= 0 ? fileName.substring(extIndex) : ""; if (!extension?.manifest.languages) { return null; @@ -201,6 +255,10 @@ class ExtensionRegistry { if (lang.extensions.includes(ext)) { return lang.id; } + + if (lang.filenames?.includes(fileName)) { + return lang.id; + } } return null; @@ -289,8 +347,7 @@ class ExtensionRegistry { inputMethod?: "stdin" | "file"; outputMethod?: "stdout" | "file"; } | null { - const ext = filePath.substring(filePath.lastIndexOf(".")); - const extension = this.getExtensionByFileExtension(ext); + const extension = this.getExtensionForFilePath(filePath); if (!extension?.manifest.formatter) { return null; @@ -371,8 +428,7 @@ class ExtensionRegistry { diagnosticFormat?: "lsp" | "regex"; diagnosticPattern?: string; } | null { - const ext = filePath.substring(filePath.lastIndexOf(".")); - const extension = this.getExtensionByFileExtension(ext); + const extension = this.getExtensionForFilePath(filePath); if (!extension?.manifest.linter) { return null; diff --git a/src/extensions/registry/extension-store.ts b/src/extensions/registry/extension-store.ts index 433aad91..b2e38be3 100644 --- a/src/extensions/registry/extension-store.ts +++ b/src/extensions/registry/extension-store.ts @@ -5,11 +5,13 @@ import { immer } from "zustand/middleware/immer"; import { wasmParserLoader } from "@/features/editor/lib/wasm-parser/loader"; import { createSelectors } from "@/utils/zustand-selectors"; import { extensionInstaller } from "../installer/extension-installer"; -import { getDownloadInfoForPlatform, getFullExtensions } from "../languages/full-extensions"; import { getHighlightQueryUrl, + getHighlightQueryUrlForExtension, + getLanguageExtensionById, getPackagedLanguageExtensions, getWasmUrlForLanguage, + initializeLanguagePackager, } from "../languages/language-packager"; import { extensionRegistry } from "../registry/extension-registry"; import type { ExtensionManifest } from "../types/extension-manifest"; @@ -50,6 +52,50 @@ interface ExtensionStoreState { }; } +const HIDDEN_MARKETPLACE_EXTENSION_IDS = new Set(["athas.tsx"]); + +function mergeMarketplaceLanguageExtensions(extensions: ExtensionManifest[]): ExtensionManifest[] { + const visibleExtensions = extensions.filter( + (manifest) => !HIDDEN_MARKETPLACE_EXTENSION_IDS.has(manifest.id), + ); + + const typescript = visibleExtensions.find((manifest) => manifest.id === "athas.typescript"); + const tsx = extensions.find((manifest) => manifest.id === "athas.tsx"); + + if (!typescript || !tsx?.languages?.length) { + return visibleExtensions; + } + + const mergedLanguages = [...(typescript.languages || [])]; + const existingLanguageIds = new Set(mergedLanguages.map((lang) => lang.id)); + + for (const language of tsx.languages) { + if (!existingLanguageIds.has(language.id)) { + mergedLanguages.push({ + ...language, + extensions: [...language.extensions], + aliases: language.aliases ? [...language.aliases] : undefined, + filenames: language.filenames ? [...language.filenames] : undefined, + }); + existingLanguageIds.add(language.id); + } + } + + const mergedActivationEvents = Array.from( + new Set([...(typescript.activationEvents || []), ...(tsx.activationEvents || [])]), + ); + + return visibleExtensions.map((manifest) => + manifest.id === typescript.id + ? { + ...manifest, + languages: mergedLanguages, + activationEvents: mergedActivationEvents, + } + : manifest, + ); +} + /** * Helper function to register a language provider with the ExtensionManager. * This consolidates the registration logic used in both loadInstalledExtensions and installExtension. @@ -64,15 +110,16 @@ async function registerLanguageProvider(params: { }): Promise { const { extensionId, languageId, displayName, version, extensions, aliases } = params; const { extensionManager } = await import("@/features/editor/extensions/manager"); + const runtimeExtensionId = `${extensionId}:${languageId}`; - if (extensionManager.isExtensionLoaded(extensionId)) { + if (extensionManager.isExtensionLoaded(runtimeExtensionId)) { return; } const { tokenizeCode, convertToEditorTokens } = await import("@/features/editor/lib/wasm-parser"); const languageExtension = { - id: extensionId, + id: runtimeExtensionId, displayName, version, category: "language", @@ -103,6 +150,135 @@ async function registerLanguageProvider(params: { await extensionManager.loadLanguageExtension(languageExtension); } +type ToolType = "lsp" | "formatter" | "linter"; +type ToolPathMap = Partial>; + +function getCommandDefault( + command: + | { + default?: string; + darwin?: string; + linux?: string; + win32?: string; + } + | undefined, +): string | undefined { + return command?.default || command?.darwin || command?.linux || command?.win32; +} + +function resolveInstalledExtensionId( + installed: { languageId: string; extensionId?: string }, + availableExtensions: Map, +): string { + const candidates = [ + installed.extensionId, + installed.extensionId?.replace(/-full$/, ""), + `athas.${installed.languageId}`, + `language.${installed.languageId}`, + ].filter((candidate): candidate is string => Boolean(candidate)); + + for (const candidate of candidates) { + if (availableExtensions.has(candidate)) { + return candidate; + } + } + + for (const [extensionId, extension] of availableExtensions) { + if (extension.manifest.languages?.some((lang) => lang.id === installed.languageId)) { + return extensionId; + } + } + + return installed.extensionId || `athas.${installed.languageId}`; +} + +async function installLanguageTools(languageId: string): Promise { + try { + await invoke("install_language_tools", { languageId }); + } catch (error) { + console.warn(`Failed to install tools for ${languageId}:`, error); + } +} + +async function getToolPath(languageId: string, toolType: ToolType): Promise { + try { + return await invoke("get_tool_path", { + languageId, + toolType, + }); + } catch { + return null; + } +} + +async function resolveToolPaths( + languageId: string, + options: { ensureInstalled?: boolean } = {}, +): Promise { + if (options.ensureInstalled) { + await installLanguageTools(languageId); + } + + const [lsp, formatter, linter] = await Promise.all([ + getToolPath(languageId, "lsp"), + getToolPath(languageId, "formatter"), + getToolPath(languageId, "linter"), + ]); + + return { + ...(lsp ? { lsp } : {}), + ...(formatter ? { formatter } : {}), + ...(linter ? { linter } : {}), + }; +} + +function buildRuntimeManifest( + manifest: ExtensionManifest, + toolPaths: ToolPathMap, +): ExtensionManifest { + const runtimeManifest: ExtensionManifest = { + ...manifest, + languages: manifest.languages?.map((lang) => ({ + ...lang, + extensions: [...lang.extensions], + aliases: lang.aliases ? [...lang.aliases] : undefined, + filenames: lang.filenames ? [...lang.filenames] : undefined, + })), + }; + + if (runtimeManifest.lsp) { + const defaultServer = getCommandDefault(runtimeManifest.lsp.server); + runtimeManifest.lsp = { + ...runtimeManifest.lsp, + server: { + default: toolPaths.lsp || defaultServer, + }, + }; + } + + if (runtimeManifest.formatter) { + const defaultCommand = getCommandDefault(runtimeManifest.formatter.command); + runtimeManifest.formatter = { + ...runtimeManifest.formatter, + command: { + default: toolPaths.formatter || defaultCommand, + }, + }; + } + + if (runtimeManifest.linter) { + const defaultCommand = getCommandDefault(runtimeManifest.linter.command); + runtimeManifest.linter = { + ...runtimeManifest.linter, + command: { + default: toolPaths.linter || defaultCommand, + }, + }; + } + + return runtimeManifest; +} + const useExtensionStoreBase = create()( immer((set, get) => ({ availableExtensions: new Map(), @@ -120,10 +296,9 @@ const useExtensionStoreBase = create()( try { // Load language extensions from packager (all installable from server) - const extensions: ExtensionManifest[] = getPackagedLanguageExtensions(); - - // Also load bundled extensions from extension registry (themes, icons only) - const bundledExtensions = extensionRegistry.getAllExtensions(); + const extensions: ExtensionManifest[] = mergeMarketplaceLanguageExtensions( + getPackagedLanguageExtensions(), + ); // Check which extensions are installed const installed = get().installedExtensions; @@ -138,35 +313,6 @@ const useExtensionStoreBase = create()( }); } - // Add bundled extensions (themes, icons - always installed) - for (const bundled of bundledExtensions) { - state.availableExtensions.set(bundled.manifest.id, { - manifest: bundled.manifest, - isInstalled: true, - isInstalling: false, - }); - - if (!state.installedExtensions.has(bundled.manifest.id)) { - state.installedExtensions.set(bundled.manifest.id, { - id: bundled.manifest.id, - name: bundled.manifest.displayName, - version: bundled.manifest.version, - installed_at: new Date().toISOString(), - enabled: true, - }); - } - } - - // Add full extensions (with LSP, formatters, etc.) - const fullExts = getFullExtensions(); - for (const manifest of fullExts) { - state.availableExtensions.set(manifest.id, { - manifest, - isInstalled: installed.has(manifest.id), - isInstalling: false, - }); - } - state.isLoadingRegistry = false; }); } catch (error) { @@ -195,30 +341,37 @@ const useExtensionStoreBase = create()( // Also check IndexedDB for installed language parsers const indexedDBInstalled = await extensionInstaller.listInstalled(); + const availableExtensions = get().availableExtensions; - // Register language extensions with the ExtensionManager - // This is critical for syntax highlighting to work on app restart // Register language extensions with the ExtensionManager for (const installed of indexedDBInstalled) { const languageId = installed.languageId; - // Use stored extensionId if available, otherwise fallback to constructed ID - const extensionId = installed.extensionId || `language.${languageId}`; - - // Get language config from available extensions if loaded - const ext = get().availableExtensions.get(extensionId); - const langConfig = ext?.manifest.languages?.[0]; - - // Default extensions if manifest not available yet - const extensions = langConfig?.extensions || [`.${languageId}`]; - const aliases = langConfig?.aliases; + const extensionId = resolveInstalledExtensionId(installed, availableExtensions); + + const extension = + availableExtensions.get(extensionId)?.manifest || + getLanguageExtensionById(languageId); + const languageConfig = extension?.languages?.find((lang) => lang.id === languageId); + const languageExtensions = languageConfig?.extensions || [`.${languageId}`]; + const aliases = languageConfig?.aliases; + + if (extension) { + const toolPaths = await resolveToolPaths(languageId); + const runtimeManifest = buildRuntimeManifest(extension, toolPaths); + extensionRegistry.registerExtension(runtimeManifest, { + isBundled: false, + isEnabled: true, + state: "installed", + }); + } try { await registerLanguageProvider({ extensionId, languageId, - displayName: ext?.manifest.displayName || languageId, + displayName: extension?.displayName || languageId, version: installed.version, - extensions, + extensions: languageExtensions, aliases, }); } catch (error) { @@ -232,13 +385,23 @@ const useExtensionStoreBase = create()( // Add language extensions from IndexedDB for (const installed of indexedDBInstalled) { - // Use stored extensionId if available, otherwise fallback to constructed ID - const extensionId = installed.extensionId || `language.${installed.languageId}`; + const extensionId = resolveInstalledExtensionId(installed, state.availableExtensions); if (!state.installedExtensions.has(extensionId)) { // Get manifest info if available, but always add to installedExtensions // to avoid timing issues where availableExtensions hasn't loaded yet - const ext = state.availableExtensions.get(extensionId); + const ext = + state.availableExtensions.get(extensionId) || + (() => { + const manifest = getLanguageExtensionById(installed.languageId); + return manifest + ? { + manifest, + isInstalled: true, + isInstalling: false, + } + : undefined; + })(); state.installedExtensions.set(extensionId, { id: extensionId, name: ext?.manifest.displayName || installed.languageId, @@ -269,16 +432,18 @@ const useExtensionStoreBase = create()( }, getExtensionForFile: (filePath: string) => { - const ext = filePath.split(".").pop()?.toLowerCase(); - if (!ext) return undefined; - - const fileExt = `.${ext}`; + const fileName = filePath.split("/").pop() || filePath; + const ext = fileName.split(".").pop()?.toLowerCase(); + const fileExt = ext ? `.${ext}` : null; // First check availableExtensions (loaded extensions) for (const [, extension] of get().availableExtensions) { if (extension.manifest.languages) { for (const lang of extension.manifest.languages) { - if (lang.extensions.includes(fileExt)) { + if ( + (fileExt && lang.extensions.includes(fileExt)) || + lang.filenames?.includes(fileName) + ) { return extension; } } @@ -290,7 +455,10 @@ const useExtensionStoreBase = create()( for (const bundled of bundledExtensions) { if (bundled.manifest.languages) { for (const lang of bundled.manifest.languages) { - if (lang.extensions.includes(fileExt)) { + if ( + (fileExt && lang.extensions.includes(fileExt)) || + lang.filenames?.includes(fileName) + ) { // Return as AvailableExtension with isInstalled: true return { manifest: bundled.manifest, @@ -325,35 +493,53 @@ const useExtensionStoreBase = create()( }); try { - // Check if this is a full extension with LSP (needs filesystem installation) - const hasLsp = extension.manifest.lsp !== undefined; - - if (hasLsp) { - // Full extension with LSP - use Tauri backend for filesystem extraction - // Get platform-specific download info - const downloadInfo = getDownloadInfoForPlatform(extension.manifest); - if (!downloadInfo) { - throw new Error( - `No download available for extension ${extensionId} on this platform`, - ); + if (extension.manifest.languages && extension.manifest.languages.length > 0) { + const languageConfigs = extension.manifest.languages; + const languageCount = languageConfigs.length; + + for (const [index, languageConfig] of languageConfigs.entries()) { + const languageId = languageConfig.id; + const wasmUrl = getWasmUrlForLanguage(languageId); + const highlightQueryUrl = + getHighlightQueryUrl(languageId) || + getHighlightQueryUrlForExtension(extension.manifest) || + `${wasmUrl.replace(/parser\.wasm$/, "highlights.scm")}`; + + await extensionInstaller.installLanguage(languageId, wasmUrl, highlightQueryUrl, { + extensionId, + version: extension.manifest.version, + checksum: extension.manifest.installation.checksum || "", + onProgress: (progress) => { + const completedLanguages = index * 100; + const normalizedProgress = + (completedLanguages + progress.percentage) / languageCount; + + set((state) => { + const ext = state.availableExtensions.get(extensionId); + if (ext) { + ext.installProgress = normalizedProgress; + } + }); + }, + }); } - await invoke("install_extension_from_url", { - extensionId, - url: downloadInfo.downloadUrl, - checksum: downloadInfo.checksum, - size: downloadInfo.size, + const primaryLanguageId = languageConfigs[0].id; + const toolPaths = await resolveToolPaths(primaryLanguageId, { ensureInstalled: true }); + const runtimeManifest = buildRuntimeManifest(extension.manifest, toolPaths); + extensionRegistry.registerExtension(runtimeManifest, { + isBundled: false, + isEnabled: true, + state: "installed", }); - // Reload installed extensions - await get().actions.loadInstalledExtensions(); - set((state) => { const ext = state.availableExtensions.get(extensionId); if (ext) { ext.isInstalling = false; ext.isInstalled = true; ext.installProgress = 100; + ext.manifest = runtimeManifest; state.installedExtensions.set(extensionId, { id: extensionId, @@ -366,83 +552,17 @@ const useExtensionStoreBase = create()( state.availableExtensions = new Map(state.availableExtensions); }); - // Trigger re-highlighting for open files that match this language - if (extension.manifest.languages) { - const { useBufferStore } = await import("@/features/editor/stores/buffer-store"); - const bufferState = useBufferStore.getState(); - const activeBuffer = bufferState.buffers.find((b) => b.isActive); - - if (activeBuffer) { - const fileExt = `.${activeBuffer.path.split(".").pop()?.toLowerCase()}`; - const matchesLanguage = extension.manifest.languages.some((lang) => - lang.extensions.includes(fileExt), - ); - - if (matchesLanguage) { - const { setSyntaxHighlightingFilePath } = await import( - "@/features/editor/extensions/builtin/syntax-highlighting" - ); - setSyntaxHighlightingFilePath(activeBuffer.path); - } - } + for (const languageConfig of languageConfigs) { + await registerLanguageProvider({ + extensionId, + languageId: languageConfig.id, + displayName: extension.manifest.displayName, + version: extension.manifest.version, + extensions: languageConfig.extensions, + aliases: languageConfig.aliases, + }); } - } else if (extension.manifest.languages && extension.manifest.languages.length > 0) { - // Simple language extension (WASM + queries only) - use IndexedDB - const languageId = extension.manifest.languages[0].id; - - // Use the download URL from the manifest, or generate it if not provided - const wasmUrl = - extension.manifest.installation.downloadUrl || getWasmUrlForLanguage(languageId); - const highlightQueryUrl = getHighlightQueryUrl(languageId); - - // Install using extension installer - await extensionInstaller.installLanguage(languageId, wasmUrl, highlightQueryUrl, { - extensionId: extensionId, // Pass the manifest ID for proper tracking - version: extension.manifest.version, - checksum: extension.manifest.installation.checksum || "", - onProgress: (progress) => { - set((state) => { - const ext = state.availableExtensions.get(extensionId); - if (ext) { - ext.installProgress = progress.percentage; - } - }); - }, - }); - // Mark as installed - set((state) => { - const ext = state.availableExtensions.get(extensionId); - if (ext) { - ext.isInstalling = false; - ext.isInstalled = true; - ext.installProgress = 100; - - // Also add to installedExtensions map for consistency - state.installedExtensions.set(extensionId, { - id: extensionId, - name: ext.manifest.displayName, - version: ext.manifest.version, - installed_at: new Date().toISOString(), - enabled: true, - }); - } - // Create new Map reference to trigger React re-render - state.availableExtensions = new Map(state.availableExtensions); - }); - - // Register the language provider with the ExtensionManager - const langConfig = extension.manifest.languages[0]; - await registerLanguageProvider({ - extensionId, - languageId, - displayName: extension.manifest.displayName, - version: extension.manifest.version, - extensions: langConfig.extensions, - aliases: langConfig.aliases, - }); - - // Trigger re-highlighting for open files that match this language const { useBufferStore } = await import("@/features/editor/stores/buffer-store"); const bufferState = useBufferStore.getState(); const activeBuffer = bufferState.buffers.find((b) => b.isActive); @@ -505,19 +625,32 @@ const useExtensionStoreBase = create()( try { // Check if this is a language extension if (extension.manifest.languages && extension.manifest.languages.length > 0) { - const languageId = extension.manifest.languages[0].id; - - // Uninstall using extension installer (removes from IndexedDB) - await extensionInstaller.uninstallLanguage(languageId); + const languageIds = extension.manifest.languages.map((language) => language.id); + + // Uninstall all languages for this extension (removes from IndexedDB) + await Promise.all( + languageIds.map(async (languageId) => { + wasmParserLoader.unloadParser(languageId); + await extensionInstaller.uninstallLanguage(languageId); + }), + ); // Also unload from extension manager if loaded const { extensionManager } = await import("@/features/editor/extensions/manager"); try { + await Promise.all( + languageIds.map((languageId) => + extensionManager.unloadLanguageExtension(`${extensionId}:${languageId}`), + ), + ); + // Backward compatibility for previously loaded single-id providers. await extensionManager.unloadLanguageExtension(extensionId); } catch (error) { console.warn(`Failed to unload language extension ${extensionId}:`, error); } + extensionRegistry.unregisterExtension(extensionId); + // Mark as not installed set((state) => { const ext = state.availableExtensions.get(extensionId); @@ -572,7 +705,7 @@ const useExtensionStoreBase = create()( const updates: string[] = []; for (const ext of installed) { - const extensionId = ext.extensionId || `language.${ext.languageId}`; + const extensionId = resolveInstalledExtensionId(ext, get().availableExtensions); const available = get().availableExtensions.get(extensionId); if (available && available.manifest.version !== ext.version) { updates.push(extensionId); @@ -600,21 +733,30 @@ const useExtensionStoreBase = create()( throw new Error(`Extension ${extensionId} not found or has no languages`); } - const languageId = extension.manifest.languages[0].id; + const languageIds = extension.manifest.languages.map((language) => language.id); // Unload from memory const { extensionManager } = await import("@/features/editor/extensions/manager"); try { + await Promise.all( + languageIds.map((languageId) => + extensionManager.unloadLanguageExtension(`${extensionId}:${languageId}`), + ), + ); + // Backward compatibility for previously loaded single-id providers. await extensionManager.unloadLanguageExtension(extensionId); } catch { // May not be loaded } - // Unload parser from memory - wasmParserLoader.unloadParser(languageId); - - // Delete from IndexedDB - await extensionInstaller.uninstallLanguage(languageId); + // Unload parsers from memory and delete from IndexedDB + await Promise.all( + languageIds.map(async (languageId) => { + wasmParserLoader.unloadParser(languageId); + await extensionInstaller.uninstallLanguage(languageId); + }), + ); + extensionRegistry.unregisterExtension(extensionId); // Remove from updates set set((state) => { @@ -679,6 +821,9 @@ async function initializeExtensionStoreImpl(): Promise { console.error("Failed to initialize WASM parser loader:", error); } + // Fetch extension manifests from CDN before loading available extensions + await initializeLanguagePackager(); + // Load available extensions first, then installed extensions // (installed extensions check needs available extensions to be loaded first) const { loadAvailableExtensions, loadInstalledExtensions, checkForUpdates } = diff --git a/src/extensions/themes/builtin/vitesse.json b/src/extensions/themes/builtin/vitesse.json index e96a103d..cbf0ea2c 100644 --- a/src/extensions/themes/builtin/vitesse.json +++ b/src/extensions/themes/builtin/vitesse.json @@ -15,8 +15,8 @@ "text-light": "#4e4f47", "text-lighter": "#6a737d", "border": "#f0f0f0", - "hover": "#f7f7f7", - "selected": "#f7f7f7", + "hover": "#e8e8e8", + "selected": "#e0e0e0", "accent": "#1c6b48" }, "syntax": { @@ -50,8 +50,8 @@ "text-light": "#4e4f47", "text-lighter": "#6a737d", "border": "#E7E5DB", - "hover": "#E7E5DB", - "selected": "#E7E5DB", + "hover": "#DBD9CF", + "selected": "#D0CEBF", "accent": "#1c6b48" }, "syntax": { diff --git a/src/extensions/themes/theme-initializer.ts b/src/extensions/themes/theme-initializer.ts index 4be54c19..4dc47e4a 100644 --- a/src/extensions/themes/theme-initializer.ts +++ b/src/extensions/themes/theme-initializer.ts @@ -72,7 +72,7 @@ export const initializeThemeSystem = async () => { tabSize: 2, lineNumbers: true, wordWrap: false, - theme: "one-dark", + theme: "vitesse-dark", }), updateSettings: () => {}, on: () => () => {}, diff --git a/src/extensions/themes/theme-loader.ts b/src/extensions/themes/theme-loader.ts index 06c5a5db..f8de80bb 100644 --- a/src/extensions/themes/theme-loader.ts +++ b/src/extensions/themes/theme-loader.ts @@ -81,23 +81,17 @@ export class ThemeLoader extends BaseThemeExtension { } } - /** - * Convert new JSON theme format to internal ThemeDefinition - * - colors → cssVariables with -- prefix - * - syntax → syntaxTokens with --syntax- prefix - * - appearance → isDark and category - */ private convertJsonToThemeDefinition(jsonTheme: JsonTheme): ThemeDefinition { - // Convert colors to CSS variables with -- prefix const cssVariables: Record = {}; for (const [key, value] of Object.entries(jsonTheme.colors)) { cssVariables[`--${key}`] = value; + cssVariables[`--color-${key}`] = value; } - // Convert syntax to syntaxTokens with --syntax- prefix const syntaxTokens: Record = {}; for (const [key, value] of Object.entries(jsonTheme.syntax)) { syntaxTokens[`--syntax-${key}`] = value; + syntaxTokens[`--color-syntax-${key}`] = value; } const isDark = jsonTheme.appearance === "dark"; diff --git a/src/extensions/types/extension-manifest.ts b/src/extensions/types/extension-manifest.ts index 41b1b927..432d99b3 100644 --- a/src/extensions/types/extension-manifest.ts +++ b/src/extensions/types/extension-manifest.ts @@ -86,6 +86,7 @@ export interface LanguageContribution { id: string; // Language ID (e.g., "rust") extensions: string[]; // File extensions (e.g., [".rs"]) aliases?: string[]; // Language aliases + filenames?: string[]; // Exact filenames (e.g., ["Dockerfile", ".bashrc"]) configuration?: string; // Path to language configuration firstLine?: string; // First line regex match } diff --git a/src/features/ai/components/chat/ai-chat.tsx b/src/features/ai/components/chat/ai-chat.tsx index c77b9941..725b31ba 100644 --- a/src/features/ai/components/chat/ai-chat.tsx +++ b/src/features/ai/components/chat/ai-chat.tsx @@ -2,7 +2,7 @@ import { memo, useCallback, useEffect, useRef, useState } from "react"; import ApiKeyModal from "@/features/ai/components/api-key-modal"; import { parseMentionsAndLoadFiles } from "@/features/ai/lib/file-mentions"; import type { AIChatProps, Message } from "@/features/ai/types/ai-chat"; -import { useFileSystemStore } from "@/features/file-system/controllers/store"; +import { useBufferStore } from "@/features/editor/stores/buffer-store"; import { useSettingsStore } from "@/features/settings/store"; import { useProjectStore } from "@/stores/project-store"; import { toast } from "@/stores/toast-store"; @@ -11,40 +11,68 @@ import { getChatCompletionStream, isAcpAgent } from "@/utils/ai-chat"; import { hasKairoAccessToken } from "@/utils/kairo-auth"; import type { ContextInfo } from "@/utils/types"; import { useChatActions, useChatState } from "../../hooks/use-chat-store"; -import { useAIChatStore } from "../../store/store"; import ChatHistorySidebar from "../history/sidebar"; import AIChatInputBar from "../input/chat-input-bar"; import { ChatHeader } from "./chat-header"; import { ChatMessages } from "./chat-messages"; -function collapseExactRepeatedResponse(content: string): string { - if (!content || content.length < 64) return content; - - const tryExact = (value: string): string => { - for (const repeatCount of [3, 2]) { - if (value.length % repeatCount !== 0) continue; - const unitLength = value.length / repeatCount; - if (unitLength < 24) continue; - const unit = value.slice(0, unitLength); - if (unit.repeat(repeatCount) === value) { - return unit; - } - } - return value; - }; +interface DirectAcpUiAction { + kind: "open_web_viewer" | "open_terminal"; + url?: string; + command?: string; +} + +const stripWrappingChars = (value: string): string => + value + .trim() + .replace(/^[`"'([{<\s]+/, "") + .replace(/[`"')\]}>.,!?;:\s]+$/, "") + .trim(); - const exact = tryExact(content); - if (exact !== content) return exact; +const normalizeWebUrl = (input: string): string | null => { + const cleaned = stripWrappingChars(input); + if (!cleaned) return null; - for (const separator of ["\n\n", "\r\n\r\n", "\n", "\r\n", " "]) { - const parts = content.split(separator); - if (parts.length === 2 && parts[0].length >= 24 && parts[0] === parts[1]) { - return parts[0]; + if (/^https?:\/\//i.test(cleaned)) { + try { + return new URL(cleaned).toString(); + } catch { + return null; } } - return content; -} + const hostLike = cleaned + .replace(/^www\./i, "www.") + .match(/^[a-z0-9.-]+\.[a-z]{2,}(?:\/[^\s]*)?$/i); + if (!hostLike) return null; + + try { + return new URL(`https://${cleaned}`).toString(); + } catch { + return null; + } +}; + +const parseDirectAcpUiAction = (message: string): DirectAcpUiAction | null => { + const text = message.trim(); + if (!text) return null; + + // Examples: "open linear.app on web", "open https://x.com in browser" + const webMatch = text.match(/\bopen\s+(.+?)\s+(?:on|in)\s+(?:web|browser|site)\b/i); + if (webMatch?.[1]) { + const url = normalizeWebUrl(webMatch[1]); + if (url) return { kind: "open_web_viewer", url }; + } + + // Examples: "open lazygit on terminal", "open npm run dev in terminal" + const terminalMatch = text.match(/\bopen\s+(.+?)\s+(?:on|in)\s+terminal\b/i); + if (terminalMatch?.[1]) { + const command = stripWrappingChars(terminalMatch[1]); + if (command) return { kind: "open_terminal", command }; + } + + return null; +}; const AIChat = memo(function AIChat({ className, @@ -59,16 +87,13 @@ const AIChat = memo(function AIChat({ const chatState = useChatState(); const chatActions = useChatActions(); - const messageQueueLength = useAIChatStore((state) => state.messageQueue.length); const messagesEndRef = useRef(null); const abortControllerRef = useRef(null); - const activeStreamRunIdRef = useRef(null); - const queueDrainInFlightRef = useRef(false); - const processMessageRef = useRef<(messageContent: string) => Promise>(async () => {}); const [permissionQueue, setPermissionQueue] = useState< Array<{ requestId: string; description: string; permissionType: string; resource: string }> >([]); + const [acpEvents, setAcpEvents] = useState>([]); useEffect(() => { if (activeBuffer) { @@ -81,6 +106,11 @@ const AIChat = memo(function AIChat({ chatActions.checkAllProviderApiKeys(); }, [settings.aiProviderId, chatActions.checkApiKey, chatActions.checkAllProviderApiKeys]); + // Clear ACP events when switching chats + useEffect(() => { + setAcpEvents([]); + }, [chatState.currentChatId]); + // Agent availability is now handled dynamically by the model-provider-selector component // No need to check Claude Code status on mount @@ -93,7 +123,7 @@ const AIChat = memo(function AIChat({ messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, []); - const buildContext = async (): Promise => { + const buildContext = async (agentId: string): Promise => { const selectedBuffers = buffers.filter((buffer) => chatState.selectedBufferIds.has(buffer.id)); // Build active buffer context, including web viewer content if applicable @@ -116,6 +146,7 @@ const AIChat = memo(function AIChat({ selectedProjectFiles: Array.from(chatState.selectedFilesPaths), projectRoot: rootFolderPath, providerId: settings.aiProviderId, + agentId, }; if (activeBuffer && !activeBuffer.isWebViewer) { @@ -170,27 +201,16 @@ const AIChat = memo(function AIChat({ abortControllerRef.current.abort(); abortControllerRef.current = null; } - activeStreamRunIdRef.current = null; chatActions.setIsTyping(false); chatActions.setStreamingMessageId(null); }; const processMessage = async (messageContent: string) => { const currentAgentId = chatActions.getCurrentAgentId(); - if (currentAgentId === "kairo-code") { - const isConnected = await hasKairoAccessToken(); - if (!isConnected) { - toast.error( - "Kairo Code requires Coline login first. Connect it in Settings > AI > Agent Authentication.", - ); - return; - } - } - const isAcp = isAcpAgent(currentAgentId); - const requiresApiKey = !isAcp && currentAgentId !== "kairo-code"; - // ACP agents don't use API keys; Kairo Code uses OAuth - if (!messageContent.trim() || (requiresApiKey && !chatState.hasApiKey)) return; + // For ACP agents (Claude Code, etc.), we don't need an API key + // For Custom API, we need an API key to be set + if (!messageContent.trim() || (!isAcp && !chatState.hasApiKey)) return; // Agents are started automatically by AcpStreamHandler when needed @@ -204,7 +224,7 @@ const AIChat = memo(function AIChat({ allProjectFiles, ); - const context = await buildContext(); + const context = await buildContext(currentAgentId); const userMessage: Message = { id: Date.now().toString(), content: messageContent.trim(), @@ -239,10 +259,39 @@ const AIChat = memo(function AIChat({ requestAnimationFrame(scrollToBottom); abortControllerRef.current = new AbortController(); - const streamRunId = `${Date.now()}-${Math.random().toString(36).slice(2)}`; - activeStreamRunIdRef.current = streamRunId; + let currentAssistantMessageId = assistantMessageId; try { + // Handle direct ACP UI intents locally so they are always reliable. + if (isAcp) { + const directAction = parseDirectAcpUiAction(messageContent); + if (directAction) { + const bufferActions = useBufferStore.getState().actions; + if (directAction.kind === "open_web_viewer" && directAction.url) { + bufferActions.openWebViewerBuffer(directAction.url); + chatActions.updateMessage(chatId, currentAssistantMessageId, { + content: `Opened ${directAction.url} in Athas web viewer.`, + isStreaming: false, + }); + } else if (directAction.kind === "open_terminal" && directAction.command) { + bufferActions.openTerminalBuffer({ + command: directAction.command, + name: directAction.command, + }); + chatActions.updateMessage(chatId, currentAssistantMessageId, { + content: `Opened terminal and ran \`${directAction.command}\`.`, + isStreaming: false, + }); + } + + chatActions.setIsTyping(false); + chatActions.setStreamingMessageId(null); + abortControllerRef.current = null; + processQueuedMessages(); + return; + } + } + const conversationContext = currentMessages .filter((msg) => msg.role !== "system") .map((msg) => ({ @@ -251,8 +300,9 @@ const AIChat = memo(function AIChat({ })); const enhancedMessage = processedMessage; - let currentAssistantMessageId = assistantMessageId; - const currentAgentId = chatActions.getCurrentAgentId(); + if (isAcp) { + setAcpEvents([]); + } await getChatCompletionStream( currentAgentId, @@ -261,7 +311,6 @@ const AIChat = memo(function AIChat({ enhancedMessage, context, (chunk: string) => { - if (activeStreamRunIdRef.current !== streamRunId) return; const currentMessages = chatActions.getCurrentMessages(); const currentMsg = currentMessages.find((m) => m.id === currentAssistantMessageId); chatActions.updateMessage(chatId, currentAssistantMessageId, { @@ -270,16 +319,7 @@ const AIChat = memo(function AIChat({ requestAnimationFrame(scrollToBottom); }, () => { - if (activeStreamRunIdRef.current !== streamRunId) return; - activeStreamRunIdRef.current = null; - const currentMessages = chatActions.getCurrentMessages(); - const currentMsg = currentMessages.find((m) => m.id === currentAssistantMessageId); - const nextContent = - isAcpAgent(currentAgentId) && currentMsg?.content - ? collapseExactRepeatedResponse(currentMsg.content) - : currentMsg?.content; chatActions.updateMessage(chatId, currentAssistantMessageId, { - ...(typeof nextContent === "string" ? { content: nextContent } : {}), isStreaming: false, }); chatActions.setIsTyping(false); @@ -287,9 +327,7 @@ const AIChat = memo(function AIChat({ abortControllerRef.current = null; processQueuedMessages(); }, - (error: string) => { - if (activeStreamRunIdRef.current !== streamRunId) return; - activeStreamRunIdRef.current = null; + (error: string, canReconnect?: boolean) => { console.error("Streaming error:", error); let errorTitle = "API Error"; @@ -300,29 +338,11 @@ const AIChat = memo(function AIChat({ const parts = error.split("|||"); const mainError = parts[0]; if (parts.length > 1) { - errorDetails = parts.slice(1).join("|||"); - } - - if (mainError.toLowerCase().includes("workspace_unavailable")) { - errorTitle = "Workspace Bridge Required"; - errorCode = "workspace_unavailable"; - errorMessage = - errorDetails || - "No workspace binding is available for this chat session. Connect the Athas workspace bridge and retry."; - } else if ( - mainError.toLowerCase().includes("kairo acp bridge error: stream") && - errorDetails.toLowerCase().includes("workspace_unavailable") - ) { - const [, workspaceMessage = ""] = errorDetails.split("|||"); - errorTitle = "Workspace Bridge Required"; - errorCode = "workspace_unavailable"; - errorMessage = - workspaceMessage || - "No workspace binding is available for this chat session. Connect the Athas workspace bridge and retry."; + errorDetails = parts[1]; } const codeMatch = mainError.match(/error:\s*(\d+)/i); - if (codeMatch && !errorCode) { + if (codeMatch) { errorCode = codeMatch[1]; if (errorCode === "429") { errorTitle = "Rate Limit Exceeded"; @@ -352,6 +372,11 @@ const AIChat = memo(function AIChat({ } } + if (canReconnect) { + errorTitle = "Connection Lost"; + errorCode = "RECONNECT"; + } + const formattedError = `[ERROR_BLOCK] title: ${errorTitle} code: ${errorCode} @@ -372,7 +397,6 @@ details: ${errorDetails || mainError} }, conversationContext, () => { - if (activeStreamRunIdRef.current !== streamRunId) return; const newMessageId = Date.now().toString(); const newAssistantMessage: Message = { id: newMessageId, @@ -387,107 +411,33 @@ details: ${errorDetails || mainError} chatActions.setStreamingMessageId(newMessageId); requestAnimationFrame(scrollToBottom); }, - (toolName: string, toolInput?: any, toolId?: string, event?: any) => { - if (activeStreamRunIdRef.current !== streamRunId) return; + (toolName: string, toolInput?: any) => { const currentMessages = chatActions.getCurrentMessages(); const currentMsg = currentMessages.find((m) => m.id === currentAssistantMessageId); - const existing = currentMsg?.toolCalls || []; - const nextToolCalls = toolId - ? existing.map((tc) => - tc.toolId === toolId - ? { - ...tc, - name: toolName, - input: toolInput, - kind: event?.kind, - status: event?.status, - content: event?.content, - locations: event?.locations, - } - : tc, - ) - : existing; - - const hasExistingById = Boolean(toolId && existing.some((tc) => tc.toolId === toolId)); - const appended = hasExistingById - ? nextToolCalls - : [ - ...nextToolCalls, - { - toolId, - name: toolName, - input: toolInput, - kind: event?.kind, - status: event?.status, - content: event?.content, - locations: event?.locations, - timestamp: new Date(), - }, - ]; - chatActions.updateMessage(chatId, currentAssistantMessageId, { isToolUse: true, toolName, - toolCalls: appended, + toolCalls: [ + ...(currentMsg?.toolCalls || []), + { + name: toolName, + input: toolInput, + timestamp: new Date(), + }, + ], }); }, - (toolName: string, event?: any) => { - if (activeStreamRunIdRef.current !== streamRunId) return; + (toolName: string) => { const currentMessages = chatActions.getCurrentMessages(); const currentMsg = currentMessages.find((m) => m.id === currentAssistantMessageId); - const normalizeTool = (value: string) => value.trim().toLowerCase(); - const isReadTool = (value: string) => { - const normalized = normalizeTool(value); - return ( - normalized === "read" || - normalized === "read_file" || - normalized === "readfile" || - normalized.includes("read") - ); - }; - - // Find the tool call that just completed - const completedToolCall = currentMsg?.toolCalls?.find( - (tc) => - (event?.toolId - ? tc.toolId === event.toolId - : normalizeTool(tc.name) === normalizeTool(toolName)) && !tc.isComplete, - ); - - // Auto-open Read files if setting is enabled - const readPath = - completedToolCall?.input?.file_path || completedToolCall?.input?.path || undefined; - if (isReadTool(toolName) && readPath) { - const { settings } = useSettingsStore.getState(); - if (settings.aiAutoOpenReadFiles) { - const { handleFileSelect } = useFileSystemStore.getState(); - handleFileSelect(readPath, false); - } - } - chatActions.updateMessage(chatId, currentAssistantMessageId, { toolCalls: currentMsg?.toolCalls?.map((tc) => - (event?.toolId - ? tc.toolId === event.toolId - : normalizeTool(tc.name) === normalizeTool(toolName)) && !tc.isComplete - ? { - ...tc, - isComplete: true, - status: event?.status || (event?.success === false ? "failed" : "completed"), - output: event?.output ?? tc.output, - error: event?.error ?? tc.error, - input: event?.input ?? tc.input, - kind: event?.kind ?? tc.kind, - content: event?.content ?? tc.content, - locations: event?.locations ?? tc.locations, - } - : tc, + tc.name === toolName && !tc.isComplete ? { ...tc, isComplete: true } : tc, ), }); }, (event) => { - if (activeStreamRunIdRef.current !== streamRunId) return; setPermissionQueue((prev) => [ ...prev, { @@ -498,16 +448,66 @@ details: ${errorDetails || mainError} }, ]); }, - undefined, + (event) => { + if (!isAcpAgent(currentAgentId)) return; + // Only show meaningful events, skip noisy ones + if (event.type === "content_chunk" || event.type === "session_complete") { + return; + } + const format = (): string | null => { + switch (event.type) { + case "tool_start": + return event.toolName; + case "tool_complete": + return null; // Skip, tool_start already shown + case "permission_request": + return null; // Handled separately with permission UI + case "prompt_complete": + return null; // Not useful to show + case "session_mode_update": + return event.modeState.currentModeId + ? `Mode: ${event.modeState.currentModeId}` + : null; + case "current_mode_update": + return `Mode: ${event.currentModeId}`; + case "slash_commands_update": + return null; // Not useful to show + case "status_changed": + return null; // Not useful to show + case "error": + return `Error: ${event.error}`; + case "ui_action": + return null; // Handled by acp-handler + } + }; + const text = format(); + if (text) { + setAcpEvents((prev) => [ + ...prev.slice(-19), + { id: `${Date.now()}-${event.type}`, text }, + ]); + } + }, chatState.mode, chatState.outputStyle, - chatId, - abortControllerRef.current?.signal, + (data: string, mediaType: string) => { + const currentMessages = chatActions.getCurrentMessages(); + const currentMsg = currentMessages.find((m) => m.id === currentAssistantMessageId); + chatActions.updateMessage(chatId, currentAssistantMessageId, { + images: [...(currentMsg?.images || []), { data, mediaType }], + }); + requestAnimationFrame(scrollToBottom); + }, + (uri: string, name: string | null) => { + const currentMessages = chatActions.getCurrentMessages(); + const currentMsg = currentMessages.find((m) => m.id === currentAssistantMessageId); + chatActions.updateMessage(chatId, currentAssistantMessageId, { + resources: [...(currentMsg?.resources || []), { uri, name }], + }); + requestAnimationFrame(scrollToBottom); + }, ); } catch (error) { - if (activeStreamRunIdRef.current === streamRunId) { - activeStreamRunIdRef.current = null; - } console.error("Failed to start streaming:", error); chatActions.updateMessage(chatId, assistantMessageId, { content: "Error: Failed to connect to AI service. Please check your API key and try again.", @@ -518,40 +518,27 @@ details: ${errorDetails || mainError} abortControllerRef.current = null; } }; - processMessageRef.current = processMessage; const processQueuedMessages = useCallback(async () => { - if (queueDrainInFlightRef.current) { - return; - } - if (chatState.isTyping || chatState.streamingMessageId) { return; } const nextMessage = chatActions.processNextMessage(); if (nextMessage) { - queueDrainInFlightRef.current = true; - try { - console.log("Processing next queued message:", nextMessage.content); - await new Promise((resolve) => setTimeout(resolve, 100)); - await processMessageRef.current(nextMessage.content); - } finally { - queueDrainInFlightRef.current = false; - } + console.log("Processing next queued message:", nextMessage.content); + await new Promise((resolve) => setTimeout(resolve, 500)); + await processMessage(nextMessage.content); } }, [chatState.isTyping, chatState.streamingMessageId, chatActions.processNextMessage]); - useEffect(() => { - if (messageQueueLength === 0) return; - if (chatState.isTyping || chatState.streamingMessageId) return; - - void processQueuedMessages(); - }, [messageQueueLength, chatState.isTyping, chatState.streamingMessageId, processQueuedMessages]); - const sendMessage = useCallback( async (messageContent: string) => { const currentAgentId = chatActions.getCurrentAgentId(); + const isAcp = isAcpAgent(currentAgentId); + // For ACP agents (Claude Code, etc.), we don't need an API key + if (!messageContent.trim() || (!isAcp && !chatState.hasApiKey)) return; + if (currentAgentId === "kairo-code") { const isConnected = await hasKairoAccessToken(); if (!isConnected) { @@ -562,10 +549,6 @@ details: ${errorDetails || mainError} } } - const isAcp = isAcpAgent(currentAgentId); - const requiresApiKey = !isAcp && currentAgentId !== "kairo-code"; - if (!messageContent.trim() || (requiresApiKey && !chatState.hasApiKey)) return; - chatActions.setInput(""); if (chatState.isTyping || chatState.streamingMessageId) { @@ -573,7 +556,7 @@ details: ${errorDetails || mainError} return; } - await processMessageRef.current(messageContent); + await processMessage(messageContent); }, [ chatState.hasApiKey, @@ -596,6 +579,13 @@ details: ${errorDetails || mainError} const handlePermission = async (approved: boolean) => { if (!currentPermission) return; try { + setAcpEvents((prev) => [ + ...prev.slice(-199), + { + id: `${Date.now()}-permission-response`, + text: `permission_response ${approved ? "allow" : "deny"}`, + }, + ]); await AcpStreamHandler.respondToPermission(currentPermission.requestId, approved); } finally { setPermissionQueue((prev) => prev.slice(1)); @@ -608,13 +598,13 @@ details: ${errorDetails || mainError} > -
- +
+
{currentPermission && ( -
-
+
+
permission: handlePermission(false)} - className="rounded border border-border bg-primary-bg px-2 py-1 text-text-lighter hover:bg-hover" + className="rounded-full border border-border bg-secondary-bg/80 px-2.5 py-1 text-text-lighter hover:bg-hover" > deny diff --git a/src/features/ai/components/chat/chat-header.tsx b/src/features/ai/components/chat/chat-header.tsx index 80a01321..22f89235 100644 --- a/src/features/ai/components/chat/chat-header.tsx +++ b/src/features/ai/components/chat/chat-header.tsx @@ -1,43 +1,9 @@ -import { invoke } from "@tauri-apps/api/core"; -import { Check, ChevronDown, Download, History, Plus, Terminal } from "lucide-react"; +import { History } from "lucide-react"; import { useEffect, useRef, useState } from "react"; -import type { AgentConfig } from "@/features/ai/types/acp"; -import { AGENT_OPTIONS, type AgentType } from "@/features/ai/types/ai-chat"; -import { toast } from "@/stores/toast-store"; +import { useUIState } from "@/stores/ui-state-store"; import Tooltip from "@/ui/tooltip"; -import { cn } from "@/utils/cn"; -import { hasKairoAccessToken } from "@/utils/kairo-auth"; import { useAIChatStore } from "../../store/store"; - -type PackageManager = "bun" | "npm" | "pnpm" | "yarn"; - -const AGENT_INSTALL_HINTS: Record< - string, - { package?: string; command?: string; type?: "npm" | "pip" | "shell" } -> = { - "claude-code": { package: "@zed-industries/claude-code-acp", type: "npm" }, - "codex-cli": { package: "@openai/codex", type: "npm" }, - "gemini-cli": { package: "@google/gemini-cli", type: "npm" }, - "kairo-code": { command: "bun add -g --no-cache @colineapp/kairo-code-acp", type: "shell" }, - "kimi-cli": { package: "kimi-cli", type: "npm" }, - opencode: { command: "curl -fsSL https://opencode.ai/install | bash", type: "shell" }, - "qwen-code": { command: "pip install qwen-code", type: "pip" }, -}; - -const getInstallCommand = (pkg: string, pm: PackageManager): string => { - switch (pm) { - case "bun": - return `bun add -g ${pkg}`; - case "pnpm": - return `pnpm add -g ${pkg}`; - case "yarn": - return `yarn global add ${pkg}`; - default: - return `npm install -g ${pkg}`; - } -}; - -const BUILT_IN_AGENTS = new Set(["custom"]); +import { UnifiedAgentSelector } from "../selectors/unified-agent-selector"; function EditableChatTitle({ title, @@ -94,7 +60,7 @@ function EditableChatTitle({ onChange={(e) => setEditValue(e.target.value)} onBlur={handleSave} onKeyDown={handleKeyDown} - className="rounded border-none bg-transparent px-1 py-0.5 font-medium text-text outline-none focus:bg-hover" + className="rounded-full border border-border bg-secondary-bg/80 px-2.5 py-1 font-medium text-text outline-none focus:border-accent/40 focus:bg-hover" style={{ minWidth: "100px", maxWidth: "200px" }} /> ); @@ -102,7 +68,7 @@ function EditableChatTitle({ return ( setIsEditing(true)} title="Click to rename chat" > @@ -114,196 +80,15 @@ function EditableChatTitle({ export function ChatHeader() { const currentChatId = useAIChatStore((state) => state.currentChatId); const getCurrentChat = useAIChatStore((state) => state.getCurrentChat); - const getCurrentAgentId = useAIChatStore((state) => state.getCurrentAgentId); const isChatHistoryVisible = useAIChatStore((state) => state.isChatHistoryVisible); const setIsChatHistoryVisible = useAIChatStore((state) => state.setIsChatHistoryVisible); - const createNewChat = useAIChatStore((state) => state.createNewChat); - const setSelectedAgentId = useAIChatStore((state) => state.setSelectedAgentId); - const changeCurrentChatAgent = useAIChatStore((state) => state.changeCurrentChatAgent); const updateChatTitle = useAIChatStore((state) => state.updateChatTitle); - const [isNewChatMenuOpen, setIsNewChatMenuOpen] = useState(false); - const [isAgentSelectorOpen, setIsAgentSelectorOpen] = useState(false); - const [installedAgents, setInstalledAgents] = useState>(new Set(BUILT_IN_AGENTS)); - const [packageManager, setPackageManager] = useState("bun"); - const [pmDropdownAgent, setPmDropdownAgent] = useState(null); - + const { openSettingsDialog } = useUIState(); const currentChat = getCurrentChat(); - const currentAgentId = getCurrentAgentId(); - const currentAgent = AGENT_OPTIONS.find((a) => a.id === currentAgentId); - - // Detect installed agents on mount - useEffect(() => { - const detectAgents = async () => { - try { - const availableAgents = await invoke("get_available_agents"); - const installed = new Set(BUILT_IN_AGENTS); - for (const agent of availableAgents) { - if (agent.installed) { - installed.add(agent.id); - } - } - setInstalledAgents(installed); - } catch (error) { - console.error("Failed to detect agents:", error); - } - }; - - detectAgents(); - }, []); - - const ensureKairoAuthenticated = async (): Promise => { - try { - const connected = await hasKairoAccessToken(); - if (connected) return true; - } catch (error) { - console.error("Failed to verify Kairo OAuth status:", error); - } - - toast.error( - "Kairo Code requires Coline login first. Connect it in Settings > AI > Agent Authentication.", - ); - return false; - }; - - const handleNewChat = async (agentId: AgentType) => { - if (agentId === "kairo-code" && !(await ensureKairoAuthenticated())) { - setIsNewChatMenuOpen(false); - return; - } - - setIsNewChatMenuOpen(false); - setSelectedAgentId(agentId); - - // Stop any running ACP agent before starting a new chat - if (currentAgent?.isAcp) { - try { - await invoke("stop_acp_agent"); - } catch (error) { - console.error("Failed to stop current agent:", error); - } - } - - const newChatId = createNewChat(agentId); - return newChatId; - }; - - const handleAgentChange = async (agentId: AgentType) => { - if (agentId === currentAgentId) { - setIsAgentSelectorOpen(false); - return; - } - - if (agentId === "kairo-code" && !(await ensureKairoAuthenticated())) { - setIsAgentSelectorOpen(false); - return; - } - - setIsAgentSelectorOpen(false); - setSelectedAgentId(agentId); - - // Stop any running ACP agent before switching - if (currentAgent?.isAcp) { - try { - await invoke("stop_acp_agent"); - } catch (error) { - console.error("Failed to stop current agent:", error); - } - } - - // Create a new chat with the selected agent - changeCurrentChatAgent(agentId); - }; - - const handleInstall = (agentId: string, agentName: string, e: React.MouseEvent) => { - e.stopPropagation(); - const hint = AGENT_INSTALL_HINTS[agentId]; - if (!hint) return; - - let command: string; - if (hint.type === "npm" && hint.package) { - command = getInstallCommand(hint.package, packageManager); - } else if (hint.command) { - command = hint.command; - } else { - return; - } - - // Create a new terminal and run the install command - window.dispatchEvent( - new CustomEvent("create-terminal-with-command", { - detail: { command, name: `Install ${agentName}` }, - }), - ); - - setIsNewChatMenuOpen(false); - }; - - const handleSelectPm = (pm: PackageManager, e: React.MouseEvent) => { - e.stopPropagation(); - setPackageManager(pm); - setPmDropdownAgent(null); - }; - - const packageManagers: PackageManager[] = ["bun", "npm", "pnpm", "yarn"]; return ( -
- {/* Agent selector - clickable to switch agents */} -
- - - - - {isAgentSelectorOpen && ( - <> -
setIsAgentSelectorOpen(false)} /> -
-
Switch to...
- {AGENT_OPTIONS.map((agent) => { - const isInstalled = installedAgents.has(agent.id); - const isCurrentAgent = agent.id === currentAgentId; - const canSelect = true; - - return ( - - ); - })} -
-
- Switching creates a new chat -
-
- - )} -
- +
{currentChatId ? ( - {/* New Chat with agent dropdown */} -
- - - - - {isNewChatMenuOpen && ( - <> -
setIsNewChatMenuOpen(false)} /> -
-
New Chat with...
- {AGENT_OPTIONS.map((agent) => { - const isInstalled = installedAgents.has(agent.id); - const installHint = AGENT_INSTALL_HINTS[agent.id]; - const isPmOpen = pmDropdownAgent === agent.id; - const showPmDropdown = - !isInstalled && agent.id !== "custom" && installHint?.type === "npm"; - const canSelect = true; - - return ( -
- - - {/* Install button for non-installed agents */} - {!isInstalled && agent.id !== "custom" && installHint && ( -
- - {showPmDropdown && ( -
- - {isPmOpen && ( -
- {packageManagers.map((pm) => ( - - ))} -
- )} -
- )} -
- )} -
- ); - })} -
- - )} -
+ openSettingsDialog("ai")} />
); } diff --git a/src/features/ai/components/chat/chat-message.tsx b/src/features/ai/components/chat/chat-message.tsx index b1f2fdc4..c18a5b22 100644 --- a/src/features/ai/components/chat/chat-message.tsx +++ b/src/features/ai/components/chat/chat-message.tsx @@ -104,12 +104,12 @@ export const ChatMessage = memo(function ChatMessage({ if (message.role === "user") { return (
-
+
{message.content}
); })} + {acpEvents && acpEvents.length > 0 && ( +
+
+ {acpEvents.map((event) => ( +
+ {event.text} +
+ ))} +
+
+ )}
); diff --git a/src/features/ai/components/github-copilot-settings.tsx b/src/features/ai/components/github-copilot-settings.tsx deleted file mode 100644 index 6d0e8a3d..00000000 --- a/src/features/ai/components/github-copilot-settings.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { AlertCircle, Zap } from "lucide-react"; -import { useUIState } from "@/stores/ui-state-store"; -import Button from "@/ui/button"; - -const GitHubCopilotSettings = () => { - // Get data from stores - const { isGitHubCopilotSettingsVisible, setIsGitHubCopilotSettingsVisible } = useUIState(); - - const isVisible = isGitHubCopilotSettingsVisible; - const onClose = () => setIsGitHubCopilotSettingsVisible(false); - - if (!isVisible) { - return null; - } - - return ( -
-
- {/* Header */} -
- -

GitHub Copilot Integration

-
- -
- - {/* Content */} -
-
- GitHub Copilot integration uses official GitHub authentication through the code Editor. -
- - {/* Coming Soon Notice */} -
-
- - Coming Soon -
-
- GitHub Copilot integration is currently in development. GitHub Copilot does not - support API key authentication. It requires OAuth-based authentication through - official GitHub channels. -
-
- - {/* Information */} -
-
- How GitHub Copilot authentication works: -
-
    -
  • Requires a GitHub Copilot subscription
  • -
  • Uses OAuth authentication with GitHub
  • -
  • Integrates through official IDE extensions
  • -
  • Does not support standalone API keys
  • -
-
- - {/* Actions */} -
- -
-
-
-
- ); -}; - -export default GitHubCopilotSettings; diff --git a/src/features/ai/components/input/chat-input-bar.tsx b/src/features/ai/components/input/chat-input-bar.tsx index bf34b361..509f36b7 100644 --- a/src/features/ai/components/input/chat-input-bar.tsx +++ b/src/features/ai/components/input/chat-input-bar.tsx @@ -1,12 +1,10 @@ -import { Send, Slash, Square } from "lucide-react"; +import { Send, Slash, Square, X } from "lucide-react"; import { memo, useCallback, useEffect, useRef, useState } from "react"; import { useAIChatStore } from "@/features/ai/store/store"; import type { SlashCommand } from "@/features/ai/types/acp"; import type { AIChatInputBarProps } from "@/features/ai/types/ai-chat"; -import { getModelById } from "@/features/ai/types/providers"; import { useEditorSettingsStore } from "@/features/editor/stores/settings-store"; import { useSettingsStore } from "@/features/settings/store"; -import { useUIState } from "@/stores/ui-state-store"; import Button from "@/ui/button"; import Dropdown from "@/ui/dropdown"; import { cn } from "@/utils/cn"; @@ -21,8 +19,6 @@ import { FileMentionDropdown } from "../mentions/file-mention-dropdown"; import { SlashCommandDropdown } from "../mentions/slash-command-dropdown"; import { ChatModeSelector } from "../selectors/chat-mode-selector"; import { ContextSelector } from "../selectors/context-selector"; -import { ModelSelectorDropdown } from "../selectors/model-selector-dropdown"; -import { SessionModeSelector } from "../selectors/session-mode-selector"; const KAIRO_GPT_REASONING_OPTIONS = [ { value: "0", label: "None" }, @@ -71,9 +67,9 @@ const AIChatInputBar = memo(function AIChatInputBar({ const [isLoadingKairoModels, setIsLoadingKairoModels] = useState(false); // Get state from stores with optimized selectors - const { settings, updateSetting } = useSettingsStore(); - const { openSettingsDialog } = useUIState(); const { fontSize, fontFamily } = useEditorSettingsStore(); + const settings = useSettingsStore((state) => state.settings); + const updateSetting = useSettingsStore((state) => state.updateSetting); // Get state from store - DO NOT subscribe to 'input' to avoid re-renders on every keystroke const isTyping = useAIChatStore((state) => state.isTyping); @@ -94,6 +90,7 @@ const AIChatInputBar = memo(function AIChatInputBar({ // ACP agents don't need API key (they handle their own auth) const isInputEnabled = isCustomAgent ? hasApiKey : true; + const isStreaming = isTyping && !!streamingMessageId; useEffect(() => { if (!isKairoAgent) { @@ -149,6 +146,16 @@ const AIChatInputBar = memo(function AIChatInputBar({ const selectPreviousSlashCommand = useAIChatStore((state) => state.selectPreviousSlashCommand); const getFilteredSlashCommands = useAIChatStore((state) => state.getFilteredSlashCommands); + // Pasted images state and actions + const pastedImages = useAIChatStore((state) => state.pastedImages); + const addPastedImage = useAIChatStore((state) => state.addPastedImage); + const removePastedImage = useAIChatStore((state) => state.removePastedImage); + const clearPastedImages = useAIChatStore((state) => state.clearPastedImages); + + // Computed state for send button + const hasImages = pastedImages.length > 0; + const isSendDisabled = isStreaming ? false : (!hasInputText && !hasImages) || !isInputEnabled; + // Highly optimized function to get plain text from contentEditable div const getPlainTextFromDiv = useCallback(() => { if (!inputRef.current) return ""; @@ -490,6 +497,72 @@ const AIChatInputBar = memo(function AIChatInputBar({ slashCommandState.active, ]); + // Handle paste - strip HTML formatting, keep only plain text. Images are added to preview. + const handlePaste = useCallback( + (e: React.ClipboardEvent) => { + const clipboardData = e.clipboardData; + if (!clipboardData) return; + + // Check for images first + const items = clipboardData.items; + let hasImage = false; + + for (let i = 0; i < items.length; i++) { + if (items[i].type.startsWith("image/")) { + hasImage = true; + e.preventDefault(); + + const file = items[i].getAsFile(); + if (file) { + const reader = new FileReader(); + reader.onload = (event) => { + const dataUrl = event.target?.result as string; + if (dataUrl) { + addPastedImage({ + id: `img-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`, + dataUrl, + name: file.name || `image-${Date.now()}.png`, + size: file.size, + }); + } + }; + reader.readAsDataURL(file); + } + } + } + + // If there was an image, don't process text + if (hasImage) return; + + // For text content, prevent default and insert plain text only + e.preventDefault(); + + // Get plain text from clipboard + const plainText = clipboardData.getData("text/plain"); + if (!plainText) return; + + // Insert plain text at cursor position + const selection = window.getSelection(); + if (!selection || selection.rangeCount === 0) return; + + const range = selection.getRangeAt(0); + range.deleteContents(); + + const textNode = document.createTextNode(plainText); + range.insertNode(textNode); + + // Move cursor to end of inserted text + range.setStartAfter(textNode); + range.setEndAfter(textNode); + selection.removeAllRanges(); + selection.addRange(range); + + // Trigger input change handler to update state + handleInputChange(); + }, + [handleInputChange, addPastedImage], + ); + // Handle file mention selection const handleFileMentionSelect = useCallback( (file: any) => { @@ -612,7 +685,9 @@ const AIChatInputBar = memo(function AIChatInputBar({ const handleSendMessage = async () => { const currentInput = useAIChatStore.getState().input; - if (!currentInput.trim() || !isInputEnabled) return; + const currentImages = useAIChatStore.getState().pastedImages; + const hasContent = currentInput.trim() || currentImages.length > 0; + if (!hasContent || !isInputEnabled) return; // Trigger send animation setIsSendAnimating(true); @@ -620,14 +695,15 @@ const AIChatInputBar = memo(function AIChatInputBar({ // Reset animation after the flying animation completes setTimeout(() => setIsSendAnimating(false), 800); - // Clear input immediately after send is triggered + // Clear input and images immediately after send is triggered setInput(""); setHasInputText(false); + clearPastedImages(); if (inputRef.current) { inputRef.current.innerHTML = ""; } - // Send the captured message + // Send the captured message (TODO: include images in message) await onSendMessage(currentInput); }; @@ -715,15 +791,41 @@ const AIChatInputBar = memo(function AIChatInputBar({ return (
-
+
+ {/* Pasted images preview */} + {pastedImages.length > 0 && ( +
+ {pastedImages.map((image) => ( +
+ {image.name} + +
+ ))} +
+ )} + {/* Input area */}
{/* Bottom row: Context + Mode + Style + Model/Agent + Send */} -
-
+
+
0 && ( -
+
{queueCount}
)}
-
+
{/* Chat mode selector */} @@ -867,65 +969,37 @@ const AIChatInputBar = memo(function AIChatInputBar({ showSlashCommands(position, ""); } }} - className="flex h-7 items-center gap-1 rounded px-1.5 text-text-lighter text-xs hover:bg-hover hover:text-text" + className="flex h-8 w-8 items-center justify-center rounded-full border border-border bg-secondary-bg/80 text-text-lighter transition-colors hover:bg-hover hover:text-text" title="Show slash commands" > )} - {/* Model selector dropdown - only shown for custom agent */} - {isCustomAgent ? ( - { - const { dynamicModels } = useAIChatStore.getState(); - const providerModels = dynamicModels[settings.aiProviderId]; - const dynamicModel = providerModels?.find((m) => m.id === settings.aiModelId); - if (dynamicModel) return dynamicModel.name; - - return ( - getModelById(settings.aiProviderId, settings.aiModelId)?.name || "Select Model" - ); - })()} - onSelect={(providerId, modelId) => { - updateSetting("aiProviderId", providerId); - updateSetting("aiModelId", modelId); - }} - onOpenSettings={() => openSettingsDialog("ai")} - hasApiKey={(providerId) => useAIChatStore.getState().hasProviderApiKey(providerId)} - /> - ) : ( - - )} - {isExpanded && step.description && ( -
+
)} diff --git a/src/features/ai/components/selectors/chat-mode-selector.tsx b/src/features/ai/components/selectors/chat-mode-selector.tsx index af06cd55..803b05c8 100644 --- a/src/features/ai/components/selectors/chat-mode-selector.tsx +++ b/src/features/ai/components/selectors/chat-mode-selector.tsx @@ -32,7 +32,7 @@ export const ChatModeSelector = memo(function ChatModeSelector({ return (
@@ -42,16 +42,18 @@ export const ChatModeSelector = memo(function ChatModeSelector({ return ( ); diff --git a/src/features/ai/components/selectors/context-selector.tsx b/src/features/ai/components/selectors/context-selector.tsx index d4d453c0..51653cc3 100644 --- a/src/features/ai/components/selectors/context-selector.tsx +++ b/src/features/ai/components/selectors/context-selector.tsx @@ -249,12 +249,12 @@ export function ContextSelector({ }, [handleKeyDown]); return ( -
+
+ )} />
); diff --git a/src/features/ai/components/selectors/unified-agent-selector.tsx b/src/features/ai/components/selectors/unified-agent-selector.tsx new file mode 100644 index 00000000..fe5467fc --- /dev/null +++ b/src/features/ai/components/selectors/unified-agent-selector.tsx @@ -0,0 +1,486 @@ +import { invoke } from "@tauri-apps/api/core"; +import { Check, ChevronDown, Key, Plus, Search, Terminal } from "lucide-react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useAIChatStore } from "@/features/ai/store/store"; +import type { AgentConfig } from "@/features/ai/types/acp"; +import { AGENT_OPTIONS, type AgentType } from "@/features/ai/types/ai-chat"; +import { getAvailableProviders } from "@/features/ai/types/providers"; +import { useSettingsStore } from "@/features/settings/store"; +import { cn } from "@/utils/cn"; +import { getProvider } from "@/utils/providers"; + +interface UnifiedAgentSelectorProps { + variant?: "header" | "input"; + onOpenSettings?: () => void; +} + +export function UnifiedAgentSelector({ + variant = "header", + onOpenSettings, +}: UnifiedAgentSelectorProps) { + const [isOpen, setIsOpen] = useState(false); + const [search, setSearch] = useState(""); + const [selectedIndex, setSelectedIndex] = useState(0); + const [installedAgents, setInstalledAgents] = useState>(new Set(["custom"])); + const [activeSection, setActiveSection] = useState<"agents" | "models">("agents"); + + const { settings, updateSetting } = useSettingsStore(); + const { dynamicModels, setDynamicModels } = useAIChatStore(); + const getCurrentAgentId = useAIChatStore((state) => state.getCurrentAgentId); + const setSelectedAgentId = useAIChatStore((state) => state.setSelectedAgentId); + const createNewChat = useAIChatStore((state) => state.createNewChat); + const changeCurrentChatAgent = useAIChatStore((state) => state.changeCurrentChatAgent); + const hasProviderApiKey = useAIChatStore((state) => state.hasProviderApiKey); + + const triggerRef = useRef(null); + const dropdownRef = useRef(null); + const inputRef = useRef(null); + + const currentAgentId = getCurrentAgentId(); + const currentAgent = AGENT_OPTIONS.find((a) => a.id === currentAgentId); + const isCustomAgent = currentAgentId === "custom"; + const providers = getAvailableProviders(); + + // Get current model name for custom agent + const currentModelName = useMemo(() => { + if (!isCustomAgent) return null; + const providerModels = dynamicModels[settings.aiProviderId]; + const dynamicModel = providerModels?.find((m) => m.id === settings.aiModelId); + if (dynamicModel) return dynamicModel.name; + const provider = providers.find((p) => p.id === settings.aiProviderId); + const staticModel = provider?.models.find((m) => m.id === settings.aiModelId); + return staticModel?.name || settings.aiModelId; + }, [isCustomAgent, dynamicModels, settings.aiProviderId, settings.aiModelId, providers]); + + // Detect installed agents + useEffect(() => { + const detectAgents = async () => { + try { + const availableAgents = await invoke("get_available_agents"); + const installed = new Set(["custom"]); + for (const agent of availableAgents) { + if (agent.installed) { + installed.add(agent.id); + } + } + setInstalledAgents(installed); + } catch { + // Silent fail + } + }; + detectAgents(); + }, []); + + // Fetch dynamic models for providers + useEffect(() => { + const fetchModels = async () => { + for (const provider of providers) { + if (dynamicModels[provider.id]?.length > 0) continue; + if (provider.requiresApiKey) continue; + const providerInstance = getProvider(provider.id); + if (providerInstance?.getModels) { + try { + const models = await providerInstance.getModels(); + if (models.length > 0) { + setDynamicModels(provider.id, models); + } + } catch { + // Silent fail + } + } + } + }; + fetchModels(); + }, [providers, dynamicModels, setDynamicModels]); + + // Build filtered items list + const filteredItems = useMemo(() => { + const items: Array<{ + type: "section" | "agent" | "provider" | "model"; + id: string; + name: string; + providerId?: string; + isInstalled?: boolean; + isCurrent?: boolean; + requiresApiKey?: boolean; + hasKey?: boolean; + }> = []; + + const searchLower = search.toLowerCase(); + + // Add agents section + if (activeSection === "agents" || !search) { + const matchingAgents = AGENT_OPTIONS.filter( + (agent) => + !search || + agent.name.toLowerCase().includes(searchLower) || + agent.description.toLowerCase().includes(searchLower), + ); + + if (matchingAgents.length > 0) { + items.push({ type: "section", id: "agents-section", name: "Agents" }); + for (const agent of matchingAgents) { + items.push({ + type: "agent", + id: agent.id, + name: agent.name, + isInstalled: installedAgents.has(agent.id), + isCurrent: agent.id === currentAgentId, + }); + } + } + } + + // Add models section (only for custom agent view or when searching) + if ((activeSection === "models" || search) && isCustomAgent) { + for (const provider of providers) { + const providerHasKey = !provider.requiresApiKey || hasProviderApiKey(provider.id); + const models = dynamicModels[provider.id] || provider.models; + + const matchingModels = models.filter( + (model) => + !search || + provider.name.toLowerCase().includes(searchLower) || + model.name.toLowerCase().includes(searchLower) || + model.id.toLowerCase().includes(searchLower), + ); + + if ( + matchingModels.length > 0 || + (!search && provider.name.toLowerCase().includes(searchLower)) + ) { + items.push({ + type: "provider", + id: `provider-${provider.id}`, + name: provider.name, + providerId: provider.id, + requiresApiKey: provider.requiresApiKey, + hasKey: providerHasKey, + }); + + if (providerHasKey) { + for (const model of matchingModels) { + items.push({ + type: "model", + id: model.id, + name: model.name, + providerId: provider.id, + isCurrent: settings.aiProviderId === provider.id && settings.aiModelId === model.id, + }); + } + } + } + } + } + + return items; + }, [ + search, + activeSection, + installedAgents, + currentAgentId, + isCustomAgent, + providers, + dynamicModels, + hasProviderApiKey, + settings.aiProviderId, + settings.aiModelId, + ]); + + const selectableItems = useMemo( + () => filteredItems.filter((item) => item.type === "agent" || item.type === "model"), + [filteredItems], + ); + + useEffect(() => { + if (isOpen && inputRef.current) { + inputRef.current.focus(); + } + }, [isOpen]); + + useEffect(() => { + setSelectedIndex(0); + }, [search, activeSection]); + + useEffect(() => { + if (!isOpen) { + setSearch(""); + setSelectedIndex(0); + setActiveSection("agents"); + } + }, [isOpen]); + + const handleAgentChange = useCallback( + async (agentId: AgentType) => { + if (variant !== "header" && agentId === currentAgentId) { + setIsOpen(false); + return; + } + + // In header variant, selecting Custom API should show models tab + if (variant === "header" && agentId === "custom") { + setSelectedAgentId(agentId); + setActiveSection("models"); + return; + } + + setIsOpen(false); + setSelectedAgentId(agentId); + + const currentAgentInfo = AGENT_OPTIONS.find((a) => a.id === currentAgentId); + if (currentAgentInfo?.isAcp) { + try { + await invoke("stop_acp_agent"); + } catch { + // Silent fail + } + } + + if (variant === "header") { + createNewChat(agentId); + } else { + changeCurrentChatAgent(agentId); + } + }, + [variant, currentAgentId, setSelectedAgentId, changeCurrentChatAgent, createNewChat], + ); + + const handleModelSelect = useCallback( + (providerId: string, modelId: string) => { + updateSetting("aiProviderId", providerId); + updateSetting("aiModelId", modelId); + setIsOpen(false); + if (variant === "header") { + createNewChat(); + } + }, + [updateSetting, variant, createNewChat], + ); + + const handleKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (!isOpen) return; + + switch (e.key) { + case "ArrowDown": + e.preventDefault(); + setSelectedIndex((prev) => Math.min(prev + 1, selectableItems.length - 1)); + break; + case "ArrowUp": + e.preventDefault(); + setSelectedIndex((prev) => Math.max(prev - 1, 0)); + break; + case "Enter": + e.preventDefault(); + if (selectableItems[selectedIndex]) { + const item = selectableItems[selectedIndex]; + if (item.type === "agent") { + handleAgentChange(item.id as AgentType); + } else if (item.type === "model" && item.providerId) { + handleModelSelect(item.providerId, item.id); + } + } + break; + case "Escape": + e.preventDefault(); + setIsOpen(false); + break; + case "Tab": + e.preventDefault(); + if (isCustomAgent) { + setActiveSection((prev) => (prev === "agents" ? "models" : "agents")); + } + break; + } + }, + [isOpen, selectableItems, selectedIndex, handleAgentChange, handleModelSelect, isCustomAgent], + ); + + let selectableIndex = -1; + + return ( +
+ {variant === "header" ? ( + + ) : ( + + )} + + {isOpen && ( + <> +
setIsOpen(false)} /> +
+ {/* Search */} +
+
+ + setSearch(e.target.value)} + onKeyDown={handleKeyDown} + placeholder="Search agents or models..." + className="flex-1 bg-transparent text-text text-xs outline-none placeholder:text-text-lighter" + /> +
+
+ + {/* Section tabs (only for custom agent) */} + {isCustomAgent && !search && ( +
+ + +
+ )} + + {/* Items */} +
+ {filteredItems.length === 0 ? ( +
No results found
+ ) : ( + filteredItems.map((item) => { + if (item.type === "section") { + return ( +
+ {item.name} +
+ ); + } + + if (item.type === "provider") { + return ( +
+ {item.name} + {item.requiresApiKey && !item.hasKey && ( + + )} +
+ ); + } + + if (item.type === "agent") { + selectableIndex++; + const itemIndex = selectableIndex; + const isSelected = itemIndex === selectedIndex; + + return ( + + ); + } + + if (item.type === "model") { + selectableIndex++; + const itemIndex = selectableIndex; + const isSelected = itemIndex === selectedIndex; + + return ( + + ); + } + + return null; + }) + )} +
+
+ + )} +
+ ); +} diff --git a/src/features/ai/store/store.ts b/src/features/ai/store/store.ts index 1cd65808..32eb7cb2 100644 --- a/src/features/ai/store/store.ts +++ b/src/features/ai/store/store.ts @@ -29,6 +29,7 @@ export const useAIChatStore = create()( currentChatId: null, selectedAgentId: "custom" as AgentType, // Default to custom (API-based) input: "", + pastedImages: [], isTyping: false, streamingMessageId: null, selectedBufferIds: new Set(), @@ -140,6 +141,18 @@ export const useAIChatStore = create()( set((state) => { state.input = input; }), + addPastedImage: (image) => + set((state) => { + state.pastedImages = [...state.pastedImages, image]; + }), + removePastedImage: (imageId) => + set((state) => { + state.pastedImages = state.pastedImages.filter((img) => img.id !== imageId); + }), + clearPastedImages: () => + set((state) => { + state.pastedImages = []; + }), setIsTyping: (isTyping) => set((state) => { state.isTyping = isTyping; @@ -768,19 +781,7 @@ export const useAIChatStore = create()( }, applyDefaultSettings: () => { - // Import settings store dynamically to avoid circular dependency - import("@/features/settings/store").then(({ useSettingsStore }) => { - const settings = useSettingsStore.getState().settings; - set((state) => { - // Apply default output style if not already set or different - if ( - settings.aiDefaultOutputStyle && - settings.aiDefaultOutputStyle !== state.outputStyle - ) { - state.outputStyle = settings.aiDefaultOutputStyle; - } - }); - }); + // No-op: settings that were applied here have been removed }, // Helper getters @@ -796,21 +797,26 @@ export const useAIChatStore = create()( }, }), { - name: "athas-ai-chat-settings-v6", - version: 2, + name: "athas-ai-chat-settings-v7", + version: 3, partialize: (state) => ({ mode: state.mode, outputStyle: state.outputStyle, selectedAgentId: state.selectedAgentId, + sessionModeState: state.sessionModeState, }), merge: (persistedState, currentState) => produce(currentState, (draft) => { - // Only merge mode, outputStyle, and selectedAgentId from localStorage + // Only merge mode, outputStyle, selectedAgentId, and sessionModeState from localStorage // Chats are loaded from SQLite separately if (persistedState) { draft.mode = (persistedState as any).mode || "chat"; draft.outputStyle = (persistedState as any).outputStyle || "default"; draft.selectedAgentId = (persistedState as any).selectedAgentId || "custom"; + draft.sessionModeState = (persistedState as any).sessionModeState || { + currentModeId: null, + availableModes: [], + }; } }), }, diff --git a/src/features/ai/store/types.ts b/src/features/ai/store/types.ts index 494fbaf1..c1767a76 100644 --- a/src/features/ai/store/types.ts +++ b/src/features/ai/store/types.ts @@ -12,12 +12,20 @@ export interface QueuedMessage { timestamp: Date; } +export interface PastedImage { + id: string; + dataUrl: string; + name: string; + size: number; +} + export interface AIChatState { // Single session state chats: Chat[]; currentChatId: string | null; selectedAgentId: AgentType; // Current agent selection for new chats input: string; + pastedImages: PastedImage[]; isTyping: boolean; streamingMessageId: string | null; selectedBufferIds: Set; @@ -82,6 +90,9 @@ export interface AIChatActions { // Input actions setInput: (input: string) => void; + addPastedImage: (image: PastedImage) => void; + removePastedImage: (imageId: string) => void; + clearPastedImages: () => void; setIsTyping: (isTyping: boolean) => void; setStreamingMessageId: (streamingMessageId: string | null) => void; toggleBufferSelection: (bufferId: string) => void; diff --git a/src/features/ai/types/acp.ts b/src/features/ai/types/acp.ts index b8ea3ab5..88f33bfb 100644 --- a/src/features/ai/types/acp.ts +++ b/src/features/ai/types/acp.ts @@ -52,6 +52,11 @@ export type StopReason = "end_turn" | "max_tokens" | "max_turn_requests" | "refu export type AcpToolStatus = "pending" | "in_progress" | "completed" | "failed"; +// UI action types that agents can request +export type UiAction = + | { action: "open_web_viewer"; url: string } + | { action: "open_terminal"; command: string | null }; + export type AcpEvent = | { type: "content_chunk"; @@ -123,4 +128,9 @@ export type AcpEvent = type: "prompt_complete"; sessionId: string; stopReason: StopReason; + } + | { + type: "ui_action"; + sessionId: string; + action: UiAction; }; diff --git a/src/features/ai/types/ai-chat.ts b/src/features/ai/types/ai-chat.ts index bfca62af..63b1ddb9 100644 --- a/src/features/ai/types/ai-chat.ts +++ b/src/features/ai/types/ai-chat.ts @@ -16,6 +16,16 @@ export interface ToolCall { isComplete?: boolean; } +export interface ImageContent { + data: string; + mediaType: string; +} + +export interface ResourceContent { + uri: string; + name: string | null; +} + export interface Message { id: string; content: string; @@ -25,6 +35,8 @@ export interface Message { isToolUse?: boolean; toolName?: string; toolCalls?: ToolCall[]; + images?: ImageContent[]; + resources?: ResourceContent[]; } // Agent types for AI chat @@ -112,6 +124,7 @@ export interface ContextInfo { projectRoot?: string; language?: string; providerId?: string; + agentId?: AgentType; } export interface AIChatProps { diff --git a/src/features/command-bar/components/file-list-item.tsx b/src/features/command-bar/components/file-list-item.tsx index 0940f214..f86f0f37 100644 --- a/src/features/command-bar/components/file-list-item.tsx +++ b/src/features/command-bar/components/file-list-item.tsx @@ -1,4 +1,5 @@ -import { ClockIcon, File } from "lucide-react"; +import { ClockIcon } from "lucide-react"; +import { FileIcon } from "@/features/file-explorer/components/file-icon"; import { CommandItem } from "@/ui/command"; import { getDirectoryPath } from "@/utils/path-helpers"; import type { FileCategory, FileItem } from "../types/command-bar"; @@ -30,10 +31,7 @@ export const FileListItem = ({ isSelected={isSelected} className="ui-font" > - +
{file.name} diff --git a/src/features/command-bar/hooks/use-file-loader.ts b/src/features/command-bar/hooks/use-file-loader.ts index e3b43338..c3796944 100644 --- a/src/features/command-bar/hooks/use-file-loader.ts +++ b/src/features/command-bar/hooks/use-file-loader.ts @@ -1,13 +1,11 @@ import { useEffect, useState } from "react"; import { useFileSystemStore } from "@/features/file-system/controllers/store"; -import { useSettingsStore } from "@/features/settings/store"; import type { FileItem } from "../types/command-bar"; import { shouldIgnoreFile } from "../utils/file-filtering"; export const useFileLoader = (isVisible: boolean) => { const getAllProjectFiles = useFileSystemStore((state) => state.getAllProjectFiles); const rootFolderPath = useFileSystemStore((state) => state.rootFolderPath); - const commandBarFileLimit = useSettingsStore((state) => state.settings.commandBarFileLimit); const [files, setFiles] = useState([]); const [isLoadingFiles, setIsLoadingFiles] = useState(false); const [isIndexing, setIsIndexing] = useState(false); @@ -43,7 +41,7 @@ export const useFileLoader = (isVisible: boolean) => { }; loadFiles(); - }, [getAllProjectFiles, isVisible, commandBarFileLimit]); + }, [getAllProjectFiles, isVisible]); return { files, isLoadingFiles, isIndexing, rootFolderPath }; }; diff --git a/src/features/database/providers/sqlite/components/context-menus.tsx b/src/features/database/providers/sqlite/components/context-menus.tsx index 66f69485..0d6d1911 100644 --- a/src/features/database/providers/sqlite/components/context-menus.tsx +++ b/src/features/database/providers/sqlite/components/context-menus.tsx @@ -25,7 +25,7 @@ export const SqliteTableMenu = ({ return (
Add New Row -
+
@@ -217,14 +220,17 @@ export const EditRowModal = ({ if (!isOpen) return null; return ( -
+

Edit Row in {tableName}

-
@@ -339,14 +345,17 @@ export const CreateTableModal = ({ isOpen, onClose, onSubmit }: CreateTableModal if (!isOpen) return null; return ( -
+

Create New Table

-
@@ -381,7 +390,7 @@ export const CreateTableModal = ({ isOpen, onClose, onSubmit }: CreateTableModal