commit 93bdeb5e953c2e35600a721007cd373dc11a379d Author: wehub-resource-sync Date: Mon Jul 13 12:08:36 2026 +0800 chore: import upstream snapshot with attribution diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..829d53f --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,8 @@ +# Link runtime statically so Visual C++ Redist is not required +[target.'cfg(all(windows, target_env = "msvc"))'] +rustflags = ["-C", "target-feature=+crt-static"] + +[profile.ci] +inherits = "dev" +opt-level = 0 +debug = false diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..55ee7ab --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,10 @@ +{ + "image": "ghcr.io/lapce/lapdev-devcontainer-lapce", + "forwardPorts": [6080], + "portsAttributes": { + "6080": { + "label": "VNC", + } + } +} + diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..1f1fcb5 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +.github +.vscode +.lapce +docs +extra/linux/docker +extra/macos +extra/windows +target +lapce.spec diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..96b3553 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,9 @@ +# Auto detect text files and perform normalization +* text=auto eol=lf + +*.rs text diff=rust eol=lf +*.toml text diff=toml eol=lf +Cargo.lock text eol=lf + +*.sh text eol=lf +*.ps1 text eol=crlf diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..2dad458 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,29 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: 'C-bug' +assignees: '' + +--- + +## Lapce Version + +The Lapce version you are using, which can be found in "About Lapce" at the top right settings icon. + +## System information + +the operating system used, including its version, e.g. Windows 10, Ubuntu 18.04 + +## Describe the bug +A clear and concise description of what the bug is. + +## Additional information + +Other information that can be used to further reproduce or isolate the problem. +This commonly includes: + +- screenshots +- logs +- theories about what might be going wrong +- workarounds that you used diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..aac61aa --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,11 @@ +blank_issues_enabled: false +contact_links: + - name: Lapce community on Discord + url: https://discord.gg/n8tGJ6Rn6D + about: Please ask and answer questions here. + - name: Lapce community on Matrix + url: https://matrix.to/#/#lapce-editor:matrix.org + about: Please ask and answer questions here. + - name: Lapce documentation + url: https://docs.lapce.dev/ + about: Documentation for Lapce diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..2464024 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,24 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: 'C-feature' +assignees: '' + +--- + +## Is your feature request related to a problem? Please describe. + +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +## Describe the solution you'd like + +A clear and concise description of what you want to happen. + +## Describe alternatives you've considered + +A clear and concise description of any alternative solutions or features you've considered. + +## Additional context + +Add any other context or screenshots about the feature request here. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..e3e1d64 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1 @@ +- [ ] Added an entry to `CHANGELOG.md` if this change could be valuable to users \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..f1d5314 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,52 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" + groups: + all-dependencies: + patterns: + - "*" + - package-ecosystem: "cargo" + directory: "/" + open-pull-requests-limit: 10 + schedule: + interval: "monthly" + groups: + all-dependencies: + patterns: + - "*" + exclude-patterns: + - "wasmtime*" + - "wasi-common" + - "wasi-experimental-http-wasmtime" + - "tracing*" + - "strum*" + - "tree-sitter" + - "floem*" + wasmtime: + patterns: + - "wasmtime*" + - "wasi-common" + - "wasi-experimental-http-wasmtime" + toml: + patterns: + - "toml" + - "toml_edit" + strum: + patterns: + - "strum" + - "strum_macros" + tracing: + patterns: + - "tracing*" + floem: + patterns: + - "floem*" + tree-sitter: + patterns: + - "tree-sitter" + windows: + patterns: + - "windows*" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..8a168ff --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,119 @@ +on: + push: + branches: + - master + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + paths: + - Cargo.toml + - Cargo.lock + - lapce-** + - .github/workflows/ci.yml + workflow_dispatch: + +name: CI + +concurrency: + group: ${{ github.ref }}-${{ github.workflow }} + cancel-in-progress: true + +env: + CARGO_TERM_COLOR: always + CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse + +permissions: {} + +jobs: + build: + name: Rust on ${{ matrix.os }} + if: github.event.pull_request.draft == false + needs: [fmt, clippy] + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + permissions: + contents: read + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + + - name: Install dependencies on Ubuntu + if: startsWith(matrix.os, 'ubuntu') + run: sudo make ubuntu-deps + + - name: Update toolchain + run: | + rustup update --no-self-update + + - name: Cache Rust dependencies + uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 + + - name: Fetch dependencies + run: cargo fetch --locked + + - name: Build + run: cargo build --profile ci --frozen + + - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f + with: + name: lapce-${{ matrix.os }} + path: | + target/ci/lapce* + retention-days: 1 + + - name: Build as portable + if: startsWith(matrix.os, 'windows') + run: cargo build --profile ci --frozen --features lapce-app/portable + + - name: Free space on Windows + if: startsWith(matrix.os, 'windows') + run: cargo clean + + - name: Run doc tests + run: cargo test --profile ci --doc --workspace + + fmt: + name: Formatting and spellchecking + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + + - name: Update toolchain & add rustfmt + run: | + rustup update + rustup component add rustfmt + + - name: Run rustfmt + run: cargo fmt --all --check + + - name: Spell Check Repo + uses: crate-ci/typos@02ea592e44b3a53c302f697cddca7641cd051c3d + + clippy: + name: Clippy on ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + + - name: Update toolchain & add clippy + run: | + rustup update --no-self-update + rustup component add clippy + + - name: Install dependencies on Ubuntu + if: startsWith(matrix.os, 'ubuntu') + run: sudo make ubuntu-deps + + - name: Cache Rust dependencies + uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 + + - name: Fetch dependencies + run: cargo fetch --locked + + - name: Run clippy + run: cargo clippy --profile ci diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..438d5a6 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,378 @@ +name: Release + +on: + schedule: + - cron: 0 0 * * * + workflow_dispatch: + inputs: + tag_name: + description: "Tag name for release" + required: false + default: nightly + push: + tags: ["v[0-9]+.[0-9]+.[0-9]+*"] + pull_request: + paths: + # trigger release workflow only if this file changed + - .github/workflows/release.yml + +concurrency: + group: ${{ github.ref }}-${{ github.workflow }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CARGO_TERM_COLOR: always + +permissions: {} + +jobs: + tagname: + runs-on: ubuntu-latest + outputs: + tag_name: ${{ steps.tag.outputs.tag }} + steps: + - id: vars + shell: bash + run: echo "sha_short=${GITHUB_SHA::7}" | tee -a $GITHUB_OUTPUT + + - if: github.event_name == 'workflow_dispatch' + run: echo "TAG_NAME=${{ github.event.inputs.tag_name }}" | tee -a $GITHUB_ENV + + - if: github.event_name == 'schedule' || github.event_name == 'pull_request' + run: echo 'TAG_NAME=nightly-${{ steps.vars.outputs.sha_short }}' | tee -a $GITHUB_ENV + + - if: github.event_name == 'push' + run: | + TAG_NAME=${{ github.ref }} + echo "TAG_NAME=${TAG_NAME#refs/tags/}" | tee -a $GITHUB_ENV + + - id: tag + run: echo "tag=$TAG_NAME" | tee -a $GITHUB_OUTPUT + + windows: + runs-on: windows-latest + needs: tagname + env: + RELEASE_TAG_NAME: ${{ needs.tagname.outputs.tag_name }} + defaults: + run: + shell: bash + permissions: + contents: read + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + + - name: Update rust + run: rustup update --no-self-update + + - name: Fetch dependencies + run: cargo fetch --locked + + - name: Build + run: cargo build --frozen --profile release-lto + + - name: Crate msi installer + run: | + candle.exe -arch "x64" -ext WixUIExtension -ext WixUtilExtension \ + -out "./lapce.wixobj" "extra/windows/wix/lapce.wxs" + + light.exe -ext WixUIExtension -ext WixUtilExtension \ + -out "./Lapce-windows.msi" -sice:ICE61 -sice:ICE91 \ + "./lapce.wixobj" + + - name: Create portable + shell: pwsh + run: | + cargo build --profile release-lto --features lapce-app/portable + Compress-Archive ./target/release-lto/lapce.exe ./Lapce-windows-portable.zip + + - name: Create lapce-proxy archive + shell: pwsh + run: | + $file = [System.IO.File]::Open((Join-Path $PWD '.\target\release-lto\lapce-proxy.exe'), [System.IO.FileMode]::Open) + $archive = [System.IO.File]::Create((Join-Path $PWD '.\lapce-proxy-windows-x86_64.gz')) + $compressor = [System.IO.Compression.GZipStream]::new($archive, [System.IO.Compression.CompressionMode]::Compress) + $file.CopyTo($compressor) + Start-Sleep -Seconds 10 + $compressor.close() + + - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f + with: + name: lapce-windows + path: | + ./lapce-proxy-windows-*.gz + ./Lapce-windows-portable.zip + ./Lapce-windows.msi + retention-days: 1 + + linux: + runs-on: ubuntu-latest + needs: tagname + env: + RELEASE_TAG_NAME: ${{ needs.tagname.outputs.tag_name }} + permissions: + contents: read + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + - name: Build deb packages + run: | + docker buildx create --driver=docker-container --use + docker buildx bake --pull ubuntu-focal-binary + + - name: Gzip + run: | + mkdir Lapce + cp ./target/linux_amd64/lapce Lapce/ + tar -zcvf ./lapce-linux-amd64.tar.gz Lapce + + rm -rf Lapce + + mkdir Lapce + cp ./target/linux_arm64/lapce Lapce/ + tar -zcvf ./lapce-linux-arm64.tar.gz Lapce + + - name: Fetch dependencies + run: cargo fetch --locked + + - name: Vendor dependencies + run: | + cargo vendor --frozen > ./vendor-config.toml + mv ./vendor-config.toml ./vendor/ + tar -zcf vendor.tar.gz ./vendor/ + + - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f + with: + name: lapce-linux + path: | + ./lapce-linux-*.tar.gz + ./vendor.tar.gz + retention-days: 1 + + deb: + runs-on: ubuntu-latest + needs: tagname + env: + RELEASE_TAG_NAME: ${{ needs.tagname.outputs.tag_name }} + permissions: + contents: read + strategy: + fail-fast: false + matrix: + include: + - os-name: debian + os-version: bookworm + - os-name: debian + os-version: bullseye + - os-name: ubuntu + os-version: jammy + - os-name: ubuntu + os-version: noble + - os-name: ubuntu + os-version: plucky + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + + - name: Build deb packages + run: | + docker buildx create --driver=docker-container --use + docker buildx bake --pull ${{ matrix.os-name }}-${{ matrix.os-version }}-package + + - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f + with: + name: lapce-${{ matrix.os-name }}-${{ matrix.os-version }} + path: | + ./target/linux_*/* + retention-days: 1 + + rpm: + runs-on: ubuntu-latest + needs: tagname + env: + RELEASE_TAG_NAME: ${{ needs.tagname.outputs.tag_name }} + permissions: + contents: read + strategy: + fail-fast: false + matrix: + include: + - os-name: fedora + os-version: 42 + - os-name: fedora + os-version: 43 + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + + - name: Build rpm packages + run: | + docker buildx create --driver=docker-container --use + docker buildx bake --pull ${{ matrix.os-name }}-${{ matrix.os-version }}-package + + - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f + with: + name: lapce-${{ matrix.os-name }}-${{ matrix.os-version }} + path: | + ./target/* + retention-days: 1 + + lapce-proxy: + runs-on: ubuntu-latest + needs: tagname + env: + RELEASE_TAG_NAME: ${{ needs.tagname.outputs.tag_name }} + permissions: + contents: read + strategy: + fail-fast: false + matrix: + include: + - os-name: alpine + os-version: "" # uses latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + + - name: Set up QEMU + uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a + + - name: Build lapce-proxy binary + run: | + docker buildx create --driver=docker-container --use + docker buildx bake --pull ${{ matrix.os-name }}-${{ matrix.os-version }} + + - name: Gzip + run: | + gzip -c ./target/linux_amd64/lapce-proxy > ./lapce-proxy-linux-x86_64.gz + gzip -c ./target/linux_arm64/lapce-proxy > ./lapce-proxy-linux-aarch64.gz + + - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f + with: + name: lapce-proxy-linux + path: | + ./lapce-proxy-linux-*.gz + retention-days: 1 + + macos: + runs-on: macos-15 + needs: tagname + env: + RELEASE_TAG_NAME: ${{ needs.tagname.outputs.tag_name }} + + NOTARIZE_USERNAME: ${{ secrets.NOTARIZE_USERNAME }} + NOTARIZE_PASSWORD: ${{ secrets.NOTARIZE_PASSWORD }} + permissions: + contents: read + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + + - name: Install ARM target + run: rustup update && rustup target add x86_64-apple-darwin + + - name: Import Certificate + uses: Apple-Actions/import-codesign-certs@b610f78488812c1e56b20e6df63ec42d833f2d14 + with: + p12-file-base64: ${{ secrets.MACOS_CERTIFICATE }} + p12-password: ${{ secrets.MACOS_CERTIFICATE_PWD }} + + - name: Fetch dependencies + run: cargo fetch --locked + + - name: Make DMG + run: make dmg-universal + + - name: Rename + run: | + cp ./target/release-lto/macos/Lapce.dmg ./target/release-lto/macos/Lapce-macos.dmg + + - name: Gzip lapce-proxy + run: | + gzip -c ./target/x86_64-apple-darwin/release-lto/lapce-proxy > ./target/release-lto/macos/lapce-proxy-darwin-x86_64.gz + gzip -c ./target/aarch64-apple-darwin/release-lto/lapce-proxy > ./target/release-lto/macos/lapce-proxy-darwin-aarch64.gz + + - name: Notarize Release Build + uses: lando/notarize-action@b5c3ef16cf2fbcf2af26dc58c90255ec242abeed + with: + product-path: "./target/release-lto/macos/Lapce-macos.dmg" + appstore-connect-username: ${{ secrets.NOTARIZE_USERNAME }} + appstore-connect-password: ${{ secrets.NOTARIZE_PASSWORD }} + appstore-connect-team-id: CYSGAZFR8D + primary-bundle-id: "io.lapce" + verbose: true + + - name: "Staple Release Build" + uses: lapce/xcode-staple@062485d6eeafe841c18a412f012e80f49e23c517 + with: + product-path: "./target/release-lto/macos/Lapce-macos.dmg" + + - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f + with: + name: lapce-macos + path: | + ./target/release-lto/macos/lapce-proxy-darwin-*.gz + ./target/release-lto/macos/Lapce-macos.dmg + retention-days: 3 + + publish: + needs: + - linux + - lapce-proxy + - deb + - rpm + - windows + - macos + runs-on: ubuntu-latest + env: + GH_REPO: ${{ github.repository }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + permissions: + contents: write + steps: + # Must perform checkout first, since it deletes the target directory + # before running, and would therefore delete the downloaded artifacts + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + + - if: github.event_name == 'workflow_dispatch' + run: echo "TAG_NAME=${{ github.event.inputs.tag_name }}" | tee -a $GITHUB_ENV + + - if: github.event_name == 'schedule' + run: echo 'TAG_NAME=nightly' | tee -a $GITHUB_ENV + + - if: github.event_name == 'push' + run: | + TAG_NAME=${{ github.ref }} + echo "TAG_NAME=${TAG_NAME#refs/tags/}" | tee -a $GITHUB_ENV + + - if: env.TAG_NAME == 'nightly' + run: | + { + echo 'SUBJECT=Lapce development build' + echo 'PRERELEASE=--prerelease' + } | tee -a $GITHUB_ENV + + - if: env.TAG_NAME == 'nightly' && github.event_name != 'pull_request' + name: Re-Tag nightly + run: | + gh release delete nightly --yes || true + git push origin :nightly || true + + - if: env.TAG_NAME != 'nightly' + run: | + { + echo 'SUBJECT=Lapce release build' + echo 'PRERELEASE=' + } | tee -a $GITHUB_ENV + + - name: Publish release + if: github.event_name != 'pull_request' + env: + DEBUG: api + run: | + gh release create $TAG_NAME $PRERELEASE --title "$TAG_NAME" --target $GITHUB_SHA \ + lapce-macos/* \ + lapce-linux/* \ + lapce-debian*/*/* \ + lapce-ubuntu*/*/* \ + lapce-fedora*/* \ + lapce-proxy-linux/* \ + lapce-windows/* diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fc7a859 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +.vscode +.lapce +.idea +.fleet +shell.nix +lib/ +target/ +core/languages/ +vendor/ + +.DS_Store +*.patch +*.tar +*.gz diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 0000000..06900d0 --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1,4 @@ +max_width = 85 +# below requires nightly +# imports_granularity = "Crate" +# group_imports = "StdExternalCrate" diff --git a/.taplo.toml b/.taplo.toml new file mode 100644 index 0000000..ee911f0 --- /dev/null +++ b/.taplo.toml @@ -0,0 +1,13 @@ +include = ["**/*.toml"] +exclude = ["lapce-proxy/src/**"] + +[[rule]] +include = ["**/Cargo.toml", ".taplo.toml", "Cargo.toml"] + +[rule.formatting] +align_entries = true +align_comments = true +array_trailing_comma = true +trailing_newline = true +column_width = 100 +inline_table_expand = false diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..f885a95 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,416 @@ +# Changelog + +## Unreleased + +### Features/Changes + +### Bug Fixes + +## 0.4.6 + +### Features/Changes + +- Add `.har` file extension as recognised JSON language () +- Remove builds for Ubuntu Oracular, add Ubuntu Plucky () + +### Bug Fixes + +- Fix mouse wheel scrolling when viewing diff () +- Fix editor tabs not selectable while appearing selectable () +- Fix flickering when reordering editor tabs () +- Fix diff viewer to scroll using mouse () +- Fix updating window scale when it changes via settings tab () +- Fix vendor tarball not being included in GitHub release +- Fix cursor style inconsistencies () + +## 0.4.5 + +### Bug Fixes + +- Fix missing/incorrect metadata for Linux packages using AppStream metainfo +- Fix incorrect version in macOS/Windows package metadata +- Fix rendering issues when using custom scale display () + +## 0.4.4 + +### Features/Changes + +- Update Fedora releases to use 41/42 +- Update Alpine to latest (used for building portable lapce-proxy) +- Remove Ubuntu Focal (20.04) + +### Bug Fixes + +- Fix dropdown menu disappearing after releasing mouse button () +- Fix issue with cargo vendoring due to duplicate `dpi` crate + +## 0.4.3 + +### Features/Changes + +- Add Ubuntu 24.04 LTS (noble) build +- Use Diff syntax for files with .diff/.patch extensions by default. +- Update titles of file dialogs for better clarity +- Bump to Rust 2024 Edition +- Implement "Find References" panel +- Implement "Go To Implementation" panel + +### Bug Fixes + +- Fix pointer event not working on plugin panel +- Fix rename/new file editor in file explorer is too small + +## 0.4.2 + +### Features/Changes + +- Implement "Run in terminal" +- Implement document symbols in a panel +- Implement "Go To Location" functionality in the Diff editor. +- Implement on screen find which is similar to `f` in vim but for the whole screen. +- Make file explorer horizontal scrollable +- Implement "Reveal in system file explorer" + +### Bug Fixes + +- Fix markdown syntax highlighting +- Fix click issue on window error message + +## 0.4.1 + +### Features/Changes + +- Add fedora builds +- Finish tree sitter dynamic library support by downloading from https://github.com/lapce/tree-sitter-grammars +- Saves scale configuration in settings to restore at startup +- text in ui can be selected and copied +- add right click context menu for editor tab +- terminal: added support for Alt+ some character input +- Terminal: right-click to add "clear all" function +- terminal: double-click to maximize bottom +- initial lsp code lens support for rust-analyzer +- Context menu for "reveal file in explorer" +- support rust-analyzer's notification "experimental/serverStatus" +- jump to definition support in Inlay Hints +- "Show Call Hierarchy" support +- Add proxy support and retry to all HTTPS GET requests. +- Implement the drag-and-drop to open file/folder + +### Bug Fixes + +- [#3203](https://github.com/lapce/lapce/pull/3203): Fallback to default theme is theme key is not found +- use windows subsystem to prevent console running +- reimplement gotodefinition on cmd/ctrl+click +- fix crash on tinyskia renderer +- show file status for scratch documents +- fix crash due to locale_config + +## 0.4.0 + +### Features/Changes + +- [#2723](https://github.com/lapce/lapce/pull/2723): Line wrapping based on width (no column-based yet) +- [#1277](https://github.com/lapce/lapce/pull/1277): Error message prompted on missing git user.email and/or user.name +- [#2910](https://github.com/lapce/lapce/pull/2910): Files can be compared in the diff editor +- [#2918](https://github.com/lapce/lapce/pull/2918): Allow searching the shortcuts overview by shortcut (e.g. "Ctrl+P") or when condition (e.g. "list_focus") +- [#2955](https://github.com/lapce/lapce/pull/2955): Using glob patterns to hide files or directories in explorer +- [#3026](https://github.com/lapce/lapce/pull/3026): Add missing file explorer context menu entries +- [#3047](https://github.com/lapce/lapce/pull/3047): Add support for different CrLf/Lf line endings per-file +- [#3053](https://github.com/lapce/lapce/pull/3053): Add tooltips to various places +- [#3069](https://github.com/lapce/lapce/pull/3069): Allow color variables for themes +- [#3086](https://github.com/lapce/lapce/pull/3086): Add folding to panels +- [#3095](https://github.com/lapce/lapce/pull/3095): Add option to hide the Open Editors section in the explorer +- [#3096](https://github.com/lapce/lapce/pull/3096): Add source control status coloring to file explorer +- [#2989](https://github.com/lapce/lapce/pull/2989): Add building packages for linux distros +- [#3123](https://github.com/lapce/lapce/pull/3123): Swap sled to plain text file for db + +### Bug Fixes + +- [#2779](https://github.com/lapce/lapce/pull/2779): Fix files detection on fresh git/VCS repository +- [#3031](https://github.com/lapce/lapce/pull/3031): Fix find not receiving inputs when clicked +- [#3142](https://github.com/lapce/lapce/pull/3142): Fix terminal default profile saving incorrectly +- [#3158](https://github.com/lapce/lapce/pull/3158): Fix ShowHover command for showing hover with keybind +- [#3119](https://github.com/lapce/lapce/pull/3119): Fix non US keyboard layout + +## 0.3.1 + +### Features/Changes + +### Bug Fixes + +- [#2754](https://github.com/lapce/lapce/pull/2754): Don't mark nonexistent files as read only (fix saving new files) +- [#2819](https://github.com/lapce/lapce/issues/2819): `Save Without Formatting` doesn't save the file + +## 0.3.0 + +### Features/Changes + +- [#2190](https://github.com/lapce/lapce/pull/2190): Rewrite with Floem UI +- [#2425](https://github.com/lapce/lapce/pull/2425): Reimplement completion lens +- [#2498](https://github.com/lapce/lapce/pull/2498): Show Lapce as an option when doing "Open With..." on Linux +- [#2549](https://github.com/lapce/lapce/pull/2549): Implement multi-line vim-motion yank and delete (`3dd`, `2yy`, etc.) +- [#2553](https://github.com/lapce/lapce/pull/2553): Implement search and replace +- [#1809](https://github.com/lapce/lapce/pull/1809): Implement debug adapter protocol + +### Bug Fixes + +- [#2650](https://github.com/lapce/lapce/pull/2650): Inform language servers that Lapce supports LSP diagnostics + +## 0.2.8 + +### Features/Changes + +- [#1964](https://github.com/lapce/lapce/pull/1964): Add option to open files at line/column +- [#2403](https://github.com/lapce/lapce/pull/2403): Add basic Vim marks feature + +### Bug Fixes + +## 0.2.7 + +### Features/Changes + +### Bug Fixes + +- [#2209](https://github.com/lapce/lapce/pull/2209): Fix macOS crashes +- [#2228](https://github.com/lapce/lapce/pull/2228): Fix `.desktop` entry to properly associate with Lapce on Wayland + +## 0.2.6 + +### Breaking changes + +- [#1820](https://github.com/lapce/lapce/pull/1820): Add remote svg icon colour to theme, disable plugin settings when none are available +- [#1988](https://github.com/lapce/lapce/pull/1987): Replace modal status background with background/foreground theme keys + +### Features/Changes + +- [#1899](https://github.com/lapce/lapce/pull/1899): Improve sorting files with numbers +- [#1831](https://github.com/lapce/lapce/pull/1831): Plugin settings shown on right click +- [#1830](https://github.com/lapce/lapce/pull/1830): Adds Clojure language support +- [#1835](https://github.com/lapce/lapce/pull/1835): Add mouse keybinds +- [#1856](https://github.com/lapce/lapce/pull/1856): Highlight git/VCS modified files in explorer, palette, and buffer tabs +- [#1574](https://github.com/lapce/lapce/pull/1574): Panel sections can be expanded/collapsed +- [#1938](https://github.com/lapce/lapce/pull/1938): Use dropdown for theme selection in settings +- [#1960](https://github.com/lapce/lapce/pull/1960): Add sticky headers and code lens for PHP +- [#1968](https://github.com/lapce/lapce/pull/1968): Completion lens (disabled by default) + - ![image](https://user-images.githubusercontent.com/13157904/211959283-c3229cfc-28d7-4676-a50d-aec7d47cde9f.png) +- [#1972](https://github.com/lapce/lapce/pull/1972): Add file duplication option in fs tree context menu +- [#1991](https://github.com/lapce/lapce/pull/1991): Implement rendering of images in markdown views +- [#2004](https://github.com/lapce/lapce/pull/2004): Add ToggleHistory command +- [#2033](https://github.com/lapce/lapce/pull/2033): Add setting for double click delay (Currently only works for opening file from the explorer) +- [#2045](https://github.com/lapce/lapce/pull/2045): Add 'Rename Symbol' option on right-click +- [#2071](https://github.com/lapce/lapce/pull/2071): Add command and keybinds to delete line +- [#2073](https://github.com/lapce/lapce/pull/2073): Add Ctrl+{a,e,k} keybinds on macOS +- [#2128](https://github.com/lapce/lapce/pull/2128): Add Lapce app icon to logo collection +- [#2127](https://github.com/lapce/lapce/pull/2127): Extended double-click options with file-only and file + folders mode +- [#1944](https://github.com/lapce/lapce/pull/1944): Add filter input in git branch selection + - ![image](https://user-images.githubusercontent.com/4404609/211232461-293e3b31-4e17-457e-825c-3018699a6fc2.png) + +### Bug Fixes + +- [#1911](https://github.com/lapce/lapce/pull/1911): Fix movement on selections with left/right arrow keys +- [#1939](https://github.com/lapce/lapce/pull/1939): Fix saving/editing newly saved-as files +- [#1971](https://github.com/lapce/lapce/pull/1971): Fix up/down movement on first/last line +- [#2036](https://github.com/lapce/lapce/pull/2036): Fix movement on selections with up/down arrow keys +- [#2056](https://github.com/lapce/lapce/pull/2056): Fix default directory of remote session file picker +- [#2072](https://github.com/lapce/lapce/pull/2072): Fix connection issues from Windows to lapce proxy +- [#2069](https://github.com/lapce/lapce/pull/2045): Fix not finding git repositories in parent path +- [#2131](https://github.com/lapce/lapce/pull/2131): Fix overwriting symlink +- [#2188](https://github.com/lapce/lapce/pull/2188): Fix auto closing matching pairs in inappropriate inputs + +## 0.2.5 + +### Breaking changes + +- [#1726](https://github.com/lapce/lapce/pull/1726): Add more panel theme keys, apply hover first, then current item colour + +### Features/Changes + +- [#1791](https://github.com/lapce/lapce/pull/1791): Add highlighting for scope lines +- [#1767](https://github.com/lapce/lapce/pull/1767): Added CMake tree-sitter syntax highlighting +- [#1759](https://github.com/lapce/lapce/pull/1759): Update C tree-sitter and highlight queries +- [#1758](https://github.com/lapce/lapce/pull/1758): Replaced dlang syntax highlighting +- [#1713](https://github.com/lapce/lapce/pull/1713): Add protobuf syntax and highlighting +- [#1720](https://github.com/lapce/lapce/pull/1720): Display signature/parameter information from LSP +- [#1723](https://github.com/lapce/lapce/pull/1723): In the palette, display the keybind for a command adjacent to it +- [#1722](https://github.com/lapce/lapce/pull/1722): Add 'Save without Formatting'; Add option to disable formatting on autosave +- [#1741](https://github.com/lapce/lapce/pull/1741): Add syntax highlighting for glsl +- [#1756](https://github.com/lapce/lapce/pull/1756): Add support for ssh port with `[user@]host[:port]` +- [#1760](https://github.com/lapce/lapce/pull/1760): Add vim motions `cw`, `ce`, `cc`, `S`, and QOL modal bind `gf` +- [#1770](https://github.com/lapce/lapce/pull/1770): Add support for terminal tabs + +### Bug Fixes + +- [#1771](https://github.com/lapce/lapce/pull/1771): Update tree-sitter-bash +- [#1737](https://github.com/lapce/lapce/pull/1726): Fix an issue that plugins can't be upgraded +- [#1724](https://github.com/lapce/lapce/pull/1724): Files and hidden folders no longer will be considered when trying to open a plugin base folder +- [#1753](https://github.com/lapce/lapce/pull/1753): Limit proxy search response size in order to avoid issues with absurdly long lines +- [#1805](https://github.com/lapce/lapce/pull/1805): Fix some LSP bugs causing code actions to not show up correctly + +## 0.2.4 + +### Features/Changes + +- [#1700](https://github.com/lapce/lapce/pull/1700): Add prisma syntax and highlighting +- [#1702](https://github.com/lapce/lapce/pull/1702): Improved svelte treesitter queries +- [#1690](https://github.com/lapce/lapce/pull/1690): Add codelens and sticky headers for Dart +- [#1711](https://github.com/lapce/lapce/pull/1711): Add zstd support for plugin +- [#1715](https://github.com/lapce/lapce/pull/1715): Add support for requests from plugins + +### Bug Fixes + +- [#1710](https://github.com/lapce/lapce/pull/1710): Fix autosave trying to save scratch files +- [#1709](https://github.com/lapce/lapce/pull/1709): Fix search result ordering +- [#1708](https://github.com/lapce/lapce/pull/1708): Fix visual issue when search panel is placed to either side panel + +## 0.2.3 + +### Features/Changes + +- [#1655](https://github.com/lapce/lapce/pull/1655): Add status foreground theme key, use author colour for plugin version +- [#1646](https://github.com/lapce/lapce/pull/1646): Fork the process when started from terminal +- [#1653](https://github.com/lapce/lapce/pull/1653): Paint plugin icons +- [#1644](https://github.com/lapce/lapce/pull/1644): Added "Reveal in File Tree" action to the editor tabs context menu +- [#1645](https://github.com/lapce/lapce/pull/1645): Add plugin search + +### Bug Fixes + +- [#1651](https://github.com/lapce/lapce/pull/1651): Fixed an issue where new windows would never be created after closing all windows on macOS. +- [#1637](https://github.com/lapce/lapce/issues/1637): Fix python using 7 spaces instead of 4 spaces to indent +- [#1669](https://github.com/lapce/lapce/issues/1669): It will now remember panel states when open a new project +- [#1706](https://github.com/lapce/lapce/issues/1706): Fix the issue that color can't be changed in theme settings + +## 0.2.2 + +### Features/Changes + +- [#1643](https://github.com/lapce/lapce/pull/1643): Use https://plugins.lapce.dev/ as the plugin registry +- [#1620](https://github.com/lapce/lapce/pull/1620): Added "Show Hover" keybinding that will trigger the hover at the cursor location +- [#1619](https://github.com/lapce/lapce/pull/1619): + - Add active/inactive tab colours + - Add primary button colour + - Add hover effect in source control panel + - Add colour preview in settings +- [#1617](https://github.com/lapce/lapce/pull/1617): Fixed a stack overflow that would crash lapce when attempting to sort a large number of PaletteItems +- [#1609](https://github.com/lapce/lapce/pull/1609): Add syntax highlighting for erlang +- [#1590](https://github.com/lapce/lapce/pull/1590): Added ability to open file and file diff from source control context menu +- [#1570](https://github.com/lapce/lapce/pull/1570): Added a basic tab context menu with common close actions +- [#1560](https://github.com/lapce/lapce/pull/1560): Added ability to copy active editor remote file path to clipboard +- [#1510](https://github.com/lapce/lapce/pull/1510): Added support to discard changes to a file +- [#1459](https://github.com/lapce/lapce/pull/1459): Implement icon theme system + - **This is a breaking change for colour themes!** + - Colour themes should now use `[color-theme]` table format in theme TOML + - `volt.toml` now use `color-themes` and `icon-themes` keys. `themes` key is not used anymore. +- [#1554](https://github.com/lapce/lapce/pull/1554): Added XML language support +- [#1472](https://github.com/lapce/lapce/pull/1472): Added SQL language support +- [#1531](https://github.com/lapce/lapce/pull/1531): Improved Ctrl+Left command on spaces at the beginning of a line +- [#1491](https://github.com/lapce/lapce/pull/1491): Added Vim shift+c to delete remainder of line +- [#1508](https://github.com/lapce/lapce/pull/1508): Show in progress when Lapce is self updating +- [#1475](https://github.com/lapce/lapce/pull/1475): Add editor setting: "Cursor Surrounding Lines" which sets minimum number of lines above and below cursor +- [#1525](https://github.com/lapce/lapce/pull/1525): Add editor indent guide +- [#1521](https://github.com/lapce/lapce/pull/1521): Show unique paths to disambiguate same file names +- [#1452](https://github.com/lapce/lapce/pull/1452): Wrap selected text with brackets/quotes +- [#1421](https://github.com/lapce/lapce/pull/1421): Add matching bracket highlighting +- [#1541](https://github.com/lapce/lapce/pull/1541): Order palette items according to last execute time + +### Bug Fixes + +- [#1566](https://github.com/lapce/lapce/pull/1565)|[#1568](https://github.com/lapce/lapce/pull/1568): Use separate colour for drag and drop background +- [#1459](https://github.com/lapce/lapce/pull/1459): Fix opening currently used logfile +- [#1505](https://github.com/lapce/lapce/pull/1505): Fix proxy download for hosts with curl without -Z flag +- [#1483](https://github.com/lapce/lapce/pull/1483): Fix showing the close icon for the first tab when opening multiple tab +- [#1477](https://github.com/lapce/lapce/pull/1477): Now use `esc` to close searchbar regardless of the current focus +- [#1507](https://github.com/lapce/lapce/pull/1507): Fixed a crash when scratch buffer is closed +- [#1547](https://github.com/lapce/lapce/pull/1547): Fix infinite cycle in workspace symbol search +- [#1628](https://github.com/lapce/lapce/pull/1541): Fix kts files not being recognized + +## 0.2.1 + +### Features/Changes + +- [#1050](https://github.com/lapce/lapce/pull/1050): Collapse groups of problems in the problem list panel +- [#1165](https://github.com/lapce/lapce/pull/1165): Command to reveal item in system file explorer +- [#1196](https://github.com/lapce/lapce/pull/1196): Always show close button on focused editor tabs +- [#1208](https://github.com/lapce/lapce/pull/1208): Sticky header breadcrumbs + - This provides a header at the top which tells you information about the current scope! Especially useful for long blocks of code + - ![image](https://user-images.githubusercontent.com/13157904/195404556-2c329ebb-f721-4d55-aa22-56a54f8e8454.png) + - As well, you can see that there is now a breadcrumb path to the current file. + - A language with syntax highlighting can have this added, even without an LSP. Take a look at `language.rs` if your language isn't supported! +- [#1198](https://github.com/lapce/lapce/pull/1198): Focus current theme/language in palette +- [#1244](https://github.com/lapce/lapce/pull/1244); Prettier plugin panel +- [#1238](https://github.com/lapce/lapce/pull/1238): Improved multicursor selection +- [#1291](https://github.com/lapce/lapce/pull/1291): Use link colour for empty editor buttons +- [#1234](https://github.com/lapce/lapce/commit/07390f0c90c0700d1f69409bf48723d15090c474): Automatic line height +- [#1262](https://github.com/lapce/lapce/pull/1262): Add absolute/relative copy path to file explorer +- [#1284](https://github.com/lapce/lapce/pull/1284): Render whitespace (default: none) + - ![image](https://user-images.githubusercontent.com/13157904/195410868-f27db85f-d7d2-4197-84f0-12d6c44e2053.png) +- [#1308](https://github.com/lapce/lapce/pull/1308): Handle LSP ResourceOp +- [#1251](https://github.com/lapce/lapce/pull/1251): Add vim's paste-before `P` command +- [#1319](https://github.com/lapce/lapce/pull/1319): Add information page for plugins +- [#1344](https://github.com/lapce/lapce/pull/1344): Replace the branch-selector menu with a scrollable list +- [#1352](https://github.com/lapce/lapce/pull/1352): Add duplicate line up/down commands +- [#1281](https://github.com/lapce/lapce/pull/1281): Implement logic for displaying plugin installation status +- [#1353](https://github.com/lapce/lapce/pull/1353): Implement syntax aware selection +- [#1358](https://github.com/lapce/lapce/pull/1358): Add autosave implementation +- [#1381](https://github.com/lapce/lapce/pull/1381): Show multiple hover items in the hover box +- [#1040](https://github.com/lapce/lapce/pull/1040): Add keybindings for `Shift-Del`, `Shift-Ins`, and `Ctrl-Ins` +- [#1401](https://github.com/lapce/lapce/pull/1401): Merge semantic and tree-sitter syntax highlighting +- [#1426](https://github.com/lapce/lapce/pull/1426): Add cursor position/current selection in status bar + - ![image](https://user-images.githubusercontent.com/13157904/195414557-dbf6cff1-3ab2-49ec-ba9d-c7507b2fc83a.png) +- [#1420](https://github.com/lapce/lapce/pull/1420): Add LSP `codeAction/resolve` support +- [#1440](https://github.com/lapce/lapce/pull/1440): IME support +- [#1449](https://github.com/lapce/lapce/pull/1449): Plugin settings in the editor support. Though this still needs some work from plugins to expose them all nicely! +- [#1441](https://github.com/lapce/lapce/pull/1441): Button for Case-Sensitive search +- [#1471](https://github.com/lapce/lapce/pull/1471): Add command to (un)install Lapce from/to PATH +- [#1419](https://github.com/lapce/lapce/pull/1419): Add atomic soft tabs: now you can move your cursor over four spaces as if it was a single block + +### Syntax / Extensions + +- [#957](https://github.com/lapce/lapce/pull/957): Replace existing tree-sitter syntax highlighting code with part of Helix's better implementation + - This means that syntax highlighting for more languages! Such as fixing markdown support, and making so that languages embedded in others (like JavaScript in HTML) work. + - Note that not all themes have updated themselves to include the extra scopes/colors. +- [#1036](https://github.com/lapce/lapce/pull/1036): Recognize ESM/CJS extensions for JavaScript/TypeScript +- [#1007](https://github.com/lapce/lapce/pull/1007): Add ability to bind a key shortcut for quitting the editor +- [#1104](https://github.com/lapce/lapce/pull/1104): Add syntax highlighting for Dockerfile, C#, and Nix +- [#1118](https://github.com/lapce/lapce/pull/1118): Recognize `pyi, pyc, pyd, pyw` extensions for Python +- [#1122](https://github.com/lapce/lapce/pull/1122): Recognize extensions for DLang +- [#1335](https://github.com/lapce/lapce/pull/1335): Highlighting for DLang +- [#1153](https://github.com/lapce/lapce/pull/1050): Recognize and add highlighting for Dart +- [#1161](https://github.com/lapce/lapce/pull/1161): Recognize and add highlighting for Svelte and LaTeX files +- [#1299](https://github.com/lapce/lapce/pull/1299): Recognize and add highlighting for Kotlin +- [#1326](https://github.com/lapce/lapce/pull/1326): Recognize and add highlighting for Vue +- [#1370](https://github.com/lapce/lapce/pull/1370): Recognize and add highlighting for R +- [#1416](https://github.com/lapce/lapce/pull/1416): Recognize and add highlighting for Scheme +- [#1145](https://github.com/lapce/lapce/pull/1145): Adds/Fixes highlighting for C/C++/TypeScript/JavaScript/Zig/Bash +- [#1272](https://github.com/lapce/lapce/pull/1272): Adds/Fixes highlighting for Elm/JSX/TSX +- [#1450](https://github.com/lapce/lapce/pull/1450): Add `tf` extension for HCL + +### Bug Fixes + +- [#1030](https://github.com/lapce/lapce/pull/1030): Don't try to open an font file with an empty name if there is no font family set +- [9f0120d](https://github.com/lapce/lapce/commit/9f0120df85e3aaaef7fbb43385bb15d88443260a): Fix excessive CPU usage in part of the code +- [bf5a98a](https://github.com/lapce/lapce/commit/bf5a98a6d432f9d2abdc1737da2d075e204771fb): Fix issue where sometimes Lapce can't open +- [#1084](https://github.com/lapce/lapce/pull/1084): Use host shell in terminal when running inside Flatpak +- [#1120](https://github.com/lapce/lapce/pull/1120): Make Alt+Backspace work in the terminal properly +- [#1127](https://github.com/lapce/lapce/pull/1127): Improve Julia highlighting +- [#1179](https://github.com/lapce/lapce/pull/1179): Various improvements/fixes to window-tab functionality +- [#1210](https://github.com/lapce/lapce/pull/1210): Fixed closing modified file when closing split +- [#1219](https://github.com/lapce/lapce/pull/1219): Fix append command behavior +- [#1250](https://github.com/lapce/lapce/pull/1250): Fix too long socket path for proxy +- [#1252](https://github.com/lapce/lapce/pull/1252): Check whether the active editor tab index actually exists, avoiding a potential crash +- [#1294](https://github.com/lapce/lapce/pull/1294): Backward word deletion should respect whitespace better +- [#1301](https://github.com/lapce/lapce/pull/1301): Fix incorrect path when going from Url -> PathBuf (such as from an LSP) +- [#1368](https://github.com/lapce/lapce/pull/1368): Fix tabstop for postfix completions +- [#1388](https://github.com/lapce/lapce/pull/1388): Fix regex search within the terminal +- [#1423](https://github.com/lapce/lapce/pull/1423): Fix multiple cursor offset after inserting opening pair +- [#1434](https://github.com/lapce/lapce/pull/1434): Join PATH with correct platform separator +- [#1443](https://github.com/lapce/lapce/pull/1443): Correct terminal font sizing +- [#1453](https://github.com/lapce/lapce/pull/1453): Trim whitespace from search results +- [#1461](https://github.com/lapce/lapce/pull/1461): Load shell environment when launching from GUI +- Many more fixes! + +### Other + +- [#1191](https://github.com/lapce/lapce/pull/1191): Tone down default inlay hint background color in Lapce dark theme +- [#1227](https://github.com/lapce/lapce/pull/1227): Don't restore cursor mode on undo +- [#1413](https://github.com/lapce/lapce/pull/1413): Disable format-on-save by default. Remember to re-enable this if you want it! +- [#1404](https://github.com/lapce/lapce/pull/1404): Log panics with full backtrace as error diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..6fd0e72 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,47 @@ +# How to contribute +Thank you for your interest in contributing to Lapce! No contribution is too small and we consider _all_ contributions to the project. There are many ways to contribute (a few are listed here) but if you think of something else, join us on [Discord](https://discord.gg/n8tGJ6Rn6D) or let us know via an [issue](https://github.com/lapce/lapce/issues). + +## Questions + +We're always around hanging on our [Discord](https://discord.gg/n8tGJ6Rn6D) server but if you're only participating on GitHub, you can open a [Discussion](https://github.com/lapce/lapce/discussions) + +## Feature Requests + +A feature request is _editor behaviour that you want to have included in Lapce_. We track feature requests on GitHub via [issues](https://github.com/lapce/lapce/issues). There are generally few kinds of features: + +### Core features + +A feature more suited to the core development of Lapce. If this is the case please make a suggestion in an [issue](https://github.com/lapce/lapce/issues). + +### Programming language support (autocompletion/intellisense/formatting) + +A feature that relates to specific programming language or development tool that provides intellisense, or various editor commands. +We do not track plugins development here, each plugin should have own issue tracker with eventual issues linked/referenced to main Lapce issue tracker. + +### Syntax highlighting + +There is main issue for tracking syntax highlighting grammars support at https://github.com/lapce/lapce/issues/272. + +--- + +To reduce the number of duplicate requests, please search through the issues to see if something has already been suggested. If a feature you want has been suggested, then comment on that issue to let us know it's popular. You can use emoji-reactions to show us just how popular. + +## Bug Reports + +Bugs should also be reported on GitHub via [issues](https://github.com/lapce/lapce/issues). This allows us to track them and see how prevalent they are. + +If you encounter a bug when using Lapce, check the issues to see if anyone else has encountered it. If it already exists, you can use emoji reactions so we can see community interest in specific issues and how important they are. + +Please follow the rule of [NoPlusOne](https://github.com/golang/go/wiki/NoPlusOne) + +## Pull Requests + +If you want to write some code, develop the documentation, or otherwise work on a certain feature or bug, let us know by replying to or creating an [issue](https://github.com/lapce/lapce/issues). + +Run `cargo fmt --all` and `cargo clippy` on your code before submitting pull requests and fix any issues; this makes sure that the CI runs only fail on genuine build errors and not formatting/Clippy lints. + +We are currently in the process of improving the documentation for new developers/code contributors. Feel free to get started, or post a message on [Discord](https://discord.gg/n8tGJ6Rn6D) to see what can be done. + +## Contact + +As always, if you have any questions or are just not sure where to start, post a message into the [Discord](https://discord.gg/n8tGJ6Rn6D) server. We suggest you start here as it is the most popular way for Lapce's contributors and users to communicate. diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..ecaecb6 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,7956 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" +dependencies = [ + "lazy_static", + "regex", +] + +[[package]] +name = "ab_glyph" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5110f1c78cf582855d895ecd0746b653db010cec6d9f5575293f27934d980a39" +dependencies = [ + "ab_glyph_rasterizer", + "owned_ttf_parser", +] + +[[package]] +name = "ab_glyph_rasterizer" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046" + +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "getrandom 0.2.7", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "alacritty_terminal" +version = "0.24.1-dev" +source = "git+https://github.com/alacritty/alacritty?rev=cacdb5bb3b72bad2c729227537979d95af75978f#cacdb5bb3b72bad2c729227537979d95af75978f" +dependencies = [ + "base64 0.22.0", + "bitflags 2.9.1", + "home", + "libc", + "log", + "miow", + "parking_lot", + "piper", + "polling", + "regex-automata", + "rustix-openpty", + "serde", + "signal-hook", + "unicode-width", + "vte", + "windows-sys 0.52.0", +] + +[[package]] +name = "allocator-api2" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" + +[[package]] +name = "ambient-authority" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9d4ee0d472d1cd2e28c97dfa124b3d8d992e10eb0a035f33f5d12e3a177ba3b" + +[[package]] +name = "android-activity" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046" +dependencies = [ + "android-properties", + "bitflags 2.9.1", + "cc", + "cesu8", + "jni", + "jni-sys", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys 0.6.0+11769913", + "num_enum", + "thiserror 1.0.69", +] + +[[package]] +name = "android-properties" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "anstyle" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" + +[[package]] +name = "anyhow" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224afbd727c3d6e4b90103ece64b8d1b67fbb1973b1046c2281eed3f3803f800" + +[[package]] +name = "arbitrary" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2e1373abdaa212b704512ec2bd8b26bd0b7d5c3f70117411a5d9a451383c859" + +[[package]] +name = "arc-swap" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6" + +[[package]] +name = "arrayref" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "as-raw-xcb-connection" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5f312b0a56c5cdf967c0aeb67f6289603354951683bc97ddc595ab974ba9aa" + +[[package]] +name = "ash" +version = "0.38.0+1.3.281" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f" +dependencies = [ + "libloading", +] + +[[package]] +name = "ashpd" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cbdf310d77fd3aaee6ea2093db7011dc2d35d2eb3481e5607f1f8d942ed99df" +dependencies = [ + "async-fs", + "async-net", + "enumflags2", + "futures-channel", + "futures-util", + "rand 0.9.1", + "raw-window-handle", + "serde", + "serde_repr", + "url", + "wayland-backend", + "wayland-client", + "wayland-protocols 0.32.10", + "zbus", +] + +[[package]] +name = "async-broadcast" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "258b52a1aa741b9f09783b2d86cf0aeeb617bbf847f6933340a39644227acbdb" +dependencies = [ + "event-listener 5.4.0", + "event-listener-strategy 0.5.0", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28243a43d821d11341ab73c80bed182dc015c514b951616cf79bd4af39af0c3" +dependencies = [ + "concurrent-queue", + "event-listener 5.4.0", + "event-listener-strategy 0.5.0", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb812ffb58524bdd10860d7d974e2f01cc0950c2438a74ee5ec2e2280c6c4ffa" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-fs" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc19683171f287921f2405677dd2ed2549c3b3bda697a563ebc3a121ace2aba1" +dependencies = [ + "async-lock 3.3.0", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-io" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059" +dependencies = [ + "async-lock 3.3.0", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix 0.38.32", + "slab", + "tracing 0.1.41", + "windows-sys 0.59.0", +] + +[[package]] +name = "async-lock" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" +dependencies = [ + "event-listener 2.5.3", +] + +[[package]] +name = "async-lock" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d034b430882f8381900d3fe6f0aaa3ad94f2cb4ac519b429692a1bc2dda4ae7b" +dependencies = [ + "event-listener 4.0.3", + "event-listener-strategy 0.4.0", + "pin-project-lite", +] + +[[package]] +name = "async-net" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" +dependencies = [ + "async-io", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-process" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63255f1dc2381611000436537bbedfe83183faa303a5a0edaf191edef06526bb" +dependencies = [ + "async-channel", + "async-io", + "async-lock 3.3.0", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener 5.4.0", + "futures-lite", + "rustix 0.38.32", + "tracing 0.1.41", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.94", +] + +[[package]] +name = "async-signal" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e47d90f65a225c4527103a8d747001fc56e375203592b25ad103e1ca13124c5" +dependencies = [ + "async-io", + "async-lock 2.8.0", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix 0.38.32", + "signal-hook-registry", + "slab", + "windows-sys 0.48.0", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.94", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "backtrace" +version = "0.3.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +dependencies = [ + "serde", +] + +[[package]] +name = "bitmaps" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031043d04099746d8db04daf1fa424b2bc8bd69d92b25962dcde24da39ab64a2" +dependencies = [ + "typenum", +] + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2 0.5.2", +] + +[[package]] +name = "block2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "340d2f0bdb2a43c1d3cd40513185b2bd7def0aa1052f956455114bc98f82dcf2" +dependencies = [ + "objc2 0.6.3", +] + +[[package]] +name = "blocking" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[package]] +name = "borsh" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2506947f73ad44e344215ccd6403ac2ae18cd8e046e581a441bf8d199f257f03" +dependencies = [ + "cfg_aliases", +] + +[[package]] +name = "bstr" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c79ad7fb2dd38f3dabd76b09c6a5a20c038fc0213ef1e9afd30eb777f120f019" +dependencies = [ + "memchr", + "regex-automata", + "serde", +] + +[[package]] +name = "bumpalo" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" + +[[package]] +name = "bytecount" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c676a478f63e9fa2dd5368a42f28bba0d6c560b775f38583c8bbaa7fcd67c9c" + +[[package]] +name = "bytemuck" +version = "1.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6b1fc10dbac614ebc03540c9dbd60e83887fda27794998c6528f1782047d540" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "965ab7eb5f8f97d2a083c799f3a1b994fc397b2fe2da5d1da1626ce15a39f2b1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.94", +] + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "bytes" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" + +[[package]] +name = "calloop" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b50b5a44d59a98c55a9eeb518f39bf7499ba19fd98ee7d22618687f3f10adbf" +dependencies = [ + "bitflags 2.9.1", + "log", + "polling", + "rustix 0.38.32", + "slab", + "thiserror 1.0.69", +] + +[[package]] +name = "calloop" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" +dependencies = [ + "bitflags 2.9.1", + "log", + "polling", + "rustix 0.38.32", + "slab", + "thiserror 1.0.69", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f0ea9b9476c7fad82841a8dbb380e2eae480c21910feba80725b46931ed8f02" +dependencies = [ + "calloop 0.12.3", + "rustix 0.38.32", + "wayland-backend", + "wayland-client", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" +dependencies = [ + "calloop 0.13.0", + "rustix 0.38.32", + "wayland-backend", + "wayland-client", +] + +[[package]] +name = "cap-fs-ext" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b779b2d0a001c125b4584ad586268fb4b92d957bff8d26d7fe0dd78283faa814" +dependencies = [ + "cap-primitives", + "cap-std", + "io-lifetimes", + "windows-sys 0.48.0", +] + +[[package]] +name = "cap-net-ext" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ffc30dee200c20b4dcb80572226f42658e1d9c4b668656d7cc59c33d50e396e" +dependencies = [ + "cap-primitives", + "cap-std", + "rustix 0.38.32", + "smallvec", +] + +[[package]] +name = "cap-primitives" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf30c373a3bee22c292b1b6a7a26736a38376840f1af3d2d806455edf8c3899" +dependencies = [ + "ambient-authority", + "fs-set-times", + "io-extras", + "io-lifetimes", + "ipnet", + "maybe-owned", + "rustix 0.38.32", + "windows-sys 0.48.0", + "winx", +] + +[[package]] +name = "cap-rand" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "577de6cff7c2a47d6b13efe5dd28bf116bd7f8f7db164ea95b7cc2640711f522" +dependencies = [ + "ambient-authority", + "rand 0.8.5", +] + +[[package]] +name = "cap-std" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84bade423fa6403efeebeafe568fdb230e8c590a275fba2ba978dd112efcf6e9" +dependencies = [ + "cap-primitives", + "io-extras", + "io-lifetimes", + "rustix 0.38.32", +] + +[[package]] +name = "cap-time-ext" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8f52b3c8f4abfe3252fd0a071f3004aaa3b18936ec97bdbd8763ce03aff6247" +dependencies = [ + "cap-primitives", + "once_cell", + "rustix 0.38.32", + "winx", +] + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] +name = "cc" +version = "1.0.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c51067fd44124faa7f870b4b1c969379ad32b2ba805aa959430ceaa384f695" +dependencies = [ + "jobserver", + "libc", + "once_cell", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets 0.52.6", +] + +[[package]] +name = "ciborium" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "effd91f6c78e5a4ace8a5d3c0b6bfaec9e2baaef55f3efc00e45fb2e477ee926" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdf919175532b369853f5d5e20b26b43112613fd6fe7aee757e35f7a44642656" + +[[package]] +name = "ciborium-ll" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "defaa24ecc093c77630e6c15e17c51f5e187bf35ee514f4e2d67baaa96dae22b" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "clap" +version = "4.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c918d541ef2913577a0f9566e9ce27cb35b6df072075769e0b26cb5a554520da" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f3e7391dad68afb0c2ede1bf619f579a3dc9c2ec67f089baa397123a2f3d1eb" +dependencies = [ + "anstyle", + "clap_lex", +] + +[[package]] +name = "clap_derive" +version = "4.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "307bc0538d5f0f83b8248db3087aa92fe504e4691294d0c96c0eabc33f47ba47" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 2.0.94", +] + +[[package]] +name = "clap_lex" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" + +[[package]] +name = "clipboard-win" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fdf5e01086b6be750428ba4a40619f847eb2e95756eee84b18e06e5f0b50342" +dependencies = [ + "lazy-bytes-cast", + "winapi", +] + +[[package]] +name = "cocoa" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c" +dependencies = [ + "bitflags 1.3.2", + "block", + "cocoa-foundation", + "core-foundation", + "core-graphics", + "foreign-types 0.5.0", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ade49b65d560ca58c403a479bb396592b155c0185eada742ee323d1d68d6318" +dependencies = [ + "bitflags 1.3.2", + "block", + "core-foundation", + "core-graphics-types", + "foreign-types 0.3.2", + "libc", + "objc", +] + +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + +[[package]] +name = "color" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18ef4657441fb193b65f34dc39b3781f0dfec23d3bd94d0eeb4e88cde421edb" +dependencies = [ + "serde", +] + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "combine" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "config" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23738e11972c7643e4ec947840fc463b6a571afcd3e735bdfce7d03c7a784aca" +dependencies = [ + "async-trait", + "lazy_static", + "nom", + "pathdiff", + "serde", + "toml 0.5.9", +] + +[[package]] +name = "copypasta" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deb85422867ca93da58b7f95fb5c0c10f6183ed6e1ef8841568968a896d3a858" +dependencies = [ + "clipboard-win", + "objc", + "objc-foundation", + "objc_id", + "smithay-clipboard", + "x11-clipboard", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + +[[package]] +name = "core-graphics" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "970a29baf4110c26fedbc7f82107d42c23f7e88e404c4577ed73fe99ff85a212" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-graphics-types", + "foreign-types 0.5.0", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "libc", +] + +[[package]] +name = "core_maths" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3b02505ccb8c50b0aa21ace0fc08c3e53adebd4e58caa18a36152803c7709a3" +dependencies = [ + "libm", +] + +[[package]] +name = "cosmic-text" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da46a9d5a8905cc538a4a5bceb6a4510de7a51049c5588c0114efce102bcbbe8" +dependencies = [ + "bitflags 2.9.1", + "fontdb 0.16.2", + "log", + "rangemap", + "rustc-hash 1.1.0", + "rustybuzz 0.14.1", + "self_cell", + "smol_str 0.2.2", + "swash", + "sys-locale", + "ttf-parser 0.21.1", + "unicode-bidi", + "unicode-linebreak", + "unicode-script", + "unicode-segmentation", +] + +[[package]] +name = "cpp_demangle" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeaa953eaad386a53111e47172c2fedba671e5684c8dd601a5f474f4f118710f" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "cpufeatures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" +dependencies = [ + "libc", +] + +[[package]] +name = "cranelift-bforest" +version = "0.101.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b5bb9245ec7dcc04d03110e538d31f0969d301c9d673145f4b4d5c3478539a3" +dependencies = [ + "cranelift-entity", +] + +[[package]] +name = "cranelift-codegen" +version = "0.101.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebb18d10e5ddac43ba4ca8fd4e310938569c3e484cc01b6372b27dc5bb4dfd28" +dependencies = [ + "bumpalo", + "cranelift-bforest", + "cranelift-codegen-meta", + "cranelift-codegen-shared", + "cranelift-control", + "cranelift-entity", + "cranelift-isle", + "gimli", + "hashbrown 0.14.5", + "log", + "regalloc2", + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cranelift-codegen-meta" +version = "0.101.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a3ce6d22982c1b9b6b012654258bab1a13947bb12703518bef06b1a4867c3d6" +dependencies = [ + "cranelift-codegen-shared", +] + +[[package]] +name = "cranelift-codegen-shared" +version = "0.101.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47220fd4f9a0ce23541652b6f16f83868d282602c600d14934b2a4c166b4bd80" + +[[package]] +name = "cranelift-control" +version = "0.101.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5a4c42672aea9b6e820046b52e47a1c05d3394a6cdf4cb3c3c4b702f954bd2" +dependencies = [ + "arbitrary", +] + +[[package]] +name = "cranelift-entity" +version = "0.101.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b4e9a3296fc827f9d35135dc2c0c8dd8d8359eb1ef904bae2d55d5bcb0c9f94" +dependencies = [ + "serde", + "serde_derive", +] + +[[package]] +name = "cranelift-frontend" +version = "0.101.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aaadf1e7cf28886bbf046eaf7ef538997bc8a7e020e578ea4957b39da87d5a1" +dependencies = [ + "cranelift-codegen", + "log", + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cranelift-isle" +version = "0.101.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45bab6d69919d210a50331d35cc6ce111567bc040aebac63a8ae130d0400a075" + +[[package]] +name = "cranelift-native" +version = "0.101.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76fb52ba71be98312f35e798d9e98e45ab2586f27584231bf7c644fa9501e8af" +dependencies = [ + "cranelift-codegen", + "libc", + "target-lexicon", +] + +[[package]] +name = "cranelift-wasm" +version = "0.101.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1b2f48857ec7c051af938c72b5fdf370a5eec255c8a2be87633241af3c05772" +dependencies = [ + "cranelift-codegen", + "cranelift-entity", + "cranelift-frontend", + "itertools 0.10.5", + "log", + "smallvec", + "wasmparser", + "wasmtime-types", +] + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "criterion" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "is-terminal", + "itertools 0.10.5", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools 0.10.5", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "ctor" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37e366bff8cd32dd8754b0991fb66b279dc48f598c3a18914852a6673deef583" +dependencies = [ + "quote", + "syn 2.0.94", +] + +[[package]] +name = "cursor-icon" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" +dependencies = [ + "serde", +] + +[[package]] +name = "darling" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 1.0.109", +] + +[[package]] +name = "darling_macro" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" +dependencies = [ + "darling_core", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "data-url" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c297a1c74b71ae29df00c3e22dd9534821d60eb9af5a0192823fa2acea70c2a" + +[[package]] +name = "debugid" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d" +dependencies = [ + "uuid", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "directories" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f51c5d4ddabd36886dd3e1438cb358cdcb0d7c499cb99cb4ac2e38e18b5cb210" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "directories-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dispatch2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +dependencies = [ + "bitflags 2.9.1", + "block2 0.6.1", + "libc", + "objc2 0.6.3", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.94", +] + +[[package]] +name = "dlib" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" +dependencies = [ + "libloading", +] + +[[package]] +name = "dmg" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abc28c350337837f23b4750f774371f63db232338c9f89bdb9eb48523a7c4722" +dependencies = [ + "log", + "plist", +] + +[[package]] +name = "document-features" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb6969eaabd2421f8a2775cfd2471a2b634372b4a25d41e3bd647b79912850a0" +dependencies = [ + "litrs", +] + +[[package]] +name = "downcast-rs" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" + +[[package]] +name = "dpi" +version = "0.1.1" +source = "git+https://github.com/rust-windowing/winit?rev=ee245c569d65fdeacf705ee5eedb564508d10ebe#ee245c569d65fdeacf705ee5eedb564508d10ebe" +dependencies = [ + "serde", +] + +[[package]] +name = "drm" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0f8a69e60d75ae7dab4ef26a59ca99f2a89d4c142089b537775ae0c198bdcde" +dependencies = [ + "bitflags 2.9.1", + "bytemuck", + "drm-ffi", + "drm-fourcc", + "rustix 0.38.32", +] + +[[package]] +name = "drm-ffi" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41334f8405792483e32ad05fbb9c5680ff4e84491883d2947a4757dc54cb2ac6" +dependencies = [ + "drm-sys", + "rustix 0.38.32", +] + +[[package]] +name = "drm-fourcc" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aafbcdb8afc29c1a7ee5fbe53b5d62f4565b35a042a662ca9fecd0b54dae6f4" + +[[package]] +name = "drm-sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d09ff881f92f118b11105ba5e34ff8f4adf27b30dae8f12e28c193af1c83176" +dependencies = [ + "libc", + "linux-raw-sys 0.6.4", +] + +[[package]] +name = "dyn-clone" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "545b22097d44f8a9581187cdf93de7a71e4722bf51200cfaba810865b49a495d" + +[[package]] +name = "educe" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417" +dependencies = [ + "enum-ordinalize", + "proc-macro2", + "quote", + "syn 2.0.94", +] + +[[package]] +name = "either" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" + +[[package]] +name = "encoding_rs" +version = "0.8.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "encoding_rs_io" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cc3c5651fb62ab8aa3103998dade57efdd028544bd300516baa31840c252a83" +dependencies = [ + "encoding_rs", +] + +[[package]] +name = "endi" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" + +[[package]] +name = "enum-ordinalize" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea0dcfa4e54eeb516fe454635a95753ddd39acda650ce703031c6973e315dd5" +dependencies = [ + "enum-ordinalize-derive", +] + +[[package]] +name = "enum-ordinalize-derive" +version = "4.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d28318a75d4aead5c4db25382e8ef717932d0346600cacae6357eb5941bc5ff" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.94", +] + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.94", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "euclid" +version = "0.22.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad9cdb4b747e485a12abb0e6566612956c7a1bafa3bdb8d682c5b6d403589e48" +dependencies = [ + "num-traits", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "event-listener" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b215c49b2b248c855fb73579eb1f4f26c38ffdc12973e20e07b91d78d5646e" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "958e4d70b6d5e81971bebec42271ec641e7ff4e170a6fa605f2b8a8b65cb97d3" +dependencies = [ + "event-listener 4.0.3", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "feedafcaa9b749175d5ac357452a9d41ea2911da598fde46ce1fe02c37751291" +dependencies = [ + "event-listener 5.4.0", + "pin-project-lite", +] + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fastrand" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" + +[[package]] +name = "fd-lock" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b0377f1edc77dbd1118507bc7a66e4ab64d2b90c66f90726dc801e73a8c68f9" +dependencies = [ + "cfg-if", + "rustix 0.38.32", + "windows-sys 0.48.0", +] + +[[package]] +name = "fdeflate" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d329bdeac514ee06249dabc27877490f17f5d371ec693360768b838e19f3ae10" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "filetime" +version = "0.2.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" +dependencies = [ + "cfg-if", + "libc", + "libredox", + "windows-sys 0.60.2", +] + +[[package]] +name = "flate2" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" + +[[package]] +name = "floem" +version = "0.2.0" +source = "git+https://github.com/lapce/floem?rev=31fa8f444c37f4c314f47d88c23ffdbc25f2ab53#31fa8f444c37f4c314f47d88c23ffdbc25f2ab53" +dependencies = [ + "bitflags 2.9.1", + "clipboard-win", + "copypasta", + "educe", + "floem-editor-core", + "floem_reactive", + "floem_renderer", + "floem_tiny_skia_renderer", + "floem_vger_renderer", + "im", + "im-rc", + "image", + "indexmap", + "lapce-xi-rope", + "muda", + "objc2 0.6.3", + "objc2-app-kit 0.3.2", + "objc2-foundation 0.3.2", + "parking_lot", + "peniko", + "raw-window-handle", + "rfd", + "rustc-hash 2.1.1", + "serde", + "sha2", + "slotmap", + "smallvec", + "strum 0.27.2", + "strum_macros 0.27.2", + "taffy", + "unicode-segmentation", + "wasm-bindgen-futures", + "web-time", + "wgpu", + "winit", +] + +[[package]] +name = "floem-editor-core" +version = "0.2.0" +source = "git+https://github.com/lapce/floem?rev=31fa8f444c37f4c314f47d88c23ffdbc25f2ab53#31fa8f444c37f4c314f47d88c23ffdbc25f2ab53" +dependencies = [ + "bitflags 2.9.1", + "itertools 0.14.0", + "lapce-xi-rope", + "memchr", + "serde", + "strum 0.27.2", + "strum_macros 0.27.2", +] + +[[package]] +name = "floem-vger" +version = "0.3.1" +source = "git+https://github.com/lapce/vger-rs.git?rev=3206d47ec1e30b645ddcb0687036ba3e1f0d98ec#3206d47ec1e30b645ddcb0687036ba3e1f0d98ec" +dependencies = [ + "cosmic-text", + "euclid", + "fontdue", + "rect_packer", + "wgpu", +] + +[[package]] +name = "floem_reactive" +version = "0.2.0" +source = "git+https://github.com/lapce/floem?rev=31fa8f444c37f4c314f47d88c23ffdbc25f2ab53#31fa8f444c37f4c314f47d88c23ffdbc25f2ab53" +dependencies = [ + "smallvec", +] + +[[package]] +name = "floem_renderer" +version = "0.2.0" +source = "git+https://github.com/lapce/floem?rev=31fa8f444c37f4c314f47d88c23ffdbc25f2ab53#31fa8f444c37f4c314f47d88c23ffdbc25f2ab53" +dependencies = [ + "cosmic-text", + "futures", + "parking_lot", + "peniko", + "resvg", + "swash", + "wasm-bindgen-futures", + "wgpu", + "winit", +] + +[[package]] +name = "floem_tiny_skia_renderer" +version = "0.2.0" +source = "git+https://github.com/lapce/floem?rev=31fa8f444c37f4c314f47d88c23ffdbc25f2ab53#31fa8f444c37f4c314f47d88c23ffdbc25f2ab53" +dependencies = [ + "anyhow", + "floem_renderer", + "peniko", + "raw-window-handle", + "resvg", + "softbuffer", +] + +[[package]] +name = "floem_vger_renderer" +version = "0.2.0" +source = "git+https://github.com/lapce/floem?rev=31fa8f444c37f4c314f47d88c23ffdbc25f2ab53#31fa8f444c37f4c314f47d88c23ffdbc25f2ab53" +dependencies = [ + "anyhow", + "floem-vger", + "floem_renderer", + "image", + "peniko", + "resvg", + "wgpu", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "font-types" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02a596f5713680923a2080d86de50fe472fb290693cf0f701187a1c8b36996b7" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "fontconfig-parser" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a595cb550439a117696039dfc69830492058211b771a2a165379f2a1a53d84d" +dependencies = [ + "roxmltree 0.19.0", +] + +[[package]] +name = "fontdb" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0299020c3ef3f60f526a4f64ab4a3d4ce116b1acbf24cdd22da0068e5d81dc3" +dependencies = [ + "fontconfig-parser", + "log", + "memmap2", + "slotmap", + "tinyvec", + "ttf-parser 0.20.0", +] + +[[package]] +name = "fontdb" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "457e789b3d1202543297a350643cf459f836cade38934e7a4cf6a39e7cde2905" +dependencies = [ + "fontconfig-parser", + "log", + "memmap2", + "slotmap", + "tinyvec", + "ttf-parser 0.25.1", +] + +[[package]] +name = "fontdue" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e57e16b3fe8ff4364c0661fdaac543fb38b29ea9bc9c2f45612d90adf931d2b" +dependencies = [ + "hashbrown 0.15.4", + "ttf-parser 0.21.1", +] + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared 0.1.1", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared 0.3.1", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.94", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs-set-times" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd738b84894214045e8414eaded76359b4a5773f0a0a56b16575110739cdcf39" +dependencies = [ + "io-lifetimes", + "rustix 0.38.32", + "windows-sys 0.48.0", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-lite" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.94", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "fxprof-processed-profile" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27d12c0aed7f1e24276a241aadc4cb8ea9f83000f34bc062b7cc2d51e3b0fabd" +dependencies = [ + "bitflags 2.9.1", + "debugid", + "fxhash", + "serde", + "serde_json", +] + +[[package]] +name = "generic-array" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "gethostname" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" +dependencies = [ + "libc", + "windows-targets 0.48.5", +] + +[[package]] +name = "getopts" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "getrandom" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", +] + +[[package]] +name = "gif" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "gimli" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" +dependencies = [ + "fallible-iterator", + "indexmap", + "stable_deref_trait", +] + +[[package]] +name = "git2" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2deb07a133b1520dc1a5690e9bd08950108873d7ed5de38dcc74d3b5ebffa110" +dependencies = [ + "bitflags 2.9.1", + "libc", + "libgit2-sys", + "log", + "openssl-probe", + "openssl-sys", + "url", +] + +[[package]] +name = "gl_generator" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" +dependencies = [ + "khronos_api", + "log", + "xml-rs", +] + +[[package]] +name = "globset" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "glow" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e5ea60d70410161c8bf5da3fdfeaa1c72ed2c15f8bbb9d19fe3a4fad085f08" +dependencies = [ + "js-sys", + "slotmap", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "glutin_wgl_sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a4e1951bbd9434a81aa496fe59ccc2235af3820d27b85f9314e279609211e2c" +dependencies = [ + "gl_generator", +] + +[[package]] +name = "gpu-alloc" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171" +dependencies = [ + "bitflags 2.9.1", + "gpu-alloc-types", +] + +[[package]] +name = "gpu-alloc-types" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4" +dependencies = [ + "bitflags 2.9.1", +] + +[[package]] +name = "gpu-allocator" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c151a2a5ef800297b4e79efa4f4bec035c5f51d5ae587287c9b952bdf734cacd" +dependencies = [ + "log", + "presser", + "thiserror 1.0.69", + "windows 0.58.0", +] + +[[package]] +name = "gpu-descriptor" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca" +dependencies = [ + "bitflags 2.9.1", + "gpu-descriptor-types", + "hashbrown 0.15.4", +] + +[[package]] +name = "gpu-descriptor-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" +dependencies = [ + "bitflags 2.9.1", +] + +[[package]] +name = "grep-matcher" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47a3141a10a43acfedc7c98a60a834d7ba00dfe7bec9071cbfc19b55b292ac02" +dependencies = [ + "memchr", +] + +[[package]] +name = "grep-regex" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f748bb135ca835da5cbc67ca0e6955f968db9c5df74ca4f56b18e1ddbc68230d" +dependencies = [ + "bstr", + "grep-matcher", + "log", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "grep-searcher" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba536ae4f69bec62d8839584dd3153d3028ef31bb229f04e09fb5a9e5a193c54" +dependencies = [ + "bstr", + "encoding_rs", + "encoding_rs_io", + "grep-matcher", + "log", + "memchr", + "memmap2", +] + +[[package]] +name = "grid" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71b01d27060ad58be4663b9e4ac9e2d4806918e8876af8912afbddd1a91d5eaa" + +[[package]] +name = "h2" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing 0.1.41", +] + +[[package]] +name = "half" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "hashbrown" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hexf-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" + +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "http" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" + +[[package]] +name = "human-sort" +version = "0.2.2" +source = "git+https://github.com/dragazo/human-sort?rev=1e74db1e09e8194ba88ad983723cf6f8b0c365da#1e74db1e09e8194ba88ad983723cf6f8b0c365da" + +[[package]] +name = "hyper" +version = "0.14.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing 0.1.41", + "want", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd911b35d940d2bd0bea0f9100068e5b97b51a1cbe13d13382f132e0365257a0" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "js-sys", + "wasm-bindgen", + "winapi", +] + +[[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2549ca8c7241c82f59c80ba2a6f415d931c5b58d24fb8412caa1a1f02c49139a" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8197e866e47b68f8f7d95249e172903bec06004b18b2937f1095d40a0c57de04" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "ignore" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbe7873dab538a9a44ad79ede1faf5f30d49f9a5c883ddbab48bce81b64b7492" +dependencies = [ + "globset", + "lazy_static", + "log", + "memchr", + "regex", + "same-file", + "thread_local", + "walkdir", + "winapi-util", +] + +[[package]] +name = "im" +version = "15.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0acd33ff0285af998aaf9b57342af478078f53492322fafc47450e09397e0e9" +dependencies = [ + "bitmaps", + "rand_core 0.6.4", + "rand_xoshiro", + "serde", + "sized-chunks", + "typenum", + "version_check", +] + +[[package]] +name = "im-rc" +version = "15.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1955a75fa080c677d3972822ec4bad316169ab1cfc6c257a942c2265dbe5fe" +dependencies = [ + "bitmaps", + "rand_core 0.6.4", + "rand_xoshiro", + "sized-chunks", + "typenum", + "version_check", +] + +[[package]] +name = "image" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd54d660e773627692c524beaad361aca785a4f9f5730ce91f42aabe5bce3d11" +dependencies = [ + "bytemuck", + "byteorder", + "image-webp 0.1.3", + "num-traits", + "png", + "zune-core", + "zune-jpeg", +] + +[[package]] +name = "image-webp" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f79afb8cbee2ef20f59ccd477a218c12a93943d075b492015ecb1bb81f8ee904" +dependencies = [ + "byteorder-lite", + "quick-error", +] + +[[package]] +name = "image-webp" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6970fe7a5300b4b42e62c52efa0187540a5bef546c60edaf554ef595d2e6f0b" +dependencies = [ + "byteorder-lite", + "quick-error", +] + +[[package]] +name = "imagesize" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285" + +[[package]] +name = "include_dir" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd" +dependencies = [ + "include_dir_macros", +] + +[[package]] +name = "include_dir_macros" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "indexmap" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +dependencies = [ + "equivalent", + "hashbrown 0.15.4", + "serde", +] + +[[package]] +name = "inotify" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" +dependencies = [ + "bitflags 1.3.2", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + +[[package]] +name = "interprocess" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f2533f3be42fffe3b5e63b71aeca416c1c3bc33e4e27be018521e76b1f38fb" +dependencies = [ + "blocking", + "cfg-if", + "futures-core", + "futures-io", + "intmap", + "libc", + "once_cell", + "rustc_version", + "spinning", + "thiserror 1.0.69", + "to_method", + "winapi", +] + +[[package]] +name = "intmap" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae52f28f45ac2bc96edb7714de995cffc174a395fb0abf5bff453587c980d7b9" + +[[package]] +name = "io-extras" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d3c230ee517ee76b1cc593b52939ff68deda3fae9e41eca426c6b4993df51c4" +dependencies = [ + "io-lifetimes", + "windows-sys 0.48.0", +] + +[[package]] +name = "io-lifetimes" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bffb4def18c48926ccac55c1223e02865ce1a821751a95920448662696e7472c" + +[[package]] +name = "ipnet" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b" + +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-terminal" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754" + +[[package]] +name = "ittapi" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25a5c0b993601cad796222ea076565c5d9f337d35592f8622c753724f06d7271" +dependencies = [ + "anyhow", + "ittapi-sys", + "log", +] + +[[package]] +name = "ittapi-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7b5e473765060536a660eed127f758cf1a810c73e49063264959c60d1727d9" +dependencies = [ + "cc", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "jobserver" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "jsonrpc-lite" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4128aba82294c14af2998831c4df3c843940e92b5cfc41bac1229d1e63b88c" +dependencies = [ + "serde", + "serde_derive", + "serde_json", +] + +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.9.1", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "khronos-egl" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" +dependencies = [ + "libc", + "libloading", + "pkg-config", +] + +[[package]] +name = "khronos_api" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" + +[[package]] +name = "kqueue" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d6112e8f37b59803ac47a42d14f1f3a59bbf72fc6857ffc5be455e28a691f8e" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8367585489f01bc55dd27404dcf56b95e6da061a256a666ab23be9ba96a2e587" +dependencies = [ + "bitflags 1.3.2", + "libc", +] + +[[package]] +name = "kurbo" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1077d333efea6170d9ccb96d3c3026f300ca0773da4938cc4c811daa6df68b0c" +dependencies = [ + "arrayvec", + "serde", + "smallvec", +] + +[[package]] +name = "lapce" +version = "0.4.6" +dependencies = [ + "lapce-app", + "lapce-proxy", +] + +[[package]] +name = "lapce-app" +version = "0.4.6" +dependencies = [ + "Inflector", + "alacritty_terminal", + "anyhow", + "backtrace", + "base64 0.21.7", + "bytemuck", + "chrono", + "clap", + "config", + "criterion", + "crossbeam-channel", + "dmg", + "flate2", + "floem", + "fs_extra", + "globset", + "im", + "include_dir", + "indexmap", + "interprocess", + "itertools 0.12.1", + "lapce-core", + "lapce-proxy", + "lapce-rpc", + "lapce-xi-rope", + "lsp-types", + "notify", + "nucleo", + "once_cell", + "open", + "parking_lot", + "percent-encoding", + "pulldown-cmark", + "rayon", + "regex", + "reqwest", + "semver", + "serde", + "serde_json", + "sha2", + "smallvec", + "structdesc", + "strum 0.27.2", + "strum_macros 0.27.2", + "tar", + "tempfile", + "thiserror 1.0.69", + "toml 0.8.2", + "toml_edit 0.20.2", + "tracing 0.2.0", + "tracing-appender", + "tracing-log", + "tracing-subscriber", + "unicode-width", + "url", + "windows-sys 0.60.2", + "zip", + "zstd", +] + +[[package]] +name = "lapce-core" +version = "0.4.6" +dependencies = [ + "ahash", + "anyhow", + "arc-swap", + "directories", + "floem-editor-core", + "git2", + "hashbrown 0.14.5", + "include_dir", + "itertools 0.12.1", + "lapce-rpc", + "lapce-xi-rope", + "libloading", + "lsp-types", + "once_cell", + "regex", + "remain", + "slotmap", + "strum 0.27.2", + "strum_macros 0.27.2", + "thiserror 1.0.69", + "tracing 0.2.0", + "tree-sitter", +] + +[[package]] +name = "lapce-proxy" +version = "0.4.6" +dependencies = [ + "alacritty_terminal", + "anyhow", + "clap", + "crossbeam-channel", + "directories", + "dyn-clone", + "flate2", + "floem-editor-core", + "git2", + "globset", + "grep-matcher", + "grep-regex", + "grep-searcher", + "ignore", + "indexmap", + "interprocess", + "jsonrpc-lite", + "lapce-core", + "lapce-rpc", + "lapce-xi-rope", + "libc", + "locale_config", + "lsp-types", + "notify", + "parking_lot", + "polling", + "psp-types", + "regex", + "reqwest", + "serde", + "serde_json", + "tar", + "toml 0.8.2", + "tracing 0.2.0", + "tracing-log", + "trash", + "url", + "walkdir", + "wasi-common", + "wasi-experimental-http-wasmtime", + "wasmtime", + "wasmtime-wasi", + "zstd", +] + +[[package]] +name = "lapce-rpc" +version = "0.4.6" +dependencies = [ + "anyhow", + "crossbeam-channel", + "human-sort", + "indexmap", + "lapce-xi-rope", + "lsp-types", + "parking_lot", + "serde", + "serde_json", + "tracing 0.2.0", + "url", +] + +[[package]] +name = "lapce-xi-rope" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6516aaa99c5059dc1a1bc02ed782d5e524699c1b4330028a6bed8259f9d9ff0a" +dependencies = [ + "bytecount", + "memchr", + "regex", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "lazy-bytes-cast" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10257499f089cd156ad82d0a9cd57d9501fa2c989068992a97eb3c27836f206b" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "leb128" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" + +[[package]] +name = "libc" +version = "0.2.180" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" + +[[package]] +name = "libgit2-sys" +version = "0.18.1+1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1dcb20f84ffcdd825c7a311ae347cce604a6f084a767dec4a4929829645290e" +dependencies = [ + "cc", + "libc", + "libssh2-sys", + "libz-sys", + "openssl-sys", + "pkg-config", +] + +[[package]] +name = "libloading" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c571b676ddfc9a8c12f1f3d3085a7b163966a8fd8098a90640953ce5f6170161" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "libm" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" + +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.9.1", + "libc", + "redox_syscall 0.5.8", +] + +[[package]] +name = "libssh2-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dc8a030b787e2119a731f1951d6a773e2280c660f8ec4b0f5e1505a386e71ee" +dependencies = [ + "cc", + "libc", + "libz-sys", + "openssl-sys", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "libz-sys" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9702761c3935f8cc2f101793272e202c72b99da8f4224a19ddcf1279a6450bbf" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "line-wrap" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd1bc4d24ad230d21fb898d1116b1801d7adfc449d42026475862ab48b11e70e" + +[[package]] +name = "linebender_resource_handle" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a5ff6bcca6c4867b1c4fd4ef63e4db7436ef363e0ad7531d1558856bae64f4" +dependencies = [ + "serde", + "serde_bytes", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" + +[[package]] +name = "linux-raw-sys" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0b5399f6804fbab912acbd8878ed3532d506b7c951b8f9f164ef90fef39e3f4" + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + +[[package]] +name = "litrs" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" + +[[package]] +name = "locale_config" +version = "0.3.1-alpha.0" +source = "git+https://github.com/lapce/locale_config.git?branch=lapce#54c9fe6a247c3618c224ec57e6c3a747bc3a96e4" +dependencies = [ + "lazy_static", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "regex", + "winapi", +] + +[[package]] +name = "lock_api" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "lsp-types" +version = "0.95.1" +source = "git+https://github.com/lapce/lsp-types?rev=feaa1e2ec80975c9dadd400a238ceacf071058e6#feaa1e2ec80975c9dadd400a238ceacf071058e6" +dependencies = [ + "bitflags 1.3.2", + "serde", + "serde_json", + "serde_repr", + "url", +] + +[[package]] +name = "mach" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa" +dependencies = [ + "libc", +] + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "maybe-owned" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4facc753ae494aeb6e3c22f839b158aebd4f9270f55cd3c79906c45476c47ab4" + +[[package]] +name = "memchr" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" + +[[package]] +name = "memfd" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2cffa4ad52c6f791f4f8b15f0c05f9824b2ced1160e88cc393d64fff9a8ac64" +dependencies = [ + "rustix 0.38.32", +] + +[[package]] +name = "memmap2" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deaba38d7abf1d4cca21cc89e932e542ba2b9258664d2a9ef0e61512039c9375" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "metal" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f569fb946490b5743ad69813cb19629130ce9374034abe31614a36402d18f99e" +dependencies = [ + "bitflags 2.9.1", + "block", + "core-graphics-types", + "foreign-types 0.5.0", + "log", + "objc", + "paste", +] + +[[package]] +name = "mime" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "log", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.48.0", +] + +[[package]] +name = "miow" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "359f76430b20a79f9e20e115b3428614e654f04fab314482fc0fda0ebd3c6044" +dependencies = [ + "windows-sys 0.48.0", +] + +[[package]] +name = "muda" +version = "0.16.1" +source = "git+https://github.com/tauri-apps/muda?rev=8e986af3cea96a729413abc75c3702dec3990bd2#8e986af3cea96a729413abc75c3702dec3990bd2" +dependencies = [ + "crossbeam-channel", + "dpi", + "keyboard-types", + "objc2 0.6.3", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "once_cell", + "png", + "thiserror 2.0.12", + "windows-sys 0.59.0", +] + +[[package]] +name = "naga" +version = "24.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e380993072e52eef724eddfcde0ed013b0c023c3f0417336ed041aa9f076994e" +dependencies = [ + "arrayvec", + "bit-set", + "bitflags 2.9.1", + "cfg_aliases", + "codespan-reporting", + "hexf-parse", + "indexmap", + "log", + "rustc-hash 1.1.0", + "spirv", + "strum 0.26.3", + "termcolor", + "thiserror 2.0.12", + "unicode-xid", +] + +[[package]] +name = "native-tls" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd7e2f3618557f980e0b17e8856252eee3c97fa12c54dff0ca290fb6266ca4a9" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.9.1", + "jni-sys", + "log", + "ndk-sys 0.6.0+11769913", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.5.0+25.2.9519653" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags 2.9.1", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + +[[package]] +name = "nom" +version = "7.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "notify" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "729f63e1ca555a43fe3efa4f3efdf4801c479da85b432242a7b726f353c88486" +dependencies = [ + "bitflags 1.3.2", + "crossbeam-channel", + "filetime", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "mio", + "serde", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "nucleo" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5262af4c94921c2646c5ac6ff7900c2af9cbb08dc26a797e18130a7019c039d4" +dependencies = [ + "nucleo-matcher", + "parking_lot", + "rayon", +] + +[[package]] +name = "nucleo-matcher" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf33f538733d1a5a3494b836ba913207f14d9d4a1d3cd67030c5061bdd2cac85" +dependencies = [ + "memchr", + "unicode-segmentation", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "num_enum" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70bf6736f74634d299d00086f02986875b3c2d924781a6a2cb6c201e73da0ceb" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56ea360eafe1022f7cc56cd7b869ed57330fb2453d0c7831d99b74c65d2f5597" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro2", + "quote", + "syn 2.0.94", +] + +[[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-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-app-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" +dependencies = [ + "bitflags 2.9.1", + "block2 0.5.1", + "libc", + "objc2 0.5.2", + "objc2-core-data", + "objc2-core-image", + "objc2-foundation 0.2.2", + "objc2-quartz-core", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.9.1", + "block2 0.6.1", + "objc2 0.6.3", + "objc2-core-foundation", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" +dependencies = [ + "bitflags 2.9.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-core-location", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-contacts" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-data" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" +dependencies = [ + "bitflags 2.9.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.9.1", + "dispatch2", + "objc2 0.6.3", +] + +[[package]] +name = "objc2-core-image" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal", +] + +[[package]] +name = "objc2-core-location" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-contacts", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.9.1", + "block2 0.5.1", + "dispatch", + "libc", + "objc2 0.5.2", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.9.1", + "objc2 0.6.3", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-link-presentation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.9.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +dependencies = [ + "bitflags 2.9.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal", +] + +[[package]] +name = "objc2-symbols" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" +dependencies = [ + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" +dependencies = [ + "bitflags 2.9.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-image", + "objc2-core-location", + "objc2-foundation 0.2.2", + "objc2-link-presentation", + "objc2-quartz-core", + "objc2-symbols", + "objc2-uniform-type-identifiers", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-uniform-type-identifiers" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" +dependencies = [ + "bitflags 2.9.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-core-location", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + +[[package]] +name = "object" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +dependencies = [ + "crc32fast", + "hashbrown 0.14.5", + "indexmap", + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "oorandom" +version = "11.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" + +[[package]] +name = "open" +version = "5.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5ca541f22b1c46d4bb9801014f234758ab4297e7870b904b6a8415b980a7388" +dependencies = [ + "is-wsl", + "libc", + "pathdiff", +] + +[[package]] +name = "openssl" +version = "0.10.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" +dependencies = [ + "bitflags 2.9.1", + "cfg-if", + "foreign-types 0.3.2", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.94", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-src" +version = "300.2.3+3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cff92b6f71555b61bb9315f7c64da3ca43d87531622120fea0195fc761b4843" +dependencies = [ + "cc", +] + +[[package]] +name = "openssl-sys" +version = "0.9.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" +dependencies = [ + "cc", + "libc", + "openssl-src", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "orbclient" +version = "0.3.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba0b26cec2e24f08ed8bb31519a9333140a6599b867dac464bb150bdb796fd43" +dependencies = [ + "libredox", +] + +[[package]] +name = "ordered-float" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" +dependencies = [ + "num-traits", +] + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "owned_ttf_parser" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "706de7e2214113d63a8238d1910463cfce781129a6f263d13fdb09ff64355ba4" +dependencies = [ + "ttf-parser 0.19.1", +] + +[[package]] +name = "parking" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.8", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "paste" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1de2e551fb905ac83f73f7aedf2f0cb4a0da7e35efa24a202a936269f1f18e1" + +[[package]] +name = "pathdiff" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" + +[[package]] +name = "peniko" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b44f9ddd2f480176b34278eb653ec1c8062f3b143a4e16eeff5ffac3334e288" +dependencies = [ + "color", + "kurbo", + "linebender_resource_handle", + "serde", + "smallvec", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pico-args" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" + +[[package]] +name = "pin-project" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be57f64e946e500c8ee36ef6331845d40a93055567ec57e8fae13efd33759b95" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c0f5fad0874fc7abcd4d750e76917eaebbecaa2c20bde22e1dbeeba8beb758c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.94", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "piper" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668d31b1c4eba19242f2088b2bf3316b82ca31082a8335764db4e083db7485d4" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pkg-config" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" + +[[package]] +name = "plist" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9d34169e64b3c7a80c8621a48adaf44e0cf62c78a9b25dd9dd35f1881a17cf9" +dependencies = [ + "base64 0.21.7", + "indexmap", + "line-wrap", + "quick-xml 0.31.0", + "time", +] + +[[package]] +name = "plotters" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2c224ba00d7cadd4d5c660deaf2098e5e80e07846537c51f9cfa4be50c1fd45" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e76628b4d3a7581389a35d5b6e2139607ad7c75b17aed325f210aa91f4a9609" + +[[package]] +name = "plotters-svg" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38f6d39893cca0701371e3c27294f09797214b86f1fb951b89ade8ec04e2abab" +dependencies = [ + "plotters-backend", +] + +[[package]] +name = "png" +version = "0.17.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd75bf2d8dd3702b9707cdbc56a5b9ef42cec752eb8b3bafc01234558442aa64" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "polling" +version = "3.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0c976a60b2d7e99d6f229e414670a9b85d13ac305cc6d1e9c134de58c5aaaf6" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix 0.38.32", + "tracing 0.1.41", + "windows-sys 0.52.0", +] + +[[package]] +name = "pollster" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" + +[[package]] +name = "potential_utf" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" + +[[package]] +name = "presser" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit 0.23.5", +] + +[[package]] +name = "proc-macro2" +version = "1.0.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "profiling" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45f10e75d83c7aec79a6aa46f897075890e156b105eebe51cfa0abce51af025f" + +[[package]] +name = "psm" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5787f7cda34e3033a72192c018bc5883100330f362ef279a8cbccfce8bb4e874" +dependencies = [ + "cc", +] + +[[package]] +name = "psp-types" +version = "0.1.0" +source = "git+https://github.com/lapce/psp-types?rev=f7fea28f59e7b2d6faa1034a21679ad49b3524ad#f7fea28f59e7b2d6faa1034a21679ad49b3524ad" +dependencies = [ + "lsp-types", + "serde", + "serde_json", +] + +[[package]] +name = "pulldown-cmark" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8746739f11d39ce5ad5c2520a9b75285310dbfe78c541ccf832d38615765aec0" +dependencies = [ + "bitflags 2.9.1", + "getopts", + "memchr", + "pulldown-cmark-escape", + "unicase", +] + +[[package]] +name = "pulldown-cmark-escape" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + +[[package]] +name = "quick-xml" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" +dependencies = [ + "memchr", +] + +[[package]] +name = "quick-xml" +version = "0.38.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.7", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.3", +] + +[[package]] +name = "rand_xoshiro" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa" +dependencies = [ + "rand_core 0.6.4", +] + +[[package]] +name = "range-alloc" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8a99fddc9f0ba0a85884b8d14e3592853e787d581ca1816c91349b10e4eeab" + +[[package]] +name = "rangemap" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60fcc7d6849342eff22c4350c8b9a989ee8ceabc4b481253e8946b9fe83d684" + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "read-fonts" +version = "0.29.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04ca636dac446b5664bd16c069c00a9621806895b8bb02c2dc68542b23b8f25d" +dependencies = [ + "bytemuck", + "font-types", +] + +[[package]] +name = "rect_packer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8ffb4dfda4b01cc420847665dc480760d596ce186f2772a66ed32fe9acb1c45" + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" +dependencies = [ + "bitflags 2.9.1", +] + +[[package]] +name = "redox_users" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +dependencies = [ + "getrandom 0.2.7", + "redox_syscall 0.2.16", + "thiserror 1.0.69", +] + +[[package]] +name = "regalloc2" +version = "0.9.3" +source = "git+https://github.com/bytecodealliance/regalloc2?rev=5d79e12d0a93b10fc181f4da409b4671dd365228#5d79e12d0a93b10fc181f4da409b4671dd365228" +dependencies = [ + "hashbrown 0.14.5", + "log", + "rustc-hash 1.1.0", + "slice-group-by", + "smallvec", +] + +[[package]] +name = "regex" +version = "1.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + +[[package]] +name = "remain" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46aef80f842736de545ada6ec65b81ee91504efd6853f4b96de7414c42ae7443" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.94", +] + +[[package]] +name = "renderdoc-sys" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" + +[[package]] +name = "reqwest" +version = "0.11.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +dependencies = [ + "base64 0.21.7", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-native-tls", + "tokio-socks", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + +[[package]] +name = "resvg" +version = "0.45.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8928798c0a55e03c9ca6c4c6846f76377427d2c1e1f7e6de3c06ae57942df43" +dependencies = [ + "gif", + "image-webp 0.2.3", + "log", + "pico-args", + "rgb", + "svgtypes", + "tiny-skia", + "usvg", + "zune-jpeg", +] + +[[package]] +name = "rfd" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef2bee61e6cffa4635c72d7d81a84294e28f0930db0ddcb0f66d10244674ebed" +dependencies = [ + "ashpd", + "block2 0.6.1", + "dispatch2", + "js-sys", + "log", + "objc2 0.6.3", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "pollster", + "raw-window-handle", + "urlencoding", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "rgb" +version = "0.8.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20ec2d3e3fc7a92ced357df9cebd5a10b6fb2aa1ee797bf7e9ce2f17dffc8f59" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "roxmltree" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f" + +[[package]] +name = "roxmltree" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" + +[[package]] +name = "rustc-demangle" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" +dependencies = [ + "bitflags 2.9.1", + "errno", + "itoa", + "libc", + "linux-raw-sys 0.4.13", + "once_cell", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustix" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +dependencies = [ + "bitflags 2.9.1", + "errno", + "libc", + "linux-raw-sys 0.11.0", + "windows-sys 0.60.2", +] + +[[package]] +name = "rustix-openpty" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a25c3aad9fc1424eb82c88087789a7d938e1829724f3e4043163baf0d13cfc12" +dependencies = [ + "errno", + "libc", + "rustix 0.38.32", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.7", +] + +[[package]] +name = "rustversion" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" + +[[package]] +name = "rustybuzz" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfb9cf8877777222e4a3bc7eb247e398b56baba500c38c1c46842431adc8b55c" +dependencies = [ + "bitflags 2.9.1", + "bytemuck", + "libm", + "smallvec", + "ttf-parser 0.21.1", + "unicode-bidi-mirroring 0.2.0", + "unicode-ccc 0.2.0", + "unicode-properties", + "unicode-script", +] + +[[package]] +name = "rustybuzz" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702" +dependencies = [ + "bitflags 2.9.1", + "bytemuck", + "core_maths", + "log", + "smallvec", + "ttf-parser 0.25.1", + "unicode-bidi-mirroring 0.4.0", + "unicode-ccc 0.4.0", + "unicode-properties", + "unicode-script", +] + +[[package]] +name = "ryu" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "sctk-adwaita" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6277f0217056f77f1d8f49f2950ac6c278c0d607c45f5ee99328d792ede24ec" +dependencies = [ + "ab_glyph", + "log", + "memmap2", + "smithay-client-toolkit 0.19.2", + "tiny-skia", +] + +[[package]] +name = "security-framework" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bc1bb97804af6631813c55739f771071e0f2ed33ee20b68c86ec505d906356c" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "self_cell" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d369a96f978623eb3dc28807c4852d6cc617fed53da5d3c400feff1ef34a714a" + +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_bytes" +version = "0.11.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8437fd221bde2d4ca316d61b90e337e9e702b3820b87d63caa9ba6c02bd06d96" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.94", +] + +[[package]] +name = "serde_json" +version = "1.0.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784b6203951c57ff748476b126ccb5e8e2959a5c19e5c617ab1956be3dbc68da" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.94", +] + +[[package]] +name = "serde_spanned" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shellexpand" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ccc8076840c4da029af4f87e4e8daeb0fca6b87bbb02e10cb60b791450e11e4" +dependencies = [ + "dirs", +] + +[[package]] +name = "signal-hook" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a253b5e89e2698464fc26b545c9edceb338e18a89effeeecfea192c3025be29d" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +dependencies = [ + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "simplecss" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a11be7c62927d9427e9f40f3444d5499d868648e2edbc4e2116de69e7ec0e89d" +dependencies = [ + "log", +] + +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + +[[package]] +name = "sized-chunks" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16d69225bde7a69b235da73377861095455d298f2b970996eec25ddbb42b3d1e" +dependencies = [ + "bitmaps", + "typenum", +] + +[[package]] +name = "skrifa" +version = "0.31.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbeb4ca4399663735553a09dd17ce7e49a0a0203f03b706b39628c4d913a8607" +dependencies = [ + "bytemuck", + "read-fonts", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "slice-group-by" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03b634d87b960ab1a38c4fe143b508576f075e7c978bfad18217645ebfdfa2ec" + +[[package]] +name = "slotmap" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" +dependencies = [ + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] + +[[package]] +name = "smithay-client-toolkit" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60e3d9941fa3bacf7c2bf4b065304faa14164151254cd16ce1b1bc8fc381600f" +dependencies = [ + "bitflags 2.9.1", + "calloop 0.12.3", + "calloop-wayland-source 0.2.0", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix 0.38.32", + "thiserror 1.0.69", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols 0.31.0", + "wayland-protocols-wlr 0.2.0", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smithay-client-toolkit" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" +dependencies = [ + "bitflags 2.9.1", + "calloop 0.13.0", + "calloop-wayland-source 0.3.0", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix 0.38.32", + "thiserror 1.0.69", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols 0.32.10", + "wayland-protocols-wlr 0.3.5", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smithay-clipboard" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb62b280ce5a5cba847669933a0948d00904cf83845c944eae96a4738cea1a6" +dependencies = [ + "libc", + "smithay-client-toolkit 0.18.0", + "wayland-backend", +] + +[[package]] +name = "smol_str" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" + +[[package]] +name = "smol_str" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9676b89cd56310a87b93dec47b11af744f34d5fc9f367b829474eec0a891350d" +dependencies = [ + "borsh", + "serde", +] + +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "softbuffer" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61d5d17f23326fe0d9b0af282f73f3af666699420fd5f42629efd9c6e7dc166f" +dependencies = [ + "as-raw-xcb-connection", + "bytemuck", + "cfg_aliases", + "cocoa", + "core-graphics", + "drm", + "fastrand", + "foreign-types 0.5.0", + "js-sys", + "log", + "memmap2", + "objc", + "raw-window-handle", + "redox_syscall 0.5.8", + "rustix 0.38.32", + "tiny-xlib", + "wasm-bindgen", + "wayland-backend", + "wayland-client", + "wayland-sys", + "web-sys", + "windows-sys 0.52.0", + "x11rb", +] + +[[package]] +name = "spinning" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d4f0e86297cad2658d92a707320d87bf4e6ae1050287f51d19b67ef3f153a7b" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spirv" +version = "0.3.0+sdk-1.3.268.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" +dependencies = [ + "bitflags 2.9.1", +] + +[[package]] +name = "sptr" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b9b39299b249ad65f3b7e96443bad61c02ca5cd3589f46cb6d610a0fd6c0d6a" + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strict-num" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" +dependencies = [ + "float-cmp", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "structdesc" +version = "0.1.0" +source = "git+https://github.com/lapce/structdesc?rev=bb56969f22fdb2c2d6c03f158fd4a2bdc983b659#bb56969f22fdb2c2d6c03f158fd4a2bdc983b659" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros 0.26.4", +] + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros 0.27.2", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.94", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.94", +] + +[[package]] +name = "svgtypes" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68c7541fff44b35860c1a7a47a7cadf3e4a304c457b58f9870d9706ece028afc" +dependencies = [ + "kurbo", + "siphasher", +] + +[[package]] +name = "swash" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f745de914febc7c9ab4388dfaf94bbc87e69f57bb41133a9b0c84d4be49856f3" +dependencies = [ + "skrifa", + "yazi", + "zeno", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "987bc0be1cdea8b10216bd06e2ca407d40b9543468fafd3ddfb02f36e77f71f3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.94", +] + +[[package]] +name = "sys-locale" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e801cf239ecd6ccd71f03d270d67dd53d13e90aab208bf4b8fe4ad957ea949b0" +dependencies = [ + "libc", +] + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "system-interface" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27ce32341b2c0b70c144bbf35627fdc1ef18c76ced5e5e7b3ee8b5ba6b2ab6a0" +dependencies = [ + "bitflags 2.9.1", + "cap-fs-ext", + "cap-std", + "fd-lock", + "io-lifetimes", + "rustix 0.38.32", + "windows-sys 0.48.0", + "winx", +] + +[[package]] +name = "taffy" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aaef0ac998e6527d6d0d5582f7e43953bb17221ac75bb8eb2fcc2db3396db1c" +dependencies = [ + "arrayvec", + "grid", + "serde", + "slotmap", +] + +[[package]] +name = "tar" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb797dad5fb5b76fcf519e702f4a589483b5ef06567f160c392832c1f5e44909" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "target-lexicon" +version = "0.12.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d0e916b1148c8e263850e1ebcbd046f333e0683c724876bb0da63ea4373dc8a" + +[[package]] +name = "tempfile" +version = "3.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +dependencies = [ + "cfg-if", + "fastrand", + "rustix 0.38.32", + "windows-sys 0.52.0", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl 2.0.12", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.94", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.94", +] + +[[package]] +name = "thread_local" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" +dependencies = [ + "once_cell", +] + +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tiny-skia" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" +dependencies = [ + "arrayref", + "arrayvec", + "bytemuck", + "cfg-if", + "log", + "png", + "tiny-skia-path", +] + +[[package]] +name = "tiny-skia-path" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", +] + +[[package]] +name = "tiny-xlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4098d49269baa034a8d1eae9bd63e9fa532148d772121dace3bcd6a6c98eb6d" +dependencies = [ + "as-raw-xcb-connection", + "ctor", + "libloading", + "tracing 0.1.41", +] + +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + +[[package]] +name = "to_method" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c4ceeeca15c8384bbc3e011dbd8fccb7f068a440b752b7d9b32ceb0ca0e2e8" + +[[package]] +name = "tokio" +version = "1.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.48.0", +] + +[[package]] +name = "tokio-macros" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.94", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-socks" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51165dfa029d2a65969413a6cc96f354b86b464498702f174a4efa13608fd8c0" +dependencies = [ + "either", + "futures-util", + "thiserror 1.0.69", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb2e075f03b3d66d8d8785356224ba688d2906a371015e225beeb65ca92c740" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing 0.1.41", +] + +[[package]] +name = "toml" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" +dependencies = [ + "serde", +] + +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap", + "toml_datetime 0.6.3", + "winnow 0.5.10", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime 0.6.3", + "winnow 0.5.10", +] + +[[package]] +name = "toml_edit" +version = "0.23.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2ad0b7ae9cfeef5605163839cb9221f453399f15cfb5c10be9885fcf56611f9" +dependencies = [ + "indexmap", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "winnow 0.7.14", +] + +[[package]] +name = "toml_parser" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" +dependencies = [ + "winnow 0.7.14", +] + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes 0.1.28", + "tracing-core 0.1.33", +] + +[[package]] +name = "tracing" +version = "0.2.0" +source = "git+https://github.com/tokio-rs/tracing?rev=908cc432a5994f6e17c8f36e13c217dc40085704#908cc432a5994f6e17c8f36e13c217dc40085704" +dependencies = [ + "pin-project-lite", + "tracing-attributes 0.2.0", + "tracing-core 0.2.0", +] + +[[package]] +name = "tracing-appender" +version = "0.2.0" +source = "git+https://github.com/tokio-rs/tracing?rev=908cc432a5994f6e17c8f36e13c217dc40085704#908cc432a5994f6e17c8f36e13c217dc40085704" +dependencies = [ + "crossbeam-channel", + "thiserror 1.0.69", + "time", + "tracing-subscriber", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.94", +] + +[[package]] +name = "tracing-attributes" +version = "0.2.0" +source = "git+https://github.com/tokio-rs/tracing?rev=908cc432a5994f6e17c8f36e13c217dc40085704#908cc432a5994f6e17c8f36e13c217dc40085704" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.94", +] + +[[package]] +name = "tracing-core" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tracing-core" +version = "0.2.0" +source = "git+https://github.com/tokio-rs/tracing?rev=908cc432a5994f6e17c8f36e13c217dc40085704#908cc432a5994f6e17c8f36e13c217dc40085704" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "git+https://github.com/tokio-rs/tracing?rev=908cc432a5994f6e17c8f36e13c217dc40085704#908cc432a5994f6e17c8f36e13c217dc40085704" +dependencies = [ + "log", + "once_cell", + "tracing-core 0.2.0", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.0" +source = "git+https://github.com/tokio-rs/tracing?rev=908cc432a5994f6e17c8f36e13c217dc40085704#908cc432a5994f6e17c8f36e13c217dc40085704" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core 0.2.0", + "tracing-log", +] + +[[package]] +name = "trash" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af3663fb8f476d674b9c61d1d2796acec725bef6bec4b41402a904252a25971e" +dependencies = [ + "chrono", + "libc", + "log", + "objc", + "once_cell", + "scopeguard", + "url", + "windows 0.44.0", +] + +[[package]] +name = "tree-sitter" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df7cc499ceadd4dcdf7ec6d4cbc34ece92c3fa07821e287aedecd4416c516dca" +dependencies = [ + "cc", + "regex", +] + +[[package]] +name = "try-lock" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" + +[[package]] +name = "ttf-parser" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a464a4b34948a5f67fddd2b823c62d9d92e44be75058b99939eae6c5b6960b33" + +[[package]] +name = "ttf-parser" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17f77d76d837a7830fe1d4f12b7b4ba4192c1888001c7164257e4bc6d21d96b4" + +[[package]] +name = "ttf-parser" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c591d83f69777866b9126b24c6dd9a18351f177e49d625920d19f989fd31cf8" + +[[package]] +name = "ttf-parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" +dependencies = [ + "core_maths", +] + +[[package]] +name = "typenum" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" + +[[package]] +name = "uds_windows" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" +dependencies = [ + "memoffset", + "tempfile", + "winapi", +] + +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" + +[[package]] +name = "unicode-bidi-mirroring" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cb788ffebc92c5948d0e997106233eeb1d8b9512f93f41651f52b6c5f5af86" + +[[package]] +name = "unicode-bidi-mirroring" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfa6e8c60bb66d49db113e0125ee8711b7647b5579dc7f5f19c42357ed039fe" + +[[package]] +name = "unicode-ccc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1df77b101bcc4ea3d78dafc5ad7e4f58ceffe0b2b16bf446aeb50b6cb4157656" + +[[package]] +name = "unicode-ccc" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + +[[package]] +name = "unicode-properties" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" + +[[package]] +name = "unicode-script" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d817255e1bed6dfd4ca47258685d14d2bdcfbc64fdc9e3819bd5848057b8ecc" + +[[package]] +name = "unicode-segmentation" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" + +[[package]] +name = "unicode-vo" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" + +[[package]] +name = "unicode-width" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "url" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "usvg" +version = "0.45.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80be9b06fbae3b8b303400ab20778c80bbaf338f563afe567cf3c9eea17b47ef" +dependencies = [ + "base64 0.22.0", + "data-url", + "flate2", + "fontdb 0.23.0", + "imagesize", + "kurbo", + "log", + "pico-args", + "roxmltree 0.20.0", + "rustybuzz 0.20.1", + "simplecss", + "siphasher", + "strict-num", + "svgtypes", + "tiny-skia-path", + "unicode-bidi", + "unicode-script", + "unicode-vo", + "xmlwriter", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "uuid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "vte" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40eb22ae96f050e0c0d6f7ce43feeae26c348fc4dea56928ca81537cfaa6188b" +dependencies = [ + "bitflags 2.9.1", + "cursor-icon", + "log", + "serde", + "utf8parse", + "vte_generate_state_changes", +] + +[[package]] +name = "vte_generate_state_changes" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d257817081c7dffcdbab24b9e62d2def62e2ff7d00b1c20062551e6cccc145ff" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +dependencies = [ + "log", + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasi-cap-std-sync" +version = "14.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5837041da0e6ec454a819bc20ab0f8a70b2c44bf4d33287aea9fdb16bc4d597" +dependencies = [ + "anyhow", + "async-trait", + "cap-fs-ext", + "cap-rand", + "cap-std", + "cap-time-ext", + "fs-set-times", + "io-extras", + "io-lifetimes", + "once_cell", + "rustix 0.38.32", + "system-interface", + "tracing 0.1.41", + "wasi-common", + "windows-sys 0.48.0", +] + +[[package]] +name = "wasi-common" +version = "14.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6efb2e9d72c6a070d62cf7b698acebab6faca9aacf26412bdecb9fabab79fd09" +dependencies = [ + "anyhow", + "bitflags 2.9.1", + "cap-rand", + "cap-std", + "io-extras", + "log", + "rustix 0.38.32", + "thiserror 1.0.69", + "tracing 0.1.41", + "wasmtime", + "wiggle", + "windows-sys 0.48.0", +] + +[[package]] +name = "wasi-experimental-http-wasmtime" +version = "0.10.0" +source = "git+https://github.com/lapce/wasi-experimental-http#21419eb785cb583ead180f25a9685fa16de7f326" +dependencies = [ + "anyhow", + "bytes", + "futures", + "http", + "reqwest", + "thiserror 1.0.69", + "tokio", + "tracing 0.1.41", + "url", + "wasi-common", + "wasmtime", + "wasmtime-wasi", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.94", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ca90ba1b5b0a70d3d49473c5579951f3bddc78d47b59256d2f9d4922b150aca" +dependencies = [ + "leb128", +] + +[[package]] +name = "wasmparser" +version = "0.115.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e06c0641a4add879ba71ccb3a1e4278fd546f76f1eafb21d8f7b07733b547cd5" +dependencies = [ + "indexmap", + "semver", +] + +[[package]] +name = "wasmprinter" +version = "0.2.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e74458a9bc5cc9c7108abfa0fe4dc88d5abf1f3baf194df3264985f17d559b5e" +dependencies = [ + "anyhow", + "wasmparser", +] + +[[package]] +name = "wasmtime" +version = "14.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76e45ad44701a658aa3eb138a5bb63a10a35fa8874419b5e6047cfa54b2eb2cd" +dependencies = [ + "anyhow", + "async-trait", + "bincode", + "bumpalo", + "cfg-if", + "encoding_rs", + "fxprof-processed-profile", + "indexmap", + "libc", + "log", + "object", + "once_cell", + "paste", + "psm", + "rayon", + "serde", + "serde_derive", + "serde_json", + "target-lexicon", + "wasm-encoder", + "wasmparser", + "wasmtime-cache", + "wasmtime-component-macro", + "wasmtime-component-util", + "wasmtime-cranelift", + "wasmtime-environ", + "wasmtime-fiber", + "wasmtime-jit", + "wasmtime-runtime", + "wasmtime-winch", + "wat", + "windows-sys 0.48.0", +] + +[[package]] +name = "wasmtime-asm-macros" +version = "14.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45a5944c8415853471b6ddff1f38b09d58fe5ac3c5dad27ee6bc03ca29e65cca" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "wasmtime-cache" +version = "14.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c16f85353656b301a4472ad649e9b17cc47400cee50a94bd9b24e7886a8130f" +dependencies = [ + "anyhow", + "base64 0.21.7", + "bincode", + "directories-next", + "log", + "rustix 0.38.32", + "serde", + "serde_derive", + "sha2", + "toml 0.5.9", + "windows-sys 0.48.0", + "zstd", +] + +[[package]] +name = "wasmtime-component-macro" +version = "14.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b174f64cd4b189396dde1bbe137f349d3c98525b8564b539f94ce978c571173d" +dependencies = [ + "anyhow", + "proc-macro2", + "quote", + "syn 2.0.94", + "wasmtime-component-util", + "wasmtime-wit-bindgen", + "wit-parser", +] + +[[package]] +name = "wasmtime-component-util" +version = "14.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b18ff6d21a0ef69de7fd4db023646b386982e78b7e5c06e6455d98cf44774954" + +[[package]] +name = "wasmtime-cranelift" +version = "14.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7dbb50f43a7eb897f222fb427b3ba50620014eb43673b5bfa50cfd5b2681e37" +dependencies = [ + "anyhow", + "cfg-if", + "cranelift-codegen", + "cranelift-control", + "cranelift-entity", + "cranelift-frontend", + "cranelift-native", + "cranelift-wasm", + "gimli", + "log", + "object", + "target-lexicon", + "thiserror 1.0.69", + "wasmparser", + "wasmtime-cranelift-shared", + "wasmtime-environ", + "wasmtime-versioned-export-macros", +] + +[[package]] +name = "wasmtime-cranelift-shared" +version = "14.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc154520d9c910f02ce90735691186f90b220f532f9b0725543b054bf0d8381" +dependencies = [ + "anyhow", + "cranelift-codegen", + "cranelift-control", + "cranelift-native", + "gimli", + "object", + "target-lexicon", + "wasmtime-environ", +] + +[[package]] +name = "wasmtime-environ" +version = "14.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d72b405647d5378ed3ff7889586d26d79825641f477f288bc72ab416136ad4da" +dependencies = [ + "anyhow", + "cranelift-entity", + "gimli", + "indexmap", + "log", + "object", + "serde", + "serde_derive", + "target-lexicon", + "thiserror 1.0.69", + "wasm-encoder", + "wasmparser", + "wasmprinter", + "wasmtime-component-util", + "wasmtime-types", +] + +[[package]] +name = "wasmtime-fiber" +version = "14.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8702d1efdf73df040a586e239fffc6883d88edf60ac6f593b41392cc1f97c754" +dependencies = [ + "cc", + "cfg-if", + "rustix 0.38.32", + "wasmtime-asm-macros", + "wasmtime-versioned-export-macros", + "windows-sys 0.48.0", +] + +[[package]] +name = "wasmtime-jit" +version = "14.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c7462341d96d44b30776c7aec411c0cc11b15fde44b58efcbf269d21570bd7a" +dependencies = [ + "addr2line", + "anyhow", + "bincode", + "cfg-if", + "cpp_demangle", + "gimli", + "ittapi", + "log", + "object", + "rustc-demangle", + "rustix 0.38.32", + "serde", + "serde_derive", + "target-lexicon", + "wasmtime-environ", + "wasmtime-jit-debug", + "wasmtime-jit-icache-coherence", + "wasmtime-runtime", + "windows-sys 0.48.0", +] + +[[package]] +name = "wasmtime-jit-debug" +version = "14.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa128cdc680b5982087ea64eb73b63e96570b338fd6438b704b313eb854fd94" +dependencies = [ + "object", + "once_cell", + "rustix 0.38.32", + "wasmtime-versioned-export-macros", +] + +[[package]] +name = "wasmtime-jit-icache-coherence" +version = "14.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0980a96b16abbdaf829858d2389697b1d6cfc6a903873fd74b7e47a6b1045584" +dependencies = [ + "cfg-if", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "wasmtime-runtime" +version = "14.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de31031471f04c0bad4f5e29b0e632db318488dd030cdb794ef15f91a52f2338" +dependencies = [ + "anyhow", + "cc", + "cfg-if", + "encoding_rs", + "indexmap", + "libc", + "log", + "mach", + "memfd", + "memoffset", + "paste", + "rand 0.8.5", + "rustix 0.38.32", + "sptr", + "wasm-encoder", + "wasmtime-asm-macros", + "wasmtime-environ", + "wasmtime-fiber", + "wasmtime-jit-debug", + "wasmtime-versioned-export-macros", + "wasmtime-wmemcheck", + "windows-sys 0.48.0", +] + +[[package]] +name = "wasmtime-types" +version = "14.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e98a2c09807eee3207991bf05b6271aa3817c548224ded6b7bac61374ef9221" +dependencies = [ + "cranelift-entity", + "serde", + "serde_derive", + "thiserror 1.0.69", + "wasmparser", +] + +[[package]] +name = "wasmtime-versioned-export-macros" +version = "14.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73190422af3b408daa3c791f97f50c62509746c09de934d69dae602c65809663" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.94", +] + +[[package]] +name = "wasmtime-wasi" +version = "14.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1022616613f6279243392b00990ac81135f0c46018eba620538392342fc93df9" +dependencies = [ + "anyhow", + "async-trait", + "bitflags 2.9.1", + "bytes", + "cap-fs-ext", + "cap-net-ext", + "cap-rand", + "cap-std", + "cap-time-ext", + "fs-set-times", + "futures", + "io-extras", + "io-lifetimes", + "libc", + "log", + "once_cell", + "rustix 0.38.32", + "system-interface", + "thiserror 1.0.69", + "tokio", + "tracing 0.1.41", + "url", + "wasi-cap-std-sync", + "wasi-common", + "wasmtime", + "wiggle", + "windows-sys 0.48.0", +] + +[[package]] +name = "wasmtime-winch" +version = "14.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eff589e9b8f701ea4d66472115d23145fe58b62d1289c51f4a2e36c5dccf6053" +dependencies = [ + "anyhow", + "cranelift-codegen", + "gimli", + "object", + "target-lexicon", + "wasmparser", + "wasmtime-cranelift-shared", + "wasmtime-environ", + "winch-codegen", +] + +[[package]] +name = "wasmtime-wit-bindgen" +version = "14.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f672060c021afd9a3ab72f4e319d1f7bb1f4e973d5e24130bb0bb11eba356f5e" +dependencies = [ + "anyhow", + "heck 0.4.1", + "indexmap", + "wit-parser", +] + +[[package]] +name = "wasmtime-wmemcheck" +version = "14.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d40e574507de689ee8ad428b5ddfc6bc81b5eff3c8d493e0b8ec1ebf7e4eadf" + +[[package]] +name = "wast" +version = "35.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ef140f1b49946586078353a453a1d28ba90adfc54dde75710bc1931de204d68" +dependencies = [ + "leb128", +] + +[[package]] +name = "wast" +version = "66.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93cb43b0ac6dd156f2c375735ccfd72b012a7c0a6e6d09503499b8d3cb6e6072" +dependencies = [ + "leb128", + "memchr", + "unicode-width", + "wasm-encoder", +] + +[[package]] +name = "wat" +version = "1.0.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e367582095d2903caeeea9acbb140e1db9c7677001efa4347c3687fd34fe7072" +dependencies = [ + "wast 66.0.2", +] + +[[package]] +name = "wayland-backend" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fee64194ccd96bf648f42a65a7e589547096dfa702f7cadef84347b66ad164f9" +dependencies = [ + "cc", + "downcast-rs", + "rustix 1.1.3", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e6faa537fbb6c186cb9f1d41f2f811a4120d1b57ec61f50da451a0c5122bec" +dependencies = [ + "bitflags 2.9.1", + "rustix 1.1.3", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-csd-frame" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" +dependencies = [ + "bitflags 2.9.1", + "cursor-icon", + "wayland-backend", +] + +[[package]] +name = "wayland-cursor" +version = "0.31.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a206e8b2b53b1d3fcb9428fec72bc278ce539e2fa81fe2bfc1ab27703d5187b9" +dependencies = [ + "rustix 0.38.32", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e253d7107ba913923dc253967f35e8561a3c65f914543e46843c88ddd729e21c" +dependencies = [ + "bitflags 2.9.1", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baeda9ffbcfc8cd6ddaade385eaf2393bd2115a69523c735f12242353c3df4f3" +dependencies = [ + "bitflags 2.9.1", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-plasma" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa98634619300a535a9a97f338aed9a5ff1e01a461943e8346ff4ae26007306b" +dependencies = [ + "bitflags 2.9.1", + "wayland-backend", + "wayland-client", + "wayland-protocols 0.32.10", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad1f61b76b6c2d8742e10f9ba5c3737f6530b4c243132c2a2ccc8aa96fe25cd6" +dependencies = [ + "bitflags 2.9.1", + "wayland-backend", + "wayland-client", + "wayland-protocols 0.31.0", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "782e12f6cd923c3c316130d56205ebab53f55d6666b7faddfad36cecaeeb4022" +dependencies = [ + "bitflags 2.9.1", + "wayland-backend", + "wayland-client", + "wayland-protocols 0.32.10", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5423e94b6a63e68e439803a3e153a9252d5ead12fd853334e2ad33997e3889e3" +dependencies = [ + "proc-macro2", + "quick-xml 0.38.4", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6dbfc3ac5ef974c92a2235805cc0114033018ae1290a72e474aa8b28cbbdfd" +dependencies = [ + "dlib", + "log", + "once_cell", + "pkg-config", +] + +[[package]] +name = "web-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "weezl" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" + +[[package]] +name = "wgpu" +version = "24.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b0b3436f0729f6cdf2e6e9201f3d39dc95813fad61d826c1ed07918b4539353" +dependencies = [ + "arrayvec", + "bitflags 2.9.1", + "cfg_aliases", + "document-features", + "js-sys", + "log", + "naga", + "parking_lot", + "profiling", + "raw-window-handle", + "smallvec", + "static_assertions", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "wgpu-core", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-core" +version = "24.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f0aa306497a238d169b9dc70659105b4a096859a34894544ca81719242e1499" +dependencies = [ + "arrayvec", + "bit-vec", + "bitflags 2.9.1", + "cfg_aliases", + "document-features", + "indexmap", + "log", + "naga", + "once_cell", + "parking_lot", + "profiling", + "raw-window-handle", + "rustc-hash 1.1.0", + "smallvec", + "thiserror 2.0.12", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-hal" +version = "24.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f112f464674ca69f3533248508ee30cb84c67cf06c25ff6800685f5e0294e259" +dependencies = [ + "android_system_properties", + "arrayvec", + "ash", + "bit-set", + "bitflags 2.9.1", + "block", + "bytemuck", + "cfg_aliases", + "core-graphics-types", + "glow", + "glutin_wgl_sys", + "gpu-alloc", + "gpu-allocator", + "gpu-descriptor", + "js-sys", + "khronos-egl", + "libc", + "libloading", + "log", + "metal", + "naga", + "ndk-sys 0.5.0+25.2.9519653", + "objc", + "once_cell", + "ordered-float", + "parking_lot", + "profiling", + "range-alloc", + "raw-window-handle", + "renderdoc-sys", + "rustc-hash 1.1.0", + "smallvec", + "thiserror 2.0.12", + "wasm-bindgen", + "web-sys", + "wgpu-types", + "windows 0.58.0", + "windows-core", +] + +[[package]] +name = "wgpu-types" +version = "24.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50ac044c0e76c03a0378e7786ac505d010a873665e2d51383dcff8dd227dc69c" +dependencies = [ + "bitflags 2.9.1", + "js-sys", + "log", + "web-sys", +] + +[[package]] +name = "wiggle" +version = "14.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "334709283558d9ebb0206cd1842c4fa619ff467d68c71eff982376d9c999d636" +dependencies = [ + "anyhow", + "async-trait", + "bitflags 2.9.1", + "thiserror 1.0.69", + "tracing 0.1.41", + "wasmtime", + "wiggle-macro", +] + +[[package]] +name = "wiggle-generate" +version = "14.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4143cb3a8c65efceba6fc3bf49769b7b5d60090f1226e708365044c1136584ee" +dependencies = [ + "anyhow", + "heck 0.4.1", + "proc-macro2", + "quote", + "shellexpand", + "syn 2.0.94", + "witx", +] + +[[package]] +name = "wiggle-macro" +version = "14.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56981968f26952a527f78cf3aeb5ac436db82d3be1682a217a1835754fa50f51" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.94", + "wiggle-generate", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0978bf7171b3d90bac376700cb56d606feb40f251a475a5d6634613564460b22" +dependencies = [ + "windows-sys 0.60.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "winch-codegen" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2942fc0530ed88259df32f09f52a4222583e1ec7c3fa3f4a911905bbf70c3b0" +dependencies = [ + "anyhow", + "cranelift-codegen", + "gimli", + "regalloc2", + "smallvec", + "target-lexicon", + "wasmparser", + "wasmtime-environ", +] + +[[package]] +name = "windows" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e745dab35a0c4c77aa3ce42d595e13d2003d6902d6b08c9ef5fc326d08da12b" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +dependencies = [ + "windows-core", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-result", + "windows-strings", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-implement" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.94", +] + +[[package]] +name = "windows-interface" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.94", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.3", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + +[[package]] +name = "winit" +version = "0.30.7" +source = "git+https://github.com/rust-windowing/winit?rev=ee245c569d65fdeacf705ee5eedb564508d10ebe#ee245c569d65fdeacf705ee5eedb564508d10ebe" +dependencies = [ + "ahash", + "android-activity", + "atomic-waker", + "bitflags 2.9.1", + "block2 0.5.1", + "bytemuck", + "calloop 0.13.0", + "cfg_aliases", + "concurrent-queue", + "core-foundation", + "core-graphics", + "cursor-icon", + "dpi", + "js-sys", + "libc", + "memmap2", + "ndk", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", + "objc2-ui-kit", + "orbclient", + "percent-encoding", + "pin-project", + "raw-window-handle", + "redox_syscall 0.5.8", + "rustix 0.38.32", + "sctk-adwaita", + "serde", + "smithay-client-toolkit 0.19.2", + "smol_str 0.3.2", + "tracing 0.1.41", + "unicode-segmentation", + "wasm-bindgen", + "wasm-bindgen-futures", + "wayland-backend", + "wayland-client", + "wayland-protocols 0.32.10", + "wayland-protocols-plasma", + "web-sys", + "web-time", + "windows-sys 0.52.0", + "x11-dl", + "x11rb", + "xkbcommon-dl", +] + +[[package]] +name = "winnow" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5504cc7644f4b593cbc05c4a55bf9bd4e94b867c3c0bd440934174d50482427d" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "winx" +version = "0.36.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357bb8e2932df531f83b052264b050b81ba0df90ee5a59b2d1d3949f344f81e5" +dependencies = [ + "bitflags 2.9.1", + "windows-sys 0.48.0", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags 2.9.1", +] + +[[package]] +name = "wit-parser" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ace9943d89bbf3dbbc71b966da0e7302057b311f36a4ac3d65ddfef17b52cf" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", +] + +[[package]] +name = "witx" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e366f27a5cabcddb2706a78296a40b8fcc451e1a6aba2fc1d94b4a01bdaaef4b" +dependencies = [ + "anyhow", + "log", + "thiserror 1.0.69", + "wast 35.0.2", +] + +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + +[[package]] +name = "x11-clipboard" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98785a09322d7446e28a13203d2cae1059a0dd3dfb32cb06d0a225f023d8286" +dependencies = [ + "libc", + "x11rb", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "x11rb" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8f25ead8c7e4cba123243a6367da5d3990e0d3affa708ea19dce96356bd9f1a" +dependencies = [ + "as-raw-xcb-connection", + "gethostname", + "libc", + "libloading", + "once_cell", + "rustix 0.38.32", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e63e71c4b8bd9ffec2c963173a4dc4cbde9ee96961d4fcb4429db9929b606c34" + +[[package]] +name = "xattr" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f" +dependencies = [ + "libc", + "linux-raw-sys 0.4.13", + "rustix 0.38.32", +] + +[[package]] +name = "xcursor" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "463705a63313cd4301184381c5e8042f0a7e9b4bb63653f216311d4ae74690b7" +dependencies = [ + "nom", +] + +[[package]] +name = "xkbcommon-dl" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" +dependencies = [ + "bitflags 2.9.1", + "dlib", + "log", + "once_cell", + "xkeysym", +] + +[[package]] +name = "xkeysym" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "054a8e68b76250b253f671d1268cb7f1ae089ec35e195b2efb2a4e9a836d0621" + +[[package]] +name = "xml-rs" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fcb9cbac069e033553e8bb871be2fbdffcab578eb25bd0f7c508cedc6dcd75a" + +[[package]] +name = "xmlwriter" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" + +[[package]] +name = "yazi" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01738255b5a16e78bbb83e7fbba0a1e7dd506905cfc53f4622d89015a03fbb5" + +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.94", + "synstructure", +] + +[[package]] +name = "zbus" +version = "5.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3a7c7cee313d044fca3f48fa782cb750c79e4ca76ba7bc7718cd4024cdf6f68" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock 3.3.0", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener 5.4.0", + "futures-core", + "futures-lite", + "hex", + "nix", + "ordered-stream", + "serde", + "serde_repr", + "tracing 0.1.41", + "uds_windows", + "windows-sys 0.59.0", + "winnow 0.7.14", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a17e7e5eec1550f747e71a058df81a9a83813ba0f6a95f39c4e218bdc7ba366a" +dependencies = [ + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "syn 2.0.94", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97" +dependencies = [ + "serde", + "static_assertions", + "winnow 0.7.14", + "zvariant", +] + +[[package]] +name = "zeno" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6df3dc4292935e51816d896edcd52aa30bc297907c26167fec31e2b0c6a32524" + +[[package]] +name = "zerocopy" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.94", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.94", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.94", +] + +[[package]] +name = "zip" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" +dependencies = [ + "byteorder", + "crc32fast", + "crossbeam-utils", + "flate2", +] + +[[package]] +name = "zstd" +version = "0.11.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "5.0.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db" +dependencies = [ + "libc", + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.1+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fd07cbbc53846d9145dbffdf6dd09a7a0aa52be46741825f5c97bdd4f73f12b" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "zune-core" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" + +[[package]] +name = "zune-jpeg" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec866b44a2a1fd6133d363f073ca1b179f438f99e7e5bfb1e33f7181facfe448" +dependencies = [ + "zune-core", +] + +[[package]] +name = "zvariant" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d30786f75e393ee63a21de4f9074d4c038d52c5b1bb4471f955db249f9dffb1" +dependencies = [ + "endi", + "enumflags2", + "serde", + "url", + "winnow 0.7.14", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75fda702cd42d735ccd48117b1630432219c0e9616bf6cb0f8350844ee4d9580" +dependencies = [ + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "syn 2.0.94", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e16edfee43e5d7b553b77872d99bc36afdda75c223ca7ad5e3fbecd82ca5fc34" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "static_assertions", + "syn 2.0.94", + "winnow 0.7.14", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..5b91afe --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,146 @@ +[package] +name = "lapce" +version = { workspace = true } +authors = { workspace = true } +edition = { workspace = true } +license = { workspace = true } +rust-version = { workspace = true } +default-run = "lapce" + +[dependencies] +lapce-app = { path = "./lapce-app" } +lapce-proxy = { path = "./lapce-proxy" } + +[[bin]] +name = "lapce" +path = "lapce-app/src/bin/lapce.rs" + +[[bin]] +name = "lapce-proxy" +path = "lapce-proxy/src/bin/lapce-proxy.rs" + +[workspace] +members = ["lapce-app", "lapce-proxy", "lapce-rpc", "lapce-core"] + +[workspace.package] +version = "0.4.6" +edition = "2024" +rust-version = "1.87.0" +license = "Apache-2.0" +homepage = "https://lapce.dev" +authors = ["Dongdong Zhou "] + +[workspace.dependencies] +anyhow = { version = "1.0" } +backtrace = { version = "0.3" } +chrono = { version = "0.4" } +clap = { version = "4.5.0", default-features = false, features = ["std", "help", "usage", "derive"] } +crossbeam-channel = { version = "0.5.12" } +directories = { version = "4.0.1" } +flate2 = { version = "1.0" } +git2 = { version = "0.20.0", features = ["vendored-openssl"] } +globset = { version = "0.4.14" } +hashbrown = { version = "0.14.5", features = ["serde"] } +im = { version = "15.0.0", features = ["serde"] } +include_dir = { version = "0.7" } +indexmap = { version = "2.0", features = ["serde"] } +interprocess = { version = "1.2.1" } +itertools = { version = "0.12.1" } +notify = { version = "5.2.0", features = ["serde"] } +once_cell = { version = "1.19" } +parking_lot = { version = "0.12.3" } +rayon = { version = "1.10.0" } +regex = { version = "1.10.5" } +remain = { version = "0.2" } +semver = { version = "1.0" } +reqwest = { version = "0.11", features = ["blocking", "json", "socks"] } +serde = { version = "1.0" } +serde_json = { version = "1.0" } +smallvec = { version = "1.15.1" } +strum = { version = "0.27.1" } +strum_macros = { version = "0.27.1" } +tar = { version = "0.4" } +tempfile = { version = "3.10.1" } +thiserror = { version = "1.0" } +toml = { version = "*" } +toml_edit = { version = "0.20.2", features = ["serde"] } +url = { version = "2.5.0" } +zstd = { version = "0.11.2" } # follow same version wasmtime-cache in lockfile + +lsp-types = { version = "0.95.1", features = ["proposed"] } # not following semver, so should be locked to patch version updates only +psp-types = { git = "https://github.com/lapce/psp-types", rev = "f7fea28f59e7b2d6faa1034a21679ad49b3524ad" } + +lapce-xi-rope = { version = "0.3.2", features = ["serde"] } + +lapce-core = { path = "./lapce-core" } +lapce-rpc = { path = "./lapce-rpc" } +lapce-proxy = { path = "./lapce-proxy" } + +[workspace.dependencies.floem] +# path = "../floem" +git = "https://github.com/lapce/floem" +rev = "31fa8f444c37f4c314f47d88c23ffdbc25f2ab53" +features = ["editor", "serde", "default-image-formats", "rfd-async-std"] + +[workspace.dependencies.floem-editor-core] +# path = "../floem/editor-core/" +git = "https://github.com/lapce/floem" +rev = "31fa8f444c37f4c314f47d88c23ffdbc25f2ab53" +features = ["serde"] + +[patch.crates-io] +# Temporarily patch lsp-types with a version that adds message-type debug +lsp-types = { git = "https://github.com/lapce/lsp-types", rev = "feaa1e2ec80975c9dadd400a238ceacf071058e6" } +regalloc2 = { rev = "5d79e12d0a93b10fc181f4da409b4671dd365228", git = "https://github.com/bytecodealliance/regalloc2" } + +# cargo vendor issue: https://github.com/rust-lang/cargo/issues/10310 +# dpi comes from winit (source) and muda (crate) +dpi = { git = "https://github.com/rust-windowing/winit", rev = "ee245c569d65fdeacf705ee5eedb564508d10ebe" } + + +[workspace.dependencies.tracing] +git = "https://github.com/tokio-rs/tracing" +rev = "908cc432a5994f6e17c8f36e13c217dc40085704" +package = "tracing" + +[workspace.dependencies.tracing-log] +git = "https://github.com/tokio-rs/tracing" +rev = "908cc432a5994f6e17c8f36e13c217dc40085704" +package = "tracing-log" + +[workspace.dependencies.tracing-subscriber] +git = "https://github.com/tokio-rs/tracing" +rev = "908cc432a5994f6e17c8f36e13c217dc40085704" +package = "tracing-subscriber" + +[workspace.dependencies.tracing-appender] +git = "https://github.com/tokio-rs/tracing" +rev = "908cc432a5994f6e17c8f36e13c217dc40085704" +package = "tracing-appender" + +[workspace.dependencies.alacritty_terminal] +git = "https://github.com/alacritty/alacritty" +rev = "cacdb5bb3b72bad2c729227537979d95af75978f" + +[workspace.dependencies.windows-sys] +version = "0" +features = ["Win32_Foundation"] + +[profile.release-lto] +inherits = "release" +lto = true +codegen-units = 1 + +# A profile which compiles all (non-workspace) dependencies in release mode +# but Lapce code in dev mode. This gives a good debugging experience for your +# code and fast performance of other people's code. After the initial +# build subsequent ones are as fast as dev mode builds. +# See https://doc.rust-lang.org/cargo/reference/profiles.html +# To use this profile: +# cargo build --profile fastdev +# cargo run --profile fastdev --bin lapce +[profile.fastdev.package."*"] +opt-level = 3 + +[profile.fastdev] +inherits = "dev" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..101a86a --- /dev/null +++ b/Makefile @@ -0,0 +1,84 @@ +# This Makefile is intended *only* for building macOS binaries of Lapce. +# It uses macOS-specific tools like `lipo`, `codesign`, and `hdiutil`, +# and requires that a valid Apple Developer signing identity is installed +# and available in the system Keychain under the fingerprint set in +# CODESIGN_IDENTITY. +# +# See `docs/building-from-source.md`. + +TARGET = lapce + +CODESIGN_IDENTITY = FAC8FBEA99169DC1980731029648F110628D6A32 + +ASSETS_DIR = extra +RELEASE_DIR = target/release-lto + +APP_NAME = Lapce.app +APP_TEMPLATE = $(ASSETS_DIR)/macos/$(APP_NAME) +APP_DIR = $(RELEASE_DIR)/macos +APP_BINARY = $(RELEASE_DIR)/$(TARGET) +APP_BINARY_DIR = $(APP_DIR)/$(APP_NAME)/Contents/MacOS +APP_EXTRAS_DIR = $(APP_DIR)/$(APP_NAME)/Contents/Resources + +DMG_NAME = Lapce.dmg +DMG_DIR = $(RELEASE_DIR)/macos + +vpath $(TARGET) $(RELEASE_DIR) +vpath $(APP_NAME) $(APP_DIR) +vpath $(DMG_NAME) $(APP_DIR) + +all: help + +help: ## Print this help message + @grep -E '^[a-zA-Z._-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' + +ubuntu-deps: + apt-get update -y + apt-get install -y clang libxkbcommon-x11-dev pkg-config libvulkan-dev libgtk-3-dev libwayland-dev xorg-dev libxcb-shape0-dev libxcb-xfixes0-dev + +binary: $(TARGET)-native ## Build a release binary +binary-universal: $(TARGET)-universal ## Build a universal release binary +$(TARGET)-native: + MACOSX_DEPLOYMENT_TARGET="10.11" cargo build --profile release-lto + @lipo target/release-lto/$(TARGET) -create -output $(APP_BINARY) +$(TARGET)-universal: + MACOSX_DEPLOYMENT_TARGET="10.11" cargo build --profile release-lto --target=x86_64-apple-darwin + MACOSX_DEPLOYMENT_TARGET="10.11" cargo build --profile release-lto --target=aarch64-apple-darwin + @lipo target/{x86_64,aarch64}-apple-darwin/release-lto/$(TARGET) -create -output $(APP_BINARY) + /usr/bin/codesign -vvv --deep --entitlements $(ASSETS_DIR)/entitlements.plist --strict --options=runtime --force -s $(CODESIGN_IDENTITY) $(APP_BINARY) + +app: $(APP_NAME)-native ## Create a Lapce.app +app-universal: $(APP_NAME)-universal ## Create a universal Lapce.app +$(APP_NAME)-%: $(TARGET)-% + @mkdir -p $(APP_BINARY_DIR) + @mkdir -p $(APP_EXTRAS_DIR) + @cp -fRp $(APP_TEMPLATE) $(APP_DIR) + @cp -fp $(APP_BINARY) $(APP_BINARY_DIR) + @touch -r "$(APP_BINARY)" "$(APP_DIR)/$(APP_NAME)" + @echo "Created '$(APP_NAME)' in '$(APP_DIR)'" + xattr -c $(APP_DIR)/$(APP_NAME)/Contents/Info.plist + xattr -c $(APP_DIR)/$(APP_NAME)/Contents/Resources/lapce.icns + /usr/bin/codesign -vvv --deep --entitlements $(ASSETS_DIR)/entitlements.plist --strict --options=runtime --force -s $(CODESIGN_IDENTITY) $(APP_DIR)/$(APP_NAME) + +dmg: $(DMG_NAME)-native ## Create a Lapce.dmg +dmg-universal: $(DMG_NAME)-universal ## Create a universal Lapce.dmg +$(DMG_NAME)-%: $(APP_NAME)-% + @echo "Packing disk image..." + @ln -sf /Applications $(DMG_DIR)/Applications + @hdiutil create $(DMG_DIR)/$(DMG_NAME) \ + -volname "Lapce" \ + -fs HFS+ \ + -srcfolder $(APP_DIR) \ + -ov -format UDZO + @echo "Packed '$(APP_NAME)' in '$(APP_DIR)'" + /usr/bin/codesign -vvv --deep --entitlements $(ASSETS_DIR)/entitlements.plist --strict --options=runtime --force -s $(CODESIGN_IDENTITY) $(DMG_DIR)/$(DMG_NAME) + +install: $(INSTALL)-native ## Mount disk image +install-universal: $(INSTALL)-native ## Mount universal disk image +$(INSTALL)-%: $(DMG_NAME)-% + @open $(DMG_DIR)/$(DMG_NAME) + +.PHONY: app binary clean dmg install $(TARGET) $(TARGET)-universal + +clean: ## Remove all build artifacts + @cargo clean diff --git a/README.md b/README.md new file mode 100644 index 0000000..edfec11 --- /dev/null +++ b/README.md @@ -0,0 +1,61 @@ +

+ +
+ Lapce +
+

+ +

Lightning-fast And Powerful Code Editor

+ + +
+ + +Lapce (IPA: /læps/) is written in pure Rust, with a UI in [Floem](https://github.com/lapce/floem). It is designed with [Rope Science](https://xi-editor.io/docs/rope_science_00.html) from the [Xi-Editor](https://github.com/xi-editor/xi-editor), enabling lightning-fast computation, and leverages [wgpu](https://github.com/gfx-rs/wgpu) for rendering. More information about the features of Lapce can be found on the [main website](https://lapce.dev) and user documentation can be found on [GitBook](https://docs.lapce.dev/). + +![](https://github.com/lapce/lapce/blob/master/extra/images/screenshot.png?raw=true) + +## Features + +* Built-in LSP ([Language Server Protocol](https://microsoft.github.io/language-server-protocol/)) support to give you intelligent code features such as: completion, diagnostics and code actions +* Modal editing support as first class citizen (Vim-like, and toggleable) +* Built-in remote development support inspired by [VSCode Remote Development](https://code.visualstudio.com/docs/remote/remote-overview). Enjoy the benefits of a "local" experience, and seamlessly gain the full power of a remote system. We also have [Lapdev](https://lap.dev/) which can help manage your remote dev environments. +* Plugins can be written in programming languages that can compile to the [WASI](https://wasi.dev/) format (C, Rust, [AssemblyScript](https://www.assemblyscript.org/)) +* Built-in terminal, so you can execute commands in your workspace, without leaving Lapce. + +## Installation + +You can find pre-built releases for Windows, Linux and macOS [here](https://github.com/lapce/lapce/releases), or [installing with a package manager](docs/installing-with-package-manager.md). +If you'd like to compile from source, you can find the [guide](docs/building-from-source.md). + +## Contributing + + + Open in Lapdev + + +[Lapdev](https://lap.dev/), developed by the Lapce team, is a cloud dev env service similar to GitHub Codespaces. By clicking the button above, you'll be taken to a fully set up Lapce dev env where you can browse the code and start developing. All dependencies are pre-installed, so you can get straight to code. + +Guidelines for contributing to Lapce can be found in [`CONTRIBUTING.md`](CONTRIBUTING.md). + +## Feedback & Contact + +The most popular place for Lapce developers and users is on the [Discord server](https://discord.gg/n8tGJ6Rn6D). + +Or, join the discussion on [Reddit](https://www.reddit.com/r/lapce/) where we are just getting started. + +There is also a [Matrix Space](https://matrix.to/#/#lapce-editor:matrix.org), which is linked to the content from the Discord server. + +## License + +Lapce is released under the Apache License Version 2, which is an open source license. You may contribute to this project, or use the code as you please as long as you adhere to its conditions. You can find a copy of the license text here: [`LICENSE`](LICENSE). diff --git a/README.wehub.md b/README.wehub.md new file mode 100644 index 0000000..5d62e32 --- /dev/null +++ b/README.wehub.md @@ -0,0 +1,7 @@ +# WeHub 来源说明 + +- 原始项目:`lapce/lapce` +- 原始仓库:https://github.com/lapce/lapce +- 导入方式:上游默认分支的最新快照 +- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准 +- 本文件仅用于记录来源,不代表 WeHub 是原项目作者 diff --git a/_typos.toml b/_typos.toml new file mode 100644 index 0000000..654c2ad --- /dev/null +++ b/_typos.toml @@ -0,0 +1,17 @@ +# https://github.com/crate-ci/typos/blob/02ea592e44b3a53c302f697cddca7641cd051c3d/docs/reference.md + +[files] + +[default] +extend-ignore-re = [ + # Ignore lines that end with `# spellchecker:disable-line` + "(?Rm)^.*(#|//)\\s*spellchecker:disable-line$", + # Ignore the line after `# spellchecker:ignore-next-line`: + "(#|//)\\s*spellchecker:ignore-next-line\\n.*", + # Ignore blocks between `# spellchecker:off` and `# spellchecker:on` + "(?s)(#|//)\\s*spellchecker:off.*?\\n\\s*(#|//)\\s*spellchecker:on", +] + +[default.extend-identifiers] + +[default.extend-words] diff --git a/defaults/dark-theme.toml b/defaults/dark-theme.toml new file mode 100644 index 0000000..8d3c6ce --- /dev/null +++ b/defaults/dark-theme.toml @@ -0,0 +1,210 @@ +#:schema ../extra/schemas/color-theme.json + +[color-theme] +name = "Lapce Dark" + +[ui] +font-family = "" +font-size = 13 +header-height = 35 +status-height = 25 +tab-min-width = 100 +activity-width = 50 +scroll-width = 10 +drop-shadow-width = 0 + +[color-theme.base] +black = "#282C34" +blue = "#61AFEF" +cyan = "#56B6C2" +green = "#98C379" +grey = "#3E4451" +magenta = "#C678DD" +orange = "#D19A66" +purple = "#C678DD" +red = "#E06C75" +white = "#ABB2BF" +yellow = "#E5C07B" + +primary-background = "$black" +# Background for 'secondary' elements: panels, palette, status bar, completion, hover +secondary-background = "#21252B" +current-background = "#2C313A" +text = "$white" +dim-text = "#5C6370" + +[color-theme.syntax] +"comment" = "$dim-text" +"constant" = "$yellow" +"type" = "$yellow" +"typeAlias" = "$yellow" +"number" = "$yellow" +"enum" = "$yellow" +"struct" = "$yellow" +"structure" = "$yellow" +"interface" = "$yellow" +"attribute" = "$yellow" +"constructor" = "$yellow" +"function" = "$blue" +"method" = "$blue" +"function.method" = "$blue" +"keyword" = "$purple" +"selfKeyword" = "$purple" +"field" = "$red" +"property" = "$red" +"enumMember" = "$red" +"enum-member" = "$red" +"string" = "$green" +"type.builtin" = "$cyan" +"builtinType" = "$cyan" +"escape" = "$cyan" +"string.escape" = "$cyan" +"embedded" = "$cyan" +"punctuation.delimiter" = "$yellow" +"text.title" = "$orange" +"text.uri" = "$cyan" +"text.reference" = "$yellow" +"variable" = "$red" +"variable.other.member" = "$red" +"tag" = "$blue" + +"markup.heading" = "$red" +"markup.bold" = "$orange" +"markup.italic" = "$orange" +"markup.list" = "$orange" +"markup.link.url" = "$blue" +"markup.link.label" = "$purple" +"markup.link.text" = "$purple" + +"bracket.color.1" = "$blue" +"bracket.color.2" = "$yellow" +"bracket.color.3" = "$purple" +"bracket.unpaired" = "$red" + +[color-theme.ui] +"lapce.error" = "$red" +"lapce.warn" = "$yellow" +"lapce.dropdown_shadow" = "#000000" +"lapce.border" = "#000000" +"lapce.scroll_bar" = "#3E4451BB" + +"lapce.button.primary.background" = "#50a14f" +"lapce.button.primary.foreground" = "$black" + +# tab +"lapce.tab.active.background" = "$primary-background" +"lapce.tab.active.foreground" = "$text" +"lapce.tab.active.underline" = "#528BFF" + +"lapce.tab.inactive.background" = "$secondary-background" +"lapce.tab.inactive.foreground" = "$text" +"lapce.tab.inactive.underline" = "#528BFF77" + +"lapce.tab.separator" = "" + +"lapce.icon.active" = "$text" +"lapce.icon.inactive" = "$dim-text" + +"lapce.remote.icon" = "$black" +"lapce.remote.local" = "#4078F2" +"lapce.remote.connected" = "#50A14F" +"lapce.remote.connecting" = "#C18401" +"lapce.remote.disconnected" = "#E45649" + +"lapce.plugin.name" = "#DDDDDD" +"lapce.plugin.description" = "$text" +"lapce.plugin.author" = "#B0B0B0" + +"editor.background" = "$primary-background" +"editor.foreground" = "$text" +"editor.dim" = "$dim-text" +"editor.focus" = "#CCCCCC" +"editor.caret" = "#528BFF" +"editor.selection" = "$grey" +"editor.current_line" = "#2C313C" +"editor.debug_break_line" = "#528abF37" +"editor.link" = "$blue" +"editor.visible_whitespace" = "$grey" +"editor.indent_guide" = "$grey" +"editor.drag_drop_background" = "#79c1fc55" +"editor.drag_drop_tab_background" = "#0b0e1455" +"editor.sticky_header_background" = "$primary-background" + +"inlay_hint.foreground" = "$text" +"inlay_hint.background" = "#528abF37" + +"error_lens.error.foreground" = "$red" +"error_lens.error.background" = "#E06C7520" +"error_lens.warning.foreground" = "$yellow" +"error_lens.warning.background" = "#E5C07B20" +"error_lens.other.foreground" = "$dim-text" +"error_lens.other.background" = "#5C637020" + +"completion_lens.foreground" = "$dim-text" + +"source_control.added" = "#50A14FCC" +"source_control.removed" = "#FF5266CC" +"source_control.modified" = "#0184BCCC" + +"tooltip.background" = "$primary-background" +"tooltip.foreground" = "$text" + +"palette.background" = "$secondary-background" +"palette.foreground" = "$text" +"palette.current.background" = "$current-background" +"palette.current.foreground" = "$text" + +"completion.background" = "$secondary-background" +"completion.current" = "$current-background" + +"hover.background" = "$secondary-background" + +"activity.background" = "$secondary-background" +"activity.current" = "$primary-background" + +"debug.breakpoint" = "$red" +"debug.breakpoint.hover" = "#E06C7566" + +"panel.background" = "$secondary-background" +"panel.foreground" = "$text" +"panel.foreground.dim" = "$dim-text" +"panel.current.background" = "$current-background" +"panel.current.foreground" = "$text" +"panel.current.foreground.dim" = "$dim-text" +"panel.hovered.background" = "#343A45" +"panel.hovered.active.background" = "$dim-text" +"panel.hovered.foreground" = "$text" +"panel.hovered.foreground.dim" = "$dim-text" + +"status.background" = "$secondary-background" +"status.foreground" = "$text" +"status.modal.normal.background" = "$blue" +"status.modal.normal.foreground" = "$black" +"status.modal.insert.background" = "$red" +"status.modal.insert.foreground" = "$black" +"status.modal.visual.background" = "$yellow" +"status.modal.visual.foreground" = "$black" +"status.modal.terminal.background" = "$purple" +"status.modal.terminal.foreground" = "$black" + +"markdown.blockquote" = "#898989" + +"terminal.cursor" = "$text" +"terminal.foreground" = "$text" +"terminal.background" = "$primary-background" +"terminal.white" = "$white" +"terminal.black" = "$black" +"terminal.red" = "$red" +"terminal.blue" = "$blue" +"terminal.green" = "$green" +"terminal.yellow" = "$yellow" +"terminal.cyan" = "$cyan" +"terminal.magenta" = "$magenta" +"terminal.bright_white" = "#C8CCD4" +"terminal.bright_red" = "$red" +"terminal.bright_blue" = "$blue" +"terminal.bright_green" = "$green" +"terminal.bright_yellow" = "$yellow" +"terminal.bright_cyan" = "$cyan" +"terminal.bright_magenta" = "$magenta" +"terminal.bright_black" = "#545862" diff --git a/defaults/icon-theme.toml b/defaults/icon-theme.toml new file mode 100644 index 0000000..51c1a7c --- /dev/null +++ b/defaults/icon-theme.toml @@ -0,0 +1,153 @@ +#:schema ../extra/schemas/icon-theme.json + +[icon-theme] +name = "Lapce Codicons" + +[icon-theme.ui] +"logo" = "lapce_logo.svg" +"menu" = "menu.svg" +"link" = "link.svg" +"error" = "error.svg" +"add" = "add.svg" +"close" = "close.svg" +"remote" = "remote.svg" +"unsaved" = "circle-filled.svg" +"warning" = "warning.svg" +"problem" = "problem.svg" +"debug" = "debug.svg" +"debug_breakpoint" = "circle-filled.svg" +"debug_alt" = "debug-alt.svg" +"debug_small" = "debug-alt-small.svg" +"debug_restart" = "debug-restart.svg" +"debug_continue" = "debug-continue.svg" +"debug_step_over" = "debug-step-over.svg" +"debug_step_into" = "debug-step-into.svg" +"debug_step_out" = "debug-step-out.svg" +"debug_pause" = "debug-pause.svg" +"debug_stop" = "debug-stop.svg" +"debug_console" = "debug-console.svg" +"debug_disconnect" = "debug-disconnect.svg" +"start" = "debug-start.svg" +"run_errors" = "run-errors.svg" +"settings" = "settings-gear.svg" +"terminal" = "terminal.svg" +"lightbulb" = "lightbulb.svg" +"extensions" = "extensions.svg" +"keyboard" = "keyboard.svg" +"breadcrumb_separator" = "chevron-right.svg" +"symbol_color" = "symbol-color.svg" +"type_hierarchy" = "type-hierarchy.svg" + +"window.close" = "chrome-close.svg" +"window.restore" = "chrome-restore.svg" +"window.maximize" = "chrome-maximize.svg" +"window.minimize" = "chrome-minimize.svg" + +"file" = "file.svg" +"file_explorer" = "files.svg" +"file_picker_up" = "arrow-up.svg" + +"image_loading" = "refresh.svg" +"image_error" = "error.svg" + +"scm.icon" = "source-control.svg" +"scm.diff.modified" = "diff-modified.svg" +"scm.diff.added" = "diff-added.svg" +"scm.diff.removed" = "diff-removed.svg" +"scm.diff.renamed" = "diff-renamed.svg" +"scm.change.add" = "add.svg" +"scm.change.remove" = "remove.svg" + +"palette.menu" = "chevron-down.svg" + +"fold" = "fold.svg" +"fold.up" = "fold-up.svg" +"fold.down" = "fold-down.svg" + +"dropdown.arrow" = "chevron-down.svg" + +"panel.fold-down" = "chevron-down.svg" +"panel.fold-up" = "chevron-right.svg" + +"location.forward" = "arrow-right.svg" +"location.backward" = "arrow-left.svg" + +"item.opened" = "chevron-down.svg" +"item.closed" = "chevron-right.svg" + +"directory.closed" = "folder.svg" +"directory.opened" = "folder-opened.svg" + +"panel.restore" = "chevron-down.svg" +"panel.maximise" = "chevron-right.svg" + +"split.horizontal" = "split-horizontal.svg" + +"tab.previous" = "chevron-left.svg" +"tab.next" = "chevron-right.svg" + +"sidebar.left.on" = "layout-sidebar-left.svg" +"sidebar.left.off" = "layout-sidebar-left-off.svg" +"sidebar.right.on" = "layout-sidebar-right.svg" +"sidebar.right.off" = "layout-sidebar-right-off.svg" + +"layout.panel.on" = "layout-panel.svg" +"layout.panel.off" = "layout-panel-off.svg" + +"search.icon" = "search.svg" +"search.clear" = "close.svg" +"search.forward" = "arrow-down.svg" +"search.backward" = "arrow-up.svg" +"search.case_sensitive" = "case-sensitive.svg" +"search.whole_word" = "whole-word.svg" +"search.regex" = "regex.svg" +"search.replace" = "replace.svg" +"search.replace_all" = "replace-all.svg" + +"document_symbol" = "symbol-class.svg" +"references" = "references.svg" +"implementation" = "combine.svg" +"symbol_kind.array" = "symbol-array.svg" +"symbol_kind.boolean" = "symbol-boolean.svg" +"symbol_kind.class" = "symbol-class.svg" +"symbol_kind.constant" = "symbol-constant.svg" +"symbol_kind.enum_member" = "symbol-enum-member.svg" +"symbol_kind.enum" = "symbol-enum.svg" +"symbol_kind.event" = "symbol-event.svg" +"symbol_kind.field" = "symbol-field.svg" +"symbol_kind.file" = "symbol-file.svg" +"symbol_kind.function" = "symbol-method.svg" +"symbol_kind.interface" = "symbol-interface.svg" +"symbol_kind.key" = "symbol-key.svg" +"symbol_kind.method" = "symbol-method.svg" +"symbol_kind.namespace" = "symbol-namespace.svg" +"symbol_kind.number" = "symbol-numeric.svg" +"symbol_kind.object" = "symbol-namespace.svg" +"symbol_kind.operator" = "symbol-operator.svg" +"symbol_kind.property" = "symbol-property.svg" +"symbol_kind.string" = "symbol-string.svg" +"symbol_kind.struct" = "symbol-structure.svg" +"symbol_kind.type_parameter" = "symbol-parameter.svg" +"symbol_kind.variable" = "symbol-variable.svg" + +"completion_item_kind.class" = "symbol-class.svg" +"completion_item_kind.constant" = "symbol-constant.svg" +"completion_item_kind.enum_member" = "symbol-enum-member.svg" +"completion_item_kind.enum" = "symbol-enum.svg" +"completion_item_kind.field" = "symbol-field.svg" +"completion_item_kind.function" = "symbol-method.svg" +"completion_item_kind.interface" = "symbol-interface.svg" +"completion_item_kind.keyword" = "symbol-keyword.svg" +"completion_item_kind.method" = "symbol-method.svg" +"completion_item_kind.module" = "symbol-namespace.svg" +"completion_item_kind.property" = "symbol-property.svg" +"completion_item_kind.snippet" = "symbol-snippet.svg" +"completion_item_kind.string" = "symbol-string.svg" +"completion_item_kind.struct" = "symbol-structure.svg" +"completion_item_kind.variable" = "symbol-variable.svg" + +[icon-theme.foldername] + +[icon-theme.filename] + +[icon-theme.extension] diff --git a/defaults/keymaps-common.toml b/defaults/keymaps-common.toml new file mode 100644 index 0000000..43fa4c7 --- /dev/null +++ b/defaults/keymaps-common.toml @@ -0,0 +1,737 @@ +# --------------------------------- General -------------------------------------------- + +[[keymaps]] +key = "F1" +command = "palette.command" + +# [[keymaps]] +# key = "ctrl+q" +# command = "quit" + +# --------------------------------- Basic editing --------------------------------------- + +[[keymaps]] +key = "alt+up" +command = "move_line_up" +mode = "i" + +[[keymaps]] +key = "alt+down" +command = "move_line_down" +mode = "i" + +[[keymaps]] +key = "Delete" +command = "delete_forward" +mode = "i" + +[[keymaps]] +key = "backspace" +command = "delete_backward" +mode = "i" + +[[keymaps]] +key = "shift+backspace" +command = "delete_forward" +mode = "i" + +[[keymaps]] +key = "Home" +command = "line_start_non_blank" +mode = "inv" + +[[keymaps]] +key = "End" +command = "line_end" +mode = "inv" + +[[keymaps]] +key = "PageUp" +command = "page_up" + +[[keymaps]] +key = "PageDown" +command = "page_down" + +[[keymaps]] +key = "Ctrl+PageUp" +command = "scroll_up" + +[[keymaps]] +key = "Ctrl+PageDown" +command = "scroll_down" + +# ------------------------------------ Multi cursor ------------------------------------- + +[[keymaps]] +key = "alt+shift+i" +command = "insert_cursor_end_of_line" +mode = "i" + +# ----------------------------------- Editor Management ------------------------------- + +[[keymaps]] +key = "ctrl+tab" +command = "next_editor_tab" + +[[keymaps]] +key = "ctrl+shift+tab" +command = "previous_editor_tab" + +# --------------------------------- Rich Language Editing ---------------------------- + +[[keymaps]] +key = "F2" +command = "rename_symbol" + +[[keymaps]] +key = "F12" +command = "goto_definition" + +[[keymaps]] +key = "g f" +command = "show_code_actions" +mode = "n" + +# ------------------------------------ Navigation ------------------------------------- + +[[keymaps]] +key = "F8" +command = "next_error" + +[[keymaps]] +key = "shift+F8" +command = "previous_error" + +[[keymaps]] +key = "ctrl+-" +command = "jump_location_backward" +mode = "i" + +[[keymaps]] +key = "ctrl+shift+-" +command = "jump_location_forward" +mode = "i" + +# --------------------------------- Integrated Terminal ------------------------------ + +[[keymaps]] +key = "Ctrl+`" +command = "toggle_terminal_focus" + +# ------------------------------------ ------------ ------------------------------------- + +[[keymaps]] +key = "tab" +command = "jump_to_next_snippet_placeholder" +when = "in_snippet" +mode = "i" + +[[keymaps]] +key = "shift+tab" +command = "jump_to_prev_snippet_placeholder" +when = "in_snippet" +mode = "i" + +[[keymaps]] +key = "esc" +command = "clear_search" +when = "search_active || search_focus" + +[[keymaps]] +key = "ctrl+shift+up" +command = "select_next_syntax_item" + +[[keymaps]] +key = "ctrl+shift+down" +command = "select_previous_syntax_item" + +[[keymaps]] +key = "ctrl+m" +command = "list.select" +when = "list_focus" + +[[keymaps]] +key = "tab" +command = "list.select" +when = "completion_focus" + +[[keymaps]] +key = "enter" +command = "list.select" +when = "list_focus" + +[[keymaps]] +key = "ctrl+p" +command = "list.previous" +when = "list_focus" + +[[keymaps]] +key = "up" +command = "list.previous" +when = "list_focus" + +[[keymaps]] +key = "PageUp" +command = "list.previous_page" +when = "list_focus" + +[[keymaps]] +key = "ctrl+n" +command = "list.next" +when = "list_focus" + +[[keymaps]] +key = "down" +command = "list.next" +when = "list_focus" + +[[keymaps]] +key = "PageDown" +command = "list.next_page" +when = "list_focus" + +[[keymaps]] +key = "o" +command = "list.expand" +when = "list_focus" +mode = "n" + +[[keymaps]] +key = "/" +command = "palette.line" +mode = "n" + +[[keymaps]] +key = "esc" +command = "modal.close" +when = "modal_focus || completion_focus" + +[[keymaps]] +key = "tab" +command = "inline_completion.select" +when = "inline_completion_visible && !search_focus && !modal_focus && !list_focus && !search_active" +mode = "i" + +[[keymaps]] +key = "esc" +command = "inline_completion.cancel" +when = "inline_completion_visible && !search_focus && !modal_focus && !list_focus && !search_active" +mode = "i" + +[[keymaps]] +key = "alt+[" +command = "inline_completion.previous" +when = "inline_completion_visible && !search_focus && !modal_focus && !list_focus && !search_active" +mode = "i" + +[[keymaps]] +key = "alt+]" +command = "inline_completion.next" +when = "inline_completion_visible && !search_focus && !modal_focus && !list_focus && !search_active" +mode = "i" + +[[keymaps]] +key = "alt+\\" +command = "inline_completion.invoke" +when = "!inline_completion_visible && !search_focus && !modal_focus && !list_focus && !search_active" +mode = "i" + +[[keymaps]] +key = "right" +command = "right" +mode = "inv" + +[[keymaps]] +key = "left" +command = "left" +mode = "inv" + +[[keymaps]] +key = "up" +command = "up" +when = "!list_focus" +mode = "inv" + +[[keymaps]] +key = "down" +command = "down" +when = "!list_focus" +mode = "inv" + +[[keymaps]] +key = "ctrl+h" +command = "delete_backward" +mode = "i" + +[[keymaps]] +key = "enter" +command = "insert_new_line" +when = "!list_focus && !input_focus" +mode = "i" + +[[keymaps]] +key = "tab" +command = "focus_replace_editor" +when = "search_focus" + +[[keymaps]] +key = "tab" +command = "focus_find_editor" +when = "replace_focus" + +[[keymaps]] +key = "shift+enter" +command = "search_backward" +when = "search_focus" +mode = "i" + +[[keymaps]] +key = "enter" +command = "search_forward" +when = "search_focus" +mode = "i" + +[[keymaps]] +key = "enter" +command = "global_search_refresh" +when = "global_search_focus" +mode = "i" + +[[keymaps]] +key = "enter" +command = "confirm_rename" +when = "rename_focus" +mode = "i" + +[[keymaps]] +key = "tab" +command = "insert_tab" +when = "!in_snippet && !completion_focus && !inline_completion_visible && !search_focus && !replace_focus" +mode = "i" + +[[keymaps]] +key = "ctrl+m" +command = "insert_new_line" +when = "!list_focus" +mode = "i" + +[[keymaps]] +key = "alt+shift+up" +command = "duplicate_line_up" +mode = "i" + +[[keymaps]] +key = "alt+shift+down" +command = "duplicate_line_down" +mode = "i" + +# ------------------------------------ Modal ----------------------------------------- + +[[keymaps]] +key = "esc" +command = "normal_mode" +mode = "niv" +when = "!search_focus && !modal_focus && !search_active && !inline_completion_visible" + +[[keymaps]] +key = "ctrl+c" +command = "normal_mode" +mode = "niv" +when = "!search_focus && !modal_focus && !search_active" + +[[keymaps]] +key = "ctrl+[" +command = "normal_mode" +mode = "niv" +when = "!search_focus && !search_active" + +[[keymaps]] +key = "shift+;" +command = "palette.command" +mode = "n" + +[[keymaps]] +key = "shift+g" +command = "go_to_line_default_last" +mode = "nv" + +[[keymaps]] +key = "g g" +command = "go_to_line_default_first" +mode = "nv" + +[[keymaps]] +key = "h" +command = "left" +mode = "nv" + +[[keymaps]] +key = "j" +command = "down" +mode = "nv" + +[[keymaps]] +key = "k" +command = "up" +mode = "nv" + +[[keymaps]] +key = "l" +command = "right" +mode = "nv" + +[[keymaps]] +key = "a" +command = "append" +mode = "n" + +[[keymaps]] +key = "shift+a" +command = "append_end_of_line" +mode = "n" + +[[keymaps]] +key = "i" +command = "insert_mode" +mode = "n" + +[[keymaps]] +key = "u" +command = "undo" +mode = "n" + +[[keymaps]] +key = "ctrl+r" +command = "redo" +mode = "n" + +[[keymaps]] +key = "ctrl+i" +command = "jump_location_forward_local" +mode = "n" + +[[keymaps]] +key = "ctrl+o" +command = "jump_location_backward_local" +mode = "n" + +[[keymaps]] +key = "ctrl+n" +command = "goto_definition" +mode = "n" + +[[keymaps]] +key = "g d" +command = "goto_definition" +mode = "n" + +[[keymaps]] +key = "g h" +command = "show_hover" +mode = "n" + +[[keymaps]] +key = "p" +command = "paste" +mode = "nv" + +[[keymaps]] +key = "shift+p" +command = "paste_before" +mode = "nv" + +[[keymaps]] +key = "shift+j" +command = "join_lines" +mode = "n" + +[[keymaps]] +key = "y" +command = "yank" +mode = "v" + +[[keymaps]] +key = "x" +command = "delete_forward" +mode = "nv" + +[[keymaps]] +key = "ctrl+p" +command = "get_references" +mode = "n" + +[[keymaps]] +key = "s" +command = "delete_forward_and_insert" +mode = "nv" + +[[keymaps]] +key = "c" +command = "delete_forward_and_insert" +mode = "v" + +[[keymaps]] +key = "shift+s" +command = "delete_line_and_insert" +mode = "n" + +[[keymaps]] +key = "c c" +command = "delete_line_and_insert" +mode = "n" + +[[keymaps]] +key = "c w" +command = "delete_word_and_insert" +mode = "n" + +[[keymaps]] +key = "c e" +command = "delete_word_and_insert" +mode = "n" + +[[keymaps]] +key = "shift+i" +command = "insert_first_non_blank" +mode = "nv" + +[[keymaps]] +key = "o" +command = "new_line_below" +mode = "nv" + +[[keymaps]] +key = "0" +command = "line_start" +mode = "nv" + +[[keymaps]] +key = "shift+6" +command = "line_start_non_blank" +mode = "nv" + +[[keymaps]] +key = "shift+4" +command = "line_end" +mode = "nv" + +[[keymaps]] +key = "w" +command = "word_forward" +mode = "nv" + +[[keymaps]] +key = "e" +command = "word_end_forward" +mode = "nv" + +[[keymaps]] +key = "b" +command = "word_backward" +mode = "nv" + +[[keymaps]] +key = "shift+o" +command = "new_line_above" +mode = "nv" + +[[keymaps]] +key = "ctrl+e" +command = "scroll_down" +mode = "nv" + +[[keymaps]] +key = "ctrl+y" +command = "scroll_up" +mode = "nv" + +[[keymaps]] +key = "ctrl+u" +command = "page_up" +mode = "nv" + +[[keymaps]] +key = "ctrl+d" +command = "page_down" +mode = "nv" + +[[keymaps]] +key = "z z" +command = "center_of_window" +mode = "nv" + +[[keymaps]] +key = "z t" +command = "top_of_window" +mode = "nv" + +[[keymaps]] +key = "z b" +command = "bottom_of_window" +mode = "nv" + +[[keymaps]] +key = "d" +command = "delete_forward" +mode = "v" + +[[keymaps]] +key = "m" +command = "create_mark" +mode = "nv" + +[[keymaps]] +key = "'" +command = "go_to_mark" +mode = "nv" + +[[keymaps]] +key = "f" +command = "inline_find_right" +mode = "nv" + +[[keymaps]] +key = "shift+f" +command = "inline_find_left" +mode = "nv" + +[[keymaps]] +key = ";" +command = "repeat_last_inline_find" +mode = "nv" + +[[keymaps]] +key = "d" +command = "motion_mode_delete" +mode = "n" + +[[keymaps]] +key = "shift+." +command = "motion_mode_indent" +mode = "n" + +[[keymaps]] +key = "shift+," +command = "motion_mode_outdent" +mode = "n" + +[[keymaps]] +key = "y" +command = "motion_mode_yank" +mode = "n" + +[[keymaps]] +key = "shift+8" +command = "search_whole_word_forward" +mode = "nv" + +[[keymaps]] +key = "n" +command = "search_forward" +mode = "nv" + +[[keymaps]] +key = "shift+n" +command = "search_backward" +mode = "nv" + +[[keymaps]] +key = "shift+5" +command = "match_pairs" +mode = "nv" + +[[keymaps]] +key = "] shift+0" +command = "next_unmatched_right_bracket" +mode = "nv" + +[[keymaps]] +key = "[ shift+9" +command = "previous_unmatched_left_bracket" +mode = "nv" + +[[keymaps]] +key = "] shift+]" +command = "next_unmatched_right_curly_bracket" +mode = "nv" + +[[keymaps]] +key = "[ shift+[" +command = "previous_unmatched_left_curly_bracket" +mode = "nv" + +[[keymaps]] +key = "v" +command = "toggle_visual_mode" +mode = "nv" + +[[keymaps]] +key = "shift+v" +command = "toggle_linewise_visual_mode" +mode = "nv" + +[[keymaps]] +key = "ctrl+v" +command = "toggle_blockwise_visual_mode" +mode = "nv" + +[[keymaps]] +key = "ctrl+w l" +command = "split_right" +mode = "n" + +[[keymaps]] +key = "ctrl+w h" +command = "split_left" +mode = "n" + +[[keymaps]] +key = "ctrl+w j" +command = "split_down" +mode = "n" + +[[keymaps]] +key = "ctrl+w k" +command = "split_up" +mode = "n" + +[[keymaps]] +key = "ctrl+w s" +command = "split_horizontal" +mode = "n" + +[[keymaps]] +key = "ctrl+w v" +command = "split_vertical" +mode = "n" + +[[keymaps]] +key = "ctrl+w c" +command = "split_close" +mode = "n" + +[[keymaps]] +key = "ctrl+w x" +command = "split_exchange" +mode = "n" + +[[keymaps]] +key = "space" +command = "toggle_code_glance" +mode = "nv" + +[[keymaps]] +key = "shift+." +command = "indent_line" +mode = "v" + +[[keymaps]] +key = "shift+," +command = "outdent_line" +mode = "v" + +[[keymaps]] +key = "shift+c" +command = "delete_to_end_and_insert" +mode = "n" diff --git a/defaults/keymaps-macos.toml b/defaults/keymaps-macos.toml new file mode 100644 index 0000000..d082620 --- /dev/null +++ b/defaults/keymaps-macos.toml @@ -0,0 +1,305 @@ +# --------------------------------- Window --------------------------------------------- + +[[keymaps]] +command = "window_close" +key = "Meta+Shift+W" + +# --------------------------------- General -------------------------------------------- + +[[keymaps]] +key = "meta+p" +command = "palette" + +[[keymaps]] +key = "meta+shift+p" +command = "palette.command" + +[[keymaps]] +key = "meta+e" +command = "toggle_code_glance" +mode = "i" + +[[keymaps]] +key = "meta+," +command = "open_settings" + +[[keymaps]] +key = "meta+k meta+s" +command = "open_keyboard_shortcuts" + +# [[keymaps]] +# key = "meta+q" +# command = "quit" + +[[keymaps]] +key = "meta+=" +command = "zoom_in" + +[[keymaps]] +key = "meta+-" +command = "zoom_out" + +[[keymaps]] +key = "meta+enter" +command = "source_control_commit" +when = "source_control_focus" + +# --------------------------------- Basic editing --------------------------------------- + +[[keymaps]] +key = "meta+z" +command = "undo" + +[[keymaps]] +key = "meta+shift+z" +command = "redo" + +[[keymaps]] +key = "meta+y" +command = "redo" + +[[keymaps]] +key = "meta+x" +command = "clipboard_cut" + +[[keymaps]] +key = "meta+c" +command = "clipboard_copy" + +[[keymaps]] +key = "meta+v" +command = "clipboard_paste" + +[[keymaps]] +key = "meta+f" +command = "search" + +[[keymaps]] +key = "alt+right" +command = "word_end_forward" +mode = "i" + +[[keymaps]] +key = "alt+left" +command = "word_backward" +mode = "i" + +[[keymaps]] +key = "meta+left" +command = "line_start_non_blank" +mode = "i" + +[[keymaps]] +key = "meta+right" +command = "line_end" +mode = "i" + +[[keymaps]] +key = "ctrl+a" +command = "line_start_non_blank" +mode = "i" + +[[keymaps]] +key = "ctrl+e" +command = "line_end" +mode = "i" + +[[keymaps]] +key = "meta+shift+k" +command = "delete_line" +mode = "i" + +[[keymaps]] +key = "alt+backspace" +command = "delete_word_backward" +mode = "i" + +[[keymaps]] +key = "meta+backspace" +command = "delete_to_beginning_of_line" +mode = "i" + +[[keymaps]] +key = "ctrl+k" +command = "delete_to_end_of_line" +mode = "i" + +[[keymaps]] +key = "alt+delete" +command = "delete_word_forward" +mode = "i" + +[[keymaps]] +key = "meta+shift+\\" +command = "match_pairs" +mode = "i" + +[[keymaps]] +key = "meta+/" +command = "toggle_line_comment" + +[[keymaps]] +key = "meta+]" +command = "indent_line" + +[[keymaps]] +key = "meta+[" +command = "outdent_line" + +[[keymaps]] +key = "meta+a" +command = "select_all" + +[[keymaps]] +key = "meta+enter" +command = "new_line_below" +when = "!source_control_focus" +mode = "i" + +[[keymaps]] +key = "meta+shift+enter" +command = "new_line_above" +mode = "i" + +# ------------------------------------ Multi cursor ------------------------------------- + +[[keymaps]] +key = "alt+meta+up" +command = "insert_cursor_above" +mode = "i" + +[[keymaps]] +key = "alt+meta+down" +command = "insert_cursor_below" +mode = "i" + +[[keymaps]] +key = "meta+l" +command = "select_current_line" +mode = "i" + +[[keymaps]] +key = "meta+shift+l" +command = "select_all_current" +mode = "i" + +[[keymaps]] +key = "meta+u" +command = "select_undo" +mode = "i" + +[[keymaps]] +key = "meta+d" +command = "select_next_current" +mode = "i" + +[[keymaps]] +key = "meta+k meta+d" +command = "select_skip_current" +mode = "i" + +# ------------------------------------ File Management -------------------------------- + +[[keymaps]] +key = "meta+s" +command = "save" + +[[keymaps]] +key = "meta+o" +command = "open_file" + +[[keymaps]] +key = "meta+n" +command = "new_file" + +# ----------------------------------- Editor Management ------------------------------- + +[[keymaps]] +key = "meta+w" +command = "split_close" + +[[keymaps]] +key = "meta+k f" +command = "close_folder" + +[[keymaps]] +key = "meta+\\" +command = "split_vertical" + +# --------------------------------- Rich Language Editing ---------------------------- + +[[keymaps]] +key = "ctrl+space" +command = "get_completion" +mode = "i" + +[[keymaps]] +key = "meta+i" +command = "get_completion" +mode = "i" + +[[keymaps]] +key = "ctrl+shift+space" +command = "get_signature" +mode = "i" + +[[keymaps]] +key = "meta+." +command = "show_code_actions" + +# --------------------------------- Display ------------------------------------------- + +[[keymaps]] +key = "meta+shift+e" +command = "toggle_file_explorer_focus" + +[[keymaps]] +key = "meta+shift+f" +command = "toggle_search_focus" + +[[keymaps]] +key = "meta+shift+x" +command = "toggle_plugin_focus" + +[[keymaps]] +key = "meta+shift+m" +command = "toggle_problem_focus" + +# ------------------------------------ Navigation ------------------------------------- + +[[keymaps]] +key = "meta+up" +command = "document_start" + +[[keymaps]] +key = "meta+down" +command = "document_end" + +[[keymaps]] +key = "ctrl+f" +command = "right" +mode = "i" + +[[keymaps]] +key = "ctrl+p" +command = "up" +when = "!list_focus" +mode = "i" + +[[keymaps]] +key = "ctrl+n" +command = "down" +when = "!list_focus" +mode = "i" + +[[keymaps]] +key = "meta+shift+o" +command = "palette.symbol" + +[[keymaps]] +key = "meta+t" +command = "palette.workspace_symbol" + +[[keymaps]] +key = "ctrl+g" +command = "palette.line" diff --git a/defaults/keymaps-nonmacos.toml b/defaults/keymaps-nonmacos.toml new file mode 100644 index 0000000..5644ec3 --- /dev/null +++ b/defaults/keymaps-nonmacos.toml @@ -0,0 +1,310 @@ +# --------------------------------- Window --------------------------------------------- + +[[keymaps]] +command = "close_window" +key = "Alt+F4" + +# --------------------------------- General -------------------------------------------- + +[[keymaps]] +key = "ctrl+p" +command = "palette" + +[[keymaps]] +key = "ctrl+shift+p" +command = "palette.command" + +[[keymaps]] +key = "ctrl+e" +command = "toggle_code_glance" +mode = "i" + +[[keymaps]] +key = "ctrl+," +command = "open_settings" + +[[keymaps]] +key = "ctrl+k ctrl+s" +command = "open_keyboard_shortcuts" + +[[keymaps]] +key = "ctrl+=" +command = "zoom_in" + +[[keymaps]] +key = "ctrl+-" +command = "zoom_out" + +[[keymaps]] +key = "ctrl+enter" +command = "source_control_commit" +when = "source_control_focus" + +# --------------------------------- Terminal copy/paste --------------------------------- + +[[keymaps]] +key = "ctrl+shift+c" +command = "clipboard_copy" +mode = "t" + +[[keymaps]] +key = "ctrl+shift+v" +command = "clipboard_paste" +mode = "t" + +# --------------------------------- Basic editing --------------------------------------- + +[[keymaps]] +key = "ctrl+z" +command = "undo" +mode = "i" + +[[keymaps]] +key = "ctrl+shift+z" +command = "redo" +mode = "i" + + +[[keymaps]] +key = "ctrl+y" +command = "redo" +mode = "i" + +[[keymaps]] +key = "ctrl+x" +command = "clipboard_cut" +mode = "i" + +[[keymaps]] +key = "shift+Delete" +command = "clipboard_cut" +mode = "i" + +[[keymaps]] +key = "ctrl+c" +command = "clipboard_copy" +mode = "i" + +[[keymaps]] +key = "ctrl+insert" +command = "clipboard_copy" +mode = "i" + +[[keymaps]] +key = "ctrl+v" +command = "clipboard_paste" +mode = "i" + +[[keymaps]] +key = "shift+insert" +command = "clipboard_paste" +mode = "i" + +[[keymaps]] +key = "ctrl+f" +command = "search" + +[[keymaps]] +key = "ctrl+right" +command = "word_end_forward" +mode = "i" + +[[keymaps]] +key = "ctrl+left" +command = "word_backward" +mode = "i" + +[[keymaps]] +key = "ctrl+backspace" +command = "delete_word_backward" +mode = "i" + +[[keymaps]] +key = "ctrl+delete" +command = "delete_word_forward" +mode = "i" + +[[keymaps]] +key = "ctrl+shift+\\" +command = "match_pairs" +mode = "i" + +[[keymaps]] +key = "ctrl+/" +command = "toggle_line_comment" + +[[keymaps]] +key = "ctrl+]" +command = "indent_line" + +[[keymaps]] +key = "ctrl+[" +command = "outdent_line" + +[[keymaps]] +key = "ctrl+a" +command = "select_all" + +[[keymaps]] +key = "ctrl+enter" +command = "new_line_below" +when = "!source_control_focus" +mode = "i" + +[[keymaps]] +key = "ctrl+shift+enter" +command = "new_line_above" +mode = "i" + +# ------------------------------------ Multi cursor ------------------------------------- + +[[keymaps]] +key = "alt+ctrl+up" +command = "insert_cursor_above" +mode = "i" + +[[keymaps]] +key = "alt+ctrl+down" +command = "insert_cursor_below" +mode = "i" + +[[keymaps]] +key = "ctrl+l" +command = "select_current_line" +mode = "i" + +[[keymaps]] +key = "ctrl+shift+l" +command = "select_all_current" +mode = "i" + +[[keymaps]] +key = "ctrl+u" +command = "select_undo" +mode = "i" + +[[keymaps]] +key = "ctrl+d" +command = "select_next_current" +mode = "i" + +[[keymaps]] +key = "ctrl+k ctrl+d" +command = "select_skip_current" +mode = "i" + +# ------------------------------------ File Management -------------------------------- + +[[keymaps]] +key = "ctrl+s" +command = "save" + +[[keymaps]] +key = "ctrl+o" +command = "open_file" + +[[keymaps]] +key = "ctrl+n" +command = "new_file" + +# ----------------------------------- Editor Management ------------------------------- + +[[keymaps]] +key = "ctrl+w" +command = "split_close" +mode = "i" + +[[keymaps]] +key = "ctrl+k f" +command = "close_folder" + +[[keymaps]] +key = "ctrl+F4" +command = "split_close" + +[[keymaps]] +key = "ctrl+\\" +command = "split_vertical" + +# --------------------------------- Rich Language Editing ---------------------------- + +[[keymaps]] +key = "ctrl+space" +command = "get_completion" +mode = "i" + +[[keymaps]] +key = "ctrl+i" +command = "get_completion" +mode = "i" + +[[keymaps]] +key = "ctrl+shift+space" +command = "get_signature" +mode = "i" + +[[keymaps]] +key = "ctrl+." +command = "show_code_actions" + +# --------------------------------- Display ------------------------------------------- + +[[keymaps]] +key = "ctrl+shift+e" +command = "toggle_file_explorer_focus" + +[[keymaps]] +key = "ctrl+shift+f" +command = "toggle_search_focus" + +[[keymaps]] +key = "ctrl+shift+x" +command = "toggle_plugin_focus" + +[[keymaps]] +key = "ctrl+shift+m" +command = "toggle_problem_focus" + +# ------------------------------------ Navigation ------------------------------------- + +[[keymaps]] +key = "Ctrl+Home" +command = "document_start" + +[[keymaps]] +key = "Ctrl+End" +command = "document_end" + +[[keymaps]] +key = "ctrl+b" +command = "left" +mode = "n" + +[[keymaps]] +key = "ctrl+f" +command = "right" +mode = "n" + +[[keymaps]] +key = "ctrl+p" +command = "up" +when = "!list_focus" +mode = "n" + +[[keymaps]] +key = "ctrl+n" +command = "down" +when = "!list_focus" +mode = "n" + +[[keymaps]] +key = "ctrl+shift+o" +command = "palette.symbol" + +[[keymaps]] +key = "ctrl+t" +command = "palette.workspace_symbol" + +[[keymaps]] +key = "ctrl+g" +command = "palette.line" diff --git a/defaults/light-theme.toml b/defaults/light-theme.toml new file mode 100644 index 0000000..570e6f7 --- /dev/null +++ b/defaults/light-theme.toml @@ -0,0 +1,216 @@ +#:schema ../extra/schemas/color-theme.json + +[color-theme] +name = "Lapce Light" + +[ui] +font-family = "" +font-size = 13 +header-height = 35 +status-height = 25 +tab-min-width = 100 +activity-width = 50 +scroll-width = 10 +drop-shadow-width = 0 + +[color-theme.base] +black = "#383A42" +blue = "#4078F2" +cyan = "#0184BC" +green = "#50A14F" +grey = "#E5E5E6" +magenta = "#A626A4" +orange = "#D19A66" +purple = "#A626A4" +red = "#E45649" +white = "#FAFAFA" +yellow = "#C18401" + +primary-background = "$white" +# Background for 'secondary' elements: panels, palette, status bar, completion, hover +secondary-background = "#EAEAEB" +current-background = "#DBDBDC" +text = "$black" +dim-text = "#A0A1A7" + +[color-theme.syntax] +"comment" = "$dim-text" +"constant" = "$yellow" +"type" = "$yellow" +"typeAlias" = "$yellow" +"number" = "$yellow" +"enum" = "$yellow" +"struct" = "$yellow" +"structure" = "$yellow" +"interface" = "$yellow" +"attribute" = "$yellow" +"constructor" = "$yellow" + +"function" = "$blue" +"method" = "$blue" +"function.method" = "$blue" + +"keyword" = "$purple" +"selfKeyword" = "$purple" + +"field" = "$red" +"property" = "$red" +"enumMember" = "$red" +"enum-member" = "$red" + +"string" = "$green" +"string.escape" = "$cyan" + +"type.builtin" = "$cyan" +"builtinType" = "$cyan" +"escape" = "$cyan" +"embedded" = "$cyan" + +"punctuation.delimiter" = "$yellow" +"text.title" = "$orange" +"text.uri" = "$cyan" +"text.reference" = "$yellow" +"variable" = "$red" +"variable.other.member" = "$red" +"tag" = "$blue" + +"markup.heading" = "$red" +"markup.bold" = "$orange" +"markup.italic" = "$orange" +"markup.list" = "$orange" +"markup.link.url" = "$blue" +"markup.link.label" = "$purple" +"markup.link.text" = "$purple" + +"bracket.color.1" = "$blue" +"bracket.color.2" = "$yellow" +"bracket.color.3" = "$purple" +"bracket.unpaired" = "$red" + +[color-theme.ui] +"lapce.error" = "#E51400" +"lapce.warn" = "#E9A700" +"lapce.dropdown_shadow" = "#B4B4B4" +"lapce.border" = "#B4B4B4" +"lapce.scroll_bar" = "#B4B4B4BB" + +"lapce.button.primary.background" = "#50a14f" +"lapce.button.primary.foreground" = "$white" + +# tab +"lapce.tab.active.background" = "$primary-background" +"lapce.tab.active.foreground" = "$text" +"lapce.tab.active.underline" = "#528BFF" + +"lapce.tab.inactive.background" = "#EAEAEB" +"lapce.tab.inactive.foreground" = "$text" +"lapce.tab.inactive.underline" = "#528BFF77" + +"lapce.tab.separator" = "#B4B4B4" + +"lapce.icon.active" = "$text" +"lapce.icon.inactive" = "$dim-text" + +"lapce.remote.icon" = "$white" +"lapce.remote.local" = "#4078F2" +"lapce.remote.connected" = "#50A14F" +"lapce.remote.connecting" = "#C18401" +"lapce.remote.disconnected" = "#E45649" + +"lapce.plugin.name" = "#444444" +"lapce.plugin.description" = "$text" +"lapce.plugin.author" = "#707070" + +"terminal.cursor" = "$text" +"terminal.foreground" = "$text" +"terminal.background" = "$primary-background" +"terminal.white" = "$white" +"terminal.black" = "$black" +"terminal.red" = "$red" +"terminal.blue" = "$blue" +"terminal.green" = "$green" +"terminal.yellow" = "$yellow" +"terminal.cyan" = "$cyan" +"terminal.magenta" = "$magenta" +"terminal.bright_white" = "#090A0B" +"terminal.bright_red" = "$red" +"terminal.bright_blue" = "$blue" +"terminal.bright_green" = "$green" +"terminal.bright_yellow" = "$yellow" +"terminal.bright_cyan" = "$cyan" +"terminal.bright_magenta" = "$magenta" +"terminal.bright_black" = "#A0A1A7" + +"editor.background" = "$primary-background" +"editor.foreground" = "$text" +"editor.dim" = "$dim-text" +"editor.focus" = "#000000" +"editor.caret" = "#526FFF" +"editor.selection" = "$grey" +"editor.current_line" = "#F2F2F2" +"editor.debug_break_line" = "#528bFF55" +"editor.link" = "$blue" +"editor.visible_whitespace" = "$grey" +"editor.indent_guide" = "$grey" +"editor.drag_drop_background" = "#79c1fc33" +"editor.drag_drop_tab_background" = "#0b0e1433" +"editor.sticky_header_background" = "$primary-background" + +"inlay_hint.foreground" = "$text" +"inlay_hint.background" = "#528bFF55" + +"error_lens.error.foreground" = "$red" +"error_lens.error.background" = "#E4564920" +"error_lens.warning.foreground" = "$yellow" +"error_lens.warning.background" = "#C1840120" +"error_lens.other.foreground" = "$dim-text" +"error_lens.other.background" = "#A0A1A720" + +"completion_lens.foreground" = "$dim-text" + +"source_control.added" = "#50A14FCC" +"source_control.removed" = "#FF5266CC" +"source_control.modified" = "#0184BCCC" + +"tooltip.background" = "$primary-background" +"tooltip.foreground" = "$text" + +"palette.background" = "$secondary-background" +"palette.foreground" = "$text" +"palette.current.background" = "$current-background" +"palette.current.foreground" = "$text" + +"completion.background" = "$secondary-background" +"completion.current" = "$current-background" + +"hover.background" = "$secondary-background" + +"activity.background" = "$secondary-background" +"activity.current" = "$primary-background" + +"debug.breakpoint" = "$red" +"debug.breakpoint.hover" = "#E4564966" + +"panel.background" = "$secondary-background" +"panel.foreground" = "$text" +"panel.foreground.dim" = "$dim-text" +"panel.current.background" = "$current-background" +"panel.current.foreground" = "$text" +"panel.current.foreground.dim" = "$dim-text" +"panel.hovered.background" = "#CBCBCB" +"panel.hovered.active.background" = "$dim-text" +"panel.hovered.foreground" = "$text" +"panel.hovered.foreground.dim" = "$dim-text" + +"status.background" = "$secondary-background" +"status.foreground" = "$text" +"status.modal.normal.background" = "$blue" +"status.modal.normal.foreground" = "$white" +"status.modal.insert.background" = "$red" +"status.modal.insert.foreground" = "$white" +"status.modal.visual.background" = "$yellow" +"status.modal.visual.foreground" = "$white" +"status.modal.terminal.background" = "$purple" +"status.modal.terminal.foreground" = "$white" + +"markdown.blockquote" = "#686868" diff --git a/defaults/run.toml b/defaults/run.toml new file mode 100644 index 0000000..35478ab --- /dev/null +++ b/defaults/run.toml @@ -0,0 +1,30 @@ + +# The run config is used for both run mode and debug mode + +[[configs]] +# the name of this task +name = "task" + +# the type of the debugger. If not set, it can't be debugged but can still be run +# type = "lldb" + +# the program to run, e.g. "${workspace}\\target\\debug\\check.exe" +program = "" + +# the program arguments, e.g. args = ["arg1", "arg2"], optional +# args = [] + +# current working directory, optional +# cwd = "${workspace}" + +# environment variables, optional +# [configs.env] +# VAR1 = "VAL1" +# VAR2 = "VAL2" + +# task to run before the run/debug session is started, optional +# [configs.prelaunch] +# program = "cargo" +# args = [ +# "build", +# ] diff --git a/defaults/settings.toml b/defaults/settings.toml new file mode 100644 index 0000000..9a72289 --- /dev/null +++ b/defaults/settings.toml @@ -0,0 +1,105 @@ +#:schema ../extra/schemas/settings.json + +[core] +modal = false +color-theme = "Lapce Dark" +icon-theme = "Lapce Codicons" +custom-titlebar = true +file-explorer-double-click = false +auto-reload-plugin = false + +[editor] +font-family = "monospace" +font-size = 13 +code-glance-font-size = 2 +line-height = 1.5 +smart-tab = true +tab-width = 4 +show-tab = true +show-bread-crumbs = true +scroll-beyond-last-line = true +cursor-surrounding-lines = 1 +wrap-style = "editor-width" +wrap-column = 80 +wrap-width = 600 # px +sticky-header = true +completion-width = 600 +completion-show-documentation = true +completion-item-show-detail = false +show-signature = true +signature-label-code-block = true +auto-closing-matching-pairs = true +auto-surround = true +hover-delay = 300 # ms +modal-mode-relative-line-numbers = true +format-on-save = false +highlight-matching-brackets = true +highlight-selection-occurrences = true +highlight-scope-lines = false +autosave-interval = 0 +format-on-autosave = true +normalize-line-endings = true +enable-inlay-hints = true +inlay-hint-font-family = "" +inlay-hint-font-size = 0 +enable-error-lens = true +only-render-error-styling = true +error-lens-end-of-line = true +error-lens-font-family = "" +error-lens-font-size = 0 +error-lens-multiline = false +enable-completion-lens = false +enable-inline-completion = true +completion-lens-font-family = "" +completion-lens-font-size = 0 +blink-interval = 500 # ms +multicursor-case-sensitive = true +multicursor-whole-words = true +render-whitespace = "none" +show-indent-guide = true +atomic-soft-tabs = false +double-click = "single" +move-focus-while-search = true +diff-context-lines = 3 +scroll-speed-modifier = 1 +bracket-pair-colorization = false +bracket-colorization-limit = 30000 +files-exclude = "**/{.git,.svn,.hg,CVS,.DS_Store,Thumbs.db}" # Glob patterns + +[terminal] +font-family = "" +font-size = 0 +line-height = 0 + +[terminal.default-profile] +macos = "default" +linux = "default" +windows = "default" + +[terminal.profiles] +default = {} + +# [terminal.profiles.example] +# command = "cargo" +# arguments = ["run"] +# environment = { "KEY" = "VALUE" } +# workdir = "/home/user" + +[ui] +scale = +1.0 +font-family = "" +font-size = 13 +icon-size = 0 +header-height = 36 +status-height = 25 +tab-min-width = 100 +tab-separator-height = "Content" +scroll-width = 10 +drop-shadow-width = 0 +palette-width = 500 +hover-font-family = "" +hover-font-size = 0 +trim-search-results-whitespace = true +list-line-height = 25 +tab-close-button = "Right" +open-editors-visible = true diff --git a/deny.toml b/deny.toml new file mode 100644 index 0000000..e33f267 --- /dev/null +++ b/deny.toml @@ -0,0 +1,15 @@ +[licenses] +# use SPDX short identifiers +allow = [ + "Apache-2.0", + "Apache-2.0 WITH LLVM-exception", + "BSD-2-Clause", + "BSD-3-Clause", + "BSL-1.0", + "CC0-1.0", + "ISC", + "MIT", + "MPL-2.0", + "Unicode-DFS-2016", + "Zlib", +] diff --git a/docker-bake.hcl b/docker-bake.hcl new file mode 100644 index 0000000..d30795f --- /dev/null +++ b/docker-bake.hcl @@ -0,0 +1,244 @@ +variable "RUST_VERSION" { + default = "1" +} + +variable "XX_VERSION" { + default = "master" +} + +variable "PACKAGE_NAME" { + default = RELEASE_TAG_NAME == "nightly" ? "lapce-nightly" : "lapce" +} + +variable "RELEASE_TAG_NAME" {} + +target "_common" { + context = "." + platforms = ["local"] + output = ["target"] + args = { + PACKAGE_NAME = PACKAGE_NAME + RELEASE_TAG_NAME = RELEASE_TAG_NAME + RUST_VERSION = RUST_VERSION + XX_VERSION = XX_VERSION + } +} + +variable "platforms" { + default = [ + "linux/amd64", + "linux/arm64", + ] +} + +target "_platforms" { + platforms = platforms +} + +group "default" { + targets = ["binary"] +} + +target "binary" { + inherits = ["_common"] + target = "binary" + args = { + ZSTD_SYS_USE_PKG_CONFIG = "1" + LIBGIT2_STATIC = "1" + LIBSSH2_STATIC = "1" + LIBZ_SYS_STATIC = "1" + OPENSSL_STATIC = "1" + OPENSSL_NO_VENDOR = "0" + PKG_CONFIG_ALL_STATIC = "1" + } +} + +target "cross-binary" { + inherits = ["binary", "_platforms"] + target = "cross-binary" +} + +target "package" { + inherits = ["_common"] + target = "package" + args = { + ZSTD_SYS_USE_PKG_CONFIG = "1" + LIBGIT2_STATIC = "0" + LIBSSH2_STATIC = "0" + LIBZ_SYS_STATIC = "0" + OPENSSL_STATIC = "0" + OPENSSL_NO_VENDOR = "1" + PKG_CONFIG_ALL_STATIC = "0" + } +} + +target "cross-package" { + inherits = ["package", "_platforms"] + target = "cross-package" +} + +// OS + +variable "DPKG_FAMILY_PACKAGES" { + default = [ + "libc6-dev", + "libssl-dev", + "zlib1g-dev", + "libzstd-dev", + "libvulkan-dev", + "libwayland-dev", + "libxcb-shape0-dev", + "libxcb-xfixes0-dev", + "libxkbcommon-x11-dev", + ] +} + +target "debian" { + inherits = [build.type] + name = "${os_name}-${build.os_version}-${build.type}" + dockerfile = "extra/linux/docker/${os_name}/Dockerfile" + args = { + DISTRIBUTION_NAME = os_name + DISTRIBUTION_VERSION = build.os_version + DISTRIBUTION_PACKAGES = join(" ", coalesce(build.packages, DPKG_FAMILY_PACKAGES)) + } + platforms = coalesce(build.platforms, platforms) + matrix = { + os_name = ["debian"] + build = [ + { packages = null, platforms = null, type = "package", os_version = "bullseye" }, # 11 + { packages = null, platforms = null, type = "package", os_version = "bookworm" }, # 12 + ] + } +} + +target "cross-debian" { + inherits = ["debian", "cross-package"] +} + +target "ubuntu" { + inherits = [build.type] + name = "${os_name}-${build.os_version}-${build.type}" + dockerfile = "extra/linux/docker/${os_name}/Dockerfile" + args = { + DISTRIBUTION_NAME = os_name + DISTRIBUTION_VERSION = build.os_version + DISTRIBUTION_PACKAGES = join(" ", coalesce(build.packages, DPKG_FAMILY_PACKAGES)) + } + platforms = coalesce(build.platforms, platforms) + matrix = { + os_name = ["ubuntu"] + build = [ + { packages = null, platforms = null, type = "package", os_version = "bionic" }, # 18.04 + { packages = null, platforms = null, type = "package", os_version = "focal" }, # 20.04 + { packages = null, platforms = null, type = "package", os_version = "jammy" }, # 22.04 + { packages = null, platforms = null, type = "package", os_version = "noble" }, # 24.04 + { packages = null, platforms = null, type = "package", os_version = "oracular" }, # 24.10 + { packages = null, platforms = null, type = "package", os_version = "plucky" }, # 25.04 + # static binary, it looks ugly to define the target this way + # but I don't have a better way to make it more friendly on CLI side without + # more terrible code-wise way to implement it + { packages = null, platforms = null, type = "binary", os_version = "focal" }, # 20.04 + ] + } +} + +target "cross-ubuntu" { + inherits = ["ubuntu", "cross-package"] +} + +variable "RHEL_FAMILY_PACKAGES" { + default = [ + "openssl-devel", + "wayland-devel", + "vulkan-loader-devel", + "libzstd-devel", + "libxcb-devel", + "libxkbcommon-x11-devel", + ] +} + +target "fedora" { + inherits = [build.type] + name = "${name}-${build.version}-${build.type}" + dockerfile = "extra/linux/docker/${name}/Dockerfile" + args = { + XX_VERSION = "test" + + DISTRIBUTION_NAME = name + DISTRIBUTION_VERSION = build.version + DISTRIBUTION_PACKAGES = join(" ", coalesce(build.packages, RHEL_FAMILY_PACKAGES)) + } + // platforms = coalesce(build.platforms, platforms) + platforms = ["linux/amd64"] + matrix = { + name = ["fedora"] + build = [ + { packages = null, platforms = null, type = "package", version = "39" }, + { packages = null, platforms = null, type = "package", version = "40" }, + { packages = null, platforms = null, type = "package", version = "41" }, + { packages = null, platforms = null, type = "package", version = "42" }, + { packages = null, platforms = null, type = "package", version = "43" }, + { packages = null, platforms = null, type = "package", version = "rawhide" }, + ] + } +} + +target "cross-fedora" { + inherits = ["fedora", "cross-package"] +} + +variable "APK_FAMILY_PACKAGES" { + default = [ + "make", + "clang", + "git", + "lld", + "build-base", + "rustup", + "openssl-dev", + "openssl-libs-static", + "libssh2-static", + "libgit2-static", + "fontconfig-static", + "freetype-static", + "zlib-static", + "gcc", + "zstd-static", + "libxcb-static", + "libxkbcommon-static", + "vulkan-loader-dev", + ] +} + +target "alpine" { + inherits = ["binary"] + name = format("${os_name}-%s", replace(build.os_version, ".", "-")) + dockerfile = "extra/linux/docker/${os_name}/Dockerfile" + args = { + DISTRIBUTION_NAME = os_name + DISTRIBUTION_VERSION = build.os_version + DISTRIBUTION_PACKAGES = join(" ", coalesce(build.packages, APK_FAMILY_PACKAGES)) + } + platforms = coalesce(build.platforms, platforms) + matrix = { + os_name = ["alpine"] + build = [ + { os_version = "", packages = null, platforms = null }, + { os_version = "3.22", packages = null, platforms = null }, + { os_version = "3.20", packages = null, platforms = null }, + { os_version = "3.18", packages = null, platforms = null }, + ] + } +} + +target "cross-alpine" { + inherits = ["alpine-3-20", "cross-binary"] +} + +target "alpine-dev" { + inherits = ["alpine-3-20"] + target = "dev" + tags = ["lapce/lapce:dev"] + output = ["type=docker"] +} diff --git a/docs/building-from-source.md b/docs/building-from-source.md new file mode 100644 index 0000000..970176e --- /dev/null +++ b/docs/building-from-source.md @@ -0,0 +1,50 @@ +## Building from source + +It is easy to build Lapce from source on a GNU/Linux distribution. Cargo handles the build process, all you need to do, is ensure the correct dependencies are installed. + +1. Install the Rust compiler and Cargo using [`rustup.rs`](https://rustup.rs/). If you already have the toolchain, ensure you are using latest Rust version. + +2. Install dependencies for your operating system: + +#### Ubuntu +```sh +sudo apt install clang libxkbcommon-x11-dev pkg-config libvulkan-dev libwayland-dev xorg-dev libxcb-shape0-dev libxcb-xfixes0-dev +``` +#### Fedora +```sh +sudo dnf install clang libxkbcommon-x11-devel libxcb-devel vulkan-loader-devel wayland-devel openssl-devel pkgconf +``` +#### Void Linux +```sh +sudo xbps-install -S base-devel clang libxkbcommon-devel vulkan-loader wayland-devel +``` + +3. Clone this repository (this command will clone to your home directory): +```sh +git clone https://github.com/lapce/lapce.git ~/lapce +``` + +4. `cd` into the repository, and run the build command with the release flag +```sh +cd ~/lapce +``` + +```sh +cargo install --path . --bin lapce --profile release-lto --locked +``` + +> If you use a different distribution, and are having trouble finding appropriate dependencies, let us know in an issue! + +Once Lapce is compiled, the executable will be available in `$HOME/.cargo/bin/lapce` and should be available in `PATH` automatically. + +## Building using Docker or Podman + +Packages available in releases are built using containers based on multi-stage Dockerfiles. To easily orchestrate builds, there is a `docker-bake.hcl` manifest in root of repository that defines all stages and targets. +If you want to build all packages for ubuntu, you can run `RELEASE_TAG_NAME=nightly docker buildx bake ubuntu` (`RELEASE_TAG_NAME` is a required environment variable used to tell what kind of release is being built as well as baking in the version itself). +To scope in to specific distribution version, you can define target with it's version counterpart from matrix, e.g. to build only Ubuntu Focal package, you can run `RELEASE_TAG_NAME=nightly docker buildx bake ubuntu-focal`. +Additionally to building multiple OS versions at the same time, Docker-based builds will also try to cross-compile Lapce for other architectures. +This does not require QEMU installed as it's done via true cross-compilation meaning `HOST` will run your native OS/CPU architecture and `TARGET` will be the wanted architecture, instead of spawning container that's running OS using `TARGET` architecture. + +> ![WARNING] +> Do not run plain targets like `ubuntu` or `fedora` if you don't have very powerful machine, as it will spawn many concurrent jobs +> which will take a long time to build. diff --git a/docs/installing-with-package-manager.md b/docs/installing-with-package-manager.md new file mode 100644 index 0000000..4acd76b --- /dev/null +++ b/docs/installing-with-package-manager.md @@ -0,0 +1,20 @@ +## Installation With Package Manager + +Lapce is available in below software repositories: + +[![Packaging status](https://repology.org/badge/vertical-allrepos/lapce.svg)](https://repology.org/project/lapce/versions) + +Lapce is also additionally available via: + +- [Flatpak](https://flathub.org/apps/details/dev.lapce.lapce) +- [Scoop](https://scoop.sh/#/apps?q=lapce) + + +For Fedora Linux, Lapce is (unofficially) available via dnf RPM package manager, using nightly as of 2024.01: + +- [copr](https://copr.fedorainfracloud.org/coprs/titaniumtown/lapce/) + +```bash +sudo dnf copr enable titaniumtown/lapce +sudo dnf install lapce +``` diff --git a/docs/new-release.md b/docs/new-release.md new file mode 100644 index 0000000..dcd66a3 --- /dev/null +++ b/docs/new-release.md @@ -0,0 +1,8 @@ +# Updating versioning for package managers and whatnot + +- App metainfo: `extra/linux/dev.lapce.lapce.metainfo.xml` +- macOS plist (`CFBundleShortVersionString`): `extra/macos/Lapce.app/Contents/Info.plist` +- Rust: `Cargo.toml` +- Obviously changelog: `CHANGELOG.md` +- RPM spec: `lapce.spec` +- Windows wix spec (``): `extra/windows/wix/lapce.wxs` diff --git a/docs/why-lapce.md b/docs/why-lapce.md new file mode 100644 index 0000000..8ecfa5c --- /dev/null +++ b/docs/why-lapce.md @@ -0,0 +1,23 @@ +I'm a Vim fan. After years of using it, I started to customize it like everybody else. I thought I had something that worked quite well until I hit one problem. My linter sometimes took seconds to check the code, and it would freeze Vim because Vim didn't have asynchronous support. I looked around; I found NeoVim and problem solved. Happy days. + +But there was always this one thing I couldn't solve. I want to make Vim pretty. I tried all kinds of themes. Tried all kinds of status bar plugins with icons in it. File explorers with file icons in them. But no, I also wanted a 1px vertical split bar, not the thick colored one or the dashed line drawn with pipe characters. After hours and hours of search, it occurred to me that it's impossible to do. Again NeoVim was the savior. It introduced external UI. The idea was that NeoVim splits the UI and the backend. The backend emits events to the UI, and the UI draws them. I couldn't wait to try it out with writing a UI in Electron, with the hope that I could solve my vertical split bar dream. But I didn't because it emits the drawing events treating it as a whole canvas, and I don't get the boundary of each splits. I started to hack NeoVim code. I made it to emit split sizes and positions, and I finally can draw the bars in the UI. With joy, I also looked at emits the command modes to the UI, so I could put the command in the middle of the window. And then echo messages, status line, etc. I pushed a PR for the hacky solution(https://github.com/neovim/neovim/pull/5686). Then I hit performance issues in Electron because Javascript was not fast enough to deserialize the drawing events from NeoVim. So I wrote another UI in Go with Qt binding(https://github.com/dzhou121/gonvim). + +I wanted to external more and more components in NeoVim, but I found it harder and harder, and I spotted Xi-Editor. I started to write a UI for it straightaway. Creating a code editor with Vim editing experience was the plan. There were things missing that I had to add in Xi. And working on it was so much easier because Xi was built to be the UI/backend architecture without the heritage(burden) of (Neo)Vim. + +Then one day, I experienced VSCode's remote development feature, and it felt so "local". I wanted to add the feature to my code editor. Then I realized that it can't be done with NeoVim or Xi's UI/backend architecture. The reason was that (Neo)Vim/Xi backends are the editing engine, so when you put the backend to a remote machine, every keyboard input needs to be sent to it, and the backend emits the drawing events to you, which will include the network latency in everything you type. That wouldn't work. The editing logic must be tightly bound with the UI to give the best editing experience. + +So the new architecture I came up with was like this: + + UI +Reads file from Proxy
+Handle keyboard/mouse events and do the edits on the file buffer
+Send the file editing delta to the proxy to keep the file in sync
+ + Proxy +Receive save event from UI and flush the file buffer to disk
+proxy the events between UI and the plugins
+ + Plugin +Communicate with UI through proxy
+ +UI sits locally. Proxy and Plugin will be in the remote box when doing remote development. With this architecture, I can make sure the editing experience is always the best, with other considerations like syntax highlighting being done in a different thread, so nothing blocks the main thread at any time. I finally had a lightning-fast and powerful code editor. (which can be beautiful as well) diff --git a/extra/entitlements.plist b/extra/entitlements.plist new file mode 100644 index 0000000..ad77a2a --- /dev/null +++ b/extra/entitlements.plist @@ -0,0 +1,14 @@ + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.allow-dyld-environment-variables + + com.apple.security.cs.disable-library-validation + + + \ No newline at end of file diff --git a/extra/fonts/DejaVu/DejaVuSans.ttf b/extra/fonts/DejaVu/DejaVuSans.ttf new file mode 100644 index 0000000..e5f7eec Binary files /dev/null and b/extra/fonts/DejaVu/DejaVuSans.ttf differ diff --git a/extra/fonts/DejaVu/DejaVuSansMono.ttf b/extra/fonts/DejaVu/DejaVuSansMono.ttf new file mode 100644 index 0000000..f578602 Binary files /dev/null and b/extra/fonts/DejaVu/DejaVuSansMono.ttf differ diff --git a/extra/fonts/DejaVu/LICENSE b/extra/fonts/DejaVu/LICENSE new file mode 100644 index 0000000..df52c17 --- /dev/null +++ b/extra/fonts/DejaVu/LICENSE @@ -0,0 +1,187 @@ +Fonts are (c) Bitstream (see below). DejaVu changes are in public domain. +Glyphs imported from Arev fonts are (c) Tavmjong Bah (see below) + + +Bitstream Vera Fonts Copyright +------------------------------ + +Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera is +a trademark of Bitstream, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of the fonts accompanying this license ("Fonts") and associated +documentation files (the "Font Software"), to reproduce and distribute the +Font Software, including without limitation the rights to use, copy, merge, +publish, distribute, and/or sell copies of the Font Software, and to permit +persons to whom the Font Software is furnished to do so, subject to the +following conditions: + +The above copyright and trademark notices and this permission notice shall +be included in all copies of one or more of the Font Software typefaces. + +The Font Software may be modified, altered, or added to, and in particular +the designs of glyphs or characters in the Fonts may be modified and +additional glyphs or characters may be added to the Fonts, only if the fonts +are renamed to names not containing either the words "Bitstream" or the word +"Vera". + +This License becomes null and void to the extent applicable to Fonts or Font +Software that has been modified and is distributed under the "Bitstream +Vera" names. + +The Font Software may be sold as part of a larger software package but no +copy of one or more of the Font Software typefaces may be sold by itself. + +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, +TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME +FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING +ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF +THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE +FONT SOFTWARE. + +Except as contained in this notice, the names of Gnome, the Gnome +Foundation, and Bitstream Inc., shall not be used in advertising or +otherwise to promote the sale, use or other dealings in this Font Software +without prior written authorization from the Gnome Foundation or Bitstream +Inc., respectively. For further information, contact: fonts at gnome dot +org. + +Arev Fonts Copyright +------------------------------ + +Copyright (c) 2006 by Tavmjong Bah. All Rights Reserved. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of the fonts accompanying this license ("Fonts") and +associated documentation files (the "Font Software"), to reproduce +and distribute the modifications to the Bitstream Vera Font Software, +including without limitation the rights to use, copy, merge, publish, +distribute, and/or sell copies of the Font Software, and to permit +persons to whom the Font Software is furnished to do so, subject to +the following conditions: + +The above copyright and trademark notices and this permission notice +shall be included in all copies of one or more of the Font Software +typefaces. + +The Font Software may be modified, altered, or added to, and in +particular the designs of glyphs or characters in the Fonts may be +modified and additional glyphs or characters may be added to the +Fonts, only if the fonts are renamed to names not containing either +the words "Tavmjong Bah" or the word "Arev". + +This License becomes null and void to the extent applicable to Fonts +or Font Software that has been modified and is distributed under the +"Tavmjong Bah Arev" names. + +The Font Software may be sold as part of a larger software package but +no copy of one or more of the Font Software typefaces may be sold by +itself. + +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL +TAVMJONG BAH BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. + +Except as contained in this notice, the name of Tavmjong Bah shall not +be used in advertising or otherwise to promote the sale, use or other +dealings in this Font Software without prior written authorization +from Tavmjong Bah. For further information, contact: tavmjong @ free +. fr. + +TeX Gyre DJV Math +----------------- +Fonts are (c) Bitstream (see below). DejaVu changes are in public domain. + +Math extensions done by B. Jackowski, P. Strzelczyk and P. Pianowski +(on behalf of TeX users groups) are in public domain. + +Letters imported from Euler Fraktur from AMSfonts are (c) American +Mathematical Society (see below). +Bitstream Vera Fonts Copyright +Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera +is a trademark of Bitstream, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of the fonts accompanying this license (“Fonts”) and associated +documentation +files (the “Font Software”), to reproduce and distribute the Font Software, +including without limitation the rights to use, copy, merge, publish, +distribute, +and/or sell copies of the Font Software, and to permit persons to whom +the Font Software is furnished to do so, subject to the following +conditions: + +The above copyright and trademark notices and this permission notice +shall be +included in all copies of one or more of the Font Software typefaces. + +The Font Software may be modified, altered, or added to, and in particular +the designs of glyphs or characters in the Fonts may be modified and +additional +glyphs or characters may be added to the Fonts, only if the fonts are +renamed +to names not containing either the words “Bitstream” or the word “Vera”. + +This License becomes null and void to the extent applicable to Fonts or +Font Software +that has been modified and is distributed under the “Bitstream Vera” +names. + +The Font Software may be sold as part of a larger software package but +no copy +of one or more of the Font Software typefaces may be sold by itself. + +THE FONT SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, +TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME +FOUNDATION +BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, +SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN +ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR +INABILITY TO USE +THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. +Except as contained in this notice, the names of GNOME, the GNOME +Foundation, +and Bitstream Inc., shall not be used in advertising or otherwise to promote +the sale, use or other dealings in this Font Software without prior written +authorization from the GNOME Foundation or Bitstream Inc., respectively. +For further information, contact: fonts at gnome dot org. + +AMSFonts (v. 2.2) copyright + +The PostScript Type 1 implementation of the AMSFonts produced by and +previously distributed by Blue Sky Research and Y&Y, Inc. are now freely +available for general use. This has been accomplished through the +cooperation +of a consortium of scientific publishers with Blue Sky Research and Y&Y. +Members of this consortium include: + +Elsevier Science IBM Corporation Society for Industrial and Applied +Mathematics (SIAM) Springer-Verlag American Mathematical Society (AMS) + +In order to assure the authenticity of these fonts, copyright will be +held by +the American Mathematical Society. This is not meant to restrict in any way +the legitimate use of the fonts, such as (but not limited to) electronic +distribution of documents containing these fonts, inclusion of these fonts +into other public domain or commercial font collections or computer +applications, use of the outline data to create derivative fonts and/or +faces, etc. However, the AMS does require that the AMS copyright notice be +removed from any derivative versions of the fonts which have been altered in +any way. In addition, to ensure the fidelity of TeX documents using Computer +Modern fonts, Professor Donald Knuth, creator of the Computer Modern faces, +has requested that any alterations which yield different font metrics be +given a different name. + +$Id$ diff --git a/extra/images/linux/code-file-with-highlight.png b/extra/images/linux/code-file-with-highlight.png new file mode 100644 index 0000000..ca8a100 Binary files /dev/null and b/extra/images/linux/code-file-with-highlight.png differ diff --git a/extra/images/logo.png b/extra/images/logo.png new file mode 100644 index 0000000..73c0b50 Binary files /dev/null and b/extra/images/logo.png differ diff --git a/extra/images/logo.svg b/extra/images/logo.svg new file mode 100644 index 0000000..6d3fe1a --- /dev/null +++ b/extra/images/logo.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/extra/images/logo_app.svg b/extra/images/logo_app.svg new file mode 100644 index 0000000..4865afe --- /dev/null +++ b/extra/images/logo_app.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/extra/images/logo_app_light.svg b/extra/images/logo_app_light.svg new file mode 100644 index 0000000..d5106d0 --- /dev/null +++ b/extra/images/logo_app_light.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/extra/images/logo_color.svg b/extra/images/logo_color.svg new file mode 100644 index 0000000..3a78178 --- /dev/null +++ b/extra/images/logo_color.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/extra/images/screenshot.png b/extra/images/screenshot.png new file mode 100644 index 0000000..d56df3d Binary files /dev/null and b/extra/images/screenshot.png differ diff --git a/extra/images/volt.png b/extra/images/volt.png new file mode 100644 index 0000000..4bcfe9b Binary files /dev/null and b/extra/images/volt.png differ diff --git a/extra/linux/dev.lapce.lapce.desktop b/extra/linux/dev.lapce.lapce.desktop new file mode 100644 index 0000000..ba7ad9a --- /dev/null +++ b/extra/linux/dev.lapce.lapce.desktop @@ -0,0 +1,20 @@ +[Desktop Entry] +Version=1.0 +Type=Application + +Name=Lapce +Comment=Lightning-fast and powerful code editor written in Rust +Categories=Development;IDE; +GenericName=Code Editor +StartupWMClass=lapce + +Icon=dev.lapce.lapce +Exec=lapce %F +Terminal=false +MimeType=text/plain;inode/directory; +Actions=new-window; + +[Desktop Action new-window] +Name=New Window +Exec=lapce --new %F +Icon=dev.lapce.lapce diff --git a/extra/linux/dev.lapce.lapce.metainfo.xml b/extra/linux/dev.lapce.lapce.metainfo.xml new file mode 100644 index 0000000..64ed1a0 --- /dev/null +++ b/extra/linux/dev.lapce.lapce.metainfo.xml @@ -0,0 +1,63 @@ + + + dev.lapce.lapce + Lapce + Dongdong Zhou, et al. + Lightning-fast and powerful code editor written in Rust + MIT + Apache-2.0 + https://lapce.dev/ + https://github.com/lapce/lapce/issues + https://docs.lapce.dev/ + + IDE + Development + + +

+ Lapce is an open source code editor written in Rust. By utilising native GUI and GPU rendering, and with the performance Rust provides, Lapce is one of the fastest code editors out there. +

+

Features:

+
    +
  • Modal Editing (Vim like) support as first class citizen (can be turned off as well)
  • +
  • Built-in LSP (Language Server Protocol) support to give you code intelligence like code completion, diagnostics and code actions etc.
  • +
  • Built-in remote development support (inspired by VSCode Remote Development) for a seamless "local" experience, benefiting from the full power of the remote system.
  • +
  • Plugins can be written in programming languages that can compile to the WASI format (C, Rust, AssemblyScript)
  • +
  • Built-in terminal, so you can execute commands in your workspace, without leaving Lapce.
  • +
+
+ + dev.lapce.lapce.desktop + + + Lapce source project opened in Lapce with Rust source code file opened + https://raw.githubusercontent.com/lapce/lapce/refs/tags/v0.4.5/extra/images/linux/code-file-with-highlight.png + + + + + https://github.com/lapce/lapce/releases/tag/v0.4.6 + + #3821 + #3832 + #3818 + #3819 + + + + https://github.com/lapce/lapce/releases/tag/v0.4.5 + + #3795 + + + + https://github.com/lapce/lapce/releases/tag/v0.4.4 + + #3795 + + + + https://github.com/lapce/lapce/releases/tag/v0.4.3 + + +
diff --git a/extra/linux/docker/alpine/Dockerfile b/extra/linux/docker/alpine/Dockerfile new file mode 100644 index 0000000..f318350 --- /dev/null +++ b/extra/linux/docker/alpine/Dockerfile @@ -0,0 +1,123 @@ +# syntax=docker/dockerfile:1 + +ARG DISTRIBUTION_VERSION +ARG RUST_VERSION +ARG XX_VERSION=latest + +FROM --platform=$BUILDPLATFORM tonistiigi/xx:${XX_VERSION} AS xx +FROM --platform=$BUILDPLATFORM rust:${RUST_VERSION}-alpine${DISTRIBUTION_VERSION} AS build-base +COPY --from=xx / / + +SHELL [ "/bin/ash", "-c" ] + +# install host dependencies +ARG DISTRIBUTION_PACKAGES +RUN \ + --mount=type=cache,target=/var/cache/apk,sharing=private \ + --mount=type=cache,target=/etc/apk/cache,sharing=private \ +< debian/control + Package: ${PACKAGE_NAME} + Version: ${RELEASE_TAG_NAME} + Conflicts: ${conflicts} + Maintainer: Jakub Panek + Architecture: ${_PACKAGE_ARCHITECTURE} + Description: Lightning-fast and Powerful Code Editor + Source: https://lapce.dev +EOL + +depends=$(dpkg-shlibdeps -O -e usr/bin/lapce) +depends=$(echo "${depends}" | sed 's/shlibs:Depends=//') +echo "Depends: ${depends}" >> debian/control +mv debian DEBIAN + +. /etc/os-release + +dpkg-deb --root-owner-group --build . "${OUTPUT_DIR}"/"${PACKAGE_NAME}.${ID}.${VERSION_CODENAME}.${_PACKAGE_ARCHITECTURE}.deb" +EOF + +FROM build-base AS dev +COPY . ./dev + +FROM scratch AS binary +COPY --from=build /output/lapce . + +FROM scratch AS cross-binary +COPY --from=build /output/lapce . + +FROM scratch AS package +COPY --from=build /output/*.deb . + +FROM scratch AS cross-package +COPY --from=build /output/*.deb . diff --git a/extra/linux/docker/fedora/Dockerfile b/extra/linux/docker/fedora/Dockerfile new file mode 100644 index 0000000..03dd426 --- /dev/null +++ b/extra/linux/docker/fedora/Dockerfile @@ -0,0 +1,190 @@ +# syntax=docker/dockerfile:1 + +ARG DISTRIBUTION_VERSION +ARG RUST_VERSION +ARG XX_VERSION=latest + +FROM --platform=$BUILDPLATFORM ghcr.io/panekj/xx:${XX_VERSION} AS xx +FROM --platform=$BUILDPLATFORM fedora:${DISTRIBUTION_VERSION} AS build-base +COPY --from=xx --link / / + +SHELL [ "/bin/bash", "-c" ] + +# install host dependencies +ARG DISTRIBUTION_PACKAGES +RUN \ + --mount=type=cache,target=/var/cache/dnf,sharing=locked \ +<> /etc/dnf/dnf.conf +dnf update -y +dnf install -y \ + bash clang lld llvm file cmake pkg-config curl git rpm-build +EOF + +ENV CARGO_HOME="/cargo" +ENV RUSTUP_HOME="/rustup" + +RUN \ +< lapce.spec < /etc/apt/apt.conf.d/keep-cache +EOF + +# install host dependencies +ARG DISTRIBUTION_PACKAGES +RUN \ + --mount=type=cache,target=/var/cache/apt,sharing=private \ + --mount=type=cache,target=/var/lib/apt,sharing=private \ +< debian/control + Package: ${PACKAGE_NAME} + Version: ${RELEASE_TAG_NAME} + Conflicts: ${conflicts} + Maintainer: Jakub Panek + Architecture: ${_PACKAGE_ARCHITECTURE} + Description: Lightning-fast and Powerful Code Editor + Source: https://lapce.dev +EOL + +depends=$(dpkg-shlibdeps -O -e usr/bin/lapce) +depends=$(echo "${depends}" | sed 's/shlibs:Depends=//') +echo "Depends: ${depends}" >> debian/control +mv debian DEBIAN + +. /etc/os-release + +dpkg-deb --root-owner-group --build . "${OUTPUT_DIR}"/"${PACKAGE_NAME}.${ID}.${VERSION_CODENAME}.${_PACKAGE_ARCHITECTURE}.deb" +EOF + +FROM scratch AS package +COPY --from=package-prepare /output/*.deb . + +FROM scratch AS cross-package +COPY --from=package-prepare /output/*.deb . diff --git a/extra/macos/Lapce.app/Contents/Info.plist b/extra/macos/Lapce.app/Contents/Info.plist new file mode 100644 index 0000000..d99ad49 --- /dev/null +++ b/extra/macos/Lapce.app/Contents/Info.plist @@ -0,0 +1,58 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + lapce + CFBundleIdentifier + io.lapce + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + Lapce + CFBundlePackageType + APPL + CFBundleShortVersionString + 0.4.6 + CFBundleSupportedPlatforms + + MacOSX + + CFBundleVersion + 1 + CFBundleIconFile + lapce.icns + NSHighResolutionCapable + + NSMainNibFile + + NSSupportsAutomaticGraphicsSwitching + + CFBundleDisplayName + Lapce + NSRequiresAquaSystemAppearance + NO + NSAppleEventsUsageDescription + An application in Lapce would like to access AppleScript. + NSCalendarsUsageDescription + An application in Lapce would like to access calendar data. + NSCameraUsageDescription + An application in Lapce would like to access the camera. + NSContactsUsageDescription + An application in Lapce wants to access your contacts. + NSLocationAlwaysUsageDescription + An application in Lapce would like to access your location information, even in the background. + NSLocationUsageDescription + An application in Lapce would like to access your location information. + NSLocationWhenInUseUsageDescription + An application in Lapce would like to access your location information while active. + NSMicrophoneUsageDescription + An application in Lapce would like to access your microphone. + NSRemindersUsageDescription + An application in Lapce would like to access your reminders. + NSSystemAdministrationUsageDescription + An application in Lapce requires elevated permissions. + + \ No newline at end of file diff --git a/extra/macos/Lapce.app/Contents/Resources/lapce.icns b/extra/macos/Lapce.app/Contents/Resources/lapce.icns new file mode 100644 index 0000000..691e4b1 Binary files /dev/null and b/extra/macos/Lapce.app/Contents/Resources/lapce.icns differ diff --git a/extra/proxy.ps1 b/extra/proxy.ps1 new file mode 100644 index 0000000..42bc12e --- /dev/null +++ b/extra/proxy.ps1 @@ -0,0 +1,47 @@ +[CmdletBinding()] +param( + [string]$version, + [string]$directory +) + +$proxy = (Join-Path $directory 'lapce.exe') + +$LapceProcesses = (Get-Process -Name 'lapce' -EA SilentlyContinue).Count +if ($LapceProcesses -ne 0) { + Write-Host 'Proxy currently in use. Aborting installation' + exit +} + +if (Test-Path $proxy) { + Write-Host 'Proxy already installed' + exit +} + +switch ($env:PROCESSOR_ARCHITECTURE) { + # Only x86_64 is supported currently + 'AMD64' { + $arch = 'x86_64' + } +} + +$url = "https://github.com/lapce/lapce/releases/download/${version}/lapce-proxy-windows-${arch}.gz" +$gzip = Join-Path "${env:TMP}" "lapce-proxy-windows-${arch}.gz" + +$webclient = [System.Net.WebClient]::new() +$webclient.DownloadFile($url, $gzip) +$webclient.Dispose() + +[System.IO.Directory]::CreateDirectory($directory) + +$archive = [System.IO.File]::Open($gzip, [System.IO.FileMode]::Open) +$proxy_file = [System.IO.File]::Create($proxy) +$compressor = [System.IO.Compression.GZipStream]::new($archive, [System.IO.Compression.CompressionMode]::Decompress) +$compressor.CopyTo($proxy_file) +Start-Sleep -Seconds 3 +$compressor.close() +$proxy_file.close() +$archive.close() + +[System.IO.File]::Delete($gzip) + +Write-Host 'lapce-proxy installed' \ No newline at end of file diff --git a/extra/proxy.sh b/extra/proxy.sh new file mode 100644 index 0000000..6542860 --- /dev/null +++ b/extra/proxy.sh @@ -0,0 +1,104 @@ +#!/bin/sh +set -eux + +# This script is written to be as POSIX as possible +# so it works fine for all Unix-like operating systems + +test_cmd() { + command -v "$1" >/dev/null +} + +# proxy version +lapce_new_ver="${1}" +# proxy directory +# eval to resolve '~' into proper user dir +eval lapce_dir="'${2}'" + +case "${lapce_new_ver}" in + v*) + lapce_new_version=$(echo "${lapce_new_ver}" | cut -d'v' -f2) + lapce_new_ver_tag="${lapce_new_ver}" + ;; + nightly*) + lapce_new_version="${lapce_new_ver}" + lapce_new_ver_tag=$(echo ${lapce_new_ver} | cut -d '-' -f1) + ;; + *) + printf 'Unknown version\n' + exit 1 + ;; +esac + +if [ -e "${lapce_dir}/lapce" ]; then + lapce_installed_ver=$("${lapce_dir}/lapce" --version | cut -d' ' -f2) + + printf '[DEBUG]: Current proxy version: %s\n' "${lapce_installed_ver}" + printf '[DEBUG]: New proxy version: %s\n' "${lapce_new_version}" + if [ "${lapce_installed_ver}" = "${lapce_new_version}" ]; then + printf 'Proxy already exists\n' + exit 0 + else + printf 'Proxy outdated. Replacing proxy\n' + rm "${lapce_dir}/lapce" + fi +fi + +for _cmd in tar gzip uname; do + if ! test_cmd "${_cmd}"; then + printf 'Missing required command: %s\n' "${_cmd}" + exit 1 + fi +done + +# Currently only linux/darwin are supported +case $(uname -s) in + Linux) os_name=linux ;; + Darwin) os_name=darwin ;; + *) + printf '[ERROR] unsupported os\n' + exit 1 + ;; +esac + +# Currently only amd64/arm64 are supported +case $(uname -m) in + x86_64|amd64|x64) arch_name=x86_64 ;; + arm64|aarch64) arch_name=aarch64 ;; + # riscv64) arch_name=riscv64 ;; + *) + printf '[ERROR] unsupported arch\n' + exit 1 + ;; +esac + +lapce_download_url="https://github.com/lapce/lapce/releases/download/${lapce_new_ver_tag}/lapce-proxy-${os_name}-${arch_name}.gz" + +printf 'Creating "%s"\n' "${lapce_dir}" +mkdir -p "${lapce_dir}" +cd "${lapce_dir}" + +if test_cmd 'curl'; then + # How old curl has these options? we'll find out + printf 'Downloading using curl\n' + curl --proto '=https' --tlsv1.2 -LfS -O "${lapce_download_url}" + # curl --proto '=https' --tlsv1.2 -LZfS -o "${tmp_dir}/lapce-proxy-${os_name}-${arch_name}.gz" "${lapce_download_url}" +elif test_cmd 'wget'; then + printf 'Downloading using wget\n' + wget "${lapce_download_url}" +else + printf 'curl/wget not found, failed to download proxy\n' + exit 1 +fi + +printf 'Decompressing gzip\n' +gzip -df "${lapce_dir}/lapce-proxy-${os_name}-${arch_name}.gz" + +printf 'Renaming proxy \n' +mv -v "${lapce_dir}/lapce-proxy-${os_name}-${arch_name}" "${lapce_dir}/lapce" + +printf 'Making it executable\n' +chmod +x "${lapce_dir}/lapce" + +printf 'lapce-proxy installed\n' + +exit 0 diff --git a/extra/schemas/color-theme.json b/extra/schemas/color-theme.json new file mode 100644 index 0000000..3dc4821 --- /dev/null +++ b/extra/schemas/color-theme.json @@ -0,0 +1,202 @@ +{ + "$schema": "http://json-schema.org/draft-06/schema#", + "$ref": "#/definitions/ColorTheme", + "definitions": { + "ColorTheme": { + "type": "object", + "additionalProperties": false, + "properties": { + "color-theme": { + "$ref": "#/definitions/ColorThemeClass" + }, + "ui": { + "$ref": "#/definitions/UI" + } + }, + "required": [], + "title": "ColorTheme" + }, + "ColorThemeClass": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string" + }, + "base": { + "$ref": "#/definitions/Base" + }, + "syntax": { + "$ref": "#/definitions/Syntax" + }, + "ui": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "required": [], + "title": "ColorThemeClass" + }, + "Base": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "required": [], + "title": "Base" + }, + "Syntax": { + "type": "object", + "additionalProperties": false, + "properties": { + "comment": { + "type": "string" + }, + "constant": { + "type": "string" + }, + "type": { + "type": "string" + }, + "typeAlias": { + "type": "string" + }, + "number": { + "type": "string" + }, + "enum": { + "type": "string" + }, + "struct": { + "type": "string" + }, + "structure": { + "type": "string" + }, + "interface": { + "type": "string" + }, + "attribute": { + "type": "string" + }, + "constructor": { + "type": "string" + }, + "function": { + "type": "string" + }, + "method": { + "type": "string" + }, + "function.method": { + "type": "string" + }, + "keyword": { + "type": "string" + }, + "selfKeyword": { + "type": "string" + }, + "field": { + "type": "string" + }, + "property": { + "type": "string" + }, + "enumMember": { + "type": "string" + }, + "enum-member": { + "type": "string" + }, + "string": { + "type": "string" + }, + "type.builtin": { + "type": "string" + }, + "builtinType": { + "type": "string" + }, + "escape": { + "type": "string" + }, + "string.escape": { + "type": "string" + }, + "embedded": { + "type": "string" + }, + "punctuation.delimiter": { + "type": "string" + }, + "text.title": { + "type": "string" + }, + "text.uri": { + "type": "string" + }, + "text.reference": { + "type": "string" + }, + "variable": { + "type": "string" + }, + "variable.other.member": { + "type": "string" + }, + "tag": { + "type": "string" + }, + "bracket.color.1": { + "type": "string" + }, + "bracket.color.2": { + "type": "string" + }, + "bracket.color.3": { + "type": "string" + }, + "bracket.unpaired": { + "type": "string" + } + }, + "required": [], + "title": "Syntax" + }, + "UI": { + "type": "object", + "additionalProperties": false, + "properties": { + "font-family": { + "type": "string" + }, + "font-size": { + "type": "integer" + }, + "header-height": { + "type": "integer" + }, + "status-height": { + "type": "integer" + }, + "tab-min-width": { + "type": "integer" + }, + "activity-width": { + "type": "integer" + }, + "scroll-width": { + "type": "integer" + }, + "drop-shadow-width": { + "type": "integer" + } + }, + "required": [], + "title": "UI" + } + } +} \ No newline at end of file diff --git a/extra/schemas/icon-theme.json b/extra/schemas/icon-theme.json new file mode 100644 index 0000000..9bcc116 --- /dev/null +++ b/extra/schemas/icon-theme.json @@ -0,0 +1,60 @@ +{ + "$schema": "http://json-schema.org/draft-06/schema#", + "$ref": "#/definitions/IconTheme", + "definitions": { + "IconTheme": { + "type": "object", + "additionalProperties": false, + "properties": { + "icon-theme": { + "$ref": "#/definitions/IconThemeClass" + } + }, + "required": [], + "title": "IconTheme" + }, + "IconThemeClass": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string" + }, + "ui": { + "$ref": "#/definitions/IconMapping" + }, + "foldername": { + "$ref": "#/definitions/IconMapping" + }, + "filename": { + "$ref": "#/definitions/IconMapping" + }, + "extension": { + "$ref": "#/definitions/IconMapping" + } + }, + "required": [], + "title": "IconThemeClass" + }, + "IconMapping": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "*" : { + "type": "string" + } + }, + "title": "IconMapping" + }, + "Extension": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "[a-zA-Z0-9]" : { + "type": "string" + } + }, + "title": "IconMapping" + } + } +} diff --git a/extra/schemas/settings.json b/extra/schemas/settings.json new file mode 100644 index 0000000..a8bba8a --- /dev/null +++ b/extra/schemas/settings.json @@ -0,0 +1,353 @@ +{ + "$schema": "http://json-schema.org/draft-06/schema#", + "$ref": "#/definitions/Settings", + "definitions": { + "Settings": { + "type": "object", + "additionalProperties": false, + "properties": { + "core": { + "$ref": "#/definitions/Core" + }, + "editor": { + "$ref": "#/definitions/Editor" + }, + "terminal": { + "$ref": "#/definitions/Terminal" + }, + "ui": { + "$ref": "#/definitions/UI" + }, + "color-theme": { + "$ref": "file://./color-theme.json" + }, + "icon-theme": { + "$schema": "file://./icon-theme.json" + } + }, + "required": [], + "title": "Settings" + }, + "Base": { + "type": "object", + "additionalProperties": false, + "properties": { + "white": { + "type": "string" + }, + "black": { + "type": "string" + }, + "grey": { + "type": "string" + }, + "blue": { + "type": "string" + }, + "red": { + "type": "string" + }, + "yellow": { + "type": "string" + }, + "orange": { + "type": "string" + }, + "green": { + "type": "string" + }, + "purple": { + "type": "string" + }, + "cyan": { + "type": "string" + }, + "magenta": { + "type": "string" + } + }, + "required": [], + "title": "Base" + }, + "Syntax": { + "type": "object", + "additionalProperties": false, + "properties": { + "comment": { + "type": "string" + }, + "constant": { + "type": "string" + }, + "type": { + "type": "string" + }, + "typeAlias": { + "type": "string" + }, + "number": { + "type": "string" + }, + "enum": { + "type": "string" + }, + "struct": { + "type": "string" + }, + "structure": { + "type": "string" + }, + "interface": { + "type": "string" + }, + "attribute": { + "type": "string" + }, + "constructor": { + "type": "string" + }, + "function": { + "type": "string" + }, + "method": { + "type": "string" + }, + "function.method": { + "type": "string" + }, + "keyword": { + "type": "string" + }, + "keyword.control": { + "type": "string" + }, + "selfKeyword": { + "type": "string" + }, + "field": { + "type": "string" + }, + "property": { + "type": "string" + }, + "enumMember": { + "type": "string" + }, + "enum-member": { + "type": "string" + }, + "variable.other.member": { + "type": "string" + }, + "string": { + "type": "string" + }, + "type.builtin": { + "type": "string" + }, + "builtinType": { + "type": "string" + }, + "escape": { + "type": "string" + }, + "embedded": { + "type": "string" + }, + "symbol": { + "type": "string" + } + }, + "required": [], + "title": "Syntax" + }, + "Core": { + "type": "object", + "additionalProperties": false, + "properties": { + "modal": { + "type": "boolean" + }, + "color-theme": { + "type": "string" + }, + "icon-theme": { + "type": "string" + }, + "custom-titlebar": { + "type": "boolean" + } + }, + "required": [], + "title": "Core" + }, + "Editor": { + "type": "object", + "additionalProperties": false, + "properties": { + "font-family": { + "type": "string" + }, + "font-size": { + "type": "integer" + }, + "code-lens-font-size": { + "type": "integer" + }, + "line-height": { + "type": "number" + }, + "tab-width": { + "type": "integer" + }, + "show-tab": { + "type": "boolean" + }, + "show-bread-crumbs": { + "type": "boolean" + }, + "scroll-beyond-last-line": { + "type": "boolean" + }, + "cursor-surrounding-lines": { + "type": "integer" + }, + "sticky-header": { + "type": "boolean" + }, + "completion-show-documentation": { + "type": "boolean" + }, + "auto-closing-matching-pairs": { + "type": "boolean" + }, + "auto-surround": { + "type": "boolean" + }, + "hover-delay": { + "type": "integer" + }, + "modal-mode-relative-line-numbers": { + "type": "boolean" + }, + "format-on-save": { + "type": "boolean" + }, + "highlight-matching-brackets": { + "type": "boolean" + }, + "highlight-selection-occurrences": { + "type": "boolean" + }, + "autosave-interval": { + "type": "integer" + }, + "enable-inlay-hints": { + "type": "boolean" + }, + "inlay-hint-font-family": { + "type": "string" + }, + "inlay-hint-font-size": { + "type": "integer" + }, + "enable-error-lens": { + "type": "boolean" + }, + "error-lens-end-of-line": { + "type": "boolean" + }, + "error-lens-font-family": { + "type": "string" + }, + "error-lens-font-size": { + "type": "integer" + }, + "blink-interval": { + "type": "integer" + }, + "multicursor-case-sensitive": { + "type": "boolean" + }, + "multicursor-whole-words": { + "type": "boolean" + }, + "render-whitespace": { + "type": "string" + }, + "show-indent-guide": { + "type": "boolean" + }, + "atomic-soft-tabs": { + "type": "boolean" + } + }, + "required": [], + "title": "Editor" + }, + "Terminal": { + "type": "object", + "additionalProperties": false, + "properties": { + "font-family": { + "type": "string" + }, + "font-size": { + "type": "integer" + }, + "line-height": { + "type": "integer" + }, + "shell": { + "type": "string" + } + }, + "required": [], + "title": "Terminal" + }, + "UI": { + "type": "object", + "additionalProperties": false, + "properties": { + "font-family": { + "type": "string" + }, + "font-size": { + "type": "integer" + }, + "icon-size": { + "type": "integer" + }, + "header-height": { + "type": "integer" + }, + "status-height": { + "type": "integer" + }, + "tab-min-width": { + "type": "integer" + }, + "scroll-width": { + "type": "integer" + }, + "drop-shadow-width": { + "type": "integer" + }, + "palette-width": { + "type": "integer" + }, + "hover-font-family": { + "type": "string" + }, + "hover-font-size": { + "type": "integer" + }, + "trim-search-results-whitespace": { + "type": "boolean" + } + }, + "required": [], + "title": "UI" + } + } +} diff --git a/extra/windows/lapce.ico b/extra/windows/lapce.ico new file mode 100644 index 0000000..c78b16b Binary files /dev/null and b/extra/windows/lapce.ico differ diff --git a/extra/windows/wix/lapce.wxs b/extra/windows/wix/lapce.wxs new file mode 100644 index 0000000..dd37ac2 --- /dev/null +++ b/extra/windows/wix/lapce.wxs @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/extra/windows/wix/license.rtf b/extra/windows/wix/license.rtf new file mode 100644 index 0000000..420cb6e --- /dev/null +++ b/extra/windows/wix/license.rtf @@ -0,0 +1,53 @@ +{\rtf1\ansi\ansicpg1252\deff0\nouicompat\deflang1033{} +{\*\generator Riched20 10.0.17763}\viewkind4\uc1 +\pard\sa200\sl276\slmult1\qc\f0\fs22\lang9 Apache License\par +Version 2.0, January 2004\par +{{\field{\*\fldinst{HYPERLINK http://www.apache.org/licenses/ }}{\fldrslt{http://www.apache.org/licenses/\ul0\cf0}}}}\f0\fs22\par + +\pard\sa200\sl276\slmult1\par +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\par +\par +1. Definitions.\par + "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.\par + "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.\par + "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.\par + "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.\par + "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.\par + "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.\par + "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).\par + "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.\par + "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."\par + "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.\par +2. Grant of Copyright License.\par +Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.\par +3. Grant of Patent License.\par +Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.\par +4. Redistribution.\par +You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:\par + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and\par + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and\par + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and\par + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.\par + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.\par +5. Submission of Contributions.\par +Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.\par +6. Trademarks.\par +This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.\par +7. Disclaimer of Warranty.\par +Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.\par +8. Limitation of Liability.\par +In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.\par +9. Accepting Warranty or Additional Liability.\par +While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.\par +END OF TERMS AND CONDITIONS\par +\par +APPENDIX: How to apply the Apache License to your work.\par +\par + To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.\par +Copyright 2016 Joe Wilm, The Alacritty Project Contributors\par +Licensed under the Apache License, Version 2.0 (the "License");you may not use this file except in compliance with the License.You may obtain a copy of the License at\par +\par +\tab {{\field{\*\fldinst{HYPERLINK http://www.apache.org/licenses/LICENSE-2.0 }}{\fldrslt{http://www.apache.org/licenses/LICENSE-2.0\ul0\cf0}}}}\f0\fs22\par +\par +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions andlimitations under the License.\par +} \ No newline at end of file diff --git a/icons/codicons/LICENSE b/icons/codicons/LICENSE new file mode 100644 index 0000000..a2c95fc --- /dev/null +++ b/icons/codicons/LICENSE @@ -0,0 +1,395 @@ +Attribution 4.0 International + +======================================================================= + +Creative Commons Corporation ("Creative Commons") is not a law firm and +does not provide legal services or legal advice. Distribution of +Creative Commons public licenses does not create a lawyer-client or +other relationship. Creative Commons makes its licenses and related +information available on an "as-is" basis. Creative Commons gives no +warranties regarding its licenses, any material licensed under their +terms and conditions, or any related information. Creative Commons +disclaims all liability for damages resulting from their use to the +fullest extent possible. + +Using Creative Commons Public Licenses + +Creative Commons public licenses provide a standard set of terms and +conditions that creators and other rights holders may use to share +original works of authorship and other material subject to copyright +and certain other rights specified in the public license below. The +following considerations are for informational purposes only, are not +exhaustive, and do not form part of our licenses. + + Considerations for licensors: Our public licenses are + intended for use by those authorized to give the public + permission to use material in ways otherwise restricted by + copyright and certain other rights. Our licenses are + irrevocable. Licensors should read and understand the terms + and conditions of the license they choose before applying it. + Licensors should also secure all rights necessary before + applying our licenses so that the public can reuse the + material as expected. Licensors should clearly mark any + material not subject to the license. This includes other CC- + licensed material, or material used under an exception or + limitation to copyright. More considerations for licensors: + wiki.creativecommons.org/Considerations_for_licensors + + Considerations for the public: By using one of our public + licenses, a licensor grants the public permission to use the + licensed material under specified terms and conditions. If + the licensor's permission is not necessary for any reason--for + example, because of any applicable exception or limitation to + copyright--then that use is not regulated by the license. Our + licenses grant only permissions under copyright and certain + other rights that a licensor has authority to grant. Use of + the licensed material may still be restricted for other + reasons, including because others have copyright or other + rights in the material. A licensor may make special requests, + such as asking that all changes be marked or described. + Although not required by our licenses, you are encouraged to + respect those requests where reasonable. More_considerations + for the public: + wiki.creativecommons.org/Considerations_for_licensees + +======================================================================= + +Creative Commons Attribution 4.0 International Public License + +By exercising the Licensed Rights (defined below), You accept and agree +to be bound by the terms and conditions of this Creative Commons +Attribution 4.0 International Public License ("Public License"). To the +extent this Public License may be interpreted as a contract, You are +granted the Licensed Rights in consideration of Your acceptance of +these terms and conditions, and the Licensor grants You such rights in +consideration of benefits the Licensor receives from making the +Licensed Material available under these terms and conditions. + + +Section 1 -- Definitions. + + a. Adapted Material means material subject to Copyright and Similar + Rights that is derived from or based upon the Licensed Material + and in which the Licensed Material is translated, altered, + arranged, transformed, or otherwise modified in a manner requiring + permission under the Copyright and Similar Rights held by the + Licensor. For purposes of this Public License, where the Licensed + Material is a musical work, performance, or sound recording, + Adapted Material is always produced where the Licensed Material is + synched in timed relation with a moving image. + + b. Adapter's License means the license You apply to Your Copyright + and Similar Rights in Your contributions to Adapted Material in + accordance with the terms and conditions of this Public License. + + c. Copyright and Similar Rights means copyright and/or similar rights + closely related to copyright including, without limitation, + performance, broadcast, sound recording, and Sui Generis Database + Rights, without regard to how the rights are labeled or + categorized. For purposes of this Public License, the rights + specified in Section 2(b)(1)-(2) are not Copyright and Similar + Rights. + + d. Effective Technological Measures means those measures that, in the + absence of proper authority, may not be circumvented under laws + fulfilling obligations under Article 11 of the WIPO Copyright + Treaty adopted on December 20, 1996, and/or similar international + agreements. + + e. Exceptions and Limitations means fair use, fair dealing, and/or + any other exception or limitation to Copyright and Similar Rights + that applies to Your use of the Licensed Material. + + f. Licensed Material means the artistic or literary work, database, + or other material to which the Licensor applied this Public + License. + + g. Licensed Rights means the rights granted to You subject to the + terms and conditions of this Public License, which are limited to + all Copyright and Similar Rights that apply to Your use of the + Licensed Material and that the Licensor has authority to license. + + h. Licensor means the individual(s) or entity(ies) granting rights + under this Public License. + + i. Share means to provide material to the public by any means or + process that requires permission under the Licensed Rights, such + as reproduction, public display, public performance, distribution, + dissemination, communication, or importation, and to make material + available to the public including in ways that members of the + public may access the material from a place and at a time + individually chosen by them. + + j. Sui Generis Database Rights means rights other than copyright + resulting from Directive 96/9/EC of the European Parliament and of + the Council of 11 March 1996 on the legal protection of databases, + as amended and/or succeeded, as well as other essentially + equivalent rights anywhere in the world. + + k. You means the individual or entity exercising the Licensed Rights + under this Public License. Your has a corresponding meaning. + + +Section 2 -- Scope. + + a. License grant. + + 1. Subject to the terms and conditions of this Public License, + the Licensor hereby grants You a worldwide, royalty-free, + non-sublicensable, non-exclusive, irrevocable license to + exercise the Licensed Rights in the Licensed Material to: + + a. reproduce and Share the Licensed Material, in whole or + in part; and + + b. produce, reproduce, and Share Adapted Material. + + 2. Exceptions and Limitations. For the avoidance of doubt, where + Exceptions and Limitations apply to Your use, this Public + License does not apply, and You do not need to comply with + its terms and conditions. + + 3. Term. The term of this Public License is specified in Section + 6(a). + + 4. Media and formats; technical modifications allowed. The + Licensor authorizes You to exercise the Licensed Rights in + all media and formats whether now known or hereafter created, + and to make technical modifications necessary to do so. The + Licensor waives and/or agrees not to assert any right or + authority to forbid You from making technical modifications + necessary to exercise the Licensed Rights, including + technical modifications necessary to circumvent Effective + Technological Measures. For purposes of this Public License, + simply making modifications authorized by this Section 2(a) + (4) never produces Adapted Material. + + 5. Downstream recipients. + + a. Offer from the Licensor -- Licensed Material. Every + recipient of the Licensed Material automatically + receives an offer from the Licensor to exercise the + Licensed Rights under the terms and conditions of this + Public License. + + b. No downstream restrictions. You may not offer or impose + any additional or different terms or conditions on, or + apply any Effective Technological Measures to, the + Licensed Material if doing so restricts exercise of the + Licensed Rights by any recipient of the Licensed + Material. + + 6. No endorsement. Nothing in this Public License constitutes or + may be construed as permission to assert or imply that You + are, or that Your use of the Licensed Material is, connected + with, or sponsored, endorsed, or granted official status by, + the Licensor or others designated to receive attribution as + provided in Section 3(a)(1)(A)(i). + + b. Other rights. + + 1. Moral rights, such as the right of integrity, are not + licensed under this Public License, nor are publicity, + privacy, and/or other similar personality rights; however, to + the extent possible, the Licensor waives and/or agrees not to + assert any such rights held by the Licensor to the limited + extent necessary to allow You to exercise the Licensed + Rights, but not otherwise. + + 2. Patent and trademark rights are not licensed under this + Public License. + + 3. To the extent possible, the Licensor waives any right to + collect royalties from You for the exercise of the Licensed + Rights, whether directly or through a collecting society + under any voluntary or waivable statutory or compulsory + licensing scheme. In all other cases the Licensor expressly + reserves any right to collect such royalties. + + +Section 3 -- License Conditions. + +Your exercise of the Licensed Rights is expressly made subject to the +following conditions. + + a. Attribution. + + 1. If You Share the Licensed Material (including in modified + form), You must: + + a. retain the following if it is supplied by the Licensor + with the Licensed Material: + + i. identification of the creator(s) of the Licensed + Material and any others designated to receive + attribution, in any reasonable manner requested by + the Licensor (including by pseudonym if + designated); + + ii. a copyright notice; + + iii. a notice that refers to this Public License; + + iv. a notice that refers to the disclaimer of + warranties; + + v. a URI or hyperlink to the Licensed Material to the + extent reasonably practicable; + + b. indicate if You modified the Licensed Material and + retain an indication of any previous modifications; and + + c. indicate the Licensed Material is licensed under this + Public License, and include the text of, or the URI or + hyperlink to, this Public License. + + 2. You may satisfy the conditions in Section 3(a)(1) in any + reasonable manner based on the medium, means, and context in + which You Share the Licensed Material. For example, it may be + reasonable to satisfy the conditions by providing a URI or + hyperlink to a resource that includes the required + information. + + 3. If requested by the Licensor, You must remove any of the + information required by Section 3(a)(1)(A) to the extent + reasonably practicable. + + 4. If You Share Adapted Material You produce, the Adapter's + License You apply must not prevent recipients of the Adapted + Material from complying with this Public License. + + +Section 4 -- Sui Generis Database Rights. + +Where the Licensed Rights include Sui Generis Database Rights that +apply to Your use of the Licensed Material: + + a. for the avoidance of doubt, Section 2(a)(1) grants You the right + to extract, reuse, reproduce, and Share all or a substantial + portion of the contents of the database; + + b. if You include all or a substantial portion of the database + contents in a database in which You have Sui Generis Database + Rights, then the database in which You have Sui Generis Database + Rights (but not its individual contents) is Adapted Material; and + + c. You must comply with the conditions in Section 3(a) if You Share + all or a substantial portion of the contents of the database. + +For the avoidance of doubt, this Section 4 supplements and does not +replace Your obligations under this Public License where the Licensed +Rights include other Copyright and Similar Rights. + + +Section 5 -- Disclaimer of Warranties and Limitation of Liability. + + a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE + EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS + AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF + ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, + IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, + WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR + PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, + ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT + KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT + ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. + + b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE + TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, + NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, + INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, + COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR + USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN + ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR + DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR + IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. + + c. The disclaimer of warranties and limitation of liability provided + above shall be interpreted in a manner that, to the extent + possible, most closely approximates an absolute disclaimer and + waiver of all liability. + + +Section 6 -- Term and Termination. + + a. This Public License applies for the term of the Copyright and + Similar Rights licensed here. However, if You fail to comply with + this Public License, then Your rights under this Public License + terminate automatically. + + b. Where Your right to use the Licensed Material has terminated under + Section 6(a), it reinstates: + + 1. automatically as of the date the violation is cured, provided + it is cured within 30 days of Your discovery of the + violation; or + + 2. upon express reinstatement by the Licensor. + + For the avoidance of doubt, this Section 6(b) does not affect any + right the Licensor may have to seek remedies for Your violations + of this Public License. + + c. For the avoidance of doubt, the Licensor may also offer the + Licensed Material under separate terms or conditions or stop + distributing the Licensed Material at any time; however, doing so + will not terminate this Public License. + + d. Sections 1, 5, 6, 7, and 8 survive termination of this Public + License. + + +Section 7 -- Other Terms and Conditions. + + a. The Licensor shall not be bound by any additional or different + terms or conditions communicated by You unless expressly agreed. + + b. Any arrangements, understandings, or agreements regarding the + Licensed Material not stated herein are separate from and + independent of the terms and conditions of this Public License. + + +Section 8 -- Interpretation. + + a. For the avoidance of doubt, this Public License does not, and + shall not be interpreted to, reduce, limit, restrict, or impose + conditions on any use of the Licensed Material that could lawfully + be made without permission under this Public License. + + b. To the extent possible, if any provision of this Public License is + deemed unenforceable, it shall be automatically reformed to the + minimum extent necessary to make it enforceable. If the provision + cannot be reformed, it shall be severed from this Public License + without affecting the enforceability of the remaining terms and + conditions. + + c. No term or condition of this Public License will be waived and no + failure to comply consented to unless expressly agreed to by the + Licensor. + + d. Nothing in this Public License constitutes or may be interpreted + as a limitation upon, or waiver of, any privileges and immunities + that apply to the Licensor or You, including from the legal + processes of any jurisdiction or authority. + + +======================================================================= + +Creative Commons is not a party to its public +licenses. Notwithstanding, Creative Commons may elect to apply one of +its public licenses to material it publishes and in those instances +will be considered the “Licensor.” The text of the Creative Commons +public licenses is dedicated to the public domain under the CC0 Public +Domain Dedication. Except for the limited purpose of indicating that +material is shared under a Creative Commons public license or as +otherwise permitted by the Creative Commons policies published at +creativecommons.org/policies, Creative Commons does not authorize the +use of the trademark "Creative Commons" or any other trademark or logo +of Creative Commons without its prior written consent including, +without limitation, in connection with any unauthorized modifications +to any of its public licenses or any other arrangements, +understandings, or agreements concerning use of licensed material. For +the avoidance of doubt, this paragraph does not form part of the +public licenses. + +Creative Commons may be contacted at creativecommons.org. \ No newline at end of file diff --git a/icons/codicons/README.md b/icons/codicons/README.md new file mode 100644 index 0000000..0b0eca8 --- /dev/null +++ b/icons/codicons/README.md @@ -0,0 +1,3 @@ +# vscode-codicons + +Source: [https://github.com/microsoft/vscode-codicons](https://github.com/microsoft/vscode-codicons) diff --git a/icons/codicons/add.svg b/icons/codicons/add.svg new file mode 100644 index 0000000..aed9bc6 --- /dev/null +++ b/icons/codicons/add.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/arrow-both.svg b/icons/codicons/arrow-both.svg new file mode 100644 index 0000000..c0fa479 --- /dev/null +++ b/icons/codicons/arrow-both.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/arrow-down.svg b/icons/codicons/arrow-down.svg new file mode 100644 index 0000000..1c9268b --- /dev/null +++ b/icons/codicons/arrow-down.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/arrow-left.svg b/icons/codicons/arrow-left.svg new file mode 100644 index 0000000..6e65875 --- /dev/null +++ b/icons/codicons/arrow-left.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/arrow-right.svg b/icons/codicons/arrow-right.svg new file mode 100644 index 0000000..f61ddd1 --- /dev/null +++ b/icons/codicons/arrow-right.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/arrow-swap.svg b/icons/codicons/arrow-swap.svg new file mode 100644 index 0000000..6496412 --- /dev/null +++ b/icons/codicons/arrow-swap.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/arrow-up.svg b/icons/codicons/arrow-up.svg new file mode 100644 index 0000000..81a0220 --- /dev/null +++ b/icons/codicons/arrow-up.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/blank.svg b/icons/codicons/blank.svg new file mode 100644 index 0000000..7aacd32 --- /dev/null +++ b/icons/codicons/blank.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/case-sensitive.svg b/icons/codicons/case-sensitive.svg new file mode 100644 index 0000000..9faa9b7 --- /dev/null +++ b/icons/codicons/case-sensitive.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/chevron-down.svg b/icons/codicons/chevron-down.svg new file mode 100644 index 0000000..59a4873 --- /dev/null +++ b/icons/codicons/chevron-down.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/chevron-left.svg b/icons/codicons/chevron-left.svg new file mode 100644 index 0000000..7095e36 --- /dev/null +++ b/icons/codicons/chevron-left.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/chevron-right.svg b/icons/codicons/chevron-right.svg new file mode 100644 index 0000000..507b2b2 --- /dev/null +++ b/icons/codicons/chevron-right.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/chevron-up.svg b/icons/codicons/chevron-up.svg new file mode 100644 index 0000000..f103cfd --- /dev/null +++ b/icons/codicons/chevron-up.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/chrome-close.svg b/icons/codicons/chrome-close.svg new file mode 100644 index 0000000..6b9a55a --- /dev/null +++ b/icons/codicons/chrome-close.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/chrome-maximize.svg b/icons/codicons/chrome-maximize.svg new file mode 100644 index 0000000..7627e7f --- /dev/null +++ b/icons/codicons/chrome-maximize.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/chrome-minimize.svg b/icons/codicons/chrome-minimize.svg new file mode 100644 index 0000000..abcc15b --- /dev/null +++ b/icons/codicons/chrome-minimize.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/chrome-restore.svg b/icons/codicons/chrome-restore.svg new file mode 100644 index 0000000..1537b4a --- /dev/null +++ b/icons/codicons/chrome-restore.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/circle-filled.svg b/icons/codicons/circle-filled.svg new file mode 100644 index 0000000..955f153 --- /dev/null +++ b/icons/codicons/circle-filled.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/close.svg b/icons/codicons/close.svg new file mode 100644 index 0000000..a63b1e0 --- /dev/null +++ b/icons/codicons/close.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/combine.svg b/icons/codicons/combine.svg new file mode 100644 index 0000000..02a09a7 --- /dev/null +++ b/icons/codicons/combine.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/debug-alt-small.svg b/icons/codicons/debug-alt-small.svg new file mode 100644 index 0000000..49020ce --- /dev/null +++ b/icons/codicons/debug-alt-small.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/debug-alt.svg b/icons/codicons/debug-alt.svg new file mode 100644 index 0000000..f1b4fdc --- /dev/null +++ b/icons/codicons/debug-alt.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/debug-console.svg b/icons/codicons/debug-console.svg new file mode 100644 index 0000000..bcdc388 --- /dev/null +++ b/icons/codicons/debug-console.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/debug-continue.svg b/icons/codicons/debug-continue.svg new file mode 100644 index 0000000..a6d8eb7 --- /dev/null +++ b/icons/codicons/debug-continue.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/debug-disconnect.svg b/icons/codicons/debug-disconnect.svg new file mode 100644 index 0000000..bb050c2 --- /dev/null +++ b/icons/codicons/debug-disconnect.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/debug-pause.svg b/icons/codicons/debug-pause.svg new file mode 100644 index 0000000..099a2cc --- /dev/null +++ b/icons/codicons/debug-pause.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/debug-restart.svg b/icons/codicons/debug-restart.svg new file mode 100644 index 0000000..9eedf67 --- /dev/null +++ b/icons/codicons/debug-restart.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/debug-start.svg b/icons/codicons/debug-start.svg new file mode 100644 index 0000000..1f9177a --- /dev/null +++ b/icons/codicons/debug-start.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/debug-step-into.svg b/icons/codicons/debug-step-into.svg new file mode 100644 index 0000000..1a684d7 --- /dev/null +++ b/icons/codicons/debug-step-into.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/debug-step-out.svg b/icons/codicons/debug-step-out.svg new file mode 100644 index 0000000..253190b --- /dev/null +++ b/icons/codicons/debug-step-out.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/debug-step-over.svg b/icons/codicons/debug-step-over.svg new file mode 100644 index 0000000..e725655 --- /dev/null +++ b/icons/codicons/debug-step-over.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/debug-stop.svg b/icons/codicons/debug-stop.svg new file mode 100644 index 0000000..f2f47bd --- /dev/null +++ b/icons/codicons/debug-stop.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/debug.svg b/icons/codicons/debug.svg new file mode 100644 index 0000000..2072b40 --- /dev/null +++ b/icons/codicons/debug.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/diff-added.svg b/icons/codicons/diff-added.svg new file mode 100644 index 0000000..9eca31e --- /dev/null +++ b/icons/codicons/diff-added.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/diff-ignored.svg b/icons/codicons/diff-ignored.svg new file mode 100644 index 0000000..bbbedec --- /dev/null +++ b/icons/codicons/diff-ignored.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/diff-modified.svg b/icons/codicons/diff-modified.svg new file mode 100644 index 0000000..cae06cb --- /dev/null +++ b/icons/codicons/diff-modified.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/diff-removed.svg b/icons/codicons/diff-removed.svg new file mode 100644 index 0000000..2da06cd --- /dev/null +++ b/icons/codicons/diff-removed.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/diff-renamed.svg b/icons/codicons/diff-renamed.svg new file mode 100644 index 0000000..f865506 --- /dev/null +++ b/icons/codicons/diff-renamed.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/diff.svg b/icons/codicons/diff.svg new file mode 100644 index 0000000..4af4ba7 --- /dev/null +++ b/icons/codicons/diff.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/discard.svg b/icons/codicons/discard.svg new file mode 100644 index 0000000..a22942f --- /dev/null +++ b/icons/codicons/discard.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/edit.svg b/icons/codicons/edit.svg new file mode 100644 index 0000000..552fbf9 --- /dev/null +++ b/icons/codicons/edit.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/error.svg b/icons/codicons/error.svg new file mode 100644 index 0000000..4cfb5bb --- /dev/null +++ b/icons/codicons/error.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/extensions.svg b/icons/codicons/extensions.svg new file mode 100644 index 0000000..bbdf50f --- /dev/null +++ b/icons/codicons/extensions.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/file-binary.svg b/icons/codicons/file-binary.svg new file mode 100644 index 0000000..18bafda --- /dev/null +++ b/icons/codicons/file-binary.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/file-code.svg b/icons/codicons/file-code.svg new file mode 100644 index 0000000..f6d50e4 --- /dev/null +++ b/icons/codicons/file-code.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/file-media.svg b/icons/codicons/file-media.svg new file mode 100644 index 0000000..0667efb --- /dev/null +++ b/icons/codicons/file-media.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/file-pdf.svg b/icons/codicons/file-pdf.svg new file mode 100644 index 0000000..b1dc9d5 --- /dev/null +++ b/icons/codicons/file-pdf.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/file-submodule.svg b/icons/codicons/file-submodule.svg new file mode 100644 index 0000000..08261df --- /dev/null +++ b/icons/codicons/file-submodule.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/file-symlink-directory.svg b/icons/codicons/file-symlink-directory.svg new file mode 100644 index 0000000..77aac76 --- /dev/null +++ b/icons/codicons/file-symlink-directory.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/file-symlink-file.svg b/icons/codicons/file-symlink-file.svg new file mode 100644 index 0000000..5cb31f5 --- /dev/null +++ b/icons/codicons/file-symlink-file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/file-zip.svg b/icons/codicons/file-zip.svg new file mode 100644 index 0000000..95da245 --- /dev/null +++ b/icons/codicons/file-zip.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/file.svg b/icons/codicons/file.svg new file mode 100644 index 0000000..18ce72c --- /dev/null +++ b/icons/codicons/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/files.svg b/icons/codicons/files.svg new file mode 100644 index 0000000..8671a7b --- /dev/null +++ b/icons/codicons/files.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/fold-down.svg b/icons/codicons/fold-down.svg new file mode 100644 index 0000000..f34b8c4 --- /dev/null +++ b/icons/codicons/fold-down.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/fold-up.svg b/icons/codicons/fold-up.svg new file mode 100644 index 0000000..11f72c2 --- /dev/null +++ b/icons/codicons/fold-up.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/fold.svg b/icons/codicons/fold.svg new file mode 100644 index 0000000..fd41cad --- /dev/null +++ b/icons/codicons/fold.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/folder-active.svg b/icons/codicons/folder-active.svg new file mode 100644 index 0000000..d50cabf --- /dev/null +++ b/icons/codicons/folder-active.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/folder-library.svg b/icons/codicons/folder-library.svg new file mode 100644 index 0000000..b5a25ac --- /dev/null +++ b/icons/codicons/folder-library.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/folder-opened.svg b/icons/codicons/folder-opened.svg new file mode 100644 index 0000000..52eb52c --- /dev/null +++ b/icons/codicons/folder-opened.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/folder.svg b/icons/codicons/folder.svg new file mode 100644 index 0000000..2940f74 --- /dev/null +++ b/icons/codicons/folder.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/gear.svg b/icons/codicons/gear.svg new file mode 100644 index 0000000..8ee3ec4 --- /dev/null +++ b/icons/codicons/gear.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/git-commit.svg b/icons/codicons/git-commit.svg new file mode 100644 index 0000000..9c11c79 --- /dev/null +++ b/icons/codicons/git-commit.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/git-compare.svg b/icons/codicons/git-compare.svg new file mode 100644 index 0000000..e08789c --- /dev/null +++ b/icons/codicons/git-compare.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/git-merge.svg b/icons/codicons/git-merge.svg new file mode 100644 index 0000000..92ea231 --- /dev/null +++ b/icons/codicons/git-merge.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/go-to-file.svg b/icons/codicons/go-to-file.svg new file mode 100644 index 0000000..4c688f2 --- /dev/null +++ b/icons/codicons/go-to-file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/group-by-ref-type.svg b/icons/codicons/group-by-ref-type.svg new file mode 100644 index 0000000..4c688a0 --- /dev/null +++ b/icons/codicons/group-by-ref-type.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/history.svg b/icons/codicons/history.svg new file mode 100644 index 0000000..53d41f7 --- /dev/null +++ b/icons/codicons/history.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/info.svg b/icons/codicons/info.svg new file mode 100644 index 0000000..fff5a9f --- /dev/null +++ b/icons/codicons/info.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/inspect.svg b/icons/codicons/inspect.svg new file mode 100644 index 0000000..2de8bfb --- /dev/null +++ b/icons/codicons/inspect.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/issues.svg b/icons/codicons/issues.svg new file mode 100644 index 0000000..666a3ba --- /dev/null +++ b/icons/codicons/issues.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/json.svg b/icons/codicons/json.svg new file mode 100644 index 0000000..ff10d4b --- /dev/null +++ b/icons/codicons/json.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/key.svg b/icons/codicons/key.svg new file mode 100644 index 0000000..a8de847 --- /dev/null +++ b/icons/codicons/key.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/keyboard.svg b/icons/codicons/keyboard.svg new file mode 100644 index 0000000..0757621 --- /dev/null +++ b/icons/codicons/keyboard.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/layout-activitybar-left.svg b/icons/codicons/layout-activitybar-left.svg new file mode 100644 index 0000000..87378a2 --- /dev/null +++ b/icons/codicons/layout-activitybar-left.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/layout-activitybar-right.svg b/icons/codicons/layout-activitybar-right.svg new file mode 100644 index 0000000..43a8940 --- /dev/null +++ b/icons/codicons/layout-activitybar-right.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/layout-centered.svg b/icons/codicons/layout-centered.svg new file mode 100644 index 0000000..9865f13 --- /dev/null +++ b/icons/codicons/layout-centered.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/layout-menubar.svg b/icons/codicons/layout-menubar.svg new file mode 100644 index 0000000..98666ab --- /dev/null +++ b/icons/codicons/layout-menubar.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/layout-panel-center.svg b/icons/codicons/layout-panel-center.svg new file mode 100644 index 0000000..d720c12 --- /dev/null +++ b/icons/codicons/layout-panel-center.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/layout-panel-justify.svg b/icons/codicons/layout-panel-justify.svg new file mode 100644 index 0000000..67a749c --- /dev/null +++ b/icons/codicons/layout-panel-justify.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/layout-panel-left.svg b/icons/codicons/layout-panel-left.svg new file mode 100644 index 0000000..181dd75 --- /dev/null +++ b/icons/codicons/layout-panel-left.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/layout-panel-off.svg b/icons/codicons/layout-panel-off.svg new file mode 100644 index 0000000..fc8464f --- /dev/null +++ b/icons/codicons/layout-panel-off.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/layout-panel-right.svg b/icons/codicons/layout-panel-right.svg new file mode 100644 index 0000000..7cf4177 --- /dev/null +++ b/icons/codicons/layout-panel-right.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/layout-panel.svg b/icons/codicons/layout-panel.svg new file mode 100644 index 0000000..7ac17fb --- /dev/null +++ b/icons/codicons/layout-panel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/layout-sidebar-left-off.svg b/icons/codicons/layout-sidebar-left-off.svg new file mode 100644 index 0000000..50019bf --- /dev/null +++ b/icons/codicons/layout-sidebar-left-off.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/layout-sidebar-left.svg b/icons/codicons/layout-sidebar-left.svg new file mode 100644 index 0000000..b513bda --- /dev/null +++ b/icons/codicons/layout-sidebar-left.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/layout-sidebar-right-off.svg b/icons/codicons/layout-sidebar-right-off.svg new file mode 100644 index 0000000..951b957 --- /dev/null +++ b/icons/codicons/layout-sidebar-right-off.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/layout-sidebar-right.svg b/icons/codicons/layout-sidebar-right.svg new file mode 100644 index 0000000..1017deb --- /dev/null +++ b/icons/codicons/layout-sidebar-right.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/layout-statusbar.svg b/icons/codicons/layout-statusbar.svg new file mode 100644 index 0000000..7f60e5b --- /dev/null +++ b/icons/codicons/layout-statusbar.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/layout.svg b/icons/codicons/layout.svg new file mode 100644 index 0000000..655d365 --- /dev/null +++ b/icons/codicons/layout.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/lightbulb-autofix.svg b/icons/codicons/lightbulb-autofix.svg new file mode 100644 index 0000000..aca11c8 --- /dev/null +++ b/icons/codicons/lightbulb-autofix.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/lightbulb.svg b/icons/codicons/lightbulb.svg new file mode 100644 index 0000000..37df948 --- /dev/null +++ b/icons/codicons/lightbulb.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/link-external.svg b/icons/codicons/link-external.svg new file mode 100644 index 0000000..9a594b7 --- /dev/null +++ b/icons/codicons/link-external.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/link.svg b/icons/codicons/link.svg new file mode 100644 index 0000000..46bf4d5 --- /dev/null +++ b/icons/codicons/link.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/menu.svg b/icons/codicons/menu.svg new file mode 100644 index 0000000..4bb38d0 --- /dev/null +++ b/icons/codicons/menu.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/merge.svg b/icons/codicons/merge.svg new file mode 100644 index 0000000..0e3049f --- /dev/null +++ b/icons/codicons/merge.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/multiple-windows.svg b/icons/codicons/multiple-windows.svg new file mode 100644 index 0000000..7ee7455 --- /dev/null +++ b/icons/codicons/multiple-windows.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/new-file.svg b/icons/codicons/new-file.svg new file mode 100644 index 0000000..caac07c --- /dev/null +++ b/icons/codicons/new-file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/new-folder.svg b/icons/codicons/new-folder.svg new file mode 100644 index 0000000..3ada63a --- /dev/null +++ b/icons/codicons/new-folder.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/open-preview.svg b/icons/codicons/open-preview.svg new file mode 100644 index 0000000..765c739 --- /dev/null +++ b/icons/codicons/open-preview.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/plugin-icon.svg b/icons/codicons/plugin-icon.svg new file mode 100644 index 0000000..93badcc --- /dev/null +++ b/icons/codicons/plugin-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/references.svg b/icons/codicons/references.svg new file mode 100644 index 0000000..280b3d8 --- /dev/null +++ b/icons/codicons/references.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/refresh.svg b/icons/codicons/refresh.svg new file mode 100644 index 0000000..9196015 --- /dev/null +++ b/icons/codicons/refresh.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/regex.svg b/icons/codicons/regex.svg new file mode 100644 index 0000000..1f45856 --- /dev/null +++ b/icons/codicons/regex.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/remote-explorer.svg b/icons/codicons/remote-explorer.svg new file mode 100644 index 0000000..0152f47 --- /dev/null +++ b/icons/codicons/remote-explorer.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/remote.svg b/icons/codicons/remote.svg new file mode 100644 index 0000000..736f9f4 --- /dev/null +++ b/icons/codicons/remote.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/remove.svg b/icons/codicons/remove.svg new file mode 100644 index 0000000..cf31dfe --- /dev/null +++ b/icons/codicons/remove.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/replace-all.svg b/icons/codicons/replace-all.svg new file mode 100644 index 0000000..f5a5153 --- /dev/null +++ b/icons/codicons/replace-all.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/replace.svg b/icons/codicons/replace.svg new file mode 100644 index 0000000..65bfaac --- /dev/null +++ b/icons/codicons/replace.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/repo-clone.svg b/icons/codicons/repo-clone.svg new file mode 100644 index 0000000..ab8f19a --- /dev/null +++ b/icons/codicons/repo-clone.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/repo-force-push.svg b/icons/codicons/repo-force-push.svg new file mode 100644 index 0000000..37c9a3d --- /dev/null +++ b/icons/codicons/repo-force-push.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/repo-forked.svg b/icons/codicons/repo-forked.svg new file mode 100644 index 0000000..0cc8093 --- /dev/null +++ b/icons/codicons/repo-forked.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/repo-pull.svg b/icons/codicons/repo-pull.svg new file mode 100644 index 0000000..8210959 --- /dev/null +++ b/icons/codicons/repo-pull.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/repo-push.svg b/icons/codicons/repo-push.svg new file mode 100644 index 0000000..8faf451 --- /dev/null +++ b/icons/codicons/repo-push.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/repo.svg b/icons/codicons/repo.svg new file mode 100644 index 0000000..72fd4ac --- /dev/null +++ b/icons/codicons/repo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/root-folder-opened.svg b/icons/codicons/root-folder-opened.svg new file mode 100644 index 0000000..b31bb8f --- /dev/null +++ b/icons/codicons/root-folder-opened.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/root-folder.svg b/icons/codicons/root-folder.svg new file mode 100644 index 0000000..93839d0 --- /dev/null +++ b/icons/codicons/root-folder.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/run-errors.svg b/icons/codicons/run-errors.svg new file mode 100644 index 0000000..37391ed --- /dev/null +++ b/icons/codicons/run-errors.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/screen-full.svg b/icons/codicons/screen-full.svg new file mode 100644 index 0000000..59d6403 --- /dev/null +++ b/icons/codicons/screen-full.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/screen-normal.svg b/icons/codicons/screen-normal.svg new file mode 100644 index 0000000..e64985e --- /dev/null +++ b/icons/codicons/screen-normal.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/search-stop.svg b/icons/codicons/search-stop.svg new file mode 100644 index 0000000..7a80b70 --- /dev/null +++ b/icons/codicons/search-stop.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/search.svg b/icons/codicons/search.svg new file mode 100644 index 0000000..13c4df0 --- /dev/null +++ b/icons/codicons/search.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/settings-gear.svg b/icons/codicons/settings-gear.svg new file mode 100644 index 0000000..0abb193 --- /dev/null +++ b/icons/codicons/settings-gear.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/settings.svg b/icons/codicons/settings.svg new file mode 100644 index 0000000..67127ea --- /dev/null +++ b/icons/codicons/settings.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/source-control.svg b/icons/codicons/source-control.svg new file mode 100644 index 0000000..edb96f8 --- /dev/null +++ b/icons/codicons/source-control.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/split-horizontal.svg b/icons/codicons/split-horizontal.svg new file mode 100644 index 0000000..d744a79 --- /dev/null +++ b/icons/codicons/split-horizontal.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/split-vertical.svg b/icons/codicons/split-vertical.svg new file mode 100644 index 0000000..3a18ab9 --- /dev/null +++ b/icons/codicons/split-vertical.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/symbol-array.svg b/icons/codicons/symbol-array.svg new file mode 100644 index 0000000..6bae1fb --- /dev/null +++ b/icons/codicons/symbol-array.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/symbol-boolean.svg b/icons/codicons/symbol-boolean.svg new file mode 100644 index 0000000..5941b6b --- /dev/null +++ b/icons/codicons/symbol-boolean.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/symbol-class.svg b/icons/codicons/symbol-class.svg new file mode 100644 index 0000000..d837c94 --- /dev/null +++ b/icons/codicons/symbol-class.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/symbol-color.svg b/icons/codicons/symbol-color.svg new file mode 100644 index 0000000..d94e57d --- /dev/null +++ b/icons/codicons/symbol-color.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/symbol-constant.svg b/icons/codicons/symbol-constant.svg new file mode 100644 index 0000000..ff60164 --- /dev/null +++ b/icons/codicons/symbol-constant.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/symbol-enum-member.svg b/icons/codicons/symbol-enum-member.svg new file mode 100644 index 0000000..1236b7b --- /dev/null +++ b/icons/codicons/symbol-enum-member.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/symbol-enum.svg b/icons/codicons/symbol-enum.svg new file mode 100644 index 0000000..c7839cc --- /dev/null +++ b/icons/codicons/symbol-enum.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/symbol-event.svg b/icons/codicons/symbol-event.svg new file mode 100644 index 0000000..ea695e8 --- /dev/null +++ b/icons/codicons/symbol-event.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/symbol-field.svg b/icons/codicons/symbol-field.svg new file mode 100644 index 0000000..a344cf0 --- /dev/null +++ b/icons/codicons/symbol-field.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/symbol-file.svg b/icons/codicons/symbol-file.svg new file mode 100644 index 0000000..f5db8ad --- /dev/null +++ b/icons/codicons/symbol-file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/symbol-interface.svg b/icons/codicons/symbol-interface.svg new file mode 100644 index 0000000..cef462e --- /dev/null +++ b/icons/codicons/symbol-interface.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/symbol-key.svg b/icons/codicons/symbol-key.svg new file mode 100644 index 0000000..c96351a --- /dev/null +++ b/icons/codicons/symbol-key.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/symbol-keyword.svg b/icons/codicons/symbol-keyword.svg new file mode 100644 index 0000000..02f6c70 --- /dev/null +++ b/icons/codicons/symbol-keyword.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/symbol-method.svg b/icons/codicons/symbol-method.svg new file mode 100644 index 0000000..1506657 --- /dev/null +++ b/icons/codicons/symbol-method.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/symbol-misc.svg b/icons/codicons/symbol-misc.svg new file mode 100644 index 0000000..330d87c --- /dev/null +++ b/icons/codicons/symbol-misc.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/symbol-namespace.svg b/icons/codicons/symbol-namespace.svg new file mode 100644 index 0000000..af267ca --- /dev/null +++ b/icons/codicons/symbol-namespace.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/symbol-numeric.svg b/icons/codicons/symbol-numeric.svg new file mode 100644 index 0000000..f654887 --- /dev/null +++ b/icons/codicons/symbol-numeric.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/symbol-operator.svg b/icons/codicons/symbol-operator.svg new file mode 100644 index 0000000..6567ca0 --- /dev/null +++ b/icons/codicons/symbol-operator.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/symbol-parameter.svg b/icons/codicons/symbol-parameter.svg new file mode 100644 index 0000000..5167ffc --- /dev/null +++ b/icons/codicons/symbol-parameter.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/symbol-property.svg b/icons/codicons/symbol-property.svg new file mode 100644 index 0000000..e5222f9 --- /dev/null +++ b/icons/codicons/symbol-property.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/symbol-ruler.svg b/icons/codicons/symbol-ruler.svg new file mode 100644 index 0000000..3d9b7c8 --- /dev/null +++ b/icons/codicons/symbol-ruler.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/symbol-snippet.svg b/icons/codicons/symbol-snippet.svg new file mode 100644 index 0000000..ddf3ea6 --- /dev/null +++ b/icons/codicons/symbol-snippet.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/symbol-string.svg b/icons/codicons/symbol-string.svg new file mode 100644 index 0000000..9c041d2 --- /dev/null +++ b/icons/codicons/symbol-string.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/symbol-structure.svg b/icons/codicons/symbol-structure.svg new file mode 100644 index 0000000..23e897e --- /dev/null +++ b/icons/codicons/symbol-structure.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/symbol-variable.svg b/icons/codicons/symbol-variable.svg new file mode 100644 index 0000000..9dd5073 --- /dev/null +++ b/icons/codicons/symbol-variable.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/terminal.svg b/icons/codicons/terminal.svg new file mode 100644 index 0000000..2ffc08e --- /dev/null +++ b/icons/codicons/terminal.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/type-hierarchy.svg b/icons/codicons/type-hierarchy.svg new file mode 100644 index 0000000..bcbc902 --- /dev/null +++ b/icons/codicons/type-hierarchy.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/unsaved.svg b/icons/codicons/unsaved.svg new file mode 100644 index 0000000..3357526 --- /dev/null +++ b/icons/codicons/unsaved.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/icons/codicons/warning.svg b/icons/codicons/warning.svg new file mode 100644 index 0000000..9a33f87 --- /dev/null +++ b/icons/codicons/warning.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/codicons/whole-word.svg b/icons/codicons/whole-word.svg new file mode 100644 index 0000000..d415932 --- /dev/null +++ b/icons/codicons/whole-word.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/lapce/lapce_logo.svg b/icons/lapce/lapce_logo.svg new file mode 100644 index 0000000..6d3fe1a --- /dev/null +++ b/icons/lapce/lapce_logo.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/icons/lapce/lapce_remote.svg b/icons/lapce/lapce_remote.svg new file mode 100644 index 0000000..6bf4828 --- /dev/null +++ b/icons/lapce/lapce_remote.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lapce-app/Cargo.toml b/lapce-app/Cargo.toml new file mode 100644 index 0000000..c0f780d --- /dev/null +++ b/lapce-app/Cargo.toml @@ -0,0 +1,85 @@ +[package] +name = "lapce-app" +license = { workspace = true } +version = { workspace = true } +authors = { workspace = true } +edition = { workspace = true } +rust-version = { workspace = true } + +[dependencies] +alacritty_terminal = { workspace = true } +anyhow = { workspace = true } +backtrace = { workspace = true } +chrono = { workspace = true } +clap = { workspace = true } +crossbeam-channel = { workspace = true } +flate2 = { workspace = true } +globset = { workspace = true } +im = { workspace = true } +include_dir = { workspace = true } +indexmap = { workspace = true } +interprocess = { workspace = true } +itertools = { workspace = true } +lapce-core = { workspace = true } +lapce-proxy = { workspace = true } +lapce-rpc = { workspace = true } +lapce-xi-rope = { workspace = true } +lsp-types = { workspace = true } +notify = { workspace = true } +once_cell = { workspace = true } +parking_lot = { workspace = true } +rayon = { workspace = true } +reqwest = { workspace = true } +smallvec = { workspace = true } +strum = { workspace = true } +strum_macros = { workspace = true } +semver = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +regex = { workspace = true } +tar = { workspace = true } +tempfile = { workspace = true } +thiserror = { workspace = true } +toml = { workspace = true } +toml_edit = { workspace = true } +tracing = { workspace = true } +tracing-log = { workspace = true } +tracing-subscriber = { workspace = true } +tracing-appender = { workspace = true } +url = { workspace = true } +zstd = { workspace = true } +floem = { workspace = true } + +pulldown-cmark = { version = "0.11.0" } +Inflector = { version = "0.11.4" } +open = { version = "5.1.4" } +unicode-width = { version = "0.1.13" } +nucleo = { version = "0.5.0" } +bytemuck = { version = "1.22.0" } +config = { version = "=0.13.4", default-features = false, features = ["toml"] } +structdesc = { git = "https://github.com/lapce/structdesc", rev = "bb56969f22fdb2c2d6c03f158fd4a2bdc983b659" } +base64 = { version = "0.21.7" } +sha2 = { version = "0.10.8" } +zip = { version = "0.6.6", default-features = false, features = ["deflate"] } +percent-encoding = { version = "2.3.1" } + +[target.'cfg(target_os="macos")'.dependencies] +fs_extra = "1.2.0" +dmg = "0.1.1" + +[target.'cfg(windows)'.dependencies.windows-sys] +workspace = true +features = ["Win32_Foundation", "Win32_System_Threading", "Win32_UI_WindowsAndMessaging"] + +[features] +default = ["updater", "vendored-fonts"] +portable = ["lapce-core/portable"] +updater = [] +vendored-fonts = [] + +[dev-dependencies] +criterion = "0.5" + +[[bench]] +name = "visual_line" +harness = false diff --git a/lapce-app/benches/visual_line.rs b/lapce-app/benches/visual_line.rs new file mode 100644 index 0000000..e78d2b7 --- /dev/null +++ b/lapce-app/benches/visual_line.rs @@ -0,0 +1,278 @@ +use std::{cell::RefCell, collections::HashMap, rc::Rc, sync::Arc}; + +use criterion::{Criterion, black_box, criterion_group, criterion_main}; +use floem::{ + reactive::Scope, + text::{Attrs, AttrsList, FamilyOwned, TextLayout, Wrap}, + views::editor::{ + layout::TextLayoutLine, + phantom_text::PhantomTextLine, + visual_line::{ + ConfigId, FontSizeCacheId, LineFontSizeProvider, Lines, ResolvedWrap, + TextLayoutProvider, VLine, + }, + }, +}; +use lapce_core::{ + buffer::rope_text::{RopeText, RopeTextRef}, + cursor::CursorAffinity, +}; +use lapce_xi_rope::Rope; + +const FONT_SIZE: usize = 12; + +// TODO: use the editor data view structures! +struct TLProv<'a> { + text: &'a Rope, + phantom: HashMap, + font_family: Vec, + has_multiline_phantom: bool, +} +impl TextLayoutProvider for TLProv<'_> { + fn text(&self) -> Rope { + self.text.clone() + } + + // An implementation relatively close to the actual new text layout impl but simplified. + // TODO(minor): It would be nice to just use the same impl as view's + fn new_text_layout( + &self, + line: usize, + font_size: usize, + wrap: ResolvedWrap, + ) -> Arc { + let rope_text = RopeTextRef::new(self.text); + let line_content_original = rope_text.line_content(line); + + // Get the line content with newline characters replaced with spaces + // and the content without the newline characters + let (line_content, _line_content_original) = + if let Some(s) = line_content_original.strip_suffix("\r\n") { + ( + format!("{s} "), + &line_content_original[..line_content_original.len() - 2], + ) + } else if let Some(s) = line_content_original.strip_suffix('\n') { + ( + format!("{s} ",), + &line_content_original[..line_content_original.len() - 1], + ) + } else { + ( + line_content_original.to_string(), + &line_content_original[..], + ) + }; + + let phantom_text = self.phantom.get(&line).cloned().unwrap_or_default(); + let line_content = phantom_text.combine_with_text(&line_content); + + let attrs = Attrs::new() + .family(&self.font_family) + .font_size(font_size as f32); + let mut attrs_list = AttrsList::new(attrs.clone()); + + // We don't do line styles, since they aren't relevant + + // Apply phantom text specific styling + for (offset, size, col, phantom) in phantom_text.offset_size_iter() { + let start = col + offset; + let end = start + size; + + let mut attrs = attrs.clone(); + if let Some(fg) = phantom.fg { + attrs = attrs.color(fg); + } + if let Some(phantom_font_size) = phantom.font_size { + attrs = attrs.font_size(phantom_font_size.min(font_size) as f32); + } + attrs_list.add_span(start..end, attrs); + // if let Some(font_family) = phantom.font_family.clone() { + // layout_builder = layout_builder.range_attribute( + // start..end, + // TextAttribute::FontFamily(font_family), + // ); + // } + } + + let mut text_layout = TextLayout::new(); + text_layout.set_wrap(Wrap::Word); + match wrap { + // We do not have to set the wrap mode if we do not set the width + ResolvedWrap::None => {} + ResolvedWrap::Column(_col) => todo!(), + ResolvedWrap::Width(px) => { + text_layout.set_size(px, f32::MAX); + } + } + text_layout.set_text(&line_content, attrs_list, None); + + // skip phantom text background styling because it doesn't shift positions + // skip severity styling + // skip diagnostic background styling + + Arc::new(TextLayoutLine { + extra_style: Vec::new(), + text: text_layout, + whitespaces: None, + indent: 0.0, + phantom_text: PhantomTextLine::default(), + }) + } + + fn before_phantom_col(&self, line: usize, col: usize) -> usize { + if let Some(phantom) = self.phantom.get(&line) { + phantom.before_col(col) + } else { + col + } + } + + fn has_multiline_phantom(&self) -> bool { + self.has_multiline_phantom + } +} +struct TestFontSize; +impl LineFontSizeProvider for TestFontSize { + fn font_size(&self, _line: usize) -> usize { + FONT_SIZE + } + + fn cache_id(&self) -> FontSizeCacheId { + 0 + } +} + +fn make_lines(text: &Rope, wrap: ResolvedWrap, init: bool) -> (TLProv<'_>, Lines) { + make_lines_ph(text, wrap, init, HashMap::new(), false) +} + +fn make_lines_ph( + text: &Rope, + wrap: ResolvedWrap, + init: bool, + ph: HashMap, + has_multiline_phantom: bool, +) -> (TLProv<'_>, Lines) { + // let wrap = Wrap::Word; + // let r_wrap = ResolvedWrap::Width(width); + let font_sizes = TestFontSize; + let text = TLProv { + text, + phantom: ph, + font_family: Vec::new(), + has_multiline_phantom, + }; + let cx = Scope::new(); + let lines = Lines::new(cx, RefCell::new(Rc::new(font_sizes))); + lines.set_wrap(wrap); + + if init { + lines.init_all(0, ConfigId::new(0, 0), &text, true); + } + + (text, lines) +} + +fn medium_rope() -> Rope { + let mut text = String::new(); + + // TODO: use some actual file's content. + for i in 0..3000 { + let content = if i % 2 == 0 { + "This is a roughly typical line of text\n" + } else if i % 3 == 0 { + "\n" + } else { + "A short line\n" + }; + + text.push_str(content); + } + + Rope::from(&text) +} + +fn visual_line(c: &mut Criterion) { + let text = medium_rope(); + + // Should be very fast because it is trivially linear and there's no multiline phantom + c.bench_function("last vline (uninit)", |b| { + let (text_prov, lines) = make_lines(&text, ResolvedWrap::None, false); + b.iter(|| { + lines.clear_last_vline(); + + let last_vline = lines.last_vline(&text_prov); + black_box(last_vline); + }) + }); + + // Unrealistic since the user will very rarely have all of the lines initialized + // Should still be fast because there's no wrapping or multiline phantom text + c.bench_function("last vline (all, no wrapping)", |b| { + let (text_prov, lines) = make_lines(&text, ResolvedWrap::None, true); + b.iter(|| { + lines.clear_last_vline(); + + let last_vline = lines.last_vline(&text_prov); + let _val = black_box(last_vline); + }) + }); + + // TODO: we could precompute line count on the text layout? + // Unrealistic since the user will very rarely have all of the lines initialized + // Still decently fast, though. <1ms + c.bench_function("last vline (all, wrapping)", |b| { + let width = 100.0; + let (text_prov, lines) = make_lines(&text, ResolvedWrap::Width(width), true); + + b.iter(|| { + // This should clear any other caching mechanisms that get added + lines.clear_last_vline(); + let last_vline = lines.last_vline(&text_prov); + let _val = black_box(last_vline); + }) + }); + + // Q: This seems like 1/5th the cost of last vline despite only being half the lines.. + c.bench_function("vline of offset (all, wrapping)", |b| { + let width = 100.0; + let (text_prov, lines) = make_lines(&text, ResolvedWrap::Width(width), true); + + // Not past the middle. If we were past the middle then it'd be just benching last vline + // calculation, which is admittedly relatively similar to this. + let offset = 1450; + + b.iter(|| { + // This should clear any other caching mechanisms that get added + lines.clear_last_vline(); + + let vline = + lines.vline_of_offset(&text_prov, offset, CursorAffinity::Backward); + let _val = black_box(vline); + }) + }); + + c.bench_function("offset of vline (all, wrapping)", |b| { + let width = 100.0; + let (text_prov, lines) = make_lines(&text, ResolvedWrap::Width(width), true); + + let vline = VLine(3300); + + b.iter(|| { + // This should clear any other caching mechanisms that get added + lines.clear_last_vline(); + + let offset = lines.offset_of_vline(&text_prov, vline); + let _val = black_box(offset); + }) + }); + + // TODO: when we have the reverse search of vline for offset, we should have a separate instance where the last vline isn't cached, which would give us a range of 'worst case' (never reused, have to recopmute it everytime) and 'best case' (always reused) + + // TODO: bench common operations, like a single line changing or the (equivalent of) cache rev + // updating. +} + +criterion_group!(benches, visual_line); +criterion_main!(benches); diff --git a/lapce-app/src/about.rs b/lapce-app/src/about.rs new file mode 100644 index 0000000..75f9281 --- /dev/null +++ b/lapce-app/src/about.rs @@ -0,0 +1,213 @@ +use std::rc::Rc; + +use floem::{ + View, + event::EventListener, + keyboard::Modifiers, + reactive::{RwSignal, Scope, SignalGet, SignalUpdate}, + style::{CursorStyle, Display, Position}, + views::{Decorators, container, label, stack, svg}, +}; +use lapce_core::{command::FocusCommand, meta::VERSION, mode::Mode}; + +use crate::{ + command::{CommandExecuted, CommandKind}, + config::color::LapceColor, + keypress::KeyPressFocus, + web_link::web_link, + window_tab::{Focus, WindowTabData}, +}; + +struct AboutUri {} + +impl AboutUri { + const LAPCE: &'static str = "https://lapce.dev"; + const GITHUB: &'static str = "https://github.com/lapce/lapce"; + const MATRIX: &'static str = "https://matrix.to/#/#lapce-editor:matrix.org"; + const DISCORD: &'static str = "https://discord.gg/n8tGJ6Rn6D"; + const CODICONS: &'static str = "https://github.com/microsoft/vscode-codicons"; +} + +#[derive(Clone, Debug)] +pub struct AboutData { + pub visible: RwSignal, + pub focus: RwSignal, +} + +impl AboutData { + pub fn new(cx: Scope, focus: RwSignal) -> Self { + let visible = cx.create_rw_signal(false); + + Self { visible, focus } + } + + pub fn open(&self) { + self.visible.set(true); + self.focus.set(Focus::AboutPopup); + } + + pub fn close(&self) { + self.visible.set(false); + self.focus.set(Focus::Workbench); + } +} + +impl KeyPressFocus for AboutData { + fn get_mode(&self) -> Mode { + Mode::Insert + } + + fn check_condition( + &self, + _condition: crate::keypress::condition::Condition, + ) -> bool { + self.visible.get_untracked() + } + + fn run_command( + &self, + command: &crate::command::LapceCommand, + _count: Option, + _mods: Modifiers, + ) -> crate::command::CommandExecuted { + match &command.kind { + CommandKind::Workbench(_) => {} + CommandKind::Edit(_) => {} + CommandKind::Move(_) => {} + CommandKind::Scroll(_) => {} + CommandKind::Focus(cmd) => { + if cmd == &FocusCommand::ModalClose { + self.close(); + } + } + CommandKind::MotionMode(_) => {} + CommandKind::MultiSelection(_) => {} + } + CommandExecuted::Yes + } + + fn receive_char(&self, _c: &str) {} + + fn focus_only(&self) -> bool { + true + } +} + +pub fn about_popup(window_tab_data: Rc) -> impl View { + let about_data = window_tab_data.about_data.clone(); + let config = window_tab_data.common.config; + let internal_command = window_tab_data.common.internal_command; + let logo_size = 100.0; + + exclusive_popup(window_tab_data, about_data.visible, move || { + stack(( + svg(move || (config.get()).logo_svg()).style(move |s| { + s.size(logo_size, logo_size) + .color(config.get().color(LapceColor::EDITOR_FOREGROUND)) + }), + label(|| "Lapce".to_string()).style(move |s| { + s.font_bold() + .margin_top(10.0) + .color(config.get().color(LapceColor::EDITOR_FOREGROUND)) + }), + label(|| format!("Version: {}", VERSION)).style(move |s| { + s.margin_top(10.0) + .color(config.get().color(LapceColor::EDITOR_DIM)) + }), + web_link( + || "Website".to_string(), + || AboutUri::LAPCE.to_string(), + move || config.get().color(LapceColor::EDITOR_LINK), + internal_command, + ) + .style(|s| s.margin_top(20.0)), + web_link( + || "GitHub".to_string(), + || AboutUri::GITHUB.to_string(), + move || config.get().color(LapceColor::EDITOR_LINK), + internal_command, + ) + .style(|s| s.margin_top(10.0)), + web_link( + || "Discord".to_string(), + || AboutUri::DISCORD.to_string(), + move || config.get().color(LapceColor::EDITOR_LINK), + internal_command, + ) + .style(|s| s.margin_top(10.0)), + web_link( + || "Matrix".to_string(), + || AboutUri::MATRIX.to_string(), + move || config.get().color(LapceColor::EDITOR_LINK), + internal_command, + ) + .style(|s| s.margin_top(10.0)), + label(|| "Attributions".to_string()).style(move |s| { + s.font_bold() + .color(config.get().color(LapceColor::EDITOR_DIM)) + .margin_top(40.0) + }), + web_link( + || "Codicons (CC-BY-4.0)".to_string(), + || AboutUri::CODICONS.to_string(), + move || config.get().color(LapceColor::EDITOR_LINK), + internal_command, + ) + .style(|s| s.margin_top(10.0)), + )) + .style(|s| s.flex_col().items_center()) + }) + .debug_name("About Popup") +} + +fn exclusive_popup( + window_tab_data: Rc, + visibility: RwSignal, + content: impl FnOnce() -> V, +) -> impl View { + let config = window_tab_data.common.config; + + container( + container( + container(content()) + .style(move |s| { + let config = config.get(); + s.padding_vert(25.0) + .padding_horiz(100.0) + .border(1.0) + .border_radius(6.0) + .border_color(config.color(LapceColor::LAPCE_BORDER)) + .background(config.color(LapceColor::PANEL_BACKGROUND)) + }) + .on_event_stop(EventListener::PointerDown, move |_| {}), + ) + .style(move |s| { + s.flex_grow(1.0) + .flex_row() + .items_center() + .hover(move |s| s.cursor(CursorStyle::Default)) + }), + ) + .on_event_stop(EventListener::PointerDown, move |_| { + window_tab_data.about_data.close(); + }) + // Prevent things behind the grayed out area from being hovered. + .on_event_stop(EventListener::PointerMove, move |_| {}) + .style(move |s| { + s.display(if visibility.get() { + Display::Flex + } else { + Display::None + }) + .position(Position::Absolute) + .size_pct(100.0, 100.0) + .flex_col() + .items_center() + .background( + config + .get() + .color(LapceColor::LAPCE_DROPDOWN_SHADOW) + .multiply_alpha(0.5), + ) + }) +} diff --git a/lapce-app/src/alert.rs b/lapce-app/src/alert.rs new file mode 100644 index 0000000..48f6bcb --- /dev/null +++ b/lapce-app/src/alert.rs @@ -0,0 +1,174 @@ +use std::{ + fmt, + rc::Rc, + sync::{Arc, atomic::AtomicU64}, +}; + +use floem::{ + View, + event::EventListener, + reactive::{ReadSignal, RwSignal, Scope, SignalGet, SignalUpdate}, + style::CursorStyle, + views::{Decorators, container, dyn_stack, label, stack, svg}, +}; + +use crate::{ + config::{LapceConfig, color::LapceColor, icon::LapceIcons}, + window_tab::CommonData, +}; + +#[derive(Clone)] +pub struct AlertButton { + pub text: String, + pub action: Rc, +} + +impl fmt::Debug for AlertButton { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut s = f.debug_struct("AlertButton"); + s.field("text", &self.text); + s.finish() + } +} + +#[derive(Clone)] +pub struct AlertBoxData { + pub active: RwSignal, + pub title: RwSignal, + pub msg: RwSignal, + pub buttons: RwSignal>, + pub config: ReadSignal>, +} + +impl AlertBoxData { + pub fn new(cx: Scope, common: Rc) -> Self { + Self { + active: cx.create_rw_signal(false), + title: cx.create_rw_signal("".to_string()), + msg: cx.create_rw_signal("".to_string()), + buttons: cx.create_rw_signal(Vec::new()), + config: common.config, + } + } +} + +pub fn alert_box(alert_data: AlertBoxData) -> impl View { + let config = alert_data.config; + let active = alert_data.active; + let title = alert_data.title; + let msg = alert_data.msg; + let buttons = alert_data.buttons; + let button_id = AtomicU64::new(0); + + container({ + container({ + stack(( + svg(move || config.get().ui_svg(LapceIcons::WARNING)).style( + move |s| { + s.size(50.0, 50.0) + .color(config.get().color(LapceColor::LAPCE_WARN)) + }, + ), + label(move || title.get()).style(move |s| { + s.margin_top(20.0) + .width_pct(100.0) + .font_bold() + .font_size((config.get().ui.font_size() + 1) as f32) + }), + label(move || msg.get()) + .style(move |s| s.width_pct(100.0).margin_top(10.0)), + dyn_stack( + move || buttons.get(), + move |_button| { + button_id.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + }, + move |button| { + label(move || button.text.clone()) + .on_click_stop(move |_| { + (button.action)(); + }) + .style(move |s| { + let config = config.get(); + s.margin_top(10.0) + .width_pct(100.0) + .justify_center() + .font_size((config.ui.font_size() + 1) as f32) + .line_height(1.6) + .border(1.0) + .border_radius(6.0) + .border_color( + config.color(LapceColor::LAPCE_BORDER), + ) + .hover(|s| { + s.cursor(CursorStyle::Pointer).background( + config.color( + LapceColor::PANEL_HOVERED_BACKGROUND, + ), + ) + }) + .active(|s| { + s.background(config.color( + LapceColor::PANEL_HOVERED_ACTIVE_BACKGROUND, + )) + }) + }) + }, + ) + .style(|s| s.flex_col().width_pct(100.0).margin_top(10.0)), + label(|| "Cancel".to_string()) + .on_click_stop(move |_| { + active.set(false); + }) + .style(move |s| { + let config = config.get(); + s.margin_top(20.0) + .width_pct(100.0) + .justify_center() + .font_size((config.ui.font_size() + 1) as f32) + .line_height(1.5) + .border(1.0) + .border_radius(6.0) + .border_color(config.color(LapceColor::LAPCE_BORDER)) + .hover(|s| { + s.cursor(CursorStyle::Pointer).background( + config + .color(LapceColor::PANEL_HOVERED_BACKGROUND), + ) + }) + .active(|s| { + s.background(config.color( + LapceColor::PANEL_HOVERED_ACTIVE_BACKGROUND, + )) + }) + }), + )) + .style(|s| s.flex_col().items_center().width_pct(100.0)) + }) + .on_event_stop(EventListener::PointerDown, |_| {}) + .style(move |s| { + let config = config.get(); + s.padding(20.0) + .width(250.0) + .border(1.0) + .border_radius(6.0) + .border_color(config.color(LapceColor::LAPCE_BORDER)) + .color(config.color(LapceColor::EDITOR_FOREGROUND)) + .background(config.color(LapceColor::PANEL_BACKGROUND)) + }) + }) + .on_event_stop(EventListener::PointerDown, move |_| {}) + .style(move |s| { + s.absolute() + .size_pct(100.0, 100.0) + .items_center() + .justify_center() + .apply_if(!active.get(), |s| s.hide()) + .background( + config + .get() + .color(LapceColor::LAPCE_DROPDOWN_SHADOW) + .multiply_alpha(0.5), + ) + }) + .debug_name("Alert Box") +} diff --git a/lapce-app/src/app.rs b/lapce-app/src/app.rs new file mode 100644 index 0000000..a1de1a5 --- /dev/null +++ b/lapce-app/src/app.rs @@ -0,0 +1,4321 @@ +#[cfg(target_os = "windows")] +use std::os::windows::process::CommandExt; +use std::{ + io::{BufReader, IsTerminal, Read, Write}, + ops::Range, + path::PathBuf, + process::Stdio, + rc::Rc, + sync::{ + Arc, + atomic::AtomicU64, + mpsc::{SyncSender, channel, sync_channel}, + }, +}; + +use anyhow::{Context, Result, anyhow}; +use clap::Parser; +use floem::{ + IntoView, View, + action::show_context_menu, + event::{Event, EventListener, EventPropagation}, + ext_event::{create_ext_action, create_signal_from_channel}, + menu::{Menu, MenuItem}, + peniko::{ + Color, + kurbo::{Point, Rect, Size}, + }, + prelude::SignalTrack, + reactive::{ + ReadSignal, RwSignal, Scope, SignalGet, SignalUpdate, SignalWith, + create_effect, create_memo, create_rw_signal, provide_context, use_context, + }, + style::{ + AlignItems, CursorStyle, Display, FlexDirection, JustifyContent, Position, + Style, + }, + taffy::{ + Line, + style_helpers::{self, auto, fr}, + }, + text::{Style as FontStyle, Weight}, + unit::PxPctAuto, + views::{ + Decorators, VirtualVector, clip, container, drag_resize_window_area, + drag_window_area, dyn_stack, + editor::{core::register::Clipboard, text::SystemClipboard}, + empty, label, rich_text, + scroll::{PropagatePointerWheel, VerticalScrollAsHorizontal, scroll}, + stack, svg, tab, text, tooltip, virtual_stack, + }, + window::{ResizeDirection, WindowConfig, WindowId}, +}; +use lapce_core::{ + command::{EditCommand, FocusCommand}, + directory::Directory, + meta, + syntax::{Syntax, highlight::reset_highlight_configs}, +}; +use lapce_rpc::{ + RpcMessage, + core::{CoreMessage, CoreNotification}, + file::PathObject, +}; +use lsp_types::{CompletionItemKind, MessageType, ShowMessageParams}; +use notify::Watcher; +use serde::{Deserialize, Serialize}; +use tracing_subscriber::{filter::Targets, reload::Handle}; + +use crate::{ + about, alert, + code_action::CodeActionStatus, + command::{ + CommandKind, InternalCommand, LapceCommand, LapceWorkbenchCommand, + WindowCommand, + }, + config::{ + LapceConfig, color::LapceColor, icon::LapceIcons, ui::TabSeparatorHeight, + watcher::ConfigWatcher, + }, + db::LapceDb, + debug::RunDebugMode, + editor::{ + diff::diff_show_more_section_view, + location::{EditorLocation, EditorPosition}, + view::editor_container_view, + }, + editor_tab::{EditorTabChild, EditorTabData}, + focus_text::focus_text, + id::{EditorTabId, SplitId}, + keymap::keymap_view, + keypress::keymap::KeyMap, + listener::Listener, + main_split::{ + SplitContent, SplitData, SplitDirection, SplitMoveDirection, TabCloseKind, + }, + markdown::MarkdownContent, + palette::{ + PaletteStatus, + item::{PaletteItem, PaletteItemContent}, + }, + panel::{position::PanelContainerPosition, view::panel_container_view}, + plugin::{PluginData, plugin_info_view}, + settings::{settings_view, theme_color_settings_view}, + status::status, + text_input::TextInputBuilder, + title::{title, window_controls_view}, + tracing::*, + update::ReleaseInfo, + window::{TabsInfo, WindowData, WindowInfo}, + window_tab::{Focus, WindowTabData}, + workspace::{LapceWorkspace, LapceWorkspaceType}, +}; + +mod grammars; +mod logging; + +#[derive(Parser)] +#[clap(name = "Lapce")] +#[clap(version=meta::VERSION)] +#[derive(Debug)] +struct Cli { + /// Launch new window even if Lapce is already running + #[clap(short, long, action)] + new: bool, + /// Don't return instantly when opened in a terminal + #[clap(short, long, action)] + wait: bool, + + /// Path(s) to plugins to load. + /// + /// This is primarily used for plugin development to make it easier to test changes to the + /// plugin without needing to copy the plugin to the plugins directory. + /// This will cause any plugin with the same author & name to not run. + #[clap(long, action)] + plugin_path: Vec, + + /// Paths to file(s) and/or folder(s) to open. + /// When path is a file (that exists or not), + /// it accepts `path:line:column` syntax + /// to specify line and column at which it should open the file + #[clap(value_parser = lapce_proxy::cli::parse_file_line_column)] + #[clap(value_hint = clap::ValueHint::AnyPath)] + paths: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AppInfo { + pub windows: Vec, +} + +#[derive(Clone)] +pub enum AppCommand { + SaveApp, + NewWindow { folder: Option }, + CloseWindow(WindowId), + WindowGotFocus(WindowId), + WindowClosed(WindowId), +} + +#[derive(Clone)] +pub struct AppData { + pub windows: RwSignal>, + pub active_window: RwSignal, + pub window_scale: RwSignal, + pub app_command: Listener, + pub app_terminated: RwSignal, + /// The latest release information + pub latest_release: RwSignal>>, + pub watcher: Arc, + pub tracing_handle: Handle, + pub config: RwSignal>, + /// Paths to extra plugins to load + pub plugin_paths: Arc>, +} + +impl AppData { + pub fn reload_config(&self) { + let config = + LapceConfig::load(&LapceWorkspace::default(), &[], &self.plugin_paths); + + self.config.set(Arc::new(config)); + self.window_scale.set(self.config.get().ui.scale()); + + let windows = self.windows.get_untracked(); + for (_, window) in windows { + window.reload_config(); + } + } + + pub fn active_window_tab(&self) -> Option> { + if let Some(window) = self.active_window() { + return window.active_window_tab(); + } + None + } + + fn active_window(&self) -> Option { + let windows = self.windows.get_untracked(); + let active_window = self.active_window.get_untracked(); + windows + .get(&active_window) + .cloned() + .or_else(|| windows.iter().next().map(|(_, window)| window.clone())) + } + + fn default_window_config(&self) -> WindowConfig { + WindowConfig::default() + .apply_default_theme(false) + .title("Lapce") + } + + pub fn new_window(&self, folder: Option) { + let config = self + .active_window() + .map(|window| { + self.default_window_config() + .size(window.common.size.get_untracked()) + .position(window.position.get_untracked() + (50.0, 50.0)) + }) + .or_else(|| { + let db: Arc = use_context().unwrap(); + db.get_window().ok().map(|info| { + self.default_window_config() + .size(info.size) + .position(info.pos) + }) + }) + .unwrap_or_else(|| { + self.default_window_config().size(Size::new(800.0, 600.0)) + }); + let config = if cfg!(target_os = "macos") + || self.config.get_untracked().core.custom_titlebar + { + config.show_titlebar(false) + } else { + config + }; + let workspace = LapceWorkspace { + path: folder, + ..Default::default() + }; + let app_data = self.clone(); + floem::new_window( + move |window_id| { + app_data.app_view( + window_id, + WindowInfo { + size: Size::ZERO, + pos: Point::ZERO, + maximised: false, + tabs: TabsInfo { + active_tab: 0, + workspaces: vec![workspace], + }, + }, + vec![], + ) + }, + Some(config), + ); + } + + pub fn run_app_command(&self, cmd: AppCommand) { + match cmd { + AppCommand::SaveApp => { + let db: Arc = use_context().unwrap(); + if let Err(err) = db.save_app(self) { + tracing::error!("{:?}", err); + } + } + AppCommand::WindowClosed(window_id) => { + if self.app_terminated.get_untracked() { + return; + } + let db: Arc = use_context().unwrap(); + if self.windows.with_untracked(|w| w.len()) == 1 { + if let Err(err) = db.insert_app(self.clone()) { + tracing::error!("{:?}", err); + } + } + let window_data = self + .windows + .try_update(|windows| windows.remove(&window_id)) + .unwrap(); + if let Some(window_data) = window_data { + window_data.scope.dispose(); + } + if let Err(err) = db.save_app(self) { + tracing::error!("{:?}", err); + } + } + AppCommand::CloseWindow(window_id) => { + floem::close_window(window_id); + } + AppCommand::NewWindow { folder } => { + self.new_window(folder); + } + AppCommand::WindowGotFocus(window_id) => { + self.active_window.set(window_id); + } + } + } + + fn create_windows( + &self, + db: Arc, + paths: Vec, + ) -> floem::Application { + let mut app = floem::Application::new(); + + let mut initial_windows = 0; + + // Split user input into known existing directors and + // file paths that exist or not + let (dirs, files): (Vec<&PathObject>, Vec<&PathObject>) = + paths.iter().partition(|p| p.is_dir); + + let files: Vec = files.into_iter().cloned().collect(); + let mut files = if files.is_empty() { None } else { Some(files) }; + + if !dirs.is_empty() { + // There were directories specified, so we'll load those as windows + + // Use the last opened window's size and position as the default + let (size, mut pos) = db + .get_window() + .map(|i| (i.size, i.pos)) + .unwrap_or_else(|_| (Size::new(800.0, 600.0), Point::new(0.0, 0.0))); + + for dir in dirs { + #[cfg(windows)] + let workspace_type = if !std::env::var("WSL_DISTRO_NAME") + .unwrap_or_default() + .is_empty() + || !std::env::var("WSL_INTEROP").unwrap_or_default().is_empty() + { + LapceWorkspaceType::RemoteWSL(crate::workspace::WslHost { + host: String::new(), + }) + } else { + LapceWorkspaceType::Local + }; + #[cfg(not(windows))] + let workspace_type = LapceWorkspaceType::Local; + + let info = WindowInfo { + size, + pos, + maximised: false, + tabs: TabsInfo { + active_tab: 0, + workspaces: vec![LapceWorkspace { + kind: workspace_type, + path: Some(dir.path.to_owned()), + last_open: 0, + }], + }, + }; + + pos += (50.0, 50.0); + + let config = self + .default_window_config() + .size(info.size) + .position(info.pos); + let config = if cfg!(target_os = "macos") + || self.config.get_untracked().core.custom_titlebar + { + config.show_titlebar(false) + } else { + config + }; + let app_data = self.clone(); + let files = files.take().unwrap_or_default(); + app = app.window( + move |window_id| app_data.app_view(window_id, info, files), + Some(config), + ); + initial_windows += 1; + } + } else if files.is_none() { + // There were no dirs and no files specified, so we'll load the last windows + match db.get_app() { + Ok(app_info) => { + for info in app_info.windows { + let config = self + .default_window_config() + .size(info.size) + .position(info.pos); + let config = if cfg!(target_os = "macos") + || self.config.get_untracked().core.custom_titlebar + { + config.show_titlebar(false) + } else { + config + }; + let app_data = self.clone(); + app = app.window( + move |window_id| { + app_data.app_view(window_id, info, vec![]) + }, + Some(config), + ); + initial_windows += 1; + } + } + Err(err) => { + tracing::error!("{:?}", err); + } + } + } + + if initial_windows == 0 { + let mut info = db.get_window().unwrap_or_else(|_| WindowInfo { + size: Size::new(800.0, 600.0), + pos: Point::ZERO, + maximised: false, + tabs: TabsInfo { + active_tab: 0, + workspaces: vec![LapceWorkspace::default()], + }, + }); + info.tabs = TabsInfo { + active_tab: 0, + workspaces: vec![LapceWorkspace::default()], + }; + let config = self + .default_window_config() + .size(info.size) + .position(info.pos); + let config = if cfg!(target_os = "macos") + || self.config.get_untracked().core.custom_titlebar + { + config.show_titlebar(false) + } else { + config + }; + let app_data = self.clone(); + app = app.window( + move |window_id| { + app_data.app_view( + window_id, + info, + files.take().unwrap_or_default(), + ) + }, + Some(config), + ); + } + + app + } + + fn app_view( + &self, + window_id: WindowId, + info: WindowInfo, + files: Vec, + ) -> impl View + use<> { + let app_view_id = create_rw_signal(floem::ViewId::new()); + let window_data = WindowData::new( + window_id, + app_view_id, + info, + self.window_scale, + self.latest_release.read_only(), + self.plugin_paths.clone(), + self.app_command, + ); + + { + let cur_window_tab = window_data.active.get_untracked(); + let (_, window_tab) = + &window_data.window_tabs.get_untracked()[cur_window_tab]; + for file in files { + let position = file.linecol.map(|pos| { + EditorPosition::Position(lsp_types::Position { + line: pos.line.saturating_sub(1) as u32, + character: pos.column.saturating_sub(1) as u32, + }) + }); + + window_tab.run_internal_command(InternalCommand::GoToLocation { + location: EditorLocation { + path: file.path.clone(), + position, + scroll_offset: None, + // Create a new editor for the file, so we don't change any current unconfirmed + // editor + ignore_unconfirmed: true, + same_editor_tab: false, + }, + }); + } + } + + self.windows.update(|windows| { + windows.insert(window_id, window_data.clone()); + }); + let window_size = window_data.common.size; + let position = window_data.position; + let window_scale = window_data.window_scale; + let app_command = window_data.app_command; + let config = window_data.config; + // The KeyDown and PointerDown event handlers both need ownership of a WindowData object. + let key_down_window_data = window_data.clone(); + let view = stack(( + workspace_tab_header(window_data.clone()), + window(window_data.clone()), + stack(( + drag_resize_window_area(ResizeDirection::West, empty()).style(|s| { + s.absolute().width(4.0).height_full().pointer_events_auto() + }), + drag_resize_window_area(ResizeDirection::North, empty()).style( + |s| s.absolute().width_full().height(4.0).pointer_events_auto(), + ), + drag_resize_window_area(ResizeDirection::East, empty()).style( + move |s| { + s.absolute() + .margin_left(window_size.get().width as f32 - 4.0) + .width(4.0) + .height_full() + .pointer_events_auto() + }, + ), + drag_resize_window_area(ResizeDirection::South, empty()).style( + move |s| { + s.absolute() + .margin_top(window_size.get().height as f32 - 4.0) + .width_full() + .height(4.0) + .pointer_events_auto() + }, + ), + drag_resize_window_area(ResizeDirection::NorthWest, empty()).style( + |s| s.absolute().width(20.0).height(4.0).pointer_events_auto(), + ), + drag_resize_window_area(ResizeDirection::NorthWest, empty()).style( + |s| s.absolute().width(4.0).height(20.0).pointer_events_auto(), + ), + drag_resize_window_area(ResizeDirection::NorthEast, empty()).style( + move |s| { + s.absolute() + .margin_left(window_size.get().width as f32 - 20.0) + .width(20.0) + .height(4.0) + .pointer_events_auto() + }, + ), + drag_resize_window_area(ResizeDirection::NorthEast, empty()).style( + move |s| { + s.absolute() + .margin_left(window_size.get().width as f32 - 4.0) + .width(4.0) + .height(20.0) + .pointer_events_auto() + }, + ), + drag_resize_window_area(ResizeDirection::SouthWest, empty()).style( + move |s| { + s.absolute() + .margin_top(window_size.get().height as f32 - 4.0) + .width(20.0) + .height(4.0) + .pointer_events_auto() + }, + ), + drag_resize_window_area(ResizeDirection::SouthWest, empty()).style( + move |s| { + s.absolute() + .margin_top(window_size.get().height as f32 - 20.0) + .width(4.0) + .height(20.0) + .pointer_events_auto() + }, + ), + drag_resize_window_area(ResizeDirection::SouthEast, empty()).style( + move |s| { + s.absolute() + .margin_left(window_size.get().width as f32 - 20.0) + .margin_top(window_size.get().height as f32 - 4.0) + .width(20.0) + .height(4.0) + .pointer_events_auto() + }, + ), + drag_resize_window_area(ResizeDirection::SouthEast, empty()).style( + move |s| { + s.absolute() + .margin_left(window_size.get().width as f32 - 4.0) + .margin_top(window_size.get().height as f32 - 20.0) + .width(4.0) + .height(20.0) + .pointer_events_auto() + }, + ), + )) + .debug_name("Drag Resize Areas") + .style(move |s| { + s.absolute() + .size_full() + .apply_if( + cfg!(target_os = "macos") + || !config.get_untracked().core.custom_titlebar, + |s| s.hide(), + ) + .pointer_events_none() + }), + )) + .style(|s| s.flex_col().size_full()); + let view_id = view.id(); + app_view_id.set(view_id); + + view_id.request_focus(); + + view.window_scale(move || window_scale.get()) + .keyboard_navigable() + .on_event(EventListener::KeyDown, move |event| { + if let Event::KeyDown(key_event) = event { + if key_down_window_data.key_down(key_event) { + view_id.request_focus(); + } + EventPropagation::Stop + } else { + EventPropagation::Continue + } + }) + .on_event(EventListener::PointerDown, { + let window_data = window_data.clone(); + move |event| { + if let Event::PointerDown(pointer_event) = event { + window_data.key_down(pointer_event); + EventPropagation::Stop + } else { + EventPropagation::Continue + } + } + }) + .on_event_stop(EventListener::WindowResized, move |event| { + if let Event::WindowResized(size) = event { + window_size.set(*size); + } + }) + .on_event_stop(EventListener::WindowMoved, move |event| { + if let Event::WindowMoved(point) = event { + position.set(*point); + } + }) + .on_event_stop(EventListener::WindowGotFocus, move |_| { + app_command.send(AppCommand::WindowGotFocus(window_id)); + }) + .on_event_stop(EventListener::WindowClosed, move |_| { + app_command.send(AppCommand::WindowClosed(window_id)); + }) + .on_event_stop(EventListener::DroppedFile, move |event: &Event| { + if let Event::DroppedFile(file) = event { + if file.path.is_dir() { + app_command.send(AppCommand::NewWindow { + folder: Some(file.path.clone()), + }); + } else if let Some(win_tab_data) = + window_data.active_window_tab() + { + win_tab_data.common.internal_command.send( + InternalCommand::GoToLocation { + location: EditorLocation { + path: file.path.clone(), + position: None, + scroll_offset: None, + ignore_unconfirmed: false, + same_editor_tab: false, + }, + }, + ) + } + } + }) + .debug_name("App View") + } +} + +/// The top bar of an Editor tab. Includes the tab forward/back buttons, the tab scroll bar and the new split and tab close all button. +fn editor_tab_header( + window_tab_data: Rc, + active_editor_tab: ReadSignal>, + editor_tab: RwSignal, + dragging: RwSignal, EditorTabId)>>, +) -> impl View { + let main_split = window_tab_data.main_split.clone(); + let plugin = window_tab_data.plugin.clone(); + let editors = window_tab_data.main_split.editors; + let diff_editors = window_tab_data.main_split.diff_editors; + let focus = window_tab_data.common.focus; + let config = window_tab_data.common.config; + let internal_command = window_tab_data.common.internal_command; + let workbench_command = window_tab_data.common.workbench_command; + let editor_tab_id = + editor_tab.with_untracked(|editor_tab| editor_tab.editor_tab_id); + + let editor_tab_active = + create_memo(move |_| editor_tab.with(|editor_tab| editor_tab.active)); + let items = move || { + let editor_tab = editor_tab.get(); + for (i, (index, _, _)) in editor_tab.children.iter().enumerate() { + if index.get_untracked() != i { + index.set(i); + } + } + editor_tab.children + }; + let key = |(_, _, child): &(RwSignal, RwSignal, EditorTabChild)| { + child.id() + }; + let is_focused = move || { + if let Focus::Workbench = focus.get() { + editor_tab.with_untracked(|e| Some(e.editor_tab_id)) + == active_editor_tab.get() + } else { + false + } + }; + + let view_fn = move |(i, layout_rect, child): ( + RwSignal, + RwSignal, + EditorTabChild, + )| { + let local_child = child.clone(); + let child_for_close = child.clone(); + let child_for_mouse_close = child.clone(); + let child_for_mouse_close_2 = child.clone(); + let main_split = main_split.clone(); + let plugin = plugin.clone(); + let child_view = { + let info = child.view_info(editors, diff_editors, plugin, config); + let hovered = create_rw_signal(false); + + use crate::config::ui::TabCloseButton; + + let tab_icon = container({ + svg("") + .update_value(move || info.with(|info| info.icon.clone())) + .style(move |s| { + let config = config.get(); + let size = config.ui.icon_size() as f32; + s.size(size, size) + .apply_opt(info.with(|info| info.color), |s, c| { + s.color(c) + }) + .apply_if( + !info.with(|info| info.is_pristine) + && config.ui.tab_close_button + == TabCloseButton::Off, + |s| s.color(config.color(LapceColor::LAPCE_WARN)), + ) + }) + }) + .style(|s| s.padding(4.)); + + let tab_content = tooltip( + label(move || info.with(|info| info.name.clone())).style(move |s| { + s.apply_if( + !info + .with(|info| info.confirmed) + .map(|confirmed| confirmed.get()) + .unwrap_or(true), + |s| s.font_style(FontStyle::Italic), + ) + .selectable(false) + }), + move || { + tooltip_tip( + config, + text(info.with(|info| { + info.path + .clone() + .map(|path| path.display().to_string()) + .unwrap_or("local".to_string()) + })), + ) + }, + ); + + let tab_close_button = clickable_icon( + move || { + if hovered.get() || info.with(|info| info.is_pristine) { + LapceIcons::CLOSE + } else { + LapceIcons::UNSAVED + } + }, + move || { + let editor_tab_id = + editor_tab.with_untracked(|t| t.editor_tab_id); + internal_command.send(InternalCommand::EditorTabChildClose { + editor_tab_id, + child: child_for_close.clone(), + }); + }, + || false, + || false, + || "Close", + config, + ) + .on_event_stop(EventListener::PointerDown, |_| {}) + .on_event_stop(EventListener::PointerEnter, move |_| { + hovered.set(true); + }) + .on_event_stop(EventListener::PointerLeave, move |_| { + hovered.set(false); + }); + + stack(( + tab_icon.style(move |s| { + let tab_close_button = config.get().ui.tab_close_button; + s.apply_if(tab_close_button == TabCloseButton::Left, |s| { + s.grid_column(Line { + start: style_helpers::line(3), + end: style_helpers::span(1), + }) + }) + }), + tab_content.style(move |s| { + let tab_close_button = config.get().ui.tab_close_button; + s.apply_if(tab_close_button == TabCloseButton::Left, |s| { + s.grid_column(Line { + start: style_helpers::line(2), + end: style_helpers::span(1), + }) + }) + .apply_if(tab_close_button == TabCloseButton::Off, |s| { + s.padding_right(4.) + }) + }), + tab_close_button.style(move |s| { + let tab_close_button = config.get().ui.tab_close_button; + s.apply_if(tab_close_button == TabCloseButton::Left, |s| { + s.grid_column(Line { + start: style_helpers::line(1), + end: style_helpers::span(1), + }) + }) + .apply_if(tab_close_button == TabCloseButton::Off, |s| s.hide()) + }), + )) + .style(move |s| { + s.items_center() + .justify_center() + .border_left(if i.get() == 0 { 1.0 } else { 0.0 }) + .border_right(1.0) + .border_color(config.get().color(LapceColor::LAPCE_BORDER)) + .padding_horiz(6.) + .gap(6.) + .grid() + .grid_template_columns(vec![auto(), fr(1.), auto()]) + .apply_if( + config.get().ui.tab_separator_height + == TabSeparatorHeight::Full, + |s| s.height_full(), + ) + }) + }; + + let confirmed = match local_child { + EditorTabChild::Editor(editor_id) => { + editors.editor_untracked(editor_id).map(|e| e.confirmed) + } + EditorTabChild::DiffEditor(diff_editor_id) => diff_editors + .with_untracked(|diff_editors| { + diff_editors + .get(&diff_editor_id) + .map(|diff_editor_data| diff_editor_data.confirmed) + }), + _ => None, + }; + + let header_content_size = create_rw_signal(Size::ZERO); + let drag_over_left: RwSignal> = create_rw_signal(None); + stack(( + child_view + .on_double_click_stop(move |_| { + if let Some(confirmed) = confirmed { + confirmed.set(true); + } + }) + .on_event(EventListener::PointerDown, move |event| { + if let Event::PointerDown(pointer_event) = event { + if pointer_event.button.is_auxiliary() { + let editor_tab_id = + editor_tab.with_untracked(|t| t.editor_tab_id); + internal_command.send( + InternalCommand::EditorTabChildClose { + editor_tab_id, + child: child_for_mouse_close.clone(), + }, + ); + EventPropagation::Stop + } else { + editor_tab.update(|editor_tab| { + editor_tab.active = i.get_untracked(); + }); + EventPropagation::Continue + } + } else { + EventPropagation::Continue + } + }) + .on_secondary_click_stop(move |_| { + let editor_tab_id = + editor_tab.with_untracked(|t| t.editor_tab_id); + + tab_secondary_click( + internal_command, + editor_tab_id, + child_for_mouse_close_2.clone(), + ); + }) + .on_event_stop(EventListener::DragStart, move |_| { + dragging.set(Some((i, editor_tab_id))); + }) + .on_event_stop(EventListener::DragEnd, move |_| { + dragging.set(None); + }) + .on_resize(move |rect| { + header_content_size.set(rect.size()); + }) + .draggable() + .dragging_style(move |s| { + let config = config.get(); + s.border(1.0) + .border_radius(6.0) + .background( + config + .color(LapceColor::PANEL_BACKGROUND) + .multiply_alpha(0.7), + ) + .border_color(config.color(LapceColor::LAPCE_BORDER)) + }) + .style(|s| s.align_items(Some(AlignItems::Center)).flex_grow(1.0)), + empty() + .style(move |s| { + s.size_full() + .border_bottom(if editor_tab_active.get() == i.get() { + 2.0 + } else { + 0.0 + }) + .border_color(config.get().color(if is_focused() { + LapceColor::LAPCE_TAB_ACTIVE_UNDERLINE + } else { + LapceColor::LAPCE_TAB_INACTIVE_UNDERLINE + })) + }) + .style(|s| { + s.absolute() + .padding_horiz(3.0) + .size_full() + .pointer_events_none() + }) + .debug_name("Drop Indicator"), + empty() + .style(move |s| { + let i = i.get(); + let drag_over_left = drag_over_left.get(); + s.absolute() + .margin_left(if i == 0 { 0.0 } else { -2.0 }) + .height_full() + .width( + header_content_size.get().width as f32 + + if i == 0 { 1.0 } else { 3.0 }, + ) + .apply_if(drag_over_left.is_none(), |s| s.hide()) + .apply_if(drag_over_left.is_some(), |s| { + if let Some(drag_over_left) = drag_over_left { + if drag_over_left { + s.border_left(3.0) + } else { + s.border_right(3.0) + } + } else { + s + } + }) + .border_color( + config + .get() + .color(LapceColor::LAPCE_TAB_ACTIVE_UNDERLINE) + .multiply_alpha(0.5), + ) + }) + .debug_name("Active Tab Indicator"), + )) + .on_resize(move |rect| { + layout_rect.set(rect); + }) + .style(move |s| { + let config = config.get(); + s.height_full() + .flex_col() + .items_center() + .justify_center() + .cursor(CursorStyle::Pointer) + .hover(|s| s.background(config.color(LapceColor::HOVER_BACKGROUND))) + }) + .debug_name("Tab and Active Indicator") + .on_event_stop(EventListener::DragOver, move |event| { + if dragging.with_untracked(|dragging| dragging.is_some()) { + if let Event::PointerMove(pointer_event) = event { + let new_left = pointer_event.pos.x + < header_content_size.get_untracked().width / 2.0; + if drag_over_left.get_untracked() != Some(new_left) { + drag_over_left.set(Some(new_left)); + } + } + } + }) + .on_event(EventListener::Drop, move |event| { + if let Some((from_index, from_editor_tab_id)) = dragging.get_untracked() + { + drag_over_left.set(None); + if let Event::PointerUp(pointer_event) = event { + let left = pointer_event.pos.x + < header_content_size.get_untracked().width / 2.0; + let index = i.get_untracked(); + let new_index = if left { index } else { index + 1 }; + main_split.move_editor_tab_child( + from_editor_tab_id, + editor_tab_id, + from_index.get_untracked(), + new_index, + ); + } + EventPropagation::Stop + } else { + EventPropagation::Continue + } + }) + .on_event_stop(EventListener::DragLeave, move |_| { + drag_over_left.set(None); + }) + }; + + let content_size = create_rw_signal(Size::ZERO); + let scroll_offset = create_rw_signal(Rect::ZERO); + stack(( + stack({ + let size = create_rw_signal(Size::ZERO); + ( + clip(empty().style(move |s| { + let config = config.get(); + s.absolute() + .height_full() + .width(size.get().width as f32) + .background(config.color(LapceColor::PANEL_BACKGROUND)) + .box_shadow_blur(3.0) + .box_shadow_color( + config.color(LapceColor::LAPCE_DROPDOWN_SHADOW), + ) + })) + .style(move |s| { + let scroll_offset = scroll_offset.get(); + s.absolute() + .width(size.get().width as f32 + 30.0) + .height_full() + .apply_if(scroll_offset.x0 == 0.0, |s| s.hide()) + }), + stack(( + clickable_icon( + || LapceIcons::TAB_PREVIOUS, + move || { + workbench_command + .send(LapceWorkbenchCommand::PreviousEditorTab); + }, + || false, + || false, + || "Previous Tab", + config, + ) + .style(|s| s.margin_horiz(6.0).margin_vert(7.0)), + clickable_icon( + || LapceIcons::TAB_NEXT, + move || { + workbench_command + .send(LapceWorkbenchCommand::NextEditorTab); + }, + || false, + || false, + || "Next Tab", + config, + ) + .style(|s| s.margin_right(6.0)), + )) + .on_resize(move |rect| { + size.set(rect.size()); + }) + .debug_name("Next/Previoius Tab Buttons") + .style(move |s| s.items_center()), + ) + }) + .style(|s| s.flex_shrink(0.)), + container( + scroll({ + dyn_stack(items, key, view_fn) + .on_resize(move |rect| { + let size = rect.size(); + if content_size.get_untracked() != size { + content_size.set(size); + } + }) + .debug_name("Horizontal Tab Stack") + .style(|s| s.height_full().items_center()) + }) + .on_scroll(move |rect| { + scroll_offset.set(rect); + }) + .ensure_visible(move || { + let active = editor_tab_active.get(); + editor_tab + .with_untracked(|editor_tab| editor_tab.children[active].1) + .get_untracked() + }) + .scroll_style(|s| s.hide_bars(true)) + .style(|s| { + s.set(VerticalScrollAsHorizontal, true) + .absolute() + .size_full() + }), + ) + .style(|s| s.height_full().flex_grow(1.0).flex_basis(0.).min_width(10.)) + .debug_name("Tab scroll"), + stack({ + let size = create_rw_signal(Size::ZERO); + ( + clip({ + empty().style(move |s| { + let config = config.get(); + s.absolute() + .height_full() + .margin_left(30.0) + .width(size.get().width as f32) + .background(config.color(LapceColor::PANEL_BACKGROUND)) + .box_shadow_blur(3.0) + .box_shadow_color( + config.color(LapceColor::LAPCE_DROPDOWN_SHADOW), + ) + }) + }) + .style(move |s| { + let content_size = content_size.get(); + let scroll_offset = scroll_offset.get(); + s.absolute() + .margin_left(-30.0) + .width(size.get().width as f32 + 30.0) + .height_full() + .apply_if(scroll_offset.x1 >= content_size.width, |s| { + s.hide() + }) + }), + stack(( + clickable_icon( + || LapceIcons::SPLIT_HORIZONTAL, + move || { + let editor_tab_id = + editor_tab.with_untracked(|t| t.editor_tab_id); + internal_command.send(InternalCommand::Split { + direction: SplitDirection::Vertical, + editor_tab_id, + }); + }, + || false, + || false, + || "Split Horizontally", + config, + ) + .style(|s| s.margin_left(6.0)), + clickable_icon( + || LapceIcons::CLOSE, + move || { + let editor_tab_id = + editor_tab.with_untracked(|t| t.editor_tab_id); + internal_command.send(InternalCommand::EditorTabClose { + editor_tab_id, + }); + }, + || false, + || false, + || "Close All", + config, + ) + .style(|s| s.margin_horiz(6.0)), + )) + .on_resize(move |rect| { + size.set(rect.size()); + }) + .style(|s| s.items_center().height_full()), + ) + }) + .debug_name("Split/Close Panel Buttons") + .style(move |s| { + let content_size = content_size.get(); + let scroll_offset = scroll_offset.get(); + s.height_full() + .flex_shrink(0.) + .margin_left(PxPctAuto::Auto) + .apply_if(scroll_offset.x1 < content_size.width, |s| { + s.margin_left(0.) + }) + }), + )) + .style(move |s| { + let config = config.get(); + s.items_center() + .max_width_full() + .border_bottom(1.0) + .border_color(config.color(LapceColor::LAPCE_BORDER)) + .background(config.color(LapceColor::PANEL_BACKGROUND)) + .height(config.ui.header_height() as i32) + }) + .debug_name("Editor Tab Header") +} + +fn editor_tab_content( + window_tab_data: Rc, + plugin: PluginData, + active_editor_tab: ReadSignal>, + editor_tab: RwSignal, +) -> impl View { + let main_split = window_tab_data.main_split.clone(); + let common = main_split.common.clone(); + let workspace = common.workspace.clone(); + let editors = main_split.editors; + let diff_editors = main_split.diff_editors; + let config = common.config; + let focus = common.focus; + let items = move || { + editor_tab + .get() + .children + .into_iter() + .map(|(_, _, child)| child) + }; + let key = |child: &EditorTabChild| child.id(); + let view_fn = move |child| { + let common = common.clone(); + let child = match child { + EditorTabChild::Editor(editor_id) => { + if let Some(editor_data) = editors.editor_untracked(editor_id) { + let editor_scope = editor_data.scope; + let editor_tab_id = editor_data.editor_tab_id; + let is_active = move |tracked: bool| { + editor_scope.track(); + let focus = if tracked { + focus.get() + } else { + focus.get_untracked() + }; + if let Focus::Workbench = focus { + let active_editor_tab = if tracked { + active_editor_tab.get() + } else { + active_editor_tab.get_untracked() + }; + let editor_tab = if tracked { + editor_tab_id.get() + } else { + editor_tab_id.get_untracked() + }; + editor_tab.is_some() && editor_tab == active_editor_tab + } else { + false + } + }; + let editor_data = create_rw_signal(editor_data); + editor_container_view( + window_tab_data.clone(), + workspace.clone(), + is_active, + editor_data, + ) + .into_any() + } else { + text("empty editor").into_any() + } + } + EditorTabChild::DiffEditor(diff_editor_id) => { + let diff_editor_data = diff_editors.with_untracked(|diff_editors| { + diff_editors.get(&diff_editor_id).cloned() + }); + if let Some(diff_editor_data) = diff_editor_data { + let focus_right = diff_editor_data.focus_right; + let diff_editor_tab_id = diff_editor_data.editor_tab_id; + let diff_editor_scope = diff_editor_data.scope; + let is_active = move |tracked: bool| { + let focus = if tracked { + focus.get() + } else { + focus.get_untracked() + }; + if let Focus::Workbench = focus { + let active_editor_tab = if tracked { + active_editor_tab.get() + } else { + active_editor_tab.get_untracked() + }; + let diff_editor_tab_id = if tracked { + diff_editor_tab_id.get() + } else { + diff_editor_tab_id.get_untracked() + }; + Some(diff_editor_tab_id) == active_editor_tab + } else { + false + } + }; + let left_viewport = diff_editor_data.left.viewport(); + let left_scroll_to = diff_editor_data.left.scroll_to(); + let right_viewport = diff_editor_data.right.viewport(); + let right_scroll_to = diff_editor_data.right.scroll_to(); + create_effect(move |_| { + let left_viewport = left_viewport.get(); + if right_viewport.get_untracked() != left_viewport { + right_scroll_to + .set(Some(left_viewport.origin().to_vec2())); + } + }); + create_effect(move |_| { + let right_viewport = right_viewport.get(); + if left_viewport.get_untracked() != right_viewport { + left_scroll_to + .set(Some(right_viewport.origin().to_vec2())); + } + }); + let left_editor = + create_rw_signal(diff_editor_data.left.clone()); + let right_editor = + create_rw_signal(diff_editor_data.right.clone()); + stack(( + container( + editor_container_view( + window_tab_data.clone(), + workspace.clone(), + move |track| { + is_active(track) + && if track { + !focus_right.get() + } else { + !focus_right.get_untracked() + } + }, + left_editor, + ) + .debug_name("Left Editor"), + ) + .on_event_cont(EventListener::PointerDown, move |_| { + focus_right.set(false); + }) + .style(move |s| { + s.height_full() + .flex_grow(1.0) + .flex_basis(0.0) + .border_right(1.0) + .border_color( + config.get().color(LapceColor::LAPCE_BORDER), + ) + }), + container( + editor_container_view( + window_tab_data.clone(), + workspace.clone(), + move |track| { + is_active(track) + && if track { + focus_right.get() + } else { + focus_right.get_untracked() + } + }, + right_editor, + ) + .debug_name("Right Editor"), + ) + .on_event_cont(EventListener::PointerDown, move |_| { + focus_right.set(true); + }) + .style(|s| s.height_full().flex_grow(1.0).flex_basis(0.0)), + diff_show_more_section_view( + &diff_editor_data.left, + &diff_editor_data.right, + ), + )) + .style(|s: Style| s.size_full()) + .on_cleanup(move || { + diff_editor_scope.dispose(); + }) + .into_any() + } else { + text("empty diff editor").into_any() + } + } + EditorTabChild::Settings(_) => { + settings_view(plugin.installed, editors, common).into_any() + } + EditorTabChild::ThemeColorSettings(_) => { + theme_color_settings_view(editors, common).into_any() + } + EditorTabChild::Keymap(_) => keymap_view(editors, common).into_any(), + EditorTabChild::Volt(_, id) => { + plugin_info_view(plugin.clone(), id).into_any() + } + }; + child.style(|s| s.size_full()) + }; + let active = move || editor_tab.with(|t| t.active); + + tab(active, items, key, view_fn) + .style(|s| s.size_full()) + .debug_name("Editor Tab Content") +} + +#[derive(Clone, Copy, PartialEq, Eq)] +enum DragOverPosition { + Top, + Bottom, + Left, + Right, + Middle, +} + +fn editor_tab( + window_tab_data: Rc, + plugin: PluginData, + active_editor_tab: ReadSignal>, + editor_tab: RwSignal, + dragging: RwSignal, EditorTabId)>>, +) -> impl View { + let main_split = window_tab_data.main_split.clone(); + let common = main_split.common.clone(); + let editor_tabs = main_split.editor_tabs; + let editor_tab_id = + editor_tab.with_untracked(|editor_tab| editor_tab.editor_tab_id); + let config = common.config; + let focus = common.focus; + let internal_command = main_split.common.internal_command; + let tab_size = create_rw_signal(Size::ZERO); + let drag_over: RwSignal> = create_rw_signal(None); + stack(( + editor_tab_header( + window_tab_data.clone(), + active_editor_tab, + editor_tab, + dragging, + ), + stack(( + editor_tab_content( + window_tab_data.clone(), + plugin.clone(), + active_editor_tab, + editor_tab, + ), + empty() + .style(move |s| { + let pos = drag_over.get(); + let width = match pos { + Some(pos) => match pos { + DragOverPosition::Top => 100.0, + DragOverPosition::Bottom => 100.0, + DragOverPosition::Left => 50.0, + DragOverPosition::Right => 50.0, + DragOverPosition::Middle => 100.0, + }, + None => 100.0, + }; + let height = match pos { + Some(pos) => match pos { + DragOverPosition::Top => 50.0, + DragOverPosition::Bottom => 50.0, + DragOverPosition::Left => 100.0, + DragOverPosition::Right => 100.0, + DragOverPosition::Middle => 100.0, + }, + None => 100.0, + }; + let size = tab_size.get_untracked(); + let margin_left = match pos { + Some(pos) => match pos { + DragOverPosition::Top => 0.0, + DragOverPosition::Bottom => 0.0, + DragOverPosition::Left => 0.0, + DragOverPosition::Right => size.width / 2.0, + DragOverPosition::Middle => 0.0, + }, + None => 0.0, + }; + let margin_top = match pos { + Some(pos) => match pos { + DragOverPosition::Top => 0.0, + DragOverPosition::Bottom => size.height / 2.0, + DragOverPosition::Left => 0.0, + DragOverPosition::Right => 0.0, + DragOverPosition::Middle => 0.0, + }, + None => 0.0, + }; + s.absolute() + .size_pct(width, height) + .margin_top(margin_top as f32) + .margin_left(margin_left as f32) + .apply_if(pos.is_none(), |s| s.hide()) + .background( + config + .get() + .color(LapceColor::EDITOR_DRAG_DROP_BACKGROUND), + ) + }) + .debug_name("Drag Over Handle"), + empty() + .on_event_stop(EventListener::DragOver, move |event| { + if dragging.with_untracked(|dragging| dragging.is_some()) { + if let Event::PointerMove(pointer_event) = event { + let size = tab_size.get_untracked(); + let pos = pointer_event.pos; + let new_drag_over = if pos.x < size.width / 4.0 { + DragOverPosition::Left + } else if pos.x > size.width * 3.0 / 4.0 { + DragOverPosition::Right + } else if pos.y < size.height / 4.0 { + DragOverPosition::Top + } else if pos.y > size.height * 3.0 / 4.0 { + DragOverPosition::Bottom + } else { + DragOverPosition::Middle + }; + if drag_over.get_untracked() != Some(new_drag_over) { + drag_over.set(Some(new_drag_over)); + } + } + } + }) + .on_event_stop(EventListener::DragLeave, move |_| { + drag_over.set(None); + }) + .on_event(EventListener::Drop, move |_| { + if let Some((from_index, from_editor_tab_id)) = + dragging.get_untracked() + { + if let Some(pos) = drag_over.get_untracked() { + match pos { + DragOverPosition::Top => { + main_split.move_editor_tab_child_to_new_split( + from_editor_tab_id, + from_index.get_untracked(), + editor_tab_id, + SplitMoveDirection::Up, + ); + } + DragOverPosition::Bottom => { + main_split.move_editor_tab_child_to_new_split( + from_editor_tab_id, + from_index.get_untracked(), + editor_tab_id, + SplitMoveDirection::Down, + ); + } + DragOverPosition::Left => { + main_split.move_editor_tab_child_to_new_split( + from_editor_tab_id, + from_index.get_untracked(), + editor_tab_id, + SplitMoveDirection::Left, + ); + } + DragOverPosition::Right => { + main_split.move_editor_tab_child_to_new_split( + from_editor_tab_id, + from_index.get_untracked(), + editor_tab_id, + SplitMoveDirection::Right, + ); + } + DragOverPosition::Middle => { + main_split.move_editor_tab_child( + from_editor_tab_id, + editor_tab_id, + from_index.get_untracked(), + editor_tab.with_untracked(|editor_tab| { + editor_tab.active + 1 + }), + ); + } + } + } + drag_over.set(None); + EventPropagation::Stop + } else { + EventPropagation::Continue + } + }) + .on_resize(move |rect| { + tab_size.set(rect.size()); + }) + .style(move |s| { + s.absolute() + .size_full() + .apply_if(dragging.get().is_none(), |s| { + s.pointer_events_none() + }) + }), + )) + .debug_name("Editor Content and Drag Over") + .style(|s| s.size_full()), + )) + .on_event_cont(EventListener::PointerDown, move |_| { + if focus.get_untracked() != Focus::Workbench { + focus.set(Focus::Workbench); + } + let editor_tab_id = editor_tab.with_untracked(|t| t.editor_tab_id); + internal_command.send(InternalCommand::FocusEditorTab { editor_tab_id }); + }) + .on_cleanup(move || { + if editor_tabs + .with_untracked(|editor_tabs| editor_tabs.contains_key(&editor_tab_id)) + { + return; + } + editor_tab + .with_untracked(|editor_tab| editor_tab.scope) + .dispose(); + }) + .style(|s| s.flex_col().size_full()) + .debug_name("Editor Tab (Content + Header)") +} + +fn split_resize_border( + splits: ReadSignal>>, + editor_tabs: ReadSignal>>, + split: ReadSignal, + config: ReadSignal>, +) -> impl View { + let content_rect = move |content: &SplitContent, tracked: bool| { + if tracked { + match content { + SplitContent::EditorTab(editor_tab_id) => { + let editor_tab_data = + editor_tabs.with(|tabs| tabs.get(editor_tab_id).cloned()); + if let Some(editor_tab_data) = editor_tab_data { + editor_tab_data.with(|editor_tab| editor_tab.layout_rect) + } else { + Rect::ZERO + } + } + SplitContent::Split(split_id) => { + if let Some(split) = + splits.with(|splits| splits.get(split_id).cloned()) + { + split.with(|split| split.layout_rect) + } else { + Rect::ZERO + } + } + } + } else { + match content { + SplitContent::EditorTab(editor_tab_id) => { + let editor_tab_data = editor_tabs + .with_untracked(|tabs| tabs.get(editor_tab_id).cloned()); + if let Some(editor_tab_data) = editor_tab_data { + editor_tab_data + .with_untracked(|editor_tab| editor_tab.layout_rect) + } else { + Rect::ZERO + } + } + SplitContent::Split(split_id) => { + if let Some(split) = + splits.with_untracked(|splits| splits.get(split_id).cloned()) + { + split.with_untracked(|split| split.layout_rect) + } else { + Rect::ZERO + } + } + } + } + }; + let direction = move |tracked: bool| { + if tracked { + split.with(|split| split.direction) + } else { + split.with_untracked(|split| split.direction) + } + }; + dyn_stack( + move || { + let data = split.get(); + data.children.into_iter().enumerate().skip(1) + }, + |(index, (_, content))| (*index, content.id()), + move |(index, (_, content))| { + let drag_start: RwSignal> = create_rw_signal(None); + let view = empty(); + let view_id = view.id(); + view.on_event_stop(EventListener::PointerDown, move |event| { + view_id.request_active(); + if let Event::PointerDown(pointer_event) = event { + drag_start.set(Some(pointer_event.pos)); + } + }) + .on_event_stop(EventListener::PointerUp, move |_| { + drag_start.set(None); + }) + .on_event_stop(EventListener::PointerMove, move |event| { + if let Event::PointerMove(pointer_event) = event { + if let Some(drag_start_point) = drag_start.get_untracked() { + let rects = split.with_untracked(|split| { + split + .children + .iter() + .map(|(_, c)| content_rect(c, false)) + .collect::>() + }); + let direction = direction(false); + match direction { + SplitDirection::Vertical => { + let left = rects[index - 1].width(); + let right = rects[index].width(); + let shift = pointer_event.pos.x - drag_start_point.x; + let left = left + shift; + let right = right - shift; + let total_width = + rects.iter().map(|r| r.width()).sum::(); + split.with_untracked(|split| { + for (i, (size, _)) in + split.children.iter().enumerate() + { + if i == index - 1 { + size.set(left / total_width); + } else if i == index { + size.set(right / total_width); + } else { + size.set(rects[i].width() / total_width); + } + } + }) + } + SplitDirection::Horizontal => { + let up = rects[index - 1].height(); + let down = rects[index].height(); + let shift = pointer_event.pos.y - drag_start_point.y; + let up = up + shift; + let down = down - shift; + let total_height = + rects.iter().map(|r| r.height()).sum::(); + split.with_untracked(|split| { + for (i, (size, _)) in + split.children.iter().enumerate() + { + if i == index - 1 { + size.set(up / total_height); + } else if i == index { + size.set(down / total_height); + } else { + size.set( + rects[i].height() / total_height, + ); + } + } + }) + } + } + } + } + }) + .style(move |s| { + let rect = content_rect(&content, true); + let is_dragging = drag_start.get().is_some(); + let direction = direction(true); + s.position(Position::Absolute) + .apply_if(direction == SplitDirection::Vertical, |style| { + style.margin_left(rect.x0 as f32 - 0.0) + }) + .apply_if(direction == SplitDirection::Horizontal, |style| { + style.margin_top(rect.y0 as f32 - 0.0) + }) + .width(match direction { + SplitDirection::Vertical => PxPctAuto::Px(4.0), + SplitDirection::Horizontal => PxPctAuto::Pct(100.0), + }) + .height(match direction { + SplitDirection::Vertical => PxPctAuto::Pct(100.0), + SplitDirection::Horizontal => PxPctAuto::Px(4.0), + }) + .flex_direction(match direction { + SplitDirection::Vertical => FlexDirection::Row, + SplitDirection::Horizontal => FlexDirection::Column, + }) + .apply_if(is_dragging, |s| { + s.cursor(match direction { + SplitDirection::Vertical => CursorStyle::ColResize, + SplitDirection::Horizontal => CursorStyle::RowResize, + }) + .background(config.get().color(LapceColor::EDITOR_CARET)) + }) + .hover(|s| { + s.cursor(match direction { + SplitDirection::Vertical => CursorStyle::ColResize, + SplitDirection::Horizontal => CursorStyle::RowResize, + }) + .background(config.get().color(LapceColor::EDITOR_CARET)) + }) + .pointer_events_auto() + }) + }, + ) + .style(|s| { + s.position(Position::Absolute) + .size_full() + .pointer_events_none() + }) + .debug_name("Split Resize Border") +} + +fn split_border( + splits: ReadSignal>>, + editor_tabs: ReadSignal>>, + split: ReadSignal, + config: ReadSignal>, +) -> impl View { + let direction = move || split.with(|split| split.direction); + dyn_stack( + move || split.get().children.into_iter().skip(1), + |(_, content)| content.id(), + move |(_, content)| { + container(empty().style(move |s| { + let direction = direction(); + s.width(match direction { + SplitDirection::Vertical => PxPctAuto::Px(1.0), + SplitDirection::Horizontal => PxPctAuto::Pct(100.0), + }) + .height(match direction { + SplitDirection::Vertical => PxPctAuto::Pct(100.0), + SplitDirection::Horizontal => PxPctAuto::Px(1.0), + }) + .background(config.get().color(LapceColor::LAPCE_BORDER)) + })) + .style(move |s| { + let rect = match &content { + SplitContent::EditorTab(editor_tab_id) => { + let editor_tab_data = editor_tabs + .with(|tabs| tabs.get(editor_tab_id).cloned()); + if let Some(editor_tab_data) = editor_tab_data { + editor_tab_data.with(|editor_tab| editor_tab.layout_rect) + } else { + Rect::ZERO + } + } + SplitContent::Split(split_id) => { + if let Some(split) = + splits.with(|splits| splits.get(split_id).cloned()) + { + split.with(|split| split.layout_rect) + } else { + Rect::ZERO + } + } + }; + let direction = direction(); + s.position(Position::Absolute) + .apply_if(direction == SplitDirection::Vertical, |style| { + style.margin_left(rect.x0 as f32 - 2.0) + }) + .apply_if(direction == SplitDirection::Horizontal, |style| { + style.margin_top(rect.y0 as f32 - 2.0) + }) + .width(match direction { + SplitDirection::Vertical => PxPctAuto::Px(4.0), + SplitDirection::Horizontal => PxPctAuto::Pct(100.0), + }) + .height(match direction { + SplitDirection::Vertical => PxPctAuto::Pct(100.0), + SplitDirection::Horizontal => PxPctAuto::Px(4.0), + }) + .flex_direction(match direction { + SplitDirection::Vertical => FlexDirection::Row, + SplitDirection::Horizontal => FlexDirection::Column, + }) + .justify_content(Some(JustifyContent::Center)) + }) + }, + ) + .style(|s| { + s.position(Position::Absolute) + .size_full() + .pointer_events_none() + }) + .debug_name("Split Border") +} + +fn split_list( + split: ReadSignal, + window_tab_data: Rc, + plugin: PluginData, + dragging: RwSignal, EditorTabId)>>, +) -> impl View { + let main_split = window_tab_data.main_split.clone(); + let editor_tabs = main_split.editor_tabs.read_only(); + let active_editor_tab = main_split.active_editor_tab.read_only(); + let splits = main_split.splits.read_only(); + let config = main_split.common.config; + let split_id = split.with_untracked(|split| split.split_id); + + let direction = move || split.with(|split| split.direction); + let items = move || split.get().children.into_iter().enumerate(); + let key = |(_index, (_, content)): &(usize, (RwSignal, SplitContent))| { + content.id() + }; + let view_fn = { + let main_split = main_split.clone(); + let window_tab_data = window_tab_data.clone(); + move |(_index, (split_size, content)): ( + usize, + (RwSignal, SplitContent), + )| { + let plugin = plugin.clone(); + let child = match &content { + SplitContent::EditorTab(editor_tab_id) => { + let editor_tab_data = editor_tabs + .with_untracked(|tabs| tabs.get(editor_tab_id).cloned()); + if let Some(editor_tab_data) = editor_tab_data { + editor_tab( + window_tab_data.clone(), + plugin.clone(), + active_editor_tab, + editor_tab_data, + dragging, + ) + .into_any() + } else { + text("empty editor tab").into_any() + } + } + SplitContent::Split(split_id) => { + if let Some(split) = + splits.with(|splits| splits.get(split_id).cloned()) + { + split_list( + split.read_only(), + window_tab_data.clone(), + plugin.clone(), + dragging, + ) + .into_any() + } else { + text("empty split").into_any() + } + } + }; + let local_main_split = main_split.clone(); + let local_local_main_split = main_split.clone(); + child + .on_resize(move |rect| match &content { + SplitContent::EditorTab(editor_tab_id) => { + local_main_split.editor_tab_update_layout( + editor_tab_id, + None, + Some(rect), + ); + } + SplitContent::Split(split_id) => { + let split_data = + splits.with(|splits| splits.get(split_id).cloned()); + if let Some(split_data) = split_data { + split_data.update(|split| { + split.layout_rect = rect; + }); + } + } + }) + .on_move(move |point| match &content { + SplitContent::EditorTab(editor_tab_id) => { + local_local_main_split.editor_tab_update_layout( + editor_tab_id, + Some(point), + None, + ); + } + SplitContent::Split(split_id) => { + let split_data = + splits.with(|splits| splits.get(split_id).cloned()); + if let Some(split_data) = split_data { + split_data.update(|split| { + split.window_origin = point; + }); + } + } + }) + .style(move |s| s.flex_grow(split_size.get() as f32).flex_basis(0.0)) + } + }; + container( + stack(( + dyn_stack(items, key, view_fn).style(move |s| { + s.flex_direction(match direction() { + SplitDirection::Vertical => FlexDirection::Row, + SplitDirection::Horizontal => FlexDirection::Column, + }) + .size_full() + }), + split_border(splits, editor_tabs, split, config), + split_resize_border(splits, editor_tabs, split, config), + )) + .style(|s| s.size_full()), + ) + .on_cleanup(move || { + if splits.with_untracked(|splits| splits.contains_key(&split_id)) { + return; + } + split + .with_untracked(|split_data| split_data.scope) + .dispose(); + }) + .debug_name("Split List") +} + +fn main_split(window_tab_data: Rc) -> impl View { + let root_split = window_tab_data.main_split.root_split; + let root_split = window_tab_data + .main_split + .splits + .get_untracked() + .get(&root_split) + .unwrap() + .read_only(); + let config = window_tab_data.main_split.common.config; + let panel = window_tab_data.panel.clone(); + let plugin = window_tab_data.plugin.clone(); + let dragging: RwSignal, EditorTabId)>> = + create_rw_signal(None); + split_list( + root_split, + window_tab_data.clone(), + plugin.clone(), + dragging, + ) + .style(move |s| { + let config = config.get(); + let is_hidden = panel.panel_bottom_maximized(true) + && panel.is_container_shown(&PanelContainerPosition::Bottom, true); + s.border_color(config.color(LapceColor::LAPCE_BORDER)) + .background(config.color(LapceColor::EDITOR_BACKGROUND)) + .apply_if(is_hidden, |s| s.display(Display::None)) + .width_full() + .flex_grow(1.0) + .flex_basis(0.0) + }) + .debug_name("Main Split") +} + +pub fn not_clickable_icon( + icon: impl Fn() -> &'static str + 'static, + active_fn: impl Fn() -> bool + 'static, + disabled_fn: impl Fn() -> bool + 'static + Copy, + tooltip_: impl Fn() -> S + 'static + Clone, + config: ReadSignal>, +) -> impl View { + tooltip_label( + config, + clickable_icon_base( + icon, + None::>, + active_fn, + disabled_fn, + config, + ), + tooltip_, + ) + .debug_name("Not Clickable Icon") +} + +pub fn clickable_icon( + icon: impl Fn() -> &'static str + 'static, + on_click: impl Fn() + 'static, + active_fn: impl Fn() -> bool + 'static, + disabled_fn: impl Fn() -> bool + 'static + Copy, + tooltip_: impl Fn() -> S + 'static + Clone, + config: ReadSignal>, +) -> impl View { + tooltip_label( + config, + clickable_icon_base(icon, Some(on_click), active_fn, disabled_fn, config), + tooltip_, + ) +} + +pub fn clickable_icon_base( + icon: impl Fn() -> &'static str + 'static, + on_click: Option, + active_fn: impl Fn() -> bool + 'static, + disabled_fn: impl Fn() -> bool + 'static + Copy, + config: ReadSignal>, +) -> impl View { + let view = container( + svg(move || config.get().ui_svg(icon())) + .style(move |s| { + let config = config.get(); + let size = config.ui.icon_size() as f32; + s.size(size, size) + .color(config.color(LapceColor::LAPCE_ICON_ACTIVE)) + .disabled(|s| { + s.color(config.color(LapceColor::LAPCE_ICON_INACTIVE)) + .cursor(CursorStyle::Default) + }) + }) + .disabled(disabled_fn), + ) + .disabled(disabled_fn) + .style(move |s| { + let config = config.get(); + s.padding(4.0) + .border_radius(6.0) + .border(1.0) + .border_color(Color::TRANSPARENT) + .apply_if(active_fn(), |s| { + s.border_color(config.color(LapceColor::EDITOR_CARET)) + }) + .hover(|s| { + s.cursor(CursorStyle::Pointer) + .background(config.color(LapceColor::PANEL_HOVERED_BACKGROUND)) + }) + .active(|s| { + s.background( + config.color(LapceColor::PANEL_HOVERED_ACTIVE_BACKGROUND), + ) + }) + }); + + if let Some(on_click) = on_click { + view.on_click_stop(move |_| { + on_click(); + }) + } else { + view + } +} + +/// A tooltip with a label inside. +/// +/// When styling an element that has the tooltip, it will style the child rather than the tooltip +/// label. +pub fn tooltip_label( + config: ReadSignal>, + child: V, + text: impl Fn() -> S + 'static + Clone, +) -> impl View { + tooltip(child, move || { + tooltip_tip( + config, + label(text.clone()).style(move |s| s.selectable(false)), + ) + }) +} + +fn tooltip_tip( + config: ReadSignal>, + child: V, +) -> impl IntoView { + container(child).style(move |s| { + let config = config.get(); + s.padding_horiz(10.0) + .padding_vert(5.0) + .font_size(config.ui.font_size() as f32) + .font_family(config.ui.font_family.clone()) + .color(config.color(LapceColor::TOOLTIP_FOREGROUND)) + .background(config.color(LapceColor::TOOLTIP_BACKGROUND)) + .border(1) + .border_radius(6) + .border_color(config.color(LapceColor::LAPCE_BORDER)) + .box_shadow_blur(3.0) + .box_shadow_color(config.color(LapceColor::LAPCE_DROPDOWN_SHADOW)) + .margin_left(0.0) + .margin_top(4.0) + }) +} + +fn workbench(window_tab_data: Rc) -> impl View { + let workbench_size = window_tab_data.common.workbench_size; + let main_split_width = window_tab_data.main_split.width; + stack(( + panel_container_view(window_tab_data.clone(), PanelContainerPosition::Left), + { + let window_tab_data = window_tab_data.clone(); + stack(( + main_split(window_tab_data.clone()), + panel_container_view( + window_tab_data, + PanelContainerPosition::Bottom, + ), + )) + .on_resize(move |rect| { + let width = rect.size().width; + if main_split_width.get_untracked() != width { + main_split_width.set(width); + } + }) + .style(|s| s.flex_col().flex_grow(1.0)) + }, + panel_container_view(window_tab_data.clone(), PanelContainerPosition::Right), + window_message_view(window_tab_data.messages, window_tab_data.common.config), + )) + .on_resize(move |rect| { + let size = rect.size(); + if size != workbench_size.get_untracked() { + workbench_size.set(size); + } + }) + .style(move |s| s.size_full()) + .debug_name("Workbench") +} + +fn palette_item( + workspace: Arc, + i: usize, + item: PaletteItem, + index: ReadSignal, + palette_item_height: f64, + config: ReadSignal>, + keymap: Option<&KeyMap>, +) -> impl View + use<> { + match &item.content { + PaletteItemContent::File { path, .. } + | PaletteItemContent::Reference { path, .. } => { + let file_name = path + .file_name() + .unwrap_or_default() + .to_string_lossy() + .into_owned(); + // let (file_name, _) = create_signal(cx.scope, file_name); + let folder = path + .parent() + .unwrap_or("".as_ref()) + .to_string_lossy() + .into_owned(); + // let (folder, _) = create_signal(cx.scope, folder); + let folder_len = folder.len(); + + let file_name_indices = item + .indices + .iter() + .filter_map(|&i| { + if folder_len > 0 { + if i > folder_len { + Some(i - folder_len - 1) + } else { + None + } + } else { + Some(i) + } + }) + .collect::>(); + let folder_indices = item + .indices + .iter() + .filter_map(|&i| if i < folder_len { Some(i) } else { None }) + .collect::>(); + + let path = path.to_path_buf(); + let style_path = path.clone(); + container( + stack(( + svg(move || config.get().file_svg(&path).0).style(move |s| { + let config = config.get(); + let size = config.ui.icon_size() as f32; + let color = config.file_svg(&style_path).1; + s.min_width(size) + .size(size, size) + .margin_right(5.0) + .apply_opt(color, Style::color) + }), + focus_text( + move || file_name.clone(), + move || file_name_indices.clone(), + move || config.get().color(LapceColor::EDITOR_FOCUS), + ) + .style(|s| s.margin_right(6.0).max_width_full()), + focus_text( + move || folder.clone(), + move || folder_indices.clone(), + move || config.get().color(LapceColor::EDITOR_FOCUS), + ) + .style(move |s| { + s.color(config.get().color(LapceColor::EDITOR_DIM)) + .min_width(0.0) + .flex_grow(1.0) + .flex_basis(0.0) + }), + )) + .style(|s| s.align_items(Some(AlignItems::Center)).max_width_full()), + ) + } + PaletteItemContent::DocumentSymbol { + kind, + name, + container_name, + .. + } => { + let kind = *kind; + let text = name.to_string(); + let hint = container_name.clone().unwrap_or_default(); + let text_indices: Vec = item + .indices + .iter() + .filter_map(|i| { + let i = *i; + if i < text.len() { Some(i) } else { None } + }) + .collect(); + let hint_indices: Vec = item + .indices + .iter() + .filter_map(|i| { + let i = *i; + if i >= text.len() { + Some(i - text.len()) + } else { + None + } + }) + .collect(); + container( + stack(( + svg(move || { + let config = config.get(); + config + .symbol_svg(&kind) + .unwrap_or_else(|| config.ui_svg(LapceIcons::FILE)) + }) + .style(move |s| { + let config = config.get(); + let size = config.ui.icon_size() as f32; + s.min_width(size).size(size, size).margin_right(5.0).color( + config.symbol_color(&kind).unwrap_or_else(|| { + config.color(LapceColor::LAPCE_ICON_ACTIVE) + }), + ) + }), + focus_text( + move || text.clone(), + move || text_indices.clone(), + move || config.get().color(LapceColor::EDITOR_FOCUS), + ) + .style(|s| s.margin_right(6.0).max_width_full()), + focus_text( + move || hint.clone(), + move || hint_indices.clone(), + move || config.get().color(LapceColor::EDITOR_FOCUS), + ) + .style(move |s| { + s.color(config.get().color(LapceColor::EDITOR_DIM)) + .min_width(0.0) + .flex_grow(1.0) + .flex_basis(0.0) + }), + )) + .style(|s| s.align_items(Some(AlignItems::Center)).max_width_full()), + ) + } + PaletteItemContent::WorkspaceSymbol { + kind, + name, + location, + .. + } => { + let text = name.to_string(); + let kind = *kind; + + let path = location.path.clone(); + let full_path = location.path.clone(); + let path = if let Some(workspace_path) = workspace.path.as_ref() { + path.strip_prefix(workspace_path) + .unwrap_or(&full_path) + .to_path_buf() + } else { + path + }; + + let hint = path.to_string_lossy().to_string(); + let text_indices: Vec = item + .indices + .iter() + .filter_map(|i| { + let i = *i; + if i < text.len() { Some(i) } else { None } + }) + .collect(); + let hint_indices: Vec = item + .indices + .iter() + .filter_map(|i| { + let i = *i; + if i >= text.len() { + Some(i - text.len()) + } else { + None + } + }) + .collect(); + container( + stack(( + svg(move || { + let config = config.get(); + config + .symbol_svg(&kind) + .unwrap_or_else(|| config.ui_svg(LapceIcons::FILE)) + }) + .style(move |s| { + let config = config.get(); + let size = config.ui.icon_size() as f32; + s.min_width(size) + .size(size, size) + .margin_right(5.0) + .color(config.color(LapceColor::LAPCE_ICON_ACTIVE)) + }), + focus_text( + move || text.clone(), + move || text_indices.clone(), + move || config.get().color(LapceColor::EDITOR_FOCUS), + ) + .style(|s| s.margin_right(6.0).max_width_full()), + focus_text( + move || hint.clone(), + move || hint_indices.clone(), + move || config.get().color(LapceColor::EDITOR_FOCUS), + ) + .style(move |s| { + s.color(config.get().color(LapceColor::EDITOR_DIM)) + .min_width(0.0) + .flex_grow(1.0) + .flex_basis(0.0) + }), + )) + .style(|s| s.align_items(Some(AlignItems::Center)).max_width_full()), + ) + } + PaletteItemContent::RunAndDebug { + mode, + config: run_config, + } => { + let mode = *mode; + let text = format!("{mode} {}", run_config.name); + let hint = format!( + "{} {}", + run_config.program, + run_config.args.clone().unwrap_or_default().join(" ") + ); + let text_indices: Vec = item + .indices + .iter() + .filter_map(|i| { + let i = *i; + if i < text.len() { Some(i) } else { None } + }) + .collect(); + let hint_indices: Vec = item + .indices + .iter() + .filter_map(|i| { + let i = *i; + if i >= text.len() { + Some(i - text.len()) + } else { + None + } + }) + .collect(); + container( + stack(( + svg(move || { + let config = config.get(); + match mode { + RunDebugMode::Run => config.ui_svg(LapceIcons::START), + RunDebugMode::Debug => config.ui_svg(LapceIcons::DEBUG), + } + }) + .style(move |s| { + let config = config.get(); + let size = config.ui.icon_size() as f32; + s.min_width(size) + .size(size, size) + .margin_right(5.0) + .color(config.color(LapceColor::LAPCE_ICON_ACTIVE)) + }), + focus_text( + move || text.clone(), + move || text_indices.clone(), + move || config.get().color(LapceColor::EDITOR_FOCUS), + ) + .style(|s| s.margin_right(6.0).max_width_full()), + focus_text( + move || hint.clone(), + move || hint_indices.clone(), + move || config.get().color(LapceColor::EDITOR_FOCUS), + ) + .style(move |s| { + s.color(config.get().color(LapceColor::EDITOR_DIM)) + .min_width(0.0) + .flex_grow(1.0) + .flex_basis(0.0) + }), + )) + .style(|s| s.align_items(Some(AlignItems::Center)).max_width_full()), + ) + } + PaletteItemContent::PaletteHelp { .. } + | PaletteItemContent::Command { .. } => { + let text = item.filter_text; + let indices = item.indices; + let keys = if let Some(keymap) = keymap { + keymap + .key + .iter() + .map(|key| key.label().trim().to_string()) + .filter(|l| !l.is_empty()) + .collect() + } else { + vec![] + }; + container( + stack(( + focus_text( + move || text.clone(), + move || indices.clone(), + move || config.get().color(LapceColor::EDITOR_FOCUS), + ) + .style(|s| { + s.flex_row() + .flex_grow(1.0) + .align_items(Some(AlignItems::Center)) + }), + stack((dyn_stack( + move || keys.clone(), + |k| k.clone(), + move |key| { + label(move || key.clone()).style(move |s| { + s.padding_horiz(5.0) + .padding_vert(1.0) + .margin_right(5.0) + .border(1.0) + .border_radius(3.0) + .border_color( + config.get().color(LapceColor::LAPCE_BORDER), + ) + .selectable(false) + }) + }, + ),)), + )) + .style(|s| s.width_full().items_center()), + ) + } + PaletteItemContent::Line { .. } + | PaletteItemContent::Workspace { .. } + | PaletteItemContent::SshHost { .. } + | PaletteItemContent::Language { .. } + | PaletteItemContent::LineEnding { .. } + | PaletteItemContent::ColorTheme { .. } + | PaletteItemContent::SCMReference { .. } + | PaletteItemContent::TerminalProfile { .. } + | PaletteItemContent::IconTheme { .. } => { + let text = item.filter_text; + let indices = item.indices; + container( + focus_text( + move || text.clone(), + move || indices.clone(), + move || config.get().color(LapceColor::EDITOR_FOCUS), + ) + .style(|s| s.align_items(Some(AlignItems::Center)).max_width_full()), + ) + } + #[cfg(windows)] + PaletteItemContent::WslHost { .. } => { + let text = item.filter_text; + let indices = item.indices; + container( + focus_text( + move || text.clone(), + move || indices.clone(), + move || config.get().color(LapceColor::EDITOR_FOCUS), + ) + .style(|s| s.align_items(Some(AlignItems::Center)).max_width_full()), + ) + } + } + .style(move |s| { + s.width_full() + .height(palette_item_height as f32) + .padding_horiz(10.0) + .apply_if(index.get() == i, |style| { + style.background( + config.get().color(LapceColor::PALETTE_CURRENT_BACKGROUND), + ) + }) + }) +} + +fn palette_input(window_tab_data: Rc) -> impl View { + let editor = window_tab_data.palette.input_editor.clone(); + let config = window_tab_data.common.config; + let focus = window_tab_data.common.focus; + let is_focused = move || focus.get() == Focus::Palette; + + let input = TextInputBuilder::new() + .is_focused(is_focused) + .build_editor(editor) + .placeholder(move || window_tab_data.palette.placeholder_text().to_owned()) + .style(|s| s.width_full()); + + container(container(input).style(move |s| { + let config = config.get(); + s.width_full() + .height(25.0) + .items_center() + .border_bottom(1.0) + .border_color(config.color(LapceColor::LAPCE_BORDER)) + .background(config.color(LapceColor::EDITOR_BACKGROUND)) + })) + .style(|s| s.padding_bottom(5.0)) +} + +struct PaletteItems(im::Vector); + +impl VirtualVector<(usize, PaletteItem)> for PaletteItems { + fn total_len(&self) -> usize { + self.0.len() + } + + fn slice( + &mut self, + range: Range, + ) -> impl Iterator { + let start = range.start; + Box::new( + self.0 + .slice(range) + .into_iter() + .enumerate() + .map(move |(i, item)| (i + start, item)), + ) + } +} + +fn palette_content( + window_tab_data: Rc, + layout_rect: ReadSignal, +) -> impl View { + let items = window_tab_data.palette.filtered_items; + let keymaps = window_tab_data + .palette + .keypress + .get_untracked() + .command_keymaps; + let index = window_tab_data.palette.index.read_only(); + let clicked_index = window_tab_data.palette.clicked_index.write_only(); + let config = window_tab_data.common.config; + let run_id = window_tab_data.palette.run_id; + let input = window_tab_data.palette.input.read_only(); + let palette_item_height = 25.0; + let workspace = window_tab_data.workspace.clone(); + stack(( + scroll({ + let workspace = workspace.clone(); + virtual_stack( + move || PaletteItems(items.get()), + move |(i, _item)| { + (run_id.get_untracked(), *i, input.get_untracked().input) + }, + move |(i, item)| { + let workspace = workspace.clone(); + let keymap = { + let cmd_kind = match &item.content { + PaletteItemContent::PaletteHelp { cmd } => { + Some(CommandKind::Workbench(cmd.clone())) + } + PaletteItemContent::Command { + cmd: LapceCommand { kind, .. }, + } => Some(kind.clone()), + _ => None, + }; + + cmd_kind + .and_then(|kind| keymaps.get(kind.str())) + .and_then(|maps| maps.first()) + }; + container(palette_item( + workspace, + i, + item, + index, + palette_item_height, + config, + keymap, + )) + .on_click_stop(move |_| { + clicked_index.set(Some(i)); + }) + .style(move |s| { + s.width_full().cursor(CursorStyle::Pointer).hover(|s| { + s.background( + config + .get() + .color(LapceColor::PANEL_HOVERED_BACKGROUND), + ) + }) + }) + }, + ) + .item_size_fixed(move || palette_item_height) + .style(|s| s.width_full().flex_col()) + }) + .ensure_visible(move || { + Size::new(1.0, palette_item_height) + .to_rect() + .with_origin(Point::new( + 0.0, + index.get() as f64 * palette_item_height, + )) + }) + .style(|s| { + s.width_full() + .min_height(0.0) + .set(PropagatePointerWheel, false) + }), + text("No matching results").style(move |s| { + s.display(if items.with(|items| items.is_empty()) { + Display::Flex + } else { + Display::None + }) + .padding_horiz(10.0) + .align_items(Some(AlignItems::Center)) + .height(palette_item_height as f32) + }), + )) + .style(move |s| { + s.flex_col() + .width_full() + .min_height(0.0) + .max_height((layout_rect.get().height() * 0.45 - 36.0).round() as f32) + .padding_bottom(5.0) + .padding_bottom(5.0) + }) +} + +fn palette_preview(window_tab_data: Rc) -> impl View { + let palette_data = window_tab_data.palette.clone(); + let workspace = palette_data.workspace.clone(); + let preview_editor = palette_data.preview_editor; + let has_preview = palette_data.has_preview; + let config = palette_data.common.config; + let preview_editor = create_rw_signal(preview_editor); + container( + container(editor_container_view( + window_tab_data, + workspace, + |_tracked: bool| true, + preview_editor, + )) + .style(move |s| { + let config = config.get(); + s.position(Position::Absolute) + .border_top(1.0) + .border_color(config.color(LapceColor::LAPCE_BORDER)) + .size_full() + .background(config.color(LapceColor::EDITOR_BACKGROUND)) + }), + ) + .style(move |s| { + s.display(if has_preview.get() { + Display::Flex + } else { + Display::None + }) + .flex_grow(1.0) + }) +} + +fn palette(window_tab_data: Rc) -> impl View { + let layout_rect = window_tab_data.layout_rect.read_only(); + let palette_data = window_tab_data.palette.clone(); + let status = palette_data.status.read_only(); + let config = palette_data.common.config; + let has_preview = palette_data.has_preview.read_only(); + container( + stack(( + palette_input(window_tab_data.clone()), + palette_content(window_tab_data.clone(), layout_rect), + palette_preview(window_tab_data.clone()), + )) + .on_event_stop(EventListener::PointerDown, move |_| {}) + .style(move |s| { + let config = config.get(); + s.width(config.ui.palette_width() as f64) + .max_width_full() + .max_height(if has_preview.get() { + PxPctAuto::Auto + } else { + PxPctAuto::Pct(100.0) + }) + .height(if has_preview.get() { + PxPctAuto::Px(layout_rect.get().height() - 10.0) + } else { + PxPctAuto::Auto + }) + .margin_top(4.0) + .border(1.0) + .border_radius(6.0) + .border_color(config.color(LapceColor::LAPCE_BORDER)) + .flex_col() + .background(config.color(LapceColor::PALETTE_BACKGROUND)) + .pointer_events_auto() + }), + ) + .style(move |s| { + s.display(if status.get() == PaletteStatus::Inactive { + Display::None + } else { + Display::Flex + }) + .position(Position::Absolute) + .size_full() + .flex_col() + .items_center() + .pointer_events_none() + }) + .debug_name("Palette Layer") +} + +fn window_message_view( + messages: RwSignal>, + config: ReadSignal>, +) -> impl View { + let view_fn = + move |(i, (title, message)): (usize, (String, ShowMessageParams))| { + stack(( + svg(move || { + if let MessageType::ERROR = message.typ { + config.get().ui_svg(LapceIcons::ERROR) + } else { + config.get().ui_svg(LapceIcons::WARNING) + } + }) + .style(move |s| { + let config = config.get(); + let size = config.ui.icon_size() as f32; + let color = if let MessageType::ERROR = message.typ { + config.color(LapceColor::LAPCE_ERROR) + } else { + config.color(LapceColor::LAPCE_WARN) + }; + s.min_width(size) + .size(size, size) + .margin_right(10.0) + .margin_top(4.0) + .color(color) + }), + stack(( + text(title.clone()).style(|s| { + s.min_width(0.0).line_height(1.8).font_weight(Weight::BOLD) + }), + text(message.message.clone()).style(|s| { + s.min_width(0.0).line_height(1.8).margin_top(5.0) + }), + )) + .style(move |s| { + s.flex_col().min_width(0.0).flex_basis(0.0).flex_grow(1.0) + }), + clickable_icon( + || LapceIcons::CLOSE, + move || { + messages.update(|messages| { + messages.remove(i); + }); + }, + || false, + || false, + || "Close", + config, + ) + .style(|s| s.margin_left(6.0)), + )) + .on_double_click_stop(move |_| { + messages.update(|messages| { + messages.remove(i); + }); + }) + .on_secondary_click_stop({ + let message = message.message.clone(); + move |_| { + let mut clipboard = SystemClipboard::new(); + if !message.is_empty() { + clipboard.put_string(&message); + } + } + }) + .on_event_stop(EventListener::PointerDown, |_| {}) + .style(move |s| { + let config = config.get(); + s.width_full() + .items_start() + .padding(10.0) + .border(1.0) + .border_radius(6.0) + .border_color(config.color(LapceColor::LAPCE_BORDER)) + .background(config.color(LapceColor::PANEL_BACKGROUND)) + .apply_if(i > 0, |s| s.margin_top(10.0)) + }) + }; + + let id = AtomicU64::new(0); + container( + container( + container( + scroll( + dyn_stack( + move || messages.get().into_iter().enumerate(), + move |_| { + id.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + }, + view_fn, + ) + .style(|s| s.flex_col().width_full()), + ) + .style(|s| { + s.absolute() + .pointer_events_auto() + .width_full() + .min_height(0.0) + .max_height_full() + .set(PropagatePointerWheel, false) + }), + ) + .style(|s| s.size_full()), + ) + .style(|s| { + s.width(360.0) + .max_width_pct(80.0) + .padding(10.0) + .height_full() + }), + ) + .style(|s| s.absolute().size_full().justify_end().pointer_events_none()) + .debug_name("Window Message View") +} + +struct VectorItems(im::Vector); + +impl VirtualVector<(usize, V)> for VectorItems { + fn total_len(&self) -> usize { + self.0.len() + } + + fn slice(&mut self, range: Range) -> impl Iterator { + let start = range.start; + self.0 + .slice(range) + .into_iter() + .enumerate() + .map(move |(i, item)| (i + start, item)) + } +} + +fn completion_kind_to_str(kind: CompletionItemKind) -> &'static str { + match kind { + CompletionItemKind::METHOD => "f", + CompletionItemKind::FUNCTION => "f", + CompletionItemKind::CLASS => "c", + CompletionItemKind::STRUCT => "s", + CompletionItemKind::VARIABLE => "v", + CompletionItemKind::INTERFACE => "i", + CompletionItemKind::ENUM => "e", + CompletionItemKind::ENUM_MEMBER => "e", + CompletionItemKind::FIELD => "v", + CompletionItemKind::PROPERTY => "p", + CompletionItemKind::CONSTANT => "d", + CompletionItemKind::MODULE => "m", + CompletionItemKind::KEYWORD => "k", + CompletionItemKind::SNIPPET => "n", + _ => "t", + } +} + +fn hover(window_tab_data: Rc) -> impl View { + let hover_data = window_tab_data.common.hover.clone(); + let config = window_tab_data.common.config; + let id = AtomicU64::new(0); + let layout_rect = window_tab_data.common.hover.layout_rect; + + scroll( + dyn_stack( + move || hover_data.content.get(), + move |_| id.fetch_add(1, std::sync::atomic::Ordering::Relaxed), + move |content| match content { + MarkdownContent::Text(text_layout) => container( + rich_text(move || text_layout.clone()) + .style(|s| s.max_width(600.0)), + ) + .style(|s| s.max_width_full()), + MarkdownContent::Image { .. } => container(empty()), + MarkdownContent::Separator => container(empty().style(move |s| { + s.width_full() + .margin_vert(5.0) + .height(1.0) + .background(config.get().color(LapceColor::LAPCE_BORDER)) + })), + }, + ) + .style(|s| s.flex_col().padding_horiz(10.0).padding_vert(5.0)), + ) + .on_resize(move |rect| { + layout_rect.set(rect); + }) + .on_event_stop(EventListener::PointerMove, |_| {}) + .on_event_stop(EventListener::PointerDown, |_| {}) + .style(move |s| { + let active = window_tab_data.common.hover.active.get(); + if !active { + s.hide() + } else { + let config = config.get(); + if let Some(origin) = window_tab_data.hover_origin() { + s.absolute() + .margin_left(origin.x as f32) + .margin_top(origin.y as f32) + .max_height(300.0) + .border(1.0) + .border_radius(6.0) + .border_color(config.color(LapceColor::LAPCE_BORDER)) + .background(config.color(LapceColor::PANEL_BACKGROUND)) + .set(PropagatePointerWheel, false) + } else { + s.hide() + } + } + }) + .debug_name("Hover Layer") +} + +fn completion(window_tab_data: Rc) -> impl View { + let completion_data = window_tab_data.common.completion; + let active_editor = window_tab_data.main_split.active_editor; + let config = window_tab_data.common.config; + let active = completion_data.with_untracked(|c| c.active); + let request_id = + move || completion_data.with_untracked(|c| (c.request_id, c.input_id)); + scroll( + virtual_stack( + move || completion_data.with(|c| VectorItems(c.filtered_items.clone())), + move |(i, _item)| (request_id(), *i), + move |(i, item)| { + stack(( + container( + text( + item.item.kind.map(completion_kind_to_str).unwrap_or(""), + ) + .style(move |s| { + s.width_full() + .justify_content(Some(JustifyContent::Center)) + }), + ) + .style(move |s| { + let config = config.get(); + let width = config.editor.line_height() as f32; + s.width(width) + .min_width(width) + .height_full() + .align_items(Some(AlignItems::Center)) + .font_weight(Weight::BOLD) + .apply_opt( + config.completion_color(item.item.kind), + |s, c| s.color(c).background(c.multiply_alpha(0.3)), + ) + }), + focus_text( + move || { + if config.get().editor.completion_item_show_detail { + item.item + .detail + .clone() + .unwrap_or(item.item.label.clone()) + } else { + item.item.label.clone() + } + }, + move || item.indices.clone(), + move || config.get().color(LapceColor::EDITOR_FOCUS), + ) + .on_click_stop(move |_| { + active.set(i); + if let Some(editor) = active_editor.get_untracked() { + editor.select_completion(); + } + }) + .on_event_stop(EventListener::PointerDown, |_| {}) + .style(move |s| { + let config = config.get(); + s.padding_horiz(5.0) + .min_width(0.0) + .align_items(Some(AlignItems::Center)) + .size_full() + .cursor(CursorStyle::Pointer) + .apply_if(active.get() == i, |s| { + s.background( + config.color(LapceColor::COMPLETION_CURRENT), + ) + }) + .hover(move |s| { + s.background( + config + .color(LapceColor::PANEL_HOVERED_BACKGROUND), + ) + }) + }), + )) + .style(move |s| { + s.align_items(Some(AlignItems::Center)) + .width_full() + .height(config.get().editor.line_height() as f32) + }) + }, + ) + .item_size_fixed(move || config.get().editor.line_height() as f64) + .style(|s| { + s.align_items(Some(AlignItems::Center)) + .width_full() + .flex_col() + }), + ) + .ensure_visible(move || { + let config = config.get(); + let active = active.get(); + Size::new(1.0, config.editor.line_height() as f64) + .to_rect() + .with_origin(Point::new( + 0.0, + active as f64 * config.editor.line_height() as f64, + )) + }) + .on_resize(move |rect| { + completion_data.update(|c| { + c.layout_rect = rect; + }); + }) + .on_event_stop(EventListener::PointerMove, |_| {}) + .style(move |s| { + let config = config.get(); + let origin = window_tab_data.completion_origin(); + s.position(Position::Absolute) + .width(config.editor.completion_width as i32) + .max_height(400.0) + .margin_left(origin.x as f32) + .margin_top(origin.y as f32) + .background(config.color(LapceColor::COMPLETION_BACKGROUND)) + .font_family(config.editor.font_family.clone()) + .font_size(config.editor.font_size() as f32) + .border_radius(6.0) + }) + .debug_name("Completion Layer") +} + +fn code_action(window_tab_data: Rc) -> impl View { + let config = window_tab_data.common.config; + let code_action = window_tab_data.code_action; + let (status, active) = code_action + .with_untracked(|code_action| (code_action.status, code_action.active)); + let request_id = + move || code_action.with_untracked(|code_action| code_action.request_id); + scroll( + container( + dyn_stack( + move || { + code_action.with(|code_action| { + code_action.filtered_items.clone().into_iter().enumerate() + }) + }, + move |(i, _item)| (request_id(), *i), + move |(i, item)| { + container( + text(item.title().replace('\n', " ")) + .style(|s| s.text_ellipsis().min_width(0.0)), + ) + .on_click_stop(move |_| { + let code_action = code_action.get_untracked(); + code_action.active.set(i); + code_action.select(); + }) + .on_event_stop(EventListener::PointerDown, |_| {}) + .style(move |s| { + let config = config.get(); + s.padding_horiz(10.0) + .align_items(Some(AlignItems::Center)) + .min_width(0.0) + .width_full() + .line_height(1.8) + .border_radius(6.0) + .cursor(CursorStyle::Pointer) + .apply_if(active.get() == i, |s| { + s.background( + config.color(LapceColor::COMPLETION_CURRENT), + ) + }) + .hover(move |s| { + s.background( + config + .color(LapceColor::PANEL_HOVERED_BACKGROUND), + ) + }) + }) + }, + ) + .style(|s| s.width_full().flex_col()), + ) + .style(|s| s.width_full().padding_vert(4.0)), + ) + .ensure_visible(move || { + let config = config.get(); + let active = active.get(); + Size::new(1.0, config.editor.line_height() as f64) + .to_rect() + .with_origin(Point::new( + 0.0, + active as f64 * config.editor.line_height() as f64, + )) + }) + .on_resize(move |rect| { + code_action.update(|c| { + c.layout_rect = rect; + }); + }) + .on_event_stop(EventListener::PointerMove, |_| {}) + .style(move |s| { + let origin = window_tab_data.code_action_origin(); + s.display(match status.get() { + CodeActionStatus::Inactive => Display::None, + CodeActionStatus::Active => Display::Flex, + }) + .position(Position::Absolute) + .width(400.0) + .max_height(400.0) + .margin_left(origin.x as f32) + .margin_top(origin.y as f32) + .background(config.get().color(LapceColor::COMPLETION_BACKGROUND)) + .border_radius(6.0) + }) + .debug_name("Code Action Layer") +} + +fn rename(window_tab_data: Rc) -> impl View { + let editor = window_tab_data.rename.editor.clone(); + let active = window_tab_data.rename.active; + let layout_rect = window_tab_data.rename.layout_rect; + let config = window_tab_data.common.config; + + container( + container( + TextInputBuilder::new() + .is_focused(move || active.get()) + .build_editor(editor) + .style(|s| s.width(150.0)), + ) + .style(move |s| { + let config = config.get(); + s.font_family(config.editor.font_family.clone()) + .font_size(config.editor.font_size() as f32) + .border(1.0) + .border_radius(6.0) + .border_color(config.color(LapceColor::LAPCE_BORDER)) + .background(config.color(LapceColor::EDITOR_BACKGROUND)) + }), + ) + .on_resize(move |rect| { + layout_rect.set(rect); + }) + .on_event_stop(EventListener::PointerMove, |_| {}) + .on_event_stop(EventListener::PointerDown, |_| {}) + .style(move |s| { + let origin = window_tab_data.rename_origin(); + s.position(Position::Absolute) + .apply_if(!active.get(), |s| s.hide()) + .margin_left(origin.x as f32) + .margin_top(origin.y as f32) + .background(config.get().color(LapceColor::PANEL_BACKGROUND)) + .border_radius(6.0) + .padding(6.0) + }) + .debug_name("Rename Layer") +} + +fn window_tab(window_tab_data: Rc) -> impl View { + let source_control = window_tab_data.source_control.clone(); + let window_origin = window_tab_data.common.window_origin; + let layout_rect = window_tab_data.layout_rect; + let config = window_tab_data.common.config; + let workbench_command = window_tab_data.common.workbench_command; + let window_tab_scope = window_tab_data.scope; + let hover_active = window_tab_data.common.hover.active; + let status_height = window_tab_data.status_height; + + let view = stack(( + stack(( + title(window_tab_data.clone()), + workbench(window_tab_data.clone()), + status( + window_tab_data.clone(), + source_control, + workbench_command, + status_height, + config, + ), + )) + .on_resize(move |rect| { + layout_rect.set(rect); + }) + .on_move(move |point| { + window_origin.set(point); + }) + .style(|s| s.size_full().flex_col()) + .debug_name("Base Layer"), + completion(window_tab_data.clone()), + hover(window_tab_data.clone()), + code_action(window_tab_data.clone()), + rename(window_tab_data.clone()), + palette(window_tab_data.clone()), + about::about_popup(window_tab_data.clone()), + alert::alert_box(window_tab_data.alert_data.clone()), + )) + .on_cleanup(move || { + window_tab_scope.dispose(); + }) + .on_event_cont(EventListener::PointerMove, move |_| { + if hover_active.get_untracked() { + hover_active.set(false); + } + }) + .style(move |s| { + let config = config.get(); + s.size_full() + .color(config.color(LapceColor::EDITOR_FOREGROUND)) + .background(config.color(LapceColor::EDITOR_BACKGROUND)) + .font_size(config.ui.font_size() as f32) + .apply_if(!config.ui.font_family.is_empty(), |s| { + s.font_family(config.ui.font_family.clone()) + }) + .class(floem::views::scroll::Handle, |s| { + s.background(config.color(LapceColor::LAPCE_SCROLL_BAR)) + }) + }) + .debug_name("Window Tab"); + + let view_id = view.id(); + window_tab_data.common.view_id.set(view_id); + view +} + +fn workspace_title(workspace: &LapceWorkspace) -> Option { + let p = workspace.path.as_ref()?; + let dir = p.file_name().unwrap_or(p.as_os_str()).to_string_lossy(); + Some(match &workspace.kind { + LapceWorkspaceType::Local => format!("{dir}"), + LapceWorkspaceType::RemoteSSH(remote) => format!("{dir} [{remote}]"), + #[cfg(windows)] + LapceWorkspaceType::RemoteWSL(remote) => format!("{dir} [{remote}]"), + }) +} + +fn workspace_tab_header(window_data: WindowData) -> impl View { + let tabs = window_data.window_tabs; + let active = window_data.active; + let config = window_data.config; + let window_tab_header_height = window_data.common.window_tab_header_height; + let available_width = create_rw_signal(0.0); + let add_icon_width = create_rw_signal(0.0); + let window_control_width = create_rw_signal(0.0); + let window_maximized = window_data.common.window_maximized; + let num_window_tabs = window_data.num_window_tabs; + let window_command = window_data.common.window_command; + + let tab_width = create_memo(move |_| { + let window_control_width = if !cfg!(target_os = "macos") + && config.get_untracked().core.custom_titlebar + { + window_control_width.get() + } else { + 0.0 + }; + let available_width = available_width.get() + - add_icon_width.get() + - if cfg!(target_os = "macos") { 75.0 } else { 0.0 } + - window_control_width + - 30.0; + let tabs_len = tabs.with(|tabs| tabs.len()); + if tabs_len > 0 { + (available_width / tabs_len as f64).min(200.0) + } else { + available_width + } + }); + + let local_window_data = window_data.clone(); + let dragging_index: RwSignal>> = create_rw_signal(None); + let view_fn = move |(index, tab): (RwSignal, Rc)| { + let drag_over_left = create_rw_signal(None); + let window_data = local_window_data.clone(); + stack(( + container({ + stack(( + stack(( + text( + workspace_title(&tab.workspace) + .unwrap_or_else(|| String::from("New Tab")), + ) + .style(|s| { + s.margin_left(10.0) + .min_width(0.0) + .flex_basis(0.0) + .flex_grow(1.0) + .selectable(false) + .text_ellipsis() + }), + { + let window_data = local_window_data.clone(); + clickable_icon( + || LapceIcons::WINDOW_CLOSE, + move || { + window_data.run_window_command( + WindowCommand::CloseWorkspaceTab { + index: Some(index.get_untracked()), + }, + ); + }, + || false, + || false, + || "Close", + config.read_only(), + ) + .style(|s| s.margin_horiz(6.0)) + }, + )) + .on_event_stop(EventListener::DragOver, move |event| { + if dragging_index.get_untracked().is_some() { + if let Event::PointerMove(pointer_event) = event { + let left = pointer_event.pos.x + < tab_width.get_untracked() / 2.0; + if drag_over_left.get_untracked() != Some(left) { + drag_over_left.set(Some(left)); + } + } + } + }) + .on_event(EventListener::Drop, move |event| { + if dragging_index.get_untracked().is_some() { + drag_over_left.set(None); + if let Event::PointerUp(pointer_event) = event { + let left = pointer_event.pos.x + < tab_width.get_untracked() / 2.0; + let index = index.get_untracked(); + let new_index = if left { index } else { index + 1 }; + if let Some(from_index) = + dragging_index.get_untracked() + { + window_data.move_tab( + from_index.get_untracked(), + new_index, + ); + } + dragging_index.set(None); + } + EventPropagation::Stop + } else { + EventPropagation::Continue + } + }) + .on_event_stop(EventListener::DragLeave, move |_| { + drag_over_left.set(None); + }) + .style(move |s| { + let config = config.get(); + s.width_full() + .min_width(0.0) + .items_center() + .border_right(1.0) + .border_color(config.color(LapceColor::LAPCE_BORDER)) + .apply_if( + cfg!(target_os = "macos") && index.get() == 0, + |s| s.border_left(1.0), + ) + }), + container(empty().style(move |s| { + s.size_full() + .apply_if(active.get() == index.get(), |s| { + s.border_bottom(2.0) + }) + .border_color( + config + .get() + .color(LapceColor::LAPCE_TAB_ACTIVE_UNDERLINE), + ) + })) + .style(move |s| { + s.position(Position::Absolute) + .padding_horiz(3.0) + .size_full() + .pointer_events_none() + }), + )) + .style(move |s| s.size_full().items_center()) + }) + .draggable() + .on_event_stop(EventListener::DragStart, move |_| { + dragging_index.set(Some(index)); + }) + .on_event_stop(EventListener::DragEnd, move |_| { + dragging_index.set(None); + }) + .dragging_style(move |s| { + let config = config.get(); + s.border(1.0) + .border_radius(6.0) + .border_color(config.color(LapceColor::LAPCE_BORDER)) + .color( + config + .color(LapceColor::EDITOR_FOREGROUND) + .multiply_alpha(0.7), + ) + .background( + config + .color(LapceColor::PANEL_BACKGROUND) + .multiply_alpha(0.7), + ) + }) + .on_click_stop(move |_| { + active.set(index.get_untracked()); + }) + .style(move |s| s.size_full()), + empty().style(move |s| { + let index = index.get(); + s.absolute() + .margin_left(if index == 0 { 0.0 } else { -2.0 }) + .width( + tab_width.get() as f32 + if index == 0 { 1.0 } else { 3.0 }, + ) + .height_full() + .border_color( + config.get().color(LapceColor::LAPCE_TAB_ACTIVE_UNDERLINE), + ) + .apply_if(drag_over_left.get().is_some(), move |s| { + let drag_over_left = drag_over_left.get_untracked().unwrap(); + if drag_over_left { + s.border_left(3.0) + } else { + s.border_right(3.0) + } + }) + .apply_if(drag_over_left.get().is_none(), move |s| s.hide()) + }), + )) + .style(move |s| s.height_full().width(tab_width.get() as f32)) + }; + + stack(( + empty().style(move |s| { + let is_macos = cfg!(target_os = "macos"); + s.min_width(75.0) + .width(75.0) + .apply_if(!is_macos, |s| s.hide()) + }), + dyn_stack( + move || { + let tabs = tabs.get(); + for (i, (index, _)) in tabs.iter().enumerate() { + if index.get_untracked() != i { + index.set(i); + } + } + tabs + }, + |(_, tab)| tab.window_tab_id, + view_fn, + ) + .style(|s| s.height_full()), + container(clickable_icon( + || LapceIcons::ADD, + move || { + window_data.run_window_command(WindowCommand::NewWorkspaceTab { + workspace: LapceWorkspace::default(), + end: true, + }); + }, + || false, + || false, + || "New Workspace Tab", + config.read_only(), + )) + .on_resize(move |rect| { + let current = add_icon_width.get_untracked(); + if rect.width() != current { + add_icon_width.set(rect.width()); + } + }) + .style(|s| { + s.height_full() + .padding_left(10.0) + .padding_right(10.0) + .items_center() + }), + drag_window_area(empty()) + .style(|s| s.height_full().flex_basis(0.0).flex_grow(1.0)), + window_controls_view( + window_command, + false, + num_window_tabs, + window_maximized, + config.read_only(), + ) + .on_resize(move |rect| { + let width = rect.width(); + if window_control_width.get_untracked() != width { + window_control_width.set(width); + } + }), + )) + .on_resize(move |rect| { + let current = available_width.get_untracked(); + if rect.width() != current { + available_width.set(rect.width()); + } + window_tab_header_height.set(rect.height()); + }) + .style(move |s| { + let config = config.get(); + s.border_bottom(1.0) + .width_full() + .height(37.0) + .font_size(config.ui.font_size() as f32) + .apply_if(!config.ui.font_family.is_empty(), |s| { + s.font_family(config.ui.font_family.clone()) + }) + .apply_if(tabs.with(|tabs| tabs.len() < 2), |s| s.hide()) + .color(config.color(LapceColor::EDITOR_FOREGROUND)) + .border_color(config.color(LapceColor::LAPCE_BORDER)) + .background(config.color(LapceColor::PANEL_BACKGROUND)) + .items_center() + }) + .debug_name("Workspace Tab Header") +} + +fn window(window_data: WindowData) -> impl View { + let window_tabs = window_data.window_tabs.read_only(); + let active = window_data.active.read_only(); + let items = move || window_tabs.get(); + let key = |(_, window_tab): &(RwSignal, Rc)| { + window_tab.window_tab_id + }; + let active = move || active.get(); + let window_focus = create_rw_signal(false); + let ime_enabled = window_data.ime_enabled; + let window_maximized = window_data.common.window_maximized; + + tab(active, items, key, |(_, window_tab_data)| { + window_tab(window_tab_data) + }) + .window_title(move || { + let active = active(); + let window_tabs = window_tabs.get(); + let workspace = window_tabs + .get(active) + .or_else(|| window_tabs.last()) + .and_then(|(_, window_tab)| window_tab.workspace.display()); + match workspace { + Some(workspace) => format!("{workspace} - Lapce"), + None => "Lapce".to_string(), + } + }) + .on_event_stop(EventListener::ImeEnabled, move |_| { + ime_enabled.set(true); + }) + .on_event_stop(EventListener::ImeDisabled, move |_| { + ime_enabled.set(false); + }) + .on_event_cont(EventListener::WindowGotFocus, move |_| { + window_focus.set(true); + }) + .on_event_cont(EventListener::WindowMaximizeChanged, move |event| { + if let Event::WindowMaximizeChanged(maximized) = event { + window_maximized.set(*maximized); + } + }) + .window_menu(move || { + window_focus.track(); + let active = active(); + let window_tabs = window_tabs.get(); + let window_tab = window_tabs.get(active).or_else(|| window_tabs.last()); + if let Some((_, window_tab)) = window_tab { + window_tab.common.keypress.track(); + let workbench_command = window_tab.common.workbench_command; + let lapce_command = window_tab.common.lapce_command; + window_menu(lapce_command, workbench_command) + } else { + Menu::new("Lapce") + } + }) + .style(|s| s.size_full()) + .debug_name("Window") +} + +pub fn launch() { + let cli = Cli::parse(); + + if !cli.wait { + logging::panic_hook(); + } + + let (reload_handle, _guard) = logging::logging(); + trace!(TraceLevel::INFO, "Starting up Lapce.."); + + #[cfg(feature = "vendored-fonts")] + { + use floem::text::{FONT_SYSTEM, fontdb::Source}; + + const FONT_DEJAVU_SANS_REGULAR: &[u8] = include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../extra/fonts/DejaVu/DejaVuSans.ttf" + )); + const FONT_DEJAVU_SANS_MONO_REGULAR: &[u8] = include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../extra/fonts/DejaVu/DejaVuSansMono.ttf" + )); + + FONT_SYSTEM + .lock() + .db_mut() + .load_font_source(Source::Binary(Arc::new(FONT_DEJAVU_SANS_REGULAR))); + FONT_SYSTEM + .lock() + .db_mut() + .load_font_source(Source::Binary(Arc::new( + FONT_DEJAVU_SANS_MONO_REGULAR, + ))); + } + + let stdin = std::io::stdin(); + if !stdin.is_terminal() { + trace!(TraceLevel::INFO, "Loading custom environment from shell"); + load_shell_env(); + } + + // small hack to unblock terminal if launched from it + // launch it as a separate process that waits + if !cli.wait { + let mut args = std::env::args().collect::>(); + args.push("--wait".to_string()); + let mut cmd = std::process::Command::new(&args[0]); + #[cfg(target_os = "windows")] + cmd.creation_flags(windows::Win32::System::Threading::CREATE_NO_WINDOW); + + let stderr_file_path = + Directory::logs_directory().unwrap().join("stderr.log"); + let stderr_file = std::fs::OpenOptions::new() + .write(true) + .truncate(true) + .create(true) + .read(true) + .open(stderr_file_path) + .unwrap(); + let stderr = Stdio::from(stderr_file); + + let stdout_file_path = + Directory::logs_directory().unwrap().join("stdout.log"); + let stdout_file = std::fs::OpenOptions::new() + .write(true) + .truncate(true) + .create(true) + .read(true) + .open(stdout_file_path) + .unwrap(); + let stdout = Stdio::from(stdout_file); + + if let Err(why) = cmd + .args(&args[1..]) + .stderr(stderr) + .stdout(stdout) + .env("LAPCE_LOG", "lapce_app::app=error,off") + .spawn() + { + eprintln!("Failed to launch lapce: {why}"); + std::process::exit(1); + }; + return; + } + + // If the cli is not requesting a new window, and we're not developing a plugin, we try to open + // in the existing Lapce process + if !cli.new { + match get_socket() { + Ok(socket) => { + if let Err(e) = try_open_in_existing_process(socket, &cli.paths) { + trace!(TraceLevel::ERROR, "failed to open path(s): {e}"); + }; + return; + } + Err(err) => { + tracing::error!("{:?}", err); + } + } + } + + #[cfg(feature = "updater")] + crate::update::cleanup(); + + if let Err(err) = lapce_proxy::register_lapce_path() { + tracing::error!("{:?}", err); + } + let db = match LapceDb::new() { + Ok(db) => Arc::new(db), + Err(e) => { + #[cfg(windows)] + logging::error_modal("Error", &format!("Failed to create LapceDb: {e}")); + + trace!(TraceLevel::ERROR, "Failed to create LapceDb: {e}"); + std::process::exit(1); + } + }; + let scope = Scope::new(); + provide_context(db.clone()); + + let window_scale = scope.create_rw_signal(1.0); + let latest_release = scope.create_rw_signal(Arc::new(None)); + let app_command = Listener::new_empty(scope); + + let plugin_paths = Arc::new(cli.plugin_path); + + let (tx, rx) = channel(); + let mut watcher = notify::recommended_watcher(ConfigWatcher::new(tx)) + .context("Failed to spawn file watcher") + .unwrap(); + if let Some(path) = LapceConfig::settings_file() { + if let Err(err) = watcher.watch(&path, notify::RecursiveMode::Recursive) { + tracing::error!("{:?}", err); + } + } + if let Some(path) = Directory::themes_directory() { + if let Err(err) = watcher.watch(&path, notify::RecursiveMode::Recursive) { + tracing::error!("{:?}", err); + } + } + if let Some(path) = LapceConfig::keymaps_file() { + if let Err(err) = watcher.watch(&path, notify::RecursiveMode::Recursive) { + tracing::error!("{:?}", err); + } + } + if let Some(path) = Directory::plugins_directory() { + if let Err(err) = watcher.watch(&path, notify::RecursiveMode::Recursive) { + tracing::error!("{:?}", err); + } + } + + let windows = scope.create_rw_signal(im::HashMap::new()); + let config = LapceConfig::load(&LapceWorkspace::default(), &[], &plugin_paths); + + // Restore scale from config + window_scale.set(config.ui.scale()); + + let config = scope.create_rw_signal(Arc::new(config)); + let app_data = AppData { + windows, + active_window: scope.create_rw_signal(WindowId::from_raw(0)), + window_scale, + app_terminated: scope.create_rw_signal(false), + watcher: Arc::new(watcher), + latest_release, + app_command, + tracing_handle: reload_handle, + config, + plugin_paths, + }; + + let app = app_data.create_windows(db.clone(), cli.paths); + + { + let app_data = app_data.clone(); + let notification = create_signal_from_channel(rx); + create_effect(move |_| { + if notification.get().is_some() { + tracing::debug!("notification reload_config"); + app_data.reload_config(); + } + }); + } + + { + let cx = Scope::new(); + let app_data = app_data.clone(); + let send = create_ext_action(cx, move |updated| { + if updated { + trace!( + TraceLevel::INFO, + "grammar or query got updated, reset highlight configs" + ); + reset_highlight_configs(); + for (_, window) in app_data.windows.get_untracked() { + for (_, tab) in window.window_tabs.get_untracked() { + for (_, doc) in tab.main_split.docs.get_untracked() { + doc.syntax.update(|syntax| { + *syntax = Syntax::from_language(syntax.language); + }); + doc.trigger_syntax_change(None); + } + } + } + } + }); + std::thread::Builder::new() + .name("FindGrammar".to_owned()) + .spawn(move || { + use self::grammars::*; + let updated = match find_grammar_release() { + Ok(release) => { + let mut updated = false; + match fetch_grammars(&release) { + Err(e) => { + trace!( + TraceLevel::ERROR, + "failed to fetch grammars: {e}" + ); + } + Ok(u) => updated |= u, + } + match fetch_queries(&release) { + Err(e) => { + trace!( + TraceLevel::ERROR, + "failed to fetch grammars: {e}" + ); + } + Ok(u) => updated |= u, + } + updated + } + Err(e) => { + trace!( + TraceLevel::ERROR, + "failed to obtain release info: {e}" + ); + false + } + }; + send(updated); + }) + .unwrap(); + } + + #[cfg(feature = "updater")] + { + let (tx, rx) = sync_channel(1); + let notification = create_signal_from_channel(rx); + let latest_release = app_data.latest_release; + create_effect(move |_| { + if let Some(release) = notification.get() { + latest_release.set(Arc::new(Some(release))); + } + }); + std::thread::Builder::new() + .name("LapceUpdater".to_owned()) + .spawn(move || { + loop { + if let Ok(release) = crate::update::get_latest_release() { + if let Err(err) = tx.send(release) { + tracing::error!("{:?}", err); + } + } + std::thread::sleep(std::time::Duration::from_secs(60 * 60)); + } + }) + .unwrap(); + } + + { + let (tx, rx) = sync_channel(1); + let notification = create_signal_from_channel(rx); + let app_data = app_data.clone(); + create_effect(move |_| { + if let Some(CoreNotification::OpenPaths { paths }) = notification.get() { + if let Some(window_tab) = app_data.active_window_tab() { + window_tab.open_paths(&paths); + // focus window after open doc + floem::action::focus_window(); + } + } + }); + std::thread::Builder::new() + .name("ListenLocalSocket".to_owned()) + .spawn(move || { + if let Err(err) = listen_local_socket(tx) { + tracing::error!("{:?}", err); + } + }) + .unwrap(); + } + + { + let app_data = app_data.clone(); + app_data.app_command.listen(move |command| { + app_data.run_app_command(command); + }); + } + + app.on_event(move |event| match event { + floem::AppEvent::WillTerminate => { + app_data.app_terminated.set(true); + if let Err(err) = db.insert_app(app_data.clone()) { + tracing::error!("{:?}", err); + } + } + floem::AppEvent::Reopen { + has_visible_windows, + } => { + if !has_visible_windows { + app_data.new_window(None); + } + } + }) + .run(); +} + +/// Uses a login shell to load the correct shell environment for the current user. +pub fn load_shell_env() { + use std::process::Command; + + use tracing::warn; + + #[cfg(not(windows))] + let shell = match std::env::var("SHELL") { + Ok(s) => s, + Err(error) => { + // Shell variable is not set, so we can't determine the correct shell executable. + trace!( + TraceLevel::ERROR, + "Failed to obtain shell environment: {error}" + ); + return; + } + }; + + #[cfg(windows)] + let shell = "powershell"; + + let mut command = Command::new(shell); + + #[cfg(not(windows))] + command.args(["--login", "-c", "printenv"]); + + #[cfg(windows)] + command.args([ + "-Command", + "Get-ChildItem env: | ForEach-Object { \"{0}={1}\" -f $_.Name, $_.Value }", + ]); + + #[cfg(windows)] + command.creation_flags(windows::Win32::System::Threading::CREATE_NO_WINDOW); + + let env = match command.output() { + Ok(output) => String::from_utf8(output.stdout).unwrap_or_default(), + + Err(error) => { + trace!( + TraceLevel::ERROR, + "Failed to obtain shell environment: {error}" + ); + return; + } + }; + + env.split('\n') + .filter_map(|line| line.split_once('=')) + .for_each(|(key, value)| unsafe { + let value = value.trim_matches('\r'); + if let Ok(v) = std::env::var(key) { + if v != value { + warn!("Overwriting '{key}', previous value: '{v}', new value '{value}'"); + } + }; + std::env::set_var(key, value); + }) +} + +pub fn get_socket() -> Result { + let local_socket = Directory::local_socket() + .ok_or_else(|| anyhow!("can't get local socket folder"))?; + let socket = + interprocess::local_socket::LocalSocketStream::connect(local_socket)?; + Ok(socket) +} + +pub fn try_open_in_existing_process( + mut socket: interprocess::local_socket::LocalSocketStream, + paths: &[PathObject], +) -> Result<()> { + let msg: CoreMessage = RpcMessage::Notification(CoreNotification::OpenPaths { + paths: paths.to_vec(), + }); + lapce_rpc::stdio::write_msg(&mut socket, msg)?; + + let (tx, rx) = crossbeam_channel::bounded(1); + std::thread::spawn(move || { + let mut buf = [0; 100]; + let received = if let Ok(n) = socket.read(&mut buf) { + &buf[..n] == b"received" + } else { + false + }; + tx.send(received) + }); + + let received = rx.recv_timeout(std::time::Duration::from_millis(500))?; + if !received { + return Err(anyhow!("didn't receive response")); + } + + Ok(()) +} + +fn listen_local_socket(tx: SyncSender) -> Result<()> { + let local_socket = Directory::local_socket() + .ok_or_else(|| anyhow!("can't get local socket folder"))?; + if local_socket.exists() { + if let Err(err) = std::fs::remove_file(&local_socket) { + tracing::error!("{:?}", err); + } + } + let socket = + interprocess::local_socket::LocalSocketListener::bind(local_socket)?; + + for stream in socket.incoming().flatten() { + let tx = tx.clone(); + std::thread::spawn(move || -> Result<()> { + let mut reader = BufReader::new(stream); + loop { + let msg: Option = + lapce_rpc::stdio::read_msg(&mut reader)?; + + if let Some(RpcMessage::Notification(msg)) = msg { + tx.send(msg)?; + } else { + trace!(TraceLevel::ERROR, "Unhandled message: {msg:?}"); + } + + let stream_ref = reader.get_mut(); + if let Err(err) = stream_ref.write_all(b"received") { + tracing::error!("{:?}", err); + } + if let Err(err) = stream_ref.flush() { + tracing::error!("{:?}", err); + } + } + }); + } + Ok(()) +} + +pub fn window_menu( + lapce_command: Listener, + workbench_command: Listener, +) -> Menu { + Menu::new("Lapce") + .entry({ + let mut menu = Menu::new("Lapce") + .entry(MenuItem::new("About Lapce").action(move || { + workbench_command.send(LapceWorkbenchCommand::ShowAbout) + })) + .separator() + .entry( + Menu::new("Settings...") + .entry(MenuItem::new("Open Settings").action(move || { + workbench_command + .send(LapceWorkbenchCommand::OpenSettings); + })) + .entry(MenuItem::new("Open Keyboard Shortcuts").action( + move || { + workbench_command.send( + LapceWorkbenchCommand::OpenKeyboardShortcuts, + ); + }, + )), + ) + .separator() + .entry(MenuItem::new("Quit Lapce").action(move || { + workbench_command.send(LapceWorkbenchCommand::Quit); + })); + if cfg!(target_os = "macos") { + menu = menu + .separator() + .entry(MenuItem::new("Hide Lapce")) + .entry(MenuItem::new("Hide Others")) + .entry(MenuItem::new("Show All")) + } + menu + }) + .separator() + .entry( + Menu::new("File") + .entry(MenuItem::new("New File").action(move || { + workbench_command.send(LapceWorkbenchCommand::NewFile); + })) + .separator() + .entry(MenuItem::new("Open").action(move || { + workbench_command.send(LapceWorkbenchCommand::OpenFile); + })) + .entry(MenuItem::new("Open Folder").action(move || { + workbench_command.send(LapceWorkbenchCommand::OpenFolder); + })) + .separator() + .entry(MenuItem::new("Save").action(move || { + lapce_command.send(LapceCommand { + kind: CommandKind::Focus(FocusCommand::Save), + data: None, + }); + })) + .entry(MenuItem::new("Save All").action(move || { + workbench_command.send(LapceWorkbenchCommand::SaveAll); + })) + .separator() + .entry(MenuItem::new("Close Folder").action(move || { + workbench_command.send(LapceWorkbenchCommand::CloseFolder); + })) + .entry(MenuItem::new("Close Window").action(move || { + workbench_command.send(LapceWorkbenchCommand::CloseWindow); + })), + ) + .entry( + Menu::new("Edit") + .entry(MenuItem::new("Cut").action(move || { + lapce_command.send(LapceCommand { + kind: CommandKind::Edit(EditCommand::ClipboardCut), + data: None, + }); + })) + .entry(MenuItem::new("Copy").action(move || { + lapce_command.send(LapceCommand { + kind: CommandKind::Edit(EditCommand::ClipboardCopy), + data: None, + }); + })) + .entry(MenuItem::new("Paste").action(move || { + lapce_command.send(LapceCommand { + kind: CommandKind::Edit(EditCommand::ClipboardPaste), + data: None, + }); + })) + .separator() + .entry(MenuItem::new("Undo").action(move || { + lapce_command.send(LapceCommand { + kind: CommandKind::Edit(EditCommand::Undo), + data: None, + }); + })) + .entry(MenuItem::new("Redo").action(move || { + lapce_command.send(LapceCommand { + kind: CommandKind::Edit(EditCommand::Redo), + data: None, + }); + })) + .separator() + .entry(MenuItem::new("Find").action(move || { + lapce_command.send(LapceCommand { + kind: CommandKind::Focus(FocusCommand::Search), + data: None, + }); + })), + ) +} +fn tab_secondary_click( + internal_command: Listener, + editor_tab_id: EditorTabId, + child: EditorTabChild, +) { + let mut menu = Menu::new(""); + let child_other = child.clone(); + let child_right = child.clone(); + let child_left = child.clone(); + menu = menu + .entry(MenuItem::new("Close").action(move || { + internal_command.send(InternalCommand::EditorTabChildClose { + editor_tab_id, + child: child.clone(), + }); + })) + .entry(MenuItem::new("Close Other Tabs").action(move || { + internal_command.send(InternalCommand::EditorTabCloseByKind { + editor_tab_id, + child: child_other.clone(), + kind: TabCloseKind::CloseOther, + }); + })) + .entry(MenuItem::new("Close All Tabs").action(move || { + internal_command.send(InternalCommand::EditorTabClose { editor_tab_id }); + })) + .entry(MenuItem::new("Close Tabs to the Right").action(move || { + internal_command.send(InternalCommand::EditorTabCloseByKind { + editor_tab_id, + child: child_right.clone(), + kind: TabCloseKind::CloseToRight, + }); + })) + .entry(MenuItem::new("Close Tabs to the Left").action(move || { + internal_command.send(InternalCommand::EditorTabCloseByKind { + editor_tab_id, + child: child_left.clone(), + kind: TabCloseKind::CloseToLeft, + }); + })); + show_context_menu(menu, None); +} diff --git a/lapce-app/src/app/grammars.rs b/lapce-app/src/app/grammars.rs new file mode 100644 index 0000000..511ecde --- /dev/null +++ b/lapce-app/src/app/grammars.rs @@ -0,0 +1,141 @@ +use std::{ + env, + fs::{self}, + path::PathBuf, +}; + +use anyhow::{Context, Result, anyhow}; +use lapce_core::directory::Directory; + +use crate::{tracing::*, update::ReleaseInfo}; + +fn get_github_api(url: &str) -> Result { + let user_agent = format!("Lapce/{}", lapce_core::meta::VERSION); + let resp = lapce_proxy::get_url(url, Some(user_agent.as_str()))?; + if !resp.status().is_success() { + return Err(anyhow!("get release info failed {}", resp.text()?)); + } + + Ok(resp.text()?) +} + +pub fn find_grammar_release() -> Result { + let releases: Vec = serde_json::from_str(&get_github_api( + "https://api.github.com/repos/lapce/tree-sitter-grammars/releases?per_page=100", + ).context("Failed to retrieve releases for tree-sitter-grammars")?)?; + + use lapce_core::meta::{RELEASE, ReleaseType, VERSION}; + + let releases = releases + .into_iter() + .filter_map(|f| { + if matches!(RELEASE, ReleaseType::Debug | ReleaseType::Nightly) { + return Some(f); + } + + let tag_name = if f.tag_name.starts_with('v') { + f.tag_name.trim_start_matches('v') + } else { + f.tag_name.as_str() + }; + + use std::cmp::Ordering; + + use semver::Version; + + let sv = Version::parse(tag_name).ok()?; + let version = Version::parse(VERSION).ok()?; + + if matches!(sv.cmp_precedence(&version), Ordering::Equal) { + Some(f) + } else { + None + } + }) + .collect::>(); + + let Some(release) = releases.first() else { + return Err(anyhow!("Couldn't find any release")); + }; + + Ok(release.to_owned()) +} + +pub fn fetch_grammars(release: &ReleaseInfo) -> Result { + let dir = Directory::grammars_directory() + .ok_or_else(|| anyhow!("can't get grammars directory"))?; + + let file_name = format!("grammars-{}-{}", env::consts::OS, env::consts::ARCH); + + let updated = download_release(dir, release, &file_name)?; + + trace!(TraceLevel::INFO, "Successfully downloaded grammars"); + + Ok(updated) +} + +pub fn fetch_queries(release: &ReleaseInfo) -> Result { + let dir = Directory::queries_directory() + .ok_or_else(|| anyhow!("can't get queries directory"))?; + + let file_name = "queries"; + + let updated = download_release(dir, release, file_name)?; + + trace!(TraceLevel::INFO, "Successfully downloaded queries"); + + Ok(updated) +} + +fn download_release( + dir: PathBuf, + release: &ReleaseInfo, + file_name: &str, +) -> Result { + if !dir.exists() { + fs::create_dir(&dir)?; + } + + let current_version = + fs::read_to_string(dir.join("version")).unwrap_or_default(); + let release_version = if release.tag_name == "nightly" { + format!("nightly-{}", &release.target_commitish[..7]) + } else { + release.tag_name.clone() + }; + + if release_version == current_version { + return Ok(false); + } + + for asset in &release.assets { + if asset.name.starts_with(file_name) { + let mut resp = lapce_proxy::get_url(&asset.browser_download_url, None)?; + if !resp.status().is_success() { + return Err(anyhow!("download file error {}", resp.text()?)); + } + + let file = tempfile::tempfile()?; + + { + use std::io::{Seek, Write}; + let file = &mut &file; + resp.copy_to(file)?; + file.flush()?; + file.rewind()?; + } + + if asset.name.ends_with(".zip") { + let mut archive = zip::ZipArchive::new(file)?; + archive.extract(&dir)?; + } else if asset.name.ends_with(".tar.zst") { + let mut archive = + tar::Archive::new(zstd::stream::read::Decoder::new(file)?); + archive.unpack(&dir)?; + } + + fs::write(dir.join("version"), &release_version)?; + } + } + Ok(true) +} diff --git a/lapce-app/src/app/logging.rs b/lapce-app/src/app/logging.rs new file mode 100644 index 0000000..bbc0586 --- /dev/null +++ b/lapce-app/src/app/logging.rs @@ -0,0 +1,153 @@ +use lapce_core::directory::Directory; +use tracing::level_filters::LevelFilter; +use tracing_appender::non_blocking::WorkerGuard; +use tracing_subscriber::{filter::Targets, reload::Handle}; + +use crate::tracing::*; + +#[inline(always)] +pub(super) fn logging() -> (Handle, Option) { + use tracing_subscriber::{filter, fmt, prelude::*, reload}; + + let (log_file, guard) = match Directory::logs_directory() + .and_then(|dir| { + tracing_appender::rolling::Builder::new() + .max_log_files(10) + .rotation(tracing_appender::rolling::Rotation::DAILY) + .filename_prefix("lapce") + .filename_suffix("log") + .build(dir) + .ok() + }) + .map(tracing_appender::non_blocking) + { + Some((log_file, guard)) => (Some(log_file), Some(guard)), + None => (None, None), + }; + + let log_file_filter_targets = filter::Targets::new() + .with_target("lapce_app", LevelFilter::DEBUG) + .with_target("lapce_proxy", LevelFilter::DEBUG) + .with_target("lapce_core", LevelFilter::DEBUG) + .with_default(LevelFilter::from_level(TraceLevel::INFO)); + let (log_file_filter, reload_handle) = + reload::Subscriber::new(log_file_filter_targets); + + let console_filter_targets = std::env::var("LAPCE_LOG") + .unwrap_or_default() + .parse::() + .unwrap_or_default(); + + let registry = tracing_subscriber::registry(); + if let Some(log_file) = log_file { + let file_layer = tracing_subscriber::fmt::subscriber() + .with_ansi(false) + .with_writer(log_file) + .with_filter(log_file_filter); + registry + .with(file_layer) + .with( + fmt::Subscriber::default() + .with_line_number(true) + .with_target(true) + .with_thread_names(true) + .with_filter(console_filter_targets), + ) + .init(); + } else { + registry + .with(fmt::Subscriber::default().with_filter(console_filter_targets)) + .init(); + }; + + (reload_handle, guard) +} + +pub(super) fn panic_hook() { + std::panic::set_hook(Box::new(move |info| { + let thread = std::thread::current(); + let thread = thread.name().unwrap_or("main"); + let backtrace = backtrace::Backtrace::new(); + + let payload = if let Some(s) = info.payload().downcast_ref::<&str>() { + s + } else { + "" + }; + + match info.location() { + Some(loc) => { + trace!( + target: "lapce_app::panic_hook", + TraceLevel::ERROR, + "thread {thread} panicked at {} | file://./{}:{}:{}\n{:?}", + payload, + loc.file(), loc.line(), loc.column(), + backtrace, + ); + } + None => { + trace!( + target: "lapce_app::panic_hook", + TraceLevel::ERROR, + "thread {thread} panicked at {}\n{:?}", + payload, + backtrace, + ); + } + } + + #[cfg(windows)] + error_modal("Error", &info.to_string()); + + #[cfg(unix)] + error_notification("Error", &info.to_string()); + })) +} + +#[cfg(windows)] +pub(super) fn error_modal(title: &str, msg: &str) -> i32 { + use std::{ffi::OsStr, iter::once, mem, os::windows::prelude::OsStrExt}; + + use windows::Win32::UI::WindowsAndMessaging::{ + MB_ICONERROR, MB_SYSTEMMODAL, MessageBoxW, + }; + + let result: i32; + + let title = OsStr::new(title) + .encode_wide() + .chain(once(0u16)) + .collect::>(); + let msg = OsStr::new(msg) + .encode_wide() + .chain(once(0u16)) + .collect::>(); + unsafe { + result = MessageBoxW( + mem::zeroed(), + msg.as_ptr(), + title.as_ptr(), + MB_ICONERROR | MB_SYSTEMMODAL, + ); + } + + result +} + +#[cfg(unix)] +pub fn error_notification(title: &str, msg: &str) { + let res = std::process::Command::new("notify-send") + .args([ + "-a", + "dev.lapce.lapce", + "-w", + "-n", + "dev.lapce.lapce", + "-c", + "error", + title, + msg, + ]) + .spawn(); +} diff --git a/lapce-app/src/bin/lapce.rs b/lapce-app/src/bin/lapce.rs new file mode 100644 index 0000000..2efd77d --- /dev/null +++ b/lapce-app/src/bin/lapce.rs @@ -0,0 +1,7 @@ +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +use lapce_app::app; + +pub fn main() { + app::launch(); +} diff --git a/lapce-app/src/code_action.rs b/lapce-app/src/code_action.rs new file mode 100644 index 0000000..eca5285 --- /dev/null +++ b/lapce-app/src/code_action.rs @@ -0,0 +1,233 @@ +use std::rc::Rc; + +use floem::{ + keyboard::Modifiers, + peniko::kurbo::Rect, + reactive::{RwSignal, Scope, SignalGet, SignalUpdate}, +}; +use lapce_core::{command::FocusCommand, mode::Mode, movement::Movement}; +use lapce_rpc::plugin::PluginId; +use lsp_types::CodeActionOrCommand; + +use crate::{ + command::{CommandExecuted, CommandKind, InternalCommand}, + keypress::{KeyPressFocus, condition::Condition}, + window_tab::{CommonData, Focus}, +}; + +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum CodeActionStatus { + Inactive, + Active, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct ScoredCodeActionItem { + pub item: CodeActionOrCommand, + pub plugin_id: PluginId, + pub score: i64, + pub indices: Vec, +} + +impl ScoredCodeActionItem { + pub fn title(&self) -> &str { + match &self.item { + CodeActionOrCommand::Command(c) => &c.title, + CodeActionOrCommand::CodeAction(c) => &c.title, + } + } +} + +#[derive(Clone, Debug)] +pub struct CodeActionData { + pub status: RwSignal, + pub active: RwSignal, + pub request_id: usize, + pub input_id: usize, + pub offset: usize, + pub items: im::Vector, + pub filtered_items: im::Vector, + pub layout_rect: Rect, + pub mouse_click: bool, + pub common: Rc, +} + +impl KeyPressFocus for CodeActionData { + fn get_mode(&self) -> Mode { + Mode::Insert + } + + fn check_condition(&self, condition: Condition) -> bool { + matches!(condition, Condition::ListFocus | Condition::ModalFocus) + } + + fn run_command( + &self, + command: &crate::command::LapceCommand, + _count: Option, + _mods: Modifiers, + ) -> crate::command::CommandExecuted { + match &command.kind { + CommandKind::Workbench(_) => {} + CommandKind::Edit(_) => {} + CommandKind::Move(_) => {} + CommandKind::Scroll(_) => {} + CommandKind::Focus(cmd) => { + self.run_focus_command(cmd); + } + CommandKind::MotionMode(_) => {} + CommandKind::MultiSelection(_) => {} + } + CommandExecuted::Yes + } + + fn receive_char(&self, _c: &str) {} +} + +impl CodeActionData { + pub fn new(cx: Scope, common: Rc) -> Self { + let status = cx.create_rw_signal(CodeActionStatus::Inactive); + let active = cx.create_rw_signal(0); + + let code_action = Self { + status, + active, + request_id: 0, + input_id: 0, + offset: 0, + items: im::Vector::new(), + filtered_items: im::Vector::new(), + layout_rect: Rect::ZERO, + mouse_click: false, + common, + }; + + { + let code_action = code_action.clone(); + cx.create_effect(move |_| { + let focus = code_action.common.focus.get(); + if focus != Focus::CodeAction + && code_action.status.get_untracked() + != CodeActionStatus::Inactive + { + code_action.cancel(); + } + }) + } + + code_action + } + + pub fn next(&self) { + let active = self.active.get_untracked(); + let new = + Movement::Down.update_index(active, self.filtered_items.len(), 1, true); + self.active.set(new); + } + + pub fn previous(&self) { + let active = self.active.get_untracked(); + let new = + Movement::Up.update_index(active, self.filtered_items.len(), 1, true); + self.active.set(new); + } + + pub fn next_page(&self) { + let config = self.common.config.get_untracked(); + let count = ((self.layout_rect.size().height + / config.editor.line_height() as f64) + .floor() as usize) + .saturating_sub(1); + let active = self.active.get_untracked(); + let new = Movement::Down.update_index( + active, + self.filtered_items.len(), + count, + false, + ); + self.active.set(new); + } + + pub fn previous_page(&self) { + let config = self.common.config.get_untracked(); + let count = ((self.layout_rect.size().height + / config.editor.line_height() as f64) + .floor() as usize) + .saturating_sub(1); + let active = self.active.get_untracked(); + let new = Movement::Up.update_index( + active, + self.filtered_items.len(), + count, + false, + ); + self.active.set(new); + } + + pub fn show( + &mut self, + plugin_id: PluginId, + code_actions: im::Vector, + offset: usize, + mouse_click: bool, + ) { + self.active.set(0); + self.status.set(CodeActionStatus::Active); + self.offset = offset; + self.mouse_click = mouse_click; + self.request_id += 1; + self.items = code_actions + .into_iter() + .map(|code_action| ScoredCodeActionItem { + item: code_action, + plugin_id, + score: 0, + indices: Vec::new(), + }) + .collect(); + self.filtered_items = self.items.clone(); + self.common.focus.set(Focus::CodeAction); + } + + fn cancel(&self) { + self.status.set(CodeActionStatus::Inactive); + self.common.focus.set(Focus::Workbench); + } + + pub fn select(&self) { + if let Some(item) = self.filtered_items.get(self.active.get_untracked()) { + self.common + .internal_command + .send(InternalCommand::RunCodeAction { + plugin_id: item.plugin_id, + action: item.item.clone(), + }); + } + self.cancel(); + } + + fn run_focus_command(&self, cmd: &FocusCommand) -> CommandExecuted { + match cmd { + FocusCommand::ModalClose => { + self.cancel(); + } + FocusCommand::ListNext => { + self.next(); + } + FocusCommand::ListNextPage => { + self.next_page(); + } + FocusCommand::ListPrevious => { + self.previous(); + } + FocusCommand::ListPreviousPage => { + self.previous_page(); + } + FocusCommand::ListSelect => { + self.select(); + } + _ => return CommandExecuted::No, + } + CommandExecuted::Yes + } +} diff --git a/lapce-app/src/code_lens.rs b/lapce-app/src/code_lens.rs new file mode 100644 index 0000000..960f938 --- /dev/null +++ b/lapce-app/src/code_lens.rs @@ -0,0 +1,100 @@ +use std::rc::Rc; + +use lapce_rpc::dap_types::{ConfigSource, RunDebugConfig}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +use crate::{command::InternalCommand, debug::RunDebugMode, window_tab::CommonData}; + +#[derive(Serialize, Deserialize)] +struct CargoArgs { + #[serde(rename = "cargoArgs")] + pub cargo_args: Vec, + + #[serde(rename = "cargoExtraArgs")] + pub cargo_extra_args: Vec, + + #[serde(rename = "executableArgs")] + pub executable_args: Vec, +} + +#[derive(Serialize, Deserialize)] +struct RustArgs { + pub args: CargoArgs, + pub kind: String, + pub label: String, + pub location: lsp_types::LocationLink, +} + +#[derive(Clone)] +pub struct CodeLensData { + common: Rc, +} + +impl CodeLensData { + pub fn new(common: Rc) -> Self { + Self { common } + } + + pub fn run(&self, command: &str, args: Vec) { + match command { + "rust-analyzer.runSingle" | "rust-analyzer.debugSingle" => { + let mode = if command == "rust-analyzer.runSingle" { + RunDebugMode::Run + } else { + RunDebugMode::Debug + }; + if let Some(config) = self.get_rust_command_config(&args, mode) { + self.common + .internal_command + .send(InternalCommand::RunAndDebug { mode, config }); + } + } + _ => { + tracing::debug!("todo {:}", command); + } + } + } + + fn get_rust_command_config( + &self, + args: &[Value], + mode: RunDebugMode, + ) -> Option { + if let Some(args) = args.first() { + let Ok(mut cargo_args) = + serde_json::from_value::(args.clone()) + else { + tracing::error!("serde error"); + return None; + }; + cargo_args + .args + .cargo_args + .extend(cargo_args.args.cargo_extra_args); + if !cargo_args.args.executable_args.is_empty() { + cargo_args.args.cargo_args.push("--".to_string()); + cargo_args + .args + .cargo_args + .extend(cargo_args.args.executable_args); + } + Some(RunDebugConfig { + ty: None, + name: cargo_args.label, + program: cargo_args.kind, + args: Some(cargo_args.args.cargo_args), + cwd: None, + env: None, + prelaunch: None, + debug_command: None, + dap_id: Default::default(), + tracing_output: mode == RunDebugMode::Debug, + config_source: ConfigSource::CodeLens, + }) + } else { + tracing::error!("no args"); + None + } + } +} diff --git a/lapce-app/src/command.rs b/lapce-app/src/command.rs new file mode 100644 index 0000000..c0cc3b8 --- /dev/null +++ b/lapce-app/src/command.rs @@ -0,0 +1,818 @@ +use std::{path::PathBuf, rc::Rc}; + +pub use floem::views::editor::command::CommandExecuted; +use floem::{ + ViewId, keyboard::Modifiers, peniko::kurbo::Vec2, + views::editor::command::Command, +}; +use indexmap::IndexMap; +use lapce_core::command::{ + EditCommand, FocusCommand, MotionModeCommand, MoveCommand, + MultiSelectionCommand, ScrollCommand, +}; +use lapce_rpc::{ + dap_types::{DapId, RunDebugConfig}, + plugin::{PluginId, VoltID}, + proxy::ProxyStatus, + terminal::{TermId, TerminalProfile}, +}; +use lsp_types::{CodeActionOrCommand, Position, WorkspaceEdit}; +use serde_json::Value; +use strum::{EnumMessage, IntoEnumIterator}; +use strum_macros::{Display, EnumIter, EnumString, IntoStaticStr}; + +use crate::{ + alert::AlertButton, + debug::RunDebugMode, + doc::Doc, + editor::location::EditorLocation, + editor_tab::EditorTabChild, + id::EditorTabId, + main_split::{SplitDirection, SplitMoveDirection, TabCloseKind}, + workspace::LapceWorkspace, +}; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct LapceCommand { + pub kind: CommandKind, + pub data: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum CommandKind { + Workbench(LapceWorkbenchCommand), + Edit(EditCommand), + Move(MoveCommand), + Scroll(ScrollCommand), + Focus(FocusCommand), + MotionMode(MotionModeCommand), + MultiSelection(MultiSelectionCommand), +} + +impl CommandKind { + pub fn desc(&self) -> Option<&'static str> { + match &self { + CommandKind::Workbench(cmd) => cmd.get_message(), + CommandKind::Edit(cmd) => cmd.get_message(), + CommandKind::Move(cmd) => cmd.get_message(), + CommandKind::Scroll(cmd) => cmd.get_message(), + CommandKind::Focus(cmd) => cmd.get_message(), + CommandKind::MotionMode(cmd) => cmd.get_message(), + CommandKind::MultiSelection(cmd) => cmd.get_message(), + } + } + + pub fn str(&self) -> &'static str { + match &self { + CommandKind::Workbench(cmd) => cmd.into(), + CommandKind::Edit(cmd) => cmd.into(), + CommandKind::Move(cmd) => cmd.into(), + CommandKind::Scroll(cmd) => cmd.into(), + CommandKind::Focus(cmd) => cmd.into(), + CommandKind::MotionMode(cmd) => cmd.into(), + CommandKind::MultiSelection(cmd) => cmd.into(), + } + } +} +impl From for CommandKind { + fn from(cmd: Command) -> Self { + use Command::*; + match cmd { + Edit(edit) => CommandKind::Edit(edit), + Move(movement) => CommandKind::Move(movement), + Scroll(scroll) => CommandKind::Scroll(scroll), + MotionMode(motion_mode) => CommandKind::MotionMode(motion_mode), + MultiSelection(multi_selection) => { + CommandKind::MultiSelection(multi_selection) + } + } + } +} + +pub fn lapce_internal_commands() -> IndexMap { + let mut commands = IndexMap::new(); + + for c in LapceWorkbenchCommand::iter() { + let command = LapceCommand { + kind: CommandKind::Workbench(c.clone()), + data: None, + }; + commands.insert(c.to_string(), command); + } + + for c in EditCommand::iter() { + let command = LapceCommand { + kind: CommandKind::Edit(c.clone()), + data: None, + }; + commands.insert(c.to_string(), command); + } + + for c in MoveCommand::iter() { + let command = LapceCommand { + kind: CommandKind::Move(c.clone()), + data: None, + }; + commands.insert(c.to_string(), command); + } + + for c in ScrollCommand::iter() { + let command = LapceCommand { + kind: CommandKind::Scroll(c.clone()), + data: None, + }; + commands.insert(c.to_string(), command); + } + + for c in FocusCommand::iter() { + let command = LapceCommand { + kind: CommandKind::Focus(c.clone()), + data: None, + }; + commands.insert(c.to_string(), command); + } + + for c in MotionModeCommand::iter() { + let command = LapceCommand { + kind: CommandKind::MotionMode(c.clone()), + data: None, + }; + commands.insert(c.to_string(), command); + } + + for c in MultiSelectionCommand::iter() { + let command = LapceCommand { + kind: CommandKind::MultiSelection(c.clone()), + data: None, + }; + commands.insert(c.to_string(), command); + } + + commands +} + +#[derive( + Display, + EnumString, + EnumIter, + Clone, + PartialEq, + Eq, + Debug, + EnumMessage, + IntoStaticStr, +)] +pub enum LapceWorkbenchCommand { + #[strum(serialize = "enable_modal_editing")] + #[strum(message = "Enable Modal Editing")] + EnableModal, + + #[strum(serialize = "disable_modal_editing")] + #[strum(message = "Disable Modal Editing")] + DisableModal, + + #[strum(serialize = "open_folder")] + #[strum(message = "Open Folder")] + OpenFolder, + + #[strum(serialize = "close_folder")] + #[strum(message = "Close Folder")] + CloseFolder, + + #[strum(serialize = "open_file")] + #[strum(message = "Open File")] + OpenFile, + + #[strum(serialize = "show_call_hierarchy")] + #[strum(message = "Show Call Hierarchy")] + ShowCallHierarchy, + + #[strum(serialize = "find_references")] + #[strum(message = "Find References")] + FindReferences, + + #[strum(serialize = "go_to_implementation")] + #[strum(message = "Go to Implementation")] + GoToImplementation, + + #[strum(serialize = "reveal_in_panel")] + #[strum(message = "Reveal in Panel")] + RevealInPanel, + + #[strum(serialize = "source_control_open_active_file_remote_url")] + #[strum(message = "Source Control: Open Remote File Url")] + SourceControlOpenActiveFileRemoteUrl, + + #[cfg(not(target_os = "macos"))] + #[strum(serialize = "reveal_in_file_explorer")] + #[strum(message = "Reveal in System File Explorer")] + RevealInFileExplorer, + + #[cfg(target_os = "macos")] + #[strum(serialize = "reveal_in_file_explorer")] + #[strum(message = "Reveal in Finder")] + RevealInFileExplorer, + + #[strum(serialize = "run_in_terminal")] + #[strum(message = "Run in Terminal")] + RunInTerminal, + + #[strum(serialize = "reveal_active_file_in_file_explorer")] + #[strum(message = "Reveal Active File in File Explorer")] + RevealActiveFileInFileExplorer, + + #[strum(serialize = "open_ui_inspector")] + #[strum(message = "Open Internal UI Inspector")] + OpenUIInspector, + + #[strum(serialize = "show_env")] + #[strum(message = "Show Environment")] + ShowEnvironment, + + #[strum(serialize = "change_color_theme")] + #[strum(message = "Change Color Theme")] + ChangeColorTheme, + + #[strum(serialize = "change_icon_theme")] + #[strum(message = "Change Icon Theme")] + ChangeIconTheme, + + #[strum(serialize = "open_settings")] + #[strum(message = "Open Settings")] + OpenSettings, + + #[strum(serialize = "open_settings_file")] + #[strum(message = "Open Settings File")] + OpenSettingsFile, + + #[strum(serialize = "open_settings_directory")] + #[strum(message = "Open Settings Directory")] + OpenSettingsDirectory, + + #[strum(serialize = "open_theme_color_settings")] + #[strum(message = "Open Theme Color Settings")] + OpenThemeColorSettings, + + #[strum(serialize = "open_keyboard_shortcuts")] + #[strum(message = "Open Keyboard Shortcuts")] + OpenKeyboardShortcuts, + + #[strum(serialize = "open_keyboard_shortcuts_file")] + #[strum(message = "Open Keyboard Shortcuts File")] + OpenKeyboardShortcutsFile, + + #[strum(serialize = "open_log_file")] + #[strum(message = "Open Log File")] + OpenLogFile, + + #[strum(serialize = "open_logs_directory")] + #[strum(message = "Open Logs Directory")] + OpenLogsDirectory, + + #[strum(serialize = "open_proxy_directory")] + #[strum(message = "Open Proxy Directory")] + OpenProxyDirectory, + + #[strum(serialize = "open_themes_directory")] + #[strum(message = "Open Themes Directory")] + OpenThemesDirectory, + + #[strum(serialize = "open_plugins_directory")] + #[strum(message = "Open Plugins Directory")] + OpenPluginsDirectory, + + #[strum(serialize = "open_grammars_directory")] + #[strum(message = "Open Grammars Directory")] + OpenGrammarsDirectory, + + #[strum(serialize = "open_queries_directory")] + #[strum(message = "Open Queries Directory")] + OpenQueriesDirectory, + + #[strum(serialize = "zoom_in")] + #[strum(message = "Zoom In")] + ZoomIn, + + #[strum(serialize = "zoom_out")] + #[strum(message = "Zoom Out")] + ZoomOut, + + #[strum(serialize = "zoom_reset")] + #[strum(message = "Reset Zoom")] + ZoomReset, + + #[strum(serialize = "close_window_tab")] + #[strum(message = "Close Current Window Tab")] + CloseWindowTab, + + #[strum(serialize = "new_window_tab")] + #[strum(message = "Create New Window Tab")] + NewWindowTab, + + #[strum(serialize = "new_terminal_tab")] + #[strum(message = "Create New Terminal Tab")] + NewTerminalTab, + + #[strum(serialize = "close_terminal_tab")] + #[strum(message = "Close Terminal Tab")] + CloseTerminalTab, + + #[strum(serialize = "next_terminal_tab")] + #[strum(message = "Next Terminal Tab")] + NextTerminalTab, + + #[strum(serialize = "previous_terminal_tab")] + #[strum(message = "Previous Terminal Tab")] + PreviousTerminalTab, + + #[strum(serialize = "next_window_tab")] + #[strum(message = "Go To Next Window Tab")] + NextWindowTab, + + #[strum(serialize = "previous_window_tab")] + #[strum(message = "Go To Previous Window Tab")] + PreviousWindowTab, + + #[strum(serialize = "reload_window")] + #[strum(message = "Reload Window")] + ReloadWindow, + + #[strum(message = "New Window")] + #[strum(serialize = "new_window")] + NewWindow, + + #[strum(message = "Close Window")] + #[strum(serialize = "close_window")] + CloseWindow, + + #[strum(message = "New File")] + #[strum(serialize = "new_file")] + NewFile, + + #[strum(serialize = "connect_ssh_host")] + #[strum(message = "Connect to SSH Host")] + ConnectSshHost, + + #[cfg(windows)] + #[strum(serialize = "connect_wsl_host")] + #[strum(message = "Connect to WSL Host")] + ConnectWslHost, + + #[strum(serialize = "disconnect_remote")] + #[strum(message = "Disconnect From Remote")] + DisconnectRemote, + + #[strum(message = "Go To Line")] + #[strum(serialize = "palette.line")] + PaletteLine, + + #[strum(serialize = "palette")] + #[strum(message = "Go to File")] + Palette, + + #[strum(message = "Go To Symbol In File")] + #[strum(serialize = "palette.symbol")] + PaletteSymbol, + + #[strum(message = "Go To Symbol In Workspace")] + #[strum(serialize = "palette.workspace_symbol")] + PaletteWorkspaceSymbol, + + #[strum(message = "Command Palette")] + #[strum(serialize = "palette.command")] + PaletteCommand, + + #[strum(message = "Open Recent Workspace")] + #[strum(serialize = "palette.workspace")] + PaletteWorkspace, + + #[strum(message = "Run and Debug")] + #[strum(serialize = "palette.run_and_debug")] + PaletteRunAndDebug, + + #[strum(message = "Source Control: Checkout")] + #[strum(serialize = "palette.scm_references")] + PaletteSCMReferences, + + #[strum(message = "List Palette Types")] + #[strum(serialize = "palette.palette_help")] + PaletteHelp, + + #[strum(message = "List Palette Types and Files")] + #[strum(serialize = "palette.palette_help_and_file")] + PaletteHelpAndFile, + + #[strum(message = "Run and Debug Restart Current Running")] + #[strum(serialize = "palette.run_and_debug_restart")] + RunAndDebugRestart, + + #[strum(message = "Run and Debug Stop Current Running")] + #[strum(serialize = "palette.run_and_debug_stop")] + RunAndDebugStop, + + #[strum(serialize = "source_control.checkout_reference")] + CheckoutReference, + + #[strum(serialize = "toggle_maximized_panel")] + ToggleMaximizedPanel, + + #[strum(serialize = "hide_panel")] + HidePanel, + + #[strum(serialize = "show_panel")] + ShowPanel, + + /// Toggles the panel passed in parameter. + #[strum(serialize = "toggle_panel_focus")] + TogglePanelFocus, + + /// Toggles the panel passed in parameter. + #[strum(serialize = "toggle_panel_visual")] + TogglePanelVisual, + + #[strum(message = "Toggle Left Panel")] + #[strum(serialize = "toggle_panel_left_visual")] + TogglePanelLeftVisual, + + #[strum(message = "Toggle Right Panel")] + #[strum(serialize = "toggle_panel_right_visual")] + TogglePanelRightVisual, + + #[strum(message = "Toggle Bottom Panel")] + #[strum(serialize = "toggle_panel_bottom_visual")] + TogglePanelBottomVisual, + + // Focus toggle commands + #[strum(message = "Toggle Terminal Focus")] + #[strum(serialize = "toggle_terminal_focus")] + ToggleTerminalFocus, + + #[strum(serialize = "toggle_source_control_focus")] + ToggleSourceControlFocus, + + #[strum(message = "Toggle Plugin Focus")] + #[strum(serialize = "toggle_plugin_focus")] + TogglePluginFocus, + + #[strum(message = "Toggle File Explorer Focus")] + #[strum(serialize = "toggle_file_explorer_focus")] + ToggleFileExplorerFocus, + + #[strum(message = "Toggle Problem Focus")] + #[strum(serialize = "toggle_problem_focus")] + ToggleProblemFocus, + + #[strum(message = "Toggle Search Focus")] + #[strum(serialize = "toggle_search_focus")] + ToggleSearchFocus, + + // Visual toggle commands + #[strum(serialize = "toggle_terminal_visual")] + ToggleTerminalVisual, + + #[strum(serialize = "toggle_source_control_visual")] + ToggleSourceControlVisual, + + #[strum(serialize = "toggle_plugin_visual")] + TogglePluginVisual, + + #[strum(serialize = "toggle_file_explorer_visual")] + ToggleFileExplorerVisual, + + #[strum(serialize = "toggle_problem_visual")] + ToggleProblemVisual, + + #[strum(serialize = "toggle_debug_visual")] + ToggleDebugVisual, + + #[strum(serialize = "toggle_search_visual")] + ToggleSearchVisual, + + #[strum(serialize = "focus_editor")] + FocusEditor, + + #[strum(serialize = "focus_terminal")] + FocusTerminal, + + #[strum(message = "Source Control: Init")] + #[strum(serialize = "source_control_init")] + SourceControlInit, + + #[strum(serialize = "source_control_commit")] + SourceControlCommit, + + #[strum(message = "Source Control: Copy Remote File Url")] + #[strum(serialize = "source_control_copy_active_file_remote_url")] + SourceControlCopyActiveFileRemoteUrl, + + #[strum(message = "Source Control: Discard File Changes")] + #[strum(serialize = "source_control_discard_active_file_changes")] + SourceControlDiscardActiveFileChanges, + + #[strum(serialize = "source_control_discard_target_file_changes")] + SourceControlDiscardTargetFileChanges, + + #[strum(message = "Source Control: Discard Workspace Changes")] + #[strum(serialize = "source_control_discard_workspace_changes")] + SourceControlDiscardWorkspaceChanges, + + #[strum(serialize = "export_current_theme_settings")] + #[strum(message = "Export current settings to a theme file")] + ExportCurrentThemeSettings, + + #[strum(serialize = "install_theme")] + #[strum(message = "Install current theme file")] + InstallTheme, + + #[strum(serialize = "change_file_language")] + #[strum(message = "Change current file language")] + ChangeFileLanguage, + + #[strum(serialize = "change_file_line_ending")] + #[strum(message = "Change current file line ending")] + ChangeFileLineEnding, + + #[strum(serialize = "next_editor_tab")] + #[strum(message = "Next Editor Tab")] + NextEditorTab, + + #[strum(serialize = "previous_editor_tab")] + #[strum(message = "Previous Editor Tab")] + PreviousEditorTab, + + #[strum(serialize = "toggle_inlay_hints")] + #[strum(message = "Toggle Inlay Hints")] + ToggleInlayHints, + + #[strum(serialize = "restart_to_update")] + RestartToUpdate, + + #[strum(serialize = "show_about")] + #[strum(message = "About Lapce")] + ShowAbout, + + #[strum(message = "Save All Files")] + #[strum(serialize = "save_all")] + SaveAll, + + #[cfg(target_os = "macos")] + #[strum(message = "Install Lapce to PATH")] + #[strum(serialize = "install_to_path")] + InstallToPATH, + + #[cfg(target_os = "macos")] + #[strum(message = "Uninstall Lapce from PATH")] + #[strum(serialize = "uninstall_from_path")] + UninstallFromPATH, + + #[strum(serialize = "jump_location_backward")] + JumpLocationBackward, + + #[strum(serialize = "jump_location_forward")] + JumpLocationForward, + + #[strum(serialize = "jump_location_backward_local")] + JumpLocationBackwardLocal, + + #[strum(serialize = "jump_location_forward_local")] + JumpLocationForwardLocal, + + #[strum(message = "Next Error in Workspace")] + #[strum(serialize = "next_error")] + NextError, + + #[strum(message = "Previous Error in Workspace")] + #[strum(serialize = "previous_error")] + PreviousError, + + #[strum(message = "Diff Files")] + #[strum(serialize = "diff_files")] + DiffFiles, + + #[strum(serialize = "quit")] + #[strum(message = "Quit Editor")] + Quit, + + #[strum(serialize = "go_to_location")] + #[strum(message = "Go to Location")] + GoToLocation, + + #[strum(serialize = "add_run_debug_config")] + #[strum(message = "Add Run Debug Config")] + AddRunDebugConfig, +} + +#[derive(Clone, Debug)] +pub enum InternalCommand { + ReloadConfig, + OpenFile { + path: PathBuf, + }, + OpenAndConfirmedFile { + path: PathBuf, + }, + OpenFileInNewTab { + path: PathBuf, + }, + MakeConfirmed, + OpenFileChanges { + path: PathBuf, + }, + ReloadFileExplorer, + /// Test whether a file/directory can be created at that path + TestPathCreation { + new_path: PathBuf, + }, + FinishRenamePath { + current_path: PathBuf, + new_path: PathBuf, + }, + FinishNewNode { + is_dir: bool, + path: PathBuf, + }, + FinishDuplicate { + source: PathBuf, + path: PathBuf, + }, + GoToLocation { + location: EditorLocation, + }, + JumpToLocation { + location: EditorLocation, + }, + PaletteReferences { + references: Vec, + }, + SaveJumpLocation { + path: PathBuf, + offset: usize, + scroll_offset: Vec2, + }, + Split { + direction: SplitDirection, + editor_tab_id: EditorTabId, + }, + SplitMove { + direction: SplitMoveDirection, + editor_tab_id: EditorTabId, + }, + SplitExchange { + editor_tab_id: EditorTabId, + }, + NewTerminal { + profile: Option, + }, + SplitTerminal { + term_id: TermId, + }, + SplitTerminalPrevious { + term_id: TermId, + }, + SplitTerminalNext { + term_id: TermId, + }, + SplitTerminalExchange { + term_id: TermId, + }, + EditorTabClose { + editor_tab_id: EditorTabId, + }, + EditorTabChildClose { + editor_tab_id: EditorTabId, + child: EditorTabChild, + }, + EditorTabCloseByKind { + editor_tab_id: EditorTabId, + child: EditorTabChild, + kind: TabCloseKind, + }, + ShowCodeActions { + offset: usize, + mouse_click: bool, + plugin_id: PluginId, + code_actions: im::Vector, + }, + RunCodeAction { + plugin_id: PluginId, + action: CodeActionOrCommand, + }, + ApplyWorkspaceEdit { + edit: WorkspaceEdit, + }, + RunAndDebug { + mode: RunDebugMode, + config: RunDebugConfig, + }, + StartRename { + path: PathBuf, + placeholder: String, + start: usize, + position: Position, + }, + Search { + pattern: Option, + }, + FindEditorReceiveChar { + s: String, + }, + ReplaceEditorReceiveChar { + s: String, + }, + FindEditorCommand { + command: LapceCommand, + count: Option, + mods: Modifiers, + }, + ReplaceEditorCommand { + command: LapceCommand, + count: Option, + mods: Modifiers, + }, + FocusEditorTab { + editor_tab_id: EditorTabId, + }, + + SetColorTheme { + name: String, + /// Whether to save the theme to the config file + save: bool, + }, + SetIconTheme { + name: String, + /// Whether to save the theme to the config file + save: bool, + }, + SetModal { + modal: bool, + }, + UpdateLogLevel { + level: tracing_subscriber::filter::LevelFilter, + }, + OpenWebUri { + uri: String, + }, + ShowAlert { + title: String, + msg: String, + buttons: Vec, + }, + HideAlert, + SaveScratchDoc { + doc: Rc, + }, + SaveScratchDoc2 { + doc: Rc, + }, + UpdateProxyStatus { + status: ProxyStatus, + }, + DapFrameScopes { + dap_id: DapId, + frame_id: usize, + }, + OpenVoltView { + volt_id: VoltID, + }, + ResetBlinkCursor, + OpenDiffFiles { + left_path: PathBuf, + right_path: PathBuf, + }, + ExecuteProcess { + program: String, + arguments: Vec, + }, + ClearTerminalBuffer { + view_id: ViewId, + tab_index: usize, + terminal_index: usize, + }, + CallHierarchyIncoming { + item_id: ViewId, + }, + StopTerminal { + term_id: TermId, + }, + RestartTerminal { + term_id: TermId, + }, +} + +#[derive(Clone)] +pub enum WindowCommand { + SetWorkspace { + workspace: LapceWorkspace, + }, + CloseWorkspaceTab { + index: Option, + }, + NewWorkspaceTab { + workspace: LapceWorkspace, + end: bool, + }, + NextWorkspaceTab, + PreviousWorkspaceTab, + NewWindow, + CloseWindow, +} diff --git a/lapce-app/src/completion.rs b/lapce-app/src/completion.rs new file mode 100644 index 0000000..2016126 --- /dev/null +++ b/lapce-app/src/completion.rs @@ -0,0 +1,417 @@ +use std::{borrow::Cow, path::PathBuf, str::FromStr, sync::Arc}; + +use floem::{ + peniko::kurbo::Rect, + reactive::{ReadSignal, RwSignal, Scope, SignalGet, SignalUpdate, SignalWith}, + views::editor::{id::EditorId, text::Document}, +}; +use lapce_core::{ + buffer::rope_text::RopeText, movement::Movement, rope_text_pos::RopeTextPosition, +}; +use lapce_rpc::{plugin::PluginId, proxy::ProxyRpcHandler}; +use lsp_types::{ + CompletionItem, CompletionResponse, CompletionTextEdit, InsertTextFormat, + Position, +}; +use nucleo::Utf32Str; + +use crate::{config::LapceConfig, editor::EditorData, snippet::Snippet}; + +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum CompletionStatus { + Inactive, + Started, + Done, +} + +#[derive(Clone, PartialEq)] +pub struct ScoredCompletionItem { + pub item: CompletionItem, + pub plugin_id: PluginId, + pub score: u32, + pub label_score: u32, + pub indices: Vec, +} + +#[derive(Clone)] +pub struct CompletionData { + pub status: CompletionStatus, + /// The current request id. This is used to discard old requests. + pub request_id: usize, + /// An input id that is used for keeping track of whether the input has changed. + pub input_id: usize, + // TODO: A `PathBuf` has the issue that the proxy may not have the same format. + // TODO(minor): It might be nice to not require a path. LSPs cannot operate on scratch buffers + // as of now, but they might be allowed in the future. + pub path: PathBuf, + /// The offset that the completion is/was started at. Used for positioning the completion elem + pub offset: usize, + /// The active completion index in the list of filtered items + pub active: RwSignal, + /// The current input that the user has typed which is being sent for consideration by the LSP + pub input: String, + /// `(Input, CompletionItems)` + pub input_items: im::HashMap>, + /// The filtered items that are being displayed to the user + pub filtered_items: im::Vector, + /// The size of the completion element. + /// This is used for positioning the element. + /// As well, it is needed for some movement commands like page up/down that need to know the + /// height to compute how far to move. + pub layout_rect: Rect, + /// The editor id that was most recently used to trigger a completion. + pub latest_editor_id: Option, + /// Matcher for filtering the completion items + matcher: RwSignal, + config: ReadSignal>, +} + +impl CompletionData { + pub fn new(cx: Scope, config: ReadSignal>) -> Self { + let active = cx.create_rw_signal(0); + Self { + status: CompletionStatus::Inactive, + request_id: 0, + input_id: 0, + path: PathBuf::new(), + offset: 0, + active, + input: "".to_string(), + input_items: im::HashMap::new(), + filtered_items: im::Vector::new(), + layout_rect: Rect::ZERO, + matcher: cx + .create_rw_signal(nucleo::Matcher::new(nucleo::Config::DEFAULT)), + latest_editor_id: None, + config, + } + } + + /// Handle the response to a completion request. + pub fn receive( + &mut self, + request_id: usize, + input: &str, + resp: &CompletionResponse, + plugin_id: PluginId, + ) { + // If we've been canceled or the request id is old, ignore the response. + if self.status == CompletionStatus::Inactive || self.request_id != request_id + { + return; + } + + let items = match resp { + CompletionResponse::Array(items) => items, + // TODO: Possibly handle the 'is_incomplete' field on List. + CompletionResponse::List(list) => &list.items, + }; + let items: im::Vector = items + .iter() + .map(|i| ScoredCompletionItem { + item: i.to_owned(), + plugin_id, + score: 0, + label_score: 0, + indices: Vec::new(), + }) + .collect(); + self.input_items.insert(input.to_string(), items); + self.filter_items(); + } + + /// Request for completion items wit the current request id. + pub fn request( + &mut self, + editor_id: EditorId, + proxy_rpc: &ProxyRpcHandler, + path: PathBuf, + input: String, + position: Position, + ) { + self.latest_editor_id = Some(editor_id); + self.input_items.insert(input.clone(), im::Vector::new()); + proxy_rpc.completion(self.request_id, path, input, position); + } + + /// Close the completion, clearing all the data. + pub fn cancel(&mut self) { + if self.status == CompletionStatus::Inactive { + return; + } + self.status = CompletionStatus::Inactive; + self.input_id = 0; + self.latest_editor_id = None; + self.active.set(0); + self.input.clear(); + self.input_items.clear(); + self.filtered_items.clear(); + } + + pub fn update_input(&mut self, input: String) { + if self.status == CompletionStatus::Inactive { + return; + } + self.input = input; + // TODO: If the user types a letter that continues the current active item, we should + // try keeping that item active. Possibly give this a setting. + // ex: `p` has `print!` and `println!` has options. If you select the second, then type + // `r` then it should stay on `println!` even as the overall filtering of the list changes. + self.active.set(0); + self.filter_items(); + } + + fn all_items(&self) -> im::Vector { + self.input_items + .get(&self.input) + .cloned() + .filter(|items| !items.is_empty()) + .unwrap_or_else(move || { + self.input_items.get("").cloned().unwrap_or_default() + }) + } + + pub fn filter_items(&mut self) { + self.input_id += 1; + if self.input.is_empty() { + self.filtered_items = self.all_items(); + return; + } + + // Filter the items by the fuzzy matching with the input text. + let mut items: im::Vector = self + .matcher + .try_update(|matcher| { + let pattern = nucleo::pattern::Pattern::parse( + &self.input, + nucleo::pattern::CaseMatching::Ignore, + nucleo::pattern::Normalization::Smart, + ); + self.all_items() + .iter() + .filter_map(|i| { + let filter_text = + i.item.filter_text.as_ref().unwrap_or(&i.item.label); + let shift = i + .item + .label + .match_indices(filter_text) + .next() + .map(|(shift, _)| shift) + .unwrap_or(0); + let mut indices = Vec::new(); + let mut filter_text_buf = Vec::new(); + let filter_text = + Utf32Str::new(filter_text, &mut filter_text_buf); + if let Some(score) = + pattern.indices(filter_text, matcher, &mut indices) + { + if shift > 0 { + for idx in indices.iter_mut() { + *idx += shift as u32; + } + } + let mut item = i.clone(); + item.score = score; + item.label_score = score; + item.indices = + indices.into_iter().map(|i| i as usize).collect(); + + let mut label_buf = Vec::new(); + let label_text = + Utf32Str::new(&i.item.label, &mut label_buf); + if let Some(score) = pattern.score(label_text, matcher) { + item.label_score = score; + } + Some(item) + } else { + None + } + }) + .collect() + }) + .unwrap(); + // Sort all the items by their score, then their label score, then their length. + items.sort_by(|a, b| { + b.score + .cmp(&a.score) + .then_with(|| b.label_score.cmp(&a.label_score)) + .then_with(|| a.item.label.len().cmp(&b.item.label.len())) + }); + self.filtered_items = items; + } + + /// Move down in the list of items. + pub fn next(&mut self) { + let active = self.active.get_untracked(); + let new = + Movement::Down.update_index(active, self.filtered_items.len(), 1, true); + self.active.set(new); + } + + /// Move up in the list of items. + pub fn previous(&mut self) { + let active = self.active.get_untracked(); + let new = + Movement::Up.update_index(active, self.filtered_items.len(), 1, true); + self.active.set(new); + } + + /// The amount of items that can be displayed in the current layout. + fn display_count(&self) -> usize { + let config = self.config.get_untracked(); + ((self.layout_rect.size().height / config.editor.line_height() as f64) + .floor() as usize) + .saturating_sub(1) + } + + /// Move to the next page of items. + pub fn next_page(&mut self) { + let count = self.display_count(); + let active = self.active.get_untracked(); + let new = Movement::Down.update_index( + active, + self.filtered_items.len(), + count, + false, + ); + self.active.set(new); + } + + /// Move to the previous page of items. + pub fn previous_page(&mut self) { + let count = self.display_count(); + let active = self.active.get_untracked(); + let new = Movement::Up.update_index( + active, + self.filtered_items.len(), + count, + false, + ); + self.active.set(new); + } + + /// The currently selected/active item. + pub fn current_item(&self) -> Option<&ScoredCompletionItem> { + self.filtered_items.get(self.active.get_untracked()) + } + + /// Update the completion lens of the document with the active completion item. + pub fn update_document_completion( + &self, + editor_data: &EditorData, + cursor_offset: usize, + ) { + let doc = editor_data.doc(); + + if !doc.content.with_untracked(|content| content.is_file()) { + return; + } + + let config = self.config.get_untracked(); + + if !config.editor.enable_completion_lens { + doc.clear_completion_lens(); + return; + } + + let completion_lens = completion_lens_text( + doc.rope_text(), + cursor_offset, + self, + doc.completion_lens().as_deref(), + ); + match completion_lens { + Some(Some(lens)) => { + let offset = self.offset + self.input.len(); + // TODO: will need to be adjusted to use visual line. + // Could just store the offset in doc. + let (line, col) = editor_data.editor.offset_to_line_col(offset); + + doc.set_completion_lens(lens, line, col); + } + // Unchanged + Some(None) => {} + None => { + doc.clear_completion_lens(); + } + } + } +} + +/// Get the text of the completion lens for the given completion item. +/// Returns `None` if the completion lens should be hidden. +/// Returns `Some(None)` if the completion lens should be shown, but not changed. +/// Returns `Some(Some(text))` if the completion lens should be shown and changed to the given text. +fn completion_lens_text( + rope_text: impl RopeText, + cursor_offset: usize, + completion: &CompletionData, + current_completion: Option<&str>, +) -> Option> { + let item = &completion.current_item()?.item; + + let item: Cow = if let Some(edit) = &item.text_edit { + // A text edit is used, because that is what will actually be inserted. + + let text_format = item + .insert_text_format + .unwrap_or(InsertTextFormat::PLAIN_TEXT); + + // We don't display insert and replace + let CompletionTextEdit::Edit(edit) = edit else { + return None; + }; + // The completion offset can be different from the current cursor offset. + let completion_offset = completion.offset; + + let start_offset = rope_text.prev_code_boundary(cursor_offset); + let edit_start = rope_text.offset_of_position(&edit.range.start); + + // If the start of the edit isn't where the cursor currently is, + // and it is not at the start of the completion, then we ignore it. + // This captures most cases that we want, even if it skips over some + // displayable edits. + if start_offset != edit_start && completion_offset != edit_start { + return None; + } + + match text_format { + InsertTextFormat::PLAIN_TEXT => { + // This is not entirely correct because it assumes that the position is + // `{start,end}_offset` when it may not necessarily be. + Cow::Borrowed(&edit.new_text) + } + InsertTextFormat::SNIPPET => { + // Parse the snippet. Bail if it's invalid. + let snippet = Snippet::from_str(&edit.new_text).ok()?; + + let text = snippet.text(); + + Cow::Owned(text) + } + _ => { + // We don't know how to support this text format. + return None; + } + } + } else { + // There's no specific text edit, so we just use the label. + Cow::Borrowed(&item.label) + }; + // We strip the prefix of the current input from the label. + // So that, for example, `p` with a completion of `println` only sets the lens text to `rintln`. + // If the text does not include a prefix in the expected position, then we do not display it. + let item = item.as_ref().strip_prefix(&completion.input)?; + + // Get only the first line of text, because Lapce does not currently support + // multi-line phantom text. + let item = item.lines().next().unwrap_or(item); + + if Some(item) == current_completion { + // If the item is the same as the current completion, then we don't display it. + Some(None) + } else { + Some(Some(item.to_string())) + } +} diff --git a/lapce-app/src/config.rs b/lapce-app/src/config.rs new file mode 100644 index 0000000..1070577 --- /dev/null +++ b/lapce-app/src/config.rs @@ -0,0 +1,1020 @@ +use std::{ + collections::HashMap, + path::{Path, PathBuf}, + sync::Arc, +}; + +use ::core::slice; +use floem::{peniko::Color, prelude::palette::css}; +use itertools::Itertools; +use lapce_core::directory::Directory; +use lapce_proxy::plugin::wasi::find_all_volts; +use lapce_rpc::plugin::VoltID; +use lsp_types::{CompletionItemKind, SymbolKind}; +use once_cell::sync::Lazy; +use parking_lot::RwLock; +use serde::Deserialize; +use strum::VariantNames; +use tracing::error; + +use self::{ + color::LapceColor, + color_theme::{ColorThemeConfig, ThemeColor, ThemeColorPreference}, + core::CoreConfig, + editor::{EditorConfig, SCALE_OR_SIZE_LIMIT, WrapStyle}, + icon::LapceIcons, + icon_theme::IconThemeConfig, + svg::SvgStore, + terminal::TerminalConfig, + ui::UIConfig, +}; +use crate::workspace::{LapceWorkspace, LapceWorkspaceType}; + +pub mod color; +pub mod color_theme; +pub mod core; +pub mod editor; +pub mod icon; +pub mod icon_theme; +pub mod svg; +pub mod terminal; +pub mod ui; +pub mod watcher; + +pub const LOGO: &str = include_str!("../../extra/images/logo.svg"); +const DEFAULT_SETTINGS: &str = include_str!("../../defaults/settings.toml"); +const DEFAULT_LIGHT_THEME: &str = include_str!("../../defaults/light-theme.toml"); +const DEFAULT_DARK_THEME: &str = include_str!("../../defaults/dark-theme.toml"); +const DEFAULT_ICON_THEME: &str = include_str!("../../defaults/icon-theme.toml"); + +static DEFAULT_CONFIG: Lazy = Lazy::new(LapceConfig::default_config); +static DEFAULT_LAPCE_CONFIG: Lazy = + Lazy::new(LapceConfig::default_lapce_config); + +static DEFAULT_DARK_THEME_CONFIG: Lazy = Lazy::new(|| { + config::Config::builder() + .add_source(config::File::from_str( + DEFAULT_DARK_THEME, + config::FileFormat::Toml, + )) + .build() + .unwrap() +}); + +/// The default theme is the dark theme. +static DEFAULT_DARK_THEME_COLOR_CONFIG: Lazy = Lazy::new(|| { + let (_, theme) = + LapceConfig::load_color_theme_from_str(DEFAULT_DARK_THEME).unwrap(); + theme.get::("color-theme") + .expect("Failed to load default dark theme. This is likely due to a missing or misnamed field in dark-theme.toml") +}); + +static DEFAULT_ICON_THEME_CONFIG: Lazy = Lazy::new(|| { + config::Config::builder() + .add_source(config::File::from_str( + DEFAULT_ICON_THEME, + config::FileFormat::Toml, + )) + .build() + .unwrap() +}); +static DEFAULT_ICON_THEME_ICON_CONFIG: Lazy = Lazy::new(|| { + DEFAULT_ICON_THEME_CONFIG.get::("icon-theme") + .expect("Failed to load default icon theme. This is likely due to a missing or misnamed field in icon-theme.toml") +}); + +/// Used for creating a `DropdownData` for a setting +#[derive(Debug, Clone)] +pub struct DropdownInfo { + /// The currently selected item. + pub active_index: usize, + pub items: im::Vector, +} + +#[derive(Debug, Clone, Deserialize, Default)] +#[serde(rename_all = "kebab-case")] +pub struct LapceConfig { + #[serde(skip)] + pub id: u64, + pub core: CoreConfig, + pub ui: UIConfig, + pub editor: EditorConfig, + pub terminal: TerminalConfig, + #[serde(default)] + pub color_theme: ColorThemeConfig, + #[serde(default)] + pub icon_theme: IconThemeConfig, + #[serde(flatten)] + pub plugins: HashMap>, + #[serde(skip)] + pub color: ThemeColor, + #[serde(skip)] + pub available_color_themes: HashMap, + #[serde(skip)] + pub available_icon_themes: + HashMap)>, + // #[serde(skip)] + // tab_layout_info: Arc>>, + #[serde(skip)] + svg_store: Arc>, + /// A list of the themes that are available. This is primarily for populating + /// the theme picker, and serves as a cache. + #[serde(skip)] + color_theme_list: im::Vector, + #[serde(skip)] + icon_theme_list: im::Vector, + /// The couple names for the wrap style + #[serde(skip)] + wrap_style_list: im::Vector, +} + +impl LapceConfig { + pub fn load( + workspace: &LapceWorkspace, + disabled_volts: &[VoltID], + extra_plugin_paths: &[PathBuf], + ) -> Self { + let config = Self::merge_config(workspace, None, None); + let mut lapce_config: LapceConfig = match config.try_deserialize() { + Ok(config) => config, + Err(error) => { + error!("Failed to deserialize configuration file: {error}"); + DEFAULT_LAPCE_CONFIG.clone() + } + }; + + lapce_config.available_color_themes = + Self::load_color_themes(disabled_volts, extra_plugin_paths); + lapce_config.available_icon_themes = + Self::load_icon_themes(disabled_volts, extra_plugin_paths); + lapce_config.resolve_theme(workspace); + + lapce_config.color_theme_list = lapce_config + .available_color_themes + .values() + .map(|(name, _)| name.clone()) + .sorted() + .collect(); + lapce_config.color_theme_list.sort(); + + lapce_config.icon_theme_list = lapce_config + .available_icon_themes + .values() + .map(|(name, _, _)| name.clone()) + .sorted() + .collect(); + lapce_config.icon_theme_list.sort(); + + lapce_config.wrap_style_list = im::vector![ + WrapStyle::None.to_string(), + WrapStyle::EditorWidth.to_string(), + // TODO: WrapStyle::WrapColumn.to_string(), + WrapStyle::WrapWidth.to_string() + ]; + + lapce_config.terminal.get_indexed_colors(); + + lapce_config + } + + fn merge_config( + workspace: &LapceWorkspace, + color_theme_config: Option, + icon_theme_config: Option, + ) -> config::Config { + let mut config = DEFAULT_CONFIG.clone(); + + if let Some(theme) = color_theme_config { + // TODO: use different color theme basis if the theme declares its color preference + // differently + config = config::Config::builder() + .add_source(config.clone()) + .add_source(DEFAULT_DARK_THEME_CONFIG.clone()) + .add_source(theme) + .build() + .unwrap_or_else(|_| config.clone()); + } + + if let Some(theme) = icon_theme_config { + config = config::Config::builder() + .add_source(config.clone()) + .add_source(theme) + .build() + .unwrap_or_else(|_| config.clone()); + } + + if let Some(path) = Self::settings_file() { + config = config::Config::builder() + .add_source(config.clone()) + .add_source(config::File::from(path.as_path()).required(false)) + .build() + .unwrap_or_else(|_| config.clone()); + } + + match workspace.kind { + LapceWorkspaceType::Local => { + if let Some(path) = workspace.path.as_ref() { + let path = path.join("./.lapce/settings.toml"); + config = config::Config::builder() + .add_source(config.clone()) + .add_source( + config::File::from(path.as_path()).required(false), + ) + .build() + .unwrap_or_else(|_| config.clone()); + } + } + LapceWorkspaceType::RemoteSSH(_) => {} + #[cfg(windows)] + LapceWorkspaceType::RemoteWSL(_) => {} + } + + config + } + + fn update_id(&mut self) { + self.id = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_millis() as u64) + .unwrap_or(0); + } + + fn default_config() -> config::Config { + config::Config::builder() + .add_source(config::File::from_str( + DEFAULT_SETTINGS, + config::FileFormat::Toml, + )) + .build() + .unwrap() + } + + fn default_lapce_config() -> LapceConfig { + let mut default_lapce_config: LapceConfig = + DEFAULT_CONFIG.clone().try_deserialize().expect("Failed to deserialize default config, this likely indicates a missing or misnamed field in settings.toml"); + default_lapce_config.color_theme = DEFAULT_DARK_THEME_COLOR_CONFIG.clone(); + default_lapce_config.icon_theme = DEFAULT_ICON_THEME_ICON_CONFIG.clone(); + default_lapce_config.resolve_colors(None); + default_lapce_config + } + + fn resolve_theme(&mut self, workspace: &LapceWorkspace) { + let default_lapce_config = DEFAULT_LAPCE_CONFIG.clone(); + + let color_theme_config = self + .available_color_themes + .get(&self.core.color_theme.to_lowercase()) + .map(|(_, config)| config) + .unwrap_or(&DEFAULT_DARK_THEME_CONFIG); + + let icon_theme_config = self + .available_icon_themes + .get(&self.core.icon_theme.to_lowercase()) + .map(|(_, config, _)| config) + .unwrap_or(&DEFAULT_ICON_THEME_CONFIG); + + let icon_theme_path = self + .available_icon_themes + .get(&self.core.icon_theme.to_lowercase()) + .map(|(_, _, path)| path); + + if let Ok(new) = Self::merge_config( + workspace, + Some(color_theme_config.clone()), + Some(icon_theme_config.clone()), + ) + .try_deserialize::() + { + self.core = new.core; + self.ui = new.ui; + self.editor = new.editor; + self.terminal = new.terminal; + self.terminal.get_indexed_colors(); + + self.color_theme = new.color_theme; + self.icon_theme = new.icon_theme; + if let Some(icon_theme_path) = icon_theme_path { + self.icon_theme.path = icon_theme_path.clone().unwrap_or_default(); + } + self.plugins = new.plugins; + } + self.resolve_colors(Some(&default_lapce_config)); + self.update_id(); + } + + fn load_color_themes( + disabled_volts: &[VoltID], + extra_plugin_paths: &[PathBuf], + ) -> HashMap { + let mut themes = Self::load_local_themes().unwrap_or_default(); + + for (key, theme) in + Self::load_plugin_color_themes(disabled_volts, extra_plugin_paths) + { + themes.insert(key, theme); + } + + let (name, theme) = + Self::load_color_theme_from_str(DEFAULT_LIGHT_THEME).unwrap(); + themes.insert(name.to_lowercase(), (name, theme)); + let (name, theme) = + Self::load_color_theme_from_str(DEFAULT_DARK_THEME).unwrap(); + themes.insert(name.to_lowercase(), (name, theme)); + + themes + } + + pub fn default_color_theme(&self) -> &ColorThemeConfig { + &DEFAULT_DARK_THEME_COLOR_CONFIG + } + + /// Set the active color theme. + /// Note that this does not save the config. + pub fn set_color_theme(&mut self, workspace: &LapceWorkspace, theme: &str) { + self.core.color_theme = theme.to_string(); + self.resolve_theme(workspace); + } + + /// Set the active icon theme. + /// Note that this does not save the config. + pub fn set_icon_theme(&mut self, workspace: &LapceWorkspace, theme: &str) { + self.core.icon_theme = theme.to_string(); + self.resolve_theme(workspace); + } + + pub fn set_modal(&mut self, _workspace: &LapceWorkspace, modal: bool) { + self.core.modal = modal; + } + + /// Get the color by the name from the current theme if it exists + /// Otherwise, get the color from the base them + /// # Panics + /// If the color was not able to be found in either theme, which may be indicative that + /// it is misspelled or needs to be added to the base-theme. + pub fn color(&self, name: &str) -> Color { + match self.color.ui.get(name) { + Some(c) => *c, + None => { + error!("Failed to find key: {name}"); + css::HOT_PINK + } + } + } + + /// Retrieve a color value whose key starts with "style." + pub fn style_color(&self, name: &str) -> Option { + self.color.syntax.get(name).copied() + } + + pub fn completion_color( + &self, + kind: Option, + ) -> Option { + let kind = kind?; + let theme_str = match kind { + CompletionItemKind::METHOD => "method", + CompletionItemKind::FUNCTION => "method", + CompletionItemKind::ENUM => "enum", + CompletionItemKind::ENUM_MEMBER => "enum-member", + CompletionItemKind::CLASS => "class", + CompletionItemKind::VARIABLE => "field", + CompletionItemKind::STRUCT => "structure", + CompletionItemKind::KEYWORD => "keyword", + CompletionItemKind::CONSTANT => "constant", + CompletionItemKind::PROPERTY => "property", + CompletionItemKind::FIELD => "field", + CompletionItemKind::INTERFACE => "interface", + CompletionItemKind::SNIPPET => "snippet", + CompletionItemKind::MODULE => "builtinType", + _ => "string", + }; + + self.style_color(theme_str) + } + + fn resolve_colors(&mut self, default_config: Option<&LapceConfig>) { + self.color.base = self + .color_theme + .base + .resolve(default_config.map(|c| &c.color_theme.base)); + self.color.ui = self + .color_theme + .resolve_ui_color(&self.color.base, default_config.map(|c| &c.color.ui)); + self.color.syntax = self.color_theme.resolve_syntax_color( + &self.color.base, + default_config.map(|c| &c.color.syntax), + ); + + let fg = self.color(LapceColor::EDITOR_FOREGROUND).to_rgba8(); + let bg = self.color(LapceColor::EDITOR_BACKGROUND).to_rgba8(); + let is_light = fg.r as u32 + fg.g as u32 + fg.b as u32 + > bg.r as u32 + bg.g as u32 + bg.b as u32; + let high_contrast = self.color_theme.high_contrast.unwrap_or(false); + self.color.color_preference = match (is_light, high_contrast) { + (true, true) => ThemeColorPreference::HighContrastLight, + (false, true) => ThemeColorPreference::HighContrastDark, + (true, false) => ThemeColorPreference::Light, + (false, false) => ThemeColorPreference::Dark, + }; + } + + fn load_local_themes() -> Option> { + let themes_folder = Directory::themes_directory()?; + let themes: HashMap = + std::fs::read_dir(themes_folder) + .ok()? + .filter_map(|entry| { + entry + .ok() + .and_then(|entry| Self::load_color_theme(&entry.path())) + }) + .collect(); + Some(themes) + } + + fn load_color_theme(path: &Path) -> Option<(String, (String, config::Config))> { + if !path.is_file() { + return None; + } + let config = config::Config::builder() + .add_source(config::File::from(path)) + .build() + .ok()?; + let table = config.get_table("color-theme").ok()?; + let name = table.get("name")?.to_string(); + Some((name.to_lowercase(), (name, config))) + } + + /// Load the given theme by its contents. + /// Returns `(name, theme fields)` + fn load_color_theme_from_str(s: &str) -> Option<(String, config::Config)> { + let config = config::Config::builder() + .add_source(config::File::from_str(s, config::FileFormat::Toml)) + .build() + .ok()?; + let table = config.get_table("color-theme").ok()?; + let name = table.get("name")?.to_string(); + Some((name, config)) + } + + fn load_icon_themes( + disabled_volts: &[VoltID], + extra_plugin_paths: &[PathBuf], + ) -> HashMap)> { + let mut themes = HashMap::new(); + + for (key, (name, theme, path)) in + Self::load_plugin_icon_themes(disabled_volts, extra_plugin_paths) + { + themes.insert(key, (name, theme, Some(path))); + } + + let (name, theme) = + Self::load_icon_theme_from_str(DEFAULT_ICON_THEME).unwrap(); + themes.insert(name.to_lowercase(), (name, theme, None)); + + themes + } + + fn load_icon_theme_from_str(s: &str) -> Option<(String, config::Config)> { + let config = config::Config::builder() + .add_source(config::File::from_str(s, config::FileFormat::Toml)) + .build() + .ok()?; + let table = config.get_table("icon-theme").ok()?; + let name = table.get("name")?.to_string(); + Some((name, config)) + } + + fn load_plugin_color_themes( + disabled_volts: &[VoltID], + extra_plugin_paths: &[PathBuf], + ) -> HashMap { + let mut themes: HashMap = HashMap::new(); + for meta in find_all_volts(extra_plugin_paths) { + if disabled_volts.contains(&meta.id()) { + continue; + } + if let Some(plugin_themes) = meta.color_themes.as_ref() { + for theme_path in plugin_themes { + if let Some((key, theme)) = + Self::load_color_theme(&PathBuf::from(theme_path)) + { + themes.insert(key, theme); + } + } + } + } + themes + } + + fn load_plugin_icon_themes( + disabled_volts: &[VoltID], + extra_plugin_paths: &[PathBuf], + ) -> HashMap { + let mut themes: HashMap = + HashMap::new(); + for meta in find_all_volts(extra_plugin_paths) { + if disabled_volts.contains(&meta.id()) { + continue; + } + if let Some(plugin_themes) = meta.icon_themes.as_ref() { + for theme_path in plugin_themes { + if let Some((key, theme)) = + Self::load_icon_theme(&PathBuf::from(theme_path)) + { + themes.insert(key, theme); + } + } + } + } + themes + } + + fn load_icon_theme( + path: &Path, + ) -> Option<(String, (String, config::Config, PathBuf))> { + if !path.is_file() { + return None; + } + let config = config::Config::builder() + .add_source(config::File::from(path)) + .build() + .ok()?; + let table = config.get_table("icon-theme").ok()?; + let name = table.get("name")?.to_string(); + Some(( + name.to_lowercase(), + (name, config, path.parent().unwrap().to_path_buf()), + )) + } + + pub fn export_theme(&self) -> String { + let mut table = toml::value::Table::new(); + let mut theme = self.color_theme.clone(); + theme.name = "".to_string(); + table.insert( + "color-theme".to_string(), + toml::Value::try_from(&theme).unwrap(), + ); + table.insert("ui".to_string(), toml::Value::try_from(&self.ui).unwrap()); + let value = toml::Value::Table(table); + toml::to_string_pretty(&value).unwrap() + } + + pub fn settings_file() -> Option { + let path = Directory::config_directory()?.join("settings.toml"); + + if !path.exists() { + if let Err(err) = std::fs::OpenOptions::new() + .create_new(true) + .write(true) + .open(&path) + { + tracing::error!("{:?}", err); + } + } + + Some(path) + } + + pub fn keymaps_file() -> Option { + let path = Directory::config_directory()?.join("keymaps.toml"); + + if !path.exists() { + if let Err(err) = std::fs::OpenOptions::new() + .create_new(true) + .write(true) + .open(&path) + { + tracing::error!("{:?}", err); + } + } + + Some(path) + } + + pub fn ui_svg(&self, icon: &'static str) -> String { + let svg = self.icon_theme.ui.get(icon).and_then(|path| { + let path = self.icon_theme.path.join(path); + self.svg_store.write().get_svg_on_disk(&path) + }); + + svg.unwrap_or_else(|| { + let name = DEFAULT_ICON_THEME_ICON_CONFIG.ui.get(icon).unwrap(); + self.svg_store.write().get_default_svg(name) + }) + } + + pub fn files_svg(&self, paths: &[&Path]) -> (String, Option) { + let svg = self + .icon_theme + .resolve_path_to_icon(paths) + .and_then(|p| self.svg_store.write().get_svg_on_disk(&p)); + + if let Some(svg) = svg { + let color = if self.icon_theme.use_editor_color.unwrap_or(false) { + Some(self.color(LapceColor::LAPCE_ICON_ACTIVE)) + } else { + None + }; + (svg, color) + } else { + ( + self.ui_svg(LapceIcons::FILE), + Some(self.color(LapceColor::LAPCE_ICON_ACTIVE)), + ) + } + } + + pub fn file_svg(&self, path: &Path) -> (String, Option) { + self.files_svg(slice::from_ref(&path)) + } + + pub fn symbol_svg(&self, kind: &SymbolKind) -> Option { + let kind_str = match *kind { + SymbolKind::ARRAY => LapceIcons::SYMBOL_KIND_ARRAY, + SymbolKind::BOOLEAN => LapceIcons::SYMBOL_KIND_BOOLEAN, + SymbolKind::CLASS => LapceIcons::SYMBOL_KIND_CLASS, + SymbolKind::CONSTANT => LapceIcons::SYMBOL_KIND_CONSTANT, + SymbolKind::ENUM_MEMBER => LapceIcons::SYMBOL_KIND_ENUM_MEMBER, + SymbolKind::ENUM => LapceIcons::SYMBOL_KIND_ENUM, + SymbolKind::EVENT => LapceIcons::SYMBOL_KIND_EVENT, + SymbolKind::FIELD => LapceIcons::SYMBOL_KIND_FIELD, + SymbolKind::FILE => LapceIcons::SYMBOL_KIND_FILE, + SymbolKind::INTERFACE => LapceIcons::SYMBOL_KIND_INTERFACE, + SymbolKind::KEY => LapceIcons::SYMBOL_KIND_KEY, + SymbolKind::FUNCTION => LapceIcons::SYMBOL_KIND_FUNCTION, + SymbolKind::METHOD => LapceIcons::SYMBOL_KIND_METHOD, + SymbolKind::OBJECT => LapceIcons::SYMBOL_KIND_OBJECT, + SymbolKind::NAMESPACE => LapceIcons::SYMBOL_KIND_NAMESPACE, + SymbolKind::NUMBER => LapceIcons::SYMBOL_KIND_NUMBER, + SymbolKind::OPERATOR => LapceIcons::SYMBOL_KIND_OPERATOR, + SymbolKind::TYPE_PARAMETER => LapceIcons::SYMBOL_KIND_TYPE_PARAMETER, + SymbolKind::PROPERTY => LapceIcons::SYMBOL_KIND_PROPERTY, + SymbolKind::STRING => LapceIcons::SYMBOL_KIND_STRING, + SymbolKind::STRUCT => LapceIcons::SYMBOL_KIND_STRUCT, + SymbolKind::VARIABLE => LapceIcons::SYMBOL_KIND_VARIABLE, + _ => return None, + }; + + Some(self.ui_svg(kind_str)) + } + + pub fn symbol_color(&self, kind: &SymbolKind) -> Option { + let theme_str = match *kind { + SymbolKind::METHOD => "method", + SymbolKind::FUNCTION => "method", + SymbolKind::ENUM => "enum", + SymbolKind::ENUM_MEMBER => "enum-member", + SymbolKind::CLASS => "class", + SymbolKind::VARIABLE => "field", + SymbolKind::STRUCT => "structure", + SymbolKind::CONSTANT => "constant", + SymbolKind::PROPERTY => "property", + SymbolKind::FIELD => "field", + SymbolKind::INTERFACE => "interface", + SymbolKind::ARRAY => "", + SymbolKind::BOOLEAN => "", + SymbolKind::EVENT => "", + SymbolKind::FILE => "", + SymbolKind::KEY => "", + SymbolKind::OBJECT => "", + SymbolKind::NAMESPACE => "", + SymbolKind::NUMBER => "number", + SymbolKind::OPERATOR => "", + SymbolKind::TYPE_PARAMETER => "", + SymbolKind::STRING => "string", + _ => return None, + }; + + self.style_color(theme_str) + } + + pub fn logo_svg(&self) -> String { + self.svg_store.read().logo_svg() + } + + /// List of the color themes that are available by their display names. + pub fn color_theme_list(&self) -> im::Vector { + self.color_theme_list.clone() + } + + /// List of the icon themes that are available by their display names. + pub fn icon_theme_list(&self) -> im::Vector { + self.icon_theme_list.clone() + } + + pub fn terminal_font_family(&self) -> &str { + if self.terminal.font_family.is_empty() { + self.editor.font_family.as_str() + } else { + self.terminal.font_family.as_str() + } + } + + pub fn terminal_font_size(&self) -> usize { + if self.terminal.font_size > 0 { + self.terminal.font_size + } else { + self.editor.font_size() + } + } + + pub fn terminal_line_height(&self) -> usize { + let font_size = self.terminal_font_size(); + + if self.terminal.line_height > 0.0 { + let line_height = if self.terminal.line_height < SCALE_OR_SIZE_LIMIT { + self.terminal.line_height * font_size as f64 + } else { + self.terminal.line_height + }; + + // Prevent overlapping lines + (line_height.round() as usize).max(font_size) + } else { + self.editor.line_height() + } + } + + pub fn terminal_get_color( + &self, + color: &alacritty_terminal::vte::ansi::Color, + colors: &alacritty_terminal::term::color::Colors, + ) -> Color { + match color { + alacritty_terminal::vte::ansi::Color::Named(color) => { + self.terminal_get_named_color(color) + } + alacritty_terminal::vte::ansi::Color::Spec(rgb) => { + Color::from_rgb8(rgb.r, rgb.g, rgb.b) + } + alacritty_terminal::vte::ansi::Color::Indexed(index) => { + if let Some(rgb) = colors[*index as usize] { + return Color::from_rgb8(rgb.r, rgb.g, rgb.b); + } + const NAMED_COLORS: [alacritty_terminal::vte::ansi::NamedColor; 16] = [ + alacritty_terminal::vte::ansi::NamedColor::Black, + alacritty_terminal::vte::ansi::NamedColor::Red, + alacritty_terminal::vte::ansi::NamedColor::Green, + alacritty_terminal::vte::ansi::NamedColor::Yellow, + alacritty_terminal::vte::ansi::NamedColor::Blue, + alacritty_terminal::vte::ansi::NamedColor::Magenta, + alacritty_terminal::vte::ansi::NamedColor::Cyan, + alacritty_terminal::vte::ansi::NamedColor::White, + alacritty_terminal::vte::ansi::NamedColor::BrightBlack, + alacritty_terminal::vte::ansi::NamedColor::BrightRed, + alacritty_terminal::vte::ansi::NamedColor::BrightGreen, + alacritty_terminal::vte::ansi::NamedColor::BrightYellow, + alacritty_terminal::vte::ansi::NamedColor::BrightBlue, + alacritty_terminal::vte::ansi::NamedColor::BrightMagenta, + alacritty_terminal::vte::ansi::NamedColor::BrightCyan, + alacritty_terminal::vte::ansi::NamedColor::BrightWhite, + ]; + if (*index as usize) < NAMED_COLORS.len() { + self.terminal_get_named_color(&NAMED_COLORS[*index as usize]) + } else { + self.terminal.indexed_colors.get(index).cloned().unwrap() + } + } + } + } + + fn terminal_get_named_color( + &self, + color: &alacritty_terminal::vte::ansi::NamedColor, + ) -> Color { + let (color, alpha) = match color { + alacritty_terminal::vte::ansi::NamedColor::Cursor => { + (LapceColor::TERMINAL_CURSOR, 1.0) + } + alacritty_terminal::vte::ansi::NamedColor::Foreground => { + (LapceColor::TERMINAL_FOREGROUND, 1.0) + } + alacritty_terminal::vte::ansi::NamedColor::Background => { + (LapceColor::TERMINAL_BACKGROUND, 1.0) + } + alacritty_terminal::vte::ansi::NamedColor::Blue => { + (LapceColor::TERMINAL_BLUE, 1.0) + } + alacritty_terminal::vte::ansi::NamedColor::Green => { + (LapceColor::TERMINAL_GREEN, 1.0) + } + alacritty_terminal::vte::ansi::NamedColor::Yellow => { + (LapceColor::TERMINAL_YELLOW, 1.0) + } + alacritty_terminal::vte::ansi::NamedColor::Red => { + (LapceColor::TERMINAL_RED, 1.0) + } + alacritty_terminal::vte::ansi::NamedColor::White => { + (LapceColor::TERMINAL_WHITE, 1.0) + } + alacritty_terminal::vte::ansi::NamedColor::Black => { + (LapceColor::TERMINAL_BLACK, 1.0) + } + alacritty_terminal::vte::ansi::NamedColor::Cyan => { + (LapceColor::TERMINAL_CYAN, 1.0) + } + alacritty_terminal::vte::ansi::NamedColor::Magenta => { + (LapceColor::TERMINAL_MAGENTA, 1.0) + } + alacritty_terminal::vte::ansi::NamedColor::BrightBlue => { + (LapceColor::TERMINAL_BRIGHT_BLUE, 1.0) + } + alacritty_terminal::vte::ansi::NamedColor::BrightGreen => { + (LapceColor::TERMINAL_BRIGHT_GREEN, 1.0) + } + alacritty_terminal::vte::ansi::NamedColor::BrightYellow => { + (LapceColor::TERMINAL_BRIGHT_YELLOW, 1.0) + } + alacritty_terminal::vte::ansi::NamedColor::BrightRed => { + (LapceColor::TERMINAL_BRIGHT_RED, 1.0) + } + alacritty_terminal::vte::ansi::NamedColor::BrightWhite => { + (LapceColor::TERMINAL_BRIGHT_WHITE, 1.0) + } + alacritty_terminal::vte::ansi::NamedColor::BrightBlack => { + (LapceColor::TERMINAL_BRIGHT_BLACK, 1.0) + } + alacritty_terminal::vte::ansi::NamedColor::BrightCyan => { + (LapceColor::TERMINAL_BRIGHT_CYAN, 1.0) + } + alacritty_terminal::vte::ansi::NamedColor::BrightMagenta => { + (LapceColor::TERMINAL_BRIGHT_MAGENTA, 1.0) + } + alacritty_terminal::vte::ansi::NamedColor::BrightForeground => { + (LapceColor::TERMINAL_FOREGROUND, 1.0) + } + alacritty_terminal::vte::ansi::NamedColor::DimBlack => { + (LapceColor::TERMINAL_BLACK, 0.66) + } + alacritty_terminal::vte::ansi::NamedColor::DimRed => { + (LapceColor::TERMINAL_RED, 0.66) + } + alacritty_terminal::vte::ansi::NamedColor::DimGreen => { + (LapceColor::TERMINAL_GREEN, 0.66) + } + alacritty_terminal::vte::ansi::NamedColor::DimYellow => { + (LapceColor::TERMINAL_YELLOW, 0.66) + } + alacritty_terminal::vte::ansi::NamedColor::DimBlue => { + (LapceColor::TERMINAL_BLUE, 0.66) + } + alacritty_terminal::vte::ansi::NamedColor::DimMagenta => { + (LapceColor::TERMINAL_MAGENTA, 0.66) + } + alacritty_terminal::vte::ansi::NamedColor::DimCyan => { + (LapceColor::TERMINAL_CYAN, 0.66) + } + alacritty_terminal::vte::ansi::NamedColor::DimWhite => { + (LapceColor::TERMINAL_WHITE, 0.66) + } + alacritty_terminal::vte::ansi::NamedColor::DimForeground => { + (LapceColor::TERMINAL_FOREGROUND, 0.66) + } + }; + self.color(color).multiply_alpha(alpha) + } + + /// Get the dropdown information for the specific setting, used for the settings UI. + /// This should aim to efficiently return the data, because it is used to determine whether to + /// update the dropdown items. + pub fn get_dropdown_info(&self, kind: &str, key: &str) -> Option { + match (kind, key) { + ("core", "color-theme") => Some(DropdownInfo { + active_index: self + .color_theme_list + .iter() + .position(|s| s == &self.color_theme.name) + .unwrap_or(0), + items: self.color_theme_list.clone(), + }), + ("core", "icon-theme") => Some(DropdownInfo { + active_index: self + .icon_theme_list + .iter() + .position(|s| s == &self.icon_theme.name) + .unwrap_or(0), + items: self.icon_theme_list.clone(), + }), + ("editor", "wrap-style") => Some(DropdownInfo { + // TODO: it would be better to have the text not be the default kebab-case when + // displayed in settings, but we would need to map back from the dropdown's value + // or index. + active_index: self + .wrap_style_list + .iter() + .flat_map(|w| WrapStyle::try_from_str(w)) + .position(|w| w == self.editor.wrap_style) + .unwrap_or(0), + items: self.wrap_style_list.clone(), + }), + ("ui", "tab-close-button") => Some(DropdownInfo { + active_index: self.ui.tab_close_button as usize, + items: ui::TabCloseButton::VARIANTS + .iter() + .map(|s| s.to_string()) + .sorted() + .collect(), + }), + ("ui", "tab-separator-height") => Some(DropdownInfo { + active_index: self.ui.tab_separator_height as usize, + items: ui::TabSeparatorHeight::VARIANTS + .iter() + .map(|s| s.to_string()) + .sorted() + .collect(), + }), + ("terminal", "default-profile") => Some(DropdownInfo { + active_index: self + .terminal + .profiles + .iter() + .position(|(profile_name, _)| { + profile_name + == self + .terminal + .default_profile + .get(std::env::consts::OS) + .unwrap_or(&String::from("default")) + }) + .unwrap_or(0), + items: self.terminal.profiles.clone().into_keys().collect(), + }), + _ => None, + } + } + + fn get_file_table() -> Option { + let path = Self::settings_file()?; + let content = std::fs::read_to_string(path).ok()?; + let document: toml_edit::Document = content.parse().ok()?; + Some(document) + } + + pub fn reset_setting(parent: &str, key: &str) -> Option<()> { + let mut main_table = Self::get_file_table().unwrap_or_default(); + + // Find the container table + let mut table = main_table.as_table_mut(); + for key in parent.split('.') { + if !table.contains_key(key) { + table.insert( + key, + toml_edit::Item::Table(toml_edit::Table::default()), + ); + } + table = table.get_mut(key)?.as_table_mut()?; + } + + table.remove(key); + + // Store + let path = Self::settings_file()?; + std::fs::write(path, main_table.to_string().as_bytes()).ok()?; + + Some(()) + } + + /// Update the config file with the given edit. + /// This should be called whenever the configuration is changed, so that it is persisted. + pub fn update_file( + parent: &str, + key: &str, + value: toml_edit::Value, + ) -> Option<()> { + // TODO: This is a hack to fix the fact that terminal default profile is saved in a + // different manner than other fields. As it is per-operating-system. + // Thus we have to instead set the terminal.default-profile.{OS} + // It would be better to not need a special hack. + let (parent, key) = if parent == "terminal" && key == "default-profile" { + ("terminal.default-profile", std::env::consts::OS) + } else { + (parent, key) + }; + + let mut main_table = Self::get_file_table().unwrap_or_default(); + + // Find the container table + let mut table = main_table.as_table_mut(); + for key in parent.split('.') { + if !table.contains_key(key) { + table.insert( + key, + toml_edit::Item::Table(toml_edit::Table::default()), + ); + } + table = table.get_mut(key)?.as_table_mut()?; + } + + // Update key + table.insert(key, toml_edit::Item::Value(value)); + + // Store + let path = Self::settings_file()?; + std::fs::write(path, main_table.to_string().as_bytes()).ok()?; + + Some(()) + } +} diff --git a/lapce-app/src/config/color.rs b/lapce-app/src/config/color.rs new file mode 100644 index 0000000..158fabf --- /dev/null +++ b/lapce-app/src/config/color.rs @@ -0,0 +1,182 @@ +use std::path::PathBuf; + +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum LoadThemeError { + #[error("themes folder not found, possibly it could not be created")] + ThemesFolderNotFound, + #[error("theme file ({theme_name}.toml) was not found in {themes_folder:?}")] + FileNotFound { + themes_folder: PathBuf, + theme_name: String, + }, + #[error("recursion limit reached for {variable_name}")] + RecursionLimitReached { variable_name: String }, + #[error("variable {variable_name} not found")] + VariableNotFound { variable_name: String }, + #[error("There was an error reading the theme file")] + Read(std::io::Error), +} + +pub struct LapceColor {} + +impl LapceColor { + pub const LAPCE_WARN: &'static str = "lapce.warn"; + pub const LAPCE_ERROR: &'static str = "lapce.error"; + pub const LAPCE_DROPDOWN_SHADOW: &'static str = "lapce.dropdown_shadow"; + pub const LAPCE_BORDER: &'static str = "lapce.border"; + pub const LAPCE_SCROLL_BAR: &'static str = "lapce.scroll_bar"; + + pub const LAPCE_BUTTON_PRIMARY_BACKGROUND: &'static str = + "lapce.button.primary.background"; + pub const LAPCE_BUTTON_PRIMARY_FOREGROUND: &'static str = + "lapce.button.primary.foreground"; + + pub const LAPCE_TAB_ACTIVE_BACKGROUND: &'static str = + "lapce.tab.active.background"; + pub const LAPCE_TAB_ACTIVE_FOREGROUND: &'static str = + "lapce.tab.active.foreground"; + pub const LAPCE_TAB_ACTIVE_UNDERLINE: &'static str = + "lapce.tab.active.underline"; + + pub const LAPCE_TAB_INACTIVE_BACKGROUND: &'static str = + "lapce.tab.inactive.background"; + pub const LAPCE_TAB_INACTIVE_FOREGROUND: &'static str = + "lapce.tab.inactive.foreground"; + pub const LAPCE_TAB_INACTIVE_UNDERLINE: &'static str = + "lapce.tab.inactive.underline"; + + pub const LAPCE_TAB_SEPARATOR: &'static str = "lapce.tab.separator"; + + pub const LAPCE_ICON_ACTIVE: &'static str = "lapce.icon.active"; + pub const LAPCE_ICON_INACTIVE: &'static str = "lapce.icon.inactive"; + + pub const LAPCE_REMOTE_ICON: &'static str = "lapce.remote.icon"; + pub const LAPCE_REMOTE_LOCAL: &'static str = "lapce.remote.local"; + pub const LAPCE_REMOTE_CONNECTED: &'static str = "lapce.remote.connected"; + pub const LAPCE_REMOTE_CONNECTING: &'static str = "lapce.remote.connecting"; + pub const LAPCE_REMOTE_DISCONNECTED: &'static str = "lapce.remote.disconnected"; + + pub const LAPCE_PLUGIN_NAME: &'static str = "lapce.plugin.name"; + pub const LAPCE_PLUGIN_DESCRIPTION: &'static str = "lapce.plugin.description"; + pub const LAPCE_PLUGIN_AUTHOR: &'static str = "lapce.plugin.author"; + + pub const EDITOR_BACKGROUND: &'static str = "editor.background"; + pub const EDITOR_FOREGROUND: &'static str = "editor.foreground"; + pub const EDITOR_DIM: &'static str = "editor.dim"; + pub const EDITOR_FOCUS: &'static str = "editor.focus"; + pub const EDITOR_CARET: &'static str = "editor.caret"; + pub const EDITOR_SELECTION: &'static str = "editor.selection"; + pub const EDITOR_DEBUG_BREAK_LINE: &'static str = "editor.debug_break_line"; + pub const EDITOR_CURRENT_LINE: &'static str = "editor.current_line"; + pub const EDITOR_LINK: &'static str = "editor.link"; + pub const EDITOR_VISIBLE_WHITESPACE: &'static str = "editor.visible_whitespace"; + pub const EDITOR_INDENT_GUIDE: &'static str = "editor.indent_guide"; + pub const EDITOR_DRAG_DROP_BACKGROUND: &'static str = + "editor.drag_drop_background"; + pub const EDITOR_STICKY_HEADER_BACKGROUND: &'static str = + "editor.sticky_header_background"; + pub const EDITOR_DRAG_DROP_TAB_BACKGROUND: &'static str = + "editor.drag_drop_tab_background"; + + pub const INLAY_HINT_FOREGROUND: &'static str = "inlay_hint.foreground"; + pub const INLAY_HINT_BACKGROUND: &'static str = "inlay_hint.background"; + + pub const ERROR_LENS_ERROR_FOREGROUND: &'static str = + "error_lens.error.foreground"; + pub const ERROR_LENS_ERROR_BACKGROUND: &'static str = + "error_lens.error.background"; + pub const ERROR_LENS_WARNING_FOREGROUND: &'static str = + "error_lens.warning.foreground"; + pub const ERROR_LENS_WARNING_BACKGROUND: &'static str = + "error_lens.warning.background"; + pub const ERROR_LENS_OTHER_FOREGROUND: &'static str = + "error_lens.other.foreground"; + pub const ERROR_LENS_OTHER_BACKGROUND: &'static str = + "error_lens.other.background"; + + pub const COMPLETION_LENS_FOREGROUND: &'static str = + "completion_lens.foreground"; + + pub const SOURCE_CONTROL_ADDED: &'static str = "source_control.added"; + pub const SOURCE_CONTROL_REMOVED: &'static str = "source_control.removed"; + pub const SOURCE_CONTROL_MODIFIED: &'static str = "source_control.modified"; + + pub const TERMINAL_CURSOR: &'static str = "terminal.cursor"; + pub const TERMINAL_BACKGROUND: &'static str = "terminal.background"; + pub const TERMINAL_FOREGROUND: &'static str = "terminal.foreground"; + pub const TERMINAL_RED: &'static str = "terminal.red"; + pub const TERMINAL_BLUE: &'static str = "terminal.blue"; + pub const TERMINAL_GREEN: &'static str = "terminal.green"; + pub const TERMINAL_YELLOW: &'static str = "terminal.yellow"; + pub const TERMINAL_BLACK: &'static str = "terminal.black"; + pub const TERMINAL_WHITE: &'static str = "terminal.white"; + pub const TERMINAL_CYAN: &'static str = "terminal.cyan"; + pub const TERMINAL_MAGENTA: &'static str = "terminal.magenta"; + + pub const TERMINAL_BRIGHT_RED: &'static str = "terminal.bright_red"; + pub const TERMINAL_BRIGHT_BLUE: &'static str = "terminal.bright_blue"; + pub const TERMINAL_BRIGHT_GREEN: &'static str = "terminal.bright_green"; + pub const TERMINAL_BRIGHT_YELLOW: &'static str = "terminal.bright_yellow"; + pub const TERMINAL_BRIGHT_BLACK: &'static str = "terminal.bright_black"; + pub const TERMINAL_BRIGHT_WHITE: &'static str = "terminal.bright_white"; + pub const TERMINAL_BRIGHT_CYAN: &'static str = "terminal.bright_cyan"; + pub const TERMINAL_BRIGHT_MAGENTA: &'static str = "terminal.bright_magenta"; + + pub const PALETTE_BACKGROUND: &'static str = "palette.background"; + pub const PALETTE_FOREGROUND: &'static str = "palette.foreground"; + pub const PALETTE_CURRENT_BACKGROUND: &'static str = + "palette.current.background"; + pub const PALETTE_CURRENT_FOREGROUND: &'static str = + "palette.current.foreground"; + + pub const COMPLETION_BACKGROUND: &'static str = "completion.background"; + pub const COMPLETION_CURRENT: &'static str = "completion.current"; + + pub const HOVER_BACKGROUND: &'static str = "hover.background"; + + pub const ACTIVITY_BACKGROUND: &'static str = "activity.background"; + pub const ACTIVITY_CURRENT: &'static str = "activity.current"; + + pub const DEBUG_BREAKPOINT: &'static str = "debug.breakpoint"; + pub const DEBUG_BREAKPOINT_HOVER: &'static str = "debug.breakpoint.hover"; + + pub const TOOLTIP_BACKGROUND: &'static str = "tooltip.background"; + pub const TOOLTIP_FOREGROUND: &'static str = "tooltip.foreground"; + + pub const PANEL_BACKGROUND: &'static str = "panel.background"; + pub const PANEL_FOREGROUND: &'static str = "panel.foreground"; + pub const PANEL_FOREGROUND_DIM: &'static str = "panel.foreground.dim"; + pub const PANEL_CURRENT_BACKGROUND: &'static str = "panel.current.background"; + pub const PANEL_CURRENT_FOREGROUND: &'static str = "panel.current.foreground"; + pub const PANEL_CURRENT_FOREGROUND_DIM: &'static str = + "panel.current.foreground.dim"; + pub const PANEL_HOVERED_BACKGROUND: &'static str = "panel.hovered.background"; + pub const PANEL_HOVERED_ACTIVE_BACKGROUND: &'static str = + "panel.hovered.active.background"; + pub const PANEL_HOVERED_FOREGROUND: &'static str = "panel.hovered.foreground"; + pub const PANEL_HOVERED_FOREGROUND_DIM: &'static str = + "panel.hovered.foreground.dim"; + + pub const STATUS_BACKGROUND: &'static str = "status.background"; + pub const STATUS_FOREGROUND: &'static str = "status.foreground"; + pub const STATUS_MODAL_NORMAL_BACKGROUND: &'static str = + "status.modal.normal.background"; + pub const STATUS_MODAL_NORMAL_FOREGROUND: &'static str = + "status.modal.normal.foreground"; + pub const STATUS_MODAL_INSERT_BACKGROUND: &'static str = + "status.modal.insert.background"; + pub const STATUS_MODAL_INSERT_FOREGROUND: &'static str = + "status.modal.insert.foreground"; + pub const STATUS_MODAL_VISUAL_BACKGROUND: &'static str = + "status.modal.visual.background"; + pub const STATUS_MODAL_VISUAL_FOREGROUND: &'static str = + "status.modal.visual.foreground"; + pub const STATUS_MODAL_TERMINAL_BACKGROUND: &'static str = + "status.modal.terminal.background"; + pub const STATUS_MODAL_TERMINAL_FOREGROUND: &'static str = + "status.modal.terminal.foreground"; + + pub const MARKDOWN_BLOCKQUOTE: &'static str = "markdown.blockquote"; +} diff --git a/lapce-app/src/config/color_theme.rs b/lapce-app/src/config/color_theme.rs new file mode 100644 index 0000000..b9c0d97 --- /dev/null +++ b/lapce-app/src/config/color_theme.rs @@ -0,0 +1,263 @@ +use std::{ + collections::{BTreeMap, HashMap}, + path::PathBuf, + str::FromStr, +}; + +use floem::{peniko::Color, prelude::palette::css}; +use serde::{Deserialize, Serialize}; + +use super::color::LoadThemeError; + +#[derive(Debug, Clone, Default)] +pub enum ThemeColorPreference { + #[default] + Light, + Dark, + HighContrastDark, + HighContrastLight, +} + +/// Holds all the resolved theme variables +#[derive(Debug, Clone, Default)] +pub struct ThemeBaseColor(HashMap); +impl ThemeBaseColor { + pub fn get(&self, name: &str) -> Option { + self.0.get(name).map(ToOwned::to_owned) + } +} + +pub const THEME_RECURSION_LIMIT: usize = 6; + +#[derive(Debug, Clone, Default)] +pub struct ThemeColor { + pub color_preference: ThemeColorPreference, + pub base: ThemeBaseColor, + pub syntax: HashMap, + pub ui: HashMap, +} + +#[derive(Debug, Clone, Deserialize, Serialize, Default)] +pub struct ThemeBaseConfig(pub BTreeMap); + +impl ThemeBaseConfig { + /// Resolve the variables in this theme base config into the actual colors. + /// The basic idea is just: `"field`: some value` does: + /// - If the value does not start with `$`, then it is a color and we return it + /// - If the value starts with `$` then it is a variable + /// - Look it up in the current theme + /// - If not found, look it up in the default theme + /// - If not found, return `Color::HOT_PINK` as a fallback + /// + /// Note that this applies even if the default theme colors have a variable. + /// This allows the default theme to have, for example, a `$uibg` variable that the current + /// them can override so that if there's ever a new ui element using that variable, the theme + /// does not have to be updated. + pub fn resolve(&self, default: Option<&ThemeBaseConfig>) -> ThemeBaseColor { + let default = default.cloned().unwrap_or_default(); + + let mut base = ThemeBaseColor(HashMap::new()); + + // We resolve all the variables to their values + for (key, value) in self.0.iter() { + match self.resolve_variable(&default, key, value, 0) { + Ok(Some(color)) => { + let color = Color::from_str(color) + .unwrap_or_else(|_| { + tracing::warn!( + "Failed to parse color theme variable for ({key}: {value})" + ); + css::HOT_PINK + }); + base.0.insert(key.to_string(), color); + } + Ok(None) => { + tracing::warn!( + "Failed to resolve color theme variable for ({key}: {value})" + ); + } + Err(err) => { + tracing::error!( + "Failed to resolve color theme variable ({key}: {value}): {err}" + ); + } + } + } + + base + } + + fn resolve_variable<'a>( + &'a self, + defaults: &'a ThemeBaseConfig, + key: &str, + value: &'a str, + i: usize, + ) -> Result, LoadThemeError> { + let Some(value) = value.strip_prefix('$') else { + return Ok(Some(value)); + }; + + if i > THEME_RECURSION_LIMIT { + return Err(LoadThemeError::RecursionLimitReached { + variable_name: key.to_string(), + }); + } + + let target = + self.get(value) + .or_else(|| defaults.get(value)) + .ok_or_else(|| LoadThemeError::VariableNotFound { + variable_name: key.to_string(), + })?; + + self.resolve_variable(defaults, value, target, i + 1) + } + + // Note: this returns an `&String` just to make it consistent with hashmap lookups that are + // also used via ui/syntax + pub fn get(&self, name: &str) -> Option<&String> { + self.0.get(name) + } + + pub fn key_values(&self) -> BTreeMap { + self.0.clone() + } +} + +#[derive(Debug, Clone, Deserialize, Serialize, Default)] +#[serde(rename_all = "kebab-case", default)] +pub struct ColorThemeConfig { + #[serde(skip)] + pub path: PathBuf, + pub name: String, + pub high_contrast: Option, + pub base: ThemeBaseConfig, + pub syntax: BTreeMap, + pub ui: BTreeMap, +} + +impl ColorThemeConfig { + fn resolve_color( + colors: &BTreeMap, + base: &ThemeBaseColor, + default: Option<&HashMap>, + ) -> HashMap { + colors + .iter() + .map(|(name, hex)| { + let color = if let Some(stripped) = hex.strip_prefix('$') { + base.get(stripped) + } else { + Color::from_str(hex).ok() + }; + + let color = color + .or_else(|| { + default.and_then(|default| default.get(name).cloned()) + }) + .unwrap_or(Color::from_rgb8(0, 0, 0)); + + (name.to_string(), color) + }) + .collect() + } + + pub(super) fn resolve_ui_color( + &self, + base: &ThemeBaseColor, + default: Option<&HashMap>, + ) -> HashMap { + Self::resolve_color(&self.ui, base, default) + } + + pub(super) fn resolve_syntax_color( + &self, + base: &ThemeBaseColor, + default: Option<&HashMap>, + ) -> HashMap { + Self::resolve_color(&self.syntax, base, default) + } +} + +#[cfg(test)] +mod tests { + use config::Config; + use floem::{peniko::Color, prelude::palette::css}; + + use crate::{config::LapceConfig, workspace::LapceWorkspace}; + + #[test] + fn test_resolve() { + // Mimicking load + let workspace = LapceWorkspace::default(); + + let config = LapceConfig::merge_config(&workspace, None, None); + let mut lapce_config: LapceConfig = config.try_deserialize().unwrap(); + + let test_theme_str = r##" +[color-theme] +name = "test" +color-preference = "dark" + +[ui] + +[color-theme.base] +"blah" = "#ff00ff" +"text" = "#000000" + +[color-theme.syntax] + +[color-theme.ui] +"lapce.error" = "#ffffff" +"editor.background" = "$blah" +"##; + println!("Test theme: {test_theme_str}"); + let test_theme_cfg = Config::builder() + .add_source(config::File::from_str( + test_theme_str, + config::FileFormat::Toml, + )) + .build() + .unwrap(); + + lapce_config.available_color_themes = + [("test".to_string(), ("test".to_string(), test_theme_cfg))] + .into_iter() + .collect(); + // lapce_config.available_icon_themes = Some(vec![]); + lapce_config.core.color_theme = "test".to_string(); + + lapce_config.resolve_theme(&workspace); + + println!("Hot Pink: {:?}", css::HOT_PINK); + // test basic override + assert_eq!( + lapce_config.color("lapce.error"), + Color::WHITE, + "Failed to get basic theme override" + ); + // test that it falls through to the dark theme for unspecified color + assert_eq!( + lapce_config.color("lapce.warn"), + Color::from_rgb8(0xE5, 0xC0, 0x7B), + "Failed to get from fallback dark theme" + ); // $yellow + // test that our custom variable worked + assert_eq!( + lapce_config.color("editor.background"), + Color::from_rgb8(0xFF, 0x00, 0xFF), + "Failed to get from custom variable" + ); + // test that for text it now uses our redeclared variable + assert_eq!( + lapce_config.color("editor.foreground"), + Color::BLACK, + "Failed to get from custom variable circle back around" + ); + + // don't bother filling color/icon theme list + // don't bother with wrap style list + // don't bother with terminal colors + } +} diff --git a/lapce-app/src/config/core.rs b/lapce-app/src/config/core.rs new file mode 100644 index 0000000..c230b9c --- /dev/null +++ b/lapce-app/src/config/core.rs @@ -0,0 +1,25 @@ +use serde::{Deserialize, Serialize}; +use structdesc::FieldNames; + +#[derive(FieldNames, Debug, Clone, Deserialize, Serialize, Default)] +#[serde(rename_all = "kebab-case")] +pub struct CoreConfig { + #[field_names(desc = "Enable modal editing (Vim like)")] + pub modal: bool, + #[field_names(desc = "Set the color theme of Lapce")] + pub color_theme: String, + #[field_names(desc = "Set the icon theme of Lapce")] + pub icon_theme: String, + #[field_names( + desc = "Enable customised titlebar and disable OS native one (Linux, BSD, Windows)" + )] + pub custom_titlebar: bool, + #[field_names( + desc = "Only allow double-click to open files in the file explorer" + )] + pub file_explorer_double_click: bool, + #[field_names( + desc = "Enable auto-reload for the plugin when its configuration changes." + )] + pub auto_reload_plugin: bool, +} diff --git a/lapce-app/src/config/editor.rs b/lapce-app/src/config/editor.rs new file mode 100644 index 0000000..5d6cb12 --- /dev/null +++ b/lapce-app/src/config/editor.rs @@ -0,0 +1,306 @@ +use floem::views::editor::text::RenderWhitespace; +use serde::{Deserialize, Serialize}; +use structdesc::FieldNames; + +pub const SCALE_OR_SIZE_LIMIT: f64 = 5.0; + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub enum ClickMode { + #[default] + #[serde(rename = "single")] + SingleClick, + #[serde(rename = "file")] + DoubleClickFile, + #[serde(rename = "all")] + DoubleClickAll, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default, PartialEq)] +#[serde(rename_all = "kebab-case")] +pub enum WrapStyle { + /// No wrapping + None, + /// Wrap at the editor width + #[default] + EditorWidth, + // /// Wrap at the wrap-column + // WrapColumn, + /// Wrap at a specific width + WrapWidth, +} +impl WrapStyle { + pub fn as_str(&self) -> &'static str { + match self { + WrapStyle::None => "none", + WrapStyle::EditorWidth => "editor-width", + // WrapStyle::WrapColumn => "wrap-column", + WrapStyle::WrapWidth => "wrap-width", + } + } + + pub fn try_from_str(s: &str) -> Option { + match s { + "none" => Some(WrapStyle::None), + "editor-width" => Some(WrapStyle::EditorWidth), + // "wrap-column" => Some(WrapStyle::WrapColumn), + "wrap-width" => Some(WrapStyle::WrapWidth), + _ => None, + } + } +} + +impl std::fmt::Display for WrapStyle { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.as_str())?; + + Ok(()) + } +} + +#[derive(FieldNames, Debug, Clone, Deserialize, Serialize, Default)] +#[serde(rename_all = "kebab-case")] +pub struct EditorConfig { + #[field_names(desc = "Set the editor font family")] + pub font_family: String, + #[field_names(desc = "Set the editor font size")] + font_size: usize, + #[field_names(desc = "Set the font size in the code glance")] + pub code_glance_font_size: usize, + #[field_names( + desc = "Set the editor line height. If less than 5.0, line height will be a multiple of the font size." + )] + line_height: f64, + #[field_names( + desc = "If enabled, when you input a tab character, it will insert indent that's detected based on your files." + )] + pub smart_tab: bool, + #[field_names(desc = "Set the tab width")] + pub tab_width: usize, + #[field_names(desc = "If opened editors are shown in a tab")] + pub show_tab: bool, + #[field_names(desc = "If navigation breadcrumbs are shown for the file")] + pub show_bread_crumbs: bool, + #[field_names(desc = "If the editor can scroll beyond the last line")] + pub scroll_beyond_last_line: bool, + #[field_names( + desc = "Set the minimum number of visible lines above and below the cursor" + )] + pub cursor_surrounding_lines: usize, + #[field_names(desc = "The kind of wrapping to perform")] + pub wrap_style: WrapStyle, + // #[field_names(desc = "The number of columns to wrap at")] + // pub wrap_column: usize, + #[field_names(desc = "The number of pixels to wrap at")] + pub wrap_width: usize, + #[field_names( + desc = "Show code context like functions and classes at the top of editor when scroll" + )] + pub sticky_header: bool, + #[field_names(desc = "The number of pixels to show completion")] + pub completion_width: usize, + #[field_names( + desc = "If the editor should show the documentation of the current completion item" + )] + pub completion_show_documentation: bool, + #[field_names( + desc = "Should the completion item use the `detail` field to replace the label `field`?" + )] + pub completion_item_show_detail: bool, + #[field_names( + desc = "If the editor should show the signature of the function as the parameters are being typed" + )] + pub show_signature: bool, + #[field_names( + desc = "If the signature view should put the codeblock into a label. This might not work nicely for LSPs which provide invalid code for their labels." + )] + pub signature_label_code_block: bool, + #[field_names( + desc = "Whether the editor should enable automatic closing of matching pairs" + )] + pub auto_closing_matching_pairs: bool, + #[field_names( + desc = "Whether the editor should automatically surround selected text when typing quotes or brackets" + )] + pub auto_surround: bool, + #[field_names( + desc = "How long (in ms) it should take before the hover information appears" + )] + pub hover_delay: u64, + #[field_names( + desc = "If modal mode should have relative line numbers (though, not in insert mode)" + )] + pub modal_mode_relative_line_numbers: bool, + #[field_names( + desc = "Whether it should format the document on save (if there is an available formatter)" + )] + pub format_on_save: bool, + + #[field_names( + desc = "Whether newlines should be automatically converted to the current line ending" + )] + pub normalize_line_endings: bool, + + #[field_names(desc = "If matching brackets are highlighted")] + pub highlight_matching_brackets: bool, + + #[field_names(desc = "If scope lines are highlighted")] + pub highlight_scope_lines: bool, + + #[field_names(desc = "If inlay hints should be displayed")] + pub enable_inlay_hints: bool, + + #[field_names( + desc = "Set the inlay hint font family. If empty, it uses the editor font family." + )] + pub inlay_hint_font_family: String, + #[field_names( + desc = "Set the inlay hint font size. If less than 5 or greater than editor font size, it uses the editor font size." + )] + pub inlay_hint_font_size: usize, + #[field_names(desc = "If diagnostics should be displayed inline")] + pub enable_error_lens: bool, + + #[field_names( + desc = "Only render the styling without displaying messages, provided that `Enable ErrorLens` is enabled" + )] + pub only_render_error_styling: bool, + #[field_names( + desc = "Whether error lens should go to the end of view line, or only to the end of the diagnostic" + )] + pub error_lens_end_of_line: bool, + #[field_names( + desc = "Whether error lens should extend over multiple lines. If false, it will have newlines stripped." + )] + pub error_lens_multiline: bool, + // TODO: Error lens but put entirely on the next line + // TODO: error lens with indentation matching. + #[field_names( + desc = "Set error lens font family. If empty, it uses the inlay hint font family." + )] + pub error_lens_font_family: String, + #[field_names( + desc = "Set the error lens font size. If 0 it uses the inlay hint font size." + )] + pub error_lens_font_size: usize, + #[field_names( + desc = "If the editor should display the completion item as phantom text" + )] + pub enable_completion_lens: bool, + #[field_names(desc = "If the editor should display inline completions")] + pub enable_inline_completion: bool, + #[field_names( + desc = "Set completion lens font family. If empty, it uses the inlay hint font family." + )] + pub completion_lens_font_family: String, + #[field_names( + desc = "Set the completion lens font size. If 0 it uses the inlay hint font size." + )] + pub completion_lens_font_size: usize, + #[field_names( + desc = "Set the cursor blink interval (in milliseconds). Set to 0 to completely disable." + )] + blink_interval: u64, + #[field_names( + desc = "Whether the multiple cursor selection is case sensitive." + )] + pub multicursor_case_sensitive: bool, + #[field_names( + desc = "Whether the multiple cursor selection only selects whole words." + )] + pub multicursor_whole_words: bool, + #[field_names( + desc = "How the editor should render whitespace characters.\nOptions: none, all, boundary, trailing." + )] + pub render_whitespace: RenderWhitespace, + #[field_names(desc = "Whether the editor show indent guide.")] + pub show_indent_guide: bool, + #[field_names( + desc = "Set the auto save delay (in milliseconds), Set to 0 to completely disable" + )] + pub autosave_interval: u64, + #[field_names( + desc = "Whether the document should be formatted when an autosave is triggered (required Format on Save)" + )] + pub format_on_autosave: bool, + #[field_names( + desc = "If enabled the cursor treats leading soft tabs as if they are hard tabs." + )] + pub atomic_soft_tabs: bool, + #[field_names( + desc = "Use a double click to interact with the file explorer.\nOptions: single (default), file or all." + )] + pub double_click: ClickMode, + #[field_names(desc = "Move the focus as you type in the global search box")] + pub move_focus_while_search: bool, + #[field_names( + desc = "Set the default number of visible lines above and below the diff block (-1 for infinite)" + )] + pub diff_context_lines: i32, + #[field_names(desc = "Whether the editor colorizes brackets")] + pub bracket_pair_colorization: bool, + #[field_names(desc = "Bracket colorization Limit")] + pub bracket_colorization_limit: u64, + #[field_names( + desc = "Glob patterns for excluding files and folders (in file explorer)" + )] + pub files_exclude: String, +} + +impl EditorConfig { + pub fn font_size(&self) -> usize { + self.font_size.clamp(6, 32) + } + + pub fn line_height(&self) -> usize { + let line_height = if self.line_height < SCALE_OR_SIZE_LIMIT { + self.line_height * self.font_size as f64 + } else { + self.line_height + }; + + // Prevent overlapping lines + (line_height.round() as usize).max(self.font_size) + } + + pub fn inlay_hint_font_size(&self) -> usize { + if self.inlay_hint_font_size < 5 + || self.inlay_hint_font_size > self.font_size + { + self.font_size() + } else { + self.inlay_hint_font_size + } + } + + pub fn error_lens_font_size(&self) -> usize { + if self.error_lens_font_size == 0 { + self.inlay_hint_font_size() + } else { + self.error_lens_font_size + } + } + + pub fn completion_lens_font_size(&self) -> usize { + if self.completion_lens_font_size == 0 { + self.inlay_hint_font_size() + } else { + self.completion_lens_font_size + } + } + + /// Returns the tab width if atomic soft tabs are enabled. + pub fn atomic_soft_tab_width(&self) -> Option { + if self.atomic_soft_tabs { + Some(self.tab_width) + } else { + None + } + } + + pub fn blink_interval(&self) -> u64 { + if self.blink_interval == 0 { + return 0; + } + self.blink_interval.max(200) + } +} diff --git a/lapce-app/src/config/icon.rs b/lapce-app/src/config/icon.rs new file mode 100644 index 0000000..9fdcbd2 --- /dev/null +++ b/lapce-app/src/config/icon.rs @@ -0,0 +1,171 @@ +pub struct LapceIcons {} + +impl LapceIcons { + pub const WINDOW_CLOSE: &'static str = "window.close"; + pub const WINDOW_RESTORE: &'static str = "window.restore"; + pub const WINDOW_MAXIMIZE: &'static str = "window.maximize"; + pub const WINDOW_MINIMIZE: &'static str = "window.minimize"; + + pub const LOGO: &'static str = "logo"; + pub const MENU: &'static str = "menu"; + pub const LINK: &'static str = "link"; + pub const ERROR: &'static str = "error"; + pub const ADD: &'static str = "add"; + pub const CLOSE: &'static str = "close"; + pub const REMOTE: &'static str = "remote"; + pub const PROBLEM: &'static str = "error"; + pub const DEBUG: &'static str = "debug"; + pub const DEBUG_ALT: &'static str = "debug_alt"; + pub const DEBUG_BREAKPOINT: &'static str = "debug_breakpoint"; + pub const DEBUG_SMALL: &'static str = "debug_small"; + pub const DEBUG_RESTART: &'static str = "debug_restart"; + pub const DEBUG_CONTINUE: &'static str = "debug_continue"; + pub const DEBUG_STEP_OVER: &'static str = "debug_step_over"; + pub const DEBUG_STEP_INTO: &'static str = "debug_step_into"; + pub const DEBUG_STEP_OUT: &'static str = "debug_step_out"; + pub const DEBUG_PAUSE: &'static str = "debug_pause"; + pub const DEBUG_STOP: &'static str = "debug_stop"; + pub const DEBUG_CONSOLE: &'static str = "debug_console"; + pub const DEBUG_DISCONNECT: &'static str = "debug_disconnect"; + pub const START: &'static str = "start"; + pub const RUN_ERRORS: &'static str = "run_errors"; + pub const UNSAVED: &'static str = "unsaved"; + pub const WARNING: &'static str = "warning"; + pub const TERMINAL: &'static str = "terminal"; + pub const SETTINGS: &'static str = "settings"; + pub const LIGHTBULB: &'static str = "lightbulb"; + pub const EXTENSIONS: &'static str = "extensions"; + pub const KEYBOARD: &'static str = "keyboard"; + pub const BREADCRUMB_SEPARATOR: &'static str = "breadcrumb_separator"; + pub const SYMBOL_COLOR: &'static str = "symbol_color"; + pub const TYPE_HIERARCHY: &'static str = "type_hierarchy"; + + pub const FILE: &'static str = "file"; + pub const FILE_EXPLORER: &'static str = "file_explorer"; + pub const FILE_PICKER_UP: &'static str = "file_picker_up"; + + pub const IMAGE_LOADING: &'static str = "image_loading"; + pub const IMAGE_ERROR: &'static str = "image_error"; + + pub const SCM: &'static str = "scm.icon"; + pub const SCM_DIFF_MODIFIED: &'static str = "scm.diff.modified"; + pub const SCM_DIFF_ADDED: &'static str = "scm.diff.added"; + pub const SCM_DIFF_REMOVED: &'static str = "scm.diff.removed"; + pub const SCM_DIFF_RENAMED: &'static str = "scm.diff.renamed"; + pub const SCM_CHANGE_ADD: &'static str = "scm.change.add"; + pub const SCM_CHANGE_REMOVE: &'static str = "scm.change.remove"; + + pub const FOLD: &'static str = "fold"; + pub const FOLD_UP: &'static str = "fold.up"; + pub const FOLD_DOWN: &'static str = "fold.down"; + + pub const PALETTE_MENU: &'static str = "palette.menu"; + + pub const DROPDOWN_ARROW: &'static str = "dropdown.arrow"; + + pub const PANEL_FOLD_UP: &'static str = "panel.fold-up"; + pub const PANEL_FOLD_DOWN: &'static str = "panel.fold-down"; + + pub const LOCATION_BACKWARD: &'static str = "location.backward"; + pub const LOCATION_FORWARD: &'static str = "location.forward"; + + pub const ITEM_OPENED: &'static str = "item.opened"; + pub const ITEM_CLOSED: &'static str = "item.closed"; + + pub const DIRECTORY_CLOSED: &'static str = "directory.closed"; + pub const DIRECTORY_OPENED: &'static str = "directory.opened"; + + pub const PANEL_RESTORE: &'static str = "panel.restore"; + pub const PANEL_MAXIMISE: &'static str = "panel.maximise"; + + pub const SPLIT_HORIZONTAL: &'static str = "split.horizontal"; + + pub const TAB_PREVIOUS: &'static str = "tab.previous"; + pub const TAB_NEXT: &'static str = "tab.next"; + + pub const SIDEBAR_LEFT: &'static str = "sidebar.left.on"; + pub const SIDEBAR_LEFT_OFF: &'static str = "sidebar.left.off"; + pub const SIDEBAR_RIGHT: &'static str = "sidebar.right.on"; + pub const SIDEBAR_RIGHT_OFF: &'static str = "sidebar.right.off"; + + pub const LAYOUT_PANEL: &'static str = "layout.panel.on"; + pub const LAYOUT_PANEL_OFF: &'static str = "layout.panel.off"; + + pub const SEARCH: &'static str = "search.icon"; + pub const SEARCH_CLEAR: &'static str = "search.clear"; + pub const SEARCH_FORWARD: &'static str = "search.forward"; + pub const SEARCH_BACKWARD: &'static str = "search.backward"; + pub const SEARCH_CASE_SENSITIVE: &'static str = "search.case_sensitive"; + pub const SEARCH_WHOLE_WORD: &'static str = "search.whole_word"; + pub const SEARCH_REGEX: &'static str = "search.regex"; + pub const SEARCH_REPLACE: &'static str = "search.replace"; + pub const SEARCH_REPLACE_ALL: &'static str = "search.replace_all"; + + pub const FILE_TYPE_CODE: &'static str = "file-code"; + pub const FILE_TYPE_MEDIA: &'static str = "file-media"; + pub const FILE_TYPE_BINARY: &'static str = "file-binary"; + pub const FILE_TYPE_ARCHIVE: &'static str = "file-zip"; + pub const FILE_TYPE_SUBMODULE: &'static str = "file-submodule"; + pub const FILE_TYPE_SYMLINK_FILE: &'static str = "file-symlink-file"; + pub const FILE_TYPE_SYMLINK_DIRECTORY: &'static str = "file-symlink-directory"; + + pub const DOCUMENT_SYMBOL: &'static str = "document_symbol"; + + pub const REFERENCES: &'static str = "references"; + + pub const IMPLEMENTATION: &'static str = "implementation"; + + pub const SYMBOL_KIND_ARRAY: &'static str = "symbol_kind.array"; + pub const SYMBOL_KIND_BOOLEAN: &'static str = "symbol_kind.boolean"; + pub const SYMBOL_KIND_CLASS: &'static str = "symbol_kind.class"; + pub const SYMBOL_KIND_CONSTANT: &'static str = "symbol_kind.constant"; + pub const SYMBOL_KIND_ENUM_MEMBER: &'static str = "symbol_kind.enum_member"; + pub const SYMBOL_KIND_ENUM: &'static str = "symbol_kind.enum"; + pub const SYMBOL_KIND_EVENT: &'static str = "symbol_kind.event"; + pub const SYMBOL_KIND_FIELD: &'static str = "symbol_kind.field"; + pub const SYMBOL_KIND_FILE: &'static str = "symbol_kind.file"; + pub const SYMBOL_KIND_FUNCTION: &'static str = "symbol_kind.function"; + pub const SYMBOL_KIND_INTERFACE: &'static str = "symbol_kind.interface"; + pub const SYMBOL_KIND_KEY: &'static str = "symbol_kind.key"; + pub const SYMBOL_KIND_METHOD: &'static str = "symbol_kind.method"; + pub const SYMBOL_KIND_NAMESPACE: &'static str = "symbol_kind.namespace"; + pub const SYMBOL_KIND_NUMBER: &'static str = "symbol_kind.number"; + pub const SYMBOL_KIND_OBJECT: &'static str = "symbol_kind.namespace"; + pub const SYMBOL_KIND_OPERATOR: &'static str = "symbol_kind.operator"; + pub const SYMBOL_KIND_PROPERTY: &'static str = "symbol_kind.property"; + pub const SYMBOL_KIND_STRING: &'static str = "symbol_kind.string"; + pub const SYMBOL_KIND_STRUCT: &'static str = "symbol_kind.struct"; + pub const SYMBOL_KIND_TYPE_PARAMETER: &'static str = + "symbol_kind.type_parameter"; + pub const SYMBOL_KIND_VARIABLE: &'static str = "symbol_kind.variable"; + + pub const COMPLETION_ITEM_KIND_CLASS: &'static str = + "completion_item_kind.class"; + pub const COMPLETION_ITEM_KIND_CONSTANT: &'static str = + "completion_item_kind.constant"; + pub const COMPLETION_ITEM_KIND_ENUM_MEMBER: &'static str = + "completion_item_kind.enum_member"; + pub const COMPLETION_ITEM_KIND_ENUM: &'static str = "completion_item_kind.enum"; + pub const COMPLETION_ITEM_KIND_FIELD: &'static str = + "completion_item_kind.field"; + pub const COMPLETION_ITEM_KIND_FUNCTION: &'static str = + "completion_item_kind.function"; + pub const COMPLETION_ITEM_KIND_INTERFACE: &'static str = + "completion_item_kind.interface"; + pub const COMPLETION_ITEM_KIND_KEYWORD: &'static str = + "completion_item_kind.keyword"; + pub const COMPLETION_ITEM_KIND_METHOD: &'static str = + "completion_item_kind.method"; + pub const COMPLETION_ITEM_KIND_MODULE: &'static str = + "completion_item_kind.module"; + pub const COMPLETION_ITEM_KIND_PROPERTY: &'static str = + "completion_item_kind.property"; + pub const COMPLETION_ITEM_KIND_SNIPPET: &'static str = + "completion_item_kind.snippet"; + pub const COMPLETION_ITEM_KIND_STRING: &'static str = + "completion_item_kind.string"; + pub const COMPLETION_ITEM_KIND_STRUCT: &'static str = + "completion_item_kind.struct"; + pub const COMPLETION_ITEM_KIND_VARIABLE: &'static str = + "completion_item_kind.variable"; +} diff --git a/lapce-app/src/config/icon_theme.rs b/lapce-app/src/config/icon_theme.rs new file mode 100644 index 0000000..262dffa --- /dev/null +++ b/lapce-app/src/config/icon_theme.rs @@ -0,0 +1,243 @@ +use std::{ + ffi::OsStr, + path::{Path, PathBuf}, +}; + +use indexmap::IndexMap; +use serde::{Deserialize, Serialize}; + +/// Returns the first item yielded from `items` if at least one item is yielded, all yielded items +/// are `Some`, and all yielded items compare equal, else returns `None`. +fn try_all_equal_value>>( + items: I, +) -> Option { + let mut items = items.into_iter(); + let first = items.next().flatten()?; + + items.try_fold(first, |initial_item, item| { + item.and_then(|item| (item == initial_item).then_some(initial_item)) + }) +} + +#[derive(Debug, Clone, Deserialize, Serialize, Default)] +#[serde(rename_all = "kebab-case", default)] +pub struct IconThemeConfig { + #[serde(skip)] + pub path: PathBuf, + pub name: String, + pub use_editor_color: Option, + pub ui: IndexMap, + pub foldername: IndexMap, + pub filename: IndexMap, + pub extension: IndexMap, +} + +impl IconThemeConfig { + /// If all paths in `paths` have the same file type (as determined by the file name or + /// extension), and there is an icon associated with that file type, returns the path of the + /// icon. + pub fn resolve_path_to_icon(&self, paths: &[&Path]) -> Option { + let file_names = paths + .iter() + .map(|path| path.file_name().and_then(OsStr::to_str)); + let file_name_icon = try_all_equal_value(file_names) + .and_then(|file_name| self.filename.get(file_name)); + + file_name_icon + .or_else(|| { + let extensions = paths + .iter() + .map(|path| path.extension().and_then(OsStr::to_str)); + + try_all_equal_value(extensions) + .and_then(|extension| self.extension.get(extension)) + }) + .map(|icon| self.path.join(icon)) + } +} + +#[cfg(test)] +mod tests { + use super::IconThemeConfig; + use crate::config::icon_theme::try_all_equal_value; + + #[test] + fn try_all_equal_value_empty_none() { + assert_eq!(Option::::None, try_all_equal_value([])); + } + + #[test] + fn try_all_equal_value_any_none_none() { + assert_eq!(Option::::None, try_all_equal_value([None])); + assert_eq!( + Option::::None, + try_all_equal_value([None, Some(1), Some(1)]) + ); + assert_eq!(Option::::None, try_all_equal_value([Some(0), None])); + assert_eq!( + Option::::None, + try_all_equal_value([Some(3), Some(3), None, Some(3)]) + ); + } + + #[test] + fn try_all_equal_value_any_different_none() { + assert_eq!(Option::::None, try_all_equal_value([Some(1), Some(2)])); + assert_eq!( + Option::::None, + try_all_equal_value([Some(1), Some(10), Some(1)]) + ); + assert_eq!( + Option::::None, + try_all_equal_value([Some(3), Some(3), Some(3), Some(3), Some(2)]) + ); + assert_eq!( + Option::::None, + try_all_equal_value([Some(5), Some(4), Some(4), Some(4), Some(4)]) + ); + assert_eq!( + Option::::None, + try_all_equal_value([Some(3), Some(0), Some(9), Some(20), Some(1)]) + ); + } + + #[test] + fn try_all_equal_value_all_same_some() { + assert_eq!(Option::::Some(1), try_all_equal_value([Some(1)])); + assert_eq!(Option::::Some(-2), try_all_equal_value([Some(-2); 2])); + assert_eq!(Option::::Some(0), try_all_equal_value([Some(0); 3])); + assert_eq!(Option::::Some(30), try_all_equal_value([Some(30); 57])); + } + + fn get_icon_theme_config() -> IconThemeConfig { + IconThemeConfig { + path: "icons".to_owned().into(), + filename: [("Makefile", "makefile.svg"), ("special.rs", "special.svg")] + .map(|(k, v)| (k.to_owned(), v.to_owned())) + .into(), + extension: [("rs", "rust.svg"), ("c", "c.svg"), ("py", "python.svg")] + .map(|(k, v)| (k.to_owned(), v.to_owned())) + .into(), + ..Default::default() + } + } + + #[test] + fn resolve_path_to_icon_no_paths_none() { + let icon_theme_config = get_icon_theme_config(); + + assert_eq!(None, icon_theme_config.resolve_path_to_icon(&[])); + } + + #[test] + fn resolve_path_to_icon_different_none() { + let icon_theme_config = get_icon_theme_config(); + + assert_eq!( + None, + icon_theme_config + .resolve_path_to_icon(&["foo.rs", "bar.c"].map(AsRef::as_ref)) + ); + assert_eq!( + None, + icon_theme_config.resolve_path_to_icon( + &["/some/path/main.py", "other/path.py", "dir1/./dir2/file.rs"] + .map(AsRef::as_ref) + ) + ); + assert_eq!( + None, + icon_theme_config.resolve_path_to_icon( + &["/root/Makefile", "dir/dir/special.rs", "../../main.rs"] + .map(AsRef::as_ref) + ) + ); + assert_eq!( + None, + icon_theme_config + .resolve_path_to_icon(&["main.c", "foo.txt"].map(AsRef::as_ref)) + ); + } + + #[test] + fn resolve_path_to_icon_no_match_none() { + let icon_theme_config = get_icon_theme_config(); + + assert_eq!( + None, + icon_theme_config.resolve_path_to_icon(&["foo"].map(AsRef::as_ref)) + ); + assert_eq!( + None, + icon_theme_config.resolve_path_to_icon( + &["/some/path/file.txt", "other/path.txt"].map(AsRef::as_ref) + ) + ); + assert_eq!( + None, + icon_theme_config.resolve_path_to_icon( + &["folder/file", "/home/user/file", "../../file"].map(AsRef::as_ref) + ) + ); + assert_eq!( + None, + icon_theme_config.resolve_path_to_icon(&[".."].map(AsRef::as_ref)) + ); + assert_eq!( + None, + icon_theme_config.resolve_path_to_icon(&["."].map(AsRef::as_ref)) + ); + } + + #[test] + fn resolve_path_to_icon_file_name_match_some() { + let icon_theme_config = get_icon_theme_config(); + + assert_eq!( + Some("icons/makefile.svg".to_owned().into()), + icon_theme_config.resolve_path_to_icon(&["Makefile"].map(AsRef::as_ref)) + ); + assert_eq!( + Some("icons/makefile.svg".to_owned().into()), + icon_theme_config.resolve_path_to_icon( + &[ + "baz/Makefile", + "/foo/bar/dir/Makefile", + ".././/././Makefile" + ] + .map(AsRef::as_ref) + ) + ); + assert_eq!( + Some("icons/special.svg".to_owned().into()), + icon_theme_config.resolve_path_to_icon( + &["dir/special.rs", "/dir1/dir2/..//./special.rs"] + .map(AsRef::as_ref) + ) + ); + } + + #[test] + fn resolve_path_to_icon_extension_match_some() { + let icon_theme_config = get_icon_theme_config(); + + assert_eq!( + Some("icons/python.svg".to_owned().into()), + icon_theme_config + .resolve_path_to_icon(&["source.py"].map(AsRef::as_ref)) + ); + assert_eq!( + Some("icons/rust.svg".to_owned().into()), + icon_theme_config.resolve_path_to_icon( + &["/home/user/main.rs", "../../special.rs.rs", "special.rs"] + .map(AsRef::as_ref) + ) + ); + assert_eq!( + Some("icons/c.svg".to_owned().into()), + icon_theme_config.resolve_path_to_icon( + &["/dir1/Makefile.c", "../main.c"].map(AsRef::as_ref) + ) + ); + } +} diff --git a/lapce-app/src/config/svg.rs b/lapce-app/src/config/svg.rs new file mode 100644 index 0000000..5a8ac83 --- /dev/null +++ b/lapce-app/src/config/svg.rs @@ -0,0 +1,65 @@ +use std::{ + collections::HashMap, + fs, + path::{Path, PathBuf}, +}; + +use include_dir::{Dir, include_dir}; + +use crate::config::LOGO; + +const CODICONS_ICONS_DIR: Dir = + include_dir!("$CARGO_MANIFEST_DIR/../icons/codicons"); +const LAPCE_ICONS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/../icons/lapce"); + +#[derive(Debug, Clone)] +pub struct SvgStore { + svgs: HashMap, + svgs_on_disk: HashMap>, +} + +impl Default for SvgStore { + fn default() -> Self { + Self::new() + } +} + +impl SvgStore { + fn new() -> Self { + let mut svgs = HashMap::new(); + svgs.insert("lapce_logo".to_string(), LOGO.to_string()); + + Self { + svgs, + svgs_on_disk: HashMap::new(), + } + } + + pub fn logo_svg(&self) -> String { + self.svgs.get("lapce_logo").unwrap().clone() + } + + pub fn get_default_svg(&mut self, name: &str) -> String { + if !self.svgs.contains_key(name) { + let file = if name == "lapce_remote.svg" || name == "lapce_logo.svg" { + LAPCE_ICONS_DIR.get_file(name).unwrap() + } else { + CODICONS_ICONS_DIR + .get_file(name) + .unwrap_or_else(|| panic!("Failed to unwrap {name}")) + }; + let content = file.contents_utf8().unwrap(); + self.svgs.insert(name.to_string(), content.to_string()); + } + self.svgs.get(name).unwrap().clone() + } + + pub fn get_svg_on_disk(&mut self, path: &Path) -> Option { + if !self.svgs_on_disk.contains_key(path) { + let svg = fs::read_to_string(path).ok(); + self.svgs_on_disk.insert(path.to_path_buf(), svg); + } + + self.svgs_on_disk.get(path).unwrap().clone() + } +} diff --git a/lapce-app/src/config/terminal.rs b/lapce-app/src/config/terminal.rs new file mode 100644 index 0000000..2f151b4 --- /dev/null +++ b/lapce-app/src/config/terminal.rs @@ -0,0 +1,101 @@ +use std::{collections::HashMap, sync::Arc}; + +use floem::peniko::Color; +use serde::{Deserialize, Serialize}; +use structdesc::FieldNames; + +#[derive(FieldNames, Debug, Clone, Deserialize, Serialize, Default)] +#[serde(rename_all = "kebab-case")] +pub struct TerminalConfig { + #[field_names( + desc = "Set the terminal font family. If empty, it uses editor font family." + )] + pub font_family: String, + #[field_names( + desc = "Set the terminal font size, If 0, it uses editor font size." + )] + pub font_size: usize, + #[field_names( + desc = "Set the terminal line height, If 0, it uses editor line height" + )] + pub line_height: f64, + + #[field_names(skip)] + pub profiles: HashMap, + #[field_names(skip)] + pub default_profile: HashMap, + + #[serde(skip)] + #[field_names(skip)] + pub indexed_colors: Arc>, +} + +#[derive(FieldNames, Debug, Clone, Deserialize, Serialize, Default)] +#[serde(rename_all = "kebab-case")] +pub struct TerminalProfile { + #[field_names(desc = "Command to execute when launching terminal")] + pub command: Option, + #[field_names(desc = "Arguments passed to command")] + pub arguments: Option>, + #[field_names(desc = "Command to execute when launching terminal")] + pub workdir: Option, + #[field_names(desc = "Arguments passed to command")] + pub environment: Option>, +} + +impl TerminalConfig { + pub fn get_indexed_colors(&mut self) { + let mut indexed_colors = HashMap::new(); + // Build colors. + for r in 0..6 { + for g in 0..6 { + for b in 0..6 { + // Override colors 16..232 with the config (if present). + let index = 16 + r * 36 + g * 6 + b; + let color = Color::from_rgb8( + if r == 0 { 0 } else { r * 40 + 55 }, + if g == 0 { 0 } else { g * 40 + 55 }, + if b == 0 { 0 } else { b * 40 + 55 }, + ); + indexed_colors.insert(index, color); + } + } + } + + let index: u8 = 232; + + for i in 0..24 { + // Override colors 232..256 with the config (if present). + + let value = i * 10 + 8; + indexed_colors.insert(index + i, Color::from_rgb8(value, value, value)); + } + + self.indexed_colors = Arc::new(indexed_colors); + } + + pub fn get_default_profile( + &self, + ) -> Option { + let profile = self.profiles.get( + self.default_profile + .get(std::env::consts::OS) + .unwrap_or(&String::from("default")), + )?; + let workdir = if let Some(workdir) = &profile.workdir { + url::Url::parse(&workdir.display().to_string()).ok() + } else { + None + }; + + let profile = profile.clone(); + + Some(lapce_rpc::terminal::TerminalProfile { + name: std::env::consts::OS.to_string(), + command: profile.command, + arguments: profile.arguments, + workdir, + environment: profile.environment, + }) + } +} diff --git a/lapce-app/src/config/ui.rs b/lapce-app/src/config/ui.rs new file mode 100644 index 0000000..79b427b --- /dev/null +++ b/lapce-app/src/config/ui.rs @@ -0,0 +1,144 @@ +use floem::text::FamilyOwned; +use serde::{Deserialize, Serialize}; +use structdesc::FieldNames; + +#[derive(FieldNames, Debug, Clone, Deserialize, Serialize, Default)] +#[serde(rename_all = "kebab-case")] +pub struct UIConfig { + #[field_names(desc = "Set the UI scale. Defaults to 1.0")] + scale: f64, + + #[field_names( + desc = "Set the UI font family. If empty, it uses system default." + )] + pub font_family: String, + + #[field_names(desc = "Set the UI base font size")] + font_size: usize, + + #[field_names(desc = "Set the icon size in the UI")] + icon_size: usize, + + #[field_names( + desc = "Set the header height for panel header and editor tab header" + )] + header_height: usize, + + #[field_names(desc = "Set the height for status line")] + status_height: usize, + + #[field_names(desc = "Set the minimum width for editor tab")] + tab_min_width: usize, + + #[field_names( + desc = "Set whether the editor tab separator should be full height or the height of the content" + )] + pub tab_separator_height: TabSeparatorHeight, + + #[field_names(desc = "Set the width for scroll bar")] + scroll_width: usize, + + #[field_names(desc = "Controls the width of drop shadow in the UI")] + drop_shadow_width: usize, + + #[field_names(desc = "Controls the width of the command palette")] + palette_width: usize, + + #[field_names( + desc = "Set the hover font family. If empty, it uses the UI font family" + )] + hover_font_family: String, + #[field_names(desc = "Set the hover font size. If 0, uses the UI font size")] + hover_font_size: usize, + + #[field_names(desc = "Trim whitespace from search results")] + pub trim_search_results_whitespace: bool, + + #[field_names(desc = "Set the line height for list items")] + list_line_height: usize, + + #[field_names(desc = "Set position of the close button in editor tabs")] + pub tab_close_button: TabCloseButton, + + #[field_names(desc = "Display the Open Editors section in the explorer")] + pub open_editors_visible: bool, +} + +#[derive( + Debug, + Clone, + Copy, + Deserialize, + Serialize, + Default, + PartialEq, + Eq, + PartialOrd, + Ord, + strum_macros::VariantNames, +)] +pub enum TabCloseButton { + Left, + #[default] + Right, + Off, +} + +#[derive( + Debug, + Clone, + Copy, + Deserialize, + Serialize, + Default, + PartialEq, + Eq, + PartialOrd, + Ord, + strum_macros::VariantNames, +)] +pub enum TabSeparatorHeight { + #[default] + Content, + Full, +} + +impl UIConfig { + pub fn scale(&self) -> f64 { + self.scale.clamp(0.1, 4.0) + } + + pub fn font_size(&self) -> usize { + self.font_size.clamp(6, 32) + } + + pub fn font_family(&self) -> Vec { + FamilyOwned::parse_list(&self.font_family).collect() + } + + pub fn header_height(&self) -> usize { + let font_size = self.font_size(); + self.header_height.max(font_size) + } + + pub fn icon_size(&self) -> usize { + if self.icon_size == 0 { + self.font_size() + } else { + self.icon_size.clamp(6, 32) + } + } + + pub fn status_height(&self) -> usize { + let font_size = self.font_size(); + self.status_height.max(font_size) + } + + pub fn palette_width(&self) -> usize { + if self.palette_width == 0 { + 500 + } else { + self.palette_width.max(100) + } + } +} diff --git a/lapce-app/src/config/watcher.rs b/lapce-app/src/config/watcher.rs new file mode 100644 index 0000000..668c7b0 --- /dev/null +++ b/lapce-app/src/config/watcher.rs @@ -0,0 +1,55 @@ +use std::sync::{Arc, atomic::AtomicBool, mpsc::Sender}; + +pub struct ConfigWatcher { + tx: Sender<()>, + delay_handler: Arc, +} + +impl notify::EventHandler for ConfigWatcher { + fn handle_event(&mut self, event: notify::Result) { + match event { + Ok(event) => match event.kind { + notify::EventKind::Create(_) + | notify::EventKind::Modify(_) + | notify::EventKind::Remove(_) => { + if self + .delay_handler + .compare_exchange( + false, + true, + std::sync::atomic::Ordering::Relaxed, + std::sync::atomic::Ordering::Relaxed, + ) + .is_ok() + { + let config_mutex = self.delay_handler.clone(); + let tx = self.tx.clone(); + std::thread::spawn(move || { + std::thread::sleep(std::time::Duration::from_millis( + 500, + )); + if let Err(err) = tx.send(()) { + tracing::error!("{:?}", err); + } + config_mutex + .store(false, std::sync::atomic::Ordering::Relaxed); + }); + } + } + _ => {} + }, + Err(err) => { + tracing::error!("{:?}", err); + } + } + } +} + +impl ConfigWatcher { + pub fn new(tx: Sender<()>) -> Self { + Self { + tx, + delay_handler: Arc::new(AtomicBool::new(false)), + } + } +} diff --git a/lapce-app/src/db.rs b/lapce-app/src/db.rs new file mode 100644 index 0000000..fd56b05 --- /dev/null +++ b/lapce-app/src/db.rs @@ -0,0 +1,452 @@ +use std::{ + path::{Path, PathBuf}, + rc::Rc, + sync::Arc, +}; + +use anyhow::{Result, anyhow}; +use crossbeam_channel::{Sender, unbounded}; +use floem::{peniko::kurbo::Vec2, reactive::SignalGet}; +use lapce_core::directory::Directory; +use lapce_rpc::plugin::VoltID; +use sha2::{Digest, Sha256}; + +use crate::{ + app::{AppData, AppInfo}, + doc::DocInfo, + panel::{data::PanelOrder, kind::PanelKind}, + window::{WindowData, WindowInfo}, + window_tab::WindowTabData, + workspace::{LapceWorkspace, WorkspaceInfo}, +}; + +const APP: &str = "app"; +const WINDOW: &str = "window"; +const WORKSPACE_INFO: &str = "workspace_info"; +const WORKSPACE_FILES: &str = "workspace_files"; +const PANEL_ORDERS: &str = "panel_orders"; +const DISABLED_VOLTS: &str = "disabled_volts"; +const RECENT_WORKSPACES: &str = "recent_workspaces"; + +pub enum SaveEvent { + App(AppInfo), + Workspace(LapceWorkspace, WorkspaceInfo), + RecentWorkspace(LapceWorkspace), + Doc(DocInfo), + DisabledVolts(Vec), + WorkspaceDisabledVolts(Arc, Vec), + PanelOrder(PanelOrder), +} + +#[derive(Clone)] +pub struct LapceDb { + folder: PathBuf, + workspace_folder: PathBuf, + save_tx: Sender, +} + +impl LapceDb { + pub fn new() -> Result { + let folder = Directory::config_directory() + .ok_or_else(|| anyhow!("can't get config directory"))? + .join("db"); + let workspace_folder = folder.join("workspaces"); + if let Err(err) = std::fs::create_dir_all(&workspace_folder) { + tracing::error!("{:?}", err); + } + + let (save_tx, save_rx) = unbounded(); + + let db = Self { + save_tx, + workspace_folder, + folder, + }; + let local_db = db.clone(); + std::thread::Builder::new() + .name("SaveEventHandler".to_owned()) + .spawn(move || -> Result<()> { + loop { + let event = save_rx.recv()?; + match event { + SaveEvent::App(info) => { + if let Err(err) = local_db.insert_app_info(info) { + tracing::error!("{:?}", err); + } + } + SaveEvent::Workspace(workspace, info) => { + if let Err(err) = + local_db.insert_workspace(&workspace, &info) + { + tracing::error!("{:?}", err); + } + } + SaveEvent::RecentWorkspace(workspace) => { + if let Err(err) = + local_db.insert_recent_workspace(workspace) + { + tracing::error!("{:?}", err); + } + } + SaveEvent::Doc(info) => { + if let Err(err) = local_db.insert_doc(&info) { + tracing::error!("{:?}", err); + } + } + SaveEvent::DisabledVolts(volts) => { + if let Err(err) = local_db.insert_disabled_volts(volts) { + tracing::error!("{:?}", err); + } + } + SaveEvent::WorkspaceDisabledVolts(workspace, volts) => { + if let Err(err) = local_db + .insert_workspace_disabled_volts(workspace, volts) + { + tracing::error!("{:?}", err); + } + } + SaveEvent::PanelOrder(order) => { + if let Err(err) = local_db.insert_panel_orders(&order) { + tracing::error!("{:?}", err); + } + } + } + } + }) + .unwrap(); + Ok(db) + } + + pub fn get_disabled_volts(&self) -> Result> { + let volts = std::fs::read_to_string(self.folder.join(DISABLED_VOLTS))?; + let volts: Vec = serde_json::from_str(&volts)?; + Ok(volts) + } + + pub fn save_disabled_volts(&self, volts: Vec) { + if let Err(err) = self.save_tx.send(SaveEvent::DisabledVolts(volts)) { + tracing::error!("{:?}", err); + } + } + + pub fn save_workspace_disabled_volts( + &self, + workspace: Arc, + volts: Vec, + ) { + if let Err(err) = self + .save_tx + .send(SaveEvent::WorkspaceDisabledVolts(workspace, volts)) + { + tracing::error!("{:?}", err); + } + } + + pub fn insert_disabled_volts(&self, volts: Vec) -> Result<()> { + let volts = serde_json::to_string_pretty(&volts)?; + std::fs::write(self.folder.join(DISABLED_VOLTS), volts)?; + Ok(()) + } + + pub fn insert_workspace_disabled_volts( + &self, + workspace: Arc, + volts: Vec, + ) -> Result<()> { + let folder = self + .workspace_folder + .join(workspace_folder_name(&workspace)); + if let Err(err) = std::fs::create_dir_all(&folder) { + tracing::error!("{:?}", err); + } + + let volts = serde_json::to_string_pretty(&volts)?; + std::fs::write(folder.join(DISABLED_VOLTS), volts)?; + Ok(()) + } + + pub fn get_workspace_disabled_volts( + &self, + workspace: &LapceWorkspace, + ) -> Result> { + let folder = self.workspace_folder.join(workspace_folder_name(workspace)); + let volts = std::fs::read_to_string(folder.join(DISABLED_VOLTS))?; + let volts: Vec = serde_json::from_str(&volts)?; + Ok(volts) + } + + pub fn recent_workspaces(&self) -> Result> { + let workspaces = + std::fs::read_to_string(self.folder.join(RECENT_WORKSPACES))?; + let workspaces: Vec = serde_json::from_str(&workspaces)?; + Ok(workspaces) + } + + pub fn update_recent_workspace(&self, workspace: &LapceWorkspace) -> Result<()> { + if workspace.path.is_none() { + return Ok(()); + } + self.save_tx + .send(SaveEvent::RecentWorkspace(workspace.clone()))?; + Ok(()) + } + + fn insert_recent_workspace(&self, workspace: LapceWorkspace) -> Result<()> { + let mut workspaces = self.recent_workspaces().unwrap_or_default(); + + let mut exits = false; + for w in workspaces.iter_mut() { + if w.path == workspace.path && w.kind == workspace.kind { + w.last_open = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(); + exits = true; + break; + } + } + if !exits { + let mut workspace = workspace; + workspace.last_open = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(); + workspaces.push(workspace); + } + workspaces.sort_by_key(|w| -(w.last_open as i64)); + let workspaces = serde_json::to_string_pretty(&workspaces)?; + std::fs::write(self.folder.join(RECENT_WORKSPACES), workspaces)?; + + Ok(()) + } + + pub fn save_window_tab(&self, data: Rc) -> Result<()> { + let workspace = (*data.workspace).clone(); + let workspace_info = data.workspace_info(); + + self.save_tx + .send(SaveEvent::Workspace(workspace, workspace_info))?; + // self.insert_unsaved_buffer(main_split)?; + + Ok(()) + } + + pub fn get_workspace_info( + &self, + workspace: &LapceWorkspace, + ) -> Result { + let info = std::fs::read_to_string( + self.workspace_folder + .join(workspace_folder_name(workspace)) + .join(WORKSPACE_INFO), + )?; + let info: WorkspaceInfo = serde_json::from_str(&info)?; + Ok(info) + } + + fn insert_workspace( + &self, + workspace: &LapceWorkspace, + info: &WorkspaceInfo, + ) -> Result<()> { + let folder = self.workspace_folder.join(workspace_folder_name(workspace)); + if let Err(err) = std::fs::create_dir_all(&folder) { + tracing::error!("{:?}", err); + } + let workspace_info = serde_json::to_string_pretty(info)?; + std::fs::write(folder.join(WORKSPACE_INFO), workspace_info)?; + Ok(()) + } + + pub fn save_app(&self, data: &AppData) -> Result<()> { + let windows = data.windows.get_untracked(); + for (_, window) in &windows { + if let Err(err) = self.save_window(window.clone()) { + tracing::error!("{:?}", err); + } + } + + let info = AppInfo { + windows: windows + .iter() + .map(|(_, window_data)| window_data.info()) + .collect(), + }; + if info.windows.is_empty() { + return Ok(()); + } + + self.save_tx.send(SaveEvent::App(info))?; + + Ok(()) + } + + pub fn insert_app_info(&self, info: AppInfo) -> Result<()> { + let info = serde_json::to_string_pretty(&info)?; + std::fs::write(self.folder.join(APP), info)?; + Ok(()) + } + + pub fn insert_app(&self, data: AppData) -> Result<()> { + let windows = data.windows.get_untracked(); + if windows.is_empty() { + // insert_app is called after window is closed, so we don't want to store it + return Ok(()); + } + for (_, window) in &windows { + if let Err(err) = self.insert_window(window.clone()) { + tracing::error!("{:?}", err); + } + } + let info = AppInfo { + windows: windows + .iter() + .map(|(_, window_data)| window_data.info()) + .collect(), + }; + self.insert_app_info(info)?; + Ok(()) + } + + pub fn get_app(&self) -> Result { + let info = std::fs::read_to_string(self.folder.join(APP))?; + let mut info: AppInfo = serde_json::from_str(&info)?; + for window in info.windows.iter_mut() { + if window.size.width < 10.0 { + window.size.width = 800.0; + } + if window.size.height < 10.0 { + window.size.height = 600.0; + } + } + Ok(info) + } + + pub fn get_window(&self) -> Result { + let info = std::fs::read_to_string(self.folder.join(WINDOW))?; + let mut info: WindowInfo = serde_json::from_str(&info)?; + if info.size.width < 10.0 { + info.size.width = 800.0; + } + if info.size.height < 10.0 { + info.size.height = 600.0; + } + Ok(info) + } + + pub fn save_window(&self, data: WindowData) -> Result<()> { + for (_, window_tab) in data.window_tabs.get_untracked().into_iter() { + if let Err(err) = self.save_window_tab(window_tab) { + tracing::error!("{:?}", err); + } + } + Ok(()) + } + + pub fn insert_window(&self, data: WindowData) -> Result<()> { + for (_, window_tab) in data.window_tabs.get_untracked().into_iter() { + if let Err(err) = self.insert_window_tab(window_tab) { + tracing::error!("{:?}", err); + } + } + let info = data.info(); + let info = serde_json::to_string_pretty(&info)?; + std::fs::write(self.folder.join(WINDOW), info)?; + Ok(()) + } + + pub fn insert_window_tab(&self, data: Rc) -> Result<()> { + let workspace = (*data.workspace).clone(); + let workspace_info = data.workspace_info(); + + self.insert_workspace(&workspace, &workspace_info)?; + // self.insert_unsaved_buffer(main_split)?; + + Ok(()) + } + + pub fn get_panel_orders(&self) -> Result { + let panel_orders = std::fs::read_to_string(self.folder.join(PANEL_ORDERS))?; + let mut panel_orders: PanelOrder = serde_json::from_str(&panel_orders)?; + + use strum::IntoEnumIterator; + for kind in PanelKind::iter() { + if kind.position(&panel_orders).is_none() { + let panels = + panel_orders.entry(kind.default_position()).or_default(); + panels.push_back(kind); + } + } + + Ok(panel_orders) + } + + pub fn save_panel_orders(&self, order: PanelOrder) { + if let Err(err) = self.save_tx.send(SaveEvent::PanelOrder(order)) { + tracing::error!("{:?}", err); + } + } + + fn insert_panel_orders(&self, order: &PanelOrder) -> Result<()> { + let info = serde_json::to_string_pretty(order)?; + std::fs::write(self.folder.join(PANEL_ORDERS), info)?; + Ok(()) + } + + pub fn save_doc_position( + &self, + workspace: &LapceWorkspace, + path: PathBuf, + cursor_offset: usize, + scroll_offset: Vec2, + ) { + let info = DocInfo { + workspace: workspace.clone(), + path, + scroll_offset: (scroll_offset.x, scroll_offset.y), + cursor_offset, + }; + if let Err(err) = self.save_tx.send(SaveEvent::Doc(info)) { + tracing::error!("{:?}", err); + } + } + + fn insert_doc(&self, info: &DocInfo) -> Result<()> { + let folder = self + .workspace_folder + .join(workspace_folder_name(&info.workspace)) + .join(WORKSPACE_FILES); + if let Err(err) = std::fs::create_dir_all(&folder) { + tracing::error!("{:?}", err); + } + let contents = serde_json::to_string_pretty(info)?; + std::fs::write(folder.join(doc_path_name(&info.path)), contents)?; + Ok(()) + } + + pub fn get_doc_info( + &self, + workspace: &LapceWorkspace, + path: &Path, + ) -> Result { + let folder = self + .workspace_folder + .join(workspace_folder_name(workspace)) + .join(WORKSPACE_FILES); + let info = std::fs::read_to_string(folder.join(doc_path_name(path)))?; + let info: DocInfo = serde_json::from_str(&info)?; + Ok(info) + } +} + +fn workspace_folder_name(workspace: &LapceWorkspace) -> String { + url::form_urlencoded::Serializer::new(String::new()) + .append_key_only(&workspace.to_string()) + .finish() +} + +fn doc_path_name(path: &Path) -> String { + let mut hasher = Sha256::new(); + hasher.update(path.to_string_lossy().as_bytes()); + format!("{:x}", hasher.finalize()) +} diff --git a/lapce-app/src/debug.rs b/lapce-app/src/debug.rs new file mode 100644 index 0000000..4ecf75d --- /dev/null +++ b/lapce-app/src/debug.rs @@ -0,0 +1,640 @@ +use std::{ + collections::{BTreeMap, HashMap}, + fmt::Display, + path::PathBuf, + rc::Rc, + time::Instant, +}; + +use floem::{ + ext_event::create_ext_action, + reactive::{Memo, RwSignal, Scope, SignalGet, SignalUpdate, SignalWith}, + views::VirtualVector, +}; +use lapce_rpc::{ + dap_types::{ + self, DapId, RunDebugConfig, SourceBreakpoint, StackFrame, Stopped, + ThreadId, Variable, + }, + proxy::ProxyResponse, + terminal::TermId, +}; +use serde::{Deserialize, Serialize}; + +use crate::{ + command::InternalCommand, + editor::location::{EditorLocation, EditorPosition}, + window_tab::CommonData, +}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum RunDebugMode { + Run, + Debug, +} + +impl Display for RunDebugMode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let s = match self { + RunDebugMode::Run => "Run", + RunDebugMode::Debug => "Debug", + }; + f.write_str(s) + } +} + +#[derive(Clone)] +pub struct RunDebugProcess { + pub mode: RunDebugMode, + pub config: RunDebugConfig, + pub stopped: bool, + pub created: Instant, + pub is_prelaunch: bool, +} + +#[derive(Deserialize, Serialize)] +pub struct RunDebugConfigs { + pub configs: Vec, +} + +#[derive(Clone)] +pub struct RunDebugData { + pub active_term: RwSignal>, + pub daps: RwSignal>, + pub breakpoints: RwSignal>>, +} + +impl RunDebugData { + pub fn new( + cx: Scope, + breakpoints: RwSignal>>, + ) -> Self { + let active_term: RwSignal> = cx.create_rw_signal(None); + let daps: RwSignal> = + cx.create_rw_signal(im::HashMap::new()); + + Self { + active_term, + daps, + breakpoints, + } + } + + pub fn source_breakpoints(&self) -> HashMap> { + self.breakpoints + .get_untracked() + .iter() + .map(|(path, breakpoints)| { + ( + path.to_path_buf(), + breakpoints + .iter() + .filter_map(|(_, b)| { + if b.active { + Some(SourceBreakpoint { + line: b.line + 1, + column: None, + condition: None, + hit_condition: None, + log_message: None, + }) + } else { + None + } + }) + .collect(), + ) + }) + .collect() + } +} + +#[derive(Clone, PartialEq)] +pub struct StackTraceData { + pub expanded: RwSignal, + pub frames: RwSignal>, + pub frames_shown: usize, +} + +#[derive(Clone, Serialize, Deserialize)] +pub struct LapceBreakpoint { + pub id: Option, + pub verified: bool, + pub message: Option, + pub line: usize, + pub offset: usize, + pub dap_line: Option, + pub active: bool, +} + +#[derive(Clone, PartialEq, Eq)] +#[allow(clippy::large_enum_variant)] +pub enum ScopeOrVar { + Scope(dap_types::Scope), + Var(dap_types::Variable), +} + +impl Default for ScopeOrVar { + fn default() -> Self { + ScopeOrVar::Scope(dap_types::Scope::default()) + } +} + +impl ScopeOrVar { + pub fn name(&self) -> &str { + match self { + ScopeOrVar::Scope(scope) => &scope.name, + ScopeOrVar::Var(var) => &var.name, + } + } + + pub fn value(&self) -> Option<&str> { + match self { + ScopeOrVar::Scope(_) => None, + ScopeOrVar::Var(var) => Some(&var.value), + } + } + + pub fn ty(&self) -> Option<&str> { + match self { + ScopeOrVar::Scope(_) => None, + ScopeOrVar::Var(var) => var.ty.as_deref(), + } + } + + pub fn reference(&self) -> usize { + match self { + ScopeOrVar::Scope(scope) => scope.variables_reference, + ScopeOrVar::Var(var) => var.variables_reference, + } + } +} + +#[derive(Clone, Default)] +pub struct DapVariable { + pub item: ScopeOrVar, + pub parent: Vec, + pub expanded: bool, + pub read: bool, + pub children: Vec, + pub children_expanded_count: usize, +} + +#[derive(Clone)] +pub struct DapData { + pub term_id: TermId, + pub dap_id: DapId, + pub stopped: RwSignal, + pub thread_id: RwSignal>, + pub stack_traces: RwSignal>, + pub variables_id: RwSignal, + pub variables: RwSignal, + pub breakline: Memo>, + pub common: Rc, +} + +impl DapData { + pub fn new( + cx: Scope, + dap_id: DapId, + term_id: TermId, + common: Rc, + ) -> Self { + let stopped = cx.create_rw_signal(false); + let thread_id = cx.create_rw_signal(None); + let stack_traces: RwSignal> = + cx.create_rw_signal(BTreeMap::new()); + let breakline = cx.create_memo(move |_| { + let thread_id = thread_id.get(); + if let Some(thread_id) = thread_id { + let trace = stack_traces + .with(|stack_traces| stack_traces.get(&thread_id).cloned()); + + if let Some(trace) = trace { + let breakline = trace.frames.with(|f| { + f.get(0) + .and_then(|f| { + f.source + .as_ref() + .map(|s| (f.line.saturating_sub(1), s)) + }) + .and_then(|(line, s)| s.path.clone().map(|p| (line, p))) + }); + return breakline; + } + None + } else { + None + } + }); + Self { + term_id, + dap_id, + stopped, + thread_id, + stack_traces, + variables_id: cx.create_rw_signal(0), + variables: cx.create_rw_signal(DapVariable { + item: ScopeOrVar::Scope(dap_types::Scope::default()), + parent: Vec::new(), + expanded: true, + read: true, + children: Vec::new(), + children_expanded_count: 0, + }), + breakline, + common, + } + } + + pub fn stopped( + &self, + cx: Scope, + stopped: &Stopped, + stack_traces: &HashMap>, + variables: &[(dap_types::Scope, Vec)], + ) { + self.stopped.set(true); + self.thread_id.update(|thread_id| { + *thread_id = Some(stopped.thread_id.unwrap_or_default()); + }); + + let main_thread_id = self.thread_id.get_untracked(); + let mut current_stack_traces = self.stack_traces.get_untracked(); + current_stack_traces.retain(|t, _| stack_traces.contains_key(t)); + for (thread_id, frames) in stack_traces { + let is_main_thread = main_thread_id.as_ref() == Some(thread_id); + if is_main_thread { + if let Some(frame) = frames.first() { + if let Some(path) = + frame.source.as_ref().and_then(|source| source.path.clone()) + { + self.common.internal_command.send( + InternalCommand::JumpToLocation { + location: EditorLocation { + path, + position: Some(EditorPosition::Line( + frame.line.saturating_sub(1), + )), + scroll_offset: None, + ignore_unconfirmed: false, + same_editor_tab: false, + }, + }, + ); + } + } + } + if let Some(current) = current_stack_traces.get_mut(thread_id) { + current.frames.set(frames.into()); + if is_main_thread { + current.expanded.set(true); + } + } else { + current_stack_traces.insert( + *thread_id, + StackTraceData { + expanded: cx.create_rw_signal(is_main_thread), + frames: cx.create_rw_signal(frames.into()), + frames_shown: 20, + }, + ); + } + } + self.stack_traces.set(current_stack_traces); + self.variables.update(|dap_var| { + dap_var.children = variables + .iter() + .enumerate() + .map(|(i, (scope, vars))| DapVariable { + item: ScopeOrVar::Scope(scope.to_owned()), + parent: Vec::new(), + expanded: i == 0, + read: true, + children: vars + .iter() + .map(|var| DapVariable { + item: ScopeOrVar::Var(var.to_owned()), + parent: vec![scope.variables_reference], + expanded: false, + read: false, + children: Vec::new(), + children_expanded_count: 0, + }) + .collect(), + children_expanded_count: if i == 0 { vars.len() } else { 0 }, + }) + .collect(); + dap_var.children_expanded_count = dap_var + .children + .iter() + .map(|v| v.children_expanded_count + 1) + .sum::(); + }); + } + + pub fn toggle_expand(&self, parent: Vec, reference: usize) { + self.variables_id.update(|id| { + *id += 1; + }); + self.variables.update(|variables| { + if let Some(var) = variables.get_var_mut(&parent, reference) { + if var.expanded { + var.expanded = false; + variables.update_count_recursive(&parent, reference); + } else { + var.expanded = true; + if !var.read { + var.read = true; + self.read_var_children(&parent, reference); + } else { + variables.update_count_recursive(&parent, reference); + } + } + } + }); + } + + fn read_var_children(&self, parent: &[usize], reference: usize) { + let root = self.variables; + let parent = parent.to_vec(); + let variables_id = self.variables_id; + + let send = create_ext_action(self.common.scope, move |result| { + if let Ok(ProxyResponse::DapVariableResponse { variables }) = result { + variables_id.update(|id| { + *id += 1; + }); + root.update(|root| { + if let Some(var) = root.get_var_mut(&parent, reference) { + let mut new_parent = parent.clone(); + new_parent.push(reference); + var.read = true; + var.children = variables + .into_iter() + .map(|v| DapVariable { + item: ScopeOrVar::Var(v), + parent: new_parent.clone(), + expanded: false, + read: false, + children: Vec::new(), + children_expanded_count: 0, + }) + .collect(); + root.update_count_recursive(&parent, reference); + } + }); + } + }); + self.common + .proxy + .dap_variable(self.dap_id, reference, move |result| { + send(result); + }); + } +} + +pub struct DapVariableViewdata { + pub item: ScopeOrVar, + pub parent: Vec, + pub expanded: bool, + pub level: usize, +} + +impl VirtualVector for DapVariable { + fn total_len(&self) -> usize { + self.children_expanded_count + } + + fn slice( + &mut self, + range: std::ops::Range, + ) -> impl Iterator { + let min = range.start; + let max = range.end; + let mut i = 0; + let mut view_items = Vec::new(); + for item in self.children.iter() { + i = item.append_view_slice(&mut view_items, min, max, i + 1, 0); + if i > max { + return view_items.into_iter(); + } + } + + view_items.into_iter() + } +} + +impl DapVariable { + pub fn append_view_slice( + &self, + view_items: &mut Vec, + min: usize, + max: usize, + current: usize, + level: usize, + ) -> usize { + if current > max { + return current; + } + if current + self.children_expanded_count < min { + return current + self.children_expanded_count; + } + + let mut i = current; + if current >= min { + view_items.push(DapVariableViewdata { + item: self.item.clone(), + parent: self.parent.clone(), + expanded: self.expanded, + level, + }); + } + + if self.expanded { + for item in self.children.iter() { + i = item.append_view_slice(view_items, min, max, i + 1, level + 1); + if i > max { + return i; + } + } + } + i + } + + pub fn get_var_mut( + &mut self, + parent: &[usize], + reference: usize, + ) -> Option<&mut DapVariable> { + let parent = if parent.is_empty() { + self + } else { + parent.iter().try_fold(self, |item, parent| { + item.children + .iter_mut() + .find(|c| c.item.reference() == *parent) + })? + }; + parent + .children + .iter_mut() + .find(|c| c.item.reference() == reference) + } + + pub fn update_count_recursive(&mut self, parent: &[usize], reference: usize) { + let mut parent = parent.to_vec(); + self.update_count(&parent, reference); + while let Some(reference) = parent.pop() { + self.update_count(&parent, reference); + } + self.children_expanded_count = self + .children + .iter() + .map(|item| item.children_expanded_count + 1) + .sum::(); + } + + pub fn update_count( + &mut self, + parent: &[usize], + reference: usize, + ) -> Option<()> { + let var = self.get_var_mut(parent, reference)?; + var.children_expanded_count = if var.expanded { + var.children + .iter() + .map(|item| item.children_expanded_count + 1) + .sum::() + } else { + 0 + }; + Some(()) + } +} + +#[cfg(test)] +mod tests { + use lapce_rpc::dap_types::{Scope, Variable}; + + use super::{DapVariable, ScopeOrVar}; + + #[test] + fn test_update_count() { + let variables = [ + ( + Scope { + variables_reference: 0, + ..Default::default() + }, + vec![ + Variable { + variables_reference: 3, + ..Default::default() + }, + Variable { + variables_reference: 4, + ..Default::default() + }, + ], + ), + ( + Scope { + variables_reference: 1, + ..Default::default() + }, + vec![ + Variable { + variables_reference: 5, + ..Default::default() + }, + Variable { + variables_reference: 6, + ..Default::default() + }, + ], + ), + ( + Scope { + variables_reference: 2, + ..Default::default() + }, + vec![ + Variable { + variables_reference: 7, + ..Default::default() + }, + Variable { + variables_reference: 8, + ..Default::default() + }, + ], + ), + ]; + + let mut root = DapVariable { + item: ScopeOrVar::Scope(Scope::default()), + parent: Vec::new(), + expanded: true, + read: true, + children: variables + .iter() + .map(|(scope, vars)| DapVariable { + item: ScopeOrVar::Scope(scope.to_owned()), + parent: Vec::new(), + expanded: true, + read: true, + children: vars + .iter() + .map(|var| DapVariable { + item: ScopeOrVar::Var(var.to_owned()), + parent: vec![scope.variables_reference], + expanded: false, + read: false, + children: Vec::new(), + children_expanded_count: 0, + }) + .collect(), + children_expanded_count: vars.len(), + }) + .collect(), + children_expanded_count: 0, + }; + root.children_expanded_count = root + .children + .iter() + .map(|v| v.children_expanded_count + 1) + .sum::(); + assert_eq!(root.children_expanded_count, 9); + + let var = root.get_var_mut(&[0], 3).unwrap(); + var.expanded = true; + var.read = true; + var.children = [ + Variable { + variables_reference: 9, + ..Default::default() + }, + Variable { + variables_reference: 10, + ..Default::default() + }, + ] + .iter() + .map(|var| DapVariable { + item: ScopeOrVar::Var(var.to_owned()), + parent: vec![0, 3], + expanded: false, + read: false, + children: Vec::new(), + children_expanded_count: 0, + }) + .collect(); + root.update_count_recursive(&[0], 3); + let var = root.get_var_mut(&[0], 3).unwrap(); + assert_eq!(var.children_expanded_count, 2); + let var = root.get_var_mut(&[], 0).unwrap(); + assert_eq!(var.children_expanded_count, 4); + assert_eq!(root.children_expanded_count, 11); + } +} diff --git a/lapce-app/src/doc.rs b/lapce-app/src/doc.rs new file mode 100644 index 0000000..f70e308 --- /dev/null +++ b/lapce-app/src/doc.rs @@ -0,0 +1,2239 @@ +use std::{ + borrow::Cow, + cell::RefCell, + collections::HashMap, + ops::Range, + path::{Path, PathBuf}, + rc::Rc, + sync::{ + Arc, + atomic::{self, AtomicUsize}, + }, + time::Duration, +}; + +use floem::{ + ViewId, + action::exec_after, + ext_event::create_ext_action, + keyboard::Modifiers, + peniko::Color, + reactive::{ + ReadSignal, RwSignal, Scope, SignalGet, SignalUpdate, SignalWith, batch, + }, + text::{Attrs, AttrsList, FamilyOwned, TextLayout}, + views::editor::{ + CursorInfo, Editor, EditorStyle, + actions::CommonAction, + command::{Command, CommandExecuted}, + id::EditorId, + layout::{LineExtraStyle, TextLayoutLine}, + phantom_text::{PhantomText, PhantomTextKind, PhantomTextLine}, + text::{Document, DocumentPhantom, PreeditData, Styling, SystemClipboard}, + view::{ScreenLines, ScreenLinesBase}, + }, +}; +use itertools::Itertools; +use lapce_core::{ + buffer::{ + Buffer, InvalLines, + diff::{DiffLines, rope_diff}, + rope_text::RopeText, + }, + char_buffer::CharBuffer, + command::EditCommand, + cursor::{Cursor, CursorAffinity}, + editor::{Action, EditConf, EditType}, + indent::IndentStyle, + language::LapceLanguage, + line_ending::LineEnding, + mode::MotionMode, + register::Register, + rope_text_pos::RopeTextPosition, + selection::{InsertDrift, Selection}, + style::line_styles, + syntax::{BracketParser, Syntax, edit::SyntaxEdit}, + word::{CharClassification, WordCursor, get_char_property}, +}; +use lapce_rpc::{ + buffer::BufferId, + plugin::PluginId, + proxy::ProxyResponse, + style::{LineStyle, LineStyles, Style}, +}; +use lapce_xi_rope::{ + Interval, Rope, RopeDelta, Transformer, + spans::{Spans, SpansBuilder}, +}; +use lsp_types::{ + CodeActionOrCommand, CodeLens, Diagnostic, DiagnosticSeverity, + DocumentSymbolResponse, InlayHint, InlayHintLabel, TextEdit, +}; +use serde::{Deserialize, Serialize}; +use smallvec::SmallVec; + +use crate::{ + command::{CommandKind, LapceCommand}, + config::{LapceConfig, color::LapceColor}, + editor::{EditorData, compute_screen_lines, gutter::FoldingRanges}, + find::{Find, FindProgress, FindResult}, + history::DocumentHistory, + keypress::KeyPressFocus, + main_split::Editors, + panel::{ + document_symbol::{SymbolData, SymbolInformationItemData}, + kind::PanelKind, + }, + window_tab::{CommonData, Focus}, + workspace::LapceWorkspace, +}; + +#[derive(Clone, Debug)] +pub struct DiagnosticData { + pub expanded: RwSignal, + pub diagnostics: RwSignal>, + pub diagnostics_span: RwSignal>, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct EditorDiagnostic { + pub range: Option<(usize, usize)>, + pub diagnostic: Diagnostic, +} + +#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] +pub struct DocHistory { + pub path: PathBuf, + pub version: String, +} + +#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] +pub enum DocContent { + /// A file at some location. This can be a remote path. + File { path: PathBuf, read_only: bool }, + /// A local document, which doesn't need to be sync to the disk. + Local, + /// A document of an old version in the source control + History(DocHistory), + /// A new file which doesn't exist in the file system + Scratch { id: BufferId, name: String }, +} + +impl DocContent { + pub fn is_local(&self) -> bool { + matches!(self, DocContent::Local) + } + + pub fn is_file(&self) -> bool { + matches!(self, DocContent::File { .. }) + } + + pub fn read_only(&self) -> bool { + match self { + DocContent::File { read_only, .. } => *read_only, + DocContent::Local => false, + DocContent::History(_) => true, + DocContent::Scratch { .. } => false, + } + } + + pub fn path(&self) -> Option<&PathBuf> { + match self { + DocContent::File { path, .. } => Some(path), + DocContent::Local => None, + DocContent::History(_) => None, + DocContent::Scratch { .. } => None, + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct DocInfo { + pub workspace: LapceWorkspace, + pub path: PathBuf, + pub scroll_offset: (f64, f64), + pub cursor_offset: usize, +} + +/// (Offset -> (Plugin the code actions are from, Code Actions)) +pub type CodeActions = + im::HashMap)>; + +pub type AllCodeLens = im::HashMap)>; + +#[derive(Clone)] +pub struct Doc { + pub scope: Scope, + pub buffer_id: BufferId, + pub content: RwSignal, + pub cache_rev: RwSignal, + /// Whether the buffer's content has been loaded/initialized into the buffer. + pub loaded: RwSignal, + pub buffer: RwSignal, + pub syntax: RwSignal, + semantic_styles: RwSignal>>, + /// Inlay hints for the document + pub inlay_hints: RwSignal>>, + /// Current completion lens text, if any. + /// This will be displayed even on views that are not focused. + pub completion_lens: RwSignal>, + /// (line, col) + pub completion_pos: RwSignal<(usize, usize)>, + + /// Current inline completion text, if any. + /// This will be displayed even on views that are not focused. + pub inline_completion: RwSignal>, + /// (line, col) + pub inline_completion_pos: RwSignal<(usize, usize)>, + + /// (Offset -> (Plugin the code actions are from, Code Actions)) + pub code_actions: RwSignal, + + pub code_lens: RwSignal, + + pub folding_ranges: RwSignal, + + /// Stores information about different versions of the document from source control. + histories: RwSignal>, + pub head_changes: RwSignal>, + + line_styles: Rc>, + pub parser: Rc>, + + /// A cache for the sticky headers which maps a line to the lines it should show in the header. + pub sticky_headers: Rc>>>>, + + pub preedit: PreeditData, + + pub find_result: FindResult, + + /// The diagnostics for the document + pub diagnostics: DiagnosticData, + + editors: Editors, + pub common: Rc, + + pub document_symbol_data: RwSignal>, +} +impl Doc { + pub fn new( + cx: Scope, + path: PathBuf, + diagnostics: DiagnosticData, + editors: Editors, + common: Rc, + ) -> Self { + let syntax = Syntax::init(&path); + let config = common.config.get_untracked(); + Doc { + scope: cx, + buffer_id: BufferId::next(), + buffer: cx.create_rw_signal(Buffer::new("")), + syntax: cx.create_rw_signal(syntax), + line_styles: Rc::new(RefCell::new(HashMap::new())), + parser: Rc::new(RefCell::new(BracketParser::new( + String::new(), + config.editor.bracket_pair_colorization, + config.editor.bracket_colorization_limit, + ))), + semantic_styles: cx.create_rw_signal(None), + inlay_hints: cx.create_rw_signal(None), + diagnostics, + completion_lens: cx.create_rw_signal(None), + completion_pos: cx.create_rw_signal((0, 0)), + inline_completion: cx.create_rw_signal(None), + inline_completion_pos: cx.create_rw_signal((0, 0)), + cache_rev: cx.create_rw_signal(0), + content: cx.create_rw_signal(DocContent::File { + path, + read_only: false, + }), + loaded: cx.create_rw_signal(false), + histories: cx.create_rw_signal(im::HashMap::new()), + head_changes: cx.create_rw_signal(im::Vector::new()), + sticky_headers: Rc::new(RefCell::new(HashMap::new())), + code_actions: cx.create_rw_signal(im::HashMap::new()), + find_result: FindResult::new(cx), + preedit: PreeditData::new(cx), + editors, + common, + code_lens: cx.create_rw_signal(im::HashMap::new()), + document_symbol_data: cx.create_rw_signal(None), + folding_ranges: cx.create_rw_signal(FoldingRanges::default()), + } + } + + pub fn new_local(cx: Scope, editors: Editors, common: Rc) -> Doc { + Self::new_content(cx, DocContent::Local, editors, common) + } + + pub fn new_content( + cx: Scope, + content: DocContent, + editors: Editors, + common: Rc, + ) -> Doc { + let cx = cx.create_child(); + let config = common.config.get_untracked(); + Self { + scope: cx, + buffer_id: BufferId::next(), + buffer: cx.create_rw_signal(Buffer::new("")), + syntax: cx.create_rw_signal(Syntax::plaintext()), + line_styles: Rc::new(RefCell::new(HashMap::new())), + parser: Rc::new(RefCell::new(BracketParser::new( + String::new(), + config.editor.bracket_pair_colorization, + config.editor.bracket_colorization_limit, + ))), + semantic_styles: cx.create_rw_signal(None), + inlay_hints: cx.create_rw_signal(None), + diagnostics: DiagnosticData { + expanded: cx.create_rw_signal(true), + diagnostics: cx.create_rw_signal(im::Vector::new()), + diagnostics_span: cx.create_rw_signal(SpansBuilder::new(0).build()), + }, + completion_lens: cx.create_rw_signal(None), + completion_pos: cx.create_rw_signal((0, 0)), + inline_completion: cx.create_rw_signal(None), + inline_completion_pos: cx.create_rw_signal((0, 0)), + cache_rev: cx.create_rw_signal(0), + content: cx.create_rw_signal(content), + histories: cx.create_rw_signal(im::HashMap::new()), + head_changes: cx.create_rw_signal(im::Vector::new()), + sticky_headers: Rc::new(RefCell::new(HashMap::new())), + loaded: cx.create_rw_signal(true), + find_result: FindResult::new(cx), + code_actions: cx.create_rw_signal(im::HashMap::new()), + preedit: PreeditData::new(cx), + editors, + common, + code_lens: cx.create_rw_signal(im::HashMap::new()), + document_symbol_data: cx.create_rw_signal(None), + folding_ranges: cx.create_rw_signal(FoldingRanges::default()), + } + } + + pub fn new_history( + cx: Scope, + content: DocContent, + editors: Editors, + common: Rc, + ) -> Doc { + let config = common.config.get_untracked(); + let syntax = if let DocContent::History(history) = &content { + Syntax::init(&history.path) + } else { + Syntax::plaintext() + }; + Self { + scope: cx, + buffer_id: BufferId::next(), + buffer: cx.create_rw_signal(Buffer::new("")), + syntax: cx.create_rw_signal(syntax), + line_styles: Rc::new(RefCell::new(HashMap::new())), + parser: Rc::new(RefCell::new(BracketParser::new( + String::new(), + config.editor.bracket_pair_colorization, + config.editor.bracket_colorization_limit, + ))), + semantic_styles: cx.create_rw_signal(None), + inlay_hints: cx.create_rw_signal(None), + diagnostics: DiagnosticData { + expanded: cx.create_rw_signal(true), + diagnostics: cx.create_rw_signal(im::Vector::new()), + diagnostics_span: cx.create_rw_signal(SpansBuilder::new(0).build()), + }, + completion_lens: cx.create_rw_signal(None), + completion_pos: cx.create_rw_signal((0, 0)), + inline_completion: cx.create_rw_signal(None), + inline_completion_pos: cx.create_rw_signal((0, 0)), + cache_rev: cx.create_rw_signal(0), + content: cx.create_rw_signal(content), + sticky_headers: Rc::new(RefCell::new(HashMap::new())), + loaded: cx.create_rw_signal(true), + histories: cx.create_rw_signal(im::HashMap::new()), + head_changes: cx.create_rw_signal(im::Vector::new()), + code_actions: cx.create_rw_signal(im::HashMap::new()), + find_result: FindResult::new(cx), + preedit: PreeditData::new(cx), + editors, + common, + code_lens: cx.create_rw_signal(im::HashMap::new()), + document_symbol_data: cx.create_rw_signal(None), + folding_ranges: cx.create_rw_signal(FoldingRanges::default()), + } + } + + /// Create a styling instance for this doc + pub fn styling(self: &Rc) -> Rc { + Rc::new(DocStyling { + config: self.common.config, + doc: self.clone(), + }) + } + + /// Create an [`Editor`] instance from this [`Doc`]. Note that this needs to be registered + /// appropriately to create the [`EditorData`] and such. + pub fn create_editor( + self: &Rc, + cx: Scope, + id: EditorId, + is_local: bool, + ) -> Editor { + let common = &self.common; + let config = common.config.get_untracked(); + let modal = config.core.modal && !is_local; + + let register = common.register; + // TODO: we could have these Rcs created once and stored somewhere, maybe on + // common, to avoid recreating them everytime. + let cursor_info = CursorInfo { + blink_interval: Rc::new(move || config.editor.blink_interval()), + blink_timer: common.window_common.cursor_blink_timer, + hidden: common.window_common.hide_cursor, + should_blink: Rc::new(should_blink(common.focus, common.keyboard_focus)), + }; + let mut editor = + Editor::new_direct(cx, id, self.clone(), self.styling(), modal); + + editor.register = register; + editor.cursor_info = cursor_info; + editor.ime_allowed = common.window_common.ime_allowed; + + editor.recreate_view_effects(); + + editor + } + + fn editor_data(&self, id: EditorId) -> Option { + self.editors.editor_untracked(id) + } + + pub fn syntax(&self) -> ReadSignal { + self.syntax.read_only() + } + + pub fn set_syntax(&self, syntax: Syntax) { + batch(|| { + self.syntax.set(syntax); + if self.semantic_styles.with_untracked(|s| s.is_none()) { + self.clear_style_cache(); + } + self.clear_sticky_headers_cache(); + }); + } + + /// Set the syntax highlighting this document should use. + pub fn set_language(&self, language: LapceLanguage) { + self.syntax.set(Syntax::from_language(language)); + } + + pub fn find(&self) -> &Find { + &self.common.find + } + + /// Whether or not the underlying buffer is loaded + pub fn loaded(&self) -> bool { + self.loaded.get_untracked() + } + + //// Initialize the content with some text, this marks the document as loaded. + pub fn init_content(&self, content: Rope) { + batch(|| { + self.syntax.with_untracked(|syntax| { + self.buffer.update(|buffer| { + buffer.init_content(content); + buffer.detect_indent(|| { + IndentStyle::from_str(syntax.language.indent_unit()) + }); + }); + }); + self.loaded.set(true); + self.on_update(None); + self.init_parser(); + self.init_diagnostics(); + self.retrieve_head(); + }); + } + + fn init_parser(&self) { + let code = self.buffer.get_untracked().to_string(); + self.syntax.with_untracked(|syntax| { + if syntax.styles.is_some() { + self.parser.borrow_mut().update_code( + code, + &self.buffer.get_untracked(), + Some(syntax), + ); + } else { + self.parser.borrow_mut().update_code( + code, + &self.buffer.get_untracked(), + None, + ); + } + }); + } + + /// Reload the document's content, and is what you should typically use when you want to *set* + /// an existing document's content. + pub fn reload(&self, content: Rope, set_pristine: bool) { + // self.code_actions.clear(); + // self.inlay_hints = None; + let delta = self + .buffer + .try_update(|buffer| buffer.reload(content, set_pristine)) + .unwrap(); + self.apply_deltas(&[delta]); + } + + pub fn handle_file_changed(&self, content: Rope) { + if self.is_pristine() { + self.reload(content, true); + } + } + + pub fn do_insert( + &self, + cursor: &mut Cursor, + s: &str, + config: &LapceConfig, + ) -> Vec<(Rope, RopeDelta, InvalLines)> { + if self.content.with_untracked(|c| c.read_only()) { + return Vec::new(); + } + + let old_cursor = cursor.mode.clone(); + let deltas = self.syntax.with_untracked(|syntax| { + self.buffer + .try_update(|buffer| { + Action::insert( + cursor, + buffer, + s, + &|buffer, c, offset| { + syntax_prev_unmatched(buffer, syntax, c, offset) + }, + config.editor.auto_closing_matching_pairs, + config.editor.auto_surround, + ) + }) + .unwrap() + }); + // Keep track of the change in the cursor mode for undo/redo + self.buffer.update(|buffer| { + buffer.set_cursor_before(old_cursor); + buffer.set_cursor_after(cursor.mode.clone()); + }); + self.apply_deltas(&deltas); + deltas + } + + pub fn do_raw_edit( + &self, + edits: &[(impl AsRef, &str)], + edit_type: EditType, + ) -> Option<(Rope, RopeDelta, InvalLines)> { + if self.content.with_untracked(|c| c.read_only()) { + return None; + } + + let (text, delta, inval_lines) = self + .buffer + .try_update(|buffer| buffer.edit(edits, edit_type)) + .unwrap(); + self.apply_deltas(&[(text.clone(), delta.clone(), inval_lines.clone())]); + Some((text, delta, inval_lines)) + } + + pub fn do_edit( + &self, + cursor: &mut Cursor, + cmd: &EditCommand, + modal: bool, + register: &mut Register, + smart_tab: bool, + ) -> Vec<(Rope, RopeDelta, InvalLines)> { + if self.content.with_untracked(|c| c.read_only()) + && !cmd.not_changing_buffer() + { + return Vec::new(); + } + + let mut clipboard = SystemClipboard::new(); + let old_cursor = cursor.mode.clone(); + let deltas = self.syntax.with_untracked(|syntax| { + self.buffer + .try_update(|buffer| { + Action::do_edit( + cursor, + buffer, + cmd, + &mut clipboard, + register, + EditConf { + comment_token: syntax.language.comment_token(), + modal, + smart_tab, + keep_indent: true, + auto_indent: true, + }, + ) + }) + .unwrap() + }); + + if !deltas.is_empty() { + self.buffer.update(|buffer| { + buffer.set_cursor_before(old_cursor); + buffer.set_cursor_after(cursor.mode.clone()); + }); + self.apply_deltas(&deltas); + } + + deltas + } + + pub fn apply_deltas(&self, deltas: &[(Rope, RopeDelta, InvalLines)]) { + let rev = self.rev() - deltas.len() as u64; + batch(|| { + for (i, (_, delta, inval)) in deltas.iter().enumerate() { + self.update_styles(delta); + self.update_inlay_hints(delta); + self.update_diagnostics(delta); + self.update_completion_lens(delta); + self.update_find_result(delta); + if let DocContent::File { path, .. } = self.content.get_untracked() { + self.update_breakpoints(delta, &path, &inval.old_text); + self.common.proxy.update( + path, + delta.clone(), + rev + i as u64 + 1, + ); + } + } + }); + + // TODO(minor): We could avoid this potential allocation since most apply_delta callers are actually using a Vec + // which we could reuse. + // We use a smallvec because there is unlikely to be more than a couple of deltas + let edits: SmallVec<[SyntaxEdit; 3]> = deltas + .iter() + .map(|(before_text, delta, _)| { + SyntaxEdit::from_delta(before_text, delta.clone()) + }) + .collect(); + self.on_update(Some(edits)); + } + + pub fn is_pristine(&self) -> bool { + self.buffer.with_untracked(|b| b.is_pristine()) + } + + /// Get the buffer's current revision. This is used to track whether the buffer has changed. + pub fn rev(&self) -> u64 { + self.buffer.with_untracked(|b| b.rev()) + } + + /// Get the buffer's line-ending. + /// Note: this may not be the same as what the actual line endings in the file are, rather this + /// is what the line-ending is set to (and what it will be saved as). + pub fn line_ending(&self) -> LineEnding { + self.buffer.with_untracked(|b| b.line_ending()) + } + + fn on_update(&self, edits: Option>) { + batch(|| { + self.trigger_syntax_change(edits); + self.trigger_head_change(); + self.check_auto_save(); + self.get_inlay_hints(); + self.find_result.reset(); + self.get_semantic_styles(); + self.do_bracket_colorization(); + self.clear_code_actions(); + self.clear_style_cache(); + self.get_code_lens(); + self.get_document_symbol(); + self.get_folding_range(); + }); + } + + fn do_bracket_colorization(&self) { + if self.parser.borrow().active { + self.syntax.with_untracked(|syntax| { + if syntax.rev == self.rev() && syntax.styles.is_some() { + self.parser.borrow_mut().update_code( + self.buffer.get_untracked().to_string(), + &self.buffer.get_untracked(), + Some(syntax), + ); + } else { + self.parser.borrow_mut().update_code( + self.buffer.get_untracked().to_string(), + &self.buffer.get_untracked(), + None, + ); + } + }) + } + } + + pub fn do_text_edit(&self, edits: &[TextEdit]) { + let edits = self.buffer.with_untracked(|buffer| { + edits + .iter() + .map(|edit| { + let selection = lapce_core::selection::Selection::region( + buffer.offset_of_position(&edit.range.start), + buffer.offset_of_position(&edit.range.end), + ); + (selection, edit.new_text.as_str()) + }) + .collect::>() + }); + self.do_raw_edit(&edits, EditType::Completion); + } + + fn check_auto_save(&self) { + let config = self.common.config.get_untracked(); + if config.editor.autosave_interval > 0 { + let Some(path) = self.content.with_untracked(|c| c.path().cloned()) + else { + return; + }; + let rev = self.rev(); + let doc = self.clone(); + let scope = self.scope; + let proxy = self.common.proxy.clone(); + let format = config.editor.format_on_save; + exec_after( + Duration::from_millis(config.editor.autosave_interval), + move |_| { + let current_rev = match doc + .buffer + .try_with_untracked(|b| b.as_ref().map(|b| b.rev())) + { + Some(rev) => rev, + None => return, + }; + + if current_rev != rev || doc.is_pristine() { + return; + } + + if format { + let send = create_ext_action(scope, move |result| { + let current_rev = doc.rev(); + if current_rev != rev { + return; + } + if let Ok(ProxyResponse::GetDocumentFormatting { + edits, + }) = result + { + doc.do_text_edit(&edits); + } + doc.save(|| {}); + }); + proxy.get_document_formatting(path, move |result| { + send(result); + }); + } else { + doc.save(|| {}); + } + }, + ); + } + } + + /// Update the styles after an edit, so the highlights are at the correct positions. + /// This does not do a reparse of the document itself. + fn update_styles(&self, delta: &RopeDelta) { + batch(|| { + self.semantic_styles.update(|styles| { + if let Some(styles) = styles.as_mut() { + styles.apply_shape(delta); + } + }); + self.syntax.update(|syntax| { + if let Some(styles) = syntax.styles.as_mut() { + styles.apply_shape(delta); + } + syntax.lens.apply_delta(delta); + }); + }); + } + + /// Update the inlay hints so their positions are correct after an edit. + fn update_inlay_hints(&self, delta: &RopeDelta) { + self.inlay_hints.update(|inlay_hints| { + if let Some(hints) = inlay_hints.as_mut() { + hints.apply_shape(delta); + } + }); + } + + pub fn trigger_syntax_change(&self, edits: Option>) { + let (rev, text) = + self.buffer.with_untracked(|b| (b.rev(), b.text().clone())); + + let doc = self.clone(); + let send = create_ext_action(self.scope, move |syntax| { + if doc.buffer.with_untracked(|b| b.rev()) == rev { + doc.syntax.set(syntax); + doc.do_bracket_colorization(); + doc.clear_style_cache(); + doc.clear_sticky_headers_cache(); + } + }); + + self.syntax.update(|syntax| { + syntax.cancel_flag.store(1, atomic::Ordering::Relaxed); + syntax.cancel_flag = Arc::new(AtomicUsize::new(0)); + }); + let mut syntax = self.syntax.get_untracked(); + rayon::spawn(move || { + syntax.parse(rev, text, edits.as_deref()); + send(syntax); + }); + } + + fn clear_style_cache(&self) { + self.line_styles.borrow_mut().clear(); + self.clear_text_cache(); + } + + fn clear_code_actions(&self) { + self.code_actions.update(|c| { + c.clear(); + }); + } + + /// Inform any dependents on this document that they should clear any cached text. + pub fn clear_text_cache(&self) { + self.cache_rev.try_update(|cache_rev| { + *cache_rev += 1; + + // TODO: ??? + // Update the text layouts within the callback so that those alerted to cache rev + // will see the now empty layouts. + // self.text_layouts.borrow_mut().clear(*cache_rev, None); + }); + } + + fn clear_sticky_headers_cache(&self) { + self.sticky_headers.borrow_mut().clear(); + } + + /// Get the active style information, either the semantic styles or the + /// tree-sitter syntax styles. + fn styles(&self) -> Option> { + if let Some(semantic_styles) = self.semantic_styles.get_untracked() { + Some(semantic_styles) + } else { + self.syntax.with_untracked(|syntax| syntax.styles.clone()) + } + } + + /// Get the style information for the particular line from semantic/syntax highlighting. + /// This caches the result if possible. + pub fn line_style(&self, line: usize) -> Arc> { + if self.line_styles.borrow().get(&line).is_none() { + let styles = self.styles(); + + let line_styles = styles + .map(|styles| { + let text = + self.buffer.with_untracked(|buffer| buffer.text().clone()); + line_styles(&text, line, &styles) + }) + .unwrap_or_default(); + self.line_styles + .borrow_mut() + .insert(line, Arc::new(line_styles)); + } + self.line_styles.borrow().get(&line).cloned().unwrap() + } + + /// Request semantic styles for the buffer from the LSP through the proxy. + pub fn get_semantic_styles(&self) { + if !self.loaded() { + return; + } + + let path = + if let DocContent::File { path, .. } = self.content.get_untracked() { + path + } else { + return; + }; + + let (atomic_rev, rev, len) = self + .buffer + .with_untracked(|b| (b.atomic_rev(), b.rev(), b.len())); + + let doc = self.clone(); + let send = create_ext_action(self.scope, move |styles| { + if let Some(styles) = styles { + if doc.buffer.with_untracked(|b| b.rev()) == rev { + doc.semantic_styles.set(Some(styles)); + doc.clear_style_cache(); + } + } + }); + + self.common.proxy.get_semantic_tokens(path, move |result| { + if let Ok(ProxyResponse::GetSemanticTokens { styles }) = result { + if styles.styles.is_empty() { + send(None); + return; + } + if atomic_rev.load(atomic::Ordering::Acquire) != rev { + send(None); + return; + } + std::thread::spawn(move || { + let mut styles_span = SpansBuilder::new(len); + for style in styles.styles { + if atomic_rev.load(atomic::Ordering::Acquire) != rev { + send(None); + return; + } + styles_span.add_span( + Interval::new(style.start, style.end), + style.style, + ); + } + + let styles = styles_span.build(); + send(Some(styles)); + }); + } else { + send(None); + } + }); + } + + pub fn get_code_lens(&self) { + let cx = self.scope; + let doc = self.clone(); + self.code_lens.update(|code_lens| { + code_lens.clear(); + }); + let rev = self.rev(); + if let DocContent::File { path, .. } = doc.content.get_untracked() { + let send = create_ext_action(cx, move |result| { + if rev != doc.rev() { + return; + } + if let Ok(ProxyResponse::GetCodeLensResponse { plugin_id, resp }) = + result + { + let Some(codelens) = resp else { + return; + }; + doc.code_lens.update(|code_lens| { + for codelens in codelens { + if codelens.command.is_none() { + continue; + } + let entry = code_lens + .entry(codelens.range.start.line as usize) + .or_insert_with(|| { + ( + plugin_id, + doc.buffer.with_untracked(|b| { + b.offset_of_line( + codelens.range.start.line as usize, + ) + }), + im::Vector::new(), + ) + }); + entry.2.push_back(codelens); + } + }); + } + }); + self.common.proxy.get_code_lens(path, move |result| { + send(result); + }); + } + } + + pub fn get_document_symbol(&self) { + let cx = self.scope; + let doc = self.clone(); + let rev = self.rev(); + if let DocContent::File { path, .. } = doc.content.get_untracked() { + let send = create_ext_action(cx, { + let path = path.clone(); + move |result| { + if rev != doc.rev() { + return; + } + if let Ok(ProxyResponse::GetDocumentSymbols { resp }) = result { + let items: Vec> = + match resp { + DocumentSymbolResponse::Flat(_symbols) => { + Vec::with_capacity(0) + } + DocumentSymbolResponse::Nested(symbols) => symbols + .into_iter() + .map(|x| { + cx.create_rw_signal( + SymbolInformationItemData::from((x, cx)), + ) + }) + .collect(), + }; + let symbol_new = Some(SymbolData::new(items, path, cx)); + doc.document_symbol_data.update(|symbol| { + *symbol = symbol_new; + }); + } + } + }); + + self.common.proxy.get_document_symbols(path, move |result| { + send(result); + }); + } + } + + /// Request inlay hints for the buffer from the LSP through the proxy. + pub fn get_inlay_hints(&self) { + if !self.loaded() { + return; + } + + let path = + if let DocContent::File { path, .. } = self.content.get_untracked() { + path + } else { + return; + }; + + let (buffer, rev, len) = self + .buffer + .with_untracked(|b| (b.clone(), b.rev(), b.len())); + + let doc = self.clone(); + let send = create_ext_action(self.scope, move |hints| { + if doc.buffer.with_untracked(|b| b.rev()) == rev { + doc.inlay_hints.set(Some(hints)); + doc.clear_text_cache(); + } + }); + + self.common.proxy.get_inlay_hints(path, move |result| { + if let Ok(ProxyResponse::GetInlayHints { mut hints }) = result { + // Sort the inlay hints by their position, as the LSP does not guarantee that it will + // provide them in the order that they are in within the file + // as well, Spans does not iterate in the order that they appear + hints.sort_by(|left, right| left.position.cmp(&right.position)); + + let mut hints_span = SpansBuilder::new(len); + for hint in hints { + let offset = buffer.offset_of_position(&hint.position).min(len); + hints_span.add_span( + Interval::new(offset, (offset + 1).min(len)), + hint, + ); + } + let hints = hints_span.build(); + send(hints); + } + }); + } + + pub fn diagnostics(&self) -> &DiagnosticData { + &self.diagnostics + } + + /// Update the diagnostics' positions after an edit so that they appear in the correct place. + fn update_diagnostics(&self, delta: &RopeDelta) { + if self + .diagnostics + .diagnostics + .with_untracked(|d| d.is_empty()) + { + return; + } + + self.diagnostics.diagnostics_span.update(|diagnostics| { + diagnostics.apply_shape(delta); + }); + } + + /// init diagnostics offset ranges from lsp positions + pub fn init_diagnostics(&self) { + let len = self.buffer.with_untracked(|b| b.len()); + let diagnostics = self.diagnostics.diagnostics.get_untracked(); + + let span = self.buffer.with_untracked(|buffer| { + let mut span = SpansBuilder::new(len); + for diag in diagnostics.iter() { + let start = buffer.offset_of_position(&diag.range.start); + let end = buffer.offset_of_position(&diag.range.end); + span.add_span(Interval::new(start, end), diag.to_owned()); + } + span.build() + }); + self.diagnostics.diagnostics_span.set(span); + + self.clear_text_cache(); + self.clear_code_actions(); + } + + pub fn get_folding_range(&self) { + // let cx = self.scope; + // let doc = self.clone(); + // let rev = self.rev(); + // if let DocContent::File { path, .. } = doc.content.get_untracked() { + // let send = create_ext_action(cx, { + // move |result| { + // if rev != doc.rev() { + // return; + // } + // if let Ok(ProxyResponse::LspFoldingRangeResponse { + // resp, .. + // }) = result + // { + // let folding = resp + // .unwrap_or_default() + // .into_iter() + // .map(|x| { + // crate::editor::gutter::FoldingRange::from_lsp(x) + // }) + // .sorted_by(|x, y| x.start.line.cmp(&y.start.line)) + // .collect(); + // doc.folding_ranges.update(|symbol| { + // symbol.0 = folding; + // }); + // } + // } + // }); + + // self.common + // .proxy + // .get_lsp_folding_range(path, move |result| { + // send(result); + // }); + // } + } + + /// Get the current completion lens text + pub fn completion_lens(&self) -> Option { + self.completion_lens.get_untracked() + } + + pub fn set_completion_lens( + &self, + completion_lens: String, + line: usize, + col: usize, + ) { + // TODO: more granular invalidation + self.completion_lens.set(Some(completion_lens)); + self.completion_pos.set((line, col)); + self.clear_text_cache(); + } + + pub fn clear_completion_lens(&self) { + // TODO: more granular invalidation + if self.completion_lens.get_untracked().is_some() { + self.completion_lens.set(None); + self.clear_text_cache(); + } + } + + fn update_breakpoints(&self, delta: &RopeDelta, path: &Path, old_text: &Rope) { + if self + .common + .breakpoints + .with_untracked(|breakpoints| breakpoints.contains_key(path)) + { + self.common.breakpoints.update(|breakpoints| { + if let Some(path_breakpoints) = breakpoints.get_mut(path) { + let mut transformer = Transformer::new(delta); + self.buffer.with_untracked(|buffer| { + *path_breakpoints = path_breakpoints + .clone() + .into_values() + .map(|mut b| { + let offset = old_text.offset_of_line(b.line); + let offset = transformer.transform(offset, false); + let line = buffer.line_of_offset(offset); + b.line = line; + b.offset = offset; + (b.line, b) + }) + .collect(); + }); + } + }); + } + } + + /// Update the completion lens position after an edit so that it appears in the correct place. + pub fn update_completion_lens(&self, delta: &RopeDelta) { + let Some(completion) = self.completion_lens.get_untracked() else { + return; + }; + + let (line, col) = self.completion_pos.get_untracked(); + let offset = self + .buffer + .with_untracked(|b| b.offset_of_line_col(line, col)); + + // If the edit is easily checkable + updateable from, then we alter the lens' text. + // In normal typing, if we didn't do this, then the text would jitter forward and then + // backwards as the completion lens is updated. + // TODO: this could also handle simple deletion, but we don't currently keep track of + // the past completion lens string content in the field. + if delta.as_simple_insert().is_some() { + let (iv, new_len) = delta.summary(); + if iv.start() == iv.end() + && iv.start() == offset + && new_len <= completion.len() + { + // Remove the # of newly inserted characters + // These aren't necessarily the same as the characters literally in the + // text, but the completion will be updated when the completion widget + // receives the update event, and it will fix this if needed. + // TODO: this could be smarter and use the insert's content + self.completion_lens + .set(Some(completion[new_len..].to_string())); + } + } + + // Shift the position by the rope delta + let mut transformer = Transformer::new(delta); + + let new_offset = transformer.transform(offset, true); + let new_pos = self + .buffer + .with_untracked(|b| b.offset_to_line_col(new_offset)); + + self.completion_pos.set(new_pos); + } + + fn update_find_result(&self, delta: &RopeDelta) { + self.find_result.occurrences.update(|s| { + *s = s.apply_delta(delta, true, InsertDrift::Default); + }) + } + + pub fn update_find(&self) { + let find_rev = self.common.find.rev.get_untracked(); + if self.find_result.find_rev.get_untracked() != find_rev { + if self + .common + .find + .search_string + .with_untracked(|search_string| { + search_string + .as_ref() + .map(|s| s.content.is_empty()) + .unwrap_or(true) + }) + { + self.find_result.occurrences.set(Selection::new()); + } + self.find_result.reset(); + self.find_result.find_rev.set(find_rev); + } + + if self.find_result.progress.get_untracked() != FindProgress::Started { + return; + } + + let search = self.common.find.search_string.get_untracked(); + let search = match search { + Some(search) => search, + None => return, + }; + if search.content.is_empty() { + return; + } + + self.find_result + .progress + .set(FindProgress::InProgress(Selection::new())); + + let find_result = self.find_result.clone(); + let send = create_ext_action(self.scope, move |occurrences: Selection| { + find_result.occurrences.set(occurrences); + find_result.progress.set(FindProgress::Ready); + }); + + let text = self.buffer.with_untracked(|b| b.text().clone()); + let case_matching = self.common.find.case_matching.get_untracked(); + let whole_words = self.common.find.whole_words.get_untracked(); + rayon::spawn(move || { + let mut occurrences = Selection::new(); + Find::find( + &text, + &search, + 0, + text.len(), + case_matching, + whole_words, + true, + &mut occurrences, + ); + send(occurrences); + }); + } + + /// Get the sticky headers for a particular line, creating them if necessary. + pub fn sticky_headers(&self, line: usize) -> Option> { + if let Some(lines) = self.sticky_headers.borrow().get(&line) { + return lines.clone(); + } + let lines = self.buffer.with_untracked(|buffer| { + let offset = buffer.offset_of_line(line + 1); + self.syntax.with_untracked(|syntax| { + syntax.sticky_headers(offset).map(|offsets| { + offsets + .iter() + .filter_map(|offset| { + let l = buffer.line_of_offset(*offset); + if l <= line { Some(l) } else { None } + }) + .dedup() + .sorted() + .collect() + }) + }) + }); + self.sticky_headers.borrow_mut().insert(line, lines.clone()); + lines + } + + pub fn head_changes(&self) -> RwSignal> { + self.head_changes + } + + /// Retrieve the `head` version of the buffer + pub fn retrieve_head(&self) { + if let DocContent::File { path, .. } = self.content.get_untracked() { + let histories = self.histories; + + let send = { + let path = path.clone(); + let doc = self.clone(); + create_ext_action(self.scope, move |result| { + if let Ok(ProxyResponse::BufferHeadResponse { + content, .. + }) = result + { + let history = DocumentHistory::new( + path.clone(), + "head".to_string(), + &content, + ); + histories.update(|histories| { + histories.insert("head".to_string(), history); + }); + + doc.trigger_head_change(); + } + }) + }; + + let path = path.clone(); + let proxy = self.common.proxy.clone(); + std::thread::spawn(move || { + proxy.get_buffer_head(path, move |result| { + send(result); + }); + }); + } + } + + pub fn trigger_head_change(&self) { + let history = if let Some(text) = + self.histories.with_untracked(|histories| { + histories + .get("head") + .map(|history| history.buffer.text().clone()) + }) { + text + } else { + return; + }; + + let rev = self.rev(); + let left_rope = history; + let (atomic_rev, right_rope) = self + .buffer + .with_untracked(|b| (b.atomic_rev(), b.text().clone())); + + let send = { + let atomic_rev = atomic_rev.clone(); + let head_changes = self.head_changes; + create_ext_action(self.scope, move |changes| { + let changes = if let Some(changes) = changes { + changes + } else { + return; + }; + + if atomic_rev.load(atomic::Ordering::Acquire) != rev { + return; + } + + head_changes.set(changes); + }) + }; + + rayon::spawn(move || { + let changes = + rope_diff(left_rope, right_rope, rev, atomic_rev.clone(), None); + send(changes.map(im::Vector::from)); + }); + } + + pub fn save(&self, after_action: impl FnOnce() + 'static) { + let content = self.content.get_untracked(); + if let DocContent::File { path, .. } = content { + let rev = self.rev(); + let buffer = self.buffer; + let send = create_ext_action(self.scope, move |result| { + if let Ok(ProxyResponse::SaveResponse {}) = result { + let current_rev = buffer.with_untracked(|buffer| buffer.rev()); + if current_rev == rev { + buffer.update(|buffer| { + buffer.set_pristine(); + }); + after_action(); + } + } + }); + + self.common.proxy.save(rev, path, true, move |result| { + send(result); + }) + } + } + + pub fn set_inline_completion( + &self, + inline_completion: String, + line: usize, + col: usize, + ) { + // TODO: more granular invalidation + batch(|| { + self.inline_completion.set(Some(inline_completion)); + self.inline_completion_pos.set((line, col)); + self.clear_text_cache(); + }); + } + + pub fn clear_inline_completion(&self) { + if self.inline_completion.with_untracked(Option::is_some) { + self.inline_completion.set(None); + self.clear_text_cache(); + } + } + + pub fn update_inline_completion(&self, delta: &RopeDelta) { + let Some(completion) = self.inline_completion.get_untracked() else { + return; + }; + + let (line, col) = self.completion_pos.get_untracked(); + let offset = self + .buffer + .with_untracked(|b| b.offset_of_line_col(line, col)); + + // If the edit is easily checkable + updateable from, then we alter the text. + // In normal typing, if we didn't do this, then the text would jitter forward and then + // backwards as the completion is updated. + // TODO: this could also handle simple deletion, but we don't currently keep track of + // the past completion string content in the field. + if delta.as_simple_insert().is_some() { + let (iv, new_len) = delta.summary(); + if iv.start() == iv.end() + && iv.start() == offset + && new_len <= completion.len() + { + // Remove the # of newly inserted characters + // These aren't necessarily the same as the characters literally in the + // text, but the completion will be updated when the completion widget + // receives the update event, and it will fix this if needed. + self.inline_completion + .set(Some(completion[new_len..].to_string())); + } + } + + // Shift the position by the rope delta + let mut transformer = Transformer::new(delta); + + let new_offset = transformer.transform(offset, true); + let new_pos = self + .buffer + .with_untracked(|b| b.offset_to_line_col(new_offset)); + + self.inline_completion_pos.set(new_pos); + } + + pub fn code_actions(&self) -> RwSignal { + self.code_actions + } + + /// Returns the offsets of the brackets enclosing the given offset. + /// Uses a language aware algorithm if syntax support is available for the current language, + /// else falls back to a language unaware algorithm. + pub fn find_enclosing_brackets(&self, offset: usize) -> Option<(usize, usize)> { + let rev = self.rev(); + self.syntax + .with_untracked(|syntax| { + (!syntax.text.is_empty() && syntax.rev == rev) + .then(|| syntax.find_enclosing_pair(offset)) + }) + // If syntax.text is empty, either the buffer is empty or we don't have syntax support + // for the current language. + // Try a language unaware search for enclosing brackets in case it is the latter. + .unwrap_or_else(|| { + self.buffer.with_untracked(|buffer| { + WordCursor::new(buffer.text(), offset).find_enclosing_pair() + }) + }) + } +} +impl Document for Doc { + fn text(&self) -> Rope { + self.buffer.with_untracked(|buffer| buffer.text().clone()) + } + + fn cache_rev(&self) -> RwSignal { + self.cache_rev + } + + fn find_unmatched(&self, offset: usize, previous: bool, ch: char) -> usize { + self.syntax().with_untracked(|syntax| { + if syntax.layers.is_some() { + syntax + .find_tag(offset, previous, &CharBuffer::from(ch)) + .unwrap_or(offset) + } else { + let text = self.text(); + let mut cursor = WordCursor::new(&text, offset); + let new_offset = if previous { + cursor.previous_unmatched(ch) + } else { + cursor.next_unmatched(ch) + }; + + new_offset.unwrap_or(offset) + } + }) + } + + fn find_matching_pair(&self, offset: usize) -> usize { + self.syntax().with_untracked(|syntax| { + if syntax.layers.is_some() { + syntax.find_matching_pair(offset).unwrap_or(offset) + } else { + let text = self.text(); + WordCursor::new(&text, offset) + .match_pairs() + .unwrap_or(offset) + } + }) + } + + fn preedit(&self) -> PreeditData { + self.preedit.clone() + } + + fn compute_screen_lines( + &self, + editor: &Editor, + base: RwSignal, + ) -> ScreenLines { + let Some(editor_data) = self.editor_data(editor.id()) else { + return ScreenLines { + lines: Default::default(), + info: Default::default(), + diff_sections: Default::default(), + base, + }; + }; + + compute_screen_lines( + self.common.config, + base, + editor_data.kind.read_only(), + &editor_data.doc_signal().get(), + editor.lines(), + editor.text_prov(), + editor.config_id(), + ) + } + + fn run_command( + &self, + ed: &Editor, + cmd: &Command, + count: Option, + modifiers: Modifiers, + ) -> CommandExecuted { + let Some(editor_data) = self.editor_data(ed.id()) else { + return CommandExecuted::No; + }; + + let cmd = CommandKind::from(cmd.clone()); + let cmd = LapceCommand { + kind: cmd, + data: None, + }; + editor_data.run_command(&cmd, count, modifiers) + } + + fn receive_char(&self, ed: &Editor, c: &str) { + let Some(editor_data) = self.editor_data(ed.id()) else { + return; + }; + + editor_data.receive_char(c); + } + + fn edit( + &self, + iter: &mut dyn Iterator, + edit_type: EditType, + ) { + let delta = self + .buffer + .try_update(|buffer| buffer.edit(iter, edit_type)) + .unwrap(); + self.apply_deltas(&[delta]); + } +} + +impl DocumentPhantom for Doc { + fn phantom_text( + &self, + _: EditorId, + _: &EditorStyle, + line: usize, + ) -> PhantomTextLine { + let config = &self.common.config.get_untracked(); + + let (start_offset, end_offset) = self.buffer.with_untracked(|buffer| { + (buffer.offset_of_line(line), buffer.offset_of_line(line + 1)) + }); + + let inlay_hints = self.inlay_hints.get_untracked(); + // If hints are enabled, and the hints field is filled, then get the hints for this line + // and convert them into PhantomText instances + let hints = config + .editor + .enable_inlay_hints + .then_some(()) + .and(inlay_hints.as_ref()) + .map(|hints| hints.iter_chunks(start_offset..end_offset)) + .into_iter() + .flatten() + .filter(|(interval, _)| { + interval.start >= start_offset && interval.start < end_offset + }) + .map(|(interval, inlay_hint)| { + let (col, affinity) = self.buffer.with_untracked(|b| { + let mut cursor = + lapce_xi_rope::Cursor::new(b.text(), interval.start); + + let next_char = cursor.peek_next_codepoint(); + let prev_char = cursor.prev_codepoint(); + + let mut affinity = None; + if let Some(prev_char) = prev_char { + let c = get_char_property(prev_char); + if c == CharClassification::Other { + affinity = Some(CursorAffinity::Backward) + } else if matches!( + c, + CharClassification::Lf + | CharClassification::Cr + | CharClassification::Space + ) { + affinity = Some(CursorAffinity::Forward) + } + }; + if affinity.is_none() { + if let Some(next_char) = next_char { + let c = get_char_property(next_char); + if c == CharClassification::Other { + affinity = Some(CursorAffinity::Forward) + } else if matches!( + c, + CharClassification::Lf + | CharClassification::Cr + | CharClassification::Space + ) { + affinity = Some(CursorAffinity::Backward) + } + } + } + + let (_, col) = b.offset_to_line_col(interval.start); + (col, affinity) + }); + let text = match &inlay_hint.label { + InlayHintLabel::String(label) => label.to_string(), + InlayHintLabel::LabelParts(parts) => { + parts.iter().map(|p| &p.value).join("") + } + }; + PhantomText { + kind: PhantomTextKind::InlayHint, + col, + text, + affinity, + fg: Some(config.color(LapceColor::INLAY_HINT_FOREGROUND)), + // font_family: Some(config.editor.inlay_hint_font_family()), + font_size: Some(config.editor.inlay_hint_font_size()), + bg: Some(config.color(LapceColor::INLAY_HINT_BACKGROUND)), + under_line: None, + } + }); + // You're quite unlikely to have more than six hints on a single line + // this later has the diagnostics added onto it, but that's still likely to be below six + // overall. + let mut text: SmallVec<[PhantomText; 6]> = hints.collect(); + + // If error lens is enabled, and the diagnostics field is filled, then get the diagnostics + // that end on this line which have a severity worse than HINT and convert them into + // PhantomText instances + + let mut diag_text: SmallVec<[PhantomText; 6]> = + self.buffer.with_untracked(|buffer| { + config + .editor + .enable_error_lens + .then_some(()) + .map(|_| self.diagnostics.diagnostics_span.get_untracked()) + .map(|diags| { + diags + .iter_chunks(start_offset..end_offset) + .filter_map(|(iv, diag)| { + let end = iv.end(); + let end_line = buffer.line_of_offset(end); + if end_line == line + && diag.severity < Some(DiagnosticSeverity::HINT) + { + let fg = { + let severity = diag + .severity + .unwrap_or(DiagnosticSeverity::WARNING); + let theme_prop = if severity + == DiagnosticSeverity::ERROR + { + LapceColor::ERROR_LENS_ERROR_FOREGROUND + } else if severity + == DiagnosticSeverity::WARNING + { + LapceColor::ERROR_LENS_WARNING_FOREGROUND + } else { + // information + hint (if we keep that) + things without a severity + LapceColor::ERROR_LENS_OTHER_FOREGROUND + }; + + config.color(theme_prop) + }; + + let text = + if config.editor.only_render_error_styling { + "".to_string() + } else if config.editor.error_lens_multiline + { + format!(" {}", diag.message) + } else { + format!( + " {}", + diag.message.lines().join(" ") + ) + }; + Some(PhantomText { + kind: PhantomTextKind::Diagnostic, + col: end_offset - start_offset, + affinity: Some(CursorAffinity::Backward), + text, + fg: Some(fg), + font_size: Some( + config.editor.error_lens_font_size(), + ), + bg: None, + under_line: None, + }) + } else { + None + } + }) + .collect::>() + }) + .unwrap_or_default() + }); + + text.append(&mut diag_text); + + let (completion_line, completion_col) = self.completion_pos.get_untracked(); + let completion_text = config + .editor + .enable_completion_lens + .then_some(()) + .and(self.completion_lens.get_untracked()) + // TODO: We're probably missing on various useful completion things to include here! + .filter(|_| line == completion_line) + .map(|completion| PhantomText { + kind: PhantomTextKind::Completion, + col: completion_col, + text: completion.clone(), + fg: Some(config.color(LapceColor::COMPLETION_LENS_FOREGROUND)), + font_size: Some(config.editor.completion_lens_font_size()), + affinity: Some(CursorAffinity::Backward), + // font_family: Some(config.editor.completion_lens_font_family()), + bg: None, + under_line: None, + // TODO: italics? + }); + if let Some(completion_text) = completion_text { + text.push(completion_text); + } + + // TODO: don't display completion lens and inline completion at the same time + // and/or merge them so that they can be shifted between like multiple inline completions + // can + let (inline_completion_line, inline_completion_col) = + self.inline_completion_pos.get_untracked(); + let inline_completion_text = config + .editor + .enable_inline_completion + .then_some(()) + .and(self.inline_completion.get_untracked()) + .filter(|_| line == inline_completion_line) + .map(|completion| PhantomText { + kind: PhantomTextKind::Completion, + col: inline_completion_col, + text: completion.clone(), + affinity: Some(CursorAffinity::Backward), + fg: Some(config.color(LapceColor::COMPLETION_LENS_FOREGROUND)), + font_size: Some(config.editor.completion_lens_font_size()), + // font_family: Some(config.editor.completion_lens_font_family()), + bg: None, + under_line: None, + // TODO: italics? + }); + if let Some(inline_completion_text) = inline_completion_text { + text.push(inline_completion_text); + } + + if let Some(preedit) = self + .preedit_phantom(Some(config.color(LapceColor::EDITOR_FOREGROUND)), line) + { + text.push(preedit) + } + + text.sort_by(|a, b| { + if a.col == b.col { + a.kind.cmp(&b.kind) + } else { + a.col.cmp(&b.col) + } + }); + + PhantomTextLine { text } + } + + fn has_multiline_phantom(&self, _: EditorId, _: &EditorStyle) -> bool { + // TODO: actually check + true + } +} +impl CommonAction for Doc { + fn exec_motion_mode( + &self, + _ed: &Editor, + cursor: &mut Cursor, + motion_mode: MotionMode, + range: Range, + is_vertical: bool, + register: &mut Register, + ) { + let deltas = self + .buffer + .try_update(move |buffer| { + Action::execute_motion_mode( + cursor, + buffer, + motion_mode, + range, + is_vertical, + register, + ) + }) + .unwrap(); + self.apply_deltas(&deltas); + } + + fn do_edit( + &self, + _ed: &Editor, + cursor: &mut Cursor, + cmd: &EditCommand, + modal: bool, + register: &mut Register, + smart_tab: bool, + ) -> bool { + let deltas = Doc::do_edit(self, cursor, cmd, modal, register, smart_tab); + !deltas.is_empty() + } +} + +#[derive(Clone)] +pub struct DocStyling { + config: ReadSignal>, + doc: Rc, +} +impl DocStyling { + fn apply_colorization( + &self, + edid: EditorId, + style: &EditorStyle, + line: usize, + attrs: &Attrs, + attrs_list: &mut AttrsList, + ) { + let config = self.config.get_untracked(); + let phantom_text = self.doc.phantom_text(edid, style, line); + if let Some(bracket_styles) = self.doc.parser.borrow().bracket_pos.get(&line) + { + for bracket_style in bracket_styles.iter() { + if let Some(fg_color) = bracket_style.style.fg_color.as_ref() { + if let Some(fg_color) = config.style_color(fg_color) { + let start = phantom_text.col_at(bracket_style.start); + let end = phantom_text.col_at(bracket_style.end); + attrs_list + .add_span(start..end, attrs.clone().color(fg_color)); + } + } + } + } + } +} +impl Styling for DocStyling { + fn id(&self) -> u64 { + self.config.with_untracked(|config| config.id) + } + + fn font_size(&self, _: EditorId, _line: usize) -> usize { + self.config + .with_untracked(|config| config.editor.font_size()) + } + + fn line_height(&self, _: EditorId, _line: usize) -> f32 { + self.config + .with_untracked(|config| config.editor.line_height()) as f32 + } + + fn font_family( + &self, + _: EditorId, + _line: usize, + ) -> std::borrow::Cow<'_, [floem::text::FamilyOwned]> { + // TODO: cache this + Cow::Owned(self.config.with_untracked(|config| { + FamilyOwned::parse_list(&config.editor.font_family).collect() + })) + } + + fn weight(&self, _: EditorId, _line: usize) -> floem::text::Weight { + floem::text::Weight::NORMAL + } + + fn italic_style(&self, _: EditorId, _line: usize) -> floem::text::Style { + floem::text::Style::Normal + } + + fn stretch(&self, _: EditorId, _line: usize) -> floem::text::Stretch { + floem::text::Stretch::Normal + } + + fn indent_line(&self, _: EditorId, line: usize, line_content: &str) -> usize { + if line_content.trim().is_empty() { + let text = self.doc.rope_text(); + let offset = text.offset_of_line(line); + if let Some(offset) = + self.doc.syntax.with_untracked(|s| s.parent_offset(offset)) + { + return text.line_of_offset(offset); + } + } + + line + } + + fn tab_width(&self, _: EditorId, _line: usize) -> usize { + self.config.with_untracked(|config| config.editor.tab_width) + } + + fn atomic_soft_tabs(&self, _: EditorId, _line: usize) -> bool { + self.config + .with_untracked(|config| config.editor.atomic_soft_tabs) + } + + fn apply_attr_styles( + &self, + edid: EditorId, + style: &EditorStyle, + line: usize, + default: Attrs, + attrs_list: &mut AttrsList, + ) { + let config = self.doc.common.config.get_untracked(); + + self.apply_colorization(edid, style, line, &default, attrs_list); + + let phantom_text = self.doc.phantom_text(edid, style, line); + for line_style in self.doc.line_style(line).iter() { + if let Some(fg_color) = line_style.style.fg_color.as_ref() { + if let Some(fg_color) = config.style_color(fg_color) { + let start = phantom_text.col_at(line_style.start); + let end = phantom_text.col_at(line_style.end); + attrs_list.add_span(start..end, default.clone().color(fg_color)); + } + } + } + } + + fn apply_layout_styles( + &self, + edid: EditorId, + style: &EditorStyle, + line: usize, + layout_line: &mut TextLayoutLine, + ) { + let doc = &self.doc; + let config = doc.common.config.get_untracked(); + + layout_line.extra_style.clear(); + let layout = &layout_line.text; + + let phantom_text = doc.phantom_text(edid, style, line); + + let phantom_styles = phantom_text + .offset_size_iter() + .filter(move |(_, _, _, p)| p.bg.is_some() || p.under_line.is_some()) + .flat_map(move |(col_shift, size, col, phantom)| { + let start = col + col_shift; + let end = start + size; + + extra_styles_for_range( + layout, + start, + end, + phantom.bg, + phantom.under_line, + None, + ) + }); + layout_line.extra_style.extend(phantom_styles); + + let (start_offset, end_offset) = doc.buffer.with_untracked(|buffer| { + (buffer.offset_of_line(line), buffer.offset_of_line(line + 1)) + }); + + let mut max_severity: Option = None; + doc.diagnostics.diagnostics_span.with_untracked(|diags| { + diags + .iter_chunks(start_offset..end_offset) + .for_each(|(iv, diag)| { + let start = iv.start(); + let end = iv.end(); + + if start <= end_offset + && end >= start_offset + && diag.severity < Some(DiagnosticSeverity::HINT) + { + let start = start.saturating_sub(start_offset); + let end = end - start_offset; + let start = phantom_text.col_after(start, true); + let end = phantom_text.col_after(end, false); + + match (diag.severity, max_severity) { + (Some(severity), Some(max)) => { + if severity < max { + max_severity = Some(severity); + } + } + (Some(severity), None) => { + max_severity = Some(severity); + } + _ => {} + } + + let color_name = match diag.severity { + Some(DiagnosticSeverity::ERROR) => { + LapceColor::LAPCE_ERROR + } + _ => LapceColor::LAPCE_WARN, + }; + let color = config.color(color_name); + let styles = extra_styles_for_range( + layout, + start, + end, + None, + None, + Some(color), + ); + layout_line.extra_style.extend(styles); + } + }); + }); + + // Add the styling for the diagnostic severity, if applicable + if let Some(max_severity) = max_severity { + let theme_prop = if max_severity == DiagnosticSeverity::ERROR { + LapceColor::ERROR_LENS_ERROR_BACKGROUND + } else if max_severity == DiagnosticSeverity::WARNING { + LapceColor::ERROR_LENS_WARNING_BACKGROUND + } else { + LapceColor::ERROR_LENS_OTHER_BACKGROUND + }; + + let size = layout.size(); + let x1 = if !config.editor.error_lens_end_of_line { + let error_end_x = size.width; + Some(error_end_x.max(size.width)) + } else { + None + }; + + // TODO(minor): Should we show the background only on wrapped lines that have the + // diagnostic actually on that line? + // That would make it more obvious where it is from and matches other editors. + layout_line.extra_style.push(LineExtraStyle { + x: 0.0, + y: 0.0, + width: x1, + height: size.height, + bg_color: Some(config.color(theme_prop)), + under_line: None, + wave_line: None, + }); + } + } + + fn paint_caret(&self, edid: EditorId, _line: usize) -> bool { + let Some(e_data) = self.doc.editor_data(edid) else { + return true; + }; + + // If the find is active, then we don't want to paint the caret + !e_data.find_focus.get_untracked() + } +} + +impl std::fmt::Debug for Doc { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&format!("Document {:?}", self.buffer_id)) + } +} + +/// Get the previous unmatched character `c` from the `offset` using `syntax` if applicable +fn syntax_prev_unmatched( + buffer: &Buffer, + syntax: &Syntax, + c: char, + offset: usize, +) -> Option { + if syntax.layers.is_some() { + syntax.find_tag(offset, true, &CharBuffer::new(c)) + } else { + WordCursor::new(buffer.text(), offset).previous_unmatched(c) + } +} + +fn should_blink( + focus: RwSignal, + keyboard_focus: RwSignal>, +) -> impl Fn() -> bool { + move || { + let Some(focus) = focus.try_get_untracked() else { + return false; + }; + if matches!( + focus, + Focus::Workbench + | Focus::Palette + | Focus::Panel(PanelKind::Plugin) + | Focus::Panel(PanelKind::Search) + | Focus::Panel(PanelKind::SourceControl) + ) { + return true; + } + + if keyboard_focus.get_untracked().is_some() { + return true; + } + false + } +} + +fn extra_styles_for_range( + text_layout: &TextLayout, + start: usize, + end: usize, + bg_color: Option, + under_line: Option, + wave_line: Option, +) -> impl Iterator + '_ { + let start_hit = text_layout.hit_position(start); + let end_hit = text_layout.hit_position(end); + + text_layout + .layout_runs() + .enumerate() + .filter_map(move |(current_line, run)| { + if current_line < start_hit.line || current_line > end_hit.line { + return None; + } + + let x = if current_line == start_hit.line { + start_hit.point.x + } else { + run.glyphs.first().map(|g| g.x).unwrap_or(0.0) as f64 + }; + let end_x = if current_line == end_hit.line { + end_hit.point.x + } else { + run.glyphs.last().map(|g| g.x + g.w).unwrap_or(0.0) as f64 + }; + let width = end_x - x; + + if width == 0.0 { + return None; + } + + let height = (run.max_ascent + run.max_descent) as f64; + let y = run.line_y as f64 - run.max_ascent as f64; + + Some(LineExtraStyle { + x, + y, + width: Some(width), + height, + bg_color, + under_line, + wave_line, + }) + }) +} diff --git a/lapce-app/src/editor.rs b/lapce-app/src/editor.rs new file mode 100644 index 0000000..9e30c6a --- /dev/null +++ b/lapce-app/src/editor.rs @@ -0,0 +1,3926 @@ +use std::{ + collections::{HashMap, HashSet}, + rc::Rc, + str::FromStr, + sync::Arc, + time::Duration, +}; + +use floem::{ + ViewId, + action::{TimerToken, exec_after, show_context_menu}, + ext_event::create_ext_action, + keyboard::Modifiers, + kurbo::{Point, Rect, Vec2}, + menu::{Menu, MenuItem}, + pointer::{MouseButton, PointerInputEvent, PointerMoveEvent}, + prelude::SignalTrack, + reactive::{ + ReadSignal, RwSignal, Scope, SignalGet, SignalUpdate, SignalWith, batch, + use_context, + }, + views::editor::{ + Editor, + command::CommandExecuted, + id::EditorId, + movement, + text::Document, + view::{ + DiffSection, DiffSectionKind, LineInfo, ScreenLines, ScreenLinesBase, + }, + visual_line::{ConfigId, Lines, TextLayoutProvider, VLine, VLineInfo}, + }, +}; +use itertools::Itertools; +use lapce_core::{ + buffer::{ + InvalLines, + diff::DiffLines, + rope_text::{RopeText, RopeTextVal}, + }, + command::{ + EditCommand, FocusCommand, MotionModeCommand, MultiSelectionCommand, + ScrollCommand, + }, + cursor::{Cursor, CursorMode}, + editor::EditType, + mode::{Mode, MotionMode}, + rope_text_pos::RopeTextPosition, + selection::{InsertDrift, SelRegion, Selection}, +}; +use lapce_rpc::{buffer::BufferId, plugin::PluginId, proxy::ProxyResponse}; +use lapce_xi_rope::{Rope, RopeDelta, Transformer}; +use lsp_types::{ + CodeActionResponse, CompletionItem, CompletionTextEdit, GotoDefinitionResponse, + HoverContents, InlayHint, InlayHintLabel, InlineCompletionTriggerKind, Location, + MarkedString, MarkupKind, Range, TextEdit, +}; +use nucleo::Utf32Str; +use serde::{Deserialize, Serialize}; +use view::StickyHeaderInfo; + +use self::{ + diff::DiffInfo, + location::{EditorLocation, EditorPosition}, +}; +use crate::{ + command::{CommandKind, InternalCommand, LapceCommand, LapceWorkbenchCommand}, + completion::CompletionStatus, + config::LapceConfig, + db::LapceDb, + doc::{Doc, DocContent}, + editor_tab::EditorTabChild, + id::{DiffEditorId, EditorTabId}, + inline_completion::{InlineCompletionItem, InlineCompletionStatus}, + keypress::{KeyPressFocus, condition::Condition}, + lsp::path_from_url, + main_split::{Editors, MainSplitData, SplitDirection, SplitMoveDirection}, + markdown::{ + MarkdownContent, from_marked_string, from_plaintext, parse_markdown, + }, + panel::{ + call_hierarchy_view::CallHierarchyItemData, + implementation_view::{init_implementation_root, map_to_location}, + kind::PanelKind, + }, + snippet::Snippet, + tracing::*, + window_tab::{CommonData, Focus, WindowTabData}, +}; + +pub mod diff; +pub mod gutter; +pub mod location; +pub mod view; + +#[derive(Clone, Debug)] +pub enum InlineFindDirection { + Left, + Right, +} + +#[derive(Clone, Serialize, Deserialize)] +pub struct EditorInfo { + pub content: DocContent, + pub unsaved: Option, + pub offset: usize, + pub scroll_offset: (f64, f64), +} + +impl EditorInfo { + pub fn to_data( + &self, + data: MainSplitData, + editor_tab_id: EditorTabId, + ) -> EditorId { + let editors = &data.editors; + let common = data.common.clone(); + match &self.content { + DocContent::File { path, .. } => { + let (doc, new_doc) = + data.get_doc(path.clone(), self.unsaved.clone()); + let editor = editors.make_from_doc( + data.scope, + doc, + Some(editor_tab_id), + None, + None, + common, + ); + editor.go_to_location( + EditorLocation { + path: path.clone(), + position: Some(EditorPosition::Offset(self.offset)), + scroll_offset: Some(Vec2::new( + self.scroll_offset.0, + self.scroll_offset.1, + )), + ignore_unconfirmed: false, + same_editor_tab: false, + }, + new_doc, + None, + ); + + editor.id() + } + DocContent::Local => editors.new_local(data.scope, common), + DocContent::History(_) => editors.new_local(data.scope, common), + DocContent::Scratch { name, .. } => { + let doc = data + .scratch_docs + .try_update(|scratch_docs| { + if let Some(doc) = scratch_docs.get(name) { + return doc.clone(); + } + let content = DocContent::Scratch { + id: BufferId::next(), + name: name.to_string(), + }; + let doc = Doc::new_content( + data.scope, + content, + data.editors, + data.common.clone(), + ); + let doc = Rc::new(doc); + if let Some(unsaved) = &self.unsaved { + doc.reload(Rope::from(unsaved), false); + } + scratch_docs.insert(name.to_string(), doc.clone()); + doc + }) + .unwrap(); + + editors.new_from_doc( + data.scope, + doc, + Some(editor_tab_id), + None, + None, + common, + ) + } + } + } +} + +#[derive(Clone)] +pub enum EditorViewKind { + Normal, + Diff(DiffInfo), +} + +impl EditorViewKind { + pub fn is_normal(&self) -> bool { + matches!(self, EditorViewKind::Normal) + } +} + +#[derive(Clone)] +pub struct OnScreenFind { + pub active: bool, + pub pattern: String, + pub regions: Vec, +} + +pub type SnippetIndex = Vec<(usize, (usize, usize))>; + +/// Shares data between cloned instances as long as the signals aren't swapped out. +#[derive(Clone, Debug)] +pub struct EditorData { + pub scope: Scope, + pub editor_tab_id: RwSignal>, + pub diff_editor_id: RwSignal>, + pub confirmed: RwSignal, + pub snippet: RwSignal>, + pub inline_find: RwSignal>, + pub on_screen_find: RwSignal, + pub last_inline_find: RwSignal>, + pub find_focus: RwSignal, + pub editor: Rc, + pub kind: RwSignal, + pub sticky_header_height: RwSignal, + pub common: Rc, + pub sticky_header_info: RwSignal, +} + +impl PartialEq for EditorData { + fn eq(&self, other: &Self) -> bool { + self.id() == other.id() + } +} + +impl EditorData { + fn new( + cx: Scope, + editor: Editor, + editor_tab_id: Option, + diff_editor_id: Option<(EditorTabId, DiffEditorId)>, + confirmed: Option>, + common: Rc, + ) -> Self { + let cx = cx.create_child(); + + let confirmed = confirmed.unwrap_or_else(|| cx.create_rw_signal(false)); + EditorData { + scope: cx, + editor_tab_id: cx.create_rw_signal(editor_tab_id), + diff_editor_id: cx.create_rw_signal(diff_editor_id), + confirmed, + snippet: cx.create_rw_signal(None), + inline_find: cx.create_rw_signal(None), + on_screen_find: cx.create_rw_signal(OnScreenFind { + active: false, + pattern: "".to_string(), + regions: Vec::new(), + }), + last_inline_find: cx.create_rw_signal(None), + find_focus: cx.create_rw_signal(false), + editor: Rc::new(editor), + kind: cx.create_rw_signal(EditorViewKind::Normal), + sticky_header_height: cx.create_rw_signal(0.0), + common, + sticky_header_info: cx.create_rw_signal(StickyHeaderInfo::default()), + } + } + + /// Create a new local editor. + /// + /// You should prefer calling [`Editors::make_local`] / [`Editors::new_local`] instead to + /// register the editor. + pub fn new_local(cx: Scope, editors: Editors, common: Rc) -> Self { + Self::new_local_id(cx, EditorId::next(), editors, common) + } + + /// Create a new local editor with the given id. + /// + /// You should prefer calling [`Editors::make_local`] / [`Editors::new_local`] instead to + /// register the editor. + pub fn new_local_id( + cx: Scope, + editor_id: EditorId, + editors: Editors, + common: Rc, + ) -> Self { + let cx = cx.create_child(); + let doc = Rc::new(Doc::new_local(cx, editors, common.clone())); + let editor = doc.create_editor(cx, editor_id, true); + Self::new(cx, editor, None, None, None, common) + } + + /// Create a new editor with a specific doc. + /// + /// You should prefer calling [`Editors::new_editor_doc`] / [`Editors::make_from_doc`] instead. + pub fn new_doc( + cx: Scope, + doc: Rc, + editor_tab_id: Option, + diff_editor_id: Option<(EditorTabId, DiffEditorId)>, + confirmed: Option>, + common: Rc, + ) -> Self { + let editor = doc.create_editor(cx, EditorId::next(), false); + Self::new(cx, editor, editor_tab_id, diff_editor_id, confirmed, common) + } + + /// Swap out the document this editor is for + pub fn update_doc(&self, doc: Rc) { + let style = doc.styling(); + self.editor.update_doc(doc, Some(style)); + } + + /// Create a new editor using the same underlying [`Doc`] + pub fn copy( + &self, + cx: Scope, + editor_tab_id: Option, + diff_editor_id: Option<(EditorTabId, DiffEditorId)>, + confirmed: Option>, + ) -> Self { + let cx = cx.create_child(); + + let confirmed = confirmed.unwrap_or_else(|| cx.create_rw_signal(true)); + + let editor = Self::new_doc( + cx, + self.doc(), + editor_tab_id, + diff_editor_id, + Some(confirmed), + self.common.clone(), + ); + editor.editor.cursor.set(self.editor.cursor.get_untracked()); + editor + .editor + .viewport + .set(self.editor.viewport.get_untracked()); + editor.editor.scroll_to.set(Some( + self.editor.viewport.get_untracked().origin().to_vec2(), + )); + editor + .editor + .last_movement + .set(self.editor.last_movement.get_untracked()); + + editor + } + + pub fn id(&self) -> EditorId { + self.editor.id() + } + + pub fn editor_info(&self, _data: &WindowTabData) -> EditorInfo { + let offset = self.cursor().get_untracked().offset(); + let scroll_offset = self.viewport().get_untracked().origin(); + let doc = self.doc(); + let is_pristine = doc.is_pristine(); + let unsaved = if is_pristine { + None + } else { + Some(doc.buffer.with_untracked(|b| b.to_string())) + }; + EditorInfo { + content: self.doc().content.get_untracked(), + unsaved, + offset, + scroll_offset: (scroll_offset.x, scroll_offset.y), + } + } + + pub fn cursor(&self) -> RwSignal { + self.editor.cursor + } + + pub fn viewport(&self) -> RwSignal { + self.editor.viewport + } + + pub fn window_origin(&self) -> RwSignal { + self.editor.window_origin + } + + pub fn scroll_delta(&self) -> RwSignal { + self.editor.scroll_delta + } + + pub fn scroll_to(&self) -> RwSignal> { + self.editor.scroll_to + } + + pub fn active(&self) -> RwSignal { + self.editor.active + } + + /// Get the line information for lines on the screen. + pub fn screen_lines(&self) -> RwSignal { + self.editor.screen_lines + } + + pub fn doc(&self) -> Rc { + let doc = self.editor.doc(); + let Ok(doc) = (doc as Rc).downcast() else { + panic!("doc is not Rc"); + }; + + doc + } + + /// The signal for the editor's document. + pub fn doc_signal(&self) -> DocSignal { + DocSignal { + inner: self.editor.doc_signal(), + } + } + + pub fn text(&self) -> Rope { + self.editor.text() + } + + pub fn rope_text(&self) -> RopeTextVal { + self.editor.rope_text() + } + + fn run_edit_command(&self, cmd: &EditCommand) -> CommandExecuted { + let doc = self.doc(); + let text = self.editor.rope_text(); + let is_local = doc.content.with_untracked(|content| content.is_local()); + let modal = self.editor.es.with_untracked(|s| s.modal()) && !is_local; + let smart_tab = self + .common + .config + .with_untracked(|config| config.editor.smart_tab); + let doc_before_edit = text.text().clone(); + let mut cursor = self.editor.cursor.get_untracked(); + let mut register = self.common.register.get_untracked(); + + let yank_data = + if let lapce_core::cursor::CursorMode::Visual { .. } = &cursor.mode { + Some(cursor.yank(&text)) + } else { + None + }; + + let deltas = + batch(|| doc.do_edit(&mut cursor, cmd, modal, &mut register, smart_tab)); + + if !deltas.is_empty() { + if let Some(data) = yank_data { + register.add_delete(data); + } + } + + self.editor.cursor.set(cursor); + self.editor.register.set(register); + + if show_completion(cmd, &doc_before_edit, &deltas) { + self.update_completion(false); + } else { + self.cancel_completion(); + } + + if *cmd == EditCommand::InsertNewLine { + // Cancel so that there's no flickering + self.cancel_inline_completion(); + self.update_inline_completion(InlineCompletionTriggerKind::Automatic); + self.quit_on_screen_find(); + } else if show_inline_completion(cmd) { + self.update_inline_completion(InlineCompletionTriggerKind::Automatic); + } else { + self.cancel_inline_completion(); + } + + self.apply_deltas(&deltas); + if let EditCommand::NormalMode = cmd { + self.snippet.set(None); + self.quit_on_screen_find(); + } + + CommandExecuted::Yes + } + + fn run_motion_mode_command( + &self, + cmd: &MotionModeCommand, + count: Option, + ) -> CommandExecuted { + let count = count.unwrap_or(1); + let motion_mode = match cmd { + MotionModeCommand::MotionModeDelete => MotionMode::Delete { count }, + MotionModeCommand::MotionModeIndent => MotionMode::Indent, + MotionModeCommand::MotionModeOutdent => MotionMode::Outdent, + MotionModeCommand::MotionModeYank => MotionMode::Yank { count }, + }; + let mut cursor = self.editor.cursor.get_untracked(); + let mut register = self.common.register.get_untracked(); + + movement::do_motion_mode( + &self.editor, + &*self.doc(), + &mut cursor, + motion_mode, + &mut register, + ); + + self.editor.cursor.set(cursor); + self.common.register.set(register); + + CommandExecuted::Yes + } + + fn run_multi_selection_command( + &self, + cmd: &MultiSelectionCommand, + ) -> CommandExecuted { + let mut cursor = self.editor.cursor.get_untracked(); + let rope_text = self.rope_text(); + let doc = self.doc(); + let config = self.common.config.get_untracked(); + + // This is currently special-cased in Lapce because floem editor does not have 'find' + match cmd { + MultiSelectionCommand::SelectAllCurrent => { + if let CursorMode::Insert(mut selection) = cursor.mode.clone() { + if !selection.is_empty() { + let find = doc.find(); + + let first = selection.first().unwrap(); + let (start, end) = if first.is_caret() { + rope_text.select_word(first.start) + } else { + (first.min(), first.max()) + }; + let search_str = rope_text.slice_to_cow(start..end); + let case_sensitive = find.case_sensitive(false); + let multicursor_case_sensitive = + config.editor.multicursor_case_sensitive; + let case_sensitive = + multicursor_case_sensitive || case_sensitive; + // let search_whole_word = config.editor.multicursor_whole_words; + find.set_case_sensitive(case_sensitive); + find.set_find(&search_str); + let mut offset = 0; + while let Some((start, end)) = + find.next(rope_text.text(), offset, false, false) + { + offset = end; + selection.add_region(SelRegion::new(start, end, None)); + } + } + cursor.set_insert(selection); + } + } + MultiSelectionCommand::SelectNextCurrent => { + if let CursorMode::Insert(mut selection) = cursor.mode.clone() { + if !selection.is_empty() { + let mut had_caret = false; + for region in selection.regions_mut() { + if region.is_caret() { + had_caret = true; + let (start, end) = + rope_text.select_word(region.start); + region.start = start; + region.end = end; + } + } + if !had_caret { + let find = doc.find(); + + let r = selection.last_inserted().unwrap(); + let search_str = + rope_text.slice_to_cow(r.min()..r.max()); + let case_sensitive = find.case_sensitive(false); + let case_sensitive = + config.editor.multicursor_case_sensitive + || case_sensitive; + // let search_whole_word = + // config.editor.multicursor_whole_words; + find.set_case_sensitive(case_sensitive); + find.set_find(&search_str); + let mut offset = r.max(); + let mut seen = HashSet::new(); + while let Some((start, end)) = + find.next(rope_text.text(), offset, false, true) + { + if !selection + .regions() + .iter() + .any(|r| r.min() == start && r.max() == end) + { + selection.add_region(SelRegion::new( + start, end, None, + )); + break; + } + if seen.contains(&end) { + break; + } + offset = end; + seen.insert(offset); + } + } + } + cursor.set_insert(selection); + } + } + MultiSelectionCommand::SelectSkipCurrent => { + if let CursorMode::Insert(mut selection) = cursor.mode.clone() { + if !selection.is_empty() { + let r = selection.last_inserted().unwrap(); + if r.is_caret() { + let (start, end) = rope_text.select_word(r.start); + selection.replace_last_inserted_region(SelRegion::new( + start, end, None, + )); + } else { + let find = doc.find(); + + let search_str = + rope_text.slice_to_cow(r.min()..r.max()); + find.set_find(&search_str); + let mut offset = r.max(); + let mut seen = HashSet::new(); + while let Some((start, end)) = + find.next(rope_text.text(), offset, false, true) + { + if !selection + .regions() + .iter() + .any(|r| r.min() == start && r.max() == end) + { + selection.replace_last_inserted_region( + SelRegion::new(start, end, None), + ); + break; + } + if seen.contains(&end) { + break; + } + offset = end; + seen.insert(offset); + } + } + } + cursor.set_insert(selection); + } + } + _ => movement::do_multi_selection(&self.editor, &mut cursor, cmd), + }; + + self.editor.cursor.set(cursor); + // self.cancel_signature(); + self.cancel_completion(); + self.cancel_inline_completion(); + CommandExecuted::Yes + } + + fn run_move_command( + &self, + movement: &lapce_core::movement::Movement, + count: Option, + mods: Modifiers, + ) -> CommandExecuted { + self.common.hover.active.set(false); + if movement.is_jump() + && movement != &self.editor.last_movement.get_untracked() + { + let path = self + .doc() + .content + .with_untracked(|content| content.path().cloned()); + if let Some(path) = path { + let offset = self.cursor().with_untracked(|c| c.offset()); + let scroll_offset = + self.viewport().get_untracked().origin().to_vec2(); + self.common.internal_command.send( + InternalCommand::SaveJumpLocation { + path, + offset, + scroll_offset, + }, + ); + } + } + self.editor.last_movement.set(movement.clone()); + + let mut cursor = self.cursor().get_untracked(); + self.common.register.update(|register| { + movement::move_cursor( + &self.editor, + &*self.doc(), + &mut cursor, + movement, + count.unwrap_or(1), + mods.shift(), + register, + ) + }); + + self.editor.cursor.set(cursor); + + if self.snippet.with_untracked(|s| s.is_some()) { + self.snippet.update(|snippet| { + let offset = self.editor.cursor.get_untracked().offset(); + let mut within_region = false; + for (_, (start, end)) in snippet.as_mut().unwrap() { + if offset >= *start && offset <= *end { + within_region = true; + break; + } + } + if !within_region { + *snippet = None; + } + }) + } + self.cancel_completion(); + CommandExecuted::Yes + } + + pub fn run_scroll_command( + &self, + cmd: &ScrollCommand, + count: Option, + mods: Modifiers, + ) -> CommandExecuted { + let prev_completion_index = self + .common + .completion + .with_untracked(|c| c.active.get_untracked()); + + match cmd { + ScrollCommand::PageUp => { + self.editor.page_move(false, mods); + } + ScrollCommand::PageDown => { + self.editor.page_move(true, mods); + } + ScrollCommand::ScrollUp => { + self.scroll(false, count.unwrap_or(1), mods); + } + ScrollCommand::ScrollDown => { + self.scroll(true, count.unwrap_or(1), mods); + } + // TODO: + ScrollCommand::CenterOfWindow => {} + ScrollCommand::TopOfWindow => {} + ScrollCommand::BottomOfWindow => {} + } + + let current_completion_index = self + .common + .completion + .with_untracked(|c| c.active.get_untracked()); + + if prev_completion_index != current_completion_index { + self.common.completion.with_untracked(|c| { + let cursor_offset = self.cursor().with_untracked(|c| c.offset()); + c.update_document_completion(self, cursor_offset); + }); + } + + CommandExecuted::Yes + } + + pub fn run_focus_command( + &self, + cmd: &FocusCommand, + _count: Option, + mods: Modifiers, + ) -> CommandExecuted { + // TODO(minor): Evaluate whether we should split this into subenums, + // such as actions specific to the actual editor pane, movement, and list movement. + let prev_completion_index = self + .common + .completion + .with_untracked(|c| c.active.get_untracked()); + + match cmd { + FocusCommand::ModalClose => { + self.cancel_completion(); + } + FocusCommand::SplitVertical => { + if let Some(editor_tab_id) = + self.editor_tab_id.read_only().get_untracked() + { + self.common.internal_command.send(InternalCommand::Split { + direction: SplitDirection::Vertical, + editor_tab_id, + }); + } else if let Some((editor_tab_id, _)) = + self.diff_editor_id.get_untracked() + { + self.common.internal_command.send(InternalCommand::Split { + direction: SplitDirection::Vertical, + editor_tab_id, + }); + } else { + return CommandExecuted::No; + } + } + FocusCommand::SplitHorizontal => { + if let Some(editor_tab_id) = self.editor_tab_id.get_untracked() { + self.common.internal_command.send(InternalCommand::Split { + direction: SplitDirection::Horizontal, + editor_tab_id, + }); + } else if let Some((editor_tab_id, _)) = + self.diff_editor_id.get_untracked() + { + self.common.internal_command.send(InternalCommand::Split { + direction: SplitDirection::Horizontal, + editor_tab_id, + }); + } else { + return CommandExecuted::No; + } + } + FocusCommand::SplitRight => { + if let Some(editor_tab_id) = self.editor_tab_id.get_untracked() { + self.common + .internal_command + .send(InternalCommand::SplitMove { + direction: SplitMoveDirection::Right, + editor_tab_id, + }); + } else if let Some((editor_tab_id, _)) = + self.diff_editor_id.get_untracked() + { + self.common + .internal_command + .send(InternalCommand::SplitMove { + direction: SplitMoveDirection::Right, + editor_tab_id, + }); + } else { + return CommandExecuted::No; + } + } + FocusCommand::SplitLeft => { + if let Some(editor_tab_id) = self.editor_tab_id.get_untracked() { + self.common + .internal_command + .send(InternalCommand::SplitMove { + direction: SplitMoveDirection::Left, + editor_tab_id, + }); + } else if let Some((editor_tab_id, _)) = + self.diff_editor_id.get_untracked() + { + self.common + .internal_command + .send(InternalCommand::SplitMove { + direction: SplitMoveDirection::Left, + editor_tab_id, + }); + } else { + return CommandExecuted::No; + } + } + FocusCommand::SplitUp => { + if let Some(editor_tab_id) = self.editor_tab_id.get_untracked() { + self.common + .internal_command + .send(InternalCommand::SplitMove { + direction: SplitMoveDirection::Up, + editor_tab_id, + }); + } else if let Some((editor_tab_id, _)) = + self.diff_editor_id.get_untracked() + { + self.common + .internal_command + .send(InternalCommand::SplitMove { + direction: SplitMoveDirection::Up, + editor_tab_id, + }); + } else { + return CommandExecuted::No; + } + } + FocusCommand::SplitDown => { + if let Some(editor_tab_id) = self.editor_tab_id.get_untracked() { + self.common + .internal_command + .send(InternalCommand::SplitMove { + direction: SplitMoveDirection::Down, + editor_tab_id, + }); + } else if let Some((editor_tab_id, _)) = + self.diff_editor_id.get_untracked() + { + self.common + .internal_command + .send(InternalCommand::SplitMove { + direction: SplitMoveDirection::Down, + editor_tab_id, + }); + } else { + return CommandExecuted::No; + } + } + FocusCommand::SplitExchange => { + if let Some(editor_tab_id) = self.editor_tab_id.get_untracked() { + self.common + .internal_command + .send(InternalCommand::SplitExchange { editor_tab_id }); + } else if let Some((editor_tab_id, _)) = + self.diff_editor_id.get_untracked() + { + self.common + .internal_command + .send(InternalCommand::SplitExchange { editor_tab_id }); + } else { + return CommandExecuted::No; + } + } + FocusCommand::SplitClose => { + if let Some(editor_tab_id) = self.editor_tab_id.get_untracked() { + self.common.internal_command.send( + InternalCommand::EditorTabChildClose { + editor_tab_id, + child: EditorTabChild::Editor(self.id()), + }, + ); + } else if let Some((editor_tab_id, diff_editor_id)) = + self.diff_editor_id.get_untracked() + { + self.common.internal_command.send( + InternalCommand::EditorTabChildClose { + editor_tab_id, + child: EditorTabChild::DiffEditor(diff_editor_id), + }, + ); + } else { + return CommandExecuted::No; + } + } + FocusCommand::ListNext => { + self.common.completion.update(|c| { + c.next(); + }); + } + FocusCommand::ListPrevious => { + self.common.completion.update(|c| { + c.previous(); + }); + } + FocusCommand::ListNextPage => { + self.common.completion.update(|c| { + c.next_page(); + }); + } + FocusCommand::ListPreviousPage => { + self.common.completion.update(|c| { + c.previous_page(); + }); + } + FocusCommand::ListSelect => { + self.select_completion(); + self.cancel_inline_completion(); + } + FocusCommand::JumpToNextSnippetPlaceholder => { + self.snippet.update(|snippet| { + if let Some(snippet_mut) = snippet.as_mut() { + let mut current = 0; + let offset = self.cursor().get_untracked().offset(); + for (i, (_, (start, end))) in snippet_mut.iter().enumerate() + { + if *start <= offset && offset <= *end { + current = i; + break; + } + } + + let last_placeholder = current + 1 >= snippet_mut.len() - 1; + + if let Some((_, (start, end))) = snippet_mut.get(current + 1) + { + let mut selection = + lapce_core::selection::Selection::new(); + let region = lapce_core::selection::SelRegion::new( + *start, *end, None, + ); + selection.add_region(region); + self.cursor().update(|cursor| { + cursor.set_insert(selection); + }); + } + + if last_placeholder { + *snippet = None; + } + // self.update_signature(); + self.cancel_completion(); + self.cancel_inline_completion(); + } + }); + } + FocusCommand::JumpToPrevSnippetPlaceholder => { + self.snippet.update(|snippet| { + if let Some(snippet_mut) = snippet.as_mut() { + let mut current = 0; + let offset = self.cursor().get_untracked().offset(); + for (i, (_, (start, end))) in snippet_mut.iter().enumerate() + { + if *start <= offset && offset <= *end { + current = i; + break; + } + } + + if current > 0 { + if let Some((_, (start, end))) = + snippet_mut.get(current - 1) + { + let mut selection = + lapce_core::selection::Selection::new(); + let region = lapce_core::selection::SelRegion::new( + *start, *end, None, + ); + selection.add_region(region); + self.cursor().update(|cursor| { + cursor.set_insert(selection); + }); + } + // self.update_signature(); + self.cancel_completion(); + self.cancel_inline_completion(); + } + } + }); + } + FocusCommand::GotoDefinition => { + self.go_to_definition(); + } + FocusCommand::ShowCodeActions => { + self.show_code_actions(false); + } + FocusCommand::SearchWholeWordForward => { + self.search_whole_word_forward(mods); + } + FocusCommand::SearchForward => { + self.search_forward(mods); + } + FocusCommand::SearchBackward => { + self.search_backward(mods); + } + FocusCommand::Save => { + self.save(true, || {}); + } + FocusCommand::SaveWithoutFormatting => { + self.save(false, || {}); + } + FocusCommand::FormatDocument => { + self.format(); + } + FocusCommand::InlineFindLeft => { + self.inline_find.set(Some(InlineFindDirection::Left)); + } + FocusCommand::InlineFindRight => { + self.inline_find.set(Some(InlineFindDirection::Right)); + } + FocusCommand::OnScreenFind => { + self.on_screen_find.update(|find| { + find.active = true; + find.pattern.clear(); + find.regions.clear(); + }); + } + FocusCommand::RepeatLastInlineFind => { + if let Some((direction, c)) = self.last_inline_find.get_untracked() { + self.inline_find(direction, &c); + } + } + FocusCommand::Rename => { + self.rename(); + } + FocusCommand::ClearSearch => { + self.clear_search(); + } + FocusCommand::Search => { + self.search(); + } + FocusCommand::FocusFindEditor => { + self.common.find.replace_focus.set(false); + } + FocusCommand::FocusReplaceEditor => { + if self.common.find.replace_active.get_untracked() { + self.common.find.replace_focus.set(true); + } + } + FocusCommand::InlineCompletionSelect => { + self.select_inline_completion(); + } + FocusCommand::InlineCompletionNext => { + self.next_inline_completion(); + } + FocusCommand::InlineCompletionPrevious => { + self.previous_inline_completion(); + } + FocusCommand::InlineCompletionCancel => { + self.cancel_inline_completion(); + } + FocusCommand::InlineCompletionInvoke => { + self.update_inline_completion(InlineCompletionTriggerKind::Invoked); + } + FocusCommand::ShowHover => { + let start_offset = self.doc().buffer.with_untracked(|b| { + b.prev_code_boundary(self.cursor().get_untracked().offset()) + }); + self.update_hover(start_offset); + } + _ => {} + } + + let current_completion_index = self + .common + .completion + .with_untracked(|c| c.active.get_untracked()); + + if prev_completion_index != current_completion_index { + self.common.completion.with_untracked(|c| { + let cursor_offset = self.cursor().with_untracked(|c| c.offset()); + c.update_document_completion(self, cursor_offset); + }); + } + + CommandExecuted::Yes + } + + /// Jump to the next/previous column on the line which matches the given text + fn inline_find(&self, direction: InlineFindDirection, c: &str) { + let offset = self.cursor().with_untracked(|c| c.offset()); + let doc = self.doc(); + let (line_content, line_start_offset) = + doc.buffer.with_untracked(|buffer| { + let line = buffer.line_of_offset(offset); + let line_content = buffer.line_content(line); + let line_start_offset = buffer.offset_of_line(line); + (line_content.to_string(), line_start_offset) + }); + let index = offset - line_start_offset; + if let Some(new_index) = match direction { + InlineFindDirection::Left => line_content[..index].rfind(c), + InlineFindDirection::Right => { + if index + 1 >= line_content.len() { + None + } else { + let index = index + + doc.buffer.with_untracked(|buffer| { + buffer.next_grapheme_offset( + offset, + 1, + buffer.offset_line_end(offset, false), + ) + }) + - offset; + line_content[index..].find(c).map(|i| i + index) + } + } + } { + self.run_move_command( + &lapce_core::movement::Movement::Offset( + new_index + line_start_offset, + ), + None, + Modifiers::empty(), + ); + } + } + + fn quit_on_screen_find(&self) { + if self.on_screen_find.with_untracked(|s| s.active) { + self.on_screen_find.update(|f| { + f.active = false; + f.pattern.clear(); + f.regions.clear(); + }) + } + } + + fn on_screen_find(&self, pattern: &str) -> Vec { + let screen_lines = self.screen_lines().get_untracked(); + let lines: HashSet = + screen_lines.lines.iter().map(|l| l.line).collect(); + + let mut matcher = nucleo::Matcher::new(nucleo::Config::DEFAULT); + let pattern = nucleo::pattern::Pattern::parse( + pattern, + nucleo::pattern::CaseMatching::Ignore, + nucleo::pattern::Normalization::Smart, + ); + let mut indices = Vec::new(); + let mut filter_text_buf = Vec::new(); + let mut items = Vec::new(); + + let buffer = self.doc().buffer; + + for line in lines { + filter_text_buf.clear(); + indices.clear(); + + buffer.with_untracked(|buffer| { + let start = buffer.offset_of_line(line); + let end = buffer.offset_of_line(line + 1); + let text = buffer.text().slice_to_cow(start..end); + let filter_text = Utf32Str::new(&text, &mut filter_text_buf); + + if let Some(score) = + pattern.indices(filter_text, &mut matcher, &mut indices) + { + indices.sort(); + let left = + start + indices.first().copied().unwrap_or(0) as usize; + let right = + start + indices.last().copied().unwrap_or(0) as usize + 1; + let right = if right == left { left + 1 } else { right }; + items.push((score, left, right)); + } + }); + } + + items.sort_by_key(|(score, _, _)| -(*score as i64)); + if let Some((_, offset, _)) = items.first().copied() { + self.run_move_command( + &lapce_core::movement::Movement::Offset(offset), + None, + Modifiers::empty(), + ); + } + + items + .into_iter() + .map(|(_, start, end)| SelRegion::new(start, end, None)) + .collect() + } + + fn go_to_definition(&self) { + let doc = self.doc(); + let path = match if doc.loaded() { + doc.content.with_untracked(|c| c.path().cloned()) + } else { + None + } { + Some(path) => path, + None => return, + }; + + let offset = self.cursor().with_untracked(|c| c.offset()); + let (start_position, position) = doc.buffer.with_untracked(|buffer| { + let start_offset = buffer.prev_code_boundary(offset); + let start_position = buffer.offset_to_position(start_offset); + let position = buffer.offset_to_position(offset); + (start_position, position) + }); + + enum DefinitionOrReference { + Location(EditorLocation), + References(Vec), + } + + let internal_command = self.common.internal_command; + let cursor = self.cursor().read_only(); + let send = create_ext_action(self.scope, move |d| { + let current_offset = cursor.with_untracked(|c| c.offset()); + if current_offset != offset { + return; + } + + match d { + DefinitionOrReference::Location(location) => { + internal_command + .send(InternalCommand::JumpToLocation { location }); + } + DefinitionOrReference::References(locations) => { + internal_command.send(InternalCommand::PaletteReferences { + references: locations + .into_iter() + .map(|l| EditorLocation { + path: path_from_url(&l.uri), + position: Some(EditorPosition::Position( + l.range.start, + )), + scroll_offset: None, + ignore_unconfirmed: false, + same_editor_tab: false, + }) + .collect(), + }); + } + } + }); + let proxy = self.common.proxy.clone(); + self.common.proxy.get_definition( + offset, + path.clone(), + position, + move |result| { + if let Ok(ProxyResponse::GetDefinitionResponse { + definition, .. + }) = result + { + if let Some(location) = match definition { + GotoDefinitionResponse::Scalar(location) => Some(location), + GotoDefinitionResponse::Array(locations) => { + if !locations.is_empty() { + Some(locations[0].clone()) + } else { + None + } + } + GotoDefinitionResponse::Link(location_links) => { + let location_link = location_links[0].clone(); + Some(Location { + uri: location_link.target_uri, + range: location_link.target_selection_range, + }) + } + } { + if location.range.start == start_position { + proxy.get_references( + path.clone(), + position, + move |result| { + if let Ok( + ProxyResponse::GetReferencesResponse { + references, + }, + ) = result + { + if references.is_empty() { + return; + } + if references.len() == 1 { + let location = &references[0]; + send(DefinitionOrReference::Location( + EditorLocation { + path: path_from_url( + &location.uri, + ), + position: Some( + EditorPosition::Position( + location.range.start, + ), + ), + scroll_offset: None, + ignore_unconfirmed: false, + same_editor_tab: false, + }, + )); + } else { + send(DefinitionOrReference::References( + references, + )); + } + } + }, + ); + } else { + let path = path_from_url(&location.uri); + send(DefinitionOrReference::Location(EditorLocation { + path, + position: Some(EditorPosition::Position( + location.range.start, + )), + scroll_offset: None, + ignore_unconfirmed: false, + same_editor_tab: false, + })); + } + } + } + }, + ); + } + + pub fn call_hierarchy(&self, window_tab_data: WindowTabData) { + let doc = self.doc(); + let path = match if doc.loaded() { + doc.content.with_untracked(|c| c.path().cloned()) + } else { + None + } { + Some(path) => path, + None => return, + }; + + let offset = self.cursor().with_untracked(|c| c.offset()); + let (_start_position, position) = doc.buffer.with_untracked(|buffer| { + let start_offset = buffer.prev_code_boundary(offset); + let start_position = buffer.offset_to_position(start_offset); + let position = buffer.offset_to_position(offset); + (start_position, position) + }); + let scope = window_tab_data.scope; + let range = Range { + start: _start_position, + end: position, + }; + self.common.proxy.show_call_hierarchy( + path, + position, + create_ext_action(self.scope, move |result| { + if let Ok(ProxyResponse::ShowCallHierarchyResponse { + items, .. + }) = result + { + if let Some(item) = items.and_then(|x| x.into_iter().next()) { + let root = scope.create_rw_signal(CallHierarchyItemData { + view_id: ViewId::new(), + item: Rc::new(item), + from_range: range, + init: false, + open: scope.create_rw_signal(true), + children: scope.create_rw_signal(Vec::with_capacity(0)), + }); + let item = root; + window_tab_data.call_hierarchy_data.root.update(|x| { + *x = Some(root); + }); + window_tab_data.show_panel(PanelKind::CallHierarchy); + window_tab_data.common.internal_command.send( + InternalCommand::CallHierarchyIncoming { + item_id: item.get_untracked().view_id, + }, + ); + } + } + }), + ); + } + + pub fn find_reference(&self, window_tab_data: WindowTabData) { + let doc = self.doc(); + let path = match if doc.loaded() { + doc.content.with_untracked(|c| c.path().cloned()) + } else { + None + } { + Some(path) => path, + None => return, + }; + + let offset = self.cursor().with_untracked(|c| c.offset()); + let (_start_position, position) = doc.buffer.with_untracked(|buffer| { + let start_offset = buffer.prev_code_boundary(offset); + let start_position = buffer.offset_to_position(start_offset); + let position = buffer.offset_to_position(offset); + (start_position, position) + }); + let scope = window_tab_data.scope; + let update_implementation = create_ext_action(self.scope, { + let window_tab_data = window_tab_data.clone(); + move |result| { + if let Ok(ProxyResponse::ReferencesResolveResponse { items }) = + result + { + window_tab_data + .main_split + .references + .update(|x| *x = init_implementation_root(items, scope)); + window_tab_data.show_panel(PanelKind::References); + } + } + }); + let proxy = self.common.proxy.clone(); + self.common.proxy.get_references( + path, + position, + create_ext_action(self.scope, move |result| { + if let Ok(ProxyResponse::GetReferencesResponse { references }) = + result + { + { + if !references.is_empty() { + proxy.references_resolve( + references, + update_implementation, + ); + } else { + window_tab_data.show_panel(PanelKind::References); + } + } + } + }), + ); + } + + pub fn go_to_implementation(&self, window_tab_data: WindowTabData) { + let doc = self.doc(); + let path = match if doc.loaded() { + doc.content.with_untracked(|c| c.path().cloned()) + } else { + None + } { + Some(path) => path, + None => return, + }; + + let offset = self.cursor().with_untracked(|c| c.offset()); + let (_start_position, position) = doc.buffer.with_untracked(|buffer| { + let start_offset = buffer.prev_code_boundary(offset); + let start_position = buffer.offset_to_position(start_offset); + let position = buffer.offset_to_position(offset); + (start_position, position) + }); + let scope = window_tab_data.scope; + let update_implementation = create_ext_action(self.scope, { + let window_tab_data = window_tab_data.clone(); + move |result| { + if let Ok(ProxyResponse::ReferencesResolveResponse { items }) = + result + { + window_tab_data + .main_split + .implementations + .update(|x| *x = init_implementation_root(items, scope)); + window_tab_data.show_panel(PanelKind::Implementation); + } + } + }); + let proxy = self.common.proxy.clone(); + self.common.proxy.go_to_implementation( + path, + position, + create_ext_action(self.scope, { + move |result| { + if let Ok(ProxyResponse::GotoImplementationResponse { + resp, + .. + }) = result + { + let locations = map_to_location(resp); + if !locations.is_empty() { + proxy.references_resolve( + locations, + update_implementation, + ); + } else { + window_tab_data.show_panel(PanelKind::Implementation); + } + } + } + }), + ); + } + + fn scroll(&self, down: bool, count: usize, mods: Modifiers) { + self.editor.scroll( + self.sticky_header_height.get_untracked(), + down, + count, + mods, + ) + } + + fn select_inline_completion(&self) { + if self + .common + .inline_completion + .with_untracked(|c| c.status == InlineCompletionStatus::Inactive) + { + return; + } + + let data = self + .common + .inline_completion + .with_untracked(|c| (c.current_item().cloned(), c.start_offset)); + self.cancel_inline_completion(); + + let (Some(item), start_offset) = data else { + return; + }; + + if let Err(err) = item.apply(self, start_offset) { + tracing::error!("{:?}", err); + } + } + + fn next_inline_completion(&self) { + if self + .common + .inline_completion + .with_untracked(|c| c.status == InlineCompletionStatus::Inactive) + { + return; + } + + self.common.inline_completion.update(|c| { + c.next(); + }); + } + + fn previous_inline_completion(&self) { + if self + .common + .inline_completion + .with_untracked(|c| c.status == InlineCompletionStatus::Inactive) + { + return; + } + + self.common.inline_completion.update(|c| { + c.previous(); + }); + } + + pub fn cancel_inline_completion(&self) { + if self + .common + .inline_completion + .with_untracked(|c| c.status == InlineCompletionStatus::Inactive) + { + return; + } + + self.common.inline_completion.update(|c| { + c.cancel(); + }); + + self.doc().clear_inline_completion(); + } + + /// Update the current inline completion + fn update_inline_completion(&self, trigger_kind: InlineCompletionTriggerKind) { + if self.get_mode() != Mode::Insert { + self.cancel_inline_completion(); + return; + } + + let doc = self.doc(); + let path = match if doc.loaded() { + doc.content.with_untracked(|c| c.path().cloned()) + } else { + None + } { + Some(path) => path, + None => return, + }; + + let offset = self.cursor().with_untracked(|c| c.offset()); + let line = doc + .buffer + .with_untracked(|buffer| buffer.line_of_offset(offset)); + let position = doc + .buffer + .with_untracked(|buffer| buffer.offset_to_position(offset)); + + let inline_completion = self.common.inline_completion; + let doc = self.doc(); + + // Update the inline completion's text if it's already active to avoid flickering + let has_relevant = inline_completion.with_untracked(|completion| { + let c_line = doc.buffer.with_untracked(|buffer| { + buffer.line_of_offset(completion.start_offset) + }); + completion.status != InlineCompletionStatus::Inactive + && line == c_line + && completion.path == path + }); + if has_relevant { + let config = self.common.config.get_untracked(); + inline_completion.update(|completion| { + completion.update_inline_completion(&config, &doc, offset); + }); + } + + let path2 = path.clone(); + let send = create_ext_action( + self.scope, + move |items: Vec| { + let items = doc.buffer.with_untracked(|buffer| { + items + .into_iter() + .map(|item| InlineCompletionItem::from_lsp(buffer, item)) + .collect() + }); + inline_completion.update(|c| { + c.set_items(items, offset, path2); + c.update_doc(&doc, offset); + }); + }, + ); + + inline_completion.update(|c| c.status = InlineCompletionStatus::Started); + + self.common.proxy.get_inline_completions( + path, + position, + trigger_kind, + move |res| { + if let Ok(ProxyResponse::GetInlineCompletions { + completions: items, + }) = res + { + let items = match items { + lsp_types::InlineCompletionResponse::Array(items) => items, + // Currently does not have any relevant extra fields + lsp_types::InlineCompletionResponse::List(items) => { + items.items + } + }; + send(items); + } + }, + ); + } + + /// Check if there are inline completions that are being rendered + fn has_inline_completions(&self) -> bool { + self.common.inline_completion.with_untracked(|completion| { + completion.status != InlineCompletionStatus::Inactive + && !completion.items.is_empty() + }) + } + + pub fn select_completion(&self) { + let item = self + .common + .completion + .with_untracked(|c| c.current_item().cloned()); + self.cancel_completion(); + let doc = self.doc(); + if let Some(item) = item { + if item.item.data.is_some() { + let editor = self.clone(); + let rev = doc.buffer.with_untracked(|buffer| buffer.rev()); + let path = doc.content.with_untracked(|c| c.path().cloned()); + let offset = self.cursor().with_untracked(|c| c.offset()); + let buffer = doc.buffer; + let content = doc.content; + let send = create_ext_action(self.scope, move |item| { + if editor.cursor().with_untracked(|c| c.offset() != offset) { + return; + } + if buffer.with_untracked(|b| b.rev()) != rev + || content.with_untracked(|content| { + content.path() != path.as_ref() + }) + { + return; + } + if let Err(err) = editor.apply_completion_item(&item) { + tracing::error!("{:?}", err); + } + }); + self.common.proxy.completion_resolve( + item.plugin_id, + item.item.clone(), + move |result| { + let item = + if let Ok(ProxyResponse::CompletionResolveResponse { + item, + }) = result + { + *item + } else { + item.item.clone() + }; + send(item); + }, + ); + } else if let Err(err) = self.apply_completion_item(&item.item) { + tracing::error!("{:?}", err); + } + } + } + + pub fn cancel_completion(&self) { + if self.common.completion.with_untracked(|c| c.status) + == CompletionStatus::Inactive + { + return; + } + self.common.completion.update(|c| { + c.cancel(); + }); + + self.doc().clear_completion_lens() + } + + /// Update the displayed autocompletion box + /// Sends a request to the LSP for completion information + fn update_completion(&self, display_if_empty_input: bool) { + if self.get_mode() != Mode::Insert { + self.cancel_completion(); + return; + } + + let doc = self.doc(); + let path = match if doc.loaded() { + doc.content.with_untracked(|c| c.path().cloned()) + } else { + None + } { + Some(path) => path, + None => return, + }; + + let offset = self.cursor().with_untracked(|c| c.offset()); + let (start_offset, input, char) = doc.buffer.with_untracked(|buffer| { + let start_offset = buffer.prev_code_boundary(offset); + let end_offset = buffer.next_code_boundary(offset); + let input = buffer.slice_to_cow(start_offset..end_offset).to_string(); + let char = if start_offset == 0 { + "".to_string() + } else { + buffer + .slice_to_cow(start_offset - 1..start_offset) + .to_string() + }; + (start_offset, input, char) + }); + if !display_if_empty_input && input.is_empty() && char != "." && char != ":" + { + self.cancel_completion(); + return; + } + + if self.common.completion.with_untracked(|completion| { + completion.status != CompletionStatus::Inactive + && completion.offset == start_offset + && completion.path == path + }) { + self.common.completion.update(|completion| { + completion.update_input(input.clone()); + + if !completion.input_items.contains_key("") { + let start_pos = doc.buffer.with_untracked(|buffer| { + buffer.offset_to_position(start_offset) + }); + completion.request( + self.id(), + &self.common.proxy, + path.clone(), + "".to_string(), + start_pos, + ); + } + + if !completion.input_items.contains_key(&input) { + let position = doc + .buffer + .with_untracked(|buffer| buffer.offset_to_position(offset)); + completion.request( + self.id(), + &self.common.proxy, + path, + input, + position, + ); + } + }); + let cursor_offset = self.cursor().with_untracked(|c| c.offset()); + self.common + .completion + .get_untracked() + .update_document_completion(self, cursor_offset); + + return; + } + + let doc = self.doc(); + self.common.completion.update(|completion| { + completion.path.clone_from(&path); + completion.offset = start_offset; + completion.input.clone_from(&input); + completion.status = CompletionStatus::Started; + completion.input_items.clear(); + completion.request_id += 1; + let start_pos = doc + .buffer + .with_untracked(|buffer| buffer.offset_to_position(start_offset)); + completion.request( + self.id(), + &self.common.proxy, + path.clone(), + "".to_string(), + start_pos, + ); + + if !input.is_empty() { + let position = doc + .buffer + .with_untracked(|buffer| buffer.offset_to_position(offset)); + completion.request( + self.id(), + &self.common.proxy, + path, + input, + position, + ); + } + }); + } + + /// Check if there are completions that are being rendered + fn has_completions(&self) -> bool { + self.common.completion.with_untracked(|completion| { + completion.status != CompletionStatus::Inactive + && !completion.filtered_items.is_empty() + }) + } + + fn apply_completion_item(&self, item: &CompletionItem) -> anyhow::Result<()> { + let doc = self.doc(); + let buffer = doc.buffer.get_untracked(); + let cursor = self.cursor().get_untracked(); + // Get all the edits which would be applied in places other than right where the cursor is + let additional_edit: Vec<_> = item + .additional_text_edits + .as_ref() + .into_iter() + .flatten() + .map(|edit| { + let selection = lapce_core::selection::Selection::region( + buffer.offset_of_position(&edit.range.start), + buffer.offset_of_position(&edit.range.end), + ); + (selection, edit.new_text.as_str()) + }) + .collect::>(); + + let text_format = item + .insert_text_format + .unwrap_or(lsp_types::InsertTextFormat::PLAIN_TEXT); + if let Some(edit) = &item.text_edit { + match edit { + CompletionTextEdit::Edit(edit) => { + let offset = cursor.offset(); + let start_offset = buffer.prev_code_boundary(offset); + let end_offset = buffer.next_code_boundary(offset); + let edit_start = buffer.offset_of_position(&edit.range.start); + let edit_end = buffer.offset_of_position(&edit.range.end); + + let selection = lapce_core::selection::Selection::region( + start_offset.min(edit_start), + end_offset.max(edit_end), + ); + match text_format { + lsp_types::InsertTextFormat::PLAIN_TEXT => { + self.do_edit( + &selection, + &[ + &[(selection.clone(), edit.new_text.as_str())][..], + &additional_edit[..], + ] + .concat(), + ); + return Ok(()); + } + lsp_types::InsertTextFormat::SNIPPET => { + self.completion_apply_snippet( + &edit.new_text, + &selection, + additional_edit, + start_offset, + )?; + return Ok(()); + } + _ => {} + } + } + CompletionTextEdit::InsertAndReplace(_) => (), + } + } + + let offset = cursor.offset(); + let start_offset = buffer.prev_code_boundary(offset); + let end_offset = buffer.next_code_boundary(offset); + let selection = Selection::region(start_offset, end_offset); + + self.do_edit( + &selection, + &[ + &[( + selection.clone(), + item.insert_text.as_deref().unwrap_or(item.label.as_str()), + )][..], + &additional_edit[..], + ] + .concat(), + ); + Ok(()) + } + + pub fn completion_apply_snippet( + &self, + snippet: &str, + selection: &Selection, + additional_edit: Vec<(Selection, &str)>, + start_offset: usize, + ) -> anyhow::Result<()> { + let snippet = Snippet::from_str(snippet)?; + let text = snippet.text(); + let mut cursor = self.cursor().get_untracked(); + let old_cursor = cursor.mode.clone(); + let (b_text, delta, inval_lines) = self + .doc() + .do_raw_edit( + &[ + &[(selection.clone(), text.as_str())][..], + &additional_edit[..], + ] + .concat(), + EditType::Completion, + ) + .ok_or_else(|| anyhow::anyhow!("not edited"))?; + + let selection = selection.apply_delta(&delta, true, InsertDrift::Default); + + let mut transformer = Transformer::new(&delta); + let offset = transformer.transform(start_offset, false); + let snippet_tabs = snippet.tabs(offset); + + let doc = self.doc(); + if snippet_tabs.is_empty() { + doc.buffer.update(|buffer| { + cursor.update_selection(buffer, selection); + buffer.set_cursor_before(old_cursor); + buffer.set_cursor_after(cursor.mode.clone()); + }); + self.cursor().set(cursor); + self.apply_deltas(&[(b_text, delta, inval_lines)]); + return Ok(()); + } + + let mut selection = lapce_core::selection::Selection::new(); + let (_tab, (start, end)) = &snippet_tabs[0]; + let region = lapce_core::selection::SelRegion::new(*start, *end, None); + selection.add_region(region); + cursor.set_insert(selection); + + doc.buffer.update(|buffer| { + buffer.set_cursor_before(old_cursor); + buffer.set_cursor_after(cursor.mode.clone()); + }); + self.cursor().set(cursor); + self.apply_deltas(&[(b_text, delta, inval_lines)]); + self.add_snippet_placeholders(snippet_tabs); + Ok(()) + } + + fn add_snippet_placeholders( + &self, + new_placeholders: Vec<(usize, (usize, usize))>, + ) { + self.snippet.update(|snippet| { + if snippet.is_none() { + if new_placeholders.len() > 1 { + *snippet = Some(new_placeholders); + } + return; + } + + let placeholders = snippet.as_mut().unwrap(); + + let mut current = 0; + let offset = self.cursor().get_untracked().offset(); + for (i, (_, (start, end))) in placeholders.iter().enumerate() { + if *start <= offset && offset <= *end { + current = i; + break; + } + } + + let v = placeholders.split_off(current); + placeholders.extend_from_slice(&new_placeholders); + placeholders.extend_from_slice(&v[1..]); + }); + } + + pub fn do_edit( + &self, + selection: &Selection, + edits: &[(impl AsRef, &str)], + ) { + let mut cursor = self.cursor().get_untracked(); + let doc = self.doc(); + let (text, delta, inval_lines) = + match doc.do_raw_edit(edits, EditType::Completion) { + Some(e) => e, + None => return, + }; + let selection = selection.apply_delta(&delta, true, InsertDrift::Default); + let old_cursor = cursor.mode.clone(); + doc.buffer.update(|buffer| { + cursor.update_selection(buffer, selection); + buffer.set_cursor_before(old_cursor); + buffer.set_cursor_after(cursor.mode.clone()); + }); + self.cursor().set(cursor); + + self.apply_deltas(&[(text, delta, inval_lines)]); + } + + pub fn do_text_edit(&self, edits: &[TextEdit]) { + let (selection, edits) = self.doc().buffer.with_untracked(|buffer| { + let selection = self.cursor().get_untracked().edit_selection(buffer); + let edits = edits + .iter() + .map(|edit| { + let selection = lapce_core::selection::Selection::region( + buffer.offset_of_position(&edit.range.start), + buffer.offset_of_position(&edit.range.end), + ); + (selection, edit.new_text.as_str()) + }) + .collect::>(); + (selection, edits) + }); + + self.do_edit(&selection, &edits); + } + + fn apply_deltas(&self, deltas: &[(Rope, RopeDelta, InvalLines)]) { + if !deltas.is_empty() && !self.confirmed.get_untracked() { + self.confirmed.set(true); + } + for (_, delta, _) in deltas { + // self.inactive_apply_delta(delta); + self.update_snippet_offset(delta); + // self.update_breakpoints(delta); + } + // self.update_signature(); + } + + fn update_snippet_offset(&self, delta: &RopeDelta) { + if self.snippet.with_untracked(|s| s.is_some()) { + self.snippet.update(|snippet| { + let mut transformer = Transformer::new(delta); + *snippet = Some( + snippet + .as_ref() + .unwrap() + .iter() + .map(|(tab, (start, end))| { + ( + *tab, + ( + transformer.transform(*start, false), + transformer.transform(*end, true), + ), + ) + }) + .collect(), + ); + }); + } + } + + fn do_go_to_location( + &self, + location: EditorLocation, + edits: Option>, + ) { + if let Some(position) = location.position { + self.go_to_position(position, location.scroll_offset, edits); + } else if let Some(edits) = edits.as_ref() { + self.do_text_edit(edits); + } else { + let db: Arc = use_context().unwrap(); + if let Ok(info) = db.get_doc_info(&self.common.workspace, &location.path) + { + self.go_to_position( + EditorPosition::Offset(info.cursor_offset), + Some(Vec2::new(info.scroll_offset.0, info.scroll_offset.1)), + edits, + ); + } + } + } + + pub fn go_to_location( + &self, + location: EditorLocation, + new_doc: bool, + edits: Option>, + ) { + if !new_doc { + self.do_go_to_location(location, edits); + } else { + let loaded = self.doc().loaded; + let editor = self.clone(); + self.scope.create_effect(move |prev_loaded| { + if prev_loaded == Some(true) { + return true; + } + + let loaded = loaded.get(); + if loaded { + editor.do_go_to_location(location.clone(), edits.clone()); + } + loaded + }); + } + } + + pub fn go_to_position( + &self, + position: EditorPosition, + scroll_offset: Option, + edits: Option>, + ) { + let offset = self + .doc() + .buffer + .with_untracked(|buffer| position.to_offset(buffer)); + let config = self.common.config.get_untracked(); + self.cursor().set(if config.core.modal { + Cursor::new(CursorMode::Normal(offset), None, None) + } else { + Cursor::new(CursorMode::Insert(Selection::caret(offset)), None, None) + }); + if let Some(scroll_offset) = scroll_offset { + self.editor.scroll_to.set(Some(scroll_offset)); + } + if let Some(edits) = edits.as_ref() { + self.do_text_edit(edits); + } + } + + pub fn get_code_actions(&self) { + let doc = self.doc(); + let path = match if doc.loaded() { + doc.content.with_untracked(|c| c.path().cloned()) + } else { + None + } { + Some(path) => path, + None => return, + }; + + let offset = self.cursor().with_untracked(|c| c.offset()); + let exists = doc + .code_actions() + .with_untracked(|c| c.contains_key(&offset)); + + if exists { + return; + } + + // insert some empty data, so that we won't make the request again + doc.code_actions().update(|c| { + c.insert(offset, (PluginId(0), im::Vector::new())); + }); + + let (position, rev, diagnostics) = doc.buffer.with_untracked(|buffer| { + let position = buffer.offset_to_position(offset); + let rev = doc.rev(); + + // Get the diagnostics for the current line, which the LSP might use to inform + // what code actions are available (such as fixes for the diagnostics). + let diagnostics = doc + .diagnostics() + .diagnostics_span + .get_untracked() + .iter_chunks(offset..offset) + .filter(|(iv, _diag)| iv.start <= offset && iv.end >= offset) + .map(|(_iv, diag)| diag) + .cloned() + .collect(); + + (position, rev, diagnostics) + }); + + let send = create_ext_action( + self.scope, + move |resp: (PluginId, CodeActionResponse)| { + if doc.rev() == rev { + doc.code_actions().update(|c| { + c.insert(offset, (resp.0, resp.1.into())); + }); + } + }, + ); + + self.common.proxy.get_code_actions( + path, + position, + diagnostics, + move |result| { + if let Ok(ProxyResponse::GetCodeActionsResponse { + plugin_id, + resp, + }) = result + { + send((plugin_id, resp)) + } + }, + ); + } + + pub fn show_code_actions(&self, mouse_click: bool) { + let offset = self.cursor().with_untracked(|c| c.offset()); + let doc = self.doc(); + let code_actions = doc + .code_actions() + .with_untracked(|c| c.get(&offset).cloned()); + if let Some((plugin_id, code_actions)) = code_actions { + if !code_actions.is_empty() { + self.common.internal_command.send( + InternalCommand::ShowCodeActions { + offset, + mouse_click, + plugin_id, + code_actions, + }, + ); + } + } + } + + fn do_save(&self, after_action: impl FnOnce() + 'static) { + self.doc().save(after_action); + } + + pub fn save( + &self, + allow_formatting: bool, + after_action: impl FnOnce() + 'static, + ) { + let doc = self.doc(); + let is_pristine = doc.is_pristine(); + let content = doc.content.get_untracked(); + + if let DocContent::Scratch { .. } = &content { + self.common + .internal_command + .send(InternalCommand::SaveScratchDoc2 { doc }); + return; + } + + if content.path().is_some() && is_pristine { + return; + } + + let config = self.common.config.get_untracked(); + let DocContent::File { path, .. } = content else { + return; + }; + + // If we are disallowing formatting (such as due to a manual save without formatting), + // then we skip normalizing line endings as a common reason for that is large files. + // (but if the save is typical, even if config format_on_save is false, we normalize) + if allow_formatting && config.editor.normalize_line_endings { + self.run_edit_command(&EditCommand::NormalizeLineEndings); + } + + let rev = doc.rev(); + let format_on_save = allow_formatting && config.editor.format_on_save; + if format_on_save { + let editor = self.clone(); + let send = create_ext_action(self.scope, move |result| { + if let Ok(Ok(ProxyResponse::GetDocumentFormatting { edits })) = + result + { + let current_rev = editor.doc().rev(); + if current_rev == rev { + editor.do_text_edit(&edits); + } + } + editor.do_save(after_action); + }); + + let (tx, rx) = crossbeam_channel::bounded(1); + let proxy = self.common.proxy.clone(); + std::thread::spawn(move || { + proxy.get_document_formatting(path, move |result| { + if let Err(err) = tx.send(result) { + tracing::error!("{:?}", err); + } + }); + let result = rx.recv_timeout(std::time::Duration::from_secs(1)); + send(result); + }); + } else { + self.do_save(after_action); + } + } + + pub fn format(&self) { + let doc = self.doc(); + let rev = doc.rev(); + let content = doc.content.get_untracked(); + + if let DocContent::File { path, .. } = content { + let editor = self.clone(); + let send = create_ext_action(self.scope, move |result| { + if let Ok(Ok(ProxyResponse::GetDocumentFormatting { edits })) = + result + { + let current_rev = editor.doc().rev(); + if current_rev == rev { + editor.do_text_edit(&edits); + } + } + }); + + let (tx, rx) = crossbeam_channel::bounded(1); + let proxy = self.common.proxy.clone(); + std::thread::spawn(move || { + proxy.get_document_formatting(path, move |result| { + if let Err(err) = tx.send(result) { + tracing::error!("{:?}", err); + } + }); + let result = rx.recv_timeout(std::time::Duration::from_secs(1)); + send(result); + }); + } + } + + fn search_whole_word_forward(&self, mods: Modifiers) { + let offset = self.cursor().with_untracked(|c| c.offset()); + let (word, buffer) = self.doc().buffer.with_untracked(|buffer| { + let (start, end) = buffer.select_word(offset); + (buffer.slice_to_cow(start..end).to_string(), buffer.clone()) + }); + self.common.internal_command.send(InternalCommand::Search { + pattern: Some(word), + }); + let next = self.common.find.next(buffer.text(), offset, false, true); + + if let Some((start, _end)) = next { + self.run_move_command( + &lapce_core::movement::Movement::Offset(start), + None, + mods, + ); + } + } + + fn search_forward(&self, mods: Modifiers) { + let offset = self.cursor().with_untracked(|c| c.offset()); + let text = self + .doc() + .buffer + .with_untracked(|buffer| buffer.text().clone()); + let next = self.common.find.next(&text, offset, false, true); + + if let Some((start, _end)) = next { + self.run_move_command( + &lapce_core::movement::Movement::Offset(start), + None, + mods, + ); + } + } + + fn search_backward(&self, mods: Modifiers) { + let offset = self.cursor().with_untracked(|c| c.offset()); + let text = self + .doc() + .buffer + .with_untracked(|buffer| buffer.text().clone()); + let next = self.common.find.next(&text, offset, true, true); + + if let Some((start, _end)) = next { + self.run_move_command( + &lapce_core::movement::Movement::Offset(start), + None, + mods, + ); + } + } + + fn replace_next(&self, text: &str) { + let offset = self.cursor().with_untracked(|c| c.offset()); + let buffer = self.doc().buffer.with_untracked(|buffer| buffer.clone()); + let next = self.common.find.next(buffer.text(), offset, false, true); + + if let Some((start, end)) = next { + let selection = Selection::region(start, end); + self.do_edit(&selection, &[(selection.clone(), text)]); + } + } + + fn replace_all(&self, text: &str) { + let offset = self.cursor().with_untracked(|c| c.offset()); + + self.doc().update_find(); + + let edits: Vec<(Selection, &str)> = self + .doc() + .find_result + .occurrences + .get_untracked() + .regions() + .iter() + .map(|region| (Selection::region(region.start, region.end), text)) + .collect(); + if !edits.is_empty() { + self.do_edit(&Selection::caret(offset), &edits); + } + } + + pub fn save_doc_position(&self) { + let doc = self.doc(); + let path = match if doc.loaded() { + doc.content.with_untracked(|c| c.path().cloned()) + } else { + None + } { + Some(path) => path, + None => return, + }; + + let cursor_offset = self.cursor().with_untracked(|c| c.offset()); + let scroll_offset = self.viewport().with_untracked(|v| v.origin().to_vec2()); + + let db: Arc = use_context().unwrap(); + db.save_doc_position( + &self.common.workspace, + path, + cursor_offset, + scroll_offset, + ); + } + + fn rename(&self) { + let doc = self.doc(); + let path = match if doc.loaded() { + doc.content.with_untracked(|c| c.path().cloned()) + } else { + None + } { + Some(path) => path, + None => return, + }; + + let offset = self.cursor().with_untracked(|c| c.offset()); + let (position, rev) = doc + .buffer + .with_untracked(|buffer| (buffer.offset_to_position(offset), doc.rev())); + + let cursor = self.cursor(); + let buffer = doc.buffer; + let internal_command = self.common.internal_command; + let local_path = path.clone(); + let send = create_ext_action(self.scope, move |result| { + if let Ok(ProxyResponse::PrepareRename { resp }) = result { + if buffer.with_untracked(|buffer| buffer.rev()) != rev { + return; + } + + if cursor.with_untracked(|c| c.offset()) != offset { + return; + } + + let (start, _end, position, placeholder) = + buffer.with_untracked(|buffer| match resp { + lsp_types::PrepareRenameResponse::Range(range) => ( + buffer.offset_of_position(&range.start), + buffer.offset_of_position(&range.end), + range.start, + None, + ), + lsp_types::PrepareRenameResponse::RangeWithPlaceholder { + range, + placeholder, + } => ( + buffer.offset_of_position(&range.start), + buffer.offset_of_position(&range.end), + range.start, + Some(placeholder), + ), + lsp_types::PrepareRenameResponse::DefaultBehavior { + .. + } => { + let start = buffer.prev_code_boundary(offset); + let position = buffer.offset_to_position(start); + ( + start, + buffer.next_code_boundary(offset), + position, + None, + ) + } + }); + let placeholder = placeholder.unwrap_or_else(|| { + buffer.with_untracked(|buffer| { + let (start, end) = buffer.select_word(offset); + buffer.slice_to_cow(start..end).to_string() + }) + }); + internal_command.send(InternalCommand::StartRename { + path: local_path.clone(), + placeholder, + start, + position, + }); + } + }); + self.common + .proxy + .prepare_rename(path, position, move |result| { + send(result); + }); + } + + #[instrument] + pub fn word_at_cursor(&self) -> String { + let doc = self.doc(); + let region = self.cursor().with_untracked(|c| match &c.mode { + lapce_core::cursor::CursorMode::Normal(offset) => { + lapce_core::selection::SelRegion::caret(*offset) + } + lapce_core::cursor::CursorMode::Visual { + start, + end, + mode: _, + } => lapce_core::selection::SelRegion::new( + *start.min(end), + doc.buffer.with_untracked(|buffer| { + buffer.next_grapheme_offset(*start.max(end), 1, buffer.len()) + }), + None, + ), + lapce_core::cursor::CursorMode::Insert(selection) => { + *selection.last_inserted().unwrap() + } + }); + + if region.is_caret() { + doc.buffer.with_untracked(|buffer| { + let (start, end) = buffer.select_word(region.start); + buffer.slice_to_cow(start..end).to_string() + }) + } else { + doc.buffer.with_untracked(|buffer| { + buffer.slice_to_cow(region.min()..region.max()).to_string() + }) + } + } + + #[instrument] + pub fn clear_search(&self) { + self.common.find.visual.set(false); + self.find_focus.set(false); + } + + #[instrument] + fn search(&self) { + let pattern = self.word_at_cursor(); + + let pattern = if pattern.contains('\n') || pattern.is_empty() { + None + } else { + Some(pattern) + }; + + self.common + .internal_command + .send(InternalCommand::Search { pattern }); + self.common.find.visual.set(true); + self.find_focus.set(true); + self.common.find.replace_focus.set(false); + } + + pub fn pointer_down(&self, pointer_event: &PointerInputEvent) { + self.cancel_completion(); + self.cancel_inline_completion(); + if let Some(editor_tab_id) = self.editor_tab_id.get_untracked() { + self.common + .internal_command + .send(InternalCommand::FocusEditorTab { editor_tab_id }); + } + if self + .doc() + .content + .with_untracked(|content| !content.is_local()) + { + self.common.focus.set(Focus::Workbench); + self.find_focus.set(false); + } + match pointer_event.button.mouse_button() { + MouseButton::Primary => { + self.active().set(true); + self.left_click(pointer_event); + + let y = + pointer_event.pos.y - self.editor.viewport.get_untracked().y0; + if self.sticky_header_height.get_untracked() > y { + let index = y as usize + / self.common.config.get_untracked().editor.line_height(); + if let (Some(path), Some(line)) = ( + self.doc().content.get_untracked().path(), + self.sticky_header_info + .get_untracked() + .sticky_lines + .get(index), + ) { + self.common.internal_command.send( + InternalCommand::JumpToLocation { + location: EditorLocation { + path: path.clone(), + position: Some(EditorPosition::Line(*line)), + scroll_offset: None, + ignore_unconfirmed: true, + same_editor_tab: false, + }, + }, + ); + return; + } + } + + if (cfg!(target_os = "macos") && pointer_event.modifiers.meta()) + || (cfg!(not(target_os = "macos")) + && pointer_event.modifiers.control()) + { + let rs = self.find_hint(pointer_event.pos); + match rs { + FindHintRs::NoMatchBreak + | FindHintRs::NoMatchContinue { .. } => { + self.common.lapce_command.send(LapceCommand { + kind: CommandKind::Focus( + FocusCommand::GotoDefinition, + ), + data: None, + }) + } + FindHintRs::MatchWithoutLocation => {} + FindHintRs::Match(location) => { + let Ok(path) = location.uri.to_file_path() else { + return; + }; + self.common.internal_command.send( + InternalCommand::JumpToLocation { + location: EditorLocation { + path, + position: Some(EditorPosition::Position( + location.range.start, + )), + scroll_offset: None, + ignore_unconfirmed: true, + same_editor_tab: false, + }, + }, + ); + } + } + } + } + MouseButton::Secondary => { + self.right_click(pointer_event); + } + _ => {} + } + } + + fn find_hint(&self, pos: Point) -> FindHintRs { + let rs = self.editor.line_col_of_point_with_phantom(pos); + let line = rs.0 as u32; + let index = rs.1 as u32; + self.doc().inlay_hints.with_untracked(|x| { + if let Some(hints) = x { + let mut pre_len = 0; + for hint in hints + .iter() + .filter_map(|(_, hint)| { + if hint.position.line == line { + Some(hint) + } else { + None + } + }) + .sorted_by(|pre, next| { + pre.position.character.cmp(&next.position.character) + }) + { + match find_hint(pre_len, index, hint) { + FindHintRs::NoMatchContinue { pre_hint_len } => { + pre_len = pre_hint_len; + } + rs => return rs, + } + } + FindHintRs::NoMatchBreak + } else { + FindHintRs::NoMatchBreak + } + }) + } + + #[instrument] + fn left_click(&self, pointer_event: &PointerInputEvent) { + match pointer_event.count { + 1 => { + self.single_click(pointer_event); + } + 2 => { + self.double_click(pointer_event); + } + 3 => { + self.triple_click(pointer_event); + } + _ => {} + } + } + + #[instrument] + fn single_click(&self, pointer_event: &PointerInputEvent) { + self.editor.single_click(pointer_event); + } + + #[instrument] + fn double_click(&self, pointer_event: &PointerInputEvent) { + self.editor.double_click(pointer_event); + } + + #[instrument] + fn triple_click(&self, pointer_event: &PointerInputEvent) { + self.editor.triple_click(pointer_event); + } + + #[instrument] + pub fn pointer_move(&self, pointer_event: &PointerMoveEvent) { + let mode = self.cursor().with_untracked(|c| c.get_mode()); + let (offset, is_inside) = + self.editor.offset_of_point(mode, pointer_event.pos); + if self.active().get_untracked() + && self.cursor().with_untracked(|c| c.offset()) != offset + { + self.cursor().update(|cursor| { + cursor.set_offset(offset, true, pointer_event.modifiers.alt()) + }); + } + if self.common.hover.active.get_untracked() { + let hover_editor_id = self.common.hover.editor_id.get_untracked(); + if hover_editor_id != self.id() { + self.common.hover.active.set(false); + } else { + let current_offset = self.common.hover.offset.get_untracked(); + let start_offset = self + .doc() + .buffer + .with_untracked(|buffer| buffer.prev_code_boundary(offset)); + if current_offset != start_offset { + self.common.hover.active.set(false); + } + } + } + let hover_delay = self.common.config.get_untracked().editor.hover_delay; + if hover_delay > 0 { + if is_inside { + let start_offset = self + .doc() + .buffer + .with_untracked(|buffer| buffer.prev_code_boundary(offset)); + + let editor = self.clone(); + let mouse_hover_timer = self.common.mouse_hover_timer; + let timer_token = + exec_after(Duration::from_millis(hover_delay), move |token| { + if mouse_hover_timer.try_get_untracked() == Some(token) + && editor.editor_tab_id.try_get_untracked().is_some() + { + editor.update_hover(start_offset); + } + }); + mouse_hover_timer.set(timer_token); + } else { + self.common.mouse_hover_timer.set(TimerToken::INVALID); + } + } + } + + #[instrument] + pub fn pointer_up(&self, pointer_event: &PointerInputEvent) { + self.editor.pointer_up(pointer_event); + } + + #[instrument] + pub fn pointer_leave(&self) { + self.common.mouse_hover_timer.set(TimerToken::INVALID); + } + + #[instrument] + fn right_click(&self, pointer_event: &PointerInputEvent) { + let mode = self.cursor().with_untracked(|c| c.get_mode()); + let (offset, _) = self.editor.offset_of_point(mode, pointer_event.pos); + let doc = self.doc(); + let pointer_inside_selection = doc.buffer.with_untracked(|buffer| { + self.cursor() + .with_untracked(|c| c.edit_selection(buffer).contains(offset)) + }); + if !pointer_inside_selection { + // move cursor to pointer position if outside current selection + self.single_click(pointer_event); + } + + let (path, is_file) = doc.content.with_untracked(|content| match content { + DocContent::File { path, .. } => { + (Some(path.to_path_buf()), path.is_file()) + } + DocContent::Local + | DocContent::History(_) + | DocContent::Scratch { .. } => (None, false), + }); + let mut menu = Menu::new(""); + let mut cmds = if is_file { + if path + .as_ref() + .and_then(|x| x.file_name().and_then(|x| x.to_str())) + .map(|x| x == "run.toml") + .unwrap_or_default() + { + vec![ + Some(CommandKind::Workbench( + LapceWorkbenchCommand::RevealInPanel, + )), + Some(CommandKind::Workbench( + LapceWorkbenchCommand::RevealInFileExplorer, + )), + Some(CommandKind::Workbench( + LapceWorkbenchCommand::SourceControlOpenActiveFileRemoteUrl, + )), + None, + Some(CommandKind::Edit(EditCommand::ClipboardCut)), + Some(CommandKind::Edit(EditCommand::ClipboardCopy)), + Some(CommandKind::Edit(EditCommand::ClipboardPaste)), + Some(CommandKind::Workbench( + LapceWorkbenchCommand::AddRunDebugConfig, + )), + None, + Some(CommandKind::Workbench( + LapceWorkbenchCommand::PaletteCommand, + )), + ] + } else { + vec![ + Some(CommandKind::Focus(FocusCommand::GotoDefinition)), + Some(CommandKind::Focus(FocusCommand::GotoTypeDefinition)), + Some(CommandKind::Workbench( + LapceWorkbenchCommand::ShowCallHierarchy, + )), + Some(CommandKind::Workbench( + LapceWorkbenchCommand::FindReferences, + )), + Some(CommandKind::Workbench( + LapceWorkbenchCommand::GoToImplementation, + )), + Some(CommandKind::Focus(FocusCommand::Rename)), + Some(CommandKind::Workbench( + LapceWorkbenchCommand::RunInTerminal, + )), + None, + Some(CommandKind::Workbench( + LapceWorkbenchCommand::RevealInPanel, + )), + Some(CommandKind::Workbench( + LapceWorkbenchCommand::RevealInFileExplorer, + )), + Some(CommandKind::Workbench( + LapceWorkbenchCommand::SourceControlOpenActiveFileRemoteUrl, + )), + None, + Some(CommandKind::Edit(EditCommand::ClipboardCut)), + Some(CommandKind::Edit(EditCommand::ClipboardCopy)), + Some(CommandKind::Edit(EditCommand::ClipboardPaste)), + None, + Some(CommandKind::Workbench( + LapceWorkbenchCommand::PaletteCommand, + )), + ] + } + } else { + vec![ + Some(CommandKind::Edit(EditCommand::ClipboardCut)), + Some(CommandKind::Edit(EditCommand::ClipboardCopy)), + Some(CommandKind::Edit(EditCommand::ClipboardPaste)), + None, + Some(CommandKind::Workbench( + LapceWorkbenchCommand::PaletteCommand, + )), + ] + }; + if self.diff_editor_id.get_untracked().is_some() && is_file { + cmds.push(Some(CommandKind::Workbench( + LapceWorkbenchCommand::GoToLocation, + ))); + } + let lapce_command = self.common.lapce_command; + for cmd in cmds { + if let Some(cmd) = cmd { + menu = menu.entry( + MenuItem::new(cmd.desc().unwrap_or_else(|| cmd.str())).action( + move || { + lapce_command.send(LapceCommand { + kind: cmd.clone(), + data: None, + }) + }, + ), + ); + } else { + menu = menu.separator(); + } + } + show_context_menu(menu, None); + } + + #[instrument] + fn update_hover(&self, offset: usize) { + let doc = self.doc(); + let path = doc + .content + .with_untracked(|content| content.path().cloned()); + let position = doc + .buffer + .with_untracked(|buffer| buffer.offset_to_position(offset)); + let path = match path { + Some(path) => path, + None => return, + }; + let config = self.common.config; + let hover_data = self.common.hover.clone(); + let editor_id = self.id(); + let send = create_ext_action(self.scope, move |resp| { + if let Ok(ProxyResponse::HoverResponse { hover, .. }) = resp { + let content = parse_hover_resp(hover, &config.get_untracked()); + hover_data.content.set(content); + hover_data.offset.set(offset); + hover_data.editor_id.set(editor_id); + hover_data.active.set(true); + } + }); + self.common.proxy.get_hover(0, path, position, |resp| { + send(resp); + }); + } + + // reset the doc inside and move cursor back + pub fn reset(&self) { + let doc = self.doc(); + doc.reload(Rope::from(""), true); + self.cursor() + .update(|cursor| cursor.set_offset(0, false, false)); + } + + pub fn visual_line(&self, line: usize) -> usize { + self.kind.with_untracked(|kind| match kind { + EditorViewKind::Normal => line, + EditorViewKind::Diff(diff) => { + let is_right = diff.is_right; + let mut last_change: Option<&DiffLines> = None; + let mut visual_line = 0; + let mut changes = diff.changes.iter().peekable(); + while let Some(change) = changes.next() { + match (is_right, change) { + (true, DiffLines::Left(range)) => { + if let Some(DiffLines::Right(_)) = changes.peek() { + } else { + visual_line += range.len(); + } + } + (false, DiffLines::Right(range)) => { + let len = if let Some(DiffLines::Left(r)) = last_change { + range.len() - r.len().min(range.len()) + } else { + range.len() + }; + if len > 0 { + visual_line += len; + } + } + (true, DiffLines::Right(range)) + | (false, DiffLines::Left(range)) => { + if line < range.end { + return visual_line + line - range.start; + } + visual_line += range.len(); + if is_right { + if let Some(DiffLines::Left(r)) = last_change { + let len = r.len() - r.len().min(range.len()); + if len > 0 { + visual_line += len; + } + } + } + } + (_, DiffLines::Both(info)) => { + let end = if is_right { + info.right.end + } else { + info.left.end + }; + if line >= end { + visual_line += info.right.len() + - info + .skip + .as_ref() + .map(|skip| skip.len().saturating_sub(1)) + .unwrap_or(0); + last_change = Some(change); + continue; + } + + let start = if is_right { + info.right.start + } else { + info.left.start + }; + if let Some(skip) = info.skip.as_ref() { + if start + skip.start > line { + return visual_line + line - start; + } else if start + skip.end > line { + return visual_line + skip.start; + } else { + return visual_line + + (line - start - skip.len() + 1); + } + } else { + return visual_line + line - start; + } + } + } + last_change = Some(change); + } + visual_line + } + }) + } + + pub fn actual_line(&self, visual_line: usize, bottom_affinity: bool) -> usize { + self.kind.with_untracked(|kind| match kind { + EditorViewKind::Normal => visual_line, + EditorViewKind::Diff(diff) => { + let is_right = diff.is_right; + let mut actual_line: usize = 0; + let mut current_visual_line = 0; + let mut last_change: Option<&DiffLines> = None; + let mut changes = diff.changes.iter().peekable(); + while let Some(change) = changes.next() { + match (is_right, change) { + (true, DiffLines::Left(range)) => { + if let Some(DiffLines::Right(_)) = changes.peek() { + } else { + current_visual_line += range.len(); + if current_visual_line >= visual_line { + return if bottom_affinity { + actual_line + } else { + actual_line.saturating_sub(1) + }; + } + } + } + (false, DiffLines::Right(range)) => { + let len = if let Some(DiffLines::Left(r)) = last_change { + range.len() - r.len().min(range.len()) + } else { + range.len() + }; + if len > 0 { + current_visual_line += len; + if current_visual_line >= visual_line { + return actual_line; + } + } + } + (true, DiffLines::Right(range)) + | (false, DiffLines::Left(range)) => { + let len = range.len(); + if current_visual_line + len > visual_line { + return range.start + + (visual_line - current_visual_line); + } + current_visual_line += len; + actual_line += len; + if is_right { + if let Some(DiffLines::Left(r)) = last_change { + let len = r.len() - r.len().min(range.len()); + if len > 0 { + current_visual_line += len; + if current_visual_line > visual_line { + return if bottom_affinity { + actual_line + } else { + actual_line - range.len() + }; + } + } + } + } + } + (_, DiffLines::Both(info)) => { + let len = info.right.len(); + let start = if is_right { + info.right.start + } else { + info.left.start + }; + + if let Some(skip) = info.skip.as_ref() { + if current_visual_line + skip.start == visual_line { + return if bottom_affinity { + actual_line + skip.end + } else { + (actual_line + skip.start).saturating_sub(1) + }; + } else if current_visual_line + skip.start + 1 + > visual_line + { + return actual_line + visual_line + - current_visual_line; + } else if current_visual_line + len - skip.len() + 1 + >= visual_line + { + return actual_line + + skip.end + + (visual_line + - current_visual_line + - skip.start + - 1); + } + actual_line += len; + current_visual_line += len - skip.len() + 1; + } else { + if current_visual_line + len > visual_line { + return start + + (visual_line - current_visual_line); + } + current_visual_line += len; + actual_line += len; + } + } + } + last_change = Some(change); + } + actual_line + } + }) + } +} + +impl KeyPressFocus for EditorData { + fn get_mode(&self) -> Mode { + if self.common.find.visual.get_untracked() && self.find_focus.get_untracked() + { + Mode::Insert + } else { + self.cursor().with_untracked(|c| c.get_mode()) + } + } + + #[instrument] + fn check_condition(&self, condition: Condition) -> bool { + match condition { + Condition::InputFocus => { + self.common.find.visual.get_untracked() + && self.find_focus.get_untracked() + } + Condition::ListFocus => self.has_completions(), + Condition::CompletionFocus => self.has_completions(), + Condition::InlineCompletionVisible => self.has_inline_completions(), + Condition::OnScreenFindActive => { + self.on_screen_find.with_untracked(|f| f.active) + } + Condition::InSnippet => self.snippet.with_untracked(|s| s.is_some()), + Condition::EditorFocus => self + .doc() + .content + .with_untracked(|content| !content.is_local()), + Condition::SearchFocus => { + self.common.find.visual.get_untracked() + && self.find_focus.get_untracked() + && !self.common.find.replace_focus.get_untracked() + } + Condition::ReplaceFocus => { + self.common.find.visual.get_untracked() + && self.find_focus.get_untracked() + && self.common.find.replace_focus.get_untracked() + } + Condition::SearchActive => { + if self.common.config.get_untracked().core.modal + && self.cursor().with_untracked(|c| !c.is_normal()) + { + false + } else { + self.common.find.visual.get_untracked() + } + } + _ => false, + } + } + + #[instrument] + fn run_command( + &self, + command: &crate::command::LapceCommand, + count: Option, + mods: Modifiers, + ) -> CommandExecuted { + if self.common.find.visual.get_untracked() && self.find_focus.get_untracked() + { + match &command.kind { + CommandKind::Edit(_) + | CommandKind::Move(_) + | CommandKind::MultiSelection(_) => { + if self.common.find.replace_focus.get_untracked() { + self.common.internal_command.send( + InternalCommand::ReplaceEditorCommand { + command: command.clone(), + count, + mods, + }, + ); + } else { + self.common.internal_command.send( + InternalCommand::FindEditorCommand { + command: command.clone(), + count, + mods, + }, + ); + } + return CommandExecuted::Yes; + } + _ => {} + } + } + + match &command.kind { + crate::command::CommandKind::Workbench(_) => CommandExecuted::No, + crate::command::CommandKind::Edit(cmd) => self.run_edit_command(cmd), + crate::command::CommandKind::Move(cmd) => { + let movement = cmd.to_movement(count); + self.run_move_command(&movement, count, mods) + } + crate::command::CommandKind::Scroll(cmd) => { + if self + .doc() + .content + .with_untracked(|content| content.is_local()) + { + return CommandExecuted::No; + } + self.run_scroll_command(cmd, count, mods) + } + crate::command::CommandKind::Focus(cmd) => { + if self + .doc() + .content + .with_untracked(|content| content.is_local()) + { + return CommandExecuted::No; + } + self.run_focus_command(cmd, count, mods) + } + crate::command::CommandKind::MotionMode(cmd) => { + self.run_motion_mode_command(cmd, count) + } + crate::command::CommandKind::MultiSelection(cmd) => { + self.run_multi_selection_command(cmd) + } + } + } + + fn expect_char(&self) -> bool { + if self.common.find.visual.get_untracked() && self.find_focus.get_untracked() + { + false + } else { + self.inline_find.with_untracked(|f| f.is_some()) + || self.on_screen_find.with_untracked(|f| f.active) + } + } + + fn receive_char(&self, c: &str) { + if self.common.find.visual.get_untracked() && self.find_focus.get_untracked() + { + // find/relace editor receive char + if self.common.find.replace_focus.get_untracked() { + self.common.internal_command.send( + InternalCommand::ReplaceEditorReceiveChar { s: c.to_string() }, + ); + } else { + self.common.internal_command.send( + InternalCommand::FindEditorReceiveChar { s: c.to_string() }, + ); + } + } else { + // normal editor receive char + if self.get_mode() == Mode::Insert { + let mut cursor = self.cursor().get_untracked(); + let deltas = self.doc().do_insert( + &mut cursor, + c, + &self.common.config.get_untracked(), + ); + self.cursor().set(cursor); + + if !c + .chars() + .all(|c| c.is_whitespace() || c.is_ascii_whitespace()) + { + self.update_completion(false); + } else { + self.cancel_completion(); + } + + self.update_inline_completion( + InlineCompletionTriggerKind::Automatic, + ); + + self.apply_deltas(&deltas); + } else if let Some(direction) = self.inline_find.get_untracked() { + self.inline_find(direction.clone(), c); + self.last_inline_find.set(Some((direction, c.to_string()))); + self.inline_find.set(None); + } else if self.on_screen_find.with_untracked(|f| f.active) { + self.on_screen_find.update(|find| { + let pattern = format!("{}{c}", find.pattern); + find.regions = self.on_screen_find(&pattern); + find.pattern = pattern; + }); + } + } + } +} + +/// Custom signal wrapper for [`Doc`], because [`Editor`] only knows it as a +/// `Rc`, and there is currently no way to have an `RwSignal>` and +/// an `RwSignal>`. +/// +// FIXME: This could possibly be swapped with a generic impl? +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct DocSignal { + // TODO: replace with ReadSignal once that impls `track` + inner: RwSignal>, +} +impl DocSignal { + pub fn get(&self) -> Rc { + let doc = self.inner.get(); + (doc as Rc) + .downcast() + .expect("doc is not Rc") + } + + pub fn get_untracked(&self) -> Rc { + let doc = self.inner.get_untracked(); + (doc as Rc) + .downcast() + .expect("doc is not Rc") + } + + pub fn with(&self, f: impl FnOnce(&Rc) -> O) -> O { + self.inner.with(|doc| { + let doc = doc.clone(); + let doc: Rc = (doc as Rc) + .downcast() + .expect("doc is not Rc"); + f(&doc) + }) + } + + pub fn with_untracked(&self, f: impl FnOnce(&Rc) -> O) -> O { + self.inner.with_untracked(|doc| { + let doc = doc.clone(); + let doc: Rc = (doc as Rc) + .downcast() + .expect("doc is not Rc"); + f(&doc) + }) + } + + pub fn track(&self) { + self.inner.track(); + } +} + +/// Checks if completion should be triggered if the received command +/// is one that inserts whitespace or deletes whitespace +fn show_completion( + cmd: &EditCommand, + doc: &Rope, + deltas: &[(Rope, RopeDelta, InvalLines)], +) -> bool { + match cmd { + EditCommand::DeleteBackward + | EditCommand::DeleteForward + | EditCommand::DeleteWordBackward + | EditCommand::DeleteWordForward + | EditCommand::DeleteForwardAndInsert => { + let start = match deltas.first().and_then(|delta| delta.1.els.first()) { + Some(lapce_xi_rope::DeltaElement::Copy(_, start)) => *start, + _ => 0, + }; + + let end = match deltas.first().and_then(|delta| delta.1.els.get(1)) { + Some(lapce_xi_rope::DeltaElement::Copy(end, _)) => *end, + _ => 0, + }; + + if start > 0 && end > start { + !doc.slice_to_cow(start..end) + .chars() + .all(|c| c.is_whitespace() || c.is_ascii_whitespace()) + } else { + true + } + } + _ => false, + } +} + +fn show_inline_completion(cmd: &EditCommand) -> bool { + matches!( + cmd, + EditCommand::DeleteBackward + | EditCommand::DeleteForward + | EditCommand::DeleteWordBackward + | EditCommand::DeleteWordForward + | EditCommand::DeleteForwardAndInsert + | EditCommand::IndentLine + | EditCommand::InsertMode + ) +} + +// TODO(minor): Should we just put this on view, since it only requires those values? +pub(crate) fn compute_screen_lines( + config: ReadSignal>, + base: RwSignal, + view_kind: ReadSignal, + doc: &Doc, + lines: &Lines, + text_prov: impl TextLayoutProvider + Clone, + config_id: ConfigId, +) -> ScreenLines { + // TODO: this should probably be a get since we need to depend on line-height + let config = config.get(); + let line_height = config.editor.line_height(); + + let (y0, y1) = base + .with_untracked(|base| (base.active_viewport.y0, base.active_viewport.y1)); + // Get the start and end (visual) lines that are visible in the viewport + let min_vline = VLine((y0 / line_height as f64).floor() as usize); + let max_vline = VLine((y1 / line_height as f64).ceil() as usize); + + let cache_rev = doc.cache_rev.get(); + lines.check_cache_rev(cache_rev); + // TODO(minor): we don't really need to depend on various subdetails that aren't affecting how + // the screen lines are set up, like the title of a scratch document. + doc.content.track(); + doc.loaded.track(); + + let min_info = once_cell::sync::Lazy::new(|| { + lines + .iter_vlines(text_prov.clone(), false, min_vline) + .next() + }); + + match view_kind.get() { + EditorViewKind::Normal => { + let mut rvlines = Vec::new(); + let mut info = HashMap::new(); + + let Some(min_info) = *min_info else { + return ScreenLines { + lines: Rc::new(rvlines), + info: Rc::new(info), + diff_sections: None, + base, + }; + }; + + // TODO: the original was min_line..max_line + 1, are we iterating too little now? + // the iterator is from min_vline..max_vline + let count = max_vline.get() - min_vline.get(); + let iter = lines.iter_rvlines_init( + text_prov, + cache_rev, + config_id, + min_info.rvline, + false, + ); + + // let range = doc.folding_ranges.get().get_folded_range(); + // let mut init_index = 0; + + for (i, vline_info) in iter.enumerate() { + if rvlines.len() >= count { + break; + } + + // let (folded, next_index) = + // range.contain_line(init_index, vline_info.rvline.line as u32); + // init_index = next_index; + // if folded { + // continue; + // } + rvlines.push(vline_info.rvline); + + let y_idx = min_vline.get() + i; + let vline_y = y_idx * line_height; + let line_y = vline_y - vline_info.rvline.line_index * line_height; + + // Add the information to make it cheap to get in the future. + // This y positions are shifted by the baseline y0 + info.insert( + vline_info.rvline, + LineInfo { + y: line_y as f64 - y0, + vline_y: vline_y as f64 - y0, + vline_info, + }, + ); + } + + ScreenLines { + lines: Rc::new(rvlines), + info: Rc::new(info), + diff_sections: None, + base, + } + } + EditorViewKind::Diff(diff_info) => { + // TODO: let lines in diff view be wrapped, possibly screen_lines should be impl'd + // on DiffEditorData + + let mut y_idx = 0; + let mut rvlines = Vec::new(); + let mut info = HashMap::new(); + let mut diff_sections = Vec::new(); + let mut last_change: Option<&DiffLines> = None; + let mut changes = diff_info.changes.iter().peekable(); + let is_right = diff_info.is_right; + + let line_y = |info: VLineInfo<()>, vline_y: usize| -> usize { + vline_y.saturating_sub(info.rvline.line_index * line_height) + }; + + while let Some(change) = changes.next() { + match (is_right, change) { + (true, DiffLines::Left(range)) => { + if let Some(DiffLines::Right(_)) = changes.peek() { + } else { + let len = range.len(); + diff_sections.push(DiffSection { + y_idx, + height: len, + kind: DiffSectionKind::NoCode, + }); + y_idx += len; + } + } + (false, DiffLines::Right(range)) => { + let len = if let Some(DiffLines::Left(r)) = last_change { + range.len() - r.len().min(range.len()) + } else { + range.len() + }; + if len > 0 { + diff_sections.push(DiffSection { + y_idx, + height: len, + kind: DiffSectionKind::NoCode, + }); + y_idx += len; + } + } + (true, DiffLines::Right(range)) + | (false, DiffLines::Left(range)) => { + // TODO: count vline count in the range instead + let height = range.len(); + + diff_sections.push(DiffSection { + y_idx, + height, + kind: if is_right { + DiffSectionKind::Added + } else { + DiffSectionKind::Removed + }, + }); + + let initial_y_idx = y_idx; + // Mopve forward by the count given. + y_idx += height; + + if y_idx < min_vline.get() { + if is_right { + if let Some(DiffLines::Left(r)) = last_change { + // TODO: count vline count in the other editor since this is skipping an amount dependent on those vlines + let len = r.len() - r.len().min(range.len()); + if len > 0 { + diff_sections.push(DiffSection { + y_idx, + height: len, + kind: DiffSectionKind::NoCode, + }); + y_idx += len; + } + }; + } + last_change = Some(change); + continue; + } + + let start_rvline = + lines.rvline_of_line(&text_prov, range.start); + + // TODO: this wouldn't need to produce vlines if screen lines didn't + // require them. + let iter = lines + .iter_rvlines_init( + &text_prov, + cache_rev, + config_id, + start_rvline, + false, + ) + .take_while(|vline_info| { + vline_info.rvline.line < range.end + }) + .enumerate(); + for (i, rvline_info) in iter { + let rvline = rvline_info.rvline; + if initial_y_idx + i < min_vline.0 { + continue; + } + + rvlines.push(rvline); + let vline_y = (initial_y_idx + i) * line_height; + info.insert( + rvline, + LineInfo { + y: line_y(rvline_info, vline_y) as f64 - y0, + vline_y: vline_y as f64 - y0, + vline_info: rvline_info, + }, + ); + + if initial_y_idx + i > max_vline.0 { + break; + } + } + + if is_right { + if let Some(DiffLines::Left(r)) = last_change { + // TODO: count vline count in the other editor since this is skipping an amount dependent on those vlines + let len = r.len() - r.len().min(range.len()); + if len > 0 { + diff_sections.push(DiffSection { + y_idx, + height: len, + kind: DiffSectionKind::NoCode, + }); + y_idx += len; + } + }; + } + } + (_, DiffLines::Both(bothinfo)) => { + let start = if is_right { + bothinfo.right.start + } else { + bothinfo.left.start + }; + let len = bothinfo.right.len(); + let diff_height = len + - bothinfo + .skip + .as_ref() + .map(|skip| skip.len().saturating_sub(1)) + .unwrap_or(0); + if y_idx + diff_height < min_vline.get() { + y_idx += diff_height; + last_change = Some(change); + continue; + } + + let start_rvline = lines.rvline_of_line(&text_prov, start); + + let mut iter = lines + .iter_rvlines_init( + &text_prov, + cache_rev, + config_id, + start_rvline, + false, + ) + .peekable(); + while let Some(rvline_info) = iter.next() { + let line = rvline_info.rvline.line; + + if line >= start + len { + break; + } + + // Skip over the lines + if let Some(skip) = bothinfo.skip.as_ref() { + if Some(skip.start) == line.checked_sub(start) { + y_idx += 1; + + // restart iterator after the skip + let start_rvline = lines.rvline_of_line( + &text_prov, + start + skip.end, + ); + + iter = lines + .iter_rvlines_init( + &text_prov, + cache_rev, + config_id, + start_rvline, + false, + ) + .peekable(); + + continue; + } + } + + // Add the vline if it is within view + if y_idx >= min_vline.get() { + rvlines.push(rvline_info.rvline); + let vline_y = y_idx * line_height; + info.insert( + rvline_info.rvline, + LineInfo { + y: line_y(rvline_info, vline_y) as f64 - y0, + vline_y: vline_y as f64 - y0, + vline_info: rvline_info, + }, + ); + } + + y_idx += 1; + + if y_idx - 1 > max_vline.get() { + break; + } + } + } + } + last_change = Some(change); + } + ScreenLines { + lines: Rc::new(rvlines), + info: Rc::new(info), + diff_sections: Some(Rc::new(diff_sections)), + base, + } + } + } +} + +fn parse_hover_resp( + hover: lsp_types::Hover, + config: &LapceConfig, +) -> Vec { + match hover.contents { + HoverContents::Scalar(text) => match text { + MarkedString::String(text) => parse_markdown(&text, 1.8, config), + MarkedString::LanguageString(code) => parse_markdown( + &format!("```{}\n{}\n```", code.language, code.value), + 1.8, + config, + ), + }, + HoverContents::Array(array) => array + .into_iter() + .map(|t| from_marked_string(t, config)) + .rev() + .reduce(|mut contents, more| { + contents.push(MarkdownContent::Separator); + contents.extend(more); + contents + }) + .unwrap_or_default(), + HoverContents::Markup(content) => match content.kind { + MarkupKind::PlainText => from_plaintext(&content.value, 1.8, config), + MarkupKind::Markdown => parse_markdown(&content.value, 1.8, config), + }, + } +} + +#[derive(Debug)] +enum FindHintRs { + NoMatchBreak, + NoMatchContinue { pre_hint_len: u32 }, + MatchWithoutLocation, + Match(Location), +} + +fn find_hint(mut pre_hint_len: u32, index: u32, hint: &InlayHint) -> FindHintRs { + use FindHintRs::*; + match &hint.label { + InlayHintLabel::String(text) => { + let actual_col = pre_hint_len + hint.position.character; + let actual_col_end = actual_col + (text.len() as u32); + if actual_col > index { + NoMatchBreak + } else if actual_col <= index && index < actual_col_end { + MatchWithoutLocation + } else { + pre_hint_len += text.len() as u32; + NoMatchContinue { pre_hint_len } + } + } + InlayHintLabel::LabelParts(parts) => { + for part in parts { + let actual_col = pre_hint_len + hint.position.character; + let actual_col_end = actual_col + part.value.len() as u32; + if index < actual_col { + return NoMatchBreak; + } else if actual_col <= index && index < actual_col_end { + if let Some(location) = &part.location { + return Match(location.clone()); + } else { + return MatchWithoutLocation; + } + } else { + pre_hint_len += part.value.len() as u32; + } + } + NoMatchContinue { pre_hint_len } + } + } +} diff --git a/lapce-app/src/editor/diff.rs b/lapce-app/src/editor/diff.rs new file mode 100644 index 0000000..5cb82c8 --- /dev/null +++ b/lapce-app/src/editor/diff.rs @@ -0,0 +1,552 @@ +use std::{rc::Rc, sync::atomic}; + +use floem::{ + View, + event::{Event, EventListener}, + ext_event::create_ext_action, + reactive::{RwSignal, Scope, SignalGet, SignalUpdate, SignalWith}, + style::CursorStyle, + views::{ + Decorators, clip, dyn_stack, editor::id::EditorId, empty, label, stack, svg, + }, +}; +use lapce_core::buffer::{ + diff::{DiffExpand, DiffLines, expand_diff_lines, rope_diff}, + rope_text::RopeText, +}; +use lapce_rpc::{buffer::BufferId, proxy::ProxyResponse}; +use lapce_xi_rope::Rope; +use serde::{Deserialize, Serialize}; + +use super::{EditorData, EditorViewKind}; +use crate::{ + config::{color::LapceColor, icon::LapceIcons}, + doc::{Doc, DocContent}, + id::{DiffEditorId, EditorTabId}, + main_split::{Editors, MainSplitData}, + wave::wave_box, + window_tab::CommonData, +}; + +#[derive(Clone)] +pub struct DiffInfo { + pub is_right: bool, + pub changes: Vec, +} + +#[derive(Clone, Serialize, Deserialize)] +pub struct DiffEditorInfo { + pub left_content: DocContent, + pub right_content: DocContent, +} + +impl DiffEditorInfo { + pub fn to_data( + &self, + data: MainSplitData, + editor_tab_id: EditorTabId, + ) -> DiffEditorData { + let cx = data.scope.create_child(); + + let diff_editor_id = DiffEditorId::next(); + + let new_doc = { + let data = data.clone(); + let common = data.common.clone(); + move |content: &DocContent| match content { + DocContent::File { path, .. } => { + let (doc, _) = data.get_doc(path.clone(), None); + doc + } + DocContent::Local => { + Rc::new(Doc::new_local(cx, data.editors, common.clone())) + } + DocContent::History(history) => { + let doc = Doc::new_history( + cx, + content.clone(), + data.editors, + common.clone(), + ); + let doc = Rc::new(doc); + + { + let doc = doc.clone(); + let send = create_ext_action(cx, move |result| { + if let Ok(ProxyResponse::BufferHeadResponse { + content, + .. + }) = result + { + doc.init_content(Rope::from(content)); + } + }); + common.proxy.get_buffer_head( + history.path.clone(), + move |result| { + send(result); + }, + ); + } + + doc + } + DocContent::Scratch { name, .. } => { + let doc_content = DocContent::Scratch { + id: BufferId::next(), + name: name.to_string(), + }; + let doc = Doc::new_content( + cx, + doc_content, + data.editors, + common.clone(), + ); + let doc = Rc::new(doc); + data.scratch_docs.update(|scratch_docs| { + scratch_docs.insert(name.to_string(), doc.clone()); + }); + doc + } + } + }; + + let left_doc = new_doc(&self.left_content); + let right_doc = new_doc(&self.right_content); + + let diff_editor_data = DiffEditorData::new( + cx, + diff_editor_id, + editor_tab_id, + left_doc, + right_doc, + data.editors, + data.common.clone(), + ); + + data.diff_editors.update(|diff_editors| { + diff_editors.insert(diff_editor_id, diff_editor_data.clone()); + }); + + diff_editor_data + } +} + +#[derive(Clone)] +pub struct DiffEditorData { + pub id: DiffEditorId, + pub editor_tab_id: RwSignal, + pub scope: Scope, + pub left: EditorData, + pub right: EditorData, + pub confirmed: RwSignal, + pub focus_right: RwSignal, +} + +impl DiffEditorData { + pub fn new( + cx: Scope, + id: DiffEditorId, + editor_tab_id: EditorTabId, + left_doc: Rc, + right_doc: Rc, + editors: Editors, + common: Rc, + ) -> Self { + let cx = cx.create_child(); + let confirmed = cx.create_rw_signal(false); + + // TODO: ensure that left/right are cleaned up + let [left, right] = [left_doc, right_doc].map(|doc| { + editors.make_from_doc( + cx, + doc, + None, + Some((editor_tab_id, id)), + Some(confirmed), + common.clone(), + ) + }); + + let data = Self { + id, + editor_tab_id: cx.create_rw_signal(editor_tab_id), + scope: cx, + left, + right, + confirmed, + focus_right: cx.create_rw_signal(true), + }; + + data.listen_diff_changes(); + + data + } + + pub fn diff_editor_info(&self) -> DiffEditorInfo { + DiffEditorInfo { + left_content: self.left.doc().content.get_untracked(), + right_content: self.right.doc().content.get_untracked(), + } + } + + pub fn copy( + &self, + cx: Scope, + editor_tab_id: EditorTabId, + diff_editor_id: EditorId, + editors: Editors, + ) -> Self { + let cx = cx.create_child(); + let confirmed = cx.create_rw_signal(true); + + let [left, right] = [&self.left, &self.right].map(|editor_data| { + editors + .make_copy( + editor_data.id(), + cx, + None, + Some((editor_tab_id, diff_editor_id)), + Some(confirmed), + ) + .unwrap() + }); + + let diff_editor = DiffEditorData { + scope: cx, + id: diff_editor_id, + editor_tab_id: cx.create_rw_signal(editor_tab_id), + focus_right: cx.create_rw_signal(true), + left, + right, + confirmed, + }; + + diff_editor.listen_diff_changes(); + diff_editor + } + + fn listen_diff_changes(&self) { + let cx = self.scope; + + let left = self.left.clone(); + let left_doc_rev = { + let left = left.clone(); + cx.create_memo(move |_| { + let doc = left.doc_signal().get(); + (doc.content.get(), doc.buffer.with(|b| b.rev())) + }) + }; + + let right = self.right.clone(); + let right_doc_rev = { + let right = right.clone(); + cx.create_memo(move |_| { + let doc = right.doc_signal().get(); + (doc.content.get(), doc.buffer.with(|b| b.rev())) + }) + }; + + cx.create_effect(move |_| { + let (_, left_rev) = left_doc_rev.get(); + let (left_editor_view, left_doc) = (left.kind, left.doc()); + let (left_atomic_rev, left_rope) = + left_doc.buffer.with_untracked(|buffer| { + (buffer.atomic_rev(), buffer.text().clone()) + }); + + let (_, right_rev) = right_doc_rev.get(); + let (right_editor_view, right_doc) = (right.kind, right.doc()); + let (right_atomic_rev, right_rope) = + right_doc.buffer.with_untracked(|buffer| { + (buffer.atomic_rev(), buffer.text().clone()) + }); + + let send = { + let right_atomic_rev = right_atomic_rev.clone(); + create_ext_action(cx, move |changes: Option>| { + let changes = if let Some(changes) = changes { + changes + } else { + return; + }; + + if left_atomic_rev.load(atomic::Ordering::Acquire) != left_rev { + return; + } + + if right_atomic_rev.load(atomic::Ordering::Acquire) != right_rev + { + return; + } + + left_editor_view.set(EditorViewKind::Diff(DiffInfo { + is_right: false, + changes: changes.clone(), + })); + right_editor_view.set(EditorViewKind::Diff(DiffInfo { + is_right: true, + changes, + })); + }) + }; + + rayon::spawn(move || { + let changes = rope_diff( + left_rope, + right_rope, + right_rev, + right_atomic_rev.clone(), + Some(3), + ); + send(changes); + }); + }); + } +} + +#[derive(Clone, PartialEq)] +struct DiffShowMoreSection { + left_actual_line: usize, + right_actual_line: usize, + skip_start: usize, + lines: usize, +} + +pub fn diff_show_more_section_view( + left_editor: &EditorData, + right_editor: &EditorData, +) -> impl View + use<> { + let left_editor_view = left_editor.kind; + let right_editor_view = right_editor.kind; + let right_screen_lines = right_editor.screen_lines(); + let right_scroll_delta = right_editor.editor.scroll_delta; + let viewport = right_editor.viewport(); + let config = right_editor.common.config; + + let each_fn = move || { + let editor_view = right_editor_view.get(); + + if let EditorViewKind::Diff(diff_info) = editor_view { + diff_info + .changes + .iter() + .filter_map(|change| { + let DiffLines::Both(info) = change else { + return None; + }; + + let skip = info.skip.as_ref()?; + + Some(DiffShowMoreSection { + left_actual_line: info.left.start, + right_actual_line: info.right.start, + skip_start: skip.start, + lines: skip.len(), + }) + }) + .collect() + } else { + Vec::new() + } + }; + + let key_fn = move |section: &DiffShowMoreSection| { + ( + section.right_actual_line + section.skip_start, + section.lines, + ) + }; + + let view_fn = move |section: DiffShowMoreSection| { + stack(( + wave_box().style(move |s| { + s.absolute() + .size_pct(100.0, 100.0) + .color(config.get().color(LapceColor::PANEL_BACKGROUND)) + }), + label(move || format!("{} Hidden Lines", section.lines)), + label(|| "|".to_string()).style(|s| s.margin_left(10.0)), + stack(( + svg(move || config.get().ui_svg(LapceIcons::FOLD)).style(move |s| { + let config = config.get(); + let size = config.ui.icon_size() as f32; + s.size(size, size) + .color(config.color(LapceColor::EDITOR_FOREGROUND)) + }), + label(|| "Expand All".to_string()).style(|s| s.margin_left(6.0)), + )) + .on_event_stop(EventListener::PointerDown, move |_| {}) + .on_click_stop(move |_event| { + left_editor_view.update(|editor_view| { + if let EditorViewKind::Diff(diff_info) = editor_view { + expand_diff_lines( + &mut diff_info.changes, + section.left_actual_line, + DiffExpand::All, + false, + ); + } + }); + right_editor_view.update(|editor_view| { + if let EditorViewKind::Diff(diff_info) = editor_view { + expand_diff_lines( + &mut diff_info.changes, + section.right_actual_line, + DiffExpand::All, + true, + ); + } + }); + }) + .style(|s| { + s.margin_left(10.0) + .height_pct(100.0) + .items_center() + .hover(|s| s.cursor(CursorStyle::Pointer)) + }), + label(|| "|".to_string()).style(|s| s.margin_left(10.0)), + stack(( + svg(move || config.get().ui_svg(LapceIcons::FOLD_UP)).style( + move |s| { + let config = config.get(); + let size = config.ui.icon_size() as f32; + s.size(size, size) + .color(config.color(LapceColor::EDITOR_FOREGROUND)) + }, + ), + label(|| "Expand Up".to_string()).style(|s| s.margin_left(6.0)), + )) + .on_event_stop(EventListener::PointerDown, move |_| {}) + .on_click_stop(move |_event| { + left_editor_view.update(|editor_view| { + if let EditorViewKind::Diff(diff_info) = editor_view { + expand_diff_lines( + &mut diff_info.changes, + section.left_actual_line, + DiffExpand::Up(10), + false, + ); + } + }); + right_editor_view.update(|editor_view| { + if let EditorViewKind::Diff(diff_info) = editor_view { + expand_diff_lines( + &mut diff_info.changes, + section.right_actual_line, + DiffExpand::Up(10), + true, + ); + } + }); + }) + .style(move |s| { + s.margin_left(10.0) + .height_pct(100.0) + .items_center() + .hover(|s| s.cursor(CursorStyle::Pointer)) + }), + label(|| "|".to_string()).style(|s| s.margin_left(10.0)), + stack(( + svg(move || config.get().ui_svg(LapceIcons::FOLD_DOWN)).style( + move |s| { + let config = config.get(); + let size = config.ui.icon_size() as f32; + s.size(size, size) + .color(config.color(LapceColor::EDITOR_FOREGROUND)) + }, + ), + label(|| "Expand Down".to_string()).style(|s| s.margin_left(6.0)), + )) + .on_event_stop(EventListener::PointerDown, move |_| {}) + .on_click_stop(move |_event| { + left_editor_view.update(|editor_view| { + if let EditorViewKind::Diff(diff_info) = editor_view { + expand_diff_lines( + &mut diff_info.changes, + section.left_actual_line, + DiffExpand::Down(10), + false, + ); + } + }); + right_editor_view.update(|editor_view| { + if let EditorViewKind::Diff(diff_info) = editor_view { + expand_diff_lines( + &mut diff_info.changes, + section.right_actual_line, + DiffExpand::Down(10), + true, + ); + } + }); + }) + .style(move |s| { + s.margin_left(10.0) + .height_pct(100.0) + .items_center() + .hover(|s| s.cursor(CursorStyle::Pointer)) + }), + )) + .on_event_cont(EventListener::PointerWheel, move |event| { + if let Event::PointerWheel(event) = event { + right_scroll_delta.set(event.delta); + } + }) + .style(move |s| { + let right_screen_lines = right_screen_lines.get(); + + let mut right_line = section.right_actual_line + section.skip_start; + let is_before_skip = if right_line > 0 { + right_line -= 1; + true + } else { + right_line += section.lines; + false + }; + + let Some(line_info) = right_screen_lines.info_for_line(right_line) + else { + return s.hide(); + }; + + let config = config.get(); + let line_height = config.editor.line_height(); + + let mut y = line_info.y - viewport.get().y0; + + if is_before_skip { + y += line_height as f64 + } else { + y -= line_height as f64 + } + + s.absolute() + .width_pct(100.0) + .height(line_height as f32) + .justify_center() + .items_center() + .margin_top(y) + .pointer_events_auto() + .hover(|s| s.cursor(CursorStyle::Default)) + }) + }; + + stack(( + empty().style(move |s| { + s.height(config.get().editor.line_height() as f32 + 1.0) + }), + clip( + dyn_stack(each_fn, key_fn, view_fn) + .style(|s| s.flex_col().size_pct(100.0, 100.0)), + ) + .style(|s| s.size_pct(100.0, 100.0)), + )) + .style(|s| { + s.absolute() + .flex_col() + .size_pct(100.0, 100.0) + .pointer_events_none() + }) + .debug_name("Diff Show More Section") +} diff --git a/lapce-app/src/editor/gutter.rs b/lapce-app/src/editor/gutter.rs new file mode 100644 index 0000000..20b0d7c --- /dev/null +++ b/lapce-app/src/editor/gutter.rs @@ -0,0 +1,402 @@ +use floem::{ + Renderer, View, ViewId, + context::PaintCx, + peniko::kurbo::{Point, Rect, Size}, + reactive::{Memo, SignalGet, SignalWith}, + text::{Attrs, AttrsList, FamilyOwned, TextLayout}, +}; +use im::HashMap; +use lapce_core::{buffer::rope_text::RopeText, mode::Mode}; +use serde::{Deserialize, Serialize}; + +use super::{EditorData, view::changes_colors_screen}; +use crate::config::{LapceConfig, color::LapceColor}; + +pub struct EditorGutterView { + id: ViewId, + editor: EditorData, + width: f64, + gutter_padding_right: Memo, +} + +pub fn editor_gutter_view( + editor: EditorData, + gutter_padding_right: Memo, +) -> EditorGutterView { + let id = ViewId::new(); + + EditorGutterView { + id, + editor, + width: 0.0, + gutter_padding_right, + } +} + +impl EditorGutterView { + fn paint_head_changes( + &self, + cx: &mut PaintCx, + e_data: &EditorData, + viewport: Rect, + is_normal: bool, + config: &LapceConfig, + ) { + if !is_normal { + return; + } + + let changes = e_data.doc().head_changes().get_untracked(); + let line_height = config.editor.line_height() as f64; + let gutter_padding_right = self.gutter_padding_right.get_untracked() as f64; + + let changes = changes_colors_screen(config, &e_data.editor, changes); + for (y, height, removed, color) in changes { + let height = if removed { + 10.0 + } else { + height as f64 * line_height + }; + let mut y = y - viewport.y0; + if removed { + y -= 5.0; + } + cx.fill( + &Size::new(3.0, height).to_rect().with_origin(Point::new( + self.width + 5.0 - gutter_padding_right, + y, + )), + color, + 0.0, + ) + } + } + + fn paint_sticky_headers( + &self, + cx: &mut PaintCx, + is_normal: bool, + config: &LapceConfig, + ) { + if !is_normal { + return; + } + + if !config.editor.sticky_header { + return; + } + let sticky_header_height = self.editor.sticky_header_height.get_untracked(); + if sticky_header_height == 0.0 { + return; + } + + let sticky_area_rect = + Size::new(self.width + 25.0 + 30.0, sticky_header_height) + .to_rect() + .with_origin(Point::new(-25.0, 0.0)) + .inflate(25.0, 0.0); + cx.fill( + &sticky_area_rect, + config.color(LapceColor::LAPCE_DROPDOWN_SHADOW), + 3.0, + ); + cx.fill( + &sticky_area_rect, + config.color(LapceColor::EDITOR_STICKY_HEADER_BACKGROUND), + 0.0, + ); + } +} + +impl View for EditorGutterView { + fn id(&self) -> ViewId { + self.id + } + + fn compute_layout( + &mut self, + _cx: &mut floem::context::ComputeLayoutCx, + ) -> Option { + if let Some(width) = self.id.get_layout().map(|l| l.size.width as f64) { + self.width = width; + } + None + } + + fn paint(&mut self, cx: &mut floem::context::PaintCx) { + let viewport = self.editor.viewport().get_untracked(); + let cursor = self.editor.cursor(); + let screen_lines = self.editor.screen_lines(); + let config = self.editor.common.config; + + let kind_is_normal = + self.editor.kind.with_untracked(|kind| kind.is_normal()); + let (offset, mode) = cursor.with_untracked(|c| (c.offset(), c.get_mode())); + let config = config.get_untracked(); + let line_height = config.editor.line_height() as f64; + let last_line = self.editor.editor.last_line(); + let current_line = self + .editor + .doc() + .buffer + .with_untracked(|buffer| buffer.line_of_offset(offset)); + + let family: Vec = + FamilyOwned::parse_list(&config.editor.font_family).collect(); + let attrs = Attrs::new() + .family(&family) + .color(config.color(LapceColor::EDITOR_DIM)) + .font_size(config.editor.font_size() as f32); + let attrs_list = AttrsList::new(attrs.clone()); + let current_line_attrs_list = AttrsList::new( + attrs + .clone() + .color(config.color(LapceColor::EDITOR_FOREGROUND)), + ); + let show_relative = config.core.modal + && config.editor.modal_mode_relative_line_numbers + && mode != Mode::Insert + && kind_is_normal; + + screen_lines.with_untracked(|screen_lines| { + for (line, y) in screen_lines.iter_lines_y() { + // If it ends up outside the bounds of the file, stop trying to display line numbers + if line > last_line { + break; + } + + let text = if show_relative { + if line == current_line { + line + 1 + } else { + line.abs_diff(current_line) + } + } else { + line + 1 + } + .to_string(); + + let mut text_layout = TextLayout::new(); + if line == current_line { + text_layout.set_text( + &text, + current_line_attrs_list.clone(), + None, + ); + } else { + text_layout.set_text(&text, attrs_list.clone(), None); + } + let size = text_layout.size(); + let height = size.height; + + cx.draw_text( + &text_layout, + Point::new( + (self.width + - size.width + - self.gutter_padding_right.get_untracked() as f64) + .max(0.0), + y + (line_height - height) / 2.0 - viewport.y0, + ), + ); + } + }); + + self.paint_head_changes(cx, &self.editor, viewport, kind_is_normal, &config); + self.paint_sticky_headers(cx, kind_is_normal, &config); + } + + fn debug_name(&self) -> std::borrow::Cow<'static, str> { + "Editor Gutter".into() + } +} + +#[derive(Default, Clone)] +pub struct FoldingRanges(pub Vec); + +#[derive(Default, Clone)] +pub struct FoldedRanges(pub Vec); + +impl FoldingRanges { + pub fn get_folded_range(&self) -> FoldedRanges { + let mut range = Vec::new(); + let mut limit_line = 0; + for item in &self.0 { + if item.start.line < limit_line && limit_line > 0 { + continue; + } + if item.status.is_folded() { + range.push(crate::editor::gutter::FoldedRange { + start: item.start, + end: item.end, + }); + limit_line = item.end.line; + } + } + + FoldedRanges(range) + } + pub fn to_display_items(&self) -> Vec { + let mut folded = HashMap::new(); + let mut unfold_start: HashMap = HashMap::new(); + let mut unfold_end = HashMap::new(); + let mut limit_line = 0; + for item in &self.0 { + if item.start.line < limit_line && limit_line > 0 { + continue; + } + match item.status { + FoldingRangeStatus::Fold => { + folded.insert( + item.start.line, + FoldingDisplayItem::Folded(item.start), + ); + limit_line = item.end.line; + } + FoldingRangeStatus::Unfold => { + unfold_start.insert( + item.start.line, + FoldingDisplayItem::UnfoldStart(item.start), + ); + unfold_end.insert( + item.end.line, + FoldingDisplayItem::UnfoldEnd(item.end), + ); + limit_line = 0; + } + }; + } + for (key, val) in unfold_end { + unfold_start.insert(key, val); + } + for (key, val) in folded { + unfold_start.insert(key, val); + } + + unfold_start.into_iter().map(|x| x.1).collect() + } +} + +impl FoldedRanges { + pub fn contain_line(&self, start_index: usize, line: u32) -> (bool, usize) { + if start_index >= self.0.len() { + return (false, start_index); + } + let mut last_index = start_index; + for range in self.0[start_index..].iter() { + if range.start.line >= line { + return (false, last_index); + } else if range.start.line < line && range.end.line > line { + return (true, last_index); + } else if range.end.line < line { + last_index += 1; + } + } + (false, last_index) + } +} + +#[derive(Debug, Clone)] +pub struct FoldedRange { + pub start: FoldingPosition, + pub end: FoldingPosition, +} + +#[derive(Debug, Clone)] +pub struct FoldingRange { + pub start: FoldingPosition, + pub end: FoldingPosition, + pub status: FoldingRangeStatus, + pub collapsed_text: Option, +} + +impl FoldingRange { + pub fn from_lsp(value: lsp_types::FoldingRange) -> Self { + let lsp_types::FoldingRange { + start_line, + start_character, + end_line, + end_character, + collapsed_text, + .. + } = value; + let status = FoldingRangeStatus::Unfold; + Self { + start: FoldingPosition { + line: start_line, + character: start_character, + // kind: kind.clone().map(|x| FoldingRangeKind::from(x)), + }, + end: FoldingPosition { + line: end_line, + character: end_character, + // kind: kind.map(|x| FoldingRangeKind::from(x)), + }, + status, + collapsed_text, + } + } +} + +#[derive(Debug, Clone, Eq, PartialEq, Hash, Copy)] +pub struct FoldingPosition { + pub line: u32, + pub character: Option, + // pub kind: Option, +} + +#[derive(Debug, Clone, Default, Eq, PartialEq)] +pub enum FoldingRangeStatus { + Fold, + #[default] + Unfold, +} + +impl FoldingRangeStatus { + pub fn click(&mut self) { + // match self { + // FoldingRangeStatus::Fold => { + // *self = FoldingRangeStatus::Unfold; + // } + // FoldingRangeStatus::Unfold => { + // *self = FoldingRangeStatus::Fold; + // } + // } + } + pub fn is_folded(&self) -> bool { + *self == Self::Fold + } +} +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] +pub enum FoldingDisplayItem { + UnfoldStart(FoldingPosition), + Folded(FoldingPosition), + UnfoldEnd(FoldingPosition), +} + +impl FoldingDisplayItem { + pub fn position(&self) -> FoldingPosition { + match self { + FoldingDisplayItem::UnfoldStart(x) => *x, + FoldingDisplayItem::Folded(x) => *x, + FoldingDisplayItem::UnfoldEnd(x) => *x, + } + } +} + +#[derive(Debug, Eq, PartialEq, Deserialize, Serialize, Clone, Hash, Copy)] +pub enum FoldingRangeKind { + Comment, + Imports, + Region, +} + +impl From for FoldingRangeKind { + fn from(value: lsp_types::FoldingRangeKind) -> Self { + match value { + lsp_types::FoldingRangeKind::Comment => FoldingRangeKind::Comment, + lsp_types::FoldingRangeKind::Imports => FoldingRangeKind::Imports, + lsp_types::FoldingRangeKind::Region => FoldingRangeKind::Region, + } + } +} diff --git a/lapce-app/src/editor/location.rs b/lapce-app/src/editor/location.rs new file mode 100644 index 0000000..35e79f5 --- /dev/null +++ b/lapce-app/src/editor/location.rs @@ -0,0 +1,34 @@ +use std::path::PathBuf; + +use floem::peniko::kurbo::Vec2; +use lapce_core::{buffer::rope_text::RopeText, rope_text_pos::RopeTextPosition}; +use lsp_types::Position; + +#[derive(Clone, Debug, PartialEq)] +pub struct EditorLocation { + pub path: PathBuf, + pub position: Option, + pub scroll_offset: Option, + // This will ignore unconfirmed editors, and always create new editors + // if there's no match path on the active editor tab + pub ignore_unconfirmed: bool, + // This will stop finding matching path on different editor tabs + pub same_editor_tab: bool, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum EditorPosition { + Line(usize), + Position(Position), + Offset(usize), +} + +impl EditorPosition { + pub fn to_offset(&self, text: &impl RopeText) -> usize { + match self { + EditorPosition::Line(n) => text.first_non_blank_character_on_line(*n), + EditorPosition::Position(position) => text.offset_of_position(position), + EditorPosition::Offset(offset) => *offset, + } + } +} diff --git a/lapce-app/src/editor/view.rs b/lapce-app/src/editor/view.rs new file mode 100644 index 0000000..309d301 --- /dev/null +++ b/lapce-app/src/editor/view.rs @@ -0,0 +1,2667 @@ +use std::{ + cmp, collections::BTreeMap, ops::DerefMut, path::PathBuf, rc::Rc, sync::Arc, +}; + +use floem::{ + Renderer, View, ViewId, + action::{set_ime_allowed, set_ime_cursor_area}, + context::{PaintCx, StyleCx}, + event::{Event, EventListener, EventPropagation}, + keyboard::Modifiers, + kurbo::Stroke, + peniko::{ + Color, + kurbo::{Line, Point, Rect, Size}, + }, + prelude::SignalTrack, + reactive::{ + Memo, ReadSignal, RwSignal, SignalGet, SignalUpdate, SignalWith, + create_effect, create_memo, create_rw_signal, + }, + style::{CursorColor, CursorStyle, Style, TextColor}, + taffy::prelude::NodeId, + views::{ + Decorators, clip, container, dyn_stack, + editor::{ + CurrentLineColor, CursorSurroundingLines, Editor, EditorStyle, + IndentGuideColor, IndentStyleProp, Modal, ModalRelativeLine, + PhantomColor, PlaceholderColor, PreeditUnderlineColor, + RenderWhitespaceProp, ScrollBeyondLastLine, SelectionColor, + ShowIndentGuide, SmartTab, VisibleWhitespaceColor, WrapProp, + text::WrapMethod, + view::{ + DiffSectionKind, EditorView as FloemEditorView, EditorViewClass, + LineRegion, ScreenLines, cursor_caret, + }, + visual_line::{RVLine, VLine}, + }, + empty, label, + scroll::{PropagatePointerWheel, scroll}, + stack, svg, + }, +}; +use itertools::Itertools; +use lapce_core::{ + buffer::{Buffer, diff::DiffLines, rope_text::RopeText}, + cursor::{CursorAffinity, CursorMode}, + selection::SelRegion, +}; +use lapce_rpc::{ + dap_types::{DapId, SourceBreakpoint}, + plugin::PluginId, +}; +use lapce_xi_rope::find::CaseMatching; +use lsp_types::CodeLens; + +use super::{DocSignal, EditorData, gutter::editor_gutter_view}; +use crate::{ + app::clickable_icon, + command::InternalCommand, + config::{LapceConfig, color::LapceColor, editor::WrapStyle, icon::LapceIcons}, + debug::{DapData, LapceBreakpoint}, + doc::DocContent, + editor::gutter::FoldingDisplayItem, + text_input::TextInputBuilder, + window_tab::{CommonData, Focus, WindowTabData}, + workspace::LapceWorkspace, +}; + +#[derive(Clone, Debug, Default)] +pub struct StickyHeaderInfo { + pub sticky_lines: Vec, + pub last_sticky_should_scroll: bool, + pub y_diff: f64, +} + +fn editor_wrap(config: &LapceConfig) -> WrapMethod { + /// Minimum width that we'll allow the view to be wrapped at. + const MIN_WRAPPED_WIDTH: f32 = 100.0; + + match config.editor.wrap_style { + WrapStyle::None => WrapMethod::None, + WrapStyle::EditorWidth => WrapMethod::EditorWidth, + WrapStyle::WrapWidth => WrapMethod::WrapWidth { + width: (config.editor.wrap_width as f32).max(MIN_WRAPPED_WIDTH), + }, + } +} + +pub fn editor_style( + config: ReadSignal>, + doc: DocSignal, + s: Style, +) -> Style { + let config = config.get(); + let doc = doc.get(); + + s.set( + IndentStyleProp, + doc.buffer.with_untracked(Buffer::indent_style), + ) + .set(CursorColor, config.color(LapceColor::EDITOR_CARET)) + .set(SelectionColor, config.color(LapceColor::EDITOR_SELECTION)) + .set( + CurrentLineColor, + config.color(LapceColor::EDITOR_CURRENT_LINE), + ) + .set( + VisibleWhitespaceColor, + config.color(LapceColor::EDITOR_VISIBLE_WHITESPACE), + ) + .set( + IndentGuideColor, + config.color(LapceColor::EDITOR_INDENT_GUIDE), + ) + .set(ScrollBeyondLastLine, config.editor.scroll_beyond_last_line) + .color(config.color(LapceColor::EDITOR_FOREGROUND)) + .set(TextColor, config.color(LapceColor::EDITOR_FOREGROUND)) + .set(PhantomColor, config.color(LapceColor::EDITOR_DIM)) + .set(PlaceholderColor, config.color(LapceColor::EDITOR_DIM)) + .set( + PreeditUnderlineColor, + config.color(LapceColor::EDITOR_FOREGROUND), + ) + .set(ShowIndentGuide, config.editor.show_indent_guide) + .set(Modal, config.core.modal) + .set( + ModalRelativeLine, + config.editor.modal_mode_relative_line_numbers, + ) + .set(SmartTab, config.editor.smart_tab) + .set(WrapProp, editor_wrap(&config)) + .set( + CursorSurroundingLines, + config.editor.cursor_surrounding_lines, + ) + .set(RenderWhitespaceProp, config.editor.render_whitespace) +} + +pub struct EditorView { + id: ViewId, + editor: EditorData, + is_active: Memo, + inner_node: Option, + viewport: RwSignal, + debug_breakline: Memo>, +} + +pub fn editor_view( + e_data: EditorData, + debug_breakline: Memo>, + is_active: impl Fn(bool) -> bool + 'static + Copy, +) -> EditorView { + let id = ViewId::new(); + let is_active = create_memo(move |_| is_active(true)); + + let viewport = e_data.viewport(); + + let doc = e_data.doc_signal(); + let view_kind = e_data.kind; + let screen_lines = e_data.screen_lines(); + create_effect(move |_| { + doc.track(); + view_kind.track(); + id.request_layout(); + }); + + let hide_cursor = e_data.common.window_common.hide_cursor; + create_effect(move |_| { + hide_cursor.track(); + let occurrences = doc.with(|doc| doc.find_result.occurrences); + occurrences.track(); + id.request_paint(); + }); + + create_effect(move |last_rev| { + let buffer = doc.with(|doc| doc.buffer); + let rev = buffer.with(|buffer| buffer.rev()); + if last_rev == Some(rev) { + return rev; + } + id.request_layout(); + rev + }); + + let config = e_data.common.config; + let sticky_header_height_signal = e_data.sticky_header_height; + let editor2 = e_data.clone(); + create_effect(move |last_rev| { + let config = config.get(); + if !config.editor.sticky_header { + return (DocContent::Local, 0, 0, Rect::ZERO, 0, None); + } + + let doc = doc.get(); + let rect = viewport.get(); + let (screen_lines_len, screen_lines_first) = screen_lines + .with(|lines| (lines.lines.len(), lines.lines.first().copied())); + let rev = ( + doc.content.get(), + doc.buffer.with(|b| b.rev()), + doc.cache_rev.get(), + rect, + screen_lines_len, + screen_lines_first, + ); + if last_rev.as_ref() == Some(&rev) { + return rev; + } + + let sticky_header_info = get_sticky_header_info( + &editor2, + viewport, + sticky_header_height_signal, + &config, + ); + + id.update_state(sticky_header_info); + + rev + }); + + let ed1 = e_data.editor.clone(); + let ed2 = ed1.clone(); + let ed3 = ed1.clone(); + + let editor_window_origin = e_data.window_origin(); + let cursor = e_data.cursor(); + let find_focus = e_data.find_focus; + let ime_allowed = e_data.common.window_common.ime_allowed; + let editor_viewport = e_data.viewport(); + let editor_cursor = e_data.cursor(); + create_effect(move |_| { + let active = is_active.get(); + if active && !find_focus.get() { + if !cursor.with(|c| c.is_insert()) { + if ime_allowed.get_untracked() { + ime_allowed.set(false); + set_ime_allowed(false); + } + } else { + if !ime_allowed.get_untracked() { + ime_allowed.set(true); + set_ime_allowed(true); + } + let (offset, affinity) = cursor.with(|c| (c.offset(), c.affinity)); + let (_, point_below) = ed1.points_of_offset(offset, affinity); + let window_origin = editor_window_origin.get(); + let viewport = editor_viewport.get(); + let pos = window_origin + + (point_below.x - viewport.x0, point_below.y - viewport.y0); + set_ime_cursor_area(pos, Size::new(800.0, 600.0)); + } + } + }); + + let doc = e_data.doc_signal(); + EditorView { + id, + editor: e_data, + is_active, + inner_node: None, + viewport, + debug_breakline, + } + .on_event(EventListener::ImePreedit, move |event| { + if !is_active.get_untracked() { + return EventPropagation::Continue; + } + + if let Event::ImePreedit { text, cursor } = event { + if text.is_empty() { + ed2.clear_preedit(); + } else { + let offset = editor_cursor.with_untracked(|c| c.offset()); + ed2.set_preedit(text.clone(), *cursor, offset); + } + } + EventPropagation::Stop + }) + .on_event(EventListener::ImeCommit, move |event| { + if !is_active.get_untracked() { + return EventPropagation::Continue; + } + + if let Event::ImeCommit(text) = event { + ed3.clear_preedit(); + ed3.receive_char(text); + } + EventPropagation::Stop + }) + .class(EditorViewClass) + .style(move |s| editor_style(config, doc, s)) +} + +impl EditorView { + fn paint_diff_sections( + &self, + cx: &mut PaintCx, + viewport: Rect, + screen_lines: &ScreenLines, + config: &LapceConfig, + ) { + let Some(diff_sections) = &screen_lines.diff_sections else { + return; + }; + for section in diff_sections.iter() { + match section.kind { + DiffSectionKind::NoCode => self.paint_diff_no_code( + cx, + viewport, + section.y_idx, + section.height, + config, + ), + DiffSectionKind::Added => { + cx.fill( + &Rect::ZERO + .with_size(Size::new( + viewport.width(), + (config.editor.line_height() * section.height) + as f64, + )) + .with_origin(Point::new( + viewport.x0, + (section.y_idx * config.editor.line_height()) as f64, + )), + config + .color(LapceColor::SOURCE_CONTROL_ADDED) + .multiply_alpha(0.2), + 0.0, + ); + } + DiffSectionKind::Removed => { + cx.fill( + &Rect::ZERO + .with_size(Size::new( + viewport.width(), + (config.editor.line_height() * section.height) + as f64, + )) + .with_origin(Point::new( + viewport.x0, + (section.y_idx * config.editor.line_height()) as f64, + )), + config + .color(LapceColor::SOURCE_CONTROL_REMOVED) + .multiply_alpha(0.2), + 0.0, + ); + } + } + } + } + + fn paint_diff_no_code( + &self, + cx: &mut PaintCx, + viewport: Rect, + start_line: usize, + height: usize, + config: &LapceConfig, + ) { + let line_height = config.editor.line_height(); + let height = (height * line_height) as f64; + let y = (start_line * line_height) as f64; + let y_end = y + height; + + if y_end < viewport.y0 || y > viewport.y1 { + return; + } + + let y = y.max(viewport.y0 - 10.0); + let y_end = y_end.min(viewport.y1 + 10.0); + let height = y_end - y; + + let start_x = viewport.x0.floor() as usize; + let start_x = start_x - start_x % 8; + + for x in (start_x..viewport.x1.ceil() as usize + 1 + height.ceil() as usize) + .step_by(8) + { + let p0 = if x as f64 > viewport.x1.ceil() { + Point::new(viewport.x1.ceil(), y + (x as f64 - viewport.x1.ceil())) + } else { + Point::new(x as f64, y) + }; + + let height = if x as f64 - height < viewport.x0.floor() { + x as f64 - viewport.x0.floor() + } else { + height + }; + if height > 0.0 { + let p1 = Point::new(x as f64 - height, y + height); + cx.stroke( + &Line::new(p0, p1), + config.color(LapceColor::EDITOR_DIM), + &Stroke::new(1.0), + ); + } + } + } + + fn paint_current_line( + &self, + cx: &mut PaintCx, + is_local: bool, + screen_lines: &ScreenLines, + ) { + let e_data = self.editor.clone(); + let ed = e_data.editor.clone(); + let cursor = self.editor.cursor(); + let config = self.editor.common.config; + + let config = config.get_untracked(); + let line_height = config.editor.line_height() as f64; + let viewport = self.viewport.get_untracked(); + + let current_line_color = ed.es.with_untracked(EditorStyle::current_line); + + let breakline = self.debug_breakline.get_untracked().and_then( + |(breakline, breakline_path)| { + if e_data + .doc() + .content + .with_untracked(|c| c.path() == Some(&breakline_path)) + { + Some(breakline) + } else { + None + } + }, + ); + + // TODO: check if this is correct + if let Some(breakline) = breakline { + if let Some(info) = screen_lines.info_for_line(breakline) { + let rect = Rect::from_origin_size( + (viewport.x0, info.vline_y), + (viewport.width(), line_height), + ); + + cx.fill( + &rect, + config.color(LapceColor::EDITOR_DEBUG_BREAK_LINE), + 0.0, + ); + } + } + + cursor.with_untracked(|cursor| { + let highlight_current_line = match cursor.mode { + CursorMode::Normal(_) | CursorMode::Insert(_) => true, + CursorMode::Visual { .. } => false, + }; + + if let Some(current_line_color) = current_line_color { + // Highlight the current line + if !is_local && highlight_current_line { + for (_, end) in cursor.regions_iter() { + // TODO: unsure if this is correct for wrapping lines + let rvline = ed.rvline_of_offset(end, cursor.affinity); + + if let Some(info) = screen_lines + .info(rvline) + .filter(|_| Some(rvline.line) != breakline) + { + let rect = Rect::from_origin_size( + (viewport.x0, info.vline_y), + (viewport.width(), line_height), + ); + + cx.fill(&rect, current_line_color, 0.0); + } + } + } + } + }); + } + + fn paint_find(&self, cx: &mut PaintCx, screen_lines: &ScreenLines) { + let find_visual = self.editor.common.find.visual.get_untracked(); + if !find_visual && self.editor.on_screen_find.with_untracked(|f| !f.active) { + return; + } + if screen_lines.lines.is_empty() { + return; + } + + let min_vline = *screen_lines.lines.first().unwrap(); + let max_vline = *screen_lines.lines.last().unwrap(); + let min_line = screen_lines.info(min_vline).unwrap().vline_info.rvline.line; + let max_line = screen_lines.info(max_vline).unwrap().vline_info.rvline.line; + + let e_data = &self.editor; + let ed = &e_data.editor; + let doc = e_data.doc(); + + let config = self.editor.common.config; + let occurrences = doc.find_result.occurrences; + + let config = config.get_untracked(); + let line_height = config.editor.line_height() as f64; + let color = config.color(LapceColor::EDITOR_FOREGROUND); + + let start = ed.offset_of_line(min_line); + let end = ed.offset_of_line(max_line + 1); + + // TODO: The selection rect creation logic for find is quite similar to the version + // within insert cursor. It would be good to deduplicate it. + if find_visual { + doc.update_find(); + for region in occurrences.with_untracked(|selection| { + selection.regions_in_range(start, end).to_vec() + }) { + self.paint_find_region( + cx, + ed, + ®ion, + color, + screen_lines, + line_height, + ); + } + } + + self.editor.on_screen_find.with_untracked(|find| { + if find.active { + for region in &find.regions { + self.paint_find_region( + cx, + ed, + region, + color, + screen_lines, + line_height, + ); + } + } + }); + } + + fn paint_find_region( + &self, + cx: &mut PaintCx, + ed: &Editor, + region: &SelRegion, + color: Color, + screen_lines: &ScreenLines, + line_height: f64, + ) { + let start = region.min(); + let end = region.max(); + + // TODO(minor): the proper affinity here should probably be tracked by selregion + let (start_rvline, start_col) = + ed.rvline_col_of_offset(start, CursorAffinity::Forward); + let (end_rvline, end_col) = + ed.rvline_col_of_offset(end, CursorAffinity::Backward); + + for line_info in screen_lines.iter_line_info() { + let rvline_info = line_info.vline_info; + let rvline = rvline_info.rvline; + let line = rvline.line; + + if rvline < start_rvline { + continue; + } + + if rvline > end_rvline { + break; + } + + let left_col = if rvline == start_rvline { start_col } else { 0 }; + let (right_col, _vline_end) = if rvline == end_rvline { + let max_col = ed.last_col(rvline_info, true); + (end_col.min(max_col), false) + } else { + (ed.last_col(rvline_info, true), true) + }; + + // TODO(minor): sel region should have the affinity of the start/end + let x0 = ed + .line_point_of_line_col( + line, + left_col, + CursorAffinity::Forward, + true, + ) + .x; + let x1 = ed + .line_point_of_line_col( + line, + right_col, + CursorAffinity::Backward, + true, + ) + .x; + + if !rvline_info.is_empty() && start != end && left_col != right_col { + let rect = Size::new(x1 - x0, line_height) + .to_rect() + .with_origin(Point::new(x0, line_info.vline_y)); + cx.stroke(&rect, color, &Stroke::new(1.0)); + } + } + } + + fn paint_sticky_headers( + &self, + cx: &mut PaintCx, + viewport: Rect, + screen_lines: &ScreenLines, + ) { + let config = self.editor.common.config.get_untracked(); + if !config.editor.sticky_header { + return; + } + if !self.editor.kind.get_untracked().is_normal() { + return; + } + + let line_height = config.editor.line_height(); + let Some(start_vline) = screen_lines.lines.first() else { + return; + }; + let start_info = screen_lines.vline_info(*start_vline).unwrap(); + let start_line = start_info.rvline.line; + + let sticky_header_info = self.editor.sticky_header_info.get_untracked(); + let total_sticky_lines = sticky_header_info.sticky_lines.len(); + + let paint_last_line = total_sticky_lines > 0 + && (sticky_header_info.last_sticky_should_scroll + || sticky_header_info.y_diff != 0.0 + || start_line + total_sticky_lines - 1 + != *sticky_header_info.sticky_lines.last().unwrap()); + + let total_sticky_lines = if paint_last_line { + total_sticky_lines + } else { + total_sticky_lines.saturating_sub(1) + }; + + if total_sticky_lines == 0 { + return; + } + + let scroll_offset = if sticky_header_info.last_sticky_should_scroll { + sticky_header_info.y_diff + } else { + 0.0 + }; + + // Clear background + + let area_height = sticky_header_info + .sticky_lines + .iter() + .copied() + .map(|line| { + let layout = self.editor.editor.text_layout(line); + layout.line_count() * line_height + }) + .sum::() as f64 + - scroll_offset; + + let sticky_area_rect = Size::new(viewport.x1, area_height) + .to_rect() + .with_origin(Point::new(0.0, viewport.y0)) + .inflate(10.0, 0.0); + + cx.fill( + &sticky_area_rect, + config.color(LapceColor::LAPCE_DROPDOWN_SHADOW), + 3.0, + ); + cx.fill( + &sticky_area_rect, + config.color(LapceColor::EDITOR_STICKY_HEADER_BACKGROUND), + 0.0, + ); + self.editor.sticky_header_info.get_untracked(); + // Paint lines + let mut y_accum = 0.0; + for (i, line) in sticky_header_info.sticky_lines.iter().copied().enumerate() + { + let y_diff = if i == total_sticky_lines - 1 { + scroll_offset + } else { + 0.0 + }; + + let text_layout = self.editor.editor.text_layout(line); + + let text_height = (text_layout.line_count() * line_height) as f64; + let height = text_height - y_diff; + + cx.save(); + + let line_area_rect = Size::new(viewport.width(), height) + .to_rect() + .with_origin(Point::new(viewport.x0, viewport.y0 + y_accum)); + + cx.clip(&line_area_rect); + + let y = viewport.y0 - y_diff + y_accum; + cx.draw_text(&text_layout.text, Point::new(viewport.x0, y)); + + y_accum += text_height; + + cx.restore(); + } + } + + fn paint_scroll_bar( + &self, + cx: &mut PaintCx, + viewport: Rect, + is_local: bool, + config: Arc, + ) { + const BAR_WIDTH: f64 = 10.0; + + if is_local { + return; + } + + cx.fill( + &Rect::ZERO + .with_size(Size::new(1.0, viewport.height())) + .with_origin(Point::new( + viewport.x0 + viewport.width() - BAR_WIDTH, + viewport.y0, + )) + .inflate(0.0, 10.0), + config.color(LapceColor::LAPCE_SCROLL_BAR), + 0.0, + ); + + if !self.editor.kind.get_untracked().is_normal() { + return; + } + + let doc = self.editor.doc(); + let total_len = doc.buffer.with_untracked(|buffer| buffer.last_line()); + let changes = doc.head_changes().get_untracked(); + let total_height = viewport.height(); + let total_width = viewport.width(); + let line_height = config.editor.line_height(); + let content_height = if config.editor.scroll_beyond_last_line { + (total_len * line_height) as f64 + total_height - line_height as f64 + } else { + (total_len * line_height) as f64 + }; + + let colors = changes_colors_all(&config, &self.editor.editor, changes); + for (y, height, _, color) in colors { + let y = y / content_height * total_height; + let height = ((height * line_height) as f64 / content_height + * total_height) + .max(3.0); + let rect = Rect::ZERO.with_size(Size::new(3.0, height)).with_origin( + Point::new( + viewport.x0 + total_width - BAR_WIDTH + 1.0, + y + viewport.y0, + ), + ); + cx.fill(&rect, color, 0.0); + } + } + + /// Paint a highlight around the characters at the given positions. + fn paint_char_highlights( + &self, + cx: &mut PaintCx, + screen_lines: &ScreenLines, + highlight_line_cols: impl Iterator, + ) { + let editor = &self.editor.editor; + let config = self.editor.common.config.get_untracked(); + let line_height = config.editor.line_height() as f64; + + for (rvline, col) in highlight_line_cols { + // Is the given line on screen? + if let Some(line_info) = screen_lines.info(rvline) { + let x0 = editor + .line_point_of_line_col( + rvline.line, + col, + CursorAffinity::Forward, + true, + ) + .x; + let x1 = editor + .line_point_of_line_col( + rvline.line, + col + 1, + CursorAffinity::Backward, + true, + ) + .x; + + let y0 = line_info.vline_y; + let y1 = y0 + line_height; + + let rect = Rect::new(x0, y0, x1, y1); + + cx.stroke( + &rect, + config.color(LapceColor::EDITOR_FOREGROUND), + &Stroke::new(1.0), + ); + } + } + } + + /// Paint scope lines between `(start_rvline, start_line, start_col)` and + /// `(end_rvline, end_line end_col)`. + fn paint_scope_lines( + &self, + cx: &mut PaintCx, + viewport: Rect, + screen_lines: &ScreenLines, + (start, start_col): (RVLine, usize), + (end, end_col): (RVLine, usize), + ) { + let editor = &self.editor.editor; + let doc = self.editor.doc(); + let config = self.editor.common.config.get_untracked(); + let line_height = config.editor.line_height() as f64; + let brush = config.color(LapceColor::EDITOR_FOREGROUND); + + if start == end { + if let Some(line_info) = screen_lines.info(start) { + // TODO: Due to line wrapping the y positions of these two spots could be different, do we need to change it? + let x0 = editor + .line_point_of_line_col( + start.line, + start_col + 1, + CursorAffinity::Forward, + true, + ) + .x; + let x1 = editor + .line_point_of_line_col( + end.line, + end_col, + CursorAffinity::Backward, + true, + ) + .x; + + if x0 < x1 { + let y = line_info.vline_y + line_height; + + let p0 = Point::new(x0, y); + let p1 = Point::new(x1, y); + let line = Line::new(p0, p1); + + cx.stroke(&line, brush, &Stroke::new(1.0)); + } + } + } else { + // Are start_line and end_line on screen? + let start_line_y = screen_lines + .info(start) + .map(|line_info| line_info.vline_y + line_height); + let end_line_y = screen_lines + .info(end) + .map(|line_info| line_info.vline_y + line_height); + + // We only need to draw anything if start_line is on or before the visible section and + // end_line is on or after the visible section. + let y0 = start_line_y.or_else(|| { + screen_lines + .lines + .first() + .is_some_and(|&first_vline| first_vline > start) + .then(|| viewport.min_y()) + }); + let y1 = end_line_y.or_else(|| { + screen_lines + .lines + .last() + .is_some_and(|&last_line| last_line < end) + .then(|| viewport.max_y()) + }); + + if let [Some(y0), Some(y1)] = [y0, y1] { + let start_x = editor + .line_point_of_line_col( + start.line, + start_col + 1, + CursorAffinity::Forward, + true, + ) + .x; + let end_x = editor + .line_point_of_line_col( + end.line, + end_col, + CursorAffinity::Backward, + true, + ) + .x; + + // TODO(minor): is this correct with line wrapping? + // The vertical line should be drawn to the left of any non-whitespace characters + // in the enclosed section. + let min_text_x = doc.buffer.with_untracked(|buffer| { + ((start.line + 1)..=end.line) + .filter(|&line| !buffer.is_line_whitespace(line)) + .map(|line| { + let non_blank_offset = + buffer.first_non_blank_character_on_line(line); + let (_, col) = + editor.offset_to_line_col(non_blank_offset); + + editor + .line_point_of_line_col( + line, + col, + CursorAffinity::Backward, + true, + ) + .x + }) + .min_by(f64::total_cmp) + }); + + let min_x = min_text_x.map_or(start_x, |min_text_x| { + cmp::min_by(min_text_x, start_x, f64::total_cmp) + }); + + // Is start_line on screen, and is the vertical line to the left of the opening + // bracket? + if let Some(y) = start_line_y.filter(|_| start_x > min_x) { + let p0 = Point::new(min_x, y); + let p1 = Point::new(start_x, y); + let line = Line::new(p0, p1); + + cx.stroke(&line, brush, &Stroke::new(1.0)); + } + + // Is end_line on screen, and is the vertical line to the left of the closing + // bracket? + if let Some(y) = end_line_y.filter(|_| end_x > min_x) { + let p0 = Point::new(min_x, y); + let p1 = Point::new(end_x, y); + let line = Line::new(p0, p1); + + cx.stroke(&line, brush, &Stroke::new(1.0)); + } + + let p0 = Point::new(min_x, y0); + let p1 = Point::new(min_x, y1); + let line = Line::new(p0, p1); + + cx.stroke(&line, brush, &Stroke::new(1.0)); + } + } + } + + /// Paint enclosing bracket highlights and scope lines if the corresponding settings are + /// enabled. + fn paint_bracket_highlights_scope_lines( + &self, + cx: &mut PaintCx, + viewport: Rect, + screen_lines: &ScreenLines, + ) { + let config = self.editor.common.config.get_untracked(); + + if config.editor.highlight_matching_brackets + || config.editor.highlight_scope_lines + { + let e_data = &self.editor; + let ed = &e_data.editor; + let offset = ed.cursor.with_untracked(|cursor| cursor.mode.offset()); + + let bracket_offsets = e_data + .doc_signal() + .with_untracked(|doc| doc.find_enclosing_brackets(offset)) + .map(|(start, end)| [start, end]); + + let bracket_line_cols = bracket_offsets.map(|bracket_offsets| { + bracket_offsets.map(|offset| { + let (rvline, col) = + ed.rvline_col_of_offset(offset, CursorAffinity::Forward); + (rvline, col) + }) + }); + + if config.editor.highlight_matching_brackets { + self.paint_char_highlights( + cx, + screen_lines, + bracket_line_cols.into_iter().flatten(), + ); + } + + if config.editor.highlight_scope_lines { + if let Some([start_line_col, end_line_col]) = bracket_line_cols { + self.paint_scope_lines( + cx, + viewport, + screen_lines, + start_line_col, + end_line_col, + ); + } + } + } + } +} + +impl View for EditorView { + fn id(&self) -> ViewId { + self.id + } + + fn style_pass(&mut self, cx: &mut StyleCx<'_>) { + let editor = &self.editor.editor; + if editor.es.try_update(|s| s.read(cx)).unwrap() { + editor.floem_style_id.update(|val| *val += 1); + cx.app_state_mut().request_paint(self.id()); + } + } + + fn debug_name(&self) -> std::borrow::Cow<'static, str> { + "Editor View".into() + } + + fn update( + &mut self, + _cx: &mut floem::context::UpdateCx, + state: Box, + ) { + if let Ok(state) = state.downcast() { + self.editor.sticky_header_info.set(*state); + self.id.request_layout(); + } + } + + fn layout( + &mut self, + cx: &mut floem::context::LayoutCx, + ) -> floem::taffy::prelude::NodeId { + cx.layout_node(self.id, true, |_cx| { + if self.inner_node.is_none() { + self.inner_node = Some(self.id.new_taffy_node()); + } + + let e_data = &self.editor; + let editor = &e_data.editor; + + let viewport_size = self.viewport.get_untracked().size(); + + let screen_lines = e_data.screen_lines().get_untracked(); + for (line, _) in screen_lines.iter_lines_y() { + // fill in text layout cache so that max width is correct. + editor.text_layout(line); + } + + let inner_node = self.inner_node.unwrap(); + + let config = self.editor.common.config.get_untracked(); + let line_height = config.editor.line_height() as f64; + + let is_local = e_data.doc().content.with_untracked(|c| c.is_local()); + + let width = editor.max_line_width() + 10.0; + let width = if !is_local { + width.max(viewport_size.width) + } else { + width + }; + let last_vline = editor.last_vline().get(); + let last_vline = e_data.visual_line(last_vline); + let last_line_height = line_height * (last_vline + 1) as f64; + let height = last_line_height.max(line_height); + let height = if !is_local { + height.max(viewport_size.height) + } else { + height + }; + + let margin_bottom = if !is_local + && editor + .es + .with_untracked(EditorStyle::scroll_beyond_last_line) + { + viewport_size.height.min(last_line_height) - line_height + } else { + 0.0 + }; + + let style = Style::new() + .width(width) + .height(height) + .margin_bottom(margin_bottom) + .to_taffy_style(); + self.id.set_taffy_style(inner_node, style); + + vec![inner_node] + }) + } + + fn compute_layout( + &mut self, + cx: &mut floem::context::ComputeLayoutCx, + ) -> Option { + let viewport = cx.current_viewport(); + if self.viewport.with_untracked(|v| v != &viewport) { + self.viewport.set(viewport); + } + None + } + + fn paint(&mut self, cx: &mut PaintCx) { + let viewport = self.viewport.get_untracked(); + let e_data = &self.editor; + let ed = &e_data.editor; + let config = e_data.common.config.get_untracked(); + let doc = e_data.doc(); + let is_local = doc.content.with_untracked(|content| content.is_local()); + let find_focus = self.editor.find_focus; + let is_active = + self.is_active.get_untracked() && !find_focus.get_untracked(); + + // We repeatedly get the screen lines because we don't currently carefully manage the + // paint functions to avoid potentially needing to recompute them, which could *maybe* + // make them invalid. + // TODO: One way to get around the above issue would be to more careful, since we + // technically don't need to stop it from *recomputing* just stop any possible changes, but + // avoiding recomputation seems easiest/clearest. + // I expect that most/all of the paint functions could restrict themselves to only what is + // within the active screen lines without issue. + let screen_lines = ed.screen_lines.get_untracked(); + self.paint_current_line(cx, is_local, &screen_lines); + FloemEditorView::paint_selection(cx, ed, &screen_lines); + let screen_lines = ed.screen_lines.get_untracked(); + self.paint_diff_sections(cx, viewport, &screen_lines, &config); + let screen_lines = ed.screen_lines.get_untracked(); + self.paint_find(cx, &screen_lines); + let screen_lines = ed.screen_lines.get_untracked(); + self.paint_bracket_highlights_scope_lines(cx, viewport, &screen_lines); + let screen_lines = ed.screen_lines.get_untracked(); + FloemEditorView::paint_text( + cx, + None, + ed, + viewport, + is_active, + &screen_lines, + ); + let screen_lines = ed.screen_lines.get_untracked(); + self.paint_sticky_headers(cx, viewport, &screen_lines); + self.paint_scroll_bar(cx, viewport, is_local, config); + } +} + +fn get_sticky_header_info( + editor_data: &EditorData, + viewport: RwSignal, + sticky_header_height_signal: RwSignal, + config: &LapceConfig, +) -> StickyHeaderInfo { + let editor = &editor_data.editor; + let doc = editor_data.doc(); + + let viewport = viewport.get(); + // TODO(minor): should this be a `get` + let screen_lines = editor.screen_lines.get(); + let line_height = config.editor.line_height() as f64; + // let start_line = (viewport.y0 / line_height).floor() as usize; + let Some(start) = screen_lines.lines.first() else { + return StickyHeaderInfo { + sticky_lines: Vec::new(), + last_sticky_should_scroll: false, + y_diff: 0.0, + }; + }; + let start_info = screen_lines.info(*start).unwrap(); + let start_line = start_info.vline_info.rvline.line; + + let y_diff = viewport.y0 - start_info.vline_y; + + let mut last_sticky_should_scroll = false; + let mut sticky_lines = Vec::new(); + if let Some(lines) = doc.sticky_headers(start_line) { + let total_lines = lines.len(); + if total_lines > 0 { + let line = start_line + total_lines; + if let Some(new_lines) = doc.sticky_headers(line) { + if new_lines.len() > total_lines { + sticky_lines = new_lines; + } else { + sticky_lines = lines; + last_sticky_should_scroll = new_lines.len() < total_lines; + if new_lines.len() < total_lines { + if let Some(new_new_lines) = + doc.sticky_headers(start_line + total_lines - 1) + { + if new_new_lines.len() < total_lines { + sticky_lines.pop(); + last_sticky_should_scroll = false; + } + } else { + sticky_lines.pop(); + last_sticky_should_scroll = false; + } + } + } + } else { + sticky_lines = lines; + last_sticky_should_scroll = true; + } + } + } + + let total_sticky_lines = sticky_lines.len(); + + let paint_last_line = total_sticky_lines > 0 + && (last_sticky_should_scroll + || y_diff != 0.0 + || start_line + total_sticky_lines - 1 != *sticky_lines.last().unwrap()); + + // Fix up the line count in case we don't need to paint the last one. + let total_sticky_lines = if paint_last_line { + total_sticky_lines + } else { + total_sticky_lines.saturating_sub(1) + }; + + if total_sticky_lines == 0 { + sticky_header_height_signal.set(0.0); + return StickyHeaderInfo { + sticky_lines: Vec::new(), + last_sticky_should_scroll: false, + y_diff: 0.0, + }; + } + + let scroll_offset = if last_sticky_should_scroll { + y_diff + } else { + 0.0 + }; + + let sticky_header_height = sticky_lines + .iter() + .enumerate() + .map(|(i, line)| { + // TODO(question): won't y_diff always be scroll_offset here? so we should just sub on + // the outside + let y_diff = if i == total_sticky_lines - 1 { + scroll_offset + } else { + 0.0 + }; + + let layout = editor.text_layout(*line); + layout.line_count() as f64 * line_height - y_diff + }) + .sum(); + + sticky_header_height_signal.set(sticky_header_height); + StickyHeaderInfo { + sticky_lines, + last_sticky_should_scroll, + y_diff, + } +} + +pub fn editor_container_view( + window_tab_data: Rc, + workspace: Arc, + is_active: impl Fn(bool) -> bool + 'static + Copy, + editor: RwSignal, +) -> impl View { + let (editor_id, find_focus, sticky_header_height, editor_view, config, doc, ed) = + editor.with_untracked(|editor| { + ( + editor.id(), + editor.find_focus, + editor.sticky_header_height, + editor.kind, + editor.common.config, + editor.doc_signal(), + editor.editor.clone(), + ) + }); + + let main_split = window_tab_data.main_split.clone(); + let editors = main_split.editors; + let scratch_docs = main_split.scratch_docs; + let find_editor = main_split.find_editor; + let replace_editor = main_split.replace_editor; + let replace_active = main_split.common.find.replace_active; + let replace_focus = main_split.common.find.replace_focus; + let debug_breakline = window_tab_data.terminal.breakline; + + let viewport = ed.viewport; + let screen_lines = ed.screen_lines; + + stack(( + editor_breadcrumbs(workspace, editor.get_untracked(), config), + stack(( + editor_gutter(window_tab_data.clone(), editor), + editor_gutter_folding_range( + window_tab_data.clone(), + doc, + screen_lines, + viewport, + ), + editor_content(editor, debug_breakline, is_active), + empty().style(move |s| { + let config = config.get(); + s.absolute() + .width_pct(100.0) + .height(sticky_header_height.get() as f32) + // .box_shadow_blur(5.0) + // .border_bottom(1.0) + // .border_color( + // config.get_color(LapceColor::LAPCE_BORDER), + // ) + .apply_if( + !config.editor.sticky_header + || sticky_header_height.get() == 0.0 + || !editor_view.get().is_normal(), + |s| s.hide(), + ) + }), + find_view( + editor, + find_editor, + find_focus, + replace_editor, + replace_active, + replace_focus, + is_active, + ) + .debug_name("find view"), + )) + .style(|s| s.width_full().flex_basis(0).flex_grow(1.0)), + )) + .on_cleanup(move || { + let editor = editor.get_untracked(); + editor.cancel_completion(); + editor.cancel_inline_completion(); + if editors.contains_untracked(editor_id) { + // editor still exist, so it might be moved to a different editor tab + return; + } + let doc = editor.doc(); + editor.scope.dispose(); + + let scratch_doc_name = + if let DocContent::Scratch { name, .. } = doc.content.get_untracked() { + Some(name.to_string()) + } else { + None + }; + if let Some(name) = scratch_doc_name { + if !scratch_docs + .with_untracked(|scratch_docs| scratch_docs.contains_key(&name)) + { + doc.scope.dispose(); + } + } + }) + .style(|s| s.flex_col().absolute().size_pct(100.0, 100.0)) + .debug_name("Editor Container") +} + +fn editor_gutter_breakpoint_view( + i: usize, + doc: DocSignal, + daps: RwSignal>, + breakpoints: RwSignal>>, + screen_lines: RwSignal, + common: Rc, + icon_padding: f32, +) -> impl View { + let hovered = create_rw_signal(false); + let config = common.config; + container( + svg(move || config.get().ui_svg(LapceIcons::DEBUG_BREAKPOINT)).style( + move |s| { + let config = config.get(); + let size = config.ui.icon_size() as f32 + 2.0; + s.size(size, size) + .color(config.color(LapceColor::DEBUG_BREAKPOINT_HOVER)) + .apply_if(!hovered.get(), |s| s.hide()) + }, + ), + ) + .on_click_stop(move |_| { + let screen_lines = screen_lines.get_untracked(); + let line = screen_lines.lines.get(i).map(|r| r.line).unwrap_or(0); + // let line = (viewport.get_untracked().y0 + // / config.get_untracked().editor.line_height() as f64) + // .floor() as usize + // + i; + let doc = doc.get_untracked(); + let offset = doc.buffer.with_untracked(|b| b.offset_of_line(line)); + if let Some(path) = doc.content.get_untracked().path() { + let path_breakpoints = breakpoints + .try_update(|breakpoints| { + let breakpoints = breakpoints.entry(path.clone()).or_default(); + if let std::collections::btree_map::Entry::Vacant(e) = + breakpoints.entry(line) + { + e.insert(LapceBreakpoint { + id: None, + verified: false, + message: None, + line, + offset, + dap_line: None, + active: true, + }); + } else { + let mut toggle_active = false; + if let Some(breakpoint) = breakpoints.get_mut(&line) { + if !breakpoint.active { + breakpoint.active = true; + toggle_active = true; + } + } + if !toggle_active { + breakpoints.remove(&line); + } + } + breakpoints.clone() + }) + .unwrap(); + let source_breakpoints: Vec = path_breakpoints + .iter() + .filter_map(|(_, b)| { + if b.active { + Some(SourceBreakpoint { + line: b.line + 1, + column: None, + condition: None, + hit_condition: None, + log_message: None, + }) + } else { + None + } + }) + .collect(); + let daps: Vec = + daps.with_untracked(|daps| daps.keys().cloned().collect()); + for dap_id in daps { + common.proxy.dap_set_breakpoints( + dap_id, + path.to_path_buf(), + source_breakpoints.clone(), + ); + } + } + }) + .on_event_stop(EventListener::PointerEnter, move |_| { + hovered.set(true); + }) + .on_event_stop(EventListener::PointerLeave, move |_| { + hovered.set(false); + }) + .style(move |s| { + let config = config.get(); + s.width(config.ui.icon_size() as f32 + icon_padding * 2.0) + .height(config.editor.line_height() as f32) + .justify_center() + .items_center() + .cursor(CursorStyle::Pointer) + }) +} + +fn editor_gutter_breakpoints( + window_tab_data: Rc, + e_data: RwSignal, + icon_padding: f32, +) -> impl View { + let breakpoints = window_tab_data.terminal.debug.breakpoints; + let daps = window_tab_data.terminal.debug.daps; + let common = window_tab_data.common.clone(); + + let (ed, doc, config) = e_data + .with_untracked(|e| (e.editor.clone(), e.doc_signal(), e.common.config)); + let viewport = ed.viewport; + let screen_lines = ed.screen_lines; + + let num_display_lines = create_memo(move |_| { + let screen_lines = screen_lines.get(); + screen_lines.lines.len() + // let viewport = viewport.get(); + // let line_height = config.get().editor.line_height() as f64; + // (viewport.height() / line_height).ceil() as usize + 1 + }); + + clip( + stack(( + dyn_stack( + move || { + let num = num_display_lines.get(); + 0..num + }, + move |i| *i, + move |i| { + editor_gutter_breakpoint_view( + i, + doc, + daps, + breakpoints, + screen_lines, + common.clone(), + icon_padding, + ) + }, + ) + .style(move |s| { + s.absolute().flex_col().margin_top( + -(viewport.get().y0 % config.get().editor.line_height() as f64) + as f32, + ) + }) + .debug_name("Breakpoint Stack"), + dyn_stack( + move || { + let e_data = e_data.get(); + let doc = e_data.doc_signal().get(); + let content = doc.content.get(); + let breakpoints = if let Some(path) = content.path() { + breakpoints + .with(|b| b.get(path).cloned()) + .unwrap_or_default() + } else { + Default::default() + }; + breakpoints.into_iter() + }, + move |(line, b)| (*line, b.active), + move |(line, breakpoint)| { + let active = breakpoint.active; + container( + svg(move || { + config.get().ui_svg(LapceIcons::DEBUG_BREAKPOINT) + }) + .style(move |s| { + let config = config.get(); + let size = config.ui.icon_size() as f32 + 2.0; + let color = if active { + LapceColor::DEBUG_BREAKPOINT + } else { + LapceColor::EDITOR_DIM + }; + let color = config.color(color); + s.size(size, size).color(color) + }), + ) + .style(move |s| { + let config = config.get(); + let line_y = screen_lines + .with(|s| s.info_for_line(line)) + .map(|l| l.y) + .unwrap_or_default(); + s.absolute() + .width(config.ui.icon_size() as f32 + icon_padding * 2.0) + .height(config.editor.line_height() as f32) + .justify_center() + .items_center() + .margin_top(line_y as f32 - viewport.get().y0 as f32) + }) + }, + ) + .style(|s| s.absolute().size_pct(100.0, 100.0)), + )) + .style(|s| s.size_pct(100.0, 100.0)), + ) + .style(move |s| { + s.absolute() + .size_pct(100.0, 100.0) + .background(config.get().color(LapceColor::EDITOR_BACKGROUND)) + }) +} + +fn editor_gutter_code_lens_view( + window_tab_data: Rc, + line: usize, + lens: (PluginId, usize, im::Vector), + screen_lines: RwSignal, + viewport: RwSignal, + icon_padding: f32, +) -> impl View { + let config = window_tab_data.common.config; + let view = container(svg(move || config.get().ui_svg(LapceIcons::START)).style( + move |s| { + let config = config.get(); + let size = config.ui.icon_size() as f32; + s.size(size, size) + .color(config.color(LapceColor::LAPCE_ICON_ACTIVE)) + }, + )) + .style(move |s| { + let config = config.get(); + s.padding(4.0) + .border_radius(6.0) + .hover(|s| { + s.cursor(CursorStyle::Pointer) + .background(config.color(LapceColor::PANEL_HOVERED_BACKGROUND)) + }) + .active(|s| { + s.background( + config.color(LapceColor::PANEL_HOVERED_ACTIVE_BACKGROUND), + ) + }) + }) + .on_click_stop({ + move |_| { + let (plugin_id, offset, lens) = lens.clone(); + window_tab_data.show_code_lens(true, plugin_id, offset, lens); + } + }); + container(view).style(move |s| { + let line_info = screen_lines.with(|s| s.info_for_line(line)); + let line_y = line_info.clone().map(|l| l.y).unwrap_or(-100.0); + let rect = viewport.get(); + let config = config.get(); + let icon_size = config.ui.icon_size(); + let width = icon_size as f32 + icon_padding * 2.0; + s.absolute() + .width(width) + .height(config.editor.line_height() as f32) + .justify_center() + .items_center() + .margin_top(line_y as f32 - rect.y0 as f32) + }) +} + +fn editor_gutter_folding_view( + window_tab_data: Rc, + screen_lines: RwSignal, + viewport: RwSignal, + folding_display_item: FoldingDisplayItem, +) -> impl View { + let config = window_tab_data.common.config; + let view = container( + svg(move || { + let icon_str = match folding_display_item { + FoldingDisplayItem::UnfoldStart(_) => LapceIcons::FOLD_DOWN, + FoldingDisplayItem::Folded(_) => LapceIcons::FOLD, + FoldingDisplayItem::UnfoldEnd(_) => LapceIcons::FOLD_UP, + }; + config.get().ui_svg(icon_str) + }) + .style(move |s| { + let config = config.get(); + let size = config.ui.icon_size() as f32; + s.size(size, size) + .color(config.color(LapceColor::LAPCE_ICON_ACTIVE)) + }), + ) + .style(move |s| { + let config = config.get(); + s.padding(4.0) + .border_radius(6.0) + .hover(|s| { + s.cursor(CursorStyle::Pointer) + .background(config.color(LapceColor::PANEL_HOVERED_BACKGROUND)) + }) + .active(|s| { + s.background( + config.color(LapceColor::PANEL_HOVERED_ACTIVE_BACKGROUND), + ) + }) + }); + container(view).style(move |s| { + let line = folding_display_item.position().line; + let line_info = screen_lines.with(|s| s.info_for_line(line as usize)); + let line_y = line_info.clone().map(|l| l.y).unwrap_or(-100.0); + let rect = viewport.get(); + let config = config.get(); + let icon_size = config.ui.icon_size(); + let width = icon_size as f32 + 4.0; + s.absolute() + .width(width / 2.0) + .height(config.editor.line_height() as f32) + .justify_center() + .items_center() + .margin_top(line_y as f32 - rect.y0 as f32) + }) +} + +fn editor_gutter_code_lens( + window_tab_data: Rc, + doc: DocSignal, + screen_lines: RwSignal, + viewport: RwSignal, + icon_padding: f32, +) -> impl View { + let config = window_tab_data.common.config; + + dyn_stack( + move || { + let doc = doc.get(); + doc.code_lens.get() + }, + move |(line, _)| (*line, doc.with_untracked(|doc| doc.rev())), + move |(line, lens)| { + editor_gutter_code_lens_view( + window_tab_data.clone(), + line, + lens, + screen_lines, + viewport, + icon_padding, + ) + }, + ) + .style(move |s| { + let config = config.get(); + let width = config.ui.icon_size() as f32 + icon_padding * 2.0; + s.absolute() + .width(width) + .height_full() + .margin_left(width - 8.0) + }) + .debug_name("CodeLens Stack") +} + +fn editor_gutter_folding_range( + window_tab_data: Rc, + doc: DocSignal, + screen_lines: RwSignal, + viewport: RwSignal, +) -> impl View { + let config = window_tab_data.common.config; + dyn_stack( + move || doc.get().folding_ranges.get().to_display_items(), + move |item| *item, + move |item| { + editor_gutter_folding_view( + window_tab_data.clone(), + screen_lines, + viewport, + item, + ) + .on_click_stop({ + move |_| { + doc.get_untracked().folding_ranges.update(|x| match item { + FoldingDisplayItem::UnfoldStart(pos) + | FoldingDisplayItem::Folded(pos) => { + x.0.iter_mut().find_map(|mut range| { + let range = range.deref_mut(); + if range.start == pos { + range.status.click(); + Some(()) + } else { + None + } + }); + } + FoldingDisplayItem::UnfoldEnd(pos) => { + x.0.iter_mut().find_map(|mut range| { + let range = range.deref_mut(); + if range.end == pos { + range.status.click(); + Some(()) + } else { + None + } + }); + } + }) + } + }) + }, + ) + .style(move |s| { + let config = config.get(); + let width = config.ui.icon_size() as f32; + // hide for now + s.width(width) + .height_full() + .margin_left(-width / 2.0) + .hide() + }) + .debug_name("Folding Range Stack") +} + +fn editor_gutter_code_actions( + e_data: RwSignal, + gutter_width: Memo, + icon_padding: f32, +) -> impl View { + let (ed, doc, config) = e_data + .with_untracked(|e| (e.editor.clone(), e.doc_signal(), e.common.config)); + let viewport = ed.viewport; + let cursor = ed.cursor; + + let code_action_vline = create_memo(move |_| { + let doc = doc.get(); + let (offset, affinity) = + cursor.with(|cursor| (cursor.offset(), cursor.affinity)); + let has_code_actions = doc + .code_actions() + .with(|c| c.get(&offset).map(|c| !c.1.is_empty()).unwrap_or(false)); + if has_code_actions { + let vline = ed.vline_of_offset(offset, affinity); + Some(vline) + } else { + None + } + }); + + container( + container( + svg(move || config.get().ui_svg(LapceIcons::LIGHTBULB)).style( + move |s| { + let config = config.get(); + let size = config.ui.icon_size() as f32; + s.size(size, size) + .color(config.color(LapceColor::LAPCE_WARN)) + }, + ), + ) + .on_click_stop(move |_| { + e_data.get_untracked().show_code_actions(true); + }) + .style(move |s| { + let config = config.get(); + s.padding(4.0) + .border_radius(6.0) + .hover(|s| { + s.cursor(CursorStyle::Pointer).background( + config.color(LapceColor::PANEL_HOVERED_BACKGROUND), + ) + }) + .active(|s| { + s.background( + config.color(LapceColor::PANEL_HOVERED_ACTIVE_BACKGROUND), + ) + }) + }), + ) + .style(move |s| { + let config = config.get(); + let viewport = viewport.get(); + let gutter_width = gutter_width.get(); + let code_action_vline = code_action_vline.get(); + let size = config.ui.icon_size() as f32; + let line_height = config.editor.line_height(); + let margin_top = if let Some(vline) = code_action_vline { + (vline.get() * line_height) as f32 - viewport.y0 as f32 + } else { + 0.0 + }; + let width = size + icon_padding * 2.0; + s.absolute() + .items_center() + .justify_center() + .margin_left(gutter_width as f32 - width + 1.0) + .margin_top(margin_top) + .width(width) + .height(line_height as f32) + .apply_if(code_action_vline.is_none(), |s| s.hide()) + }) + .debug_name("Code Action LightBulb") +} + +fn editor_gutter( + window_tab_data: Rc, + e_data: RwSignal, +) -> impl View { + let icon_padding = 6.0; + + let (ed, doc, config) = e_data + .with_untracked(|e| (e.editor.clone(), e.doc_signal(), e.common.config)); + let viewport = ed.viewport; + let scroll_delta = ed.scroll_delta; + let screen_lines = ed.screen_lines; + + let gutter_rect = create_rw_signal(Rect::ZERO); + let gutter_width = create_memo(move |_| gutter_rect.get().width()); + + let icon_total_width = move || { + let icon_size = config.get().ui.icon_size() as f32; + icon_size + icon_padding * 2.0 + }; + + let gutter_padding_right = create_memo(move |_| icon_total_width() + 6.0); + + stack(( + stack(( + empty().style(move |s| s.width(icon_total_width() * 2.0 - 8.0)), + label(move || { + let doc = doc.get(); + doc.buffer.with(|b| b.last_line() + 1).to_string() + }), + empty().style(move |s| s.width(gutter_padding_right.get())), + )) + .debug_name("Centered Last Line Count") + .style(|s| s.height_pct(100.0)), + editor_gutter_breakpoints(window_tab_data.clone(), e_data, icon_padding), + clip( + stack(( + editor_gutter_code_lens( + window_tab_data.clone(), + doc, + screen_lines, + viewport, + icon_padding, + ), + editor_gutter_view(e_data.get_untracked(), gutter_padding_right) + .on_resize(move |rect| { + gutter_rect.set(rect); + }) + .on_event_stop(EventListener::PointerWheel, move |event| { + if let Event::PointerWheel(pointer_event) = event { + scroll_delta.set(pointer_event.delta); + } + }) + .style(|s| s.size_pct(100.0, 100.0)), + editor_gutter_code_actions(e_data, gutter_width, icon_padding), + )) + .style(|s| s.size_pct(100.0, 100.0)), + ) + .style(move |s| s.absolute().size_pct(100.0, 100.0)), + )) + .style(|s| s.height_pct(100.0)) + .debug_name("Editor Gutter") +} + +fn editor_breadcrumbs( + workspace: Arc, + e_data: EditorData, + config: ReadSignal>, +) -> impl View { + let doc = e_data.doc_signal(); + let doc_path = create_memo(move |_| { + let doc = doc.get(); + let content = doc.content.get(); + if let DocContent::History(history) = &content { + Some(history.path.clone()) + } else { + content.path().cloned() + } + }); + container( + scroll( + stack(( + { + let workspace = workspace.clone(); + dyn_stack( + move || { + let full_path = doc_path.get().unwrap_or_default(); + let mut path = full_path; + if let Some(workspace_path) = + workspace.clone().path.as_ref() + { + path = path + .strip_prefix(workspace_path) + .unwrap_or(&path) + .to_path_buf(); + } + path.ancestors() + .filter_map(|path| { + Some( + path.file_name()? + .to_string_lossy() + .into_owned(), + ) + }) + .collect::>() + .into_iter() + .rev() + .enumerate() + }, + |(i, section)| (*i, section.to_string()), + move |(i, section)| { + stack(( + svg(move || { + config + .get() + .ui_svg(LapceIcons::BREADCRUMB_SEPARATOR) + }) + .style(move |s| { + let config = config.get(); + let size = config.ui.icon_size() as f32; + s.apply_if(i == 0, |s| s.hide()) + .size(size, size) + .color( + config.color( + LapceColor::LAPCE_ICON_ACTIVE, + ), + ) + }), + label(move || section.clone()) + .style(move |s| s.selectable(false)), + )) + .style(|s| s.items_center()) + }, + ) + .style(|s| s.padding_horiz(10.0)) + }, + label(move || { + let doc = doc.get(); + if let DocContent::History(history) = doc.content.get() { + format!("({})", history.version) + } else { + "".to_string() + } + }) + .style(move |s| { + let doc = doc.get(); + let is_history = doc.content.with_untracked(|content| { + matches!(content, DocContent::History(_)) + }); + + s.padding_right(10.0).apply_if(!is_history, |s| s.hide()) + }), + )) + .style(|s| s.items_center()), + ) + .scroll_to(move || { + doc.track(); + Some(Point::new(3000.0, 0.0)) + }) + .scroll_style(|s| s.hide_bars(true)) + .style(move |s| { + s.absolute() + .size_pct(100.0, 100.0) + .border_bottom(1.0) + .border_color(config.get().color(LapceColor::LAPCE_BORDER)) + .items_center() + }), + ) + .style(move |s| { + let config = config.get_untracked(); + let line_height = config.editor.line_height(); + s.items_center() + .width_pct(100.0) + .height(line_height as f32) + .apply_if(doc_path.get().is_none(), |s| s.hide()) + .apply_if(!config.editor.show_bread_crumbs, |s| s.hide()) + }) + .debug_name("Editor BreadCrumbs") +} + +fn editor_content( + e_data: RwSignal, + debug_breakline: Memo>, + is_active: impl Fn(bool) -> bool + 'static + Copy, +) -> impl View { + let ( + cursor, + scroll_delta, + scroll_to, + window_origin, + viewport, + sticky_header_height, + config, + editor, + ) = e_data.with_untracked(|editor| { + ( + editor.cursor().read_only(), + editor.scroll_delta().read_only(), + editor.scroll_to(), + editor.window_origin(), + editor.viewport(), + editor.sticky_header_height, + editor.common.config, + editor.editor.clone(), + ) + }); + + { + create_effect(move |_| { + is_active(true); + let e_data = e_data.get_untracked(); + e_data.cancel_completion(); + e_data.cancel_inline_completion(); + }); + } + + let current_scroll = create_rw_signal(Rect::ZERO); + + scroll({ + let editor_content_view = + editor_view(e_data.get_untracked(), debug_breakline, is_active).style( + move |s| { + s.absolute() + .margin_left(1.0) + .min_size_full() + .cursor(CursorStyle::Text) + }, + ); + + let id = editor_content_view.id(); + editor.editor_view_id.set(Some(id)); + + let editor2 = editor.clone(); + editor_content_view + .on_event_cont(EventListener::FocusGained, move |_| { + editor.editor_view_focused.notify(); + }) + .on_event_cont(EventListener::FocusLost, move |_| { + editor2.editor_view_focus_lost.notify(); + }) + .on_event_cont(EventListener::PointerDown, move |event| { + if let Event::PointerDown(pointer_event) = event { + id.request_active(); + e_data.get_untracked().pointer_down(pointer_event); + } + }) + .on_event_stop(EventListener::PointerMove, move |event| { + if let Event::PointerMove(pointer_event) = event { + e_data.get_untracked().pointer_move(pointer_event); + } + }) + .on_event_stop(EventListener::PointerUp, move |event| { + if let Event::PointerUp(pointer_event) = event { + e_data.get_untracked().pointer_up(pointer_event); + } + }) + .on_event_stop(EventListener::PointerLeave, move |event| { + if let Event::PointerLeave = event { + e_data.get_untracked().pointer_leave(); + } + }) + }) + .on_move(move |point| { + window_origin.set(point); + }) + .on_scroll(move |rect| { + if rect.y0 != current_scroll.get_untracked().y0 { + // only cancel completion if scrolled vertically + let e_data = e_data.get_untracked(); + e_data.cancel_completion(); + e_data.cancel_inline_completion(); + } + current_scroll.set(rect); + }) + .scroll_to(move || scroll_to.get().map(|s| s.to_point())) + .scroll_delta(move || scroll_delta.get()) + .ensure_visible(move || { + let e_data = e_data.get_untracked(); + let cursor = cursor.get(); + let offset = cursor.offset(); + e_data.doc_signal().track(); + e_data.kind.track(); + + let LineRegion { x, width, rvline } = cursor_caret( + &e_data.editor, + offset, + !cursor.is_insert(), + cursor.affinity, + ); + let config = config.get_untracked(); + let line_height = config.editor.line_height(); + // TODO: is there a good way to avoid the calculation of the vline here? + let vline = e_data.editor.vline_of_rvline(rvline); + let vline = e_data.visual_line(vline.get()); + let rect = Rect::from_origin_size( + (x, (vline * line_height) as f64), + (width, line_height as f64), + ) + .inflate(10.0, 0.0); + + let viewport = viewport.get_untracked(); + let smallest_distance = (viewport.y0 - rect.y0) + .abs() + .min((viewport.y1 - rect.y0).abs()) + .min((viewport.y0 - rect.y1).abs()) + .min((viewport.y1 - rect.y1).abs()); + let biggest_distance = (viewport.y0 - rect.y0) + .abs() + .max((viewport.y1 - rect.y0).abs()) + .max((viewport.y0 - rect.y1).abs()) + .max((viewport.y1 - rect.y1).abs()); + let jump_to_middle = biggest_distance > viewport.height() + && smallest_distance > viewport.height() / 2.0; + + if jump_to_middle { + rect.inflate(0.0, viewport.height() / 2.0) + } else { + let cursor_surrounding_lines = + e_data.editor.es.with(|s| s.cursor_surrounding_lines()); + let mut rect = rect; + rect.y0 -= (cursor_surrounding_lines * line_height) as f64 + + sticky_header_height.get_untracked(); + rect.y1 += (cursor_surrounding_lines * line_height) as f64; + rect + } + }) + .style(|s| s.size_full().set(PropagatePointerWheel, false)) + .debug_name("Editor Content") +} + +fn search_editor_view( + find_editor: EditorData, + find_focus: RwSignal, + is_active: impl Fn(bool) -> bool + 'static + Copy, + replace_focus: RwSignal, +) -> impl View { + let config = find_editor.common.config; + + let case_matching = find_editor.common.find.case_matching; + let whole_word = find_editor.common.find.whole_words; + let is_regex = find_editor.common.find.is_regex; + let visual = find_editor.common.find.visual; + + stack(( + TextInputBuilder::new() + .is_focused(move || { + is_active(true) + && visual.get() + && find_focus.get() + && !replace_focus.get() + }) + .build_editor(find_editor) + .on_event_cont(EventListener::PointerDown, move |_| { + find_focus.set(true); + replace_focus.set(false); + }) + .style(|s| s.width_pct(100.0)), + clickable_icon( + || LapceIcons::SEARCH_CASE_SENSITIVE, + move || { + let new = match case_matching.get_untracked() { + CaseMatching::Exact => CaseMatching::CaseInsensitive, + CaseMatching::CaseInsensitive => CaseMatching::Exact, + }; + case_matching.set(new); + }, + move || case_matching.get() == CaseMatching::Exact, + || false, + || "Case Sensitive", + config, + ) + .style(|s| s.padding_vert(4.0)), + clickable_icon( + || LapceIcons::SEARCH_WHOLE_WORD, + move || { + whole_word.update(|whole_word| { + *whole_word = !*whole_word; + }); + }, + move || whole_word.get(), + || false, + || "Whole Word", + config, + ) + .style(|s| s.padding_left(6.0)), + clickable_icon( + || LapceIcons::SEARCH_REGEX, + move || { + is_regex.update(|is_regex| { + *is_regex = !*is_regex; + }); + }, + move || is_regex.get(), + || false, + || "Use Regex", + config, + ) + .style(|s| s.padding_horiz(6.0)), + )) + .style(move |s| { + let config = config.get(); + s.width(200.0) + .items_center() + .border(1.0) + .border_radius(6.0) + .border_color(config.color(LapceColor::LAPCE_BORDER)) + .background(config.color(LapceColor::EDITOR_BACKGROUND)) + }) +} + +fn replace_editor_view( + replace_editor: EditorData, + replace_active: RwSignal, + replace_focus: RwSignal, + is_active: impl Fn(bool) -> bool + 'static + Copy, + find_focus: RwSignal, +) -> impl View { + let config = replace_editor.common.config; + let visual = replace_editor.common.find.visual; + + stack(( + TextInputBuilder::new() + .is_focused(move || { + is_active(true) + && visual.get() + && find_focus.get() + && replace_active.get() + && replace_focus.get() + }) + .build_editor(replace_editor) + .on_event_cont(EventListener::PointerDown, move |_| { + find_focus.set(true); + replace_focus.set(true); + }) + .style(|s| s.width_pct(100.0)), + empty().style(move |s| { + let config = config.get(); + let size = config.ui.icon_size() as f32 + 10.0; + s.size(0.0, size).padding_vert(4.0) + }), + )) + .style(move |s| { + let config = config.get(); + s.width(200.0) + .items_center() + .border(1.0) + .border_radius(6.0) + .border_color(config.color(LapceColor::LAPCE_BORDER)) + .background(config.color(LapceColor::EDITOR_BACKGROUND)) + }) +} + +fn find_view( + editor: RwSignal, + find_editor: EditorData, + find_focus: RwSignal, + replace_editor: EditorData, + replace_active: RwSignal, + replace_focus: RwSignal, + is_active: impl Fn(bool) -> bool + 'static + Copy, +) -> impl View { + let common = find_editor.common.clone(); + let config = common.config; + let find_visual = common.find.visual; + let replace_doc = replace_editor.doc_signal(); + let focus = common.focus; + + let find_pos = create_memo(move |_| { + let visual = find_visual.get(); + if !visual { + return (0, 0); + } + let editor = editor.get_untracked(); + let cursor = editor.cursor(); + let offset = cursor.with(|cursor| cursor.offset()); + let occurrences = editor.doc_signal().get().find_result.occurrences; + occurrences.with(|occurrences| { + for (i, region) in occurrences.regions().iter().enumerate() { + if offset <= region.max() { + return (i + 1, occurrences.regions().len()); + } + } + (occurrences.regions().len(), occurrences.regions().len()) + }) + }); + + container( + stack(( + stack(( + clickable_icon( + move || { + if replace_active.get() { + LapceIcons::ITEM_OPENED + } else { + LapceIcons::ITEM_CLOSED + } + }, + move || { + replace_active.update(|active| *active = !*active); + }, + move || false, + || false, + || "Toggle Replace", + config, + ) + .style(|s| s.padding_horiz(6.0)), + search_editor_view( + find_editor, + find_focus, + is_active, + replace_focus, + ), + label(move || { + let (current, all) = find_pos.get(); + if all == 0 { + "No Results".to_string() + } else { + format!("{current} of {all}") + } + }) + .style(|s| s.margin_left(6.0).min_width(70.0)), + clickable_icon( + || LapceIcons::SEARCH_BACKWARD, + move || { + editor.get_untracked().search_backward(Modifiers::empty()); + }, + move || false, + || false, + || "Previous Match", + config, + ) + .style(|s| s.padding_left(6.0)), + clickable_icon( + || LapceIcons::SEARCH_FORWARD, + move || { + editor.get_untracked().search_forward(Modifiers::empty()); + }, + move || false, + || false, + || "Next Match", + config, + ) + .style(|s| s.padding_left(6.0)), + clickable_icon( + || LapceIcons::CLOSE, + move || { + editor.get_untracked().clear_search(); + }, + move || false, + || false, + || "Close", + config, + ) + .style(|s| s.padding_horiz(6.0)), + )) + .style(|s| s.items_center()), + stack(( + empty().style(move |s| { + let config = config.get(); + let width = config.ui.icon_size() as f32 + 10.0 + 6.0 * 2.0; + s.width(width) + }), + replace_editor_view( + replace_editor, + replace_active, + replace_focus, + is_active, + find_focus, + ), + clickable_icon( + || LapceIcons::SEARCH_REPLACE, + move || { + let text = replace_doc + .get_untracked() + .buffer + .with_untracked(|b| b.to_string()); + editor.get_untracked().replace_next(&text); + }, + move || false, + || false, + || "Replace Next", + config, + ) + .style(|s| s.padding_left(6.0)), + clickable_icon( + || LapceIcons::SEARCH_REPLACE_ALL, + move || { + let text = replace_doc + .get_untracked() + .buffer + .with_untracked(|b| b.to_string()); + editor.get_untracked().replace_all(&text); + }, + move || false, + || false, + || "Replace All", + config, + ) + .style(|s| s.padding_left(6.0)), + )) + .style(move |s| { + s.items_center() + .margin_top(4.0) + .apply_if(!replace_active.get(), |s| s.hide()) + }), + )) + .style(move |s| { + let config = config.get(); + s.margin_right(50.0) + .background(config.color(LapceColor::PANEL_BACKGROUND)) + .border_radius(6.0) + .border(1.0) + .border_color(config.color(LapceColor::LAPCE_BORDER)) + .padding_vert(4.0) + .cursor(CursorStyle::Default) + .flex_col() + }) + .on_event_stop(EventListener::PointerDown, move |_| { + // Shift the editor tab focus to the editor the find search is attached to + // So that if you have two tabs open side-by-side (and thus two find views), + // clicking on one will shift the focus to the editor it's attached to + let editor = editor.get_untracked(); + if let Some(editor_tab_id) = editor.editor_tab_id.get_untracked() { + editor + .common + .internal_command + .send(InternalCommand::FocusEditorTab { editor_tab_id }); + } + focus.set(Focus::Workbench); + // Request focus on the app view, as our current method of dispatching pointer events + // is from the app_view to the actual editor. That's also why this stops the pointer + // event is stopped here, as otherwise our default handling would make it go through to + // the editor. + common + .window_common + .app_view_id + .get_untracked() + .request_focus(); + }), + ) + .style(move |s| { + s.absolute() + .margin_top(-1.0) + .width_pct(100.0) + .justify_end() + .apply_if(!find_visual.get(), |s| s.hide()) + }) +} + +/// Iterator over (len, color, modified) for each change in the diff +fn changes_color_iter<'a>( + changes: &'a im::Vector, + config: &'a LapceConfig, +) -> impl Iterator, bool)> + 'a { + let mut last_change = None; + changes.iter().map(move |change| { + let len = match change { + DiffLines::Left(_range) => 0, + DiffLines::Both(info) => info.right.len(), + DiffLines::Right(range) => range.len(), + }; + let mut modified = false; + let color = match change { + DiffLines::Left(_range) => { + Some(config.color(LapceColor::SOURCE_CONTROL_REMOVED)) + } + DiffLines::Right(_range) => { + if let Some(DiffLines::Left(_)) = last_change.as_ref() { + modified = true; + } + if modified { + Some(config.color(LapceColor::SOURCE_CONTROL_MODIFIED)) + } else { + Some(config.color(LapceColor::SOURCE_CONTROL_ADDED)) + } + } + _ => None, + }; + + last_change = Some(change.clone()); + + (len, color, modified) + }) +} + +// TODO: both of the changes color functions could easily return iterators + +/// Get the position and coloring information for over the entire current [`ScreenLines`] +/// Returns `(y, height_idx, removed, color)` +pub fn changes_colors_screen( + config: &LapceConfig, + editor: &Editor, + changes: im::Vector, +) -> Vec<(f64, usize, bool, Color)> { + let screen_lines = editor.screen_lines.get_untracked(); + + let Some((min, max)) = screen_lines.rvline_range() else { + return Vec::new(); + }; + + let line_height = config.editor.line_height(); + let mut line = 0; + let mut colors = Vec::new(); + + for (len, color, modified) in changes_color_iter(&changes, config) { + let pre_line = line; + + line += len; + if line < min.line { + continue; + } + + if let Some(color) = color { + if modified { + colors.pop(); + } + + let rvline = editor.rvline_of_line(pre_line); + let vline = editor.vline_of_line(pre_line); + let y = (vline.0 * line_height) as f64; + let height = { + // Accumulate the number of line indices each potentially wrapped line spans + let end_line = rvline.line + len; + + editor.iter_rvlines_over(false, rvline, end_line).count() + }; + let removed = len == 0; + + colors.push((y, height, removed, color)); + } + + if line > max.line { + break; + } + } + + colors +} + +// TODO: limit the visual line that changes are considered past to some reasonable number +// TODO(minor): This could be a `changes_colors_range` with some minor changes, but it isn't needed +/// Get the position and coloring information for over the entire current [`ScreenLines`] +/// Returns `(y, height_idx, removed, color)` +pub fn changes_colors_all( + config: &LapceConfig, + ed: &Editor, + changes: im::Vector, +) -> Vec<(f64, usize, bool, Color)> { + let line_height = config.editor.line_height(); + + let mut line = 0; + let mut colors = Vec::new(); + + let mut vline_iter = ed.iter_vlines(false, VLine(0)).peekable(); + + for (len, color, modified) in changes_color_iter(&changes, config) { + let pre_line = line; + + line += len; + + // Skip over all vlines that are before the current line + vline_iter + .by_ref() + .peeking_take_while(|info| info.rvline.line < pre_line) + .count(); + + if let Some(color) = color { + if modified { + colors.pop(); + } + + // Find the info with a line == pre_line + let Some(info) = vline_iter.peek() else { + continue; + }; + + let y = info.vline.get() * line_height; + let end_line = info.rvline.line + len; + let height = vline_iter + .by_ref() + .peeking_take_while(|info| info.rvline.line < end_line) + .count(); + let removed = len == 0; + + colors.push((y as f64, height, removed, color)); + } + + if vline_iter.peek().is_none() { + break; + } + } + + colors +} diff --git a/lapce-app/src/editor_tab.rs b/lapce-app/src/editor_tab.rs new file mode 100644 index 0000000..1845bf9 --- /dev/null +++ b/lapce-app/src/editor_tab.rs @@ -0,0 +1,484 @@ +use std::{ + path::{Path, PathBuf}, + rc::Rc, + sync::Arc, +}; + +use floem::{ + peniko::{ + Color, + kurbo::{Point, Rect}, + }, + reactive::{ + Memo, ReadSignal, RwSignal, Scope, SignalGet, SignalUpdate, SignalWith, + create_memo, create_rw_signal, + }, + views::editor::id::EditorId, +}; +use lapce_rpc::plugin::VoltID; +use serde::{Deserialize, Serialize}; + +use crate::{ + config::{LapceConfig, color::LapceColor, icon::LapceIcons}, + doc::{Doc, DocContent}, + editor::{ + EditorData, EditorInfo, + diff::{DiffEditorData, DiffEditorInfo}, + location::EditorLocation, + }, + id::{ + DiffEditorId, EditorTabId, KeymapId, SettingsId, SplitId, + ThemeColorSettingsId, VoltViewId, + }, + main_split::{Editors, MainSplitData}, + plugin::PluginData, + window_tab::WindowTabData, +}; + +#[derive(Clone, Serialize, Deserialize)] +pub enum EditorTabChildInfo { + Editor(EditorInfo), + DiffEditor(DiffEditorInfo), + Settings, + ThemeColorSettings, + Keymap, + Volt(VoltID), +} + +impl EditorTabChildInfo { + pub fn to_data( + &self, + data: MainSplitData, + editor_tab_id: EditorTabId, + ) -> EditorTabChild { + match &self { + EditorTabChildInfo::Editor(editor_info) => { + let editor_id = editor_info.to_data(data, editor_tab_id); + EditorTabChild::Editor(editor_id) + } + EditorTabChildInfo::DiffEditor(diff_editor_info) => { + let diff_editor_data = diff_editor_info.to_data(data, editor_tab_id); + EditorTabChild::DiffEditor(diff_editor_data.id) + } + EditorTabChildInfo::Settings => { + EditorTabChild::Settings(SettingsId::next()) + } + EditorTabChildInfo::ThemeColorSettings => { + EditorTabChild::ThemeColorSettings(ThemeColorSettingsId::next()) + } + EditorTabChildInfo::Keymap => EditorTabChild::Keymap(KeymapId::next()), + EditorTabChildInfo::Volt(id) => { + EditorTabChild::Volt(VoltViewId::next(), id.to_owned()) + } + } + } +} + +#[derive(Clone, Serialize, Deserialize)] +pub struct EditorTabInfo { + pub active: usize, + pub is_focus: bool, + pub children: Vec, +} + +impl EditorTabInfo { + pub fn to_data( + &self, + data: MainSplitData, + split: SplitId, + ) -> RwSignal { + let editor_tab_id = EditorTabId::next(); + let editor_tab_data = { + let cx = data.scope.create_child(); + let editor_tab_data = EditorTabData { + scope: cx, + editor_tab_id, + split, + active: self.active, + children: self + .children + .iter() + .map(|child| { + ( + cx.create_rw_signal(0), + cx.create_rw_signal(Rect::ZERO), + child.to_data(data.clone(), editor_tab_id), + ) + }) + .collect(), + layout_rect: Rect::ZERO, + window_origin: Point::ZERO, + locations: cx.create_rw_signal(im::Vector::new()), + current_location: cx.create_rw_signal(0), + }; + cx.create_rw_signal(editor_tab_data) + }; + if self.is_focus { + data.active_editor_tab.set(Some(editor_tab_id)); + } + data.editor_tabs.update(|editor_tabs| { + editor_tabs.insert(editor_tab_id, editor_tab_data); + }); + editor_tab_data + } +} + +pub enum EditorTabChildSource { + Editor { path: PathBuf, doc: Rc }, + DiffEditor { left: Rc, right: Rc }, + NewFileEditor, + Settings, + ThemeColorSettings, + Keymap, + Volt(VoltID), +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum EditorTabChild { + Editor(EditorId), + DiffEditor(DiffEditorId), + Settings(SettingsId), + ThemeColorSettings(ThemeColorSettingsId), + Keymap(KeymapId), + Volt(VoltViewId, VoltID), +} + +#[derive(PartialEq)] +pub struct EditorTabChildViewInfo { + pub icon: String, + pub color: Option, + pub name: String, + pub path: Option, + pub confirmed: Option>, + pub is_pristine: bool, +} + +impl EditorTabChild { + pub fn id(&self) -> u64 { + match self { + EditorTabChild::Editor(id) => id.to_raw(), + EditorTabChild::DiffEditor(id) => id.to_raw(), + EditorTabChild::Settings(id) => id.to_raw(), + EditorTabChild::ThemeColorSettings(id) => id.to_raw(), + EditorTabChild::Keymap(id) => id.to_raw(), + EditorTabChild::Volt(id, _) => id.to_raw(), + } + } + + pub fn is_settings(&self) -> bool { + matches!(self, EditorTabChild::Settings(_)) + } + + pub fn child_info(&self, data: &WindowTabData) -> EditorTabChildInfo { + match &self { + EditorTabChild::Editor(editor_id) => { + let editor_data = data + .main_split + .editors + .editor_untracked(*editor_id) + .unwrap(); + EditorTabChildInfo::Editor(editor_data.editor_info(data)) + } + EditorTabChild::DiffEditor(diff_editor_id) => { + let diff_editor_data = data + .main_split + .diff_editors + .get_untracked() + .get(diff_editor_id) + .cloned() + .unwrap(); + EditorTabChildInfo::DiffEditor(diff_editor_data.diff_editor_info()) + } + EditorTabChild::Settings(_) => EditorTabChildInfo::Settings, + EditorTabChild::ThemeColorSettings(_) => { + EditorTabChildInfo::ThemeColorSettings + } + EditorTabChild::Keymap(_) => EditorTabChildInfo::Keymap, + EditorTabChild::Volt(_, id) => EditorTabChildInfo::Volt(id.to_owned()), + } + } + + pub fn view_info( + &self, + editors: Editors, + diff_editors: RwSignal>, + plugin: PluginData, + config: ReadSignal>, + ) -> Memo { + match self.clone() { + EditorTabChild::Editor(editor_id) => create_memo(move |_| { + let config = config.get(); + let editor_data = editors.editor(editor_id); + let path = if let Some(editor_data) = editor_data { + let doc = editor_data.doc_signal().get(); + let (content, is_pristine, confirmed) = ( + doc.content.get(), + doc.buffer.with(|b| b.is_pristine()), + editor_data.confirmed, + ); + match content { + DocContent::File { path, .. } => { + Some((path, confirmed, is_pristine)) + } + DocContent::Local => None, + DocContent::History(_) => None, + DocContent::Scratch { name, .. } => { + Some((PathBuf::from(name), confirmed, is_pristine)) + } + } + } else { + None + }; + let (icon, color, name, confirmed, is_pristine) = match path { + Some((ref path, confirmed, is_pritine)) => { + let (svg, color) = config.file_svg(path); + ( + svg, + color, + path.file_name() + .unwrap_or_default() + .to_string_lossy() + .into_owned(), + confirmed, + is_pritine, + ) + } + None => ( + config.ui_svg(LapceIcons::FILE), + Some(config.color(LapceColor::LAPCE_ICON_ACTIVE)), + "local".to_string(), + create_rw_signal(true), + true, + ), + }; + EditorTabChildViewInfo { + icon, + color, + name, + path: path.map(|opt| opt.0), + confirmed: Some(confirmed), + is_pristine, + } + }), + EditorTabChild::DiffEditor(diff_editor_id) => create_memo(move |_| { + let config = config.get(); + let diff_editor_data = diff_editors + .with(|diff_editors| diff_editors.get(&diff_editor_id).cloned()); + let confirmed = diff_editor_data.as_ref().map(|d| d.confirmed); + + let info = diff_editor_data + .map(|diff_editor_data| { + [diff_editor_data.left, diff_editor_data.right].map(|data| { + let (content, is_pristine) = + data.doc_signal().with(|doc| { + ( + doc.content.get(), + doc.buffer.with(|b| b.is_pristine()), + ) + }); + match content { + DocContent::File { path, .. } => { + Some((path, is_pristine)) + } + DocContent::Local => None, + DocContent::History(_) => None, + DocContent::Scratch { name, .. } => { + Some((PathBuf::from(name), is_pristine)) + } + } + }) + }) + .unwrap_or([None, None]); + + let (icon, color, path, is_pristine) = match info { + [Some((path, is_pristine)), None] + | [None, Some((path, is_pristine))] => { + let (svg, color) = config.file_svg(&path); + ( + svg, + color, + format!( + "{} (Diff)", + path.file_name() + .unwrap_or_default() + .to_string_lossy() + ), + is_pristine, + ) + } + [ + Some((left_path, left_is_pristine)), + Some((right_path, right_is_pristine)), + ] => { + let (svg, color) = + config.files_svg(&[&left_path, &right_path]); + let [left_file_name, right_file_name] = + [&left_path, &right_path].map(|path| { + path.file_name() + .unwrap_or_default() + .to_string_lossy() + }); + ( + svg, + color, + format!("{left_file_name} - {right_file_name} (Diff)"), + left_is_pristine && right_is_pristine, + ) + } + [None, None] => ( + config.ui_svg(LapceIcons::FILE), + Some(config.color(LapceColor::LAPCE_ICON_ACTIVE)), + "local".to_string(), + true, + ), + }; + EditorTabChildViewInfo { + icon, + color, + name: path, + path: None, + confirmed, + is_pristine, + } + }), + EditorTabChild::Settings(_) => create_memo(move |_| { + let config = config.get(); + EditorTabChildViewInfo { + icon: config.ui_svg(LapceIcons::SETTINGS), + color: Some(config.color(LapceColor::LAPCE_ICON_ACTIVE)), + name: "Settings".to_string(), + path: None, + confirmed: None, + is_pristine: true, + } + }), + EditorTabChild::ThemeColorSettings(_) => create_memo(move |_| { + let config = config.get(); + EditorTabChildViewInfo { + icon: config.ui_svg(LapceIcons::SYMBOL_COLOR), + color: Some(config.color(LapceColor::LAPCE_ICON_ACTIVE)), + name: "Theme Colors".to_string(), + path: None, + confirmed: None, + is_pristine: true, + } + }), + EditorTabChild::Keymap(_) => create_memo(move |_| { + let config = config.get(); + EditorTabChildViewInfo { + icon: config.ui_svg(LapceIcons::KEYBOARD), + color: Some(config.color(LapceColor::LAPCE_ICON_ACTIVE)), + name: "Keyboard Shortcuts".to_string(), + path: None, + confirmed: None, + is_pristine: true, + } + }), + EditorTabChild::Volt(_, id) => create_memo(move |_| { + let config = config.get(); + let display_name = plugin + .installed + .with(|volts| volts.get(&id).cloned()) + .map(|volt| volt.meta.with(|m| m.display_name.clone())) + .or_else(|| { + plugin.available.volts.with(|volts| { + let volt = volts.get(&id); + volt.map(|volt| { + volt.info.with(|m| m.display_name.clone()) + }) + }) + }) + .unwrap_or_else(|| id.name.clone()); + EditorTabChildViewInfo { + icon: config.ui_svg(LapceIcons::EXTENSIONS), + color: Some(config.color(LapceColor::LAPCE_ICON_ACTIVE)), + name: display_name, + path: None, + confirmed: None, + is_pristine: true, + } + }), + } + } +} + +#[derive(Clone)] +pub struct EditorTabData { + pub scope: Scope, + pub split: SplitId, + pub editor_tab_id: EditorTabId, + pub active: usize, + pub children: Vec<(RwSignal, RwSignal, EditorTabChild)>, + pub window_origin: Point, + pub layout_rect: Rect, + pub locations: RwSignal>, + pub current_location: RwSignal, +} + +impl EditorTabData { + pub fn get_editor( + &self, + editors: Editors, + path: &Path, + ) -> Option<(usize, EditorData)> { + for (i, child) in self.children.iter().enumerate() { + if let (_, _, EditorTabChild::Editor(editor_id)) = child { + if let Some(editor) = editors.editor_untracked(*editor_id) { + let is_path = editor.doc().content.with_untracked(|content| { + if let DocContent::File { path: p, .. } = content { + p == path + } else { + false + } + }); + if is_path { + return Some((i, editor)); + } + } + } + } + None + } + + pub fn get_unconfirmed_editor_tab_child( + &self, + editors: Editors, + diff_editors: &im::HashMap, + ) -> Option<(usize, EditorTabChild)> { + for (i, (_, _, child)) in self.children.iter().enumerate() { + match child { + EditorTabChild::Editor(editor_id) => { + if let Some(editor) = editors.editor_untracked(*editor_id) { + let confirmed = editor.confirmed.get_untracked(); + if !confirmed { + return Some((i, child.clone())); + } + } + } + EditorTabChild::DiffEditor(diff_editor_id) => { + if let Some(diff_editor) = diff_editors.get(diff_editor_id) { + let confirmed = diff_editor.confirmed.get_untracked(); + if !confirmed { + return Some((i, child.clone())); + } + } + } + _ => (), + } + } + None + } + + pub fn tab_info(&self, data: &WindowTabData) -> EditorTabInfo { + EditorTabInfo { + active: self.active, + is_focus: data.main_split.active_editor_tab.get_untracked() + == Some(self.editor_tab_id), + children: self + .children + .iter() + .map(|(_, _, child)| child.child_info(data)) + .collect(), + } + } +} diff --git a/lapce-app/src/file_explorer/data.rs b/lapce-app/src/file_explorer/data.rs new file mode 100644 index 0000000..a4f3ac5 --- /dev/null +++ b/lapce-app/src/file_explorer/data.rs @@ -0,0 +1,681 @@ +use std::{ + borrow::Cow, + collections::HashMap, + ffi::OsStr, + path::{Path, PathBuf}, + rc::Rc, + sync::Arc, +}; + +use floem::{ + action::show_context_menu, + event::EventPropagation, + ext_event::create_ext_action, + keyboard::Modifiers, + menu::{Menu, MenuItem}, + reactive::{ReadSignal, RwSignal, Scope, SignalGet, SignalUpdate, SignalWith}, + views::editor::text::SystemClipboard, +}; +use globset::Glob; +use lapce_core::{ + command::{EditCommand, FocusCommand}, + mode::Mode, + register::Clipboard, +}; +use lapce_rpc::{ + file::{ + Duplicating, FileNodeItem, FileNodeViewKind, Naming, NamingState, NewNode, + Renaming, + }, + proxy::ProxyResponse, +}; + +use crate::{ + command::{CommandExecuted, CommandKind, InternalCommand, LapceCommand}, + config::LapceConfig, + editor::EditorData, + keypress::{KeyPressFocus, condition::Condition}, + main_split::Editors, + window_tab::CommonData, +}; + +enum RenamedPath { + NotRenaming, + NameUnchanged, + Renamed { + current_path: PathBuf, + new_path: PathBuf, + }, +} + +#[derive(Clone, Debug)] +pub struct FileExplorerData { + pub root: RwSignal, + pub naming: RwSignal, + pub naming_editor_data: EditorData, + pub common: Rc, + pub scroll_to_line: RwSignal>, + left_diff_path: RwSignal>, + pub select: RwSignal>, +} + +impl KeyPressFocus for FileExplorerData { + fn get_mode(&self) -> Mode { + Mode::Insert + } + + fn check_condition(&self, condition: Condition) -> bool { + self.naming.with_untracked(Naming::is_accepting_input) + && condition == Condition::ModalFocus + } + + fn run_command( + &self, + command: &LapceCommand, + count: Option, + mods: Modifiers, + ) -> CommandExecuted { + if self.naming.with_untracked(Naming::is_accepting_input) { + match command.kind { + CommandKind::Focus(FocusCommand::ModalClose) => { + self.cancel_naming(); + CommandExecuted::Yes + } + CommandKind::Edit(EditCommand::InsertNewLine) => { + self.finish_naming(); + CommandExecuted::Yes + } + CommandKind::Edit(_) => { + let command_executed = + self.naming_editor_data.run_command(command, count, mods); + + if let Some(new_path) = self.naming_path() { + self.common + .internal_command + .send(InternalCommand::TestPathCreation { new_path }); + } + + command_executed + } + _ => self.naming_editor_data.run_command(command, count, mods), + } + } else { + CommandExecuted::No + } + } + + fn receive_char(&self, c: &str) { + if self.naming.with_untracked(Naming::is_accepting_input) { + self.naming_editor_data.receive_char(c); + + if let Some(new_path) = self.naming_path() { + self.common + .internal_command + .send(InternalCommand::TestPathCreation { new_path }); + } + } + } +} + +impl FileExplorerData { + pub fn new(cx: Scope, editors: Editors, common: Rc) -> Self { + let path = common.workspace.path.clone().unwrap_or_default(); + let root = cx.create_rw_signal(FileNodeItem { + path: path.clone(), + is_dir: true, + read: false, + open: false, + children: HashMap::new(), + children_open_count: 0, + }); + let naming = cx.create_rw_signal(Naming::None); + let naming_editor_data = editors.make_local(cx, common.clone()); + let data = Self { + root, + naming, + naming_editor_data, + common, + scroll_to_line: cx.create_rw_signal(None), + left_diff_path: cx.create_rw_signal(None), + select: cx.create_rw_signal(None), + }; + if data.common.workspace.path.is_some() { + // only fill in the child files if there is open folder + data.toggle_expand(&path); + } + data + } + + /// Reload the file explorer data via reading the root directory. + /// Note that this will not update immediately. + pub fn reload(&self) { + let path = self.root.with_untracked(|root| root.path.clone()); + self.read_dir(&path); + } + + /// Toggle whether the directory is expanded or not. + /// Does nothing if the path does not exist or is not a directory. + pub fn toggle_expand(&self, path: &Path) { + let Some(Some(read)) = self.root.try_update(|root| { + let read = if let Some(node) = root.get_file_node_mut(path) { + if !node.is_dir { + return None; + } + node.open = !node.open; + Some(node.read) + } else { + None + }; + + if Some(true) == read { + root.update_node_count_recursive(path); + } + read + }) else { + return; + }; + + // Read the directory's files if they haven't been read yet + if !read { + self.read_dir(path); + } + } + + pub fn read_dir(&self, path: &Path) { + self.read_dir_cb(path, |_| {}); + } + + /// Read the directory's information and update the file explorer tree. + /// `done : FnOnce(was_read: bool)` is called when the operation is completed, whether success, + /// failure, or ignored. + pub fn read_dir_cb(&self, path: &Path, done: impl FnOnce(bool) + 'static) { + let root = self.root; + let data = self.clone(); + let config = self.common.config; + let send = { + let path = path.to_path_buf(); + create_ext_action(self.common.scope, move |result| { + let Ok(ProxyResponse::ReadDirResponse { mut items }) = result else { + done(false); + return; + }; + + root.update(|root| { + // Get the node for this path, which should already exist if we're calling + // read_dir on it. + if let Some(node) = root.get_file_node_mut(&path) { + // TODO: do not recreate glob every time we read a directory + // Retain only items that are not excluded from view by the configuration + match Glob::new(&config.get().editor.files_exclude) { + Ok(glob) => { + let matcher = glob.compile_matcher(); + items.retain(|i| !matcher.is_match(&i.path)); + } + Err(e) => tracing::error!( + target:"files_exclude", + "Failed to compile glob: {}", + e + ), + } + + node.read = true; + + // Remove paths that no longer exist + let removed_paths: Vec = node + .children + .keys() + .filter(|p| !items.iter().any(|i| &&i.path == p)) + .map(PathBuf::from) + .collect(); + for path in removed_paths { + node.children.remove(&path); + } + + // Reread dirs that were already read and add new paths + for item in items { + if let Some(existing) = node.children.get(&item.path) { + if existing.read { + data.read_dir(&existing.path); + } + } else { + node.children.insert(item.path.clone(), item); + } + } + } + root.update_node_count_recursive(&path); + }); + + done(true); + }) + }; + + // Ask the proxy for the directory information + self.common.proxy.read_dir(path.to_path_buf(), send); + } + + /// Returns `true` if `path` exists in the file explorer tree and is a directory, `false` + /// otherwise. + fn is_dir(&self, path: &Path) -> bool { + self.root.with_untracked(|root| { + root.get_file_node(path).is_some_and(|node| node.is_dir) + }) + } + + /// The current path that we're renaming to / creating or duplicating a node at. + /// Note: returns `None` when renaming if the file name has not changed. + fn naming_path(&self) -> Option { + self.naming.with_untracked(|naming| match naming { + Naming::None => None, + Naming::Renaming(_) => { + if let RenamedPath::Renamed { new_path, .. } = self.renamed_path() { + Some(new_path) + } else { + None + } + } + Naming::NewNode(n) => { + let relative_path = self.naming_editor_data.text(); + let relative_path: Cow = match relative_path.slice_to_cow(..) + { + Cow::Borrowed(path) => Cow::Borrowed(path.as_ref()), + Cow::Owned(path) => Cow::Owned(path.into()), + }; + + Some(n.base_path.join(relative_path)) + } + Naming::Duplicating(d) => { + let relative_path = self.naming_editor_data.text(); + let relative_path: Cow = match relative_path.slice_to_cow(..) + { + Cow::Borrowed(path) => Cow::Borrowed(path.as_ref()), + Cow::Owned(path) => Cow::Owned(path.into()), + }; + + let new_path = + d.path.parent().unwrap_or("".as_ref()).join(relative_path); + + Some(new_path) + } + }) + } + + /// If there is an in progress rename and the user has entered a path that differs from the + /// current path, gets the current and new paths of the renamed node. + fn renamed_path(&self) -> RenamedPath { + self.naming.with_untracked(|naming| { + if let Some(current_path) = naming.as_renaming().map(|x| &x.path) { + let current_file_name = current_path.file_name().unwrap_or_default(); + // `new_relative_path` is the new path relative to the parent directory, unless the + // user has entered an absolute path. + let new_relative_path = self.naming_editor_data.text(); + + let new_relative_path: Cow = + match new_relative_path.slice_to_cow(..) { + Cow::Borrowed(path) => Cow::Borrowed(path.as_ref()), + Cow::Owned(path) => Cow::Owned(path.into()), + }; + + if new_relative_path == current_file_name { + RenamedPath::NameUnchanged + } else { + let new_path = current_path + .parent() + .unwrap_or("".as_ref()) + .join(new_relative_path); + + RenamedPath::Renamed { + current_path: current_path.to_owned(), + new_path, + } + } + } else { + RenamedPath::NotRenaming + } + }) + } + + /// If a naming is in progress and the user has entered a valid path, send the request to + /// actually perform the change. + pub fn finish_naming(&self) { + match self.naming.get_untracked() { + Naming::None => {} + Naming::Renaming(_) => { + let renamed_path = self.renamed_path(); + match renamed_path { + // Should not occur + RenamedPath::NotRenaming => {} + RenamedPath::NameUnchanged => { + self.cancel_naming(); + } + RenamedPath::Renamed { + current_path, + new_path, + } => { + self.common.internal_command.send( + InternalCommand::FinishRenamePath { + current_path, + new_path, + }, + ); + } + } + } + Naming::NewNode(n) => { + let Some(path) = self.naming_path() else { + return; + }; + + self.common + .internal_command + .send(InternalCommand::FinishNewNode { + is_dir: n.is_dir, + path, + }); + } + Naming::Duplicating(d) => { + let Some(path) = self.naming_path() else { + return; + }; + + self.common.internal_command.send( + InternalCommand::FinishDuplicate { + source: d.path.to_owned(), + path, + }, + ); + } + } + } + + /// Closes the naming text box without applying the effect. + pub fn cancel_naming(&self) { + self.naming.set(Naming::None); + } + + pub fn click(&self, path: &Path, config: ReadSignal>) { + if self.is_dir(path) { + self.toggle_expand(path); + } else if !config.get_untracked().core.file_explorer_double_click { + self.common + .internal_command + .send(InternalCommand::OpenFile { + path: path.to_path_buf(), + }) + } + } + + pub fn reveal_in_file_tree(&self, path: PathBuf) { + let done = self + .root + .try_update(|root| { + // the directories in which the file are located are all read and opened + if root.get_file_node(&path).is_some() { + for current_path in path.ancestors() { + if let Some(file) = root.get_file_node_mut(current_path) { + if file.is_dir { + file.open = true; + } + } + } + root.update_node_count_recursive(&path); + true + } else { + // read and open the directories in which the file are located + let mut read_dir = None; + // Whether the file is in the workspace + let mut exist = false; + for current_path in path.ancestors() { + if let Some(file) = root.get_file_node_mut(current_path) { + exist = true; + if file.is_dir { + file.open = true; + if !file.read { + read_dir = Some(current_path.to_path_buf()) + } + } + break; + } + } + if let (true, Some(dir)) = (exist, read_dir) { + let explorer = self.clone(); + let select_path = path.clone(); + self.read_dir_cb(&dir, move |_| { + explorer.reveal_in_file_tree(select_path); + }) + } + false + } + }) + .unwrap_or(false); + if done { + let (found, line) = + self.root.with_untracked(|x| x.find_file_at_line(&path)); + if found { + self.scroll_to_line.set(Some(line)); + self.select.set(Some(FileNodeViewKind::Path(path))); + } + } + } + + pub fn double_click( + &self, + path: &Path, + config: ReadSignal>, + ) -> EventPropagation { + if self.is_dir(path) { + EventPropagation::Continue + } else if config.get_untracked().core.file_explorer_double_click { + self.common.internal_command.send( + InternalCommand::OpenAndConfirmedFile { + path: path.to_path_buf(), + }, + ); + EventPropagation::Stop + } else { + self.common + .internal_command + .send(InternalCommand::MakeConfirmed); + EventPropagation::Stop + } + } + + pub fn secondary_click(&self, path: &Path) { + let common = self.common.clone(); + let path_a = path.to_owned(); + let left_diff_path = self.left_diff_path; + // TODO: should we just pass is_dir into secondary click? + let is_dir = self.is_dir(path); + + let Some(workspace_path) = self.common.workspace.path.as_ref() else { + // There is no context menu if we are not in a workspace + return; + }; + + let is_workspace = path == workspace_path; + + let base_path_a = if is_dir { + Some(path_a.clone()) + } else { + path_a.parent().map(ToOwned::to_owned) + }; + let base_path_a = base_path_a.as_ref().unwrap_or(workspace_path); + + let mut menu = Menu::new(""); + + let base_path = base_path_a.clone(); + let data = self.clone(); + let naming = self.naming; + menu = menu.entry(MenuItem::new("New File").action(move || { + let base_path_b = &base_path; + let base_path = base_path.clone(); + data.read_dir_cb(base_path_b, move |was_read| { + if !was_read { + tracing::warn!( + "Failed to read directory, avoiding creating node in: {:?}", + base_path + ); + return; + } + + naming.set(Naming::NewNode(NewNode { + state: NamingState::Naming, + base_path: base_path.clone(), + is_dir: false, + editor_needs_reset: true, + })); + }); + })); + + let base_path = base_path_a.clone(); + let data = self.clone(); + let naming = self.naming; + menu = menu.entry(MenuItem::new("New Directory").action(move || { + let base_path_b = &base_path; + let base_path = base_path.clone(); + data.read_dir_cb(base_path_b, move |was_read| { + if !was_read { + tracing::warn!( + "Failed to read directory, avoiding creating node in: {:?}", + base_path + ); + return; + } + + naming.set(Naming::NewNode(NewNode { + state: NamingState::Naming, + base_path: base_path.clone(), + is_dir: true, + editor_needs_reset: true, + })); + }) + })); + + menu = menu.separator(); + + // TODO: there are situations where we can open the file explorer to remote files + if !common.workspace.kind.is_remote() { + let path = path_a.clone(); + #[cfg(not(target_os = "macos"))] + let title = "Reveal in system file explorer"; + #[cfg(target_os = "macos")] + let title = "Reveal in Finder"; + menu = menu.entry(MenuItem::new(title).action(move || { + let path = path.parent().unwrap_or(&path); + if !path.exists() { + return; + } + + if let Err(err) = open::that(path) { + tracing::error!( + "Failed to reveal file in system file explorer: {}", + err + ); + } + })); + } + + if !is_workspace { + let path = path_a.clone(); + menu = menu.entry(MenuItem::new("Rename").action(move || { + naming.set(Naming::Renaming(Renaming { + state: NamingState::Naming, + path: path.clone(), + editor_needs_reset: true, + })); + })); + + let path = path_a.clone(); + menu = menu.entry(MenuItem::new("Duplicate").action(move || { + naming.set(Naming::Duplicating(Duplicating { + state: NamingState::Naming, + path: path.clone(), + editor_needs_reset: true, + })); + })); + + // TODO: it is common for shift+right click to make 'Move file to trash' an actual + // Delete, which can be useful for large files. + let path = path_a.clone(); + let proxy = common.proxy.clone(); + let trash_text = if is_dir { + "Move Directory to Trash" + } else { + "Move File to Trash" + }; + menu = menu.entry(MenuItem::new(trash_text).action(move || { + proxy.trash_path(path.clone(), |res| { + if let Err(err) = res { + tracing::warn!("Failed to trash path: {:?}", err); + } + }) + })); + } + + menu = menu.separator(); + + let path = path_a.clone(); + menu = menu.entry(MenuItem::new("Copy Path").action(move || { + let mut clipboard = SystemClipboard::new(); + clipboard.put_string(path.to_string_lossy()); + })); + + let path = path_a.clone(); + let workspace = common.workspace.clone(); + menu = menu.entry(MenuItem::new("Copy Relative Path").action(move || { + let relative_path = if let Some(workspace_path) = &workspace.path { + path.strip_prefix(workspace_path).unwrap_or(&path) + } else { + path.as_ref() + }; + + let mut clipboard = SystemClipboard::new(); + clipboard.put_string(relative_path.to_string_lossy()); + })); + + menu = menu.separator(); + + let path = path_a.clone(); + menu = menu.entry( + MenuItem::new("Select for Compare") + .action(move || left_diff_path.set(Some(path.clone()))), + ); + + if let Some(left_path) = self.left_diff_path.get_untracked() { + let common = self.common.clone(); + let right_path = path_a.to_owned(); + + menu = menu.entry(MenuItem::new("Compare with Selected").action( + move || { + common + .internal_command + .send(InternalCommand::OpenDiffFiles { + left_path: left_path.clone(), + right_path: right_path.clone(), + }) + }, + )) + } + + menu = menu.separator(); + + let internal_command = common.internal_command; + menu = menu.entry(MenuItem::new("Refresh").action(move || { + internal_command.send(InternalCommand::ReloadFileExplorer); + })); + + show_context_menu(menu, None); + } + + pub fn middle_click(&self, path: &Path) -> EventPropagation { + if self.is_dir(path) { + EventPropagation::Continue + } else { + self.common + .internal_command + .send(InternalCommand::OpenFileInNewTab { + path: path.to_path_buf(), + }); + EventPropagation::Stop + } + } +} diff --git a/lapce-app/src/file_explorer/mod.rs b/lapce-app/src/file_explorer/mod.rs new file mode 100644 index 0000000..14b4d9b --- /dev/null +++ b/lapce-app/src/file_explorer/mod.rs @@ -0,0 +1,3 @@ +pub mod data; +pub mod node; +pub mod view; diff --git a/lapce-app/src/file_explorer/node.rs b/lapce-app/src/file_explorer/node.rs new file mode 100644 index 0000000..68b2386 --- /dev/null +++ b/lapce-app/src/file_explorer/node.rs @@ -0,0 +1,38 @@ +use floem::views::VirtualVector; +use lapce_rpc::file::{FileNodeItem, FileNodeViewData, Naming}; + +pub struct FileNodeVirtualList { + file_node_item: FileNodeItem, + naming: Naming, +} + +impl FileNodeVirtualList { + pub fn new(file_node_item: FileNodeItem, naming: Naming) -> Self { + Self { + file_node_item, + naming, + } + } +} + +impl VirtualVector for FileNodeVirtualList { + fn total_len(&self) -> usize { + self.file_node_item.children_open_count + 1 + } + + fn slice( + &mut self, + range: std::ops::Range, + ) -> impl Iterator { + let naming = &self.naming; + let root = &self.file_node_item; + + let min = range.start; + let max = range.end; + let mut view_items = Vec::new(); + + root.append_view_slice(&mut view_items, naming, min, max, 0, 1); + + view_items.into_iter() + } +} diff --git a/lapce-app/src/file_explorer/view.rs b/lapce-app/src/file_explorer/view.rs new file mode 100644 index 0000000..1960e46 --- /dev/null +++ b/lapce-app/src/file_explorer/view.rs @@ -0,0 +1,620 @@ +use std::{path::Path, rc::Rc, sync::Arc}; + +use floem::{ + View, + event::{Event, EventListener}, + kurbo::Rect, + peniko::Color, + reactive::{ + ReadSignal, RwSignal, SignalGet, SignalUpdate, SignalWith, create_rw_signal, + }, + style::{AlignItems, CursorStyle, Position, Style}, + text::Style as FontStyle, + views::{ + Container, Decorators, container, dyn_stack, label, scroll, stack, svg, + virtual_stack, + }, +}; +use lapce_core::selection::Selection; +use lapce_rpc::{ + file::{FileNodeViewData, FileNodeViewKind, Naming}, + source_control::FileDiffKind, +}; +use lapce_xi_rope::Rope; + +use super::{data::FileExplorerData, node::FileNodeVirtualList}; +use crate::{ + app::clickable_icon, + command::InternalCommand, + config::{LapceConfig, color::LapceColor, icon::LapceIcons}, + editor_tab::{EditorTabChild, EditorTabData}, + panel::{ + data::PanelSection, kind::PanelKind, position::PanelPosition, + view::PanelBuilder, + }, + plugin::PluginData, + source_control::SourceControlData, + text_input::TextInputBuilder, + window_tab::{Focus, WindowTabData}, +}; + +/// Blends `foreground` with `background`. +/// +/// Uses the alpha channel from `foreground` - if `foreground` is opaque, `foreground` will be +/// returned unchanged. +/// +/// The result is always opaque regardless of the transparency of the inputs. +fn blend_colors(background: Color, foreground: Color) -> Color { + let background = background.to_rgba8(); + let foreground = foreground.to_rgba8(); + let a: u16 = foreground.a.into(); + let [r, g, b] = [ + [background.r, foreground.r], + [background.g, foreground.g], + [background.b, foreground.b], + ] + .map(|x| x.map(u16::from)) + .map(|[b, f]| (a * f + (255 - a) * b) / 255) + .map(|x| x as u8); + + Color::from_rgba8(r, g, b, 255) +} + +pub fn file_explorer_panel( + window_tab_data: Rc, + position: PanelPosition, +) -> impl View { + let config = window_tab_data.common.config; + let data = window_tab_data.file_explorer.clone(); + let source_control = window_tab_data.source_control.clone(); + PanelBuilder::new(config, position) + .add_height_style( + "Open Editors", + 150.0, + container(open_editors_view(window_tab_data.clone())) + .style(|s| s.size_full()), + window_tab_data.panel.section_open(PanelSection::OpenEditor), + move |s| s.apply_if(!config.get().ui.open_editors_visible, |s| s.hide()), + ) + .add( + "File Explorer", + container(file_explorer_view(data, source_control)) + .style(|s| s.size_full()), + window_tab_data + .panel + .section_open(PanelSection::FileExplorer), + ) + .build() + .debug_name("File Explorer Panel") +} + +/// Initialize the file explorer's naming (renaming, creating, etc.) editor with the given path. +fn initialize_naming_editor_with_path(data: &FileExplorerData, path: &Path) { + let file_name = path.file_name().unwrap_or_default().to_string_lossy(); + // Start with the part of the file or directory name before the extension + // selected. + let selection_end = { + let without_leading_dot = file_name.strip_prefix('.').unwrap_or(&file_name); + let idx = without_leading_dot + .find('.') + .unwrap_or(without_leading_dot.len()); + + idx + file_name.len() - without_leading_dot.len() + }; + + initialize_naming_editor(data, &file_name, Some(selection_end)); +} + +fn initialize_naming_editor( + data: &FileExplorerData, + text: &str, + selection_end: Option, +) { + let text = Rope::from(text); + let selection_end = selection_end.unwrap_or(text.len()); + + let doc = data.naming_editor_data.doc(); + doc.reload(text, true); + data.naming_editor_data + .cursor() + .update(|cursor| cursor.set_insert(Selection::region(0, selection_end))); + + data.naming + .update(|naming| naming.set_editor_needs_reset(false)); +} + +fn file_node_text_color( + config: ReadSignal>, + node: FileNodeViewData, + source_control: SourceControlData, +) -> Color { + let diff = source_control.file_diffs.with(|file_diffs| { + let FileNodeViewKind::Path(path) = &node.kind else { + return None; + }; + + if node.is_dir { + file_diffs + .keys() + .find(|p| p.as_path().starts_with(path)) + .map(|_| FileDiffKind::Modified) + } else { + file_diffs.get(path).map(|(diff, _)| diff.kind()) + } + }); + + let color = match diff { + Some(FileDiffKind::Modified | FileDiffKind::Renamed) => { + LapceColor::SOURCE_CONTROL_MODIFIED + } + Some(FileDiffKind::Added) => LapceColor::SOURCE_CONTROL_ADDED, + Some(FileDiffKind::Deleted) => LapceColor::SOURCE_CONTROL_REMOVED, + None => LapceColor::PANEL_FOREGROUND, + }; + + config.get().color(color) +} + +fn file_node_text_view( + data: FileExplorerData, + node: FileNodeViewData, + source_control: SourceControlData, +) -> impl View { + let config = data.common.config; + let ui_line_height = data.common.ui_line_height; + + match node.kind.clone() { + FileNodeViewKind::Path(path) => { + if node.is_root { + let file = path.clone(); + container(( + label(move || { + file.file_name() + .map(|f| f.to_string_lossy().to_string()) + .unwrap_or_default() + }) + .style(move |s| { + s.height(ui_line_height.get()) + .color(file_node_text_color( + config, + node.clone(), + source_control.clone(), + )) + .padding_right(5.0) + .selectable(false) + }), + label(move || path.to_string_lossy().to_string()).style( + move |s| { + s.height(ui_line_height.get()) + .color( + config + .get() + .color(LapceColor::PANEL_FOREGROUND_DIM), + ) + .selectable(false) + }, + ), + )) + } else { + container( + label(move || { + path.file_name() + .map(|f| f.to_string_lossy().to_string()) + .unwrap_or_default() + }) + .style(move |s| { + s.height(ui_line_height.get()) + .color(file_node_text_color( + config, + node.clone(), + source_control.clone(), + )) + .selectable(false) + }), + ) + } + } + FileNodeViewKind::Renaming { path, err } => { + if data.naming.with_untracked(Naming::editor_needs_reset) { + initialize_naming_editor_with_path(&data, &path); + } + + file_node_input_view(data, err.clone()) + } + FileNodeViewKind::Naming { err } => { + if data.naming.with_untracked(Naming::editor_needs_reset) { + initialize_naming_editor(&data, "", None); + } + + file_node_input_view(data, err.clone()) + } + FileNodeViewKind::Duplicating { source, err } => { + if data.naming.with_untracked(Naming::editor_needs_reset) { + initialize_naming_editor_with_path(&data, &source); + } + + file_node_input_view(data, err.clone()) + } + } +} + +/// Input used for naming a file/directory +fn file_node_input_view(data: FileExplorerData, err: Option) -> Container { + let ui_line_height = data.common.ui_line_height; + + let naming_editor_data = data.naming_editor_data.clone(); + let text_input_file_explorer_data = data.clone(); + let focus = data.common.focus; + let config = data.common.config; + + let is_focused = move || { + focus.with_untracked(|focus| focus == &Focus::Panel(PanelKind::FileExplorer)) + }; + let text_input_view = TextInputBuilder::new() + .is_focused(is_focused) + .key_focus(text_input_file_explorer_data) + .build_editor(naming_editor_data.clone()) + .on_event_stop(EventListener::FocusLost, move |_| { + data.finish_naming(); + data.naming.set(Naming::None); + }) + .style(move |s| { + s.width_full() + .height(ui_line_height.get()) + .padding(0.0) + .margin(0.0) + .border_radius(6.0) + .border(1.0) + .border_color(config.get().color(LapceColor::LAPCE_BORDER)) + }); + + let text_input_id = text_input_view.id(); + text_input_id.request_focus(); + + if let Some(err) = err { + container( + stack(( + text_input_view, + label(move || err.clone()).style(move |s| { + let config = config.get(); + + let editor_background_color = + config.color(LapceColor::PANEL_CURRENT_BACKGROUND); + let error_background_color = + config.color(LapceColor::ERROR_LENS_ERROR_BACKGROUND); + + let background_color = blend_colors( + editor_background_color, + error_background_color, + ); + + s.position(Position::Absolute) + .inset_top(ui_line_height.get()) + .width_full() + .color(config.color(LapceColor::ERROR_LENS_ERROR_FOREGROUND)) + .background(background_color) + .z_index(100) + }), + )) + .style(|s| s.flex_grow(1.0)), + ) + } else { + container(text_input_view) + } + .style(move |s| s.width_full()) +} + +fn file_explorer_view( + data: FileExplorerData, + source_control: SourceControlData, +) -> impl View { + let root = data.root; + let ui_line_height = data.common.ui_line_height; + let config = data.common.config; + let naming = data.naming; + let scroll_to_line = data.scroll_to_line; + let select = data.select; + let secondary_click_data = data.clone(); + let scroll_rect = create_rw_signal(Rect::ZERO); + + scroll( + virtual_stack( + move || FileNodeVirtualList::new(root.get(), data.naming.get()), + move |node| (node.kind.clone(), node.is_dir, node.open, node.level), + move |node| { + let level = node.level; + let data = data.clone(); + let click_data = data.clone(); + let double_click_data = data.clone(); + let secondary_click_data = data.clone(); + let aux_click_data = data.clone(); + let kind = node.kind.clone(); + let open = node.open; + let is_dir = node.is_dir; + + let view = stack(( + svg(move || { + let config = config.get(); + let svg_str = match open { + true => LapceIcons::ITEM_OPENED, + false => LapceIcons::ITEM_CLOSED, + }; + config.ui_svg(svg_str) + }) + .style(move |s| { + let config = config.get(); + let size = config.ui.icon_size() as f32; + + let color = if is_dir { + config.color(LapceColor::LAPCE_ICON_ACTIVE) + } else { + Color::TRANSPARENT + }; + s.size(size, size) + .flex_shrink(0.0) + .margin_left(10.0) + .color(color) + }), + { + let kind = kind.clone(); + let kind_for_style = kind.clone(); + // TODO: use the current naming input as the path for the file svg + svg(move || { + let config = config.get(); + if is_dir { + let svg_str = match open { + true => LapceIcons::DIRECTORY_OPENED, + false => LapceIcons::DIRECTORY_CLOSED, + }; + config.ui_svg(svg_str) + } else if let Some(path) = kind.path() { + config.file_svg(path).0 + } else { + config.ui_svg(LapceIcons::FILE) + } + }) + .style(move |s| { + let config = config.get(); + let size = config.ui.icon_size() as f32; + + s.size(size, size) + .flex_shrink(0.0) + .margin_horiz(6.0) + .apply_if(is_dir, |s| { + s.color( + config.color(LapceColor::LAPCE_ICON_ACTIVE), + ) + }) + .apply_if(!is_dir, |s| { + s.apply_opt( + kind_for_style + .path() + .and_then(|p| config.file_svg(p).1), + Style::color, + ) + }) + }) + }, + file_node_text_view(data, node, source_control.clone()), + )) + .style({ + let kind = kind.clone(); + move |s| { + s.padding_right(15.0) + .min_width_full() + .padding_left((level * 10) as f32) + .align_items(AlignItems::Center) + .hover(|s| { + s.background( + config + .get() + .color(LapceColor::PANEL_HOVERED_BACKGROUND), + ) + .cursor(CursorStyle::Pointer) + }) + .apply_if( + select.get().map(|x| x == kind).unwrap_or_default(), + |x| { + x.background( + config.get().color( + LapceColor::PANEL_CURRENT_BACKGROUND, + ), + ) + }, + ) + } + }) + .debug_name("file item"); + + // Only handle click events if we are not naming the file node + if let FileNodeViewKind::Path(path) = &kind { + let click_path = path.clone(); + let double_click_path = path.clone(); + let secondary_click_path = path.clone(); + let aux_click_path = path.clone(); + view.on_click_stop({ + let kind = kind.clone(); + move |_| { + click_data.click(&click_path, config); + select.update(|x| *x = Some(kind.clone())); + } + }) + .on_double_click({ + move |_| { + double_click_data + .double_click(&double_click_path, config) + } + }) + .on_secondary_click_stop(move |_| { + secondary_click_data.secondary_click(&secondary_click_path); + }) + .on_event_stop( + EventListener::PointerDown, + move |event| { + if let Event::PointerDown(pointer_event) = event { + if pointer_event.button.is_auxiliary() { + aux_click_data.middle_click(&aux_click_path); + } + } + }, + ) + } else { + view + } + }, + ) + .item_size_fixed(move || ui_line_height.get()) + .style(|s| s.absolute().flex_col().min_width_full()), + ) + .style(|s| s.absolute().size_full().line_height(1.8)) + .on_secondary_click_stop(move |_| { + if let Naming::None = naming.get_untracked() { + if let Some(path) = &secondary_click_data.common.workspace.path { + secondary_click_data.secondary_click(path); + } + } + }) + .on_resize(move |rect| { + scroll_rect.set(rect); + }) + .scroll_to(move || { + if let Some(line) = scroll_to_line.get() { + let line_height = ui_line_height.get_untracked(); + Some( + ( + 0.0, + line * line_height - scroll_rect.get_untracked().height() / 2.0, + ) + .into(), + ) + } else { + None + } + }) +} + +fn open_editors_view(window_tab_data: Rc) -> impl View { + let diff_editors = window_tab_data.main_split.diff_editors; + let editors = window_tab_data.main_split.editors; + let editor_tabs = window_tab_data.main_split.editor_tabs; + let config = window_tab_data.common.config; + let internal_command = window_tab_data.common.internal_command; + let active_editor_tab = window_tab_data.main_split.active_editor_tab; + let plugin = window_tab_data.plugin.clone(); + + let child_view = move |plugin: PluginData, + editor_tab: RwSignal, + child_index: RwSignal, + child: EditorTabChild| { + let editor_tab_id = + editor_tab.with_untracked(|editor_tab| editor_tab.editor_tab_id); + let child_for_close = child.clone(); + let info = child.view_info(editors, diff_editors, plugin, config); + let hovered = create_rw_signal(false); + + stack(( + clickable_icon( + move || { + if hovered.get() || info.with(|info| info.is_pristine) { + LapceIcons::CLOSE + } else { + LapceIcons::UNSAVED + } + }, + move || { + let editor_tab_id = + editor_tab.with_untracked(|t| t.editor_tab_id); + internal_command.send(InternalCommand::EditorTabChildClose { + editor_tab_id, + child: child_for_close.clone(), + }); + }, + || false, + || false, + || "Close", + config, + ) + .on_event_stop(EventListener::PointerEnter, move |_| { + hovered.set(true); + }) + .on_event_stop(EventListener::PointerLeave, move |_| { + hovered.set(false); + }) + .on_event_stop(EventListener::PointerDown, |_| {}) + .style(|s| s.margin_left(10.0)), + container(svg(move || info.with(|info| info.icon.clone())).style( + move |s| { + let size = config.get().ui.icon_size() as f32; + s.size(size, size) + .apply_opt(info.with(|info| info.color), |s, c| s.color(c)) + }, + )) + .style(|s| s.padding_horiz(6.0)), + label(move || info.with(|info| info.name.clone())).style(move |s| { + s.apply_if( + !info + .with(|info| info.confirmed) + .map(|confirmed| confirmed.get()) + .unwrap_or(true), + |s| s.font_style(FontStyle::Italic), + ) + }), + )) + .style(move |s| { + let config = config.get(); + s.items_center() + .width_pct(100.0) + .cursor(CursorStyle::Pointer) + .apply_if( + active_editor_tab.get() == Some(editor_tab_id) + && editor_tab.with(|editor_tab| editor_tab.active) + == child_index.get(), + |s| { + s.background( + config.color(LapceColor::PANEL_CURRENT_BACKGROUND), + ) + }, + ) + .hover(|s| { + s.background(config.color(LapceColor::PANEL_HOVERED_BACKGROUND)) + }) + }) + .on_event_cont(EventListener::PointerDown, move |_| { + editor_tab.update(|editor_tab| { + editor_tab.active = child_index.get_untracked(); + }); + active_editor_tab.set(Some(editor_tab_id)); + }) + }; + + scroll( + dyn_stack( + move || editor_tabs.get().into_iter().enumerate(), + move |(index, (editor_tab_id, _))| (*index, *editor_tab_id), + move |(index, (_, editor_tab))| { + let plugin = plugin.clone(); + stack(( + label(move || format!("Group {}", index + 1)) + .style(|s| s.margin_left(10.0)), + dyn_stack( + move || editor_tab.get().children, + move |(_, _, child)| child.id(), + move |(child_index, _, child)| { + child_view( + plugin.clone(), + editor_tab, + child_index, + child, + ) + }, + ) + .style(|s| s.flex_col().width_pct(100.0)), + )) + .style(|s| s.flex_col()) + }, + ) + .style(|s| s.flex_col().width_pct(100.0)), + ) + .style(|s| s.absolute().size_full().line_height(1.8)) + .debug_name("Open Editors") +} diff --git a/lapce-app/src/find.rs b/lapce-app/src/find.rs new file mode 100644 index 0000000..293ee21 --- /dev/null +++ b/lapce-app/src/find.rs @@ -0,0 +1,545 @@ +use std::cmp::{max, min}; + +use floem::{ + prelude::SignalTrack, + reactive::{RwSignal, Scope, SignalGet, SignalUpdate, SignalWith}, +}; +use lapce_core::{ + selection::{SelRegion, Selection}, + word::WordCursor, +}; +use lapce_xi_rope::{ + Cursor, Interval, Rope, + find::{CaseMatching, find, is_multiline_regex}, +}; +use regex::{Regex, RegexBuilder}; +use serde::{Deserialize, Serialize}; + +const REGEX_SIZE_LIMIT: usize = 1000000; + +/// Indicates what changed in the find state. +#[derive(PartialEq, Debug, Clone)] +pub enum FindProgress { + /// Incremental find is done/not running. + Ready, + + /// The find process just started. + Started, + + /// Incremental find is in progress. Keeps tracked of already searched range. + InProgress(Selection), +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct FindStatus { + /// Identifier for the current search query. + id: usize, + + /// The current search query. + chars: Option, + + /// Whether the active search is case matching. + case_sensitive: Option, + + /// Whether the search query is considered as regular expression. + is_regex: Option, + + /// Query only matches whole words. + whole_words: Option, + + /// Total number of matches. + matches: usize, + + /// Line numbers which have find results. + lines: Vec, +} + +#[derive(Clone)] +pub struct FindSearchString { + pub content: String, + pub regex: Option, +} + +#[derive(Clone)] +pub struct Find { + pub rev: RwSignal, + /// If the find is shown + pub visual: RwSignal, + /// The currently active search string. + pub search_string: RwSignal>, + /// The case matching setting for the currently active search. + pub case_matching: RwSignal, + /// Query matches only whole words. + pub whole_words: RwSignal, + /// The search query should be considered as regular expression. + pub is_regex: RwSignal, + /// replace editor is shown + pub replace_active: RwSignal, + /// replace editor is focused + pub replace_focus: RwSignal, + /// Triggered by changes in the search string + pub triggered_by_changes: RwSignal, +} + +impl Find { + pub fn new(cx: Scope) -> Self { + let find = Self { + rev: cx.create_rw_signal(0), + visual: cx.create_rw_signal(false), + search_string: cx.create_rw_signal(None), + case_matching: cx.create_rw_signal(CaseMatching::CaseInsensitive), + whole_words: cx.create_rw_signal(false), + is_regex: cx.create_rw_signal(false), + replace_active: cx.create_rw_signal(false), + replace_focus: cx.create_rw_signal(false), + triggered_by_changes: cx.create_rw_signal(false), + }; + + { + let find = find.clone(); + cx.create_effect(move |_| { + find.is_regex.with(|_| ()); + let s = find.search_string.with_untracked(|s| { + if let Some(s) = s.as_ref() { + s.content.clone() + } else { + "".to_string() + } + }); + if !s.is_empty() { + find.set_find(&s); + } + }); + } + + { + let find = find.clone(); + cx.create_effect(move |_| { + find.search_string.track(); + find.case_matching.track(); + find.whole_words.track(); + find.rev.update(|rev| { + *rev += 1; + }); + }); + } + + find + } + + /// Returns `true` if case sensitive, otherwise `false` + pub fn case_sensitive(&self, tracked: bool) -> bool { + match if tracked { + self.case_matching.get() + } else { + self.case_matching.get_untracked() + } { + CaseMatching::Exact => true, + CaseMatching::CaseInsensitive => false, + } + } + + /// Sets find case sensitivity. + pub fn set_case_sensitive(&self, case_sensitive: bool) { + if self.case_sensitive(false) == case_sensitive { + return; + } + + let case_matching = if case_sensitive { + CaseMatching::Exact + } else { + CaseMatching::CaseInsensitive + }; + self.case_matching.set(case_matching); + } + + pub fn set_find(&self, search_string: &str) { + if search_string.is_empty() { + self.search_string.set(None); + return; + } + + if !self.visual.get_untracked() { + self.visual.set(true); + } + + let is_regex = self.is_regex.get_untracked(); + + let search_string_unchanged = self.search_string.with_untracked(|search| { + if let Some(s) = search { + s.content == search_string && s.regex.is_some() == is_regex + } else { + false + } + }); + + if search_string_unchanged { + return; + } + + // create regex from untrusted input + let regex = match is_regex { + false => None, + true => RegexBuilder::new(search_string) + .size_limit(REGEX_SIZE_LIMIT) + .case_insensitive(!self.case_sensitive(false)) + .build() + .ok(), + }; + self.triggered_by_changes.set(true); + self.search_string.set(Some(FindSearchString { + content: search_string.to_string(), + regex, + })); + } + + pub fn next( + &self, + text: &Rope, + offset: usize, + reverse: bool, + wrap: bool, + ) -> Option<(usize, usize)> { + if !self.visual.get_untracked() { + self.visual.set(true); + } + let case_matching = self.case_matching.get_untracked(); + let whole_words = self.whole_words.get_untracked(); + self.search_string.with_untracked( + |search_string| -> Option<(usize, usize)> { + let search_string = search_string.as_ref()?; + if !reverse { + let mut raw_lines = text.lines_raw(offset..text.len()); + let mut find_cursor = Cursor::new(text, offset); + while let Some(start) = find( + &mut find_cursor, + &mut raw_lines, + case_matching, + &search_string.content, + search_string.regex.as_ref(), + ) { + let end = find_cursor.pos(); + + if whole_words + && !Self::is_matching_whole_words(text, start, end) + { + raw_lines = + text.lines_raw(find_cursor.pos()..text.len()); + continue; + } + raw_lines = text.lines_raw(find_cursor.pos()..text.len()); + + if start > offset { + return Some((start, end)); + } + } + if wrap { + let mut raw_lines = text.lines_raw(0..offset); + let mut find_cursor = Cursor::new(text, 0); + while let Some(start) = find( + &mut find_cursor, + &mut raw_lines, + case_matching, + &search_string.content, + search_string.regex.as_ref(), + ) { + let end = find_cursor.pos(); + + if whole_words + && !Self::is_matching_whole_words(text, start, end) + { + raw_lines = + text.lines_raw(find_cursor.pos()..offset); + continue; + } + return Some((start, end)); + } + } + } else { + let mut raw_lines = text.lines_raw(0..offset); + let mut find_cursor = Cursor::new(text, 0); + let mut regions = Vec::new(); + while let Some(start) = find( + &mut find_cursor, + &mut raw_lines, + case_matching, + &search_string.content, + search_string.regex.as_ref(), + ) { + let end = find_cursor.pos(); + raw_lines = text.lines_raw(find_cursor.pos()..offset); + if whole_words + && !Self::is_matching_whole_words(text, start, end) + { + continue; + } + if start < offset { + regions.push((start, end)); + } + } + if !regions.is_empty() { + return Some(regions[regions.len() - 1]); + } + if wrap { + let mut raw_lines = text.lines_raw(offset..text.len()); + let mut find_cursor = Cursor::new(text, offset); + let mut regions = Vec::new(); + while let Some(start) = find( + &mut find_cursor, + &mut raw_lines, + case_matching, + &search_string.content, + search_string.regex.as_ref(), + ) { + let end = find_cursor.pos(); + + if whole_words + && !Self::is_matching_whole_words(text, start, end) + { + raw_lines = + text.lines_raw(find_cursor.pos()..text.len()); + continue; + } + raw_lines = + text.lines_raw(find_cursor.pos()..text.len()); + + if start > offset { + regions.push((start, end)); + } + } + if !regions.is_empty() { + return Some(regions[regions.len() - 1]); + } + } + } + None + }, + ) + } + + /// Checks if the start and end of a match is matching whole words. + fn is_matching_whole_words(text: &Rope, start: usize, end: usize) -> bool { + let mut word_end_cursor = WordCursor::new(text, end - 1); + let mut word_start_cursor = WordCursor::new(text, start + 1); + + if word_start_cursor.prev_code_boundary() != start { + return false; + } + + if word_end_cursor.next_code_boundary() != end { + return false; + } + + true + } + + /// Returns `true` if the search query is a multi-line regex. + pub fn is_multiline_regex(&self) -> bool { + self.search_string.with_untracked(|search| { + if let Some(search) = search.as_ref() { + search.regex.is_some() && is_multiline_regex(&search.content) + } else { + false + } + }) + } + + #[allow(clippy::too_many_arguments)] + pub fn find( + text: &Rope, + search: &FindSearchString, + start: usize, + end: usize, + case_matching: CaseMatching, + whole_words: bool, + include_slop: bool, + occurrences: &mut Selection, + ) { + let search_string = &search.content; + + let slop = if include_slop { + search.content.len() * 2 + } else { + 0 + }; + + // expand region to be able to find occurrences around the region's edges + let expanded_start = max(start, slop) - slop; + let expanded_end = min(end + slop, text.len()); + let from = text + .at_or_prev_codepoint_boundary(expanded_start) + .unwrap_or(0); + let to = text + .at_or_next_codepoint_boundary(expanded_end) + .unwrap_or_else(|| text.len()); + let mut to_cursor = Cursor::new(text, to); + let _ = to_cursor.next_leaf(); + + let sub_text = text.subseq(Interval::new(0, to_cursor.pos())); + let mut find_cursor = Cursor::new(&sub_text, from); + + let mut raw_lines = text.lines_raw(from..to); + + while let Some(start) = find( + &mut find_cursor, + &mut raw_lines, + case_matching, + search_string, + search.regex.as_ref(), + ) { + let end = find_cursor.pos(); + + if whole_words && !Self::is_matching_whole_words(text, start, end) { + raw_lines = text.lines_raw(find_cursor.pos()..to); + continue; + } + + let region = SelRegion::new(start, end, None); + let (_, e) = occurrences.add_range_distinct(region); + // in case of ambiguous search results (e.g. search "aba" in "ababa"), + // the search result closer to the beginning of the file wins + if e != end { + // Skip the search result and keep the occurrence that is closer to + // the beginning of the file. Re-align the cursor to the kept + // occurrence + find_cursor.set(e); + raw_lines = text.lines_raw(find_cursor.pos()..to); + continue; + } + + // in case current cursor matches search result (for example query a* matches) + // all cursor positions, then cursor needs to be increased so that search + // continues at next position. Otherwise, search will result in overflow since + // search will always repeat at current cursor position. + if start == end { + // determine whether end of text is reached and stop search or increase + // cursor manually + if end + 1 >= text.len() { + break; + } else { + find_cursor.set(end + 1); + } + } + + // update line iterator so that line starts at current cursor position + raw_lines = text.lines_raw(find_cursor.pos()..to); + } + } + + /// Execute the search on the provided text in the range provided by `start` and `end`. + pub fn update_find( + &self, + text: &Rope, + start: usize, + end: usize, + include_slop: bool, + occurrences: &mut Selection, + ) { + if self.search_string.with_untracked(|search| search.is_none()) { + return; + } + + let search = self.search_string.get_untracked().unwrap(); + let search_string = &search.content; + // extend the search by twice the string length (twice, because case matching may increase + // the length of an occurrence) + let slop = if include_slop { + search.content.len() * 2 + } else { + 0 + }; + + // expand region to be able to find occurrences around the region's edges + let expanded_start = max(start, slop) - slop; + let expanded_end = min(end + slop, text.len()); + let from = text + .at_or_prev_codepoint_boundary(expanded_start) + .unwrap_or(0); + let to = text + .at_or_next_codepoint_boundary(expanded_end) + .unwrap_or_else(|| text.len()); + let mut to_cursor = Cursor::new(text, to); + let _ = to_cursor.next_leaf(); + + let sub_text = text.subseq(Interval::new(0, to_cursor.pos())); + let mut find_cursor = Cursor::new(&sub_text, from); + + let mut raw_lines = text.lines_raw(from..to); + + let case_matching = self.case_matching.get_untracked(); + let whole_words = self.whole_words.get_untracked(); + while let Some(start) = find( + &mut find_cursor, + &mut raw_lines, + case_matching, + search_string, + search.regex.as_ref(), + ) { + let end = find_cursor.pos(); + + if whole_words && !Self::is_matching_whole_words(text, start, end) { + raw_lines = text.lines_raw(find_cursor.pos()..to); + continue; + } + + let region = SelRegion::new(start, end, None); + let (_, e) = occurrences.add_range_distinct(region); + // in case of ambiguous search results (e.g. search "aba" in "ababa"), + // the search result closer to the beginning of the file wins + if e != end { + // Skip the search result and keep the occurrence that is closer to + // the beginning of the file. Re-align the cursor to the kept + // occurrence + find_cursor.set(e); + raw_lines = text.lines_raw(find_cursor.pos()..to); + continue; + } + + // in case current cursor matches search result (for example query a* matches) + // all cursor positions, then cursor needs to be increased so that search + // continues at next position. Otherwise, search will result in overflow since + // search will always repeat at current cursor position. + if start == end { + // determine whether end of text is reached and stop search or increase + // cursor manually + if end + 1 >= text.len() { + break; + } else { + find_cursor.set(end + 1); + } + } + + // update line iterator so that line starts at current cursor position + raw_lines = text.lines_raw(find_cursor.pos()..to); + } + } +} + +#[derive(Clone)] +pub struct FindResult { + pub find_rev: RwSignal, + pub progress: RwSignal, + pub occurrences: RwSignal, + pub search_string: RwSignal>, + pub case_matching: RwSignal, + pub whole_words: RwSignal, + pub is_regex: RwSignal, +} + +impl FindResult { + pub fn new(cx: Scope) -> Self { + Self { + find_rev: cx.create_rw_signal(0), + progress: cx.create_rw_signal(FindProgress::Started), + occurrences: cx.create_rw_signal(Selection::new()), + search_string: cx.create_rw_signal(None), + case_matching: cx.create_rw_signal(CaseMatching::Exact), + whole_words: cx.create_rw_signal(false), + is_regex: cx.create_rw_signal(false), + } + } + + pub fn reset(&self) { + self.progress.set(FindProgress::Started); + } +} diff --git a/lapce-app/src/focus_text.rs b/lapce-app/src/focus_text.rs new file mode 100644 index 0000000..597bc50 --- /dev/null +++ b/lapce-app/src/focus_text.rs @@ -0,0 +1,287 @@ +use floem::{ + Renderer, View, ViewId, + peniko::{ + Color, + kurbo::{Point, Rect}, + }, + prop_extractor, + reactive::create_effect, + style::{FontFamily, FontSize, LineHeight, Style, TextColor}, + taffy::prelude::NodeId, + text::{Attrs, AttrsList, FamilyOwned, TextLayout, Weight}, +}; + +prop_extractor! { + Extractor { + color: TextColor, + font_size: FontSize, + font_family: FontFamily, + line_height: LineHeight, + } +} + +enum FocusTextState { + Text(String), + FocusColor(Color), + FocusIndices(Vec), +} + +pub fn focus_text( + text: impl Fn() -> String + 'static, + focus_indices: impl Fn() -> Vec + 'static, + focus_color: impl Fn() -> Color + 'static, +) -> FocusText { + let id = ViewId::new(); + + create_effect(move |_| { + let new_text = text(); + id.update_state(FocusTextState::Text(new_text)); + }); + + create_effect(move |_| { + let focus_color = focus_color(); + id.update_state(FocusTextState::FocusColor(focus_color)); + }); + + create_effect(move |_| { + let focus_indices = focus_indices(); + id.update_state(FocusTextState::FocusIndices(focus_indices)); + }); + + FocusText { + id, + text: "".to_string(), + text_layout: None, + focus_color: Color::BLACK, + focus_indices: Vec::new(), + text_node: None, + available_text: None, + available_width: None, + available_text_layout: None, + style: Default::default(), + } +} + +pub struct FocusText { + id: ViewId, + text: String, + text_layout: Option, + focus_color: Color, + focus_indices: Vec, + text_node: Option, + available_text: Option, + available_width: Option, + available_text_layout: Option, + style: Extractor, +} + +impl FocusText { + fn set_text_layout(&mut self) { + let mut attrs = + Attrs::new().color(self.style.color().unwrap_or(Color::BLACK)); + if let Some(font_size) = self.style.font_size() { + attrs = attrs.font_size(font_size); + } + let font_family = self.style.font_family().as_ref().map(|font_family| { + let family: Vec = + FamilyOwned::parse_list(font_family).collect(); + family + }); + if let Some(font_family) = font_family.as_ref() { + attrs = attrs.family(font_family); + } + if let Some(line_height) = self.style.line_height() { + attrs = attrs.line_height(line_height); + } + + let mut attrs_list = AttrsList::new(attrs.clone()); + + for &i_start in &self.focus_indices { + let i_end = self + .text + .char_indices() + .find(|(i, _)| *i == i_start) + .map(|(_, c)| c.len_utf8() + i_start); + let i_end = if let Some(i_end) = i_end { + i_end + } else { + continue; + }; + attrs_list.add_span( + i_start..i_end, + attrs.clone().color(self.focus_color).weight(Weight::BOLD), + ); + } + let mut text_layout = TextLayout::new(); + text_layout.set_text(&self.text, attrs_list, None); + self.text_layout = Some(text_layout); + + if let Some(new_text) = self.available_text.as_ref() { + let new_text_len = new_text.len(); + + let mut attrs = + Attrs::new().color(self.style.color().unwrap_or(Color::BLACK)); + if let Some(font_size) = self.style.font_size() { + attrs = attrs.font_size(font_size); + } + let font_family = self.style.font_family().as_ref().map(|font_family| { + let family: Vec = + FamilyOwned::parse_list(font_family).collect(); + family + }); + if let Some(font_family) = font_family.as_ref() { + attrs = attrs.family(font_family); + } + + let mut attrs_list = AttrsList::new(attrs.clone()); + + for &i_start in &self.focus_indices { + if i_start + 3 > new_text_len { + break; + } + let i_end = self + .text + .char_indices() + .find(|(i, _)| *i == i_start) + .map(|(_, c)| c.len_utf8() + i_start); + let i_end = if let Some(i_end) = i_end { + i_end + } else { + continue; + }; + attrs_list.add_span( + i_start..i_end, + attrs.clone().color(self.focus_color).weight(Weight::BOLD), + ); + } + let mut text_layout = TextLayout::new(); + text_layout.set_text(new_text, attrs_list, None); + self.available_text_layout = Some(text_layout); + } + } +} + +impl View for FocusText { + fn id(&self) -> ViewId { + self.id + } + + fn update( + &mut self, + _cx: &mut floem::context::UpdateCx, + state: Box, + ) { + if let Ok(state) = state.downcast() { + match *state { + FocusTextState::Text(text) => { + self.text = text; + } + FocusTextState::FocusColor(color) => { + self.focus_color = color; + } + FocusTextState::FocusIndices(indices) => { + self.focus_indices = indices; + } + } + self.set_text_layout(); + self.id.request_layout(); + } + } + + fn style_pass(&mut self, cx: &mut floem::context::StyleCx<'_>) { + if self.style.read(cx) { + self.set_text_layout(); + self.id.request_layout(); + } + } + + fn layout( + &mut self, + cx: &mut floem::context::LayoutCx, + ) -> floem::taffy::prelude::NodeId { + cx.layout_node(self.id, true, |_cx| { + if self.text_layout.is_none() { + self.set_text_layout(); + } + + let text_layout = self.text_layout.as_ref().unwrap(); + let size = text_layout.size(); + let width = size.width.ceil() as f32; + let height = size.height as f32; + + if self.text_node.is_none() { + self.text_node = Some(self.id.new_taffy_node()); + } + let text_node = self.text_node.unwrap(); + + let style = Style::new().width(width).height(height).to_taffy_style(); + self.id.set_taffy_style(text_node, style); + vec![text_node] + }) + } + + fn compute_layout( + &mut self, + _cx: &mut floem::context::ComputeLayoutCx, + ) -> Option { + let text_node = self.text_node.unwrap(); + let layout = self.id.taffy_layout(text_node).unwrap_or_default(); + let text_layout = self.text_layout.as_ref().unwrap(); + let width = text_layout.size().width as f32; + if width > layout.size.width { + if self.available_width != Some(layout.size.width) { + let mut dots_text = TextLayout::new(); + let mut attrs = Attrs::new().color( + self.style + .color() + .unwrap_or_else(|| Color::from_rgb8(0xf0, 0xf0, 0xea)), + ); + if let Some(font_size) = self.style.font_size() { + attrs = attrs.font_size(font_size); + } + let font_family = + self.style.font_family().as_ref().map(|font_family| { + let family: Vec = + FamilyOwned::parse_list(font_family).collect(); + family + }); + if let Some(font_family) = font_family.as_ref() { + attrs = attrs.family(font_family); + } + dots_text.set_text("...", AttrsList::new(attrs), None); + + let dots_width = dots_text.size().width as f32; + let width_left = layout.size.width - dots_width; + let hit_point = + text_layout.hit_point(Point::new(width_left as f64, 0.0)); + let index = hit_point.index; + + let new_text = if index > 0 { + format!("{}...", &self.text[..index]) + } else { + "".to_string() + }; + self.available_text = Some(new_text); + self.available_width = Some(layout.size.width); + self.set_text_layout(); + } + } else { + self.available_text = None; + self.available_width = None; + self.available_text_layout = None; + } + + None + } + + fn paint(&mut self, cx: &mut floem::context::PaintCx) { + let text_node = self.text_node.unwrap(); + let location = self.id.taffy_layout(text_node).unwrap_or_default().location; + let point = Point::new(location.x as f64, location.y as f64); + if let Some(text_layout) = self.available_text_layout.as_ref() { + cx.draw_text(text_layout, point); + } else { + cx.draw_text(self.text_layout.as_ref().unwrap(), point); + } + } +} diff --git a/lapce-app/src/global_search.rs b/lapce-app/src/global_search.rs new file mode 100644 index 0000000..557bf28 --- /dev/null +++ b/lapce-app/src/global_search.rs @@ -0,0 +1,200 @@ +use std::{ops::Range, path::PathBuf, rc::Rc}; + +use floem::{ + ext_event::create_ext_action, + keyboard::Modifiers, + reactive::{Memo, RwSignal, Scope, SignalGet, SignalUpdate, SignalWith}, + views::VirtualVector, +}; +use indexmap::IndexMap; +use lapce_core::{mode::Mode, selection::Selection}; +use lapce_rpc::proxy::{ProxyResponse, SearchMatch}; +use lapce_xi_rope::Rope; + +use crate::{ + command::{CommandExecuted, CommandKind}, + editor::EditorData, + keypress::{KeyPressFocus, condition::Condition}, + main_split::MainSplitData, + window_tab::CommonData, +}; + +#[derive(Clone)] +pub struct SearchMatchData { + pub expanded: RwSignal, + pub matches: RwSignal>, + pub line_height: Memo, +} + +impl SearchMatchData { + pub fn height(&self) -> f64 { + let line_height = self.line_height.get(); + let count = if self.expanded.get() { + self.matches.with(|m| m.len()) + 1 + } else { + 1 + }; + line_height * count as f64 + } +} + +#[derive(Clone, Debug)] +pub struct GlobalSearchData { + pub editor: EditorData, + pub search_result: RwSignal>, + pub main_split: MainSplitData, + pub common: Rc, +} + +impl KeyPressFocus for GlobalSearchData { + fn get_mode(&self) -> Mode { + Mode::Insert + } + + fn check_condition(&self, condition: Condition) -> bool { + matches!(condition, Condition::PanelFocus) + } + + fn run_command( + &self, + command: &crate::command::LapceCommand, + count: Option, + mods: Modifiers, + ) -> CommandExecuted { + match &command.kind { + CommandKind::Workbench(_) => {} + CommandKind::Scroll(_) => {} + CommandKind::Focus(_) => {} + CommandKind::Edit(_) + | CommandKind::Move(_) + | CommandKind::MultiSelection(_) => { + return self.editor.run_command(command, count, mods); + } + CommandKind::MotionMode(_) => {} + } + CommandExecuted::No + } + + fn receive_char(&self, c: &str) { + self.editor.receive_char(c); + } +} + +impl VirtualVector<(PathBuf, SearchMatchData)> for GlobalSearchData { + fn total_len(&self) -> usize { + self.search_result.with(|result| { + result + .iter() + .map(|(_, data)| { + if data.expanded.get() { + data.matches.with(|m| m.len()) + 1 + } else { + 1 + } + }) + .sum() + }) + } + + fn slice( + &mut self, + _range: Range, + ) -> impl Iterator { + self.search_result.get().into_iter() + } +} + +impl GlobalSearchData { + pub fn new(cx: Scope, main_split: MainSplitData) -> Self { + let common = main_split.common.clone(); + let editor = main_split.editors.make_local(cx, common.clone()); + let search_result = cx.create_rw_signal(IndexMap::new()); + + let global_search = Self { + editor, + search_result, + main_split, + common, + }; + + { + let global_search = global_search.clone(); + let buffer = global_search.editor.doc().buffer; + cx.create_effect(move |_| { + let pattern = buffer.with(|buffer| buffer.to_string()); + if pattern.is_empty() { + global_search.search_result.update(|r| r.clear()); + return; + } + let case_sensitive = global_search.common.find.case_sensitive(true); + let whole_word = global_search.common.find.whole_words.get(); + let is_regex = global_search.common.find.is_regex.get(); + let send = { + let global_search = global_search.clone(); + create_ext_action(cx, move |result| { + if let Ok(ProxyResponse::GlobalSearchResponse { matches }) = + result + { + global_search.update_matches(matches); + } + }) + }; + global_search.common.proxy.global_search( + pattern, + case_sensitive, + whole_word, + is_regex, + move |result| { + send(result); + }, + ); + }); + } + + { + let buffer = global_search.editor.doc().buffer; + let main_split = global_search.main_split.clone(); + cx.create_effect(move |_| { + let content = buffer.with(|buffer| buffer.to_string()); + main_split.set_find_pattern(Some(content)); + }); + } + + global_search + } + + fn update_matches(&self, matches: IndexMap>) { + let current = self.search_result.get_untracked(); + + self.search_result.set( + matches + .into_iter() + .map(|(path, matches)| { + let match_data = + current.get(&path).cloned().unwrap_or_else(|| { + SearchMatchData { + expanded: self.common.scope.create_rw_signal(true), + matches: self + .common + .scope + .create_rw_signal(im::Vector::new()), + line_height: self.common.ui_line_height, + } + }); + + match_data.matches.set(matches.into()); + + (path, match_data) + }) + .collect(), + ); + } + + pub fn set_pattern(&self, pattern: String) { + let pattern_len = pattern.len(); + self.editor.doc().reload(Rope::from(pattern), true); + self.editor + .cursor() + .update(|cursor| cursor.set_insert(Selection::region(0, pattern_len))); + } +} diff --git a/lapce-app/src/history.rs b/lapce-app/src/history.rs new file mode 100644 index 0000000..36125f5 --- /dev/null +++ b/lapce-app/src/history.rs @@ -0,0 +1,26 @@ +use std::path::PathBuf; + +use lapce_core::buffer::Buffer; + +#[derive(Clone)] +pub struct DocumentHistory { + pub buffer: Buffer, + // path: PathBuf, + // version: String, + // styles: Arc>, + // line_styles: Rc>, + // text_layouts: Rc>, +} + +impl DocumentHistory { + pub fn new(_path: PathBuf, _version: String, content: &str) -> Self { + Self { + buffer: Buffer::new(content), + // path, + // version, + // styles: Arc::new(Spans::default()), + // line_styles: Rc::new(RefCell::new(LineStyles::new())), + // text_layouts: Rc::new(RefCell::new(TextLayoutCache::new())), + } + } +} diff --git a/lapce-app/src/hover.rs b/lapce-app/src/hover.rs new file mode 100644 index 0000000..67a2641 --- /dev/null +++ b/lapce-app/src/hover.rs @@ -0,0 +1,28 @@ +use floem::{ + peniko::kurbo::Rect, + reactive::{RwSignal, Scope}, + views::editor::id::EditorId, +}; + +use crate::markdown::MarkdownContent; + +#[derive(Clone)] +pub struct HoverData { + pub active: RwSignal, + pub offset: RwSignal, + pub editor_id: RwSignal, + pub content: RwSignal>, + pub layout_rect: RwSignal, +} + +impl HoverData { + pub fn new(cx: Scope) -> Self { + Self { + active: cx.create_rw_signal(false), + offset: cx.create_rw_signal(0), + content: cx.create_rw_signal(Vec::new()), + editor_id: cx.create_rw_signal(EditorId::next()), + layout_rect: cx.create_rw_signal(Rect::ZERO), + } + } +} diff --git a/lapce-app/src/id.rs b/lapce-app/src/id.rs new file mode 100644 index 0000000..87011d4 --- /dev/null +++ b/lapce-app/src/id.rs @@ -0,0 +1,11 @@ +use floem::views::editor::id::Id; + +pub type SplitId = Id; +pub type WindowTabId = Id; +pub type EditorTabId = Id; +pub type SettingsId = Id; +pub type KeymapId = Id; +pub type ThemeColorSettingsId = Id; +pub type VoltViewId = Id; +pub type DiffEditorId = Id; +pub type TerminalTabId = Id; diff --git a/lapce-app/src/inline_completion.rs b/lapce-app/src/inline_completion.rs new file mode 100644 index 0000000..9482771 --- /dev/null +++ b/lapce-app/src/inline_completion.rs @@ -0,0 +1,288 @@ +use std::{borrow::Cow, ops::Range, path::PathBuf, str::FromStr}; + +use floem::reactive::{RwSignal, Scope, SignalGet, SignalUpdate, SignalWith, batch}; +use lapce_core::{ + buffer::{ + Buffer, + rope_text::{RopeText, RopeTextRef}, + }, + rope_text_pos::RopeTextPosition, + selection::Selection, +}; +use lsp_types::InsertTextFormat; + +use crate::{config::LapceConfig, doc::Doc, editor::EditorData, snippet::Snippet}; + +// TODO: we could integrate completion lens with this, so it is considered at the same time + +/// Redefinition of lsp types inline completion item with offset range +#[derive(Debug, Clone)] +pub struct InlineCompletionItem { + /// The text to replace the range with. + pub insert_text: String, + /// Text used to decide if this inline completion should be shown. + pub filter_text: Option, + /// The range (of offsets) to replace + pub range: Option>, + pub command: Option, + pub insert_text_format: Option, +} +impl InlineCompletionItem { + pub fn from_lsp(buffer: &Buffer, item: lsp_types::InlineCompletionItem) -> Self { + let range = item.range.map(|r| { + let start = buffer.offset_of_position(&r.start); + let end = buffer.offset_of_position(&r.end); + start..end + }); + Self { + insert_text: item.insert_text, + filter_text: item.filter_text, + range, + command: item.command, + insert_text_format: item.insert_text_format, + } + } + + pub fn apply( + &self, + editor: &EditorData, + start_offset: usize, + ) -> anyhow::Result<()> { + let text_format = self + .insert_text_format + .unwrap_or(InsertTextFormat::PLAIN_TEXT); + + let selection = if let Some(range) = &self.range { + Selection::region(range.start, range.end) + } else { + Selection::caret(start_offset) + }; + + match text_format { + InsertTextFormat::PLAIN_TEXT => editor.do_edit( + &selection, + &[(selection.clone(), self.insert_text.as_str())], + ), + InsertTextFormat::SNIPPET => { + editor.completion_apply_snippet( + &self.insert_text, + &selection, + Vec::new(), + start_offset, + )?; + } + _ => { + // We don't know how to support this text format + } + } + + Ok(()) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum InlineCompletionStatus { + /// The inline completion is not active. + Inactive, + /// The inline completion is active and is waiting for the server to respond. + Started, + /// The inline completion is active and has received a response from the server. + Active, +} + +#[derive(Clone)] +pub struct InlineCompletionData { + pub status: InlineCompletionStatus, + /// The active inline completion index in the list of completions. + pub active: RwSignal, + pub items: im::Vector, + pub start_offset: usize, + pub path: PathBuf, +} +impl InlineCompletionData { + pub fn new(cx: Scope) -> Self { + Self { + status: InlineCompletionStatus::Inactive, + active: cx.create_rw_signal(0), + items: im::vector![], + start_offset: 0, + path: PathBuf::new(), + } + } + + pub fn current_item(&self) -> Option<&InlineCompletionItem> { + let active = self.active.get_untracked(); + self.items.get(active) + } + + pub fn next(&mut self) { + if !self.items.is_empty() { + let next_index = (self.active.get_untracked() + 1) % self.items.len(); + self.active.set(next_index); + } + } + + pub fn previous(&mut self) { + if !self.items.is_empty() { + let prev_index = if self.active.get_untracked() == 0 { + self.items.len() - 1 + } else { + self.active.get_untracked() - 1 + }; + self.active.set(prev_index); + } + } + + pub fn cancel(&mut self) { + if self.status == InlineCompletionStatus::Inactive { + return; + } + + self.items.clear(); + self.status = InlineCompletionStatus::Inactive; + } + + /// Set the items for the inline completion. + /// Sets `active` to `0` and `status` to `InlineCompletionStatus::Active`. + pub fn set_items( + &mut self, + items: im::Vector, + start_offset: usize, + path: PathBuf, + ) { + batch(|| { + self.items = items; + self.active.set(0); + self.status = InlineCompletionStatus::Active; + self.start_offset = start_offset; + self.path = path; + }); + } + + pub fn update_doc(&self, doc: &Doc, offset: usize) { + if self.status != InlineCompletionStatus::Active { + doc.clear_inline_completion(); + return; + } + + if self.items.is_empty() { + doc.clear_inline_completion(); + return; + } + + let active = self.active.get_untracked(); + let active = if active >= self.items.len() { + self.active.set(0); + 0 + } else { + active + }; + + let item = &self.items[active]; + let text = item.insert_text.clone(); + + // TODO: is range really meant to be used for this? + let offset = item.range.as_ref().map(|r| r.start).unwrap_or(offset); + let (line, col) = doc + .buffer + .with_untracked(|buffer| buffer.offset_to_line_col(offset)); + doc.set_inline_completion(text, line, col); + } + + pub fn update_inline_completion( + &self, + config: &LapceConfig, + doc: &Doc, + cursor_offset: usize, + ) { + if !config.editor.enable_inline_completion { + doc.clear_inline_completion(); + return; + } + + let text = doc.buffer.with_untracked(|buffer| buffer.text().clone()); + let text = RopeTextRef::new(&text); + let Some(item) = self.current_item() else { + // TODO(minor): should we cancel completion + return; + }; + + let completion = doc.inline_completion.with_untracked(|cur| { + let cur = cur.as_deref(); + inline_completion_text(text, self.start_offset, cursor_offset, item, cur) + }); + + match completion { + ICompletionRes::Hide => { + doc.clear_inline_completion(); + } + ICompletionRes::Unchanged => {} + ICompletionRes::Set(new, shift) => { + let offset = self.start_offset + shift; + let (line, col) = text.offset_to_line_col(offset); + doc.set_inline_completion(new, line, col); + } + } + } +} + +enum ICompletionRes { + Hide, + Unchanged, + Set(String, usize), +} + +/// Get the text of the inline completion item +fn inline_completion_text( + rope_text: impl RopeText, + start_offset: usize, + cursor_offset: usize, + item: &InlineCompletionItem, + current_completion: Option<&str>, +) -> ICompletionRes { + let text_format = item + .insert_text_format + .unwrap_or(InsertTextFormat::PLAIN_TEXT); + + // TODO: is this check correct? I mostly copied it from completion lens + let cursor_prev_offset = rope_text.prev_code_boundary(cursor_offset); + if let Some(range) = &item.range { + let edit_start = range.start; + + // If the start of the edit isn't where the cursor currently is, and is not at the start of + // the inline completion, then we ignore it. + if cursor_prev_offset != edit_start && start_offset != edit_start { + return ICompletionRes::Hide; + } + } + + let text = match text_format { + InsertTextFormat::PLAIN_TEXT => Cow::Borrowed(&item.insert_text), + InsertTextFormat::SNIPPET => { + let Ok(snippet) = Snippet::from_str(&item.insert_text) else { + return ICompletionRes::Hide; + }; + let text = snippet.text(); + + Cow::Owned(text) + } + _ => { + // We don't know how to support this text format + return ICompletionRes::Hide; + } + }; + + let range = start_offset..rope_text.offset_line_end(start_offset, true); + let prefix = rope_text.slice_to_cow(range); + // We strip the prefix of the current input from the label. + // So that, for example `p` with a completion of `println` will show `rintln`. + let Some(text) = text.strip_prefix(prefix.as_ref()) else { + return ICompletionRes::Hide; + }; + + if Some(text) == current_completion { + ICompletionRes::Unchanged + } else { + ICompletionRes::Set(text.to_string(), prefix.len()) + } +} diff --git a/lapce-app/src/keymap.rs b/lapce-app/src/keymap.rs new file mode 100644 index 0000000..f89639b --- /dev/null +++ b/lapce-app/src/keymap.rs @@ -0,0 +1,562 @@ +use std::{rc::Rc, sync::Arc}; + +use floem::{ + View, + event::{Event, EventListener}, + reactive::{ + Memo, ReadSignal, RwSignal, Scope, SignalGet, SignalUpdate, SignalWith, + create_effect, create_memo, create_rw_signal, + }, + style::CursorStyle, + views::{ + Decorators, container, dyn_stack, label, scroll, stack, text, virtual_stack, + }, +}; +use lapce_core::mode::Modes; + +use crate::{ + command::LapceCommand, + config::{LapceConfig, color::LapceColor}, + keypress::{ + KeyPressData, + keymap::{KeyMap, KeyMapPress}, + }, + main_split::Editors, + text_input::TextInputBuilder, + window_tab::CommonData, +}; + +#[derive(Clone)] +pub struct KeymapPicker { + cmd: RwSignal>, + keymap: RwSignal>, + keys: RwSignal>, +} + +pub fn keymap_view(editors: Editors, common: Rc) -> impl View { + let config = common.config; + let keypress = common.keypress; + let ui_line_height_memo = common.ui_line_height; + let ui_line_height = move || ui_line_height_memo.get() * 1.2; + let modal = create_memo(move |_| config.get().core.modal); + let picker = KeymapPicker { + cmd: create_rw_signal(None), + keymap: create_rw_signal(None), + keys: create_rw_signal(Vec::new()), + }; + + let cx = Scope::current(); + let text_input_view = TextInputBuilder::new().build(cx, editors, common.clone()); + let doc = text_input_view.doc_signal(); + + let items = move || { + let doc = doc.get(); + let pattern = doc.buffer.with(|b| b.to_string().to_lowercase()); + let keypress = keypress.get(); + let mut items = keypress + .commands_with_keymap + .iter() + .filter_map(|keymap| { + let cmd = keypress.commands.get(&keymap.command).cloned()?; + + let cmd_name_contains_pattern = + cmd.kind.str().replace('_', " ").contains(&pattern); + let cmd_desc_contains_pattern = cmd + .kind + .desc() + .map(|desc| desc.to_lowercase().contains(&pattern)) + .unwrap_or(false); + let shortcut_contains_pattern = keymap + .key + .iter() + .any(|k| k.label().trim().to_lowercase().contains(&pattern)); + let when_contains_pattern = keymap + .when + .as_ref() + .map(|when| when.to_lowercase().contains(&pattern)) + .unwrap_or(false); + + if cmd_name_contains_pattern + || cmd_desc_contains_pattern + || shortcut_contains_pattern + || when_contains_pattern + { + Some((cmd, Some(keymap.clone()))) + } else { + None + } + }) + .collect::)>>(); + items.extend(keypress.commands_without_keymap.iter().filter_map(|cmd| { + let match_pattern = cmd.kind.str().replace('_', " ").contains(&pattern) + || cmd + .kind + .desc() + .map(|desc| desc.to_lowercase().contains(&pattern)) + .unwrap_or(false); + if !match_pattern { + return None; + } + Some((cmd.clone(), None)) + })); + items + .into_iter() + .enumerate() + .collect::))>>() + }; + + let view_fn = + move |(i, (cmd, keymap)): (usize, (LapceCommand, Option))| { + let local_keymap = keymap.clone(); + let local_cmd = cmd.clone(); + stack(( + container( + text( + cmd.kind + .desc() + .map(|desc| desc.to_string()) + .unwrap_or_else(|| cmd.kind.str().replace('_', " ")), + ) + .style(|s| { + s.text_ellipsis() + .absolute() + .items_center() + .min_width(0.0) + .padding_horiz(10.0) + .size_pct(100.0, 100.0) + }), + ) + .style(move |s| { + s.height_pct(100.0) + .min_width(0.0) + .flex_basis(0.0) + .flex_grow(1.0) + .border_right(1.0) + .border_color(config.get().color(LapceColor::LAPCE_BORDER)) + }), + { + let keymap = keymap.clone(); + dyn_stack( + move || { + keymap + .as_ref() + .map(|keymap| { + keymap + .key + .iter() + .map(|key| key.label()) + .filter(|l| !l.is_empty()) + .collect::>() + }) + .unwrap_or_default() + }, + |k| k.clone(), + move |key| { + text(key.clone()).style(move |s| { + s.padding_horiz(5.0) + .padding_vert(1.0) + .margin_right(5.0) + .border(1.0) + .border_radius(3.0) + .border_color( + config.get().color(LapceColor::LAPCE_BORDER), + ) + }) + }, + ) + .style(move |s| { + s.items_center() + .padding_horiz(10.0) + .min_width(200.0) + .height_pct(100.0) + .border_right(1.0) + .border_color( + config.get().color(LapceColor::LAPCE_BORDER), + ) + }) + }, + { + let keymap = keymap.clone(); + let bits = [ + (Modes::INSERT, "Insert"), + (Modes::NORMAL, "Normal"), + (Modes::VISUAL, "Visual"), + (Modes::TERMINAL, "Terminal"), + ]; + let modes = keymap + .as_ref() + .map(|keymap| { + bits.iter() + .filter_map(|(bit, mode)| { + if keymap.modes.contains(*bit) { + Some(mode.to_string()) + } else { + None + } + }) + .collect::>() + }) + .unwrap_or_default(); + dyn_stack( + move || modes.clone(), + |m| m.clone(), + move |mode| { + text(mode.clone()).style(move |s| { + s.padding_horiz(5.0) + .padding_vert(1.0) + .margin_right(5.0) + .border(1.0) + .border_radius(3.0) + .border_color( + config.get().color(LapceColor::LAPCE_BORDER), + ) + }) + }, + ) + .style(move |s| { + s.items_center() + .padding_horiz(10.0) + .min_width(200.0) + .height_pct(100.0) + .border_right(1.0) + .border_color( + config.get().color(LapceColor::LAPCE_BORDER), + ) + .apply_if(!modal.get(), |s| s.hide()) + }) + }, + container( + text( + keymap + .as_ref() + .and_then(|keymap| keymap.when.clone()) + .unwrap_or_default(), + ) + .style(|s| { + s.text_ellipsis() + .absolute() + .items_center() + .min_width(0.0) + .padding_horiz(10.0) + .size_pct(100.0, 100.0) + }), + ) + .style(move |s| { + s.height_pct(100.0) + .min_width(0.0) + .flex_basis(0.0) + .flex_grow(1.0) + }), + )) + .on_click_stop(move |_| { + let keymap = if let Some(keymap) = local_keymap.clone() { + keymap + } else { + KeyMap { + command: local_cmd.kind.str().to_string(), + key: Vec::new(), + modes: Modes::empty(), + when: None, + } + }; + picker.keymap.set(Some(keymap)); + picker.cmd.set(Some(local_cmd.clone())); + picker.keys.update(|keys| { + keys.clear(); + }); + }) + .style(move |s| { + let config = config.get(); + s.items_center() + .height(ui_line_height() as f32) + .width_pct(100.0) + .apply_if(i % 2 > 0, |s| { + s.background(config.color(LapceColor::EDITOR_CURRENT_LINE)) + }) + .border_bottom(1.0) + .border_color(config.color(LapceColor::LAPCE_BORDER)) + }) + }; + + stack(( + container( + text_input_view + .placeholder(|| "Search Key Bindings".to_string()) + .keyboard_navigable() + .request_focus(|| {}) + .style(move |s| { + s.width_pct(100.0) + .border_radius(6.0) + .border(1.0) + .border_color(config.get().color(LapceColor::LAPCE_BORDER)) + }), + ) + .style(|s| s.padding_bottom(10.0).width_pct(100.0)), + stack(( + container(text("Command").style(move |s| { + s.text_ellipsis().padding_horiz(10.0).min_width(0.0) + })) + .style(move |s| { + s.items_center() + .height_pct(100.0) + .min_width(0.0) + .flex_basis(0.0) + .flex_grow(1.0) + .border_right(1.0) + .border_color(config.get().color(LapceColor::LAPCE_BORDER)) + }), + text("Key Binding").style(move |s| { + s.width(200.0) + .items_center() + .padding_horiz(10.0) + .height_pct(100.0) + .border_right(1.0) + .border_color(config.get().color(LapceColor::LAPCE_BORDER)) + }), + text("Modes").style(move |s| { + s.width(200.0) + .items_center() + .padding_horiz(10.0) + .height_pct(100.0) + .border_right(1.0) + .border_color(config.get().color(LapceColor::LAPCE_BORDER)) + .apply_if(!modal.get(), |s| s.hide()) + }), + container(text("When").style(move |s| { + s.text_ellipsis().padding_horiz(10.0).min_width(0.0) + })) + .style(move |s| { + s.items_center() + .height_pct(100.0) + .min_width(0.0) + .flex_basis(0.0) + .flex_grow(1.0) + }), + )) + .style(move |s| { + let config = config.get(); + s.font_bold() + .height(ui_line_height() as f32) + .width_pct(100.0) + .border_top(1.0) + .border_bottom(1.0) + .border_color(config.color(LapceColor::LAPCE_BORDER)) + .background(config.color(LapceColor::EDITOR_CURRENT_LINE)) + }), + container( + scroll( + virtual_stack( + items, + |(i, (cmd, keymap)): &( + usize, + (LapceCommand, Option), + )| { (*i, cmd.kind.str(), keymap.clone()) }, + view_fn, + ) + .item_size_fixed(ui_line_height) + .style(|s| s.flex_col().width_pct(100.0)), + ) + .style(|s| s.absolute().size_pct(100.0, 100.0)), + ) + .style(|s| s.width_pct(100.0).flex_basis(0.0).flex_grow(1.0)), + keyboard_picker_view(picker, common.ui_line_height, config), + )) + .style(|s| { + s.absolute() + .size_pct(100.0, 100.0) + .flex_col() + .padding_top(20.0) + .padding_left(20.0) + .padding_right(20.0) + }) +} + +fn keyboard_picker_view( + picker: KeymapPicker, + ui_line_height: Memo, + config: ReadSignal>, +) -> impl View { + let picker_cmd = picker.cmd; + let view = container( + stack(( + label(move || { + picker_cmd.with(|cmd| { + cmd.as_ref() + .map(|cmd| { + cmd.kind + .desc() + .map(|desc| desc.to_string()) + .unwrap_or_else(|| cmd.kind.str().replace('_', " ")) + }) + .unwrap_or_default() + }) + }), + dyn_stack( + move || { + picker + .keys + .get() + .iter() + .map(|(key, _)| key.label()) + .filter(|l| !l.is_empty()) + .enumerate() + .collect::>() + }, + |(i, k)| (*i, k.clone()), + move |(_, key)| { + text(key.clone()).style(move |s| { + s.padding_horiz(5.0) + .padding_vert(1.0) + .margin_right(5.0) + .height(ui_line_height.get() as f32) + .border(1.0) + .border_radius(6.0) + .border_color( + config.get().color(LapceColor::LAPCE_BORDER), + ) + }) + }, + ) + .style(move |s| { + let config = config.get(); + s.items_center() + .justify_center() + .width_pct(100.0) + .margin_top(20.0) + .height((ui_line_height.get() as f32) * 1.2) + .border(1.0) + .border_radius(6.0) + .border_color(config.color(LapceColor::LAPCE_BORDER)) + .background(config.color(LapceColor::EDITOR_BACKGROUND)) + }), + stack(( + text("Save") + .style(move |s| { + let config = config.get(); + s.width(100.0) + .justify_center() + .padding_vert(8.0) + .border(1.0) + .border_radius(6.0) + .border_color(config.color(LapceColor::LAPCE_BORDER)) + .hover(|s| { + s.cursor(CursorStyle::Pointer).background( + config + .color(LapceColor::PANEL_HOVERED_BACKGROUND), + ) + }) + .active(|s| { + s.background(config.color( + LapceColor::PANEL_HOVERED_ACTIVE_BACKGROUND, + )) + }) + }) + .on_click_stop(move |_| { + let keymap = picker.keymap.get_untracked(); + if let Some(keymap) = keymap { + let keys = picker.keys.get_untracked(); + picker.keymap.set(None); + KeyPressData::update_file( + &keymap, + &keys + .iter() + .map(|(key, _)| key.clone()) + .collect::>(), + ); + } + }), + text("Cancel") + .style(move |s| { + let config = config.get(); + s.margin_left(20.0) + .width(100.0) + .justify_center() + .padding_vert(8.0) + .border(1.0) + .border_radius(6.0) + .border_color(config.color(LapceColor::LAPCE_BORDER)) + .hover(|s| { + s.cursor(CursorStyle::Pointer).background( + config + .color(LapceColor::PANEL_HOVERED_BACKGROUND), + ) + }) + .active(|s| { + s.background(config.color( + LapceColor::PANEL_HOVERED_ACTIVE_BACKGROUND, + )) + }) + }) + .on_click_stop(move |_| { + picker.keymap.set(None); + }), + )) + .style(move |s| { + let config = config.get(); + s.items_center() + .justify_center() + .width_pct(100.0) + .margin_top(20.0) + .border_color(config.color(LapceColor::LAPCE_BORDER)) + }), + )) + .on_event_stop(EventListener::PointerDown, |_| {}) + .style(move |s| { + let config = config.get(); + s.items_center() + .flex_col() + .padding(20.0) + .width(400.0) + .border(1.0) + .border_radius(6.0) + .border_color(config.color(LapceColor::LAPCE_BORDER)) + .background(config.color(LapceColor::PANEL_BACKGROUND)) + }), + ) + .keyboard_navigable() + .on_event_stop(EventListener::KeyDown, move |event| { + if let Event::KeyDown(key_event) = event { + if let Some(keypress) = KeyPressData::keypress(key_event) { + if let Some(keypress) = keypress.keymap_press() { + picker.keys.update(|keys| { + if let Some((last_key, last_key_confirmed)) = keys.last() { + if !*last_key_confirmed && last_key.is_modifiers() { + keys.pop(); + } + } + if keys.len() == 2 { + keys.clear(); + } + keys.push((keypress, false)); + }) + } + } + } + }) + .on_event_stop(EventListener::KeyUp, move |event| { + if let Event::KeyUp(_key_event) = event { + picker.keys.update(|keys| { + if let Some((_last_key, last_key_confirmed)) = keys.last_mut() { + *last_key_confirmed = true; + } + }) + } + }) + .style(move |s| { + s.absolute() + .size_pct(100.0, 100.0) + .items_center() + .justify_center() + .apply_if(picker.keymap.with(|keymap| keymap.is_none()), |s| s.hide()) + }) + .debug_name("keyboard picker"); + + let id = view.id(); + create_effect(move |_| { + if picker.keymap.with(|k| k.is_some()) { + id.request_focus(); + } + }); + + view +} diff --git a/lapce-app/src/keypress.rs b/lapce-app/src/keypress.rs new file mode 100644 index 0000000..7dc0b09 --- /dev/null +++ b/lapce-app/src/keypress.rs @@ -0,0 +1,701 @@ +pub mod condition; +mod key; +pub mod keymap; +mod loader; +mod press; + +use std::{path::PathBuf, rc::Rc, str::FromStr, time::SystemTime}; + +use anyhow::Result; +use floem::{ + keyboard::{Key, KeyEvent, KeyEventExtModifierSupplement, Modifiers, NamedKey}, + pointer::{MouseButton, PointerButton, PointerInputEvent}, + reactive::{RwSignal, Scope, SignalUpdate, SignalWith}, +}; +use indexmap::IndexMap; +use itertools::Itertools; +use lapce_core::mode::{Mode, Modes}; + +pub use self::press::KeyPress; +use self::{ + key::KeyInput, + keymap::{KeyMap, KeyMapPress}, + loader::KeyMapLoader, +}; +use crate::{ + command::{CommandExecuted, CommandKind, LapceCommand, lapce_internal_commands}, + config::LapceConfig, + keypress::{ + condition::{CheckCondition, Condition}, + keymap::KeymapMatch, + }, + tracing::*, +}; + +const DEFAULT_KEYMAPS_COMMON: &str = + include_str!("../../defaults/keymaps-common.toml"); +const DEFAULT_KEYMAPS_MACOS: &str = + include_str!("../../defaults/keymaps-macos.toml"); +const DEFAULT_KEYMAPS_NONMACOS: &str = + include_str!("../../defaults/keymaps-nonmacos.toml"); + +pub trait KeyPressFocus: std::fmt::Debug { + fn get_mode(&self) -> Mode; + + fn check_condition(&self, condition: Condition) -> bool; + + fn run_command( + &self, + command: &LapceCommand, + count: Option, + mods: Modifiers, + ) -> CommandExecuted; + + fn expect_char(&self) -> bool { + false + } + + fn focus_only(&self) -> bool { + false + } + + fn receive_char(&self, c: &str); +} +impl KeyPressFocus for () { + fn get_mode(&self) -> Mode { + Mode::Normal + } + + fn check_condition(&self, _condition: Condition) -> bool { + false + } + + fn run_command( + &self, + _command: &LapceCommand, + _count: Option, + _mods: Modifiers, + ) -> CommandExecuted { + CommandExecuted::No + } + + fn expect_char(&self) -> bool { + false + } + + fn focus_only(&self) -> bool { + false + } + + fn receive_char(&self, _c: &str) {} +} +impl KeyPressFocus for Box { + fn get_mode(&self) -> Mode { + (**self).get_mode() + } + + fn check_condition(&self, condition: Condition) -> bool { + (**self).check_condition(condition) + } + + fn run_command( + &self, + command: &LapceCommand, + count: Option, + mods: Modifiers, + ) -> CommandExecuted { + (**self).run_command(command, count, mods) + } + + fn expect_char(&self) -> bool { + (**self).expect_char() + } + + fn focus_only(&self) -> bool { + (**self).focus_only() + } + + fn receive_char(&self, c: &str) { + (**self).receive_char(c) + } +} + +#[derive(Clone, Copy, Debug)] +pub enum EventRef<'a> { + Keyboard(&'a floem::keyboard::KeyEvent), + Pointer(&'a floem::pointer::PointerInputEvent), +} + +impl<'a> From<&'a KeyEvent> for EventRef<'a> { + fn from(ev: &'a KeyEvent) -> Self { + Self::Keyboard(ev) + } +} + +impl<'a> From<&'a PointerInputEvent> for EventRef<'a> { + fn from(ev: &'a PointerInputEvent) -> Self { + Self::Pointer(ev) + } +} + +pub struct KeyPressHandle { + pub handled: bool, + pub keypress: KeyPress, + pub keymatch: KeymapMatch, +} + +#[derive(Clone, Debug)] +pub struct KeyPressData { + count: RwSignal>, + pending_keypress: RwSignal<(Vec, Option)>, + pub commands: Rc>, + pub keymaps: Rc, Vec>>, + pub command_keymaps: Rc>>, + pub commands_with_keymap: Rc>, + pub commands_without_keymap: Rc>, +} + +impl KeyPressData { + pub fn new(cx: Scope, config: &LapceConfig) -> Self { + let (keymaps, command_keymaps) = + Self::get_keymaps(config).unwrap_or((IndexMap::new(), IndexMap::new())); + let mut keypress = Self { + count: cx.create_rw_signal(None), + pending_keypress: cx.create_rw_signal((Vec::new(), None)), + keymaps: Rc::new(keymaps), + command_keymaps: Rc::new(command_keymaps), + commands: Rc::new(lapce_internal_commands()), + commands_with_keymap: Rc::new(Vec::new()), + commands_without_keymap: Rc::new(Vec::new()), + }; + keypress.load_commands(); + keypress + } + + pub fn update_keymaps(&mut self, config: &LapceConfig) { + if let Ok((new_keymaps, new_command_keymaps)) = Self::get_keymaps(config) { + self.keymaps = Rc::new(new_keymaps); + self.command_keymaps = Rc::new(new_command_keymaps); + self.load_commands(); + } + } + + fn load_commands(&mut self) { + let mut commands_with_keymap = Vec::new(); + let mut commands_without_keymap = Vec::new(); + for (_, keymaps) in self.command_keymaps.iter() { + for keymap in keymaps.iter() { + if self.commands.get(&keymap.command).is_some() { + commands_with_keymap.push(keymap.clone()); + } + } + } + + for (_, cmd) in self.commands.iter() { + if self + .command_keymaps + .get(cmd.kind.str()) + .map(|x| x.is_empty()) + .unwrap_or(true) + { + commands_without_keymap.push(cmd.clone()); + } + } + + self.commands_with_keymap = Rc::new(commands_with_keymap); + self.commands_without_keymap = Rc::new(commands_without_keymap); + } + + fn handle_count( + &self, + focus: &T, + keypress: &KeyPress, + ) -> bool { + if focus.expect_char() { + return false; + } + let mode = focus.get_mode(); + if mode == Mode::Insert || mode == Mode::Terminal { + return false; + } + + if !keypress.mods.is_empty() { + return false; + } + + if let KeyInput::Keyboard { + logical: Key::Character(c), + .. + } = &keypress.key + { + if let Ok(n) = c.parse::() { + if self.count.with_untracked(|count| count.is_some()) || n > 0 { + self.count + .update(|count| *count = Some(count.unwrap_or(0) * 10 + n)); + return true; + } + } + } + + false + } + + fn run_command( + &self, + command: &str, + count: Option, + mods: Modifiers, + focus: &T, + ) -> CommandExecuted { + if let Some(cmd) = self.commands.get(command) { + focus.run_command(cmd, count, mods) + } else { + CommandExecuted::No + } + } + + pub fn keypress<'a>(event: impl Into>) -> Option { + let event = event.into(); + + let keypress = match event { + EventRef::Keyboard(ev) => KeyPress { + key: KeyInput::Keyboard { + logical: ev.key.logical_key.to_owned(), + physical: ev.key.physical_key, + key_without_modifiers: ev.key.key_without_modifiers(), + location: ev.key.location, + repeat: ev.key.repeat, + }, + mods: Self::get_key_modifiers(ev), + }, + EventRef::Pointer(ev) => KeyPress { + key: KeyInput::Pointer(ev.button), + mods: ev.modifiers, + }, + }; + Some(keypress) + } + + pub fn key_down<'a, T: KeyPressFocus + ?Sized>( + &self, + event: impl Into>, + focus: &T, + ) -> KeyPressHandle { + let keypress = match Self::keypress(event) { + Some(keypress) => keypress, + None => { + return KeyPressHandle { + handled: false, + keymatch: KeymapMatch::None, + keypress: KeyPress { + key: KeyInput::Pointer(PointerButton::Mouse( + MouseButton::Primary, + )), + mods: Modifiers::empty(), + }, + }; + } + }; + + if self.handle_count(focus, &keypress) { + return KeyPressHandle { + handled: true, + keymatch: KeymapMatch::None, + keypress, + }; + } + + self.pending_keypress + .update(|(pending_keypress, last_time)| { + let last_time = last_time.replace(SystemTime::now()); + if let Some(last_time_val) = last_time { + if last_time_val + .elapsed() + .map(|x| x.as_millis() > 1000) + .unwrap_or_default() + { + pending_keypress.clear(); + } + } + pending_keypress.push(keypress.clone()); + }); + + let keymatch = + self.pending_keypress + .with_untracked(|(pending_keypress, _)| { + self.match_keymap(pending_keypress, focus) + }); + self.handle_keymatch(focus, keymatch, keypress) + } + + pub fn handle_keymatch( + &self, + focus: &T, + keymatch: KeymapMatch, + keypress: KeyPress, + ) -> KeyPressHandle { + let mods = keypress.mods; + match &keymatch { + KeymapMatch::Full(command) => { + self.pending_keypress + .update(|(pending_keypress, last_time)| { + last_time.take(); + pending_keypress.clear(); + }); + let count = self.count.try_update(|count| count.take()).unwrap(); + let handled = self.run_command(command, count, mods, focus) + == CommandExecuted::Yes; + return KeyPressHandle { + handled, + keymatch, + keypress, + }; + } + KeymapMatch::Multiple(commands) => { + self.pending_keypress + .update(|(pending_keypress, last_time)| { + last_time.take(); + pending_keypress.clear(); + }); + let count = self.count.try_update(|count| count.take()).unwrap(); + for command in commands { + let handled = self.run_command(command, count, mods, focus) + == CommandExecuted::Yes; + if handled { + return KeyPressHandle { + handled, + keymatch, + keypress, + }; + } + } + + return KeyPressHandle { + handled: false, + keymatch, + keypress, + }; + } + KeymapMatch::Prefix => { + // Here pending_keypress contains only a prefix of some keymap, so let's keep + // collecting key presses. + return KeyPressHandle { + handled: true, + keymatch, + keypress, + }; + } + KeymapMatch::None => { + self.pending_keypress + .update(|(pending_keypress, last_time)| { + pending_keypress.clear(); + last_time.take(); + }); + if focus.get_mode() == Mode::Insert { + let old_keypress = keypress.clone(); + let mut keypress = keypress.clone(); + keypress.mods.set(Modifiers::SHIFT, false); + if let KeymapMatch::Full(command) = + self.match_keymap(&[keypress], focus) + { + if let Some(cmd) = self.commands.get(&command) { + if let CommandKind::Move(_) = cmd.kind { + let handled = focus.run_command(cmd, None, mods) + == CommandExecuted::Yes; + return KeyPressHandle { + handled, + keymatch, + keypress: old_keypress, + }; + } + } + } + } + } + } + + let mut mods = keypress.mods; + + #[cfg(target_os = "macos")] + { + mods.set(Modifiers::SHIFT, false); + mods.set(Modifiers::ALT, false); + } + #[cfg(not(target_os = "macos"))] + { + mods.set(Modifiers::SHIFT, false); + mods.set(Modifiers::ALTGR, false); + } + if mods.is_empty() { + if let KeyInput::Keyboard { logical, .. } = &keypress.key { + if let Key::Character(c) = logical { + focus.receive_char(c); + self.count.set(None); + return KeyPressHandle { + handled: true, + keymatch, + keypress, + }; + } else if let Key::Named(NamedKey::Space) = logical { + focus.receive_char(" "); + self.count.set(None); + return KeyPressHandle { + handled: true, + keymatch, + keypress, + }; + } + } + } + + KeyPressHandle { + handled: false, + keymatch, + keypress, + } + } + + fn get_key_modifiers(key_event: &KeyEvent) -> Modifiers { + let mut mods = key_event.modifiers; + + match &key_event.key.logical_key { + Key::Named(NamedKey::Shift) => mods.set(Modifiers::SHIFT, false), + Key::Named(NamedKey::Alt) => mods.set(Modifiers::ALT, false), + Key::Named(NamedKey::Meta) => mods.set(Modifiers::META, false), + Key::Named(NamedKey::Control) => mods.set(Modifiers::CONTROL, false), + Key::Named(NamedKey::AltGraph) => mods.set(Modifiers::ALTGR, false), + _ => (), + } + + mods + } + + fn match_keymap( + &self, + keypresses: &[KeyPress], + check: &T, + ) -> KeymapMatch { + let keypresses: Vec = + keypresses.iter().filter_map(|k| k.keymap_press()).collect(); + let matches: Vec<_> = self + .keymaps + .get(&keypresses) + .map(|keymaps| { + keymaps + .iter() + .filter(|keymap| { + if check.expect_char() + && keypresses.len() == 1 + && keypresses[0].is_char() + { + return false; + } + if !keymap.modes.is_empty() + && !keymap.modes.contains(check.get_mode().into()) + { + return false; + } + if let Some(condition) = &keymap.when { + if !Self::check_condition(condition, check) { + return false; + } + } + true + }) + .collect() + }) + .unwrap_or_default(); + + if matches.is_empty() { + KeymapMatch::None + } else if matches.len() == 1 && matches[0].key == keypresses { + KeymapMatch::Full(matches[0].command.clone()) + } else if matches.len() > 1 + && matches.iter().filter(|m| m.key != keypresses).count() == 0 + { + KeymapMatch::Multiple( + matches.iter().rev().map(|m| m.command.clone()).collect(), + ) + } else { + KeymapMatch::Prefix + } + } + + fn check_condition( + condition: &str, + check: &T, + ) -> bool { + fn check_one_condition( + condition: &str, + check: &T, + ) -> bool { + let trimmed = condition.trim(); + if let Some(stripped) = trimmed.strip_prefix('!') { + if let Ok(condition) = Condition::from_str(stripped) { + !check.check_condition(condition) + } else { + true + } + } else if let Ok(condition) = Condition::from_str(trimmed) { + check.check_condition(condition) + } else { + false + } + } + + match CheckCondition::parse_first(condition) { + CheckCondition::Single(condition) => { + check_one_condition(condition, check) + } + CheckCondition::Or(left, right) => { + let left = check_one_condition(left, check); + let right = Self::check_condition(right, check); + + left || right + } + CheckCondition::And(left, right) => { + let left = check_one_condition(left, check); + let right = Self::check_condition(right, check); + + left && right + } + } + } + + #[allow(clippy::type_complexity)] + fn get_keymaps( + config: &LapceConfig, + ) -> Result<( + IndexMap, Vec>, + IndexMap>, + )> { + let is_modal = config.core.modal; + + let mut loader = KeyMapLoader::new(); + + if let Err(err) = loader.load_from_str(DEFAULT_KEYMAPS_COMMON, is_modal) { + trace!(TraceLevel::ERROR, "Failed to load common defaults: {err}"); + } + + let os_keymaps = if std::env::consts::OS == "macos" { + DEFAULT_KEYMAPS_MACOS + } else { + DEFAULT_KEYMAPS_NONMACOS + }; + + if let Err(err) = loader.load_from_str(os_keymaps, is_modal) { + trace!(TraceLevel::ERROR, "Failed to load OS defaults: {err}"); + } + + if let Some(path) = Self::file() { + if let Ok(content) = std::fs::read_to_string(&path) { + if let Err(err) = loader.load_from_str(&content, is_modal) { + trace!(TraceLevel::WARN, "Failed to load from {path:?}: {err}"); + } + } + } + + Ok(loader.finalize()) + } + + pub fn file() -> Option { + LapceConfig::keymaps_file() + } + + fn get_file_array() -> Option { + let path = Self::file()?; + let content = std::fs::read_to_string(path).ok()?; + let document: toml_edit::Document = content.parse().ok()?; + document + .as_table() + .get("keymaps")? + .as_array_of_tables() + .cloned() + } + + pub fn update_file(keymap: &KeyMap, keys: &[KeyMapPress]) -> Option<()> { + let mut array = Self::get_file_array().unwrap_or_default(); + let index = array.iter().position(|value| { + Some(keymap.command.as_str()) + == value.get("command").and_then(|c| c.as_str()) + && keymap.when.as_deref() + == value.get("when").and_then(|w| w.as_str()) + && keymap.modes == get_modes(value) + && Some(keymap.key.clone()) + == value + .get("key") + .and_then(|v| v.as_str()) + .map(KeyMapPress::parse) + }); + + if let Some(index) = index { + if !keys.is_empty() { + array.get_mut(index)?.insert( + "key", + toml_edit::value(toml_edit::Value::from(keys.iter().join(" "))), + ); + } else { + array.remove(index); + }; + } else { + let mut table = toml_edit::Table::new(); + table.insert( + "command", + toml_edit::value(toml_edit::Value::from(keymap.command.clone())), + ); + if !keymap.modes.is_empty() { + table.insert( + "mode", + toml_edit::value(toml_edit::Value::from( + keymap.modes.to_string(), + )), + ); + } + if let Some(when) = keymap.when.as_ref() { + table.insert( + "when", + toml_edit::value(toml_edit::Value::from(when.to_string())), + ); + } + + if !keys.is_empty() { + table.insert( + "key", + toml_edit::value(toml_edit::Value::from(keys.iter().join(" "))), + ); + array.push(table.clone()); + } + + if !keymap.key.is_empty() { + table.insert( + "key", + toml_edit::value(toml_edit::Value::from( + keymap.key.iter().join(" "), + )), + ); + table.insert( + "command", + toml_edit::value(toml_edit::Value::from(format!( + "-{}", + keymap.command + ))), + ); + array.push(table.clone()); + } + } + + let mut table = toml_edit::Document::new(); + table.insert("keymaps", toml_edit::Item::ArrayOfTables(array)); + let path = Self::file()?; + std::fs::write(path, table.to_string().as_bytes()).ok()?; + None + } +} + +fn get_modes(toml_keymap: &toml_edit::Table) -> Modes { + toml_keymap + .get("mode") + .and_then(|v| v.as_str()) + .map(Modes::parse) + .unwrap_or_else(Modes::empty) +} diff --git a/lapce-app/src/keypress/condition.rs b/lapce-app/src/keypress/condition.rs new file mode 100644 index 0000000..c1f8d18 --- /dev/null +++ b/lapce-app/src/keypress/condition.rs @@ -0,0 +1,162 @@ +use strum_macros::EnumString; + +#[derive(Debug, PartialEq, Eq)] +pub(super) enum CheckCondition<'a> { + Single(&'a str), + Or(&'a str, &'a str), + And(&'a str, &'a str), +} + +impl<'a> CheckCondition<'a> { + pub(super) fn parse_first(condition: &'a str) -> Self { + let or = condition.match_indices("||").next(); + let and = condition.match_indices("&&").next(); + + match (or, and) { + (None, None) => CheckCondition::Single(condition), + (Some((pos, _)), None) => { + CheckCondition::Or(&condition[..pos], &condition[pos + 2..]) + } + (None, Some((pos, _))) => { + CheckCondition::And(&condition[..pos], &condition[pos + 2..]) + } + (Some((or_pos, _)), Some((and_pos, _))) => { + if or_pos < and_pos { + CheckCondition::Or( + &condition[..or_pos], + &condition[or_pos + 2..], + ) + } else { + CheckCondition::And( + &condition[..and_pos], + &condition[and_pos + 2..], + ) + } + } + } + } +} + +#[derive(Clone, Copy, Debug, EnumString, PartialEq, Eq)] +pub enum Condition { + #[strum(serialize = "editor_focus")] + EditorFocus, + #[strum(serialize = "input_focus")] + InputFocus, + #[strum(serialize = "list_focus")] + ListFocus, + #[strum(serialize = "palette_focus")] + PaletteFocus, + #[strum(serialize = "completion_focus")] + CompletionFocus, + #[strum(serialize = "inline_completion_visible")] + InlineCompletionVisible, + #[strum(serialize = "modal_focus")] + ModalFocus, + #[strum(serialize = "in_snippet")] + InSnippet, + #[strum(serialize = "terminal_focus")] + TerminalFocus, + #[strum(serialize = "source_control_focus")] + SourceControlFocus, + #[strum(serialize = "panel_focus")] + PanelFocus, + #[strum(serialize = "rename_focus")] + RenameFocus, + #[strum(serialize = "search_active")] + SearchActive, + #[strum(serialize = "on_screen_find_active")] + OnScreenFindActive, + #[strum(serialize = "search_focus")] + SearchFocus, + #[strum(serialize = "replace_focus")] + ReplaceFocus, +} + +#[cfg(test)] +mod test { + use floem::keyboard::Modifiers; + use lapce_core::mode::Mode; + + use super::Condition; + use crate::keypress::{KeyPressData, KeyPressFocus, condition::CheckCondition}; + + #[derive(Clone, Copy, Debug)] + struct MockFocus { + accepted_conditions: &'static [Condition], + } + + impl KeyPressFocus for MockFocus { + fn check_condition(&self, condition: Condition) -> bool { + self.accepted_conditions.contains(&condition) + } + + fn get_mode(&self) -> Mode { + unimplemented!() + } + + fn run_command( + &self, + _command: &crate::command::LapceCommand, + _count: Option, + _mods: Modifiers, + ) -> crate::command::CommandExecuted { + unimplemented!() + } + + fn receive_char(&self, _c: &str) { + unimplemented!() + } + } + + #[test] + fn test_parse() { + assert_eq!( + CheckCondition::Or("foo", "bar"), + CheckCondition::parse_first("foo||bar") + ); + assert_eq!( + CheckCondition::And("foo", "bar"), + CheckCondition::parse_first("foo&&bar") + ); + assert_eq!( + CheckCondition::And("foo", "bar||baz"), + CheckCondition::parse_first("foo&&bar||baz") + ); + assert_eq!( + CheckCondition::And("foo ", " bar || baz"), + CheckCondition::parse_first("foo && bar || baz") + ); + } + + #[test] + fn test_check_condition() { + let focus = MockFocus { + accepted_conditions: &[Condition::EditorFocus, Condition::ListFocus], + }; + + let test_cases = [ + ("editor_focus", true), + ("list_focus", true), + ("!editor_focus", false), + ("!list_focus", false), + ("editor_focus || list_focus", true), + ("editor_focus || !list_focus", true), + ("!editor_focus || list_focus", true), + ("editor_focus && list_focus", true), + ("editor_focus && !list_focus", false), + ("!editor_focus && list_focus", false), + ("editor_focus && list_focus || baz", true), + ("editor_focus && list_focus && baz", false), + ("editor_focus && list_focus && !baz", true), + ]; + + for (condition, should_accept) in test_cases.into_iter() { + assert_eq!( + should_accept, + KeyPressData::check_condition(condition, &focus), + "Condition check failed. Condition: {condition}. Expected result: {should_accept}", + ); + } + } +} diff --git a/lapce-app/src/keypress/key.rs b/lapce-app/src/keypress/key.rs new file mode 100644 index 0000000..a78a413 --- /dev/null +++ b/lapce-app/src/keypress/key.rs @@ -0,0 +1,74 @@ +use floem::keyboard::{Key, KeyLocation, NamedKey, PhysicalKey}; + +use super::keymap::KeyMapKey; + +#[derive(Clone, Debug)] +pub(crate) enum KeyInput { + Keyboard { + physical: PhysicalKey, + logical: Key, + location: KeyLocation, + key_without_modifiers: Key, + repeat: bool, + }, + Pointer(floem::pointer::PointerButton), +} + +impl KeyInput { + pub fn keymap_key(&self) -> Option { + if let KeyInput::Keyboard { + repeat, logical, .. + } = self + { + if *repeat + && (matches!( + logical, + Key::Named(NamedKey::Meta) + | Key::Named(NamedKey::Shift) + | Key::Named(NamedKey::Alt) + | Key::Named(NamedKey::Control), + )) + { + return None; + } + } + + Some(match self { + KeyInput::Pointer(b) => KeyMapKey::Pointer(*b), + KeyInput::Keyboard { + physical, + key_without_modifiers, + logical, + location, + .. + } => { + #[allow(clippy::single_match)] + match location { + KeyLocation::Numpad => { + return Some(KeyMapKey::Logical(logical.to_owned())); + } + _ => {} + } + + match key_without_modifiers { + Key::Named(_) => { + KeyMapKey::Logical(key_without_modifiers.to_owned()) + } + Key::Character(c) => { + if c == " " { + KeyMapKey::Logical(Key::Named(NamedKey::Space)) + } else if c.len() == 1 && c.is_ascii() { + KeyMapKey::Logical(Key::Character( + c.to_lowercase().into(), + )) + } else { + KeyMapKey::Physical(*physical) + } + } + Key::Unidentified(_) => KeyMapKey::Physical(*physical), + Key::Dead(_) => KeyMapKey::Physical(*physical), + } + } + }) + } +} diff --git a/lapce-app/src/keypress/keymap.rs b/lapce-app/src/keypress/keymap.rs new file mode 100644 index 0000000..da3401d --- /dev/null +++ b/lapce-app/src/keypress/keymap.rs @@ -0,0 +1,707 @@ +use std::{fmt::Display, str::FromStr}; + +use floem::{ + keyboard::{Key, KeyCode, Modifiers, NamedKey, PhysicalKey}, + pointer::{MouseButton, PointerButton}, +}; +use lapce_core::mode::Modes; + +#[derive(PartialEq, Debug, Clone)] +pub enum KeymapMatch { + Full(String), + Multiple(Vec), + Prefix, + None, +} + +#[derive(PartialEq, Eq, Hash, Clone, Debug)] +pub struct KeyMap { + pub key: Vec, + pub modes: Modes, + pub when: Option, + pub command: String, +} + +#[derive(PartialEq, Eq, Clone, Debug)] +pub enum KeyMapKey { + Pointer(PointerButton), + Logical(Key), + Physical(PhysicalKey), +} + +impl std::hash::Hash for KeyMapKey { + fn hash(&self, state: &mut H) { + match self { + Self::Pointer(btn) => (btn.mouse_button() as u8).hash(state), + Self::Logical(key) => key.hash(state), + Self::Physical(physical) => physical.hash(state), + } + } +} + +#[derive(PartialEq, Eq, Hash, Clone, Debug)] +pub struct KeyMapPress { + pub key: KeyMapKey, + pub mods: Modifiers, +} + +impl KeyMapPress { + pub fn is_char(&self) -> bool { + let mut mods = self.mods; + mods.set(Modifiers::SHIFT, false); + if mods.is_empty() { + if let KeyMapKey::Logical(Key::Character(_)) = &self.key { + return true; + } + } + false + } + + pub fn is_modifiers(&self) -> bool { + if let KeyMapKey::Physical(physical) = &self.key { + matches!( + physical, + PhysicalKey::Code(KeyCode::Meta) + | PhysicalKey::Code(KeyCode::SuperLeft) + | PhysicalKey::Code(KeyCode::SuperRight) + | PhysicalKey::Code(KeyCode::ShiftLeft) + | PhysicalKey::Code(KeyCode::ShiftRight) + | PhysicalKey::Code(KeyCode::ControlLeft) + | PhysicalKey::Code(KeyCode::ControlRight) + | PhysicalKey::Code(KeyCode::AltLeft) + | PhysicalKey::Code(KeyCode::AltRight) + ) + } else if let KeyMapKey::Logical(Key::Named(key)) = &self.key { + matches!( + key, + NamedKey::Meta + | NamedKey::Super + | NamedKey::Shift + | NamedKey::Control + | NamedKey::Alt + | NamedKey::AltGraph + ) + } else { + false + } + } + + pub fn label(&self) -> String { + let mut keys = String::from(""); + if self.mods.control() { + keys.push_str("Ctrl+"); + } + if self.mods.alt() { + keys.push_str("Alt+"); + } + if self.mods.altgr() { + keys.push_str("AltGr+"); + } + if self.mods.meta() { + let keyname = match std::env::consts::OS { + "macos" => "Cmd+", + "windows" => "Win+", + _ => "Meta+", + }; + keys.push_str(keyname); + } + if self.mods.shift() { + keys.push_str("Shift+"); + } + keys.push_str(&self.key.to_string()); + keys + } + + pub fn parse(key: &str) -> Vec { + key.split(' ') + .filter_map(|k| { + let (modifiers, key) = if k == "+" { + ("", "+") + } else if let Some(remaining) = k.strip_suffix("++") { + (remaining, "+") + } else { + match k.rsplit_once('+') { + Some(pair) => pair, + None => ("", k), + } + }; + + let key = match key.parse().ok() { + Some(key) => key, + None => { + // Skip past unrecognized key definitions + tracing::warn!("Unrecognized key: {key}"); + return None; + } + }; + + let mut mods = Modifiers::empty(); + for part in modifiers.to_lowercase().split('+') { + match part { + "ctrl" => mods.set(Modifiers::CONTROL, true), + "meta" => mods.set(Modifiers::META, true), + "shift" => mods.set(Modifiers::SHIFT, true), + "alt" => mods.set(Modifiers::ALT, true), + "altgr" => mods.set(Modifiers::ALTGR, true), + "" => (), + other => tracing::warn!("Invalid key modifier: {}", other), + } + } + + Some(KeyMapPress { key, mods }) + }) + .collect() + } +} + +impl FromStr for KeyMapKey { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + let key = if s.starts_with('[') && s.ends_with(']') { + let code = match s[1..s.len() - 2].to_lowercase().as_str() { + "esc" => KeyCode::Escape, + "space" => KeyCode::Space, + "bs" => KeyCode::Backspace, + "up" => KeyCode::ArrowUp, + "down" => KeyCode::ArrowDown, + "left" => KeyCode::ArrowLeft, + "right" => KeyCode::ArrowRight, + "del" => KeyCode::Delete, + "alt" => KeyCode::AltLeft, + "altgraph" => KeyCode::AltRight, + "capslock" => KeyCode::CapsLock, + "control" => KeyCode::ControlLeft, + "fn" => KeyCode::Fn, + "fnlock" => KeyCode::FnLock, + "meta" => KeyCode::Meta, + "numlock" => KeyCode::NumLock, + "scrolllock" => KeyCode::ScrollLock, + "shift" => KeyCode::ShiftLeft, + "hyper" => KeyCode::Hyper, + "super" => KeyCode::Meta, + "enter" => KeyCode::Enter, + "tab" => KeyCode::Tab, + "arrowdown" => KeyCode::ArrowDown, + "arrowleft" => KeyCode::ArrowLeft, + "arrowright" => KeyCode::ArrowRight, + "arrowup" => KeyCode::ArrowUp, + "end" => KeyCode::End, + "home" => KeyCode::Home, + "pagedown" => KeyCode::PageDown, + "pageup" => KeyCode::PageUp, + "backspace" => KeyCode::Backspace, + "copy" => KeyCode::Copy, + "cut" => KeyCode::Cut, + "delete" => KeyCode::Delete, + "insert" => KeyCode::Insert, + "paste" => KeyCode::Paste, + "undo" => KeyCode::Undo, + "again" => KeyCode::Again, + "contextmenu" => KeyCode::ContextMenu, + "escape" => KeyCode::Escape, + "find" => KeyCode::Find, + "help" => KeyCode::Help, + "pause" => KeyCode::Pause, + "play" => KeyCode::MediaPlayPause, + "props" => KeyCode::Props, + "select" => KeyCode::Select, + "eject" => KeyCode::Eject, + "power" => KeyCode::Power, + "printscreen" => KeyCode::PrintScreen, + "wakeup" => KeyCode::WakeUp, + "convert" => KeyCode::Convert, + "nonconvert" => KeyCode::NonConvert, + "hiragana" => KeyCode::Hiragana, + "katakana" => KeyCode::Katakana, + "f1" => KeyCode::F1, + "f2" => KeyCode::F2, + "f3" => KeyCode::F3, + "f4" => KeyCode::F4, + "f5" => KeyCode::F5, + "f6" => KeyCode::F6, + "f7" => KeyCode::F7, + "f8" => KeyCode::F8, + "f9" => KeyCode::F9, + "f10" => KeyCode::F10, + "f11" => KeyCode::F11, + "f12" => KeyCode::F12, + "mediastop" => KeyCode::MediaStop, + "open" => KeyCode::Open, + _ => { + return Err(anyhow::anyhow!( + "unrecognized physical key code {}", + &s[1..s.len() - 2] + )); + } + }; + KeyMapKey::Physical(PhysicalKey::Code(code)) + } else { + let key = match s.to_lowercase().as_str() { + "esc" => Key::Named(NamedKey::Escape), + "space" => Key::Named(NamedKey::Space), + "bs" => Key::Named(NamedKey::Backspace), + "up" => Key::Named(NamedKey::ArrowUp), + "down" => Key::Named(NamedKey::ArrowDown), + "left" => Key::Named(NamedKey::ArrowLeft), + "right" => Key::Named(NamedKey::ArrowRight), + "del" => Key::Named(NamedKey::Delete), + "alt" => Key::Named(NamedKey::Alt), + "altgraph" => Key::Named(NamedKey::AltGraph), + "capslock" => Key::Named(NamedKey::CapsLock), + "control" => Key::Named(NamedKey::Control), + "fn" => Key::Named(NamedKey::Fn), + "fnlock" => Key::Named(NamedKey::FnLock), + "meta" => Key::Named(NamedKey::Meta), + "numlock" => Key::Named(NamedKey::NumLock), + "scrolllock" => Key::Named(NamedKey::ScrollLock), + "shift" => Key::Named(NamedKey::Shift), + "hyper" => Key::Named(NamedKey::Hyper), + "super" => Key::Named(NamedKey::Meta), + "enter" => Key::Named(NamedKey::Enter), + "tab" => Key::Named(NamedKey::Tab), + "arrowdown" => Key::Named(NamedKey::ArrowDown), + "arrowleft" => Key::Named(NamedKey::ArrowLeft), + "arrowright" => Key::Named(NamedKey::ArrowRight), + "arrowup" => Key::Named(NamedKey::ArrowUp), + "end" => Key::Named(NamedKey::End), + "home" => Key::Named(NamedKey::Home), + "pagedown" => Key::Named(NamedKey::PageDown), + "pageup" => Key::Named(NamedKey::PageUp), + "backspace" => Key::Named(NamedKey::Backspace), + "copy" => Key::Named(NamedKey::Copy), + "cut" => Key::Named(NamedKey::Cut), + "delete" => Key::Named(NamedKey::Delete), + "insert" => Key::Named(NamedKey::Insert), + "paste" => Key::Named(NamedKey::Paste), + "undo" => Key::Named(NamedKey::Undo), + "again" => Key::Named(NamedKey::Again), + "contextmenu" => Key::Named(NamedKey::ContextMenu), + "escape" => Key::Named(NamedKey::Escape), + "find" => Key::Named(NamedKey::Find), + "help" => Key::Named(NamedKey::Help), + "pause" => Key::Named(NamedKey::Pause), + "play" => Key::Named(NamedKey::MediaPlayPause), + "props" => Key::Named(NamedKey::Props), + "select" => Key::Named(NamedKey::Select), + "eject" => Key::Named(NamedKey::Eject), + "power" => Key::Named(NamedKey::Power), + "printscreen" => Key::Named(NamedKey::PrintScreen), + "wakeup" => Key::Named(NamedKey::WakeUp), + "convert" => Key::Named(NamedKey::Convert), + "nonconvert" => Key::Named(NamedKey::NonConvert), + "hiragana" => Key::Named(NamedKey::Hiragana), + "katakana" => Key::Named(NamedKey::Katakana), + "f1" => Key::Named(NamedKey::F1), + "f2" => Key::Named(NamedKey::F2), + "f3" => Key::Named(NamedKey::F3), + "f4" => Key::Named(NamedKey::F4), + "f5" => Key::Named(NamedKey::F5), + "f6" => Key::Named(NamedKey::F6), + "f7" => Key::Named(NamedKey::F7), + "f8" => Key::Named(NamedKey::F8), + "f9" => Key::Named(NamedKey::F9), + "f10" => Key::Named(NamedKey::F10), + "f11" => Key::Named(NamedKey::F11), + "f12" => Key::Named(NamedKey::F12), + "mediastop" => Key::Named(NamedKey::MediaStop), + "open" => Key::Named(NamedKey::Open), + _ => Key::Character(s.to_lowercase().into()), + }; + KeyMapKey::Logical(key) + }; + Ok(key) + } +} + +impl Display for KeyMapPress { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.mods.contains(Modifiers::CONTROL) { + if let Err(err) = f.write_str("Ctrl+") { + tracing::error!("{:?}", err); + } + } + if self.mods.contains(Modifiers::ALT) { + if let Err(err) = f.write_str("Alt+") { + tracing::error!("{:?}", err); + } + } + if self.mods.contains(Modifiers::ALTGR) { + if let Err(err) = f.write_str("AltGr+") { + tracing::error!("{:?}", err); + } + } + if self.mods.contains(Modifiers::META) { + if let Err(err) = f.write_str("Meta+") { + tracing::error!("{:?}", err); + } + } + if self.mods.contains(Modifiers::SHIFT) { + if let Err(err) = f.write_str("Shift+") { + tracing::error!("{:?}", err); + } + } + f.write_str(&self.key.to_string()) + } +} + +impl Display for KeyMapKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use floem::pointer::PointerButton as B; + + match self { + Self::Physical(physical) => { + f.write_str("[")?; + match physical { + PhysicalKey::Unidentified(_) => f.write_str("Unidentified"), + PhysicalKey::Code(KeyCode::Backquote) => { + f.write_str("Backquote") + } + PhysicalKey::Code(KeyCode::Backslash) => { + f.write_str("Backslash") + } + PhysicalKey::Code(KeyCode::BracketLeft) => { + f.write_str("BracketLeft") + } + PhysicalKey::Code(KeyCode::BracketRight) => { + f.write_str("BracketRight") + } + PhysicalKey::Code(KeyCode::Comma) => f.write_str("Comma"), + PhysicalKey::Code(KeyCode::Digit0) => f.write_str("0"), + PhysicalKey::Code(KeyCode::Digit1) => f.write_str("1"), + PhysicalKey::Code(KeyCode::Digit2) => f.write_str("2"), + PhysicalKey::Code(KeyCode::Digit3) => f.write_str("3"), + PhysicalKey::Code(KeyCode::Digit4) => f.write_str("4"), + PhysicalKey::Code(KeyCode::Digit5) => f.write_str("5"), + PhysicalKey::Code(KeyCode::Digit6) => f.write_str("6"), + PhysicalKey::Code(KeyCode::Digit7) => f.write_str("7"), + PhysicalKey::Code(KeyCode::Digit8) => f.write_str("8"), + PhysicalKey::Code(KeyCode::Digit9) => f.write_str("9"), + PhysicalKey::Code(KeyCode::Equal) => f.write_str("Equal"), + PhysicalKey::Code(KeyCode::IntlBackslash) => { + f.write_str("IntlBackslash") + } + PhysicalKey::Code(KeyCode::IntlRo) => f.write_str("IntlRo"), + PhysicalKey::Code(KeyCode::IntlYen) => f.write_str("IntlYen"), + PhysicalKey::Code(KeyCode::KeyA) => f.write_str("A"), + PhysicalKey::Code(KeyCode::KeyB) => f.write_str("B"), + PhysicalKey::Code(KeyCode::KeyC) => f.write_str("C"), + PhysicalKey::Code(KeyCode::KeyD) => f.write_str("D"), + PhysicalKey::Code(KeyCode::KeyE) => f.write_str("E"), + PhysicalKey::Code(KeyCode::KeyF) => f.write_str("F"), + PhysicalKey::Code(KeyCode::KeyG) => f.write_str("G"), + PhysicalKey::Code(KeyCode::KeyH) => f.write_str("H"), + PhysicalKey::Code(KeyCode::KeyI) => f.write_str("I"), + PhysicalKey::Code(KeyCode::KeyJ) => f.write_str("J"), + PhysicalKey::Code(KeyCode::KeyK) => f.write_str("K"), + PhysicalKey::Code(KeyCode::KeyL) => f.write_str("L"), + PhysicalKey::Code(KeyCode::KeyM) => f.write_str("M"), + PhysicalKey::Code(KeyCode::KeyN) => f.write_str("N"), + PhysicalKey::Code(KeyCode::KeyO) => f.write_str("O"), + PhysicalKey::Code(KeyCode::KeyP) => f.write_str("P"), + PhysicalKey::Code(KeyCode::KeyQ) => f.write_str("Q"), + PhysicalKey::Code(KeyCode::KeyR) => f.write_str("R"), + PhysicalKey::Code(KeyCode::KeyS) => f.write_str("S"), + PhysicalKey::Code(KeyCode::KeyT) => f.write_str("T"), + PhysicalKey::Code(KeyCode::KeyU) => f.write_str("U"), + PhysicalKey::Code(KeyCode::KeyV) => f.write_str("V"), + PhysicalKey::Code(KeyCode::KeyW) => f.write_str("W"), + PhysicalKey::Code(KeyCode::KeyX) => f.write_str("X"), + PhysicalKey::Code(KeyCode::KeyY) => f.write_str("Y"), + PhysicalKey::Code(KeyCode::KeyZ) => f.write_str("Z"), + PhysicalKey::Code(KeyCode::Minus) => f.write_str("Minus"), + PhysicalKey::Code(KeyCode::Period) => f.write_str("Period"), + PhysicalKey::Code(KeyCode::Quote) => f.write_str("Quote"), + PhysicalKey::Code(KeyCode::Semicolon) => { + f.write_str("Semicolon") + } + PhysicalKey::Code(KeyCode::Slash) => f.write_str("Slash"), + PhysicalKey::Code(KeyCode::AltLeft) => f.write_str("Alt"), + PhysicalKey::Code(KeyCode::AltRight) => f.write_str("Alt"), + PhysicalKey::Code(KeyCode::Backspace) => { + f.write_str("Backspace") + } + PhysicalKey::Code(KeyCode::CapsLock) => f.write_str("CapsLock"), + PhysicalKey::Code(KeyCode::ContextMenu) => { + f.write_str("ContextMenu") + } + PhysicalKey::Code(KeyCode::ControlLeft) => f.write_str("Ctrl"), + PhysicalKey::Code(KeyCode::ControlRight) => f.write_str("Ctrl"), + PhysicalKey::Code(KeyCode::Enter) => f.write_str("Enter"), + PhysicalKey::Code(KeyCode::SuperLeft) => f.write_str("Meta"), + PhysicalKey::Code(KeyCode::SuperRight) => f.write_str("Meta"), + PhysicalKey::Code(KeyCode::ShiftLeft) => f.write_str("Shift"), + PhysicalKey::Code(KeyCode::ShiftRight) => f.write_str("Shift"), + PhysicalKey::Code(KeyCode::Space) => f.write_str("Space"), + PhysicalKey::Code(KeyCode::Tab) => f.write_str("Tab"), + PhysicalKey::Code(KeyCode::Convert) => f.write_str("Convert"), + PhysicalKey::Code(KeyCode::KanaMode) => f.write_str("KanaMode"), + PhysicalKey::Code(KeyCode::Lang1) => f.write_str("Lang1"), + PhysicalKey::Code(KeyCode::Lang2) => f.write_str("Lang2"), + PhysicalKey::Code(KeyCode::Lang3) => f.write_str("Lang3"), + PhysicalKey::Code(KeyCode::Lang4) => f.write_str("Lang4"), + PhysicalKey::Code(KeyCode::Lang5) => f.write_str("Lang5"), + PhysicalKey::Code(KeyCode::NonConvert) => { + f.write_str("NonConvert") + } + PhysicalKey::Code(KeyCode::Delete) => f.write_str("Delete"), + PhysicalKey::Code(KeyCode::End) => f.write_str("End"), + PhysicalKey::Code(KeyCode::Help) => f.write_str("Help"), + PhysicalKey::Code(KeyCode::Home) => f.write_str("Home"), + PhysicalKey::Code(KeyCode::Insert) => f.write_str("Insert"), + PhysicalKey::Code(KeyCode::PageDown) => f.write_str("PageDown"), + PhysicalKey::Code(KeyCode::PageUp) => f.write_str("PageUp"), + PhysicalKey::Code(KeyCode::ArrowDown) => f.write_str("Down"), + PhysicalKey::Code(KeyCode::ArrowLeft) => f.write_str("Left"), + PhysicalKey::Code(KeyCode::ArrowRight) => f.write_str("Right"), + PhysicalKey::Code(KeyCode::ArrowUp) => f.write_str("Up"), + PhysicalKey::Code(KeyCode::NumLock) => f.write_str("NumLock"), + PhysicalKey::Code(KeyCode::Numpad0) => f.write_str("Numpad0"), + PhysicalKey::Code(KeyCode::Numpad1) => f.write_str("Numpad1"), + PhysicalKey::Code(KeyCode::Numpad2) => f.write_str("Numpad2"), + PhysicalKey::Code(KeyCode::Numpad3) => f.write_str("Numpad3"), + PhysicalKey::Code(KeyCode::Numpad4) => f.write_str("Numpad4"), + PhysicalKey::Code(KeyCode::Numpad5) => f.write_str("Numpad5"), + PhysicalKey::Code(KeyCode::Numpad6) => f.write_str("Numpad6"), + PhysicalKey::Code(KeyCode::Numpad7) => f.write_str("Numpad7"), + PhysicalKey::Code(KeyCode::Numpad8) => f.write_str("Numpad8"), + PhysicalKey::Code(KeyCode::Numpad9) => f.write_str("Numpad9"), + PhysicalKey::Code(KeyCode::NumpadAdd) => { + f.write_str("NumpadAdd") + } + PhysicalKey::Code(KeyCode::NumpadBackspace) => { + f.write_str("NumpadBackspace") + } + PhysicalKey::Code(KeyCode::NumpadClear) => { + f.write_str("NumpadClear") + } + PhysicalKey::Code(KeyCode::NumpadClearEntry) => { + f.write_str("NumpadClearEntry") + } + PhysicalKey::Code(KeyCode::NumpadComma) => { + f.write_str("NumpadComma") + } + PhysicalKey::Code(KeyCode::NumpadDecimal) => { + f.write_str("NumpadDecimal") + } + PhysicalKey::Code(KeyCode::NumpadDivide) => { + f.write_str("NumpadDivide") + } + PhysicalKey::Code(KeyCode::NumpadEnter) => { + f.write_str("NumpadEnter") + } + PhysicalKey::Code(KeyCode::NumpadEqual) => { + f.write_str("NumpadEqual") + } + PhysicalKey::Code(KeyCode::NumpadHash) => { + f.write_str("NumpadHash") + } + PhysicalKey::Code(KeyCode::NumpadMemoryAdd) => { + f.write_str("NumpadMemoryAdd") + } + PhysicalKey::Code(KeyCode::NumpadMemoryClear) => { + f.write_str("NumpadMemoryClear") + } + PhysicalKey::Code(KeyCode::NumpadMemoryRecall) => { + f.write_str("NumpadMemoryRecall") + } + PhysicalKey::Code(KeyCode::NumpadMemoryStore) => { + f.write_str("NumpadMemoryStore") + } + PhysicalKey::Code(KeyCode::NumpadMemorySubtract) => { + f.write_str("NumpadMemorySubtract") + } + PhysicalKey::Code(KeyCode::NumpadMultiply) => { + f.write_str("NumpadMultiply") + } + PhysicalKey::Code(KeyCode::NumpadParenLeft) => { + f.write_str("NumpadParenLeft") + } + PhysicalKey::Code(KeyCode::NumpadParenRight) => { + f.write_str("NumpadParenRight") + } + PhysicalKey::Code(KeyCode::NumpadStar) => { + f.write_str("NumpadStar") + } + PhysicalKey::Code(KeyCode::NumpadSubtract) => { + f.write_str("NumpadSubtract") + } + PhysicalKey::Code(KeyCode::Escape) => f.write_str("Escape"), + PhysicalKey::Code(KeyCode::Fn) => f.write_str("Fn"), + PhysicalKey::Code(KeyCode::FnLock) => f.write_str("FnLock"), + PhysicalKey::Code(KeyCode::PrintScreen) => { + f.write_str("PrintScreen") + } + PhysicalKey::Code(KeyCode::ScrollLock) => { + f.write_str("ScrollLock") + } + PhysicalKey::Code(KeyCode::Pause) => f.write_str("Pause"), + PhysicalKey::Code(KeyCode::BrowserBack) => { + f.write_str("BrowserBack") + } + PhysicalKey::Code(KeyCode::BrowserFavorites) => { + f.write_str("BrowserFavorites") + } + PhysicalKey::Code(KeyCode::BrowserForward) => { + f.write_str("BrowserForward") + } + PhysicalKey::Code(KeyCode::BrowserHome) => { + f.write_str("BrowserHome") + } + PhysicalKey::Code(KeyCode::BrowserRefresh) => { + f.write_str("BrowserRefresh") + } + PhysicalKey::Code(KeyCode::BrowserSearch) => { + f.write_str("BrowserSearch") + } + PhysicalKey::Code(KeyCode::BrowserStop) => { + f.write_str("BrowserStop") + } + PhysicalKey::Code(KeyCode::Eject) => f.write_str("Eject"), + PhysicalKey::Code(KeyCode::LaunchApp1) => { + f.write_str("LaunchApp1") + } + PhysicalKey::Code(KeyCode::LaunchApp2) => { + f.write_str("LaunchApp2") + } + PhysicalKey::Code(KeyCode::LaunchMail) => { + f.write_str("LaunchMail") + } + PhysicalKey::Code(KeyCode::MediaPlayPause) => { + f.write_str("MediaPlayPause") + } + PhysicalKey::Code(KeyCode::MediaSelect) => { + f.write_str("MediaSelect") + } + PhysicalKey::Code(KeyCode::MediaStop) => { + f.write_str("MediaStop") + } + PhysicalKey::Code(KeyCode::MediaTrackNext) => { + f.write_str("MediaTrackNext") + } + PhysicalKey::Code(KeyCode::MediaTrackPrevious) => { + f.write_str("MediaTrackPrevious") + } + PhysicalKey::Code(KeyCode::Power) => f.write_str("Power"), + PhysicalKey::Code(KeyCode::Sleep) => f.write_str("Sleep"), + PhysicalKey::Code(KeyCode::AudioVolumeDown) => { + f.write_str("AudioVolumeDown") + } + PhysicalKey::Code(KeyCode::AudioVolumeMute) => { + f.write_str("AudioVolumeMute") + } + PhysicalKey::Code(KeyCode::AudioVolumeUp) => { + f.write_str("AudioVolumeUp") + } + PhysicalKey::Code(KeyCode::WakeUp) => f.write_str("WakeUp"), + PhysicalKey::Code(KeyCode::Meta) => match std::env::consts::OS { + "macos" => f.write_str("Cmd"), + "windows" => f.write_str("Win"), + _ => f.write_str("Meta"), + }, + PhysicalKey::Code(KeyCode::Hyper) => f.write_str("Hyper"), + PhysicalKey::Code(KeyCode::Turbo) => f.write_str("Turbo"), + PhysicalKey::Code(KeyCode::Abort) => f.write_str("Abort"), + PhysicalKey::Code(KeyCode::Resume) => f.write_str("Resume"), + PhysicalKey::Code(KeyCode::Suspend) => f.write_str("Suspend"), + PhysicalKey::Code(KeyCode::Again) => f.write_str("Again"), + PhysicalKey::Code(KeyCode::Copy) => f.write_str("Copy"), + PhysicalKey::Code(KeyCode::Cut) => f.write_str("Cut"), + PhysicalKey::Code(KeyCode::Find) => f.write_str("Find"), + PhysicalKey::Code(KeyCode::Open) => f.write_str("Open"), + PhysicalKey::Code(KeyCode::Paste) => f.write_str("Paste"), + PhysicalKey::Code(KeyCode::Props) => f.write_str("Props"), + PhysicalKey::Code(KeyCode::Select) => f.write_str("Select"), + PhysicalKey::Code(KeyCode::Undo) => f.write_str("Undo"), + PhysicalKey::Code(KeyCode::Hiragana) => f.write_str("Hiragana"), + PhysicalKey::Code(KeyCode::Katakana) => f.write_str("Katakana"), + PhysicalKey::Code(KeyCode::F1) => f.write_str("F1"), + PhysicalKey::Code(KeyCode::F2) => f.write_str("F2"), + PhysicalKey::Code(KeyCode::F3) => f.write_str("F3"), + PhysicalKey::Code(KeyCode::F4) => f.write_str("F4"), + PhysicalKey::Code(KeyCode::F5) => f.write_str("F5"), + PhysicalKey::Code(KeyCode::F6) => f.write_str("F6"), + PhysicalKey::Code(KeyCode::F7) => f.write_str("F7"), + PhysicalKey::Code(KeyCode::F8) => f.write_str("F8"), + PhysicalKey::Code(KeyCode::F9) => f.write_str("F9"), + PhysicalKey::Code(KeyCode::F10) => f.write_str("F10"), + PhysicalKey::Code(KeyCode::F11) => f.write_str("F11"), + PhysicalKey::Code(KeyCode::F12) => f.write_str("F12"), + PhysicalKey::Code(KeyCode::F13) => f.write_str("F13"), + PhysicalKey::Code(KeyCode::F14) => f.write_str("F14"), + PhysicalKey::Code(KeyCode::F15) => f.write_str("F15"), + PhysicalKey::Code(KeyCode::F16) => f.write_str("F16"), + PhysicalKey::Code(KeyCode::F17) => f.write_str("F17"), + PhysicalKey::Code(KeyCode::F18) => f.write_str("F18"), + PhysicalKey::Code(KeyCode::F19) => f.write_str("F19"), + PhysicalKey::Code(KeyCode::F20) => f.write_str("F20"), + PhysicalKey::Code(KeyCode::F21) => f.write_str("F21"), + PhysicalKey::Code(KeyCode::F22) => f.write_str("F22"), + PhysicalKey::Code(KeyCode::F23) => f.write_str("F23"), + PhysicalKey::Code(KeyCode::F24) => f.write_str("F24"), + PhysicalKey::Code(KeyCode::F25) => f.write_str("F25"), + PhysicalKey::Code(KeyCode::F26) => f.write_str("F26"), + PhysicalKey::Code(KeyCode::F27) => f.write_str("F27"), + PhysicalKey::Code(KeyCode::F28) => f.write_str("F28"), + PhysicalKey::Code(KeyCode::F29) => f.write_str("F29"), + PhysicalKey::Code(KeyCode::F30) => f.write_str("F30"), + PhysicalKey::Code(KeyCode::F31) => f.write_str("F31"), + PhysicalKey::Code(KeyCode::F32) => f.write_str("F32"), + PhysicalKey::Code(KeyCode::F33) => f.write_str("F33"), + PhysicalKey::Code(KeyCode::F34) => f.write_str("F34"), + PhysicalKey::Code(KeyCode::F35) => f.write_str("F35"), + _ => f.write_str("Unidentified"), + }?; + f.write_str("]") + } + Self::Logical(key) => match key { + Key::Named(key) => match key { + NamedKey::Backspace => f.write_str("Backspace"), + NamedKey::CapsLock => f.write_str("CapsLock"), + NamedKey::Enter => f.write_str("Enter"), + NamedKey::Delete => f.write_str("Delete"), + NamedKey::End => f.write_str("End"), + NamedKey::Home => f.write_str("Home"), + NamedKey::PageDown => f.write_str("PageDown"), + NamedKey::PageUp => f.write_str("PageUp"), + NamedKey::ArrowDown => f.write_str("ArrowDown"), + NamedKey::ArrowUp => f.write_str("ArrowUp"), + NamedKey::ArrowLeft => f.write_str("ArrowLeft"), + NamedKey::ArrowRight => f.write_str("ArrowRight"), + NamedKey::Escape => f.write_str("Escape"), + NamedKey::Fn => f.write_str("Fn"), + NamedKey::Space => f.write_str("Space"), + NamedKey::Shift => f.write_str("Shift"), + NamedKey::Meta => f.write_str("Meta"), + NamedKey::Super => f.write_str("Meta"), + NamedKey::Control => f.write_str("Ctrl"), + NamedKey::Alt => f.write_str("Alt"), + NamedKey::AltGraph => f.write_str("AltGraph"), + NamedKey::Tab => f.write_str("Tab"), + NamedKey::F1 => f.write_str("F1"), + NamedKey::F2 => f.write_str("F2"), + NamedKey::F3 => f.write_str("F3"), + NamedKey::F4 => f.write_str("F4"), + NamedKey::F5 => f.write_str("F5"), + NamedKey::F6 => f.write_str("F6"), + NamedKey::F7 => f.write_str("F7"), + NamedKey::F8 => f.write_str("F8"), + NamedKey::F9 => f.write_str("F9"), + NamedKey::F10 => f.write_str("F10"), + NamedKey::F11 => f.write_str("F11"), + NamedKey::F12 => f.write_str("F12"), + _ => f.write_str("Unidentified"), + }, + Key::Character(s) => f.write_str(s), + Key::Unidentified(_) => f.write_str("Unidentified"), + Key::Dead(_) => f.write_str("dead"), + }, + Self::Pointer(B::Mouse(MouseButton::Auxiliary)) => { + f.write_str("MouseMiddle") + } + Self::Pointer(B::Mouse(MouseButton::X2)) => f.write_str("MouseForward"), + Self::Pointer(B::Mouse(MouseButton::X1)) => f.write_str("MouseBackward"), + Self::Pointer(_) => f.write_str("MouseUnimplemented"), + } + } +} diff --git a/lapce-app/src/keypress/loader.rs b/lapce-app/src/keypress/loader.rs new file mode 100644 index 0000000..3b9ba94 --- /dev/null +++ b/lapce-app/src/keypress/loader.rs @@ -0,0 +1,253 @@ +use anyhow::{Result, anyhow}; +use indexmap::IndexMap; +use lapce_core::mode::Modes; +use tracing::{debug, error}; + +use super::keymap::{KeyMap, KeyMapPress}; + +pub struct KeyMapLoader { + keymaps: IndexMap, Vec>, + command_keymaps: IndexMap>, +} + +impl KeyMapLoader { + pub fn new() -> Self { + Self { + keymaps: Default::default(), + command_keymaps: Default::default(), + } + } + + pub fn load_from_str<'a>( + &'a mut self, + s: &str, + modal: bool, + ) -> Result<&'a mut Self> { + let toml_keymaps: toml_edit::Document = s.parse()?; + let toml_keymaps = toml_keymaps + .get("keymaps") + .and_then(|v| v.as_array_of_tables()) + .ok_or_else(|| anyhow!("no keymaps"))?; + + for toml_keymap in toml_keymaps { + let keymap = match Self::get_keymap(toml_keymap, modal) { + Ok(Some(keymap)) => keymap, + Ok(None) => { + // Keymap ignored + continue; + } + Err(err) => { + error!("Could not parse keymap: {err}"); + continue; + } + }; + + let (command, bind) = match keymap.command.strip_prefix('-') { + Some(cmd) => (cmd.to_string(), false), + None => (keymap.command.clone(), true), + }; + + let current_keymaps = self.command_keymaps.entry(command).or_default(); + if bind { + current_keymaps.push(keymap.clone()); + for i in 1..keymap.key.len() + 1 { + let key = keymap.key[..i].to_vec(); + self.keymaps.entry(key).or_default().push(keymap.clone()); + } + } else { + let is_keymap = |k: &KeyMap| -> bool { + k.when == keymap.when + && k.modes == keymap.modes + && k.key == keymap.key + }; + if let Some(index) = current_keymaps.iter().position(is_keymap) { + current_keymaps.remove(index); + } + for i in 1..keymap.key.len() + 1 { + if let Some(keymaps) = self.keymaps.get_mut(&keymap.key[..i]) { + if let Some(index) = keymaps.iter().position(is_keymap) { + keymaps.remove(index); + } + } + } + } + } + + Ok(self) + } + + #[allow(clippy::type_complexity)] + pub fn finalize( + self, + ) -> ( + IndexMap, Vec>, + IndexMap>, + ) { + let Self { + keymaps: map, + command_keymaps: command_map, + } = self; + + (map, command_map) + } + + fn get_keymap( + toml_keymap: &toml_edit::Table, + modal: bool, + ) -> Result> { + let key = toml_keymap + .get("key") + .and_then(|v| v.as_str()) + .ok_or_else(|| anyhow!("no key in keymap"))?; + + let modes = get_modes(toml_keymap); + // If not using modal editing, remove keymaps that only make sense in modal. + if !modal + && !modes.is_empty() + && !modes.contains(Modes::INSERT) + && !modes.contains(Modes::TERMINAL) + { + debug!("Keymap ignored: {}", key); + return Ok(None); + } + + Ok(Some(KeyMap { + key: KeyMapPress::parse(key), + modes, + when: toml_keymap + .get("when") + .and_then(|w| w.as_str()) + .map(|w| w.to_string()), + command: toml_keymap + .get("command") + .and_then(|c| c.as_str()) + .map(|w| w.trim().to_string()) + .unwrap_or_default(), + })) + } +} + +fn get_modes(toml_keymap: &toml_edit::Table) -> Modes { + toml_keymap + .get("mode") + .and_then(|v| v.as_str()) + .map(Modes::parse) + .unwrap_or_else(Modes::empty) +} + +#[cfg(test)] +mod tests { + use floem::keyboard::Key; + + use super::*; + use crate::keypress::keymap::KeyMapKey; + + #[test] + fn test_keymap() { + let keymaps = r#" +[[keymaps]] +key = "ctrl+w l l" +command = "right" +when = "n" + +[[keymaps]] +key = "ctrl+w l" +command = "right" +when = "n" + +[[keymaps]] +key = "ctrl+w h" +command = "left" +when = "n" + +[[keymaps]] +key = "ctrl+w" +command = "left" +when = "n" + +[[keymaps]] +key = "End" +command = "line_end" +when = "n" + +[[keymaps]] +key = "shift+i" +command = "insert_first_non_blank" +when = "n" + +[[keymaps]] +key = "MouseForward" +command = "jump_location_forward" + +[[keymaps]] +key = "MouseBackward" +command = "jump_location_backward" + +[[keymaps]] +key = "Ctrl+MouseMiddle" +command = "goto_definition" + "#; + let mut loader = KeyMapLoader::new(); + loader.load_from_str(keymaps, true).unwrap(); + + let (keymaps, _) = loader.finalize(); + + // Lower case modifiers + let keypress = KeyMapPress::parse("ctrl+w"); + assert_eq!(keymaps.get(&keypress).unwrap().len(), 4); + + let keypress = KeyMapPress::parse("ctrl+w l"); + assert_eq!(keymaps.get(&keypress).unwrap().len(), 2); + + let keypress = KeyMapPress::parse("ctrl+w h"); + assert_eq!(keymaps.get(&keypress).unwrap().len(), 1); + + let keypress = KeyMapPress::parse("ctrl+w l l"); + assert_eq!(keymaps.get(&keypress).unwrap().len(), 1); + + let keypress = KeyMapPress::parse("end"); + assert_eq!(keymaps.get(&keypress).unwrap().len(), 1); + + // Upper case modifiers + let keypress = KeyMapPress::parse("Ctrl+w"); + assert_eq!(keymaps.get(&keypress).unwrap().len(), 4); + + let keypress = KeyMapPress::parse("Ctrl+w l"); + assert_eq!(keymaps.get(&keypress).unwrap().len(), 2); + + let keypress = KeyMapPress::parse("Ctrl+w h"); + assert_eq!(keymaps.get(&keypress).unwrap().len(), 1); + + let keypress = KeyMapPress::parse("Ctrl+w l l"); + assert_eq!(keymaps.get(&keypress).unwrap().len(), 1); + + let keypress = KeyMapPress::parse("End"); + assert_eq!(keymaps.get(&keypress).unwrap().len(), 1); + + // No modifier + let keypress = KeyMapPress::parse("shift+i"); + assert_eq!(keymaps.get(&keypress).unwrap().len(), 1); + + // Mouse keys + let keypress = KeyMapPress::parse("MouseForward"); + assert_eq!(keymaps.get(&keypress).unwrap().len(), 1); + + let keypress = KeyMapPress::parse("mousebackward"); + assert_eq!(keymaps.get(&keypress).unwrap().len(), 1); + + let keypress = KeyMapPress::parse("Ctrl+MouseMiddle"); + assert_eq!(keymaps.get(&keypress).unwrap().len(), 1); + + let keypress = KeyMapPress::parse("Ctrl++"); + assert_eq!( + keypress[0].key, + KeyMapKey::Logical(Key::Character("+".into())) + ); + + let keypress = KeyMapPress::parse("+"); + assert_eq!( + keypress[0].key, + KeyMapKey::Logical(Key::Character("+".into())) + ); + } +} diff --git a/lapce-app/src/keypress/press.rs b/lapce-app/src/keypress/press.rs new file mode 100644 index 0000000..af262ac --- /dev/null +++ b/lapce-app/src/keypress/press.rs @@ -0,0 +1,18 @@ +use floem::keyboard::Modifiers; + +use super::{key::KeyInput, keymap::KeyMapPress}; + +#[derive(Clone, Debug)] +pub struct KeyPress { + pub(super) key: KeyInput, + pub(super) mods: Modifiers, +} + +impl KeyPress { + pub fn keymap_press(&self) -> Option { + self.key.keymap_key().map(|key| KeyMapPress { + key, + mods: self.mods, + }) + } +} diff --git a/lapce-app/src/lib.rs b/lapce-app/src/lib.rs new file mode 100644 index 0000000..43a55ce --- /dev/null +++ b/lapce-app/src/lib.rs @@ -0,0 +1,50 @@ +pub mod about; +pub mod alert; +pub mod app; +pub mod code_action; +pub mod code_lens; +pub mod command; +pub mod completion; +pub mod config; +pub mod db; +pub mod debug; +pub mod doc; +pub mod editor; +pub mod editor_tab; +pub mod file_explorer; +pub mod find; +pub mod focus_text; +pub mod global_search; +pub mod history; +pub mod hover; +pub mod id; +pub mod inline_completion; +pub mod keymap; +pub mod keypress; +pub mod listener; +pub mod lsp; +pub mod main_split; +pub mod markdown; +pub mod palette; +pub mod panel; +pub mod plugin; +pub mod proxy; +pub mod rename; +pub mod settings; +pub mod snippet; +pub mod source_control; +pub mod status; +pub mod terminal; +pub mod text_area; +pub mod text_input; +pub mod title; +pub mod tracing; +pub mod update; +pub mod wave; +pub mod web_link; +pub mod window; +pub mod window_tab; +pub mod workspace; + +#[cfg(windows)] +extern crate windows_sys as windows; diff --git a/lapce-app/src/listener.rs b/lapce-app/src/listener.rs new file mode 100644 index 0000000..55e4fa7 --- /dev/null +++ b/lapce-app/src/listener.rs @@ -0,0 +1,65 @@ +use floem::reactive::{RwSignal, Scope, SignalGet, SignalUpdate}; + +/// A signal listener that receives 'events' from the outside and runs the callback. +/// This is implemented using effects and normal rw signals. This should be used when it doesn't +/// make sense to think of it as 'storing' a value, like an `RwSignal` would typically be used for. +/// +/// Copied/Cloned listeners refer to the same listener. +#[derive(Debug)] +pub struct Listener { + cx: Scope, + val: RwSignal>, +} + +impl Listener { + pub fn new(cx: Scope, on_val: impl Fn(T) + 'static) -> Listener { + let val = cx.create_rw_signal(None); + + let listener = Listener { val, cx }; + listener.listen(on_val); + + listener + } + + /// Construct a listener when you can't yet give it a callback. + /// Call `listen` to set a callback. + pub fn new_empty(cx: Scope) -> Listener { + let val = cx.create_rw_signal(None); + Listener { val, cx } + } + + pub fn scope(&self) -> Scope { + self.cx + } + + /// Listen for values sent to this listener. + pub fn listen(self, on_val: impl Fn(T) + 'static) { + self.listen_with(self.cx, on_val) + } + + /// Listen for values sent to this listener. + /// Allows creating the effect with a custom scope, letting it be disposed of. + pub fn listen_with(self, cx: Scope, on_val: impl Fn(T) + 'static) { + let val = self.val; + + cx.create_effect(move |_| { + // TODO(minor): Signals could have a `take` method to avoid cloning. + if let Some(cmd) = val.get() { + on_val(cmd); + } + }); + } + + /// Send a value to the listener. + pub fn send(&self, v: T) { + self.val.set(Some(v)); + } +} + +impl Copy for Listener {} + +impl Clone for Listener { + fn clone(&self) -> Self { + *self + } +} diff --git a/lapce-app/src/lsp.rs b/lapce-app/src/lsp.rs new file mode 100644 index 0000000..9562f5b --- /dev/null +++ b/lapce-app/src/lsp.rs @@ -0,0 +1,92 @@ +use std::path::PathBuf; + +use tracing::{Level, event}; +use url::Url; + +// Rust-analyzer returns paths in the form of "file:///:/...", which gets parsed into URL +// as "/://" which is then interpreted by PathBuf::new() as a UNIX-like path from root. +// This function strips the additional / from the beginning, if the first segment is a drive letter. +#[cfg(windows)] +pub fn path_from_url(url: &Url) -> PathBuf { + use percent_encoding::percent_decode_str; + + event!(Level::DEBUG, "Converting `{:?}` to path", url); + + if let Ok(path) = url.to_file_path() { + return path; + } + + let path = url.path(); + + let path = if path.contains('%') { + percent_decode_str(path) + .decode_utf8() + .unwrap_or(std::borrow::Cow::from(path)) + } else { + std::borrow::Cow::from(path) + }; + + if let Some(path) = path.strip_prefix('/') { + event!(Level::DEBUG, "Found `/` prefix"); + if let Some((maybe_drive_letter, path_second_part)) = + path.split_once(['/', '\\']) + { + event!(Level::DEBUG, maybe_drive_letter); + event!(Level::DEBUG, path_second_part); + + let b = maybe_drive_letter.as_bytes(); + + if !b.is_empty() && !b[0].is_ascii_alphabetic() { + event!(Level::ERROR, "First byte is not ascii alphabetic: {b:?}"); + } + + match maybe_drive_letter.len() { + 2 => match maybe_drive_letter.chars().nth(1) { + Some(':') => { + event!(Level::DEBUG, "Returning path `{:?}`", path); + return PathBuf::from(path); + } + v => { + event!( + Level::ERROR, + "Unhandled 'maybe_drive_letter' chars: {v:?}" + ); + } + }, + 4 => { + if maybe_drive_letter.contains("%3A") { + let path = path.replace("%3A", ":"); + event!(Level::DEBUG, "Returning path `{:?}`", path); + return PathBuf::from(path); + } else { + event!( + Level::ERROR, + "Unhandled 'maybe_drive_letter' pattern: {maybe_drive_letter:?}" + ); + } + } + v => { + event!( + Level::ERROR, + "Unhandled 'maybe_drive_letter' length: {v}" + ); + } + } + } + } + + event!(Level::DEBUG, "Returning unmodified path `{:?}`", path); + PathBuf::from(path.into_owned()) +} + +#[cfg(not(windows))] +pub fn path_from_url(url: &Url) -> PathBuf { + event!(Level::DEBUG, "Converting `{:?}` to path", url); + url.to_file_path().unwrap_or_else(|_| { + let path = url.path(); + if let Ok(path) = percent_encoding::percent_decode_str(path).decode_utf8() { + return PathBuf::from(path.into_owned()); + } + PathBuf::from(path) + }) +} diff --git a/lapce-app/src/main_split.rs b/lapce-app/src/main_split.rs new file mode 100644 index 0000000..6dabd13 --- /dev/null +++ b/lapce-app/src/main_split.rs @@ -0,0 +1,3105 @@ +use std::{ + collections::HashMap, + path::{Path, PathBuf}, + rc::Rc, +}; + +use floem::{ + action::save_as, + ext_event::create_ext_action, + file::{FileDialogOptions, FileInfo}, + keyboard::Modifiers, + peniko::kurbo::{Point, Rect, Vec2}, + reactive::{Memo, RwSignal, Scope, SignalGet, SignalUpdate, SignalWith}, + views::editor::id::EditorId, +}; +use itertools::Itertools; +use lapce_core::{ + buffer::rope_text::RopeText, command::FocusCommand, cursor::Cursor, + rope_text_pos::RopeTextPosition, selection::Selection, syntax::Syntax, +}; +use lapce_rpc::{ + buffer::BufferId, + core::FileChanged, + plugin::{PluginId, VoltID}, + proxy::ProxyResponse, +}; +use lapce_xi_rope::{Rope, spans::SpansBuilder}; +use lsp_types::{ + CodeAction, CodeActionOrCommand, DiagnosticSeverity, DocumentChangeOperation, + DocumentChanges, OneOf, Position, TextEdit, Url, WorkspaceEdit, +}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use tracing::{Level, event}; + +use crate::{ + alert::AlertButton, + code_lens::CodeLensData, + command::InternalCommand, + doc::{DiagnosticData, Doc, DocContent, DocHistory, EditorDiagnostic}, + editor::{ + EditorData, + diff::DiffEditorData, + location::{EditorLocation, EditorPosition}, + }, + editor_tab::{ + EditorTabChild, EditorTabChildSource, EditorTabData, EditorTabInfo, + }, + id::{ + DiffEditorId, EditorTabId, KeymapId, SettingsId, SplitId, + ThemeColorSettingsId, VoltViewId, + }, + keypress::{EventRef, KeyPressData, KeyPressHandle}, + panel::implementation_view::ReferencesRoot, + window_tab::{CommonData, Focus, WindowTabData}, +}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub enum SplitDirection { + Vertical, + Horizontal, +} + +#[derive(Clone, Copy, Debug)] +pub enum SplitMoveDirection { + Up, + Down, + Right, + Left, +} + +impl SplitMoveDirection { + pub fn direction(&self) -> SplitDirection { + match self { + SplitMoveDirection::Up | SplitMoveDirection::Down => { + SplitDirection::Horizontal + } + SplitMoveDirection::Left | SplitMoveDirection::Right => { + SplitDirection::Vertical + } + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum SplitContent { + EditorTab(EditorTabId), + Split(SplitId), +} + +impl SplitContent { + pub fn id(&self) -> u64 { + match self { + SplitContent::EditorTab(id) => id.to_raw(), + SplitContent::Split(id) => id.to_raw(), + } + } + + pub fn content_info(&self, data: &WindowTabData) -> SplitContentInfo { + match &self { + SplitContent::EditorTab(editor_tab_id) => { + let editor_tab_data = data + .main_split + .editor_tabs + .get_untracked() + .get(editor_tab_id) + .cloned() + .unwrap(); + SplitContentInfo::EditorTab( + editor_tab_data.get_untracked().tab_info(data), + ) + } + SplitContent::Split(split_id) => { + let split_data = data + .main_split + .splits + .get_untracked() + .get(split_id) + .cloned() + .unwrap(); + SplitContentInfo::Split(split_data.get_untracked().split_info(data)) + } + } + } +} + +#[derive(Clone)] +pub struct SplitData { + pub scope: Scope, + pub parent_split: Option, + pub split_id: SplitId, + pub children: Vec<(RwSignal, SplitContent)>, + pub direction: SplitDirection, + pub window_origin: Point, + pub layout_rect: Rect, +} + +#[derive(Clone, Serialize, Deserialize)] +pub struct SplitInfo { + pub children: Vec, + pub direction: SplitDirection, +} + +impl SplitInfo { + pub fn to_data( + &self, + data: MainSplitData, + parent_split: Option, + split_id: SplitId, + ) -> RwSignal { + let split_data = { + let cx = data.scope.create_child(); + let split_data = SplitData { + scope: cx, + split_id, + direction: self.direction, + parent_split, + children: self + .children + .iter() + .map(|child| { + ( + cx.create_rw_signal(1.0), + child.to_data(data.clone(), split_id), + ) + }) + .collect(), + window_origin: Point::ZERO, + layout_rect: Rect::ZERO, + }; + cx.create_rw_signal(split_data) + }; + data.splits.update(|splits| { + splits.insert(split_id, split_data); + }); + split_data + } +} + +#[derive(Clone, Serialize, Deserialize)] +pub enum SplitContentInfo { + EditorTab(EditorTabInfo), + Split(SplitInfo), +} + +impl SplitContentInfo { + pub fn to_data( + &self, + data: MainSplitData, + parent_split: SplitId, + ) -> SplitContent { + match &self { + SplitContentInfo::EditorTab(tab_info) => { + let tab_data = tab_info.to_data(data, parent_split); + SplitContent::EditorTab( + tab_data.with_untracked(|tab_data| tab_data.editor_tab_id), + ) + } + SplitContentInfo::Split(split_info) => { + let split_id = SplitId::next(); + split_info.to_data(data, Some(parent_split), split_id); + SplitContent::Split(split_id) + } + } + } +} + +impl SplitData { + pub fn split_info(&self, data: &WindowTabData) -> SplitInfo { + SplitInfo { + direction: self.direction, + children: self + .children + .iter() + .map(|(_, child)| child.content_info(data)) + .collect(), + } + } + + pub fn editor_tab_index(&self, editor_tab_id: EditorTabId) -> Option { + self.children + .iter() + .position(|(_, c)| c == &SplitContent::EditorTab(editor_tab_id)) + } + + pub fn content_index(&self, content: &SplitContent) -> Option { + self.children.iter().position(|(_, c)| c == content) + } +} + +/// All the editors in a main split +#[derive(Clone, Copy)] +pub struct Editors(pub RwSignal>); +impl Editors { + fn new(cx: Scope) -> Self { + Self(cx.create_rw_signal(im::HashMap::new())) + } + + /// Add an editor to the editors. + /// Returns the id of the editor. + pub fn insert(&self, editor: EditorData) -> EditorId { + let id = editor.id(); + self.0.update(|editors| { + if editors.insert(id, editor).is_some() { + event!(Level::WARN, "Inserted EditorId that already exists"); + } + }); + + id + } + + pub fn insert_with_id(&self, id: EditorId, editor: EditorData) { + self.0.update(|editors| { + editors.insert(id, editor); + }); + } + + pub fn new_local(&self, cx: Scope, common: Rc) -> EditorId { + let editor = EditorData::new_local(cx, *self, common); + + self.insert(editor) + } + + /// Equivalent to [`Self::new_local`], but immediately gets the created editor. + pub fn make_local(&self, cx: Scope, common: Rc) -> EditorData { + let id = self.new_local(cx, common); + self.editor_untracked(id).unwrap() + } + + pub fn new_from_doc( + &self, + cx: Scope, + doc: Rc, + editor_tab_id: Option, + diff_editor_id: Option<(EditorTabId, DiffEditorId)>, + confirmed: Option>, + common: Rc, + ) -> EditorId { + let editor = EditorData::new_doc( + cx, + doc, + editor_tab_id, + diff_editor_id, + confirmed, + common, + ); + + self.insert(editor) + } + + /// Equivalent to [`Self::new_editor_doc`], but immediately gets the created editor. + pub fn make_from_doc( + &self, + cx: Scope, + doc: Rc, + editor_tab_id: Option, + diff_editor_id: Option<(EditorTabId, DiffEditorId)>, + confirmed: Option>, + common: Rc, + ) -> EditorData { + let id = self.new_from_doc( + cx, + doc, + editor_tab_id, + diff_editor_id, + confirmed, + common, + ); + self.editor_untracked(id).unwrap() + } + + /// Copy an existing editor which is inserted into [`Editors`] + pub fn copy( + &self, + editor_id: EditorId, + cx: Scope, + editor_tab_id: Option, + diff_editor_id: Option<(EditorTabId, DiffEditorId)>, + confirmed: Option>, + ) -> Option { + let editor = self.editor_untracked(editor_id)?; + let new_editor = editor.copy(cx, editor_tab_id, diff_editor_id, confirmed); + + Some(self.insert(new_editor)) + } + + pub fn make_copy( + &self, + editor_id: EditorId, + cx: Scope, + editor_tab_id: Option, + diff_editor_id: Option<(EditorTabId, DiffEditorId)>, + confirmed: Option>, + ) -> Option { + let editor_id = + self.copy(editor_id, cx, editor_tab_id, diff_editor_id, confirmed)?; + self.editor_untracked(editor_id) + } + + pub fn remove(&self, id: EditorId) -> Option { + self.0.try_update(|editors| editors.remove(&id)).unwrap() + } + + pub fn get_editor_id_by_path(&self, path: &Path) -> Option { + self.0.with_untracked(|x| { + for (id, data) in x { + if data.doc().content.with_untracked(|x| { + if let Some(doc_path) = x.path() { + doc_path == path + } else { + false + } + }) { + return Some(*id); + } + } + None + }) + } + + pub fn contains_untracked(&self, id: EditorId) -> bool { + self.0.with_untracked(|editors| editors.contains_key(&id)) + } + + /// Get the editor (tracking the signal) + pub fn editor(&self, id: EditorId) -> Option { + self.0.with(|editors| editors.get(&id).cloned()) + } + + /// Get the editor (not tracking the signal) + pub fn editor_untracked(&self, id: EditorId) -> Option { + self.0.with_untracked(|editors| editors.get(&id).cloned()) + } + + pub fn with_editors( + &self, + f: impl FnOnce(&im::HashMap) -> O, + ) -> O { + self.0.with(f) + } + + pub fn with_editors_untracked( + &self, + f: impl FnOnce(&im::HashMap) -> O, + ) -> O { + self.0.with_untracked(f) + } +} + +#[derive(Clone)] +pub struct MainSplitData { + pub scope: Scope, + pub root_split: SplitId, + pub active_editor_tab: RwSignal>, + pub splits: RwSignal>>, + pub editor_tabs: RwSignal>>, + pub editors: Editors, + pub diff_editors: RwSignal>, + pub docs: RwSignal>>, + pub scratch_docs: RwSignal>>, + pub diagnostics: RwSignal>, + pub references: RwSignal, + pub implementations: RwSignal, + pub active_editor: Memo>, + pub find_editor: EditorData, + pub replace_editor: EditorData, + pub locations: RwSignal>, + pub current_location: RwSignal, + pub width: RwSignal, + pub code_lens: RwSignal, + pub common: Rc, +} + +impl std::fmt::Debug for MainSplitData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("MainSplitData") + .field("root_split", &self.root_split) + .finish() + } +} + +impl MainSplitData { + pub fn new(cx: Scope, common: Rc) -> Self { + let splits = cx.create_rw_signal(im::HashMap::new()); + let active_editor_tab = cx.create_rw_signal(None); + let editor_tabs: RwSignal< + im::HashMap>, + > = cx.create_rw_signal(im::HashMap::new()); + let editors = Editors::new(cx); + let diff_editors: RwSignal> = + cx.create_rw_signal(im::HashMap::new()); + let docs: RwSignal>> = + cx.create_rw_signal(im::HashMap::new()); + let scratch_docs = cx.create_rw_signal(im::HashMap::new()); + let locations = cx.create_rw_signal(im::Vector::new()); + let references = cx.create_rw_signal(ReferencesRoot::default()); + let implementations = cx.create_rw_signal( + crate::panel::implementation_view::ReferencesRoot::default(), + ); + let current_location = cx.create_rw_signal(0); + let diagnostics = cx.create_rw_signal(im::HashMap::new()); + let find_editor = editors.make_local(cx, common.clone()); + let replace_editor = editors.make_local(cx, common.clone()); + + let active_editor = cx.create_memo(move |_| -> Option { + let active_editor_tab = active_editor_tab.get()?; + let editor_tab = editor_tabs + .with(|editor_tabs| editor_tabs.get(&active_editor_tab).copied())?; + let (_, _, child) = editor_tab.with(|editor_tab| { + editor_tab.children.get(editor_tab.active).cloned() + })?; + + let editor = match child { + EditorTabChild::Editor(editor_id) => editors.editor(editor_id)?, + EditorTabChild::DiffEditor(diff_editor_id) => { + let diff_editor = diff_editors.with(|diff_editors| { + diff_editors.get(&diff_editor_id).cloned() + })?; + if diff_editor.focus_right.get() { + diff_editor.right + } else { + diff_editor.left + } + } + _ => return None, + }; + + Some(editor) + }); + + { + let buffer = find_editor.doc().buffer; + let find = common.find.clone(); + cx.create_effect(move |_| { + let content = buffer.with(|buffer| buffer.to_string()); + find.set_find(&content); + }); + } + + Self { + scope: cx, + root_split: SplitId::next(), + splits, + active_editor_tab, + editor_tabs, + editors, + diff_editors, + docs, + scratch_docs, + active_editor, + find_editor, + replace_editor, + diagnostics, + locations, + current_location, + width: cx.create_rw_signal(0.0), + code_lens: cx.create_rw_signal(CodeLensData::new(common.clone())), + common, + references, + implementations, + } + } + + pub fn key_down<'a>( + &self, + event: impl Into>, + keypress: &KeyPressData, + ) -> Option { + let active_editor_tab = self.active_editor_tab.get_untracked()?; + let editor_tab = self.editor_tabs.with_untracked(|editor_tabs| { + editor_tabs.get(&active_editor_tab).copied() + })?; + let (_, _, child) = editor_tab.with_untracked(|editor_tab| { + editor_tab.children.get(editor_tab.active).cloned() + })?; + match child { + EditorTabChild::Editor(editor_id) => { + let editor = self.editors.editor_untracked(editor_id)?; + let handle = keypress.key_down(event, &editor); + editor.get_code_actions(); + Some(handle) + } + EditorTabChild::DiffEditor(diff_editor_id) => { + let diff_editor = + self.diff_editors.with_untracked(|diff_editors| { + diff_editors.get(&diff_editor_id).cloned() + })?; + let editor = if diff_editor.focus_right.get_untracked() { + &diff_editor.right + } else { + &diff_editor.left + }; + let handle = keypress.key_down(event, editor); + editor.get_code_actions(); + Some(handle) + } + EditorTabChild::Settings(_) => None, + EditorTabChild::ThemeColorSettings(_) => None, + EditorTabChild::Keymap(_) => None, + EditorTabChild::Volt(_, _) => None, + } + } + + fn save_current_jump_location(&self) -> bool { + if let Some(editor) = self.active_editor.get_untracked() { + let (cursor, viewport) = (editor.cursor(), editor.viewport()); + let path = editor + .doc() + .content + .with_untracked(|content| content.path().cloned()); + if let Some(path) = path { + let offset = cursor.with_untracked(|c| c.offset()); + let scroll_offset = viewport.get_untracked().origin().to_vec2(); + return self.save_jump_location(path, offset, scroll_offset); + } + } + false + } + + pub fn save_jump_location( + &self, + path: PathBuf, + offset: usize, + scroll_offset: Vec2, + ) -> bool { + let mut locations = self.locations.get_untracked(); + if let Some(last_location) = locations.last() { + if last_location.path == path + && last_location.position == Some(EditorPosition::Offset(offset)) + && last_location.scroll_offset == Some(scroll_offset) + { + return false; + } + } + let location = EditorLocation { + path, + position: Some(EditorPosition::Offset(offset)), + scroll_offset: Some(scroll_offset), + ignore_unconfirmed: false, + same_editor_tab: false, + }; + locations.push_back(location.clone()); + let current_location = locations.len(); + self.locations.set(locations); + self.current_location.set(current_location); + + let active_editor_tab_id = self.active_editor_tab.get_untracked(); + let editor_tabs = self.editor_tabs.get_untracked(); + if let Some((locations, current_location)) = active_editor_tab_id + .and_then(|id| editor_tabs.get(&id)) + .map(|editor_tab| { + editor_tab.with_untracked(|editor_tab| { + (editor_tab.locations, editor_tab.current_location) + }) + }) + { + let mut l = locations.get_untracked(); + l.push_back(location); + let new_current_location = l.len(); + locations.set(l); + current_location.set(new_current_location); + } + + true + } + + pub fn jump_to_location( + &self, + location: EditorLocation, + edits: Option>, + ) { + self.save_current_jump_location(); + self.go_to_location(location, edits); + } + + pub fn get_doc( + &self, + path: PathBuf, + unsaved: Option, + ) -> (Rc, bool) { + let cx = self.scope; + let doc = self.docs.with_untracked(|docs| docs.get(&path).cloned()); + if let Some(doc) = doc { + (doc, false) + } else { + let diagnostic_data = self.get_diagnostic_data(&path); + + let doc = Doc::new( + cx, + path.clone(), + diagnostic_data, + self.editors, + self.common.clone(), + ); + let doc = Rc::new(doc); + self.docs.update(|docs| { + docs.insert(path.clone(), doc.clone()); + }); + + { + let doc = doc.clone(); + let local_doc = doc.clone(); + let send = create_ext_action(cx, move |result| { + if let Ok(ProxyResponse::NewBufferResponse { + content, + read_only, + }) = result + { + local_doc.init_content(Rope::from(content)); + if read_only { + local_doc.content.update(|content| { + if let DocContent::File { read_only, .. } = content { + *read_only = true; + } + }); + } else if let Some(unsaved) = unsaved { + local_doc.reload(Rope::from(unsaved), false); + } + } + }); + + self.common + .proxy + .new_buffer(doc.buffer_id, path, move |result| { + send(result); + }); + } + doc.get_code_lens(); + doc.get_folding_range(); + doc.get_document_symbol(); + (doc, true) + } + } + + pub fn go_to_location( + &self, + location: EditorLocation, + edits: Option>, + ) { + if self.common.focus.get_untracked() != Focus::Workbench { + self.common.focus.set(Focus::Workbench); + } + let path = location.path.clone(); + let (doc, new_doc) = self.get_doc(path.clone(), None); + + let child = self.get_editor_tab_child( + EditorTabChildSource::Editor { path, doc }, + location.ignore_unconfirmed, + location.same_editor_tab, + ); + if let EditorTabChild::Editor(editor_id) = child { + if let Some(editor) = self.editors.editor_untracked(editor_id) { + editor.go_to_location(location, new_doc, edits); + } + } + } + + pub fn open_file_changes(&self, path: PathBuf) { + let (right, _) = self.get_doc(path.clone(), None); + let left = Doc::new_history( + self.scope, + DocContent::History(DocHistory { + path: path.clone(), + version: "head".to_string(), + }), + self.editors, + self.common.clone(), + ); + let left = Rc::new(left); + + let send = { + let left = left.clone(); + create_ext_action(self.scope, move |result| { + if let Ok(ProxyResponse::BufferHeadResponse { content, .. }) = result + { + left.init_content(Rope::from(content)); + } + }) + }; + self.common.proxy.get_buffer_head(path, move |result| { + send(result); + }); + + self.get_editor_tab_child( + EditorTabChildSource::DiffEditor { left, right }, + false, + false, + ); + } + + pub fn open_diff_files(&self, left_path: PathBuf, right_path: PathBuf) { + let [left, right] = + [left_path, right_path].map(|path| self.get_doc(path, None).0); + + self.get_editor_tab_child( + EditorTabChildSource::DiffEditor { left, right }, + false, + false, + ); + } + + fn new_editor_tab( + &self, + editor_tab_id: EditorTabId, + split_id: SplitId, + ) -> RwSignal { + let editor_tab = { + let cx = self.scope.create_child(); + let editor_tab = EditorTabData { + scope: cx, + split: split_id, + active: 0, + editor_tab_id, + children: vec![], + window_origin: Point::ZERO, + layout_rect: Rect::ZERO, + locations: cx.create_rw_signal(im::Vector::new()), + current_location: cx.create_rw_signal(0), + }; + cx.create_rw_signal(editor_tab) + }; + self.editor_tabs.update(|editor_tabs| { + editor_tabs.insert(editor_tab_id, editor_tab); + }); + editor_tab + } + + fn get_editor_tab_child( + &self, + source: EditorTabChildSource, + ignore_unconfirmed: bool, + same_editor_tab: bool, + ) -> EditorTabChild { + let config = self.common.config.get_untracked(); + + let active_editor_tab_id = self.active_editor_tab.get_untracked(); + let editor_tabs = self.editor_tabs.get_untracked(); + let active_editor_tab = active_editor_tab_id + .and_then(|id| editor_tabs.get(&id)) + .cloned(); + + let editors = self.editors; + let diff_editors = self.diff_editors.get_untracked(); + + let active_editor_tab = if let Some(editor_tab) = active_editor_tab { + editor_tab + } else if editor_tabs.is_empty() { + let editor_tab_id = EditorTabId::next(); + let editor_tab = self.new_editor_tab(editor_tab_id, self.root_split); + let root_split = self.splits.with_untracked(|splits| { + splits.get(&self.root_split).cloned().unwrap() + }); + root_split.update(|root_split| { + root_split.children = vec![( + root_split.scope.create_rw_signal(1.0), + SplitContent::EditorTab(editor_tab_id), + )]; + }); + self.active_editor_tab.set(Some(editor_tab_id)); + editor_tab + } else { + let (editor_tab_id, editor_tab) = editor_tabs.iter().next().unwrap(); + self.active_editor_tab.set(Some(*editor_tab_id)); + *editor_tab + }; + + let is_same_diff_editor = + |diff_editor_id: &DiffEditorId, left: &Rc, right: &Rc| { + diff_editors + .get(diff_editor_id) + .map(|diff_editor| { + left.content.get_untracked() + == diff_editor.left.doc().content.get_untracked() + && right.content.get_untracked() + == diff_editor.right.doc().content.get_untracked() + }) + .unwrap_or(false) + }; + + let selected = if !config.editor.show_tab { + active_editor_tab.with_untracked(|editor_tab| { + for (i, (_, _, child)) in editor_tab.children.iter().enumerate() { + let can_be_selected = match child { + EditorTabChild::Editor(editor_id) => { + if let Some(editor) = + editors.editor_untracked(*editor_id) + { + let doc = editor.doc(); + let same_path = + if let EditorTabChildSource::Editor { + path, + .. + } = &source + { + doc.content.with_untracked(|content| { + content + .path() + .map(|p| p == path) + .unwrap_or(false) + }) + } else { + false + }; + + same_path + || doc.buffer.with_untracked(|b| b.is_pristine()) + } else { + false + } + } + EditorTabChild::DiffEditor(diff_editor_id) => { + if let EditorTabChildSource::DiffEditor { left, right } = + &source + { + is_same_diff_editor(diff_editor_id, left, right) + || diff_editors + .get(diff_editor_id) + .map(|diff_editor| { + diff_editor.left.doc().is_pristine() + && diff_editor + .right + .doc() + .is_pristine() + }) + .unwrap_or(false) + } else { + false + } + } + EditorTabChild::Settings(_) => true, + EditorTabChild::ThemeColorSettings(_) => true, + EditorTabChild::Keymap(_) => true, + EditorTabChild::Volt(_, _) => true, + }; + + if can_be_selected { + return Some(i); + } + } + None + }) + } else { + match &source { + EditorTabChildSource::Editor { path, .. } => active_editor_tab + .with_untracked(|editor_tab| { + editor_tab + .get_editor(editors, path) + .map(|(i, _)| i) + .or_else(|| { + if ignore_unconfirmed { + None + } else { + editor_tab + .get_unconfirmed_editor_tab_child( + editors, + &diff_editors, + ) + .map(|(i, _)| i) + } + }) + }), + EditorTabChildSource::DiffEditor { left, right } => { + if let Some(index) = + active_editor_tab.with_untracked(|editor_tab| { + editor_tab.children.iter().position(|(_, _, child)| { + if let EditorTabChild::DiffEditor(diff_editor_id) = + child + { + is_same_diff_editor(diff_editor_id, left, right) + } else { + false + } + }) + }) + { + Some(index) + } else if ignore_unconfirmed { + None + } else { + active_editor_tab.with_untracked(|editor_tab| { + editor_tab + .get_unconfirmed_editor_tab_child( + editors, + &diff_editors, + ) + .map(|(i, _)| i) + }) + } + } + EditorTabChildSource::NewFileEditor => { + if ignore_unconfirmed { + None + } else { + active_editor_tab.with_untracked(|editor_tab| { + editor_tab + .get_unconfirmed_editor_tab_child( + editors, + &diff_editors, + ) + .map(|(i, _)| i) + }) + } + } + EditorTabChildSource::Settings => { + if let Some(index) = + active_editor_tab.with_untracked(|editor_tab| { + editor_tab.children.iter().position(|(_, _, child)| { + matches!(child, EditorTabChild::Settings(_)) + }) + }) + { + Some(index) + } else if ignore_unconfirmed { + None + } else { + active_editor_tab.with_untracked(|editor_tab| { + editor_tab + .get_unconfirmed_editor_tab_child( + editors, + &diff_editors, + ) + .map(|(i, _)| i) + }) + } + } + EditorTabChildSource::ThemeColorSettings => { + if let Some(index) = + active_editor_tab.with_untracked(|editor_tab| { + editor_tab.children.iter().position(|(_, _, child)| { + matches!( + child, + EditorTabChild::ThemeColorSettings(_) + ) + }) + }) + { + Some(index) + } else if ignore_unconfirmed { + None + } else { + active_editor_tab.with_untracked(|editor_tab| { + editor_tab + .get_unconfirmed_editor_tab_child( + editors, + &diff_editors, + ) + .map(|(i, _)| i) + }) + } + } + EditorTabChildSource::Keymap => { + if let Some(index) = + active_editor_tab.with_untracked(|editor_tab| { + editor_tab.children.iter().position(|(_, _, child)| { + matches!(child, EditorTabChild::Keymap(_)) + }) + }) + { + Some(index) + } else if ignore_unconfirmed { + None + } else { + active_editor_tab.with_untracked(|editor_tab| { + editor_tab + .get_unconfirmed_editor_tab_child( + editors, + &diff_editors, + ) + .map(|(i, _)| i) + }) + } + } + EditorTabChildSource::Volt(id) => { + if let Some(index) = + active_editor_tab.with_untracked(|editor_tab| { + editor_tab.children.iter().position(|(_, _, child)| { + if let EditorTabChild::Volt(_, current_id) = child { + current_id == id + } else { + false + } + }) + }) + { + Some(index) + } else if ignore_unconfirmed { + None + } else { + active_editor_tab.with_untracked(|editor_tab| { + editor_tab + .get_unconfirmed_editor_tab_child( + editors, + &diff_editors, + ) + .map(|(i, _)| i) + }) + } + } + } + }; + + let new_child_from_source = + |editor_tab_id: EditorTabId, source: &EditorTabChildSource| match source + { + EditorTabChildSource::Editor { doc, .. } => { + let editor_id = editors.new_from_doc( + self.scope, + doc.clone(), + Some(editor_tab_id), + None, + None, + self.common.clone(), + ); + + EditorTabChild::Editor(editor_id) + } + EditorTabChildSource::NewFileEditor => { + let name = self.get_name_for_new_file(); + let doc_content = DocContent::Scratch { + id: BufferId::next(), + name: name.clone(), + }; + let doc = Doc::new_content( + self.scope, + doc_content, + self.editors, + self.common.clone(), + ); + let doc = Rc::new(doc); + self.scratch_docs.update(|scratch_docs| { + scratch_docs.insert(name, doc.clone()); + }); + let editor_id = editors.new_from_doc( + self.scope, + doc, + Some(editor_tab_id), + None, + None, + self.common.clone(), + ); + + EditorTabChild::Editor(editor_id) + } + EditorTabChildSource::Settings => { + EditorTabChild::Settings(SettingsId::next()) + } + EditorTabChildSource::ThemeColorSettings => { + EditorTabChild::ThemeColorSettings(SettingsId::next()) + } + EditorTabChildSource::Keymap => { + EditorTabChild::Keymap(KeymapId::next()) + } + EditorTabChildSource::Volt(id) => { + EditorTabChild::Volt(VoltViewId::next(), id.to_owned()) + } + EditorTabChildSource::DiffEditor { left, right } => { + let diff_editor_id = DiffEditorId::next(); + let diff_editor = DiffEditorData::new( + self.scope, + diff_editor_id, + editor_tab_id, + left.clone(), + right.clone(), + editors, + self.common.clone(), + ); + self.diff_editors.update(|diff_editors| { + diff_editors.insert(diff_editor_id, diff_editor); + }); + EditorTabChild::DiffEditor(diff_editor_id) + } + }; + + if let Some(selected) = selected { + let (editor_tab_id, current_child) = + active_editor_tab.with_untracked(|editor_tab| { + let editor_tab_id = editor_tab.editor_tab_id; + let (_, _, current_child) = &editor_tab.children[selected]; + match current_child { + EditorTabChild::Editor(editor_id) => { + if let Some(editor) = + editors.editor_untracked(*editor_id) + { + editor.save_doc_position(); + } + } + EditorTabChild::DiffEditor(_) => {} + EditorTabChild::Settings(_) => {} + EditorTabChild::ThemeColorSettings(_) => {} + EditorTabChild::Keymap(_) => {} + EditorTabChild::Volt(_, _) => {} + } + (editor_tab_id, current_child.clone()) + }); + + // firstly, if they are the same type of child, load the new doc to the old editor + let is_same = match (¤t_child, &source) { + ( + EditorTabChild::Editor(editor_id), + EditorTabChildSource::Editor { path, doc }, + ) => { + if let Some(editor) = editors.editor_untracked(*editor_id) { + let same_path = editor + .doc() + .content + .with_untracked(|content| content.path() == Some(path)); + if !same_path { + editor.update_doc(doc.clone()); + editor.cursor().set(Cursor::origin( + self.common.config.with_untracked(|c| c.core.modal), + )); + } + } + + true + } + ( + EditorTabChild::DiffEditor(diff_editor_id), + EditorTabChildSource::DiffEditor { left, right }, + ) => { + if !is_same_diff_editor(diff_editor_id, left, right) { + if let Some(diff_editor) = diff_editors.get(diff_editor_id) { + diff_editor.left.update_doc(left.clone()); + diff_editor.right.update_doc(right.clone()); + } + } + true + } + (EditorTabChild::Settings(_), EditorTabChildSource::Settings) => { + true + } + _ => false, + }; + if is_same { + let (_, _, child) = active_editor_tab.with_untracked(|editor_tab| { + editor_tab.children[selected].clone() + }); + active_editor_tab.update(|editor_tab| { + editor_tab.active = selected; + }); + return child; + } + + // We're loading a different kind of child, clean up the old resources + match ¤t_child { + EditorTabChild::Editor(editor_id) => { + self.remove_editor(*editor_id); + } + EditorTabChild::DiffEditor(diff_editor_id) => { + self.diff_editors.update(|diff_editors| { + diff_editors.remove(diff_editor_id); + }); + } + EditorTabChild::Settings(_) => {} + EditorTabChild::ThemeColorSettings(_) => {} + EditorTabChild::Keymap(_) => {} + EditorTabChild::Volt(_, _) => {} + } + + // Now loading the new child + let child = new_child_from_source(editor_tab_id, &source); + active_editor_tab.update(|editor_tab| { + editor_tab.children[selected] = ( + editor_tab.scope.create_rw_signal(0), + editor_tab.scope.create_rw_signal(Rect::ZERO), + child.clone(), + ); + editor_tab.active = selected; + }); + return child; + } + + // check file exists in non active editor tabs + if config.editor.show_tab && !ignore_unconfirmed && !same_editor_tab { + for (editor_tab_id, editor_tab) in &editor_tabs { + if Some(*editor_tab_id) != active_editor_tab_id { + if let Some(index) = + editor_tab.with_untracked(|editor_tab| match &source { + EditorTabChildSource::Editor { path, .. } => editor_tab + .get_editor(editors, path) + .map(|(index, _)| index), + EditorTabChildSource::DiffEditor { left, right } => { + editor_tab.children.iter().position( + |(_, _, child)| { + if let EditorTabChild::DiffEditor( + diff_editor_id, + ) = child + { + is_same_diff_editor( + diff_editor_id, + left, + right, + ) + } else { + false + } + }, + ) + } + EditorTabChildSource::Settings => editor_tab + .children + .iter() + .position(|(_, _, child)| { + matches!(child, EditorTabChild::Settings(_)) + }), + EditorTabChildSource::ThemeColorSettings => editor_tab + .children + .iter() + .position(|(_, _, child)| { + matches!( + child, + EditorTabChild::ThemeColorSettings(_) + ) + }), + EditorTabChildSource::Keymap => editor_tab + .children + .iter() + .position(|(_, _, child)| { + matches!(child, EditorTabChild::Keymap(_)) + }), + EditorTabChildSource::Volt(id) => editor_tab + .children + .iter() + .position(|(_, _, child)| { + if let EditorTabChild::Volt(_, current_id) = + child + { + current_id == id + } else { + false + } + }), + EditorTabChildSource::NewFileEditor => None, + }) + { + self.active_editor_tab.set(Some(*editor_tab_id)); + editor_tab.update(|editor_tab| { + editor_tab.active = index; + }); + let (_, _, child) = + editor_tab.with_untracked(|editor_tab| { + editor_tab.children[index].clone() + }); + return child; + } + } + } + } + + let editor_tab_id = + active_editor_tab.with_untracked(|editor_tab| editor_tab.editor_tab_id); + let child = new_child_from_source(editor_tab_id, &source); + + active_editor_tab.update(|editor_tab| { + let active = editor_tab + .active + .min(editor_tab.children.len().saturating_sub(1)); + let new_active = if editor_tab.children.is_empty() { + 0 + } else { + active + 1 + }; + editor_tab.children.insert( + new_active, + ( + self.scope.create_rw_signal(0), + self.scope.create_rw_signal(Rect::ZERO), + child.clone(), + ), + ); + editor_tab.active = new_active; + }); + + child + } + + pub fn remove_editor(&self, editor_id: EditorId) { + if let Some(editor) = self.editors.remove(editor_id) { + editor.save_doc_position(); + + let doc = editor.doc(); + let (content, _) = (doc.content.get_untracked(), doc.is_pristine()); + if let DocContent::Scratch { name, .. } = content { + let doc_exists = self.editors.with_editors_untracked(|editors| { + editors.iter().any(|(_, editor_data)| { + let doc = editor_data.doc(); + + if let DocContent::Scratch { + name: current_name, .. + } = &doc.content.get_untracked() + { + current_name == &name + } else { + false + } + }) + }); + if !doc_exists { + self.scratch_docs.update(|scratch_docs| { + scratch_docs.remove(&name); + }); + } + } + } + } + + pub fn jump_location_backward(&self, local: bool) { + let (locations, current_location) = if local { + let active_editor_tab_id = self.active_editor_tab.get_untracked(); + let editor_tabs = self.editor_tabs.get_untracked(); + if let Some((locations, current_location)) = active_editor_tab_id + .and_then(|id| editor_tabs.get(&id)) + .map(|editor_tab| { + editor_tab.with_untracked(|editor_tab| { + (editor_tab.locations, editor_tab.current_location) + }) + }) + { + (locations, current_location) + } else { + return; + } + } else { + (self.locations, self.current_location) + }; + + let locations_value = locations.get_untracked(); + let current_location_value = current_location.get_untracked(); + if current_location_value < 1 { + return; + } + + if current_location_value >= locations_value.len() { + // if we are at the head of the locations, save the current location + // before jump back + if self.save_current_jump_location() { + current_location.update(|l| { + *l -= 1; + }); + } + } + + current_location.update(|l| { + *l -= 1; + }); + let current_location_value = current_location.get_untracked(); + current_location.set(current_location_value); + let mut location = locations_value[current_location_value].clone(); + // for local jumps, we keep on the same editor tab + // because we only jump on the same split + location.same_editor_tab = local; + + self.go_to_location(location, None); + } + + pub fn jump_location_forward(&self, local: bool) { + let (locations, current_location) = if local { + let active_editor_tab_id = self.active_editor_tab.get_untracked(); + let editor_tabs = self.editor_tabs.get_untracked(); + if let Some((locations, current_location)) = active_editor_tab_id + .and_then(|id| editor_tabs.get(&id)) + .map(|editor_tab| { + editor_tab.with_untracked(|editor_tab| { + (editor_tab.locations, editor_tab.current_location) + }) + }) + { + (locations, current_location) + } else { + return; + } + } else { + (self.locations, self.current_location) + }; + + let locations_value = locations.get_untracked(); + let current_location_value = current_location.get_untracked(); + if locations_value.is_empty() { + return; + } + if current_location_value >= locations_value.len() - 1 { + return; + } + current_location.set(current_location_value + 1); + let mut location = locations_value[current_location_value + 1].clone(); + // for local jumps, we keep on the same editor tab + // because we only jump on the same split + location.same_editor_tab = local; + self.go_to_location(location, None); + } + + pub fn split( + &self, + direction: SplitDirection, + editor_tab_id: EditorTabId, + ) -> Option<()> { + let editor_tabs = self.editor_tabs.get_untracked(); + let editor_tab = editor_tabs.get(&editor_tab_id).copied()?; + + let split_id = editor_tab.with_untracked(|editor_tab| editor_tab.split); + let splits = self.splits.get_untracked(); + let split = splits.get(&split_id).copied()?; + let split_direction = split.with_untracked(|split| split.direction); + + let (index, children_len) = split.with_untracked(|split| { + split + .children + .iter() + .position(|(_, c)| c == &SplitContent::EditorTab(editor_tab_id)) + .map(|index| (index, split.children.len())) + })?; + + if split_direction == direction { + let new_editor_tab = editor_tab.with_untracked(|editor_tab| { + self.split_editor_tab(self.scope, split_id, editor_tab) + })?; + let new_editor_tab_id = + new_editor_tab.with_untracked(|editor_tab| editor_tab.editor_tab_id); + split.update(|split| { + for (size, _) in &split.children { + size.set(1.0); + } + split.children.insert( + index + 1, + ( + split.scope.create_rw_signal(1.0), + SplitContent::EditorTab(new_editor_tab_id), + ), + ); + }); + } else if children_len == 1 { + let new_editor_tab = editor_tab.with_untracked(|editor_tab| { + self.split_editor_tab(self.scope, split_id, editor_tab) + })?; + let new_editor_tab_id = + new_editor_tab.with_untracked(|editor_tab| editor_tab.editor_tab_id); + split.update(|split| { + split.direction = direction; + for (size, _) in &split.children { + size.set(1.0); + } + split.children.push(( + split.scope.create_rw_signal(1.0), + SplitContent::EditorTab(new_editor_tab_id), + )); + }); + } else { + let new_split_id = SplitId::next(); + + editor_tab.update(|editor_tab| { + editor_tab.split = new_split_id; + }); + let new_editor_tab = editor_tab.with_untracked(|editor_tab| { + self.split_editor_tab(self.scope, new_split_id, editor_tab) + })?; + let new_editor_tab_id = + new_editor_tab.with_untracked(|editor_tab| editor_tab.editor_tab_id); + + let new_split = { + let cx = self.scope.create_child(); + let new_split = SplitData { + scope: cx, + parent_split: Some(split_id), + split_id: new_split_id, + children: vec![ + ( + cx.create_rw_signal(1.0), + SplitContent::EditorTab(editor_tab_id), + ), + ( + cx.create_rw_signal(1.0), + SplitContent::EditorTab(new_editor_tab_id), + ), + ], + direction, + window_origin: Point::ZERO, + layout_rect: Rect::ZERO, + }; + cx.create_rw_signal(new_split) + }; + self.splits.update(|splits| { + splits.insert(new_split_id, new_split); + }); + split.update(|split| { + let size = split.children[index].0.get_untracked(); + split.children[index] = ( + split.scope.create_rw_signal(size), + SplitContent::Split(new_split_id), + ); + }); + } + + Some(()) + } + + fn split_editor_tab( + &self, + cx: Scope, + split_id: SplitId, + editor_tab: &EditorTabData, + ) -> Option> { + let (_, _, child) = editor_tab.children.get(editor_tab.active)?; + + let editor_tab_id = EditorTabId::next(); + + let new_child = match child { + EditorTabChild::Editor(editor_id) => { + let editor_id = self + .editors + .copy(*editor_id, cx, Some(editor_tab_id), None, None) + .unwrap(); + + EditorTabChild::Editor(editor_id) + } + EditorTabChild::DiffEditor(diff_editor_id) => { + let new_diff_editor_id = DiffEditorId::next(); + let diff_editor = self + .diff_editors + .get_untracked() + .get(diff_editor_id)? + .copy(cx, editor_tab_id, new_diff_editor_id, self.editors); + self.diff_editors.update(|diff_editors| { + diff_editors.insert(new_diff_editor_id, diff_editor); + }); + EditorTabChild::DiffEditor(new_diff_editor_id) + } + EditorTabChild::Settings(_) => { + EditorTabChild::Settings(SettingsId::next()) + } + EditorTabChild::ThemeColorSettings(_) => { + EditorTabChild::ThemeColorSettings(ThemeColorSettingsId::next()) + } + EditorTabChild::Keymap(_) => EditorTabChild::Keymap(KeymapId::next()), + EditorTabChild::Volt(_, id) => { + EditorTabChild::Volt(VoltViewId::next(), id.to_owned()) + } + }; + + let editor_tab = { + let cx = self.scope.create_child(); + let editor_tab = EditorTabData { + scope: cx, + split: split_id, + editor_tab_id, + active: 0, + children: vec![( + cx.create_rw_signal(0), + cx.create_rw_signal(Rect::ZERO), + new_child, + )], + window_origin: Point::ZERO, + layout_rect: Rect::ZERO, + locations: cx.create_rw_signal(editor_tab.locations.get_untracked()), + current_location: cx + .create_rw_signal(editor_tab.current_location.get_untracked()), + }; + cx.create_rw_signal(editor_tab) + }; + self.editor_tabs.update(|editor_tabs| { + editor_tabs.insert(editor_tab_id, editor_tab); + }); + Some(editor_tab) + } + + pub fn split_move( + &self, + direction: SplitMoveDirection, + editor_tab_id: EditorTabId, + ) -> Option<()> { + let editor_tabs = self.editor_tabs.get_untracked(); + let editor_tab = editor_tabs.get(&editor_tab_id).copied()?; + + let rect = editor_tab.with_untracked(|editor_tab| { + editor_tab.layout_rect.with_origin(editor_tab.window_origin) + }); + + match direction { + SplitMoveDirection::Up => { + for (_, e) in editor_tabs.iter() { + let current_rect = e.with_untracked(|e| { + e.layout_rect.with_origin(e.window_origin) + }); + if (current_rect.y1 - rect.y0).abs() < 3.0 + && current_rect.x0 <= rect.x0 + && rect.x0 < current_rect.x1 + { + let new_editor_tab_id = + e.with_untracked(|e| e.editor_tab_id); + self.active_editor_tab.set(Some(new_editor_tab_id)); + return Some(()); + } + } + } + SplitMoveDirection::Down => { + for (_, e) in editor_tabs.iter() { + let current_rect = e.with_untracked(|e| { + e.layout_rect.with_origin(e.window_origin) + }); + if (current_rect.y0 - rect.y1).abs() < 3.0 + && current_rect.x0 <= rect.x0 + && rect.x0 < current_rect.x1 + { + let new_editor_tab_id = + e.with_untracked(|e| e.editor_tab_id); + self.active_editor_tab.set(Some(new_editor_tab_id)); + return Some(()); + } + } + } + SplitMoveDirection::Right => { + for (_, e) in editor_tabs.iter() { + let current_rect = e.with_untracked(|e| { + e.layout_rect.with_origin(e.window_origin) + }); + if (rect.x1 - current_rect.x0).abs() < 3.0 + && current_rect.y0 <= rect.y0 + && rect.y0 < current_rect.y1 + { + let new_editor_tab_id = + e.with_untracked(|e| e.editor_tab_id); + self.active_editor_tab.set(Some(new_editor_tab_id)); + return Some(()); + } + } + } + SplitMoveDirection::Left => { + for (_, e) in editor_tabs.iter() { + let current_rect = e.with_untracked(|e| { + e.layout_rect.with_origin(e.window_origin) + }); + if (current_rect.x1 - rect.x0).abs() < 3.0 + && current_rect.y0 <= rect.y0 + && rect.y0 < current_rect.y1 + { + let new_editor_tab_id = + e.with_untracked(|e| e.editor_tab_id); + self.active_editor_tab.set(Some(new_editor_tab_id)); + return Some(()); + } + } + } + } + + Some(()) + } + + pub fn split_exchange(&self, editor_tab_id: EditorTabId) -> Option<()> { + let editor_tabs = self.editor_tabs.get_untracked(); + let editor_tab = editor_tabs.get(&editor_tab_id).copied()?; + + let split_id = editor_tab.with_untracked(|editor_tab| editor_tab.split); + let splits = self.splits.get_untracked(); + let split = splits.get(&split_id).copied()?; + + split.update(|split| { + let index = split + .children + .iter() + .position(|(_, c)| c == &SplitContent::EditorTab(editor_tab_id)); + if let Some(index) = index { + if index < split.children.len() - 1 { + split.children.swap(index, index + 1); + } + self.split_content_focus(&split.children[index].1); + } + }); + + Some(()) + } + + fn split_content_focus(&self, content: &SplitContent) { + match content { + SplitContent::EditorTab(editor_tab_id) => { + self.active_editor_tab.set(Some(*editor_tab_id)); + } + SplitContent::Split(split_id) => { + self.split_focus(*split_id); + } + } + } + + fn split_focus(&self, split_id: SplitId) -> Option<()> { + let splits = self.splits.get_untracked(); + let split = splits.get(&split_id).copied()?; + + let split_children = split.with_untracked(|split| split.children.clone()); + let content = split_children.first()?; + self.split_content_focus(&content.1); + + Some(()) + } + + fn split_remove(&self, split_id: SplitId) -> Option<()> { + if split_id == self.root_split { + return Some(()); + } + + let splits = self.splits.get_untracked(); + let split = splits.get(&split_id).copied()?; + self.splits.update(|splits| { + splits.remove(&split_id); + }); + let parent_split_id = split.with_untracked(|split| split.parent_split)?; + let parent_split = splits.get(&parent_split_id).copied()?; + + let split_len = parent_split + .try_update(|split| { + split + .children + .retain(|(_, c)| c != &SplitContent::Split(split_id)); + split.children.len() + }) + .unwrap(); + if split_len == 0 { + self.split_remove(parent_split_id); + } else if split_len == 1 { + let parent_parent_split_id = + parent_split.with_untracked(|split| split.parent_split)?; + let parent_parent_split = + splits.get(&parent_parent_split_id).copied()?; + let parent_split_index = + parent_parent_split.with_untracked(|split| { + split.content_index(&SplitContent::Split(parent_split_id)) + })?; + let orphan = parent_split + .try_update(|split| split.children.remove(0)) + .unwrap(); + self.split_content_set_parent(&orphan.1, parent_parent_split_id); + parent_parent_split.update(|parent_parent_split| { + parent_parent_split.children[parent_split_index] = orphan; + }); + self.split_remove(parent_split_id); + } + + Some(()) + } + + fn editor_tab_remove(&self, editor_tab_id: EditorTabId) -> Option<()> { + let editor_tabs = self.editor_tabs.get_untracked(); + let editor_tab = editor_tabs.get(&editor_tab_id).copied()?; + self.editor_tabs.update(|editor_tabs| { + editor_tabs.remove(&editor_tab_id); + }); + + let split_id = editor_tab.with_untracked(|editor_tab| editor_tab.split); + let splits = self.splits.get_untracked(); + let split = splits.get(&split_id).copied()?; + let parent_split_id = split.with_untracked(|split| split.parent_split); + + let is_active = self + .active_editor_tab + .with_untracked(|active| active == &Some(editor_tab_id)); + + let index = split.with_untracked(|split| { + split + .children + .iter() + .position(|(_, c)| c == &SplitContent::EditorTab(editor_tab_id)) + })?; + split.update(|split| { + split.children.remove(index); + for (size, _) in &split.children { + size.set(1.0); + } + }); + let split_children = split.with_untracked(|split| split.children.clone()); + + if is_active { + if split_children.is_empty() { + let new_focus = parent_split_id + .and_then(|split_id| splits.get(&split_id)) + .and_then(|split| { + let index = split.with_untracked(|split| { + split.children.iter().position(|(_, c)| { + c == &SplitContent::Split(split_id) + }) + })?; + Some((index, split)) + }) + .and_then(|(index, split)| { + split.with_untracked(|split| { + if index == split.children.len() - 1 { + if index > 0 { + Some(split.children[index - 1]) + } else { + None + } + } else { + Some(split.children[index + 1]) + } + }) + }); + if let Some((_, content)) = new_focus { + self.split_content_focus(&content); + } + } else { + let (_, content) = + split_children[index.min(split_children.len() - 1)]; + self.split_content_focus(&content); + } + } + + if split_children.is_empty() { + self.split_remove(split_id); + } else if split_children.len() == 1 { + if let Some(parent_split_id) = parent_split_id { + let parent_split = splits.get(&parent_split_id).copied()?; + let split_index = parent_split.with_untracked(|split| { + split.content_index(&SplitContent::Split(split_id)) + })?; + let (_, orphan) = + split.try_update(|split| split.children.remove(0)).unwrap(); + self.split_content_set_parent(&orphan, parent_split_id); + parent_split.update(|parent_split| { + let size = parent_split.children[split_index].0.get_untracked(); + parent_split.children[split_index] = + (parent_split.scope.create_rw_signal(size), orphan); + }); + self.split_remove(split_id); + } + } + + Some(()) + } + + pub fn editor_tab_close(&self, editor_tab_id: EditorTabId) -> Option<()> { + let editor_tabs = self.editor_tabs.get_untracked(); + let editor_tab = editor_tabs.get(&editor_tab_id).copied()?; + let editor_tab = editor_tab.get_untracked(); + for (_, _, child) in editor_tab.children { + self.editor_tab_child_close(editor_tab_id, child, false); + } + + Some(()) + } + + fn editor_tab_child_close_warning( + &self, + child: &EditorTabChild, + ) -> Option<(String, Rc, EditorData)> { + match child { + EditorTabChild::Editor(editor_id) => { + let editor = self.editors.editor_untracked(*editor_id)?; + let doc = editor.doc(); + let doc_content = doc.content.get_untracked(); + let is_dirty = !doc.is_pristine(); + if is_dirty { + let exists = self.editors.with_editors_untracked(|editors| { + editors.iter().any(|(id, editor)| { + let doc = editor.doc(); + id != editor_id + && doc.content.with_untracked(|content| { + content == &doc_content + }) + }) + }); + if !exists { + return match doc_content { + DocContent::File { path, .. } => Some(( + path.file_name()?.to_str()?.to_string(), + doc, + editor, + )), + DocContent::Local => None, + DocContent::History(_) => None, + DocContent::Scratch { name, .. } => { + Some((name, doc, editor)) + } + }; + } + } + None + } + EditorTabChild::DiffEditor(_) => None, + EditorTabChild::Settings(_) => None, + EditorTabChild::ThemeColorSettings(_) => None, + EditorTabChild::Keymap(_) => None, + EditorTabChild::Volt(_, _) => None, + } + } + + pub fn split_exchange_active(&self) -> Option<()> { + let active_editor_tab = self.active_editor_tab.get_untracked()?; + self.split_exchange(active_editor_tab)?; + Some(()) + } + + pub fn split_move_active(&self, direction: SplitMoveDirection) -> Option<()> { + let active_editor_tab = self.active_editor_tab.get_untracked()?; + self.split_move(direction, active_editor_tab)?; + Some(()) + } + + pub fn split_active(&self, direction: SplitDirection) -> Option<()> { + let active_editor_tab = self.active_editor_tab.get_untracked()?; + self.split(direction, active_editor_tab)?; + Some(()) + } + + pub fn editor_tab_child_close_active(&self) -> Option<()> { + let active_editor_tab = self.active_editor_tab.get_untracked()?; + let editor_tab = self.editor_tabs.with_untracked(|editor_tabs| { + editor_tabs.get(&active_editor_tab).copied() + })?; + let (_, _, child) = editor_tab.with_untracked(|editor_tab| { + editor_tab.children.get(editor_tab.active).cloned() + })?; + self.editor_tab_child_close(active_editor_tab, child, false); + Some(()) + } + + pub fn editor_tab_child_close_by_kind( + &self, + editor_tab_id: EditorTabId, + child: EditorTabChild, + kind: TabCloseKind, + ) -> Option<()> { + let tabs_to_close: Vec = { + let editor_tabs = self.editor_tabs.get_untracked(); + + let editor_tab = editor_tabs.get(&editor_tab_id).copied()?; + let editor_tab = editor_tab.get_untracked(); + match kind { + TabCloseKind::CloseOther => editor_tab + .children + .iter() + .filter_map(|x| { + if x.2 != child { + Some(x.2.clone()) + } else { + None + } + }) + .collect(), + TabCloseKind::CloseToLeft => { + let mut tabs_to_close = Vec::new(); + for child_tab in &editor_tab.children { + if child_tab.2 != child { + tabs_to_close.push(child_tab.2.clone()); + } else { + break; + } + } + tabs_to_close + } + TabCloseKind::CloseToRight => { + let mut tabs_to_close = Vec::new(); + let mut add_to_tabs = false; + for child_tab in &editor_tab.children { + if child_tab.2 != child && add_to_tabs { + tabs_to_close.push(child_tab.2.clone()); + } else { + add_to_tabs = true; + } + } + tabs_to_close + } + } + }; + for child_tab in tabs_to_close { + self.editor_tab_child_close(editor_tab_id, child_tab, false); + } + Some(()) + } + + pub fn editor_tab_child_close( + &self, + editor_tab_id: EditorTabId, + child: EditorTabChild, + force: bool, + ) -> Option<()> { + if !force { + if let Some((name, doc, editor)) = + self.editor_tab_child_close_warning(&child) + { + let internal_command = self.common.internal_command; + let main_split = self.clone(); + + let doc_content = doc.content.get_untracked(); + let save_button = match doc_content { + DocContent::Scratch { .. } => { + let child = child.clone(); + let doc = doc.clone(); + let save_action = Rc::new(move || { + let child = child.clone(); + let main_split = main_split.clone(); + let doc = doc.clone(); + internal_command.send(InternalCommand::HideAlert); + save_as( + FileDialogOptions::new().title("Save File"), + move |file: Option| { + let main_split = main_split.clone(); + let child = child.clone(); + let local_main_split = main_split.clone(); + if let Some(mut file) = file { + main_split.save_as( + doc.clone(), + if let Some(path) = file.path.pop() { + path + } else { + tracing::error!("No path"); + return; + }, + move || { + local_main_split + .clone() + .editor_tab_child_close( + editor_tab_id, + child.clone(), + false, + ); + }, + ); + } + }, + ); + }); + Some(AlertButton { + text: "Save".to_string(), + action: save_action, + }) + } + DocContent::File { .. } => { + let editor = editor.clone(); + let editors = self.editors; + let editor_id = editor.id(); + let save_action = Rc::new(move || { + internal_command.send(InternalCommand::HideAlert); + editor.save(false, move || { + if let Some(editor) = + editors.editor_untracked(editor_id) + { + editor.clone().run_focus_command( + &FocusCommand::SplitClose, + None, + Modifiers::empty(), + ); + } + }); + }); + Some(AlertButton { + text: "Save".to_string(), + action: save_action, + }) + } + DocContent::Local => None, + DocContent::History(_) => None, + }; + if let Some(save_button) = save_button { + let main_split = self.clone(); + let child = child.clone(); + self.common + .internal_command + .send(InternalCommand::ShowAlert { + title: format!( + "Do you want to save the changes you made to {name}?" + ), + msg: "Your changes will be lost if you don't save them." + .to_string(), + buttons: vec![ + save_button, + AlertButton { + text: "Don't Save".to_string(), + action: Rc::new(move || { + internal_command + .send(InternalCommand::HideAlert); + main_split.editor_tab_child_close( + editor_tab_id, + child.clone(), + true, + ); + }), + }, + ], + }); + } + + return Some(()); + } + } + + let editor_tabs = self.editor_tabs.get_untracked(); + let editor_tab = editor_tabs.get(&editor_tab_id).copied()?; + + let index = editor_tab.with_untracked(|editor_tab| { + editor_tab.children.iter().position(|(_, _, c)| c == &child) + })?; + + let editor_tab_children_len = editor_tab + .try_update(|editor_tab| { + editor_tab.children.remove(index); + editor_tab.active = + index.min(editor_tab.children.len().saturating_sub(1)); + editor_tab.children.len() + }) + .unwrap(); + + match child { + EditorTabChild::Editor(editor_id) => { + self.remove_editor(editor_id); + } + EditorTabChild::DiffEditor(diff_editor_id) => { + let removed_diff_editor = self + .diff_editors + .try_update(|diff_editors| diff_editors.remove(&diff_editor_id)) + .unwrap(); + if let Some(diff_editor) = removed_diff_editor { + diff_editor.right.save_doc_position(); + self.editors.remove(diff_editor.right.editor.id()); + self.editors.remove(diff_editor.left.editor.id()); + } + } + EditorTabChild::Settings(_) => {} + EditorTabChild::ThemeColorSettings(_) => {} + EditorTabChild::Keymap(_) => {} + EditorTabChild::Volt(_, _) => {} + } + + if editor_tab_children_len == 0 { + self.editor_tab_remove(editor_tab_id); + } + + Some(()) + } + + pub fn editor_tab_update_layout( + &self, + editor_tab_id: &EditorTabId, + window_origin: Option, + rect: Option, + ) -> Option<()> { + let editor_tabs = self.editor_tabs.get_untracked(); + let editor_tab = editor_tabs.get(editor_tab_id).copied()?; + editor_tab.update(|editor_tab| { + if let Some(window_origin) = window_origin { + editor_tab.window_origin = window_origin; + } + if let Some(rect) = rect { + editor_tab.layout_rect = rect; + } + }); + Some(()) + } + + pub fn run_code_action(&self, plugin_id: PluginId, action: CodeActionOrCommand) { + match action { + CodeActionOrCommand::Command(command) => { + self.run_code_lens( + &command.command, + command.arguments.unwrap_or_default(), + ); + } + CodeActionOrCommand::CodeAction(action) => { + if let Some(edit) = action.edit.as_ref() { + self.apply_workspace_edit(edit); + } else { + self.resolve_code_action(plugin_id, action); + } + } + } + } + + pub fn run_code_lens(&self, command: &str, args: Vec) { + self.code_lens.get_untracked().run(command, args); + } + + /// Resolve a code action and apply its held workspace edit + fn resolve_code_action(&self, plugin_id: PluginId, action: CodeAction) { + let main_split = self.clone(); + let send = create_ext_action(self.scope, move |edit| { + main_split.apply_workspace_edit(&edit); + }); + self.common + .proxy + .code_action_resolve(action, plugin_id, move |result| { + if let Ok(ProxyResponse::CodeActionResolveResponse { item }) = result + { + if let Some(edit) = item.edit { + send(edit); + } + } + }); + } + + /// Perform a workspace edit, which are from the LSP (such as code actions, or symbol renaming) + pub fn apply_workspace_edit(&self, edit: &WorkspaceEdit) { + if let Some(DocumentChanges::Operations(_op)) = + edit.document_changes.as_ref() + { + // TODO + } + + if let Some(edits) = workspace_edits(edit) { + for (url, edits) in edits { + if let Ok(path) = url.to_file_path() { + let active_path = self + .active_editor + .get_untracked() + .map(|editor| editor.doc()) + .map(|doc| doc.content.get_untracked()) + .and_then(|content| content.path().cloned()); + let position = if active_path.as_ref() == Some(&path) { + None + } else { + edits + .first() + .map(|edit| EditorPosition::Position(edit.range.start)) + }; + let location = EditorLocation { + path, + position, + scroll_offset: None, + ignore_unconfirmed: true, + same_editor_tab: false, + }; + self.jump_to_location(location, Some(edits)); + } + } + } + } + + pub fn next_error(&self) { + let file_diagnostics = + self.file_diagnostics_items(DiagnosticSeverity::ERROR); + if file_diagnostics.is_empty() { + return; + } + let active_editor = self.active_editor.get_untracked(); + let active_path = active_editor + .map(|editor| (editor.doc(), editor.cursor())) + .and_then(|(doc, cursor)| { + let offset = cursor.with_untracked(|c| c.offset()); + let (path, position) = ( + doc.content.get_untracked().path().cloned(), + doc.buffer.with_untracked(|b| b.offset_to_position(offset)), + ); + path.map(|path| (path, offset, position)) + }); + let (path, position) = + next_in_file_errors_offset(active_path, &file_diagnostics); + let location = EditorLocation { + path, + position: Some(position), + scroll_offset: None, + ignore_unconfirmed: false, + same_editor_tab: false, + }; + self.jump_to_location(location, None); + } + + fn file_diagnostics_items( + &self, + severity: DiagnosticSeverity, + ) -> Vec<(PathBuf, Vec)> { + let diagnostics = self.diagnostics.get_untracked(); + diagnostics + .into_iter() + .filter_map(|(path, diagnostic)| { + let span = diagnostic.diagnostics_span.get_untracked(); + if !span.is_empty() { + let diags = span + .iter() + .filter_map(|(iv, diag)| { + if diag.severity == Some(severity) { + Some(EditorDiagnostic { + range: Some((iv.start, iv.end)), + diagnostic: diag.to_owned(), + }) + } else { + None + } + }) + .collect::>(); + if !diags.is_empty() { + Some((path, diags)) + } else { + None + } + } else { + let diagnostics = diagnostic.diagnostics.get_untracked(); + let diagnostics: Vec = diagnostics + .into_iter() + .filter(|d| d.severity == Some(severity)) + .map(|d| EditorDiagnostic { + range: None, + diagnostic: d, + }) + .collect(); + if !diagnostics.is_empty() { + Some((path, diagnostics)) + } else { + None + } + } + }) + .sorted_by_key(|(path, _)| path.clone()) + .collect() + } + + pub fn get_diagnostic_data(&self, path: &Path) -> DiagnosticData { + if let Some(d) = self.diagnostics.with_untracked(|d| d.get(path).cloned()) { + d + } else { + let diagnostic_data = DiagnosticData { + expanded: self.scope.create_rw_signal(true), + diagnostics: self.scope.create_rw_signal(im::Vector::new()), + diagnostics_span: self + .scope + .create_rw_signal(SpansBuilder::new(0).build()), + }; + self.diagnostics.update(|d| { + d.insert(path.to_path_buf(), diagnostic_data.clone()); + }); + diagnostic_data + } + } + + pub fn open_file_changed(&self, path: &Path, content: &FileChanged) { + tracing::debug!("open_file_changed {:?}", path); + match content { + FileChanged::Change(content) => { + let doc = self.docs.with_untracked(|docs| docs.get(path).cloned()); + let doc = match doc { + Some(doc) => doc, + None => return, + }; + doc.handle_file_changed(Rope::from(content)); + } + FileChanged::Delete => { + if self.docs.with_untracked(|x| x.get(path).is_none()) { + return; + } + let Some(editor_id) = self.editors.get_editor_id_by_path(path) + else { + return; + }; + let id = editor_id.to_raw(); + if let Some(tab_id) = self.editor_tabs.with_untracked(|x| { + for (tab_id, tab_data) in x { + if tab_data.with_untracked(|x| { + x.children.iter().any(|(_, _, child)| child.id() == id) + }) { + return Some(*tab_id); + } + } + None + }) { + self.editor_tab_close(tab_id); + } + self.editors.remove(editor_id); + self.docs.update(|x| { + x.remove(path); + }); + } + } + } + + pub fn set_find_pattern(&self, pattern: Option) { + if let Some(pattern) = pattern { + self.find_editor.doc().reload(Rope::from(pattern), true); + } + let pattern_len = self + .find_editor + .doc() + .buffer + .with_untracked(|buffer| buffer.len()); + self.find_editor + .cursor() + .update(|cursor| cursor.set_insert(Selection::region(0, pattern_len))); + } + + pub fn open_volt_view(&self, id: VoltID) { + self.get_editor_tab_child(EditorTabChildSource::Volt(id), false, false); + } + + pub fn open_settings(&self) { + self.get_editor_tab_child(EditorTabChildSource::Settings, false, false); + } + + pub fn open_theme_color_settings(&self) { + self.get_editor_tab_child( + EditorTabChildSource::ThemeColorSettings, + false, + false, + ); + } + + pub fn open_keymap(&self) { + self.get_editor_tab_child(EditorTabChildSource::Keymap, false, false); + } + + pub fn new_file(&self) -> EditorTabChild { + self.get_editor_tab_child(EditorTabChildSource::NewFileEditor, false, false) + } + + pub fn save_as(&self, doc: Rc, path: PathBuf, action: impl Fn() + 'static) { + let (buffer_id, doc_content, rev, content) = ( + doc.buffer_id, + doc.content.get_untracked(), + doc.rev(), + doc.buffer.with_untracked(|b| b.to_string()), + ); + match doc_content { + DocContent::Scratch { .. } => { + let send = { + let path = path.clone(); + create_ext_action(self.scope, move |result| { + if let Err(err) = result { + event!( + Level::WARN, + "Failed to save as a file: {:?}", + err + ); + } else { + let syntax = Syntax::init(&path); + doc.content.set(DocContent::File { + path: path.clone(), + read_only: false, + }); + doc.buffer.update(|buffer| { + buffer.set_pristine(); + }); + doc.set_syntax(syntax); + doc.trigger_syntax_change(None); + action(); + } + }) + }; + self.common.proxy.save_buffer_as( + buffer_id, + path, + rev, + content, + true, + Box::new(move |result| { + send(result); + }), + ); + } + DocContent::Local => {} + DocContent::File { .. } => {} + DocContent::History(_) => {} + } + } + + pub fn save_as2( + &self, + doc: Rc, + path: PathBuf, + action: impl Fn() + 'static, + ) { + let (buffer_id, doc_content, rev, content) = ( + doc.buffer_id, + doc.content.get_untracked(), + doc.rev(), + doc.buffer.with_untracked(|b| b.to_string()), + ); + match doc_content { + DocContent::Scratch { .. } => { + let send = { + let path = path.clone(); + create_ext_action(self.scope, move |result| { + if let Err(err) = result { + event!( + Level::WARN, + "Failed to save as a file: {:?}", + err + ); + } else { + let syntax = Syntax::init(&path); + doc.content.set(DocContent::File { + path: path.clone(), + read_only: false, + }); + doc.buffer.update(|buffer| { + buffer.set_pristine(); + }); + doc.set_syntax(syntax); + doc.trigger_syntax_change(None); + action(); + } + }) + }; + self.common.proxy.save_buffer_as( + buffer_id, + path, + rev, + content, + true, + Box::new(move |result| { + send(result); + }), + ); + } + DocContent::Local => {} + DocContent::File { .. } => {} + DocContent::History(_) => {} + } + } + + fn get_name_for_new_file(&self) -> String { + const PREFIX: &str = "Untitled-"; + + // Checking just the current scratch_docs rather than all the different document + // collections seems to be the right thing to do. The user may have genuine 'new N' + // files tucked away somewhere in their workspace. + let new_num = self.scratch_docs.with_untracked(|scratch_docs| { + scratch_docs + .values() + .filter_map(|doc| { + doc.content.with_untracked(|content| match content { + DocContent::Scratch { name, .. } => { + // The unwraps are safe because scratch docs are always + // titled the same format and the user cannot change the name. + let num_part = name.strip_prefix(PREFIX).unwrap(); + let num = num_part.parse::().unwrap(); + Some(num) + } + _ => None, + }) + }) + .max() + .unwrap_or(0) + + 1 + }); + + format!("{PREFIX}{new_num}") + } + + pub fn can_jump_location_backward(&self, tracked: bool) -> bool { + if tracked { + self.current_location.get() >= 1 + } else { + self.current_location.get_untracked() >= 1 + } + } + + pub fn can_jump_location_forward(&self, tracked: bool) -> bool { + if tracked { + !(self.locations.with(|l| l.is_empty()) + || self.current_location.get() + >= self.locations.with(|l| l.len()) - 1) + } else { + !(self.locations.with_untracked(|l| l.is_empty()) + || self.current_location.get_untracked() + >= self.locations.with_untracked(|l| l.len()) - 1) + } + } + + pub fn save_scratch_doc(&self, doc: Rc) { + let main_split = self.clone(); + save_as( + FileDialogOptions::new().title("Save File"), + move |file: Option| { + if let Some(mut file) = file { + main_split.save_as( + doc.clone(), + if let Some(path) = file.path.pop() { + path + } else { + tracing::error!("No path"); + return; + }, + move || {}, + ); + } + }, + ); + } + + pub fn save_scratch_doc2(&self, doc: Rc) { + let main_split = self.clone(); + save_as( + FileDialogOptions::new().title("Save File"), + move |file: Option| { + if let Some(mut file) = file { + main_split.save_as2( + doc.clone(), + if let Some(path) = file.path.pop() { + path + } else { + tracing::error!("No path"); + return; + }, + move || {}, + ); + } + }, + ); + } + + pub fn move_editor_tab_child( + &self, + from_tab: EditorTabId, + to_tab: EditorTabId, + from_index: usize, + to_index: usize, + ) -> Option<()> { + let from_editor_tab = self + .editor_tabs + .with_untracked(|editor_tabs| editor_tabs.get(&from_tab).cloned())?; + + if from_tab == to_tab { + if from_index == to_index { + return Some(()); + } + + let to_index = if from_index < to_index { + to_index - 1 + } else { + to_index + }; + + from_editor_tab.update(|tab| { + let child = tab.children.remove(from_index); + tab.children.insert(to_index, child); + tab.active = to_index; + }); + } else { + let to_editor_tab = self + .editor_tabs + .with_untracked(|editor_tabs| editor_tabs.get(&to_tab).cloned())?; + + let (_, _, child) = from_editor_tab + .try_update(|tab| { + let child = tab.children.remove(from_index); + tab.active = + tab.active.min(tab.children.len().saturating_sub(1)); + child + }) + .unwrap(); + + self.editor_tab_child_set_parent(&child, to_tab); + to_editor_tab.update(|tab| { + tab.children.insert( + to_index, + ( + tab.scope.create_rw_signal(to_index), + tab.scope.create_rw_signal(Rect::ZERO), + child, + ), + ); + tab.active = to_index; + }); + self.active_editor_tab.set(Some(to_tab)); + + if from_editor_tab.with_untracked(|tab| tab.children.is_empty()) { + self.editor_tab_remove(from_tab); + } + } + + Some(()) + } + + fn split_content_set_parent( + &self, + content: &SplitContent, + parent_split_id: SplitId, + ) -> Option<()> { + match content { + SplitContent::EditorTab(editor_tab_id) => { + let editor_tab = self.editor_tabs.with_untracked(|editor_tabs| { + editor_tabs.get(editor_tab_id).copied() + })?; + editor_tab.update(|editor_tab| { + editor_tab.split = parent_split_id; + }); + } + SplitContent::Split(split_id) => { + let split = self + .splits + .with_untracked(|splits| splits.get(split_id).copied())?; + split.update(|split| { + split.parent_split = Some(parent_split_id); + }); + } + } + Some(()) + } + + fn editor_tab_child_set_parent( + &self, + child: &EditorTabChild, + editor_tab_id: EditorTabId, + ) -> Option<()> { + match child { + EditorTabChild::Editor(editor_id) => { + let editor = self.editors.editor(*editor_id)?; + editor.editor_tab_id.set(Some(editor_tab_id)); + } + EditorTabChild::DiffEditor(diff_editor_id) => { + let diff_editor = + self.diff_editors.with_untracked(|diff_editors| { + diff_editors.get(diff_editor_id).cloned() + })?; + diff_editor.editor_tab_id.set(editor_tab_id); + diff_editor + .left + .diff_editor_id + .set(Some((editor_tab_id, *diff_editor_id))); + diff_editor + .right + .diff_editor_id + .set(Some((editor_tab_id, *diff_editor_id))); + } + EditorTabChild::Settings(_) => {} + EditorTabChild::ThemeColorSettings(_) => {} + EditorTabChild::Keymap(_) => {} + EditorTabChild::Volt(_, _) => {} + } + Some(()) + } + + pub fn move_editor_tab_child_to_new_split( + &self, + from_tab: EditorTabId, + from_index: usize, + to_tab: EditorTabId, + split: SplitMoveDirection, + ) -> Option<()> { + let from_editor_tab = self + .editor_tabs + .with_untracked(|editor_tabs| editor_tabs.get(&from_tab).cloned())?; + let to_editor_tab = self + .editor_tabs + .with_untracked(|editor_tabs| editor_tabs.get(&to_tab).cloned())?; + let to_split_id = + to_editor_tab.with_untracked(|editor_tab| editor_tab.split); + let to_split = self + .splits + .with_untracked(|splits| splits.get(&to_split_id).cloned())?; + { + let to_split_direction = + to_split.with_untracked(|split| split.direction); + if split.direction() != to_split_direction { + let children_len = + to_split.with_untracked(|split| split.children.len()); + if children_len <= 1 { + to_split.update(|to_split| { + to_split.direction = split.direction(); + }); + } + } + } + + let to_split_direction = to_split.with_untracked(|split| split.direction); + if split.direction() == to_split_direction { + let index = + to_split.with_untracked(|split| split.editor_tab_index(to_tab))?; + let index = match split { + SplitMoveDirection::Up => index, + SplitMoveDirection::Down => index + 1, + SplitMoveDirection::Right => index + 1, + SplitMoveDirection::Left => index, + }; + let new_editor_tab_id = EditorTabId::next(); + + let (_, _, child) = from_editor_tab + .try_update(|tab| { + let child = tab.children.remove(from_index); + tab.active = + tab.active.min(tab.children.len().saturating_sub(1)); + child + }) + .unwrap(); + + self.editor_tab_child_set_parent(&child, new_editor_tab_id); + + let cx = self.scope.create_child(); + let new_editor_tab = EditorTabData { + scope: cx, + split: to_split_id, + editor_tab_id: new_editor_tab_id, + active: 0, + children: vec![( + cx.create_rw_signal(0), + cx.create_rw_signal(Rect::ZERO), + child, + )], + window_origin: Point::ZERO, + layout_rect: Rect::ZERO, + locations: cx.create_rw_signal(im::Vector::new()), + current_location: cx.create_rw_signal(0), + }; + self.editor_tabs.update(|editor_tabs| { + editor_tabs.insert( + new_editor_tab.editor_tab_id, + new_editor_tab.scope.create_rw_signal(new_editor_tab), + ); + }); + to_split.update(|split| { + for (size, _) in &split.children { + size.set(1.0); + } + split.children.insert( + index, + ( + split.scope.create_rw_signal(1.0), + SplitContent::EditorTab(new_editor_tab_id), + ), + ); + }); + self.active_editor_tab.set(Some(new_editor_tab_id)); + } else { + let index = + to_split.with_untracked(|split| split.editor_tab_index(to_tab))?; + let (size, existing_editor_tab) = to_split + .try_update(|split| split.children.remove(index)) + .unwrap(); + + let new_split_id = SplitId::next(); + + if let SplitContent::EditorTab(existing_editor_tab_id) = + existing_editor_tab + { + let editor_tab = self.editor_tabs.with_untracked(|editor_tabs| { + editor_tabs.get(&existing_editor_tab_id).cloned() + })?; + editor_tab.update(|editor_tab| { + editor_tab.split = new_split_id; + }); + } + + let new_editor_tab_id = EditorTabId::next(); + + let (_, _, child) = from_editor_tab + .try_update(|tab| { + let child = tab.children.remove(from_index); + tab.active = + tab.active.min(tab.children.len().saturating_sub(1)); + child + }) + .unwrap(); + self.editor_tab_child_set_parent(&child, new_editor_tab_id); + + let new_editor_tab = { + let cx = self.scope.create_child(); + EditorTabData { + scope: cx, + split: new_split_id, + editor_tab_id: new_editor_tab_id, + active: 0, + children: vec![( + cx.create_rw_signal(0), + cx.create_rw_signal(Rect::ZERO), + child, + )], + window_origin: Point::ZERO, + layout_rect: Rect::ZERO, + locations: cx.create_rw_signal(im::Vector::new()), + current_location: cx.create_rw_signal(0), + } + }; + self.editor_tabs.update(|editor_tabs| { + editor_tabs.insert( + new_editor_tab.editor_tab_id, + new_editor_tab.scope.create_rw_signal(new_editor_tab), + ); + }); + + let scope = self.scope.create_child(); + let new_split_children = match split { + SplitMoveDirection::Up | SplitMoveDirection::Left => { + vec![ + ( + scope.create_rw_signal(1.0), + SplitContent::EditorTab(new_editor_tab_id), + ), + (scope.create_rw_signal(1.0), existing_editor_tab), + ] + } + SplitMoveDirection::Down | SplitMoveDirection::Right => { + vec![ + (scope.create_rw_signal(1.0), existing_editor_tab), + ( + scope.create_rw_signal(1.0), + SplitContent::EditorTab(new_editor_tab_id), + ), + ] + } + }; + let new_split = SplitData { + scope, + split_id: new_split_id, + parent_split: Some(to_split_id), + children: new_split_children, + direction: split.direction(), + window_origin: Point::ZERO, + layout_rect: Rect::ZERO, + }; + self.splits.update(|splits| { + splits.insert( + new_split_id, + new_split.scope.create_rw_signal(new_split), + ); + }); + to_split.update(|split| { + split.children.insert( + index, + ( + split.scope.create_rw_signal(size.get_untracked()), + SplitContent::Split(new_split_id), + ), + ); + }); + self.active_editor_tab.set(Some(new_editor_tab_id)); + } + + if from_editor_tab.with_untracked(|tab| tab.children.is_empty()) { + self.editor_tab_remove(from_tab); + } + + Some(()) + } + + pub fn export_theme(&self) { + let child = self.new_file(); + if let EditorTabChild::Editor(id) = child { + if let Some(editor) = self.editors.editor_untracked(id) { + let doc = editor.doc(); + doc.reload( + Rope::from(self.common.config.get_untracked().export_theme()), + true, + ); + } + } + } + + pub fn show_env(&self) { + let child = self.new_file(); + if let EditorTabChild::Editor(id) = child { + if let Some(editor) = self.editors.editor_untracked(id) { + let doc = editor.doc(); + doc.reload( + Rope::from( + std::env::vars().map(|(k, v)| format!("{k}={v}")).join("\n"), + ), + true, + ); + } + } + } + + pub fn get_active_editor(&self) -> Option { + let active_editor_tab = self.active_editor_tab.get()?; + let editor_tabs = self.editor_tabs; + let editor_tab = editor_tabs + .with(|editor_tabs| editor_tabs.get(&active_editor_tab).copied())?; + let (_, _, child) = editor_tab.with(|editor_tab| { + editor_tab.children.get(editor_tab.active).cloned() + })?; + match child { + EditorTabChild::Editor(editor_id) => self.editors.editor(editor_id), + _ => None, + } + } +} + +fn workspace_edits(edit: &WorkspaceEdit) -> Option>> { + if let Some(changes) = edit.changes.as_ref() { + return Some(changes.clone()); + } + + let changes = edit.document_changes.as_ref()?; + let edits = match changes { + DocumentChanges::Edits(edits) => edits + .iter() + .map(|e| { + ( + e.text_document.uri.clone(), + e.edits + .iter() + .map(|e| match e { + OneOf::Left(e) => e.clone(), + OneOf::Right(e) => e.text_edit.clone(), + }) + .collect(), + ) + }) + .collect::>>(), + DocumentChanges::Operations(ops) => ops + .iter() + .filter_map(|o| match o { + DocumentChangeOperation::Op(_op) => None, + DocumentChangeOperation::Edit(e) => Some(( + e.text_document.uri.clone(), + e.edits + .iter() + .map(|e| match e { + OneOf::Left(e) => e.clone(), + OneOf::Right(e) => e.text_edit.clone(), + }) + .collect(), + )), + }) + .collect::>>(), + }; + Some(edits) +} + +fn next_in_file_errors_offset( + active_path: Option<(PathBuf, usize, Position)>, + file_diagnostics: &[(PathBuf, Vec)], +) -> (PathBuf, EditorPosition) { + if let Some((active_path, offset, position)) = active_path { + for (current_path, diagnostics) in file_diagnostics { + if &active_path == current_path { + for diagnostic in diagnostics { + if let Some((start, _)) = diagnostic.range { + if start > offset { + return ( + (*current_path).clone(), + EditorPosition::Offset(start), + ); + } + } + + if diagnostic.diagnostic.range.start.line > position.line + || (diagnostic.diagnostic.range.start.line == position.line + && diagnostic.diagnostic.range.start.character + > position.character) + { + return ( + (*current_path).clone(), + EditorPosition::Position( + diagnostic.diagnostic.range.start, + ), + ); + } + } + } + if current_path > &active_path { + if let Some((start, _)) = diagnostics[0].range { + return ((*current_path).clone(), EditorPosition::Offset(start)); + } + return ( + (*current_path).clone(), + if let Some((start, _)) = diagnostics[0].range { + EditorPosition::Offset(start) + } else { + EditorPosition::Position( + diagnostics[0].diagnostic.range.start, + ) + }, + ); + } + } + } + + ( + file_diagnostics[0].0.clone(), + if let Some((start, _)) = file_diagnostics[0].1[0].range { + EditorPosition::Offset(start) + } else { + EditorPosition::Position(file_diagnostics[0].1[0].diagnostic.range.start) + }, + ) +} + +#[derive(Clone, Copy, Debug)] +pub enum TabCloseKind { + CloseOther, + CloseToLeft, + CloseToRight, +} diff --git a/lapce-app/src/markdown.rs b/lapce-app/src/markdown.rs new file mode 100644 index 0000000..cd2843e --- /dev/null +++ b/lapce-app/src/markdown.rs @@ -0,0 +1,344 @@ +use floem::text::{ + Attrs, AttrsList, FamilyOwned, LineHeightValue, Style, TextLayout, Weight, +}; +use lapce_core::{language::LapceLanguage, syntax::Syntax}; +use lapce_xi_rope::Rope; +use lsp_types::MarkedString; +use pulldown_cmark::{CodeBlockKind, CowStr, Event, Options, Parser, Tag}; +use smallvec::SmallVec; + +use crate::config::{LapceConfig, color::LapceColor}; + +#[derive(Clone)] +pub enum MarkdownContent { + Text(TextLayout), + Image { url: String, title: String }, + Separator, +} + +pub fn parse_markdown( + text: &str, + line_height: f64, + config: &LapceConfig, +) -> Vec { + let mut res = Vec::new(); + + let mut current_text = String::new(); + let code_font_family: Vec = + FamilyOwned::parse_list(&config.editor.font_family).collect(); + + let default_attrs = Attrs::new() + .color(config.color(LapceColor::EDITOR_FOREGROUND)) + .font_size(config.ui.font_size() as f32) + .line_height(LineHeightValue::Normal(line_height as f32)); + let mut attr_list = AttrsList::new(default_attrs.clone()); + + let mut builder_dirty = false; + + let mut pos = 0; + + let mut tag_stack: SmallVec<[(usize, Tag); 4]> = SmallVec::new(); + + let parser = Parser::new_ext( + text, + Options::ENABLE_TABLES + | Options::ENABLE_FOOTNOTES + | Options::ENABLE_STRIKETHROUGH + | Options::ENABLE_TASKLISTS + | Options::ENABLE_HEADING_ATTRIBUTES, + ); + let mut last_text = CowStr::from(""); + // Whether we should add a newline on the next entry + // This is used so that we don't emit newlines at the very end of the generation + let mut add_newline = false; + for event in parser { + // Add the newline since we're going to be outputting more + if add_newline { + current_text.push('\n'); + builder_dirty = true; + pos += 1; + add_newline = false; + } + + match event { + Event::Start(tag) => { + tag_stack.push((pos, tag)); + } + Event::End(end_tag) => { + if let Some((start_offset, tag)) = tag_stack.pop() { + if end_tag != tag.to_end() { + tracing::warn!("Mismatched markdown tag"); + continue; + } + + if let Some(attrs) = attribute_for_tag( + default_attrs.clone(), + &tag, + &code_font_family, + config, + ) { + attr_list + .add_span(start_offset..pos.max(start_offset), attrs); + } + + if should_add_newline_after_tag(&tag) { + add_newline = true; + } + + match &tag { + Tag::CodeBlock(kind) => { + let language = + if let CodeBlockKind::Fenced(language) = kind { + md_language_to_lapce_language(language) + } else { + None + }; + + highlight_as_code( + &mut attr_list, + default_attrs.clone().family(&code_font_family), + language, + &last_text, + start_offset, + config, + ); + builder_dirty = true; + } + Tag::Image { + link_type: _, + dest_url: dest, + title, + id: _, + } => { + // TODO: Are there any link types that would change how the + // image is rendered? + + if builder_dirty { + let mut text_layout = TextLayout::new(); + text_layout.set_text(¤t_text, attr_list, None); + res.push(MarkdownContent::Text(text_layout)); + attr_list = AttrsList::new(default_attrs.clone()); + current_text.clear(); + pos = 0; + builder_dirty = false; + } + + res.push(MarkdownContent::Image { + url: dest.to_string(), + title: title.to_string(), + }); + } + _ => { + // Presumably? + builder_dirty = true; + } + } + } else { + tracing::warn!("Unbalanced markdown tag") + } + } + Event::Text(text) => { + if let Some((_, tag)) = tag_stack.last() { + if should_skip_text_in_tag(tag) { + continue; + } + } + current_text.push_str(&text); + pos += text.len(); + last_text = text; + builder_dirty = true; + } + Event::Code(text) => { + attr_list.add_span( + pos..pos + text.len(), + default_attrs.clone().family(&code_font_family), + ); + current_text.push_str(&text); + pos += text.len(); + builder_dirty = true; + } + // TODO: Some minimal 'parsing' of html could be useful here, since some things use + // basic html like `text`. + Event::Html(text) => { + attr_list.add_span( + pos..pos + text.len(), + default_attrs + .clone() + .family(&code_font_family) + .color(config.color(LapceColor::MARKDOWN_BLOCKQUOTE)), + ); + current_text.push_str(&text); + pos += text.len(); + builder_dirty = true; + } + Event::HardBreak => { + current_text.push('\n'); + pos += 1; + builder_dirty = true; + } + Event::SoftBreak => { + current_text.push(' '); + pos += 1; + builder_dirty = true; + } + Event::Rule => {} + Event::FootnoteReference(_text) => {} + Event::TaskListMarker(_text) => {} + Event::InlineHtml(_) => {} // TODO(panekj): Implement + Event::InlineMath(_) => {} // TODO(panekj): Implement + Event::DisplayMath(_) => {} // TODO(panekj): Implement + } + } + + if builder_dirty { + let mut text_layout = TextLayout::new(); + text_layout.set_text(¤t_text, attr_list, None); + res.push(MarkdownContent::Text(text_layout)); + } + + res +} + +fn attribute_for_tag<'a>( + default_attrs: Attrs<'a>, + tag: &Tag, + code_font_family: &'a [FamilyOwned], + config: &LapceConfig, +) -> Option> { + use pulldown_cmark::HeadingLevel; + match tag { + Tag::Heading { + level, + id: _, + classes: _, + attrs: _, + } => { + // The size calculations are based on the em values given at + // https://drafts.csswg.org/css2/#html-stylesheet + let font_scale = match level { + HeadingLevel::H1 => 2.0, + HeadingLevel::H2 => 1.5, + HeadingLevel::H3 => 1.17, + HeadingLevel::H4 => 1.0, + HeadingLevel::H5 => 0.83, + HeadingLevel::H6 => 0.75, + }; + let font_size = font_scale * config.ui.font_size() as f64; + Some( + default_attrs + .font_size(font_size as f32) + .weight(Weight::BOLD), + ) + } + Tag::BlockQuote(_block_quote) => Some( + default_attrs + .style(Style::Italic) + .color(config.color(LapceColor::MARKDOWN_BLOCKQUOTE)), + ), + Tag::CodeBlock(_) => Some(default_attrs.family(code_font_family)), + Tag::Emphasis => Some(default_attrs.style(Style::Italic)), + Tag::Strong => Some(default_attrs.weight(Weight::BOLD)), + // TODO: Strikethrough support + Tag::Link { + link_type: _, + dest_url: _, + title: _, + id: _, + } => { + // TODO: Link support + Some(default_attrs.color(config.color(LapceColor::EDITOR_LINK))) + } + // All other tags are currently ignored + _ => None, + } +} + +/// Decides whether newlines should be added after a specific markdown tag +fn should_add_newline_after_tag(tag: &Tag) -> bool { + !matches!( + tag, + Tag::Emphasis | Tag::Strong | Tag::Strikethrough | Tag::Link { .. } + ) +} + +/// Whether it should skip the text node after a specific tag +/// For example, images are skipped because it emits their title as a separate text node. +fn should_skip_text_in_tag(tag: &Tag) -> bool { + matches!(tag, Tag::Image { .. }) +} + +fn md_language_to_lapce_language(lang: &str) -> Option { + // TODO: There are many other names commonly used that should be supported + LapceLanguage::from_name(lang) +} + +/// Highlight the text in a richtext builder like it was a markdown codeblock +pub fn highlight_as_code( + attr_list: &mut AttrsList, + default_attrs: Attrs, + language: Option, + text: &str, + start_offset: usize, + config: &LapceConfig, +) { + let syntax = language.map(Syntax::from_language); + + let styles = syntax + .map(|mut syntax| { + syntax.parse(0, Rope::from(text), None); + syntax.styles + }) + .unwrap_or(None); + + if let Some(styles) = styles { + for (range, style) in styles.iter() { + if let Some(color) = style + .fg_color + .as_ref() + .and_then(|fg| config.style_color(fg)) + { + attr_list.add_span( + start_offset + range.start..start_offset + range.end, + default_attrs.clone().color(color), + ); + } + } + } +} + +pub fn from_marked_string( + text: MarkedString, + config: &LapceConfig, +) -> Vec { + match text { + MarkedString::String(text) => parse_markdown(&text, 1.8, config), + // This is a short version of a code block + MarkedString::LanguageString(code) => { + // TODO: We could simply construct the MarkdownText directly + // Simply construct the string as if it was written directly + parse_markdown( + &format!("```{}\n{}\n```", code.language, code.value), + 1.8, + config, + ) + } + } +} + +pub fn from_plaintext( + text: &str, + line_height: f64, + config: &LapceConfig, +) -> Vec { + let mut text_layout = TextLayout::new(); + text_layout.set_text( + text, + AttrsList::new( + Attrs::new() + .font_size(config.ui.font_size() as f32) + .line_height(LineHeightValue::Normal(line_height as f32)), + ), + None, + ); + vec![MarkdownContent::Text(text_layout)] +} diff --git a/lapce-app/src/palette.rs b/lapce-app/src/palette.rs new file mode 100644 index 0000000..fb85013 --- /dev/null +++ b/lapce-app/src/palette.rs @@ -0,0 +1,1700 @@ +use std::{ + cell::RefCell, + collections::{HashMap, HashSet}, + path::PathBuf, + rc::Rc, + sync::{ + Arc, + atomic::{AtomicU64, Ordering}, + mpsc::{Receiver, Sender, TryRecvError, channel}, + }, + time::Instant, +}; + +use anyhow::Result; +use floem::{ + ext_event::{create_ext_action, create_signal_from_channel}, + keyboard::Modifiers, + reactive::{ + ReadSignal, RwSignal, Scope, SignalGet, SignalUpdate, SignalWith, + use_context, + }, +}; +use im::Vector; +use itertools::Itertools; +use lapce_core::{ + buffer::rope_text::RopeText, command::FocusCommand, language::LapceLanguage, + line_ending::LineEnding, mode::Mode, movement::Movement, selection::Selection, + syntax::Syntax, +}; +use lapce_rpc::proxy::ProxyResponse; +use lapce_xi_rope::Rope; +use lsp_types::{DocumentSymbol, DocumentSymbolResponse}; +use nucleo::Utf32Str; +use strum::{EnumMessage, IntoEnumIterator}; +use tracing::error; + +use self::{ + item::{PaletteItem, PaletteItemContent}, + kind::PaletteKind, +}; +use crate::{ + command::{ + CommandExecuted, CommandKind, InternalCommand, LapceCommand, WindowCommand, + }, + db::LapceDb, + debug::{RunDebugConfigs, RunDebugMode}, + editor::{ + EditorData, + location::{EditorLocation, EditorPosition}, + }, + keypress::{KeyPressData, KeyPressFocus, condition::Condition}, + lsp::path_from_url, + main_split::MainSplitData, + source_control::SourceControlData, + window_tab::{CommonData, Focus}, + workspace::{LapceWorkspace, LapceWorkspaceType, SshHost}, +}; + +pub mod item; +pub mod kind; + +pub const DEFAULT_RUN_TOML: &str = include_str!("../../defaults/run.toml"); + +#[derive(Clone, PartialEq, Eq)] +pub enum PaletteStatus { + Inactive, + Started, + Done, +} + +#[derive(Clone, Debug)] +pub struct PaletteInput { + pub input: String, + pub kind: PaletteKind, +} + +impl PaletteInput { + /// Update the current input in the palette, and the kind of palette it is + pub fn update_input(&mut self, input: String, kind: PaletteKind) { + self.kind = kind.get_palette_kind(&input); + self.input = self.kind.get_input(&input).to_string(); + } +} + +#[derive(Clone)] +pub struct PaletteData { + run_id_counter: Arc, + pub run_id: RwSignal, + pub workspace: Arc, + pub status: RwSignal, + pub index: RwSignal, + pub preselect_index: RwSignal>, + pub items: RwSignal>, + pub filtered_items: ReadSignal>, + pub input: RwSignal, + kind: RwSignal, + pub input_editor: EditorData, + pub preview_editor: EditorData, + pub has_preview: RwSignal, + pub keypress: ReadSignal, + /// Listened on for which entry in the palette has been clicked + pub clicked_index: RwSignal>, + pub executed_commands: Rc>>, + pub executed_run_configs: Rc>>, + pub main_split: MainSplitData, + pub references: RwSignal>, + pub source_control: SourceControlData, + pub common: Rc, + left_diff_path: RwSignal>, +} + +impl std::fmt::Debug for PaletteData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("PaletteData").finish() + } +} + +impl PaletteData { + pub fn new( + cx: Scope, + workspace: Arc, + main_split: MainSplitData, + keypress: ReadSignal, + source_control: SourceControlData, + common: Rc, + ) -> Self { + let status = cx.create_rw_signal(PaletteStatus::Inactive); + let items = cx.create_rw_signal(im::Vector::new()); + let preselect_index = cx.create_rw_signal(None); + let index = cx.create_rw_signal(0); + let references = cx.create_rw_signal(Vec::new()); + let input = cx.create_rw_signal(PaletteInput { + input: "".to_string(), + kind: PaletteKind::File, + }); + let kind = cx.create_rw_signal(PaletteKind::File); + let input_editor = main_split.editors.make_local(cx, common.clone()); + let preview_editor = main_split.editors.make_local(cx, common.clone()); + let has_preview = cx.create_rw_signal(false); + let run_id = cx.create_rw_signal(0); + let run_id_counter = Arc::new(AtomicU64::new(0)); + + let (run_tx, run_rx) = channel(); + { + let run_id = run_id.read_only(); + let input = input.read_only(); + let items = items.read_only(); + let tx = run_tx; + { + let tx = tx.clone(); + // this effect only monitors items change + cx.create_effect(move |_| { + let items = items.get(); + let input = input.get_untracked(); + let run_id = run_id.get_untracked(); + let preselect_index = + preselect_index.try_update(|i| i.take()).unwrap(); + if let Err(err) = + tx.send((run_id, input.input, items, preselect_index)) + { + tracing::error!("{:?}", err); + } + }); + } + // this effect only monitors input change + cx.create_effect(move |last_kind| { + let input = input.get(); + let kind = input.kind; + if last_kind != Some(kind) { + return kind; + } + let items = items.get_untracked(); + let run_id = run_id.get_untracked(); + if let Err(err) = tx.send((run_id, input.input, items, None)) { + tracing::error!("{:?}", err); + } + kind + }); + } + let (resp_tx, resp_rx) = channel(); + { + let run_id = run_id_counter.clone(); + std::thread::Builder::new() + .name("PaletteUpdateProcess".to_owned()) + .spawn(move || { + Self::update_process(run_id, run_rx, resp_tx); + }) + .unwrap(); + } + let (filtered_items, set_filtered_items) = + cx.create_signal(im::Vector::new()); + { + let resp = create_signal_from_channel(resp_rx); + let run_id = run_id.read_only(); + let input = input.read_only(); + cx.create_effect(move |_| { + if let Some(( + filter_run_id, + filter_input, + new_items, + preselect_index, + )) = resp.get() + { + if run_id.get_untracked() == filter_run_id + && input.get_untracked().input == filter_input + { + set_filtered_items.set(new_items); + let i = preselect_index.unwrap_or(0); + index.set(i); + } + } + }); + } + + let clicked_index = cx.create_rw_signal(Option::::None); + let left_diff_path = cx.create_rw_signal(None); + + let palette = Self { + run_id_counter, + main_split, + run_id, + workspace, + status, + index, + preselect_index, + items, + filtered_items, + input_editor, + preview_editor, + has_preview, + input, + kind, + keypress, + clicked_index, + executed_commands: Rc::new(RefCell::new(HashMap::new())), + executed_run_configs: Rc::new(RefCell::new(HashMap::new())), + references, + source_control, + common, + left_diff_path, + }; + + { + let palette = palette.clone(); + let clicked_index = clicked_index.read_only(); + let index = index.write_only(); + cx.create_effect(move |_| { + if let Some(clicked_index) = clicked_index.get() { + index.set(clicked_index); + palette.select(); + } + }); + } + + { + let palette = palette.clone(); + let doc = palette.input_editor.doc(); + let input = palette.input; + let status = palette.status.read_only(); + let preset_kind = palette.kind.read_only(); + // Monitors when the palette's input changes, so that it can update the stored input + // and kind of palette. + cx.create_effect(move |last_input| { + // TODO(minor, perf): this could have perf issues if the user accidentally pasted a huge amount of text into the palette. + let new_input = doc.buffer.with(|buffer| buffer.to_string()); + + let status = status.get_untracked(); + if status == PaletteStatus::Inactive { + // If the status is inactive, we set the input to None, + // so that when we actually run the palette, the input + // can be compared with this None. + return None; + } + + let last_input = last_input.flatten(); + + // If the input is not equivalent to the current input, or not initialized, then we + // need to update the information about the palette. + let changed = last_input.as_deref() != Some(new_input.as_str()); + + if changed { + let new_kind = input + .try_update(|input| { + let kind = input.kind; + input.update_input( + new_input.clone(), + preset_kind.get_untracked(), + ); + if last_input.is_none() || kind != input.kind { + Some(input.kind) + } else { + None + } + }) + .unwrap(); + if let Some(new_kind) = new_kind { + palette.run_inner(new_kind); + } else if input + .with_untracked(|i| i.kind == PaletteKind::WorkspaceSymbol) + { + palette.run_inner(PaletteKind::WorkspaceSymbol); + } + } + Some(new_input) + }); + } + + { + let palette = palette.clone(); + cx.create_effect(move |_| { + let _ = palette.index.get(); + palette.preview(); + }); + } + + { + let palette = palette.clone(); + cx.create_effect(move |_| { + let focus = palette.common.focus.get(); + if focus != Focus::Palette + && palette.status.get_untracked() != PaletteStatus::Inactive + { + palette.cancel(); + } + }); + } + + palette + } + + /// Start and focus the palette for the given kind. + pub fn run(&self, kind: PaletteKind) { + self.common.focus.set(Focus::Palette); + self.status.set(PaletteStatus::Started); + let symbol = kind.symbol(); + self.kind.set(kind); + // Refresh the palette input with only the symbol prefix, losing old content. + self.input_editor.doc().reload(Rope::from(symbol), true); + self.input_editor + .cursor() + .update(|cursor| cursor.set_insert(Selection::caret(symbol.len()))); + } + + /// Get the placeholder text to use in the palette input field. + pub fn placeholder_text(&self) -> &'static str { + match self.kind.get() { + PaletteKind::SshHost => { + "Type [user@]host or select a previously connected workspace below" + } + PaletteKind::DiffFiles => { + if self.left_diff_path.with(Option::is_some) { + "Select right file" + } else { + "Seleft left file" + } + } + _ => "", + } + } + + /// Execute the internal behavior of the palette for the given kind. This ignores updating and + /// focusing the palette input. + fn run_inner(&self, kind: PaletteKind) { + self.has_preview.set(false); + + let run_id = self.run_id_counter.fetch_add(1, Ordering::Relaxed) + 1; + self.run_id.set(run_id); + + match kind { + PaletteKind::PaletteHelp => self.get_palette_help(), + PaletteKind::File | PaletteKind::DiffFiles => { + self.get_files(); + } + PaletteKind::HelpAndFile => self.get_palette_help_and_file(), + PaletteKind::Line => { + self.get_lines(); + } + PaletteKind::Command => { + self.get_commands(); + } + PaletteKind::Workspace => { + self.get_workspaces(); + } + PaletteKind::Reference => { + self.get_references(); + } + PaletteKind::DocumentSymbol => { + self.get_document_symbols(); + } + PaletteKind::WorkspaceSymbol => { + self.get_workspace_symbols(); + } + PaletteKind::SshHost => { + self.get_ssh_hosts(); + } + #[cfg(windows)] + PaletteKind::WslHost => { + self.get_wsl_hosts(); + } + PaletteKind::RunAndDebug => { + self.get_run_configs(); + } + PaletteKind::ColorTheme => { + self.get_color_themes(); + } + PaletteKind::IconTheme => { + self.get_icon_themes(); + } + PaletteKind::Language => { + self.get_languages(); + } + PaletteKind::LineEnding => { + self.get_line_endings(); + } + PaletteKind::SCMReferences => { + self.get_scm_references(); + } + PaletteKind::TerminalProfile => self.get_terminal_profiles(), + } + } + + /// Initialize the palette with a list of the available palette kinds. + fn get_palette_help(&self) { + let items = self.get_palette_help_items(); + self.items.set(items); + } + + fn get_palette_help_items(&self) -> Vector { + PaletteKind::iter() + .filter_map(|kind| { + // Don't include PaletteHelp as the user is already here. + (kind != PaletteKind::PaletteHelp) + .then(|| { + let symbol = kind.symbol(); + + // Only include palette kinds accessible by typing a prefix into the + // palette. + (kind == PaletteKind::File || !symbol.is_empty()) + .then_some(kind) + }) + .flatten() + }) + .filter_map(|kind| kind.command().map(|cmd| (kind, cmd))) + .map(|(kind, cmd)| { + let description = kind.symbol().to_string() + + " " + + cmd.get_message().unwrap_or(""); + + PaletteItem { + content: PaletteItemContent::PaletteHelp { cmd }, + filter_text: description, + score: 0, + indices: vec![], + } + }) + .collect() + } + + fn get_palette_help_and_file(&self) { + let help_items: Vector = self.get_palette_help_items(); + self.get_files_and_prepend(Some(help_items)); + } + + // get the files in the current workspace + // and prepend items if prepend is some + // e.g. help_and_file + fn get_files_and_prepend(&self, prepend: Option>) { + let workspace = self.workspace.clone(); + let set_items = self.items.write_only(); + let send = + create_ext_action(self.common.scope, move |items: Vec| { + let items = items + .into_iter() + .map(|full_path| { + // Strip the workspace prefix off the path, to avoid clutter + let path = + if let Some(workspace_path) = workspace.path.as_ref() { + full_path + .strip_prefix(workspace_path) + .unwrap_or(&full_path) + .to_path_buf() + } else { + full_path.clone() + }; + let filter_text = path.to_string_lossy().into_owned(); + PaletteItem { + content: PaletteItemContent::File { path, full_path }, + filter_text, + score: 0, + indices: Vec::new(), + } + }) + .collect::>(); + let mut new_items = im::Vector::new(); + if let Some(prepend) = prepend { + new_items.append(prepend); + } + new_items.append(items); + set_items.set(new_items); + }); + self.common.proxy.get_files(move |result| { + if let Ok(ProxyResponse::GetFilesResponse { items }) = result { + send(items); + } + }); + } + + /// Initialize the palette with the files in the current workspace. + fn get_files(&self) { + self.get_files_and_prepend(None); + } + + /// Initialize the palette with the lines in the current document. + fn get_lines(&self) { + let editor = self.main_split.active_editor.get_untracked(); + let doc = match editor { + Some(editor) => editor.doc(), + None => { + return; + } + }; + + let buffer = doc.buffer.get_untracked(); + let last_line_number = buffer.last_line() + 1; + let last_line_number_len = last_line_number.to_string().len(); + let items = buffer + .text() + .lines(0..buffer.len()) + .enumerate() + .map(|(i, l)| { + let line_number = i + 1; + let text = format!( + "{}{} {}", + line_number, + vec![" "; last_line_number_len - line_number.to_string().len()] + .join(""), + l + ); + PaletteItem { + content: PaletteItemContent::Line { + line: i, + content: text.clone(), + }, + filter_text: text, + score: 0, + indices: vec![], + } + }) + .collect(); + self.items.set(items); + } + + fn get_commands(&self) { + const EXCLUDED_ITEMS: &[&str] = &["palette.command"]; + + let items = self.keypress.with_untracked(|keypress| { + // Get all the commands we've executed, and sort them by how recently they were + // executed. Ignore commands without descriptions. + let mut items: im::Vector = self + .executed_commands + .borrow() + .iter() + .sorted_by_key(|(_, i)| *i) + .rev() + .filter_map(|(key, _)| { + keypress.commands.get(key).and_then(|c| { + c.kind.desc().as_ref().map(|m| PaletteItem { + content: PaletteItemContent::Command { cmd: c.clone() }, + filter_text: m.to_string(), + score: 0, + indices: vec![], + }) + }) + }) + .collect(); + // Add all the rest of the commands, ignoring palette commands (because we're in it) + // and commands that are sorted earlier due to being executed. + items.extend(keypress.commands.iter().filter_map(|(_, c)| { + if EXCLUDED_ITEMS.contains(&c.kind.str()) { + return None; + } + + if self.executed_commands.borrow().contains_key(c.kind.str()) { + return None; + } + + c.kind.desc().as_ref().map(|m| PaletteItem { + content: PaletteItemContent::Command { cmd: c.clone() }, + filter_text: m.to_string(), + score: 0, + indices: vec![], + }) + })); + + items + }); + + self.items.set(items); + } + + /// Initialize the palette with all the available workspaces, local and remote. + fn get_workspaces(&self) { + let db: Arc = use_context().unwrap(); + let workspaces = db.recent_workspaces().unwrap_or_default(); + + let items = workspaces + .into_iter() + .filter_map(|w| { + let text = w.path.as_ref()?.to_str()?.to_string(); + let filter_text = match &w.kind { + LapceWorkspaceType::Local => text, + LapceWorkspaceType::RemoteSSH(remote) => { + format!("[{remote}] {text}") + } + #[cfg(windows)] + LapceWorkspaceType::RemoteWSL(remote) => { + format!("[{remote}] {text}") + } + }; + Some(PaletteItem { + content: PaletteItemContent::Workspace { workspace: w }, + filter_text, + score: 0, + indices: vec![], + }) + }) + .collect(); + + self.items.set(items); + } + + /// Initialize the list of references in the file, from the current editor location. + fn get_references(&self) { + let items = self + .references + .get_untracked() + .into_iter() + .map(|l| { + let full_path = l.path.clone(); + let mut path = l.path.clone(); + if let Some(workspace_path) = self.workspace.path.as_ref() { + path = path + .strip_prefix(workspace_path) + .unwrap_or(&full_path) + .to_path_buf(); + } + let filter_text = path.to_str().unwrap_or("").to_string(); + PaletteItem { + content: PaletteItemContent::Reference { path, location: l }, + filter_text, + score: 0, + indices: vec![], + } + }) + .collect(); + + self.items.set(items); + } + + fn get_document_symbols(&self) { + let editor = self.main_split.active_editor.get_untracked(); + let doc = match editor { + Some(editor) => editor.doc(), + None => { + self.items.update(|items| items.clear()); + return; + } + }; + let path = doc + .content + .with_untracked(|content| content.path().cloned()); + let path = match path { + Some(path) => path, + None => { + self.items.update(|items| items.clear()); + return; + } + }; + + let set_items = self.items.write_only(); + let send = create_ext_action(self.common.scope, move |result| { + if let Ok(ProxyResponse::GetDocumentSymbols { resp }) = result { + let items = Self::format_document_symbol_resp(resp); + set_items.set(items); + } else { + set_items.update(|items| items.clear()); + } + }); + + self.common.proxy.get_document_symbols(path, move |result| { + send(result); + }); + } + + fn format_document_symbol_resp( + resp: DocumentSymbolResponse, + ) -> im::Vector { + match resp { + DocumentSymbolResponse::Flat(symbols) => symbols + .iter() + .map(|s| { + let mut filter_text = s.name.clone(); + if let Some(container_name) = s.container_name.as_ref() { + filter_text += container_name; + } + PaletteItem { + content: PaletteItemContent::DocumentSymbol { + kind: s.kind, + name: s.name.replace('\n', "↵"), + range: s.location.range, + container_name: s.container_name.clone(), + }, + filter_text, + score: 0, + indices: Vec::new(), + } + }) + .collect(), + DocumentSymbolResponse::Nested(symbols) => { + let mut items = im::Vector::new(); + for s in symbols { + Self::format_document_symbol(&mut items, None, s) + } + items + } + } + } + + fn format_document_symbol( + items: &mut im::Vector, + parent: Option, + s: DocumentSymbol, + ) { + items.push_back(PaletteItem { + content: PaletteItemContent::DocumentSymbol { + kind: s.kind, + name: s.name.replace('\n', "↵"), + range: s.range, + container_name: parent, + }, + filter_text: s.name.clone(), + score: 0, + indices: Vec::new(), + }); + if let Some(children) = s.children { + let parent = Some(s.name.replace('\n', "↵")); + for child in children { + Self::format_document_symbol(items, parent.clone(), child); + } + } + } + + fn get_workspace_symbols(&self) { + let input = self.input.get_untracked().input; + + let set_items = self.items.write_only(); + let send = create_ext_action(self.common.scope, move |result| { + if let Ok(ProxyResponse::GetWorkspaceSymbols { symbols }) = result { + let items: im::Vector = symbols + .iter() + .map(|s| { + // TODO: Should we be using filter text? + let mut filter_text = s.name.clone(); + if let Some(container_name) = s.container_name.as_ref() { + filter_text += container_name; + } + PaletteItem { + content: PaletteItemContent::WorkspaceSymbol { + kind: s.kind, + name: s.name.clone(), + location: EditorLocation { + path: path_from_url(&s.location.uri), + position: Some(EditorPosition::Position( + s.location.range.start, + )), + scroll_offset: None, + ignore_unconfirmed: false, + same_editor_tab: false, + }, + container_name: s.container_name.clone(), + }, + filter_text, + score: 0, + indices: Vec::new(), + } + }) + .collect(); + set_items.set(items); + } else { + set_items.update(|items| items.clear()); + } + }); + + self.common + .proxy + .get_workspace_symbols(input, move |result| { + send(result); + }); + } + + fn get_ssh_hosts(&self) { + let db: Arc = use_context().unwrap(); + let workspaces = db.recent_workspaces().unwrap_or_default(); + let mut hosts = HashSet::new(); + for workspace in workspaces.iter() { + if let LapceWorkspaceType::RemoteSSH(host) = &workspace.kind { + hosts.insert(host.clone()); + } + } + + let items = hosts + .iter() + .map(|host| PaletteItem { + content: PaletteItemContent::SshHost { host: host.clone() }, + filter_text: host.to_string(), + score: 0, + indices: vec![], + }) + .collect(); + self.items.set(items); + } + + #[cfg(windows)] + fn get_wsl_hosts(&self) { + use std::{os::windows::process::CommandExt, process}; + let cmd = process::Command::new("wsl") + .creation_flags(0x08000000) // CREATE_NO_WINDOW + .arg("-l") + .arg("-v") + .stdout(process::Stdio::piped()) + .output(); + + let distros = if let Ok(proc) = cmd { + let distros = String::from_utf16(bytemuck::cast_slice(&proc.stdout)) + .unwrap_or_default() + .lines() + .skip(1) + .filter_map(|line| { + let line = line.trim_start(); + // let default = line.starts_with('*'); + let name = line + .trim_start_matches('*') + .trim_start() + .split(' ') + .next()?; + Some(name.to_string()) + }) + .collect(); + + distros + } else { + vec![] + }; + + let db: Arc = use_context().unwrap(); + let workspaces = db.recent_workspaces().unwrap_or_default(); + let mut hosts = HashSet::new(); + for distro in distros { + hosts.insert(distro); + } + + for workspace in workspaces.iter() { + if let LapceWorkspaceType::RemoteWSL(host) = &workspace.kind { + hosts.insert(host.host.clone()); + } + } + + let items = hosts + .iter() + .map(|host| PaletteItem { + content: PaletteItemContent::WslHost { + host: crate::workspace::WslHost { host: host.clone() }, + }, + filter_text: host.to_string(), + score: 0, + indices: vec![], + }) + .collect(); + self.items.set(items); + } + + fn set_run_configs(&self, content: String) { + let configs: Option = toml::from_str(&content).ok(); + if configs.is_none() { + if let Some(path) = self.workspace.path.as_ref() { + let path = path.join(".lapce").join("run.toml"); + self.common + .internal_command + .send(InternalCommand::OpenFile { path }); + } + } + + let executed_run_configs = self.executed_run_configs.borrow(); + let mut items = Vec::new(); + if let Some(configs) = configs.as_ref() { + for config in &configs.configs { + items.push(( + executed_run_configs + .get(&(RunDebugMode::Run, config.name.clone())), + PaletteItem { + content: PaletteItemContent::RunAndDebug { + mode: RunDebugMode::Run, + config: config.clone(), + }, + filter_text: format!( + "Run {} {} {}", + config.name, + config.program, + config.args.clone().unwrap_or_default().join(" ") + ), + score: 0, + indices: vec![], + }, + )); + if config.ty.is_some() { + items.push(( + executed_run_configs + .get(&(RunDebugMode::Debug, config.name.clone())), + PaletteItem { + content: PaletteItemContent::RunAndDebug { + mode: RunDebugMode::Debug, + config: config.clone(), + }, + filter_text: format!( + "Debug {} {} {}", + config.name, + config.program, + config.args.clone().unwrap_or_default().join(" ") + ), + score: 0, + indices: vec![], + }, + )); + } + } + } + + items.sort_by_key(|(executed, _item)| std::cmp::Reverse(executed.copied())); + self.items + .set(items.into_iter().map(|(_, item)| item).collect()); + } + + fn get_run_configs(&self) { + if let Some(workspace) = self.common.workspace.path.as_deref() { + let run_toml = workspace.join(".lapce").join("run.toml"); + let (doc, new_doc) = self.main_split.get_doc(run_toml.clone(), None); + if !new_doc { + let content = doc.buffer.with_untracked(|b| b.to_string()); + self.set_run_configs(content); + } else { + let loaded = doc.loaded; + let palette = self.clone(); + self.common.scope.create_effect(move |prev_loaded| { + if prev_loaded == Some(true) { + return true; + } + + let loaded = loaded.get(); + if loaded { + let content = doc.buffer.with_untracked(|b| b.to_string()); + if content.is_empty() { + doc.reload(Rope::from(DEFAULT_RUN_TOML), false); + } + palette.set_run_configs(content); + } + loaded + }); + } + } + } + + fn get_color_themes(&self) { + let config = self.common.config.get_untracked(); + let items = config + .color_theme_list() + .iter() + .map(|name| PaletteItem { + content: PaletteItemContent::ColorTheme { name: name.clone() }, + filter_text: name.clone(), + score: 0, + indices: Vec::new(), + }) + .collect(); + self.preselect_matching( + &items, + &self.common.config.get_untracked().color_theme.name, + ); + self.items.set(items); + } + + fn get_icon_themes(&self) { + let config = self.common.config.get_untracked(); + let items = config + .icon_theme_list() + .iter() + .map(|name| PaletteItem { + content: PaletteItemContent::IconTheme { name: name.clone() }, + filter_text: name.clone(), + score: 0, + indices: Vec::new(), + }) + .collect(); + self.preselect_matching( + &items, + &self.common.config.get_untracked().icon_theme.name, + ); + self.items.set(items); + } + + fn get_languages(&self) { + let langs = LapceLanguage::languages(); + let items = langs + .iter() + .map(|lang| PaletteItem { + content: PaletteItemContent::Language { + name: lang.to_string(), + }, + filter_text: lang.to_string(), + score: 0, + indices: Vec::new(), + }) + .collect(); + if let Some(editor) = self.main_split.active_editor.get_untracked() { + let doc = editor.doc(); + let language = + doc.syntax().with_untracked(|syntax| syntax.language.name()); + self.preselect_matching(&items, language); + } + self.items.set(items); + } + + fn get_line_endings(&self) { + let items = [LineEnding::Lf, LineEnding::CrLf] + .iter() + .map(|l| PaletteItem { + content: PaletteItemContent::LineEnding { kind: *l }, + filter_text: l.as_str().to_string(), + score: 0, + indices: Vec::new(), + }) + .collect(); + if let Some(editor) = self.main_split.active_editor.get_untracked() { + let doc = editor.doc(); + let line_ending = doc.line_ending(); + self.preselect_matching(&items, line_ending.as_str()); + } + self.items.set(items); + } + + fn get_scm_references(&self) { + let branches = self.source_control.branches.get_untracked(); + let tags = self.source_control.tags.get_untracked(); + let mut items: im::Vector = im::Vector::new(); + for refs in branches.into_iter() { + items.push_back(PaletteItem { + content: PaletteItemContent::SCMReference { + name: refs.to_owned(), + }, + filter_text: refs.to_owned(), + score: 0, + indices: Vec::new(), + }); + } + for refs in tags.into_iter() { + items.push_back(PaletteItem { + content: PaletteItemContent::SCMReference { + name: refs.to_owned(), + }, + filter_text: refs.to_owned(), + score: 0, + indices: Vec::new(), + }); + } + self.items.set(items); + } + + fn get_terminal_profiles(&self) { + let profiles = self.common.config.get().terminal.profiles.clone(); + let mut items: im::Vector = im::Vector::new(); + + for (name, profile) in profiles.into_iter() { + let uri = match lsp_types::Url::parse(&format!( + "file://{}", + profile.workdir.unwrap_or_default().display() + )) { + Ok(v) => Some(v), + Err(e) => { + error!("Failed to parse uri: {e}"); + None + } + }; + + items.push_back(PaletteItem { + content: PaletteItemContent::TerminalProfile { + name: name.to_owned(), + profile: lapce_rpc::terminal::TerminalProfile { + name: name.to_owned(), + command: profile.command, + arguments: profile.arguments, + workdir: uri, + environment: profile.environment, + }, + }, + filter_text: name.to_owned(), + score: 0, + indices: Vec::new(), + }); + } + + self.items.set(items); + } + + fn preselect_matching(&self, items: &im::Vector, matching: &str) { + let Some((idx, _)) = items + .iter() + .find_position(|item| item.filter_text == matching) + else { + return; + }; + + self.preselect_index.set(Some(idx)); + } + + fn select(&self) { + let index = self.index.get_untracked(); + let items = self.filtered_items.get_untracked(); + self.close(); + if let Some(item) = items.get(index) { + match &item.content { + PaletteItemContent::PaletteHelp { cmd } => { + let cmd = LapceCommand { + kind: CommandKind::Workbench(cmd.clone()), + data: None, + }; + + self.common.lapce_command.send(cmd); + } + PaletteItemContent::File { full_path, .. } => { + if self.kind.get_untracked() == PaletteKind::DiffFiles { + if let Some(left_path) = + self.left_diff_path.try_update(Option::take).flatten() + { + self.common.internal_command.send( + InternalCommand::OpenDiffFiles { + left_path, + right_path: full_path.clone(), + }, + ); + } else { + self.left_diff_path.set(Some(full_path.clone())); + self.run(PaletteKind::DiffFiles); + } + } else { + self.common.internal_command.send( + InternalCommand::OpenFile { + path: full_path.clone(), + }, + ); + } + } + PaletteItemContent::Line { line, .. } => { + let editor = self.main_split.active_editor.get_untracked(); + let doc = match editor { + Some(editor) => editor.doc(), + None => { + return; + } + }; + let path = doc + .content + .with_untracked(|content| content.path().cloned()); + let path = match path { + Some(path) => path, + None => return, + }; + self.common.internal_command.send( + InternalCommand::JumpToLocation { + location: EditorLocation { + path, + position: Some(EditorPosition::Line(*line)), + scroll_offset: None, + ignore_unconfirmed: false, + same_editor_tab: false, + }, + }, + ); + } + PaletteItemContent::Command { cmd } => { + self.common.lapce_command.send(cmd.clone()); + } + PaletteItemContent::Workspace { workspace } => { + self.common.window_common.window_command.send( + WindowCommand::SetWorkspace { + workspace: workspace.clone(), + }, + ); + } + PaletteItemContent::Reference { location, .. } => { + self.common.internal_command.send( + InternalCommand::JumpToLocation { + location: location.clone(), + }, + ); + } + PaletteItemContent::SshHost { host } => { + self.common.window_common.window_command.send( + WindowCommand::SetWorkspace { + workspace: LapceWorkspace { + kind: LapceWorkspaceType::RemoteSSH(host.clone()), + path: None, + last_open: 0, + }, + }, + ); + } + #[cfg(windows)] + PaletteItemContent::WslHost { host } => { + self.common.window_common.window_command.send( + WindowCommand::SetWorkspace { + workspace: LapceWorkspace { + kind: LapceWorkspaceType::RemoteWSL(host.clone()), + path: None, + last_open: 0, + }, + }, + ); + } + PaletteItemContent::DocumentSymbol { range, .. } => { + let editor = self.main_split.active_editor.get_untracked(); + let doc = match editor { + Some(editor) => editor.doc(), + None => { + return; + } + }; + let path = doc + .content + .with_untracked(|content| content.path().cloned()); + let path = match path { + Some(path) => path, + None => return, + }; + self.common.internal_command.send( + InternalCommand::JumpToLocation { + location: EditorLocation { + path, + position: Some(EditorPosition::Position( + range.start, + )), + scroll_offset: None, + ignore_unconfirmed: false, + same_editor_tab: false, + }, + }, + ); + } + PaletteItemContent::WorkspaceSymbol { location, .. } => { + self.common.internal_command.send( + InternalCommand::JumpToLocation { + location: location.clone(), + }, + ); + } + PaletteItemContent::RunAndDebug { mode, config } => { + self.common.internal_command.send( + InternalCommand::RunAndDebug { + mode: *mode, + config: config.clone(), + }, + ); + } + PaletteItemContent::ColorTheme { name } => self + .common + .internal_command + .send(InternalCommand::SetColorTheme { + name: name.clone(), + save: true, + }), + PaletteItemContent::IconTheme { name } => self + .common + .internal_command + .send(InternalCommand::SetIconTheme { + name: name.clone(), + save: true, + }), + PaletteItemContent::Language { name } => { + let editor = self.main_split.active_editor.get_untracked(); + let doc = match editor { + Some(editor) => editor.doc(), + None => { + return; + } + }; + if name.is_empty() || name.to_lowercase().eq("plain text") { + doc.set_syntax(Syntax::plaintext()) + } else { + let lang = match LapceLanguage::from_name(name) { + Some(v) => v, + None => return, + }; + doc.set_language(lang); + } + doc.trigger_syntax_change(None); + } + PaletteItemContent::LineEnding { kind } => { + let Some(editor) = self.main_split.active_editor.get_untracked() + else { + return; + }; + let doc = editor.doc(); + + doc.buffer.update(|buffer| { + buffer.set_line_ending(*kind); + }); + } + PaletteItemContent::SCMReference { name } => { + self.common + .lapce_command + .send(crate::command::LapceCommand { + kind: CommandKind::Workbench( + crate::command::LapceWorkbenchCommand::CheckoutReference, + ), + data: Some(serde_json::json!(name.to_owned())), + }); + } + PaletteItemContent::TerminalProfile { name: _, profile } => self + .common + .internal_command + .send(InternalCommand::NewTerminal { + profile: Some(profile.to_owned()), + }), + } + } else if self.kind.get_untracked() == PaletteKind::SshHost { + let input = self.input.with_untracked(|input| input.input.clone()); + let ssh = SshHost::from_string(&input); + self.common.window_common.window_command.send( + WindowCommand::SetWorkspace { + workspace: LapceWorkspace { + kind: LapceWorkspaceType::RemoteSSH(ssh), + path: None, + last_open: 0, + }, + }, + ); + } + } + + /// Update the preview for the currently active palette item, if it has one. + fn preview(&self) { + if self.status.get_untracked() == PaletteStatus::Inactive { + return; + } + + let index = self.index.get_untracked(); + let items = self.filtered_items.get_untracked(); + if let Some(item) = items.get(index) { + match &item.content { + PaletteItemContent::PaletteHelp { .. } => {} + PaletteItemContent::File { .. } => {} + PaletteItemContent::Line { line, .. } => { + self.has_preview.set(true); + let editor = self.main_split.active_editor.get_untracked(); + let doc = match editor { + Some(editor) => editor.doc(), + None => { + return; + } + }; + let path = doc + .content + .with_untracked(|content| content.path().cloned()); + let path = match path { + Some(path) => path, + None => return, + }; + self.preview_editor.update_doc(doc); + self.preview_editor.go_to_location( + EditorLocation { + path, + position: Some(EditorPosition::Line(*line)), + scroll_offset: None, + ignore_unconfirmed: false, + same_editor_tab: false, + }, + false, + None, + ); + } + PaletteItemContent::Command { .. } => {} + PaletteItemContent::Workspace { .. } => {} + PaletteItemContent::RunAndDebug { .. } => {} + PaletteItemContent::SshHost { .. } => {} + #[cfg(windows)] + PaletteItemContent::WslHost { .. } => {} + PaletteItemContent::Language { .. } => {} + PaletteItemContent::LineEnding { .. } => {} + PaletteItemContent::Reference { location, .. } => { + self.has_preview.set(true); + let (doc, new_doc) = + self.main_split.get_doc(location.path.clone(), None); + self.preview_editor.update_doc(doc); + self.preview_editor.go_to_location( + location.clone(), + new_doc, + None, + ); + } + PaletteItemContent::DocumentSymbol { range, .. } => { + self.has_preview.set(true); + let editor = self.main_split.active_editor.get_untracked(); + let doc = match editor { + Some(editor) => editor.doc(), + None => { + return; + } + }; + let path = doc + .content + .with_untracked(|content| content.path().cloned()); + let path = match path { + Some(path) => path, + None => return, + }; + self.preview_editor.update_doc(doc); + self.preview_editor.go_to_location( + EditorLocation { + path, + position: Some(EditorPosition::Position(range.start)), + scroll_offset: None, + ignore_unconfirmed: false, + same_editor_tab: false, + }, + false, + None, + ); + } + PaletteItemContent::WorkspaceSymbol { location, .. } => { + self.has_preview.set(true); + let (doc, new_doc) = + self.main_split.get_doc(location.path.clone(), None); + self.preview_editor.update_doc(doc); + self.preview_editor.go_to_location( + location.clone(), + new_doc, + None, + ); + } + PaletteItemContent::ColorTheme { name } => self + .common + .internal_command + .send(InternalCommand::SetColorTheme { + name: name.clone(), + save: false, + }), + PaletteItemContent::IconTheme { name } => self + .common + .internal_command + .send(InternalCommand::SetIconTheme { + name: name.clone(), + save: false, + }), + PaletteItemContent::SCMReference { .. } => {} + PaletteItemContent::TerminalProfile { .. } => {} + } + } + } + + /// Cancel the palette, doing cleanup specific to the palette kind. + fn cancel(&self) { + if let PaletteKind::ColorTheme | PaletteKind::IconTheme = + self.kind.get_untracked() + { + // TODO(minor): We don't really need to reload the *entire config* here! + self.common + .internal_command + .send(InternalCommand::ReloadConfig); + } + + self.left_diff_path.set(None); + self.close(); + } + + /// Close the palette, reverting focus back to the workbench. + fn close(&self) { + self.status.set(PaletteStatus::Inactive); + if self.common.focus.get_untracked() == Focus::Palette { + self.common.focus.set(Focus::Workbench); + } + self.has_preview.set(false); + self.items.update(|items| items.clear()); + self.input_editor.doc().reload(Rope::from(""), true); + self.input_editor + .cursor() + .update(|cursor| cursor.set_insert(Selection::caret(0))); + } + + /// Move to the next entry in the palette list, wrapping around if needed. + fn next(&self) { + let index = self.index.get_untracked(); + let len = self.filtered_items.with_untracked(|i| i.len()); + let new_index = Movement::Down.update_index(index, len, 1, true); + self.index.set(new_index); + } + + /// Move to the previous entry in the palette list, wrapping around if needed. + fn previous(&self) { + let index = self.index.get_untracked(); + let len = self.filtered_items.with_untracked(|i| i.len()); + let new_index = Movement::Up.update_index(index, len, 1, true); + self.index.set(new_index); + } + + fn next_page(&self) { + // TODO: implement + } + + fn previous_page(&self) { + // TODO: implement + } + + fn run_focus_command(&self, cmd: &FocusCommand) -> CommandExecuted { + match cmd { + FocusCommand::ModalClose => { + self.cancel(); + } + FocusCommand::ListNext => { + self.next(); + } + FocusCommand::ListNextPage => { + self.next_page(); + } + FocusCommand::ListPrevious => { + self.previous(); + } + FocusCommand::ListPreviousPage => { + self.previous_page(); + } + FocusCommand::ListSelect => { + self.select(); + } + _ => return CommandExecuted::No, + } + CommandExecuted::Yes + } + + fn filter_items( + run_id: Arc, + current_run_id: u64, + input: &str, + items: im::Vector, + matcher: &mut nucleo::Matcher, + ) -> Option> { + if input.is_empty() { + return Some(items); + } + + let pattern = nucleo::pattern::Pattern::parse( + input, + nucleo::pattern::CaseMatching::Ignore, + nucleo::pattern::Normalization::Smart, + ); + + // NOTE: We collect into a Vec to sort as we are hitting a worst-case behavior in + // `im::Vector` that can lead to a stack overflow! + let mut filtered_items = Vec::new(); + let mut indices = Vec::new(); + let mut filter_text_buf = Vec::new(); + for i in &items { + // If the run id has ever changed, then we'll just bail out of this filtering to avoid + // wasting effort. This would happen, for example, on the user continuing to type. + if run_id.load(std::sync::atomic::Ordering::Acquire) != current_run_id { + return None; + } + + indices.clear(); + filter_text_buf.clear(); + let filter_text = Utf32Str::new(&i.filter_text, &mut filter_text_buf); + if let Some(score) = pattern.indices(filter_text, matcher, &mut indices) + { + let mut item = i.clone(); + item.score = score; + item.indices = indices.iter().map(|i| *i as usize).collect(); + filtered_items.push(item); + } + } + + filtered_items.sort_by(|a, b| { + let order = b.score.cmp(&a.score); + match order { + std::cmp::Ordering::Equal => a.filter_text.cmp(&b.filter_text), + _ => order, + } + }); + + if run_id.load(std::sync::atomic::Ordering::Acquire) != current_run_id { + return None; + } + Some(filtered_items.into()) + } + + fn update_process( + run_id: Arc, + receiver: Receiver<(u64, String, im::Vector, Option)>, + resp_tx: Sender<(u64, String, im::Vector, Option)>, + ) { + fn receive_batch( + receiver: &Receiver<( + u64, + String, + im::Vector, + Option, + )>, + ) -> Result<(u64, String, im::Vector, Option)> { + let (mut run_id, mut input, mut items, mut preselect_index) = + receiver.recv()?; + loop { + match receiver.try_recv() { + Ok(update) => { + run_id = update.0; + input = update.1; + items = update.2; + preselect_index = update.3; + } + Err(TryRecvError::Empty) => break, + Err(TryRecvError::Disconnected) => break, + } + } + Ok((run_id, input, items, preselect_index)) + } + + let mut matcher = + nucleo::Matcher::new(nucleo::Config::DEFAULT.match_paths()); + loop { + if let Ok((current_run_id, input, items, preselect_index)) = + receive_batch(&receiver) + { + if let Some(filtered_items) = Self::filter_items( + run_id.clone(), + current_run_id, + &input, + items, + &mut matcher, + ) { + if let Err(err) = resp_tx.send(( + current_run_id, + input, + filtered_items, + preselect_index, + )) { + tracing::error!("{:?}", err); + } + } + } else { + return; + } + } + } +} + +impl KeyPressFocus for PaletteData { + fn get_mode(&self) -> lapce_core::mode::Mode { + Mode::Insert + } + + fn check_condition( + &self, + condition: crate::keypress::condition::Condition, + ) -> bool { + matches!( + condition, + Condition::ListFocus | Condition::PaletteFocus | Condition::ModalFocus + ) + } + + fn run_command( + &self, + command: &crate::command::LapceCommand, + count: Option, + mods: Modifiers, + ) -> CommandExecuted { + match &command.kind { + CommandKind::Workbench(_) => {} + CommandKind::Scroll(_) => {} + CommandKind::Focus(cmd) => { + self.run_focus_command(cmd); + } + CommandKind::Edit(_) + | CommandKind::Move(_) + | CommandKind::MultiSelection(_) => { + self.input_editor.run_command(command, count, mods); + } + CommandKind::MotionMode(_) => {} + } + CommandExecuted::Yes + } + + fn receive_char(&self, c: &str) { + self.input_editor.receive_char(c); + } +} diff --git a/lapce-app/src/palette/item.rs b/lapce-app/src/palette/item.rs new file mode 100644 index 0000000..8f0cf04 --- /dev/null +++ b/lapce-app/src/palette/item.rs @@ -0,0 +1,87 @@ +use std::path::PathBuf; + +use lapce_core::line_ending::LineEnding; +use lapce_rpc::dap_types::RunDebugConfig; +use lsp_types::{Range, SymbolKind}; + +use crate::{ + command::{LapceCommand, LapceWorkbenchCommand}, + debug::RunDebugMode, + editor::location::EditorLocation, + workspace::{LapceWorkspace, SshHost}, +}; + +#[derive(Clone, Debug, PartialEq)] +pub struct PaletteItem { + pub content: PaletteItemContent, + pub filter_text: String, + pub score: u32, + pub indices: Vec, +} + +#[derive(Clone, Debug, PartialEq)] +pub enum PaletteItemContent { + PaletteHelp { + cmd: LapceWorkbenchCommand, + }, + File { + path: PathBuf, + full_path: PathBuf, + }, + Line { + line: usize, + content: String, + }, + Command { + cmd: LapceCommand, + }, + Workspace { + workspace: LapceWorkspace, + }, + Reference { + path: PathBuf, + location: EditorLocation, + }, + DocumentSymbol { + kind: SymbolKind, + name: String, + range: Range, + container_name: Option, + }, + WorkspaceSymbol { + kind: SymbolKind, + name: String, + container_name: Option, + location: EditorLocation, + }, + SshHost { + host: SshHost, + }, + #[cfg(windows)] + WslHost { + host: crate::workspace::WslHost, + }, + RunAndDebug { + mode: RunDebugMode, + config: RunDebugConfig, + }, + ColorTheme { + name: String, + }, + IconTheme { + name: String, + }, + Language { + name: String, + }, + LineEnding { + kind: LineEnding, + }, + SCMReference { + name: String, + }, + TerminalProfile { + name: String, + profile: lapce_rpc::terminal::TerminalProfile, + }, +} diff --git a/lapce-app/src/palette/kind.rs b/lapce-app/src/palette/kind.rs new file mode 100644 index 0000000..1a1b894 --- /dev/null +++ b/lapce-app/src/palette/kind.rs @@ -0,0 +1,162 @@ +use strum_macros::EnumIter; + +use crate::command::LapceWorkbenchCommand; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, EnumIter)] +pub enum PaletteKind { + PaletteHelp, + File, + Line, + Command, + Workspace, + Reference, + DocumentSymbol, + WorkspaceSymbol, + SshHost, + #[cfg(windows)] + WslHost, + RunAndDebug, + ColorTheme, + IconTheme, + Language, + LineEnding, + SCMReferences, + TerminalProfile, + DiffFiles, + HelpAndFile, +} + +impl PaletteKind { + /// The symbol/prefix that is used to signify the behavior of the palette. + pub fn symbol(&self) -> &'static str { + match &self { + PaletteKind::PaletteHelp => "?", + PaletteKind::Line => "/", + PaletteKind::DocumentSymbol => "@", + PaletteKind::WorkspaceSymbol => "#", + // PaletteKind::GlobalSearch => "?", + PaletteKind::Workspace => ">", + PaletteKind::Command => ":", + PaletteKind::TerminalProfile => "<", + PaletteKind::File + | PaletteKind::Reference + | PaletteKind::SshHost + | PaletteKind::RunAndDebug + | PaletteKind::ColorTheme + | PaletteKind::IconTheme + | PaletteKind::Language + | PaletteKind::LineEnding + | PaletteKind::SCMReferences + | PaletteKind::HelpAndFile + | PaletteKind::DiffFiles => "", + #[cfg(windows)] + PaletteKind::WslHost => "", + } + } + + /// Extract the palette kind from the input string. This is most often a prefix. + pub fn from_input(input: &str) -> PaletteKind { + match input { + _ if input.starts_with('?') => PaletteKind::PaletteHelp, + _ if input.starts_with('/') => PaletteKind::Line, + _ if input.starts_with('@') => PaletteKind::DocumentSymbol, + _ if input.starts_with('#') => PaletteKind::WorkspaceSymbol, + _ if input.starts_with('>') => PaletteKind::Workspace, + _ if input.starts_with(':') => PaletteKind::Command, + _ if input.starts_with('<') => PaletteKind::TerminalProfile, + _ => PaletteKind::File, + } + } + + /// Get the [`LapceWorkbenchCommand`] that opens this palette kind, if one exists. + pub fn command(self) -> Option { + match self { + PaletteKind::PaletteHelp => Some(LapceWorkbenchCommand::PaletteHelp), + PaletteKind::Line => Some(LapceWorkbenchCommand::PaletteLine), + PaletteKind::DocumentSymbol => { + Some(LapceWorkbenchCommand::PaletteSymbol) + } + PaletteKind::WorkspaceSymbol => { + Some(LapceWorkbenchCommand::PaletteWorkspaceSymbol) + } + PaletteKind::Workspace => Some(LapceWorkbenchCommand::PaletteWorkspace), + PaletteKind::Command => Some(LapceWorkbenchCommand::PaletteCommand), + PaletteKind::File => Some(LapceWorkbenchCommand::Palette), + PaletteKind::HelpAndFile => { + Some(LapceWorkbenchCommand::PaletteHelpAndFile) + } + PaletteKind::Reference => None, // InternalCommand::PaletteReferences + PaletteKind::SshHost => Some(LapceWorkbenchCommand::ConnectSshHost), + #[cfg(windows)] + PaletteKind::WslHost => Some(LapceWorkbenchCommand::ConnectWslHost), + PaletteKind::RunAndDebug => { + Some(LapceWorkbenchCommand::PaletteRunAndDebug) + } + PaletteKind::ColorTheme => Some(LapceWorkbenchCommand::ChangeColorTheme), + PaletteKind::IconTheme => Some(LapceWorkbenchCommand::ChangeIconTheme), + PaletteKind::Language => Some(LapceWorkbenchCommand::ChangeFileLanguage), + PaletteKind::LineEnding => { + Some(LapceWorkbenchCommand::ChangeFileLineEnding) + } + PaletteKind::SCMReferences => { + Some(LapceWorkbenchCommand::PaletteSCMReferences) + } + PaletteKind::TerminalProfile => None, // InternalCommand::NewTerminal + PaletteKind::DiffFiles => Some(LapceWorkbenchCommand::DiffFiles), + } + } + + // pub fn has_preview(&self) -> bool { + // matches!( + // self, + // PaletteType::Line + // | PaletteType::DocumentSymbol + // | PaletteType::WorkspaceSymbol + // | PaletteType::GlobalSearch + // | PaletteType::Reference + // ) + // } + + pub fn get_input<'a>(&self, input: &'a str) -> &'a str { + match self { + #[cfg(windows)] + PaletteKind::WslHost => input, + PaletteKind::File + | PaletteKind::Reference + | PaletteKind::SshHost + | PaletteKind::RunAndDebug + | PaletteKind::ColorTheme + | PaletteKind::IconTheme + | PaletteKind::Language + | PaletteKind::LineEnding + | PaletteKind::SCMReferences | PaletteKind::HelpAndFile + | PaletteKind::DiffFiles => input, + PaletteKind::PaletteHelp + | PaletteKind::Command + | PaletteKind::Workspace + | PaletteKind::DocumentSymbol + | PaletteKind::WorkspaceSymbol + | PaletteKind::Line + | PaletteKind::TerminalProfile + // | PaletteType::GlobalSearch + => input.get(1..).unwrap_or(""), + } + } + + /// Get the palette kind that it should be considered as based on the current + /// [`PaletteKind`] and the current input. + pub fn get_palette_kind(&self, input: &str) -> PaletteKind { + if self == &PaletteKind::HelpAndFile && input.is_empty() { + return *self; + } + + if self != &PaletteKind::File + && self != &PaletteKind::HelpAndFile + && self.symbol() == "" + { + return *self; + } + + PaletteKind::from_input(input) + } +} diff --git a/lapce-app/src/panel/call_hierarchy_view.rs b/lapce-app/src/panel/call_hierarchy_view.rs new file mode 100644 index 0000000..0c4862b --- /dev/null +++ b/lapce-app/src/panel/call_hierarchy_view.rs @@ -0,0 +1,251 @@ +use std::{ops::AddAssign, rc::Rc}; + +use floem::{ + IntoView, View, ViewId, + reactive::{RwSignal, SignalGet, SignalUpdate, SignalWith}, + style::CursorStyle, + views::{ + Decorators, VirtualVector, container, empty, label, scroll, stack, svg, + virtual_stack, + }, +}; +use lsp_types::{CallHierarchyItem, Range}; + +use super::position::PanelPosition; +use crate::{ + command::InternalCommand, + config::{color::LapceColor, icon::LapceIcons}, + editor::location::EditorLocation, + window_tab::{CommonData, WindowTabData}, +}; + +#[derive(Clone, Debug)] +pub struct CallHierarchyData { + pub root: RwSignal>>, + pub common: Rc, + pub scroll_to_line: RwSignal>, +} + +#[derive(Debug, Clone)] +pub struct CallHierarchyItemData { + pub view_id: ViewId, + pub item: Rc, + pub from_range: Range, + pub init: bool, + pub open: RwSignal, + pub children: RwSignal>>, +} + +impl CallHierarchyItemData { + pub fn child_count(&self) -> usize { + let mut count = 1; + if self.open.get() { + for child in self.children.get_untracked() { + count += child.with(|x| x.child_count()) + } + } + count + } + + pub fn find_by_id( + root: RwSignal, + view_id: ViewId, + ) -> Option> { + if root.get_untracked().view_id == view_id { + Some(root) + } else { + root.get_untracked() + .children + .get_untracked() + .into_iter() + .find_map(|x| Self::find_by_id(x, view_id)) + } + } +} + +fn get_children( + data: RwSignal, + next: &mut usize, + min: usize, + max: usize, + level: usize, +) -> Vec<(usize, usize, RwSignal)> { + let mut children = Vec::new(); + if *next >= min && *next < max { + children.push((*next, level, data)); + } else if *next >= max { + return children; + } + next.add_assign(1); + if data.get_untracked().open.get() { + for child in data.get().children.get_untracked() { + let child_children = get_children(child, next, min, max, level + 1); + children.extend(child_children); + if *next > max { + break; + } + } + } + children +} + +pub struct VirtualList { + root: Option>, +} + +impl VirtualList { + pub fn new(root: Option>) -> Self { + Self { root } + } +} + +impl VirtualVector<(usize, usize, RwSignal)> for VirtualList { + fn total_len(&self) -> usize { + if let Some(root) = &self.root { + root.with(|x| x.child_count()) + } else { + 0 + } + } + + fn slice( + &mut self, + range: std::ops::Range, + ) -> impl Iterator)> { + if let Some(root) = &self.root { + let min = range.start; + let max = range.end; + let children = get_children(*root, &mut 0, min, max, 0); + children.into_iter() + } else { + Vec::new().into_iter() + } + } +} +pub fn show_hierarchy_panel( + window_tab_data: Rc, + _position: PanelPosition, +) -> impl View { + let call_hierarchy_data = window_tab_data.call_hierarchy_data.clone(); + let config = call_hierarchy_data.common.config; + let ui_line_height = call_hierarchy_data.common.ui_line_height; + let scroll_to_line = call_hierarchy_data.scroll_to_line; + scroll( + virtual_stack( + move || VirtualList::new(call_hierarchy_data.root.get()), + move |(_, _, item)| item.get_untracked().view_id, + move |(_, level, rw_data)| { + let data = rw_data.get_untracked(); + let open = data.open; + let kind = data.item.kind; + stack(( + container( + svg(move || { + let config = config.get(); + let svg_str = match open.get() { + true => LapceIcons::ITEM_OPENED, + false => LapceIcons::ITEM_CLOSED, + }; + config.ui_svg(svg_str) + }) + .style(move |s| { + let config = config.get(); + let size = config.ui.icon_size() as f32; + s.size(size, size) + .color(config.color(LapceColor::LAPCE_ICON_ACTIVE)) + }) + ) + .style(|s| s.padding(4.0).margin_left(6.0).margin_right(2.0)) + .on_click_stop({ + let window_tab_data = window_tab_data.clone(); + move |_x| { + open.update(|x| { + *x = !*x; + }); + if !rw_data.get_untracked().init { + window_tab_data.common.internal_command.send( + InternalCommand::CallHierarchyIncoming { + item_id: rw_data.get_untracked().view_id, + }, + ); + } + } + }), + svg(move || { + let config = config.get(); + config + .symbol_svg(&kind) + .unwrap_or_else(|| config.ui_svg(LapceIcons::FILE)) + }).style(move |s| { + let config = config.get(); + let size = config.ui.icon_size() as f32; + s.min_width(size) + .size(size, size) + .margin_right(5.0) + .color(config.symbol_color(&kind).unwrap_or_else(|| { + config.color(LapceColor::LAPCE_ICON_ACTIVE) + })) + }), + data.item.name.clone().into_view(), + if data.item.detail.is_some() { + label(move || { + data.item.detail.clone().unwrap_or_default().replace('\n', "↵") + }).style(move |s| s.margin_left(6.0) + .color(config.get().color(LapceColor::EDITOR_DIM)) + ).into_any() + } else { + empty().into_any() + }, + )) + .style(move |s| { + s.padding_right(5.0) + .height(ui_line_height.get()) + .padding_left((level * 10) as f32) + .items_center() + .hover(|s| { + s.background( + config + .get() + .color(LapceColor::PANEL_HOVERED_BACKGROUND), + ) + .cursor(CursorStyle::Pointer) + }) + }) + .on_click_stop({ + let window_tab_data = window_tab_data.clone(); + let data = rw_data; + move |_| { + if !rw_data.get_untracked().init { + window_tab_data.common.internal_command.send( + InternalCommand::CallHierarchyIncoming { item_id: rw_data.get_untracked().view_id }, + ); + } + let data = data.get_untracked(); + if let Ok(path) = data.item.uri.to_file_path() { + window_tab_data + .common + .internal_command + .send(InternalCommand::JumpToLocation { location: EditorLocation { + path, + position: Some(crate::editor::location::EditorPosition::Position(data.from_range.start)), + scroll_offset: None, + ignore_unconfirmed: false, + same_editor_tab: false, + } }); + } + } + }) + }, + ).item_size_fixed(move || ui_line_height.get()) + .style(|s| s.flex_col().absolute().min_width_full()), + ) + .style(|s| s.absolute().size_full()) + .scroll_to(move || { + if let Some(line) = scroll_to_line.get() { + let line_height = ui_line_height.get(); + Some((0.0, line * line_height).into()) + } else { + None + } + }) +} diff --git a/lapce-app/src/panel/data.rs b/lapce-app/src/panel/data.rs new file mode 100644 index 0000000..b18f06d --- /dev/null +++ b/lapce-app/src/panel/data.rs @@ -0,0 +1,464 @@ +use std::{rc::Rc, sync::Arc}; + +use floem::{ + kurbo::Size, + reactive::{ + Memo, RwSignal, Scope, SignalGet, SignalUpdate, SignalWith, use_context, + }, +}; +use serde::{Deserialize, Serialize}; + +use super::{ + kind::PanelKind, + position::{PanelContainerPosition, PanelPosition}, + style::PanelStyle, +}; +use crate::{ + db::LapceDb, + window_tab::{CommonData, Focus}, +}; + +pub type PanelOrder = im::HashMap>; + +pub fn default_panel_order() -> PanelOrder { + let mut order = PanelOrder::new(); + order.insert( + PanelPosition::LeftTop, + im::vector![ + PanelKind::FileExplorer, + PanelKind::Plugin, + PanelKind::SourceControl, + PanelKind::Debug, + ], + ); + order.insert( + PanelPosition::BottomLeft, + im::vector![ + PanelKind::Terminal, + PanelKind::Search, + PanelKind::Problem, + PanelKind::CallHierarchy, + PanelKind::References, + PanelKind::Implementation + ], + ); + order.insert( + PanelPosition::RightTop, + im::vector![PanelKind::DocumentSymbol,], + ); + + order +} + +#[derive(Clone, Copy, Serialize, Deserialize, Hash, PartialEq, Eq)] +pub enum PanelSection { + OpenEditor, + FileExplorer, + Error, + Warn, + Changes, + Installed, + Available, + Process, + Variable, + StackFrame, + Breakpoint, +} + +#[derive(Clone, Serialize, Deserialize)] +pub struct PanelSize { + pub left: f64, + pub left_split: f64, + pub bottom: f64, + pub bottom_split: f64, + pub right: f64, + pub right_split: f64, +} + +#[derive(Clone, Serialize, Deserialize)] +pub struct PanelInfo { + pub panels: PanelOrder, + pub styles: im::HashMap, + pub size: PanelSize, + pub sections: im::HashMap, +} + +#[derive(Clone)] +pub struct PanelData { + pub panels: RwSignal, + pub styles: RwSignal>, + pub size: RwSignal, + pub available_size: Memo, + pub sections: RwSignal>>, + pub common: Rc, +} + +impl PanelData { + pub fn new( + cx: Scope, + panels: im::HashMap>, + available_size: Memo, + sections: im::HashMap, + common: Rc, + ) -> Self { + let panels = cx.create_rw_signal(panels); + + let mut styles = im::HashMap::new(); + styles.insert( + PanelPosition::LeftTop, + PanelStyle { + active: 0, + shown: true, + maximized: false, + }, + ); + styles.insert( + PanelPosition::LeftBottom, + PanelStyle { + active: 0, + shown: false, + maximized: false, + }, + ); + styles.insert( + PanelPosition::BottomLeft, + PanelStyle { + active: 0, + shown: true, + maximized: false, + }, + ); + styles.insert( + PanelPosition::BottomRight, + PanelStyle { + active: 0, + shown: false, + maximized: false, + }, + ); + styles.insert( + PanelPosition::RightTop, + PanelStyle { + active: 0, + shown: false, + maximized: false, + }, + ); + styles.insert( + PanelPosition::RightBottom, + PanelStyle { + active: 0, + shown: false, + maximized: false, + }, + ); + let styles = cx.create_rw_signal(styles); + let size = cx.create_rw_signal(PanelSize { + left: 250.0, + left_split: 0.5, + bottom: 300.0, + bottom_split: 0.5, + right: 250.0, + right_split: 0.5, + }); + let sections = cx.create_rw_signal( + sections + .into_iter() + .map(|(key, value)| (key, cx.create_rw_signal(value))) + .collect(), + ); + + Self { + panels, + styles, + size, + available_size, + sections, + common, + } + } + + pub fn panel_info(&self) -> PanelInfo { + PanelInfo { + panels: self.panels.get_untracked(), + styles: self.styles.get_untracked(), + size: self.size.get_untracked(), + sections: self + .sections + .get_untracked() + .into_iter() + .map(|(key, value)| (key, value.get_untracked())) + .collect(), + } + } + + pub fn is_container_shown( + &self, + position: &PanelContainerPosition, + tracked: bool, + ) -> bool { + self.is_position_shown(&position.first(), tracked) + || self.is_position_shown(&position.second(), tracked) + } + + pub fn is_position_empty( + &self, + position: &PanelPosition, + tracked: bool, + ) -> bool { + if tracked { + self.panels + .with(|panels| panels.get(position).map(|p| p.is_empty())) + .unwrap_or(true) + } else { + self.panels + .with_untracked(|panels| panels.get(position).map(|p| p.is_empty())) + .unwrap_or(true) + } + } + + pub fn is_position_shown( + &self, + position: &PanelPosition, + tracked: bool, + ) -> bool { + let styles = if tracked { + self.styles.get() + } else { + self.styles.get_untracked() + }; + styles.get(position).map(|s| s.shown).unwrap_or(false) + } + + pub fn panel_position( + &self, + kind: &PanelKind, + ) -> Option<(usize, PanelPosition)> { + self.panels + .with_untracked(|panels| panel_position(panels, kind)) + } + + pub fn is_panel_visible(&self, kind: &PanelKind) -> bool { + if let Some((index, position)) = self.panel_position(kind) { + if let Some(style) = self + .styles + .with_untracked(|styles| styles.get(&position).cloned()) + { + return style.active == index && style.shown; + } + } + false + } + + pub fn show_panel(&self, kind: &PanelKind) { + if let Some((index, position)) = self.panel_position(kind) { + self.styles.update(|styles| { + if let Some(style) = styles.get_mut(&position) { + style.shown = true; + style.active = index; + } + }); + } + } + + pub fn hide_panel(&self, kind: &PanelKind) { + if let Some((_, position)) = self.panel_position(kind) { + if let Some((active_panel, _)) = + self.active_panel_at_position(&position, false) + { + if &active_panel == kind { + self.set_shown(&position, false); + let peer_position = position.peer(); + if let Some(order) = self + .panels + .with_untracked(|panels| panels.get(&peer_position).cloned()) + { + if order.is_empty() { + self.set_shown(&peer_position, false); + } + } + } + } + } + } + + /// Get the active panel kind at that position, if any. + /// `tracked` decides whether it should track the signal or not. + pub fn active_panel_at_position( + &self, + position: &PanelPosition, + tracked: bool, + ) -> Option<(PanelKind, bool)> { + let style = if tracked { + self.styles.with(|styles| styles.get(position).cloned())? + } else { + self.styles + .with_untracked(|styles| styles.get(position).cloned())? + }; + let order = if tracked { + self.panels.with(|panels| panels.get(position).cloned())? + } else { + self.panels + .with_untracked(|panels| panels.get(position).cloned())? + }; + order + .get(style.active) + .cloned() + .or_else(|| order.last().cloned()) + .map(|p| (p, style.shown)) + } + + pub fn set_shown(&self, position: &PanelPosition, shown: bool) { + self.styles.update(|styles| { + if let Some(style) = styles.get_mut(position) { + style.shown = shown; + } + }); + } + + pub fn toggle_active_maximize(&self) { + let focus = self.common.focus.get_untracked(); + if let Focus::Panel(kind) = focus { + if let Some((_, pos)) = self.panel_position(&kind) { + if pos.is_bottom() { + self.toggle_bottom_maximize(); + } + } + } + } + + pub fn toggle_maximize(&self, kind: &PanelKind) { + if let Some((_, p)) = self.panel_position(kind) { + if p.is_bottom() { + self.toggle_bottom_maximize(); + } + } + } + + pub fn toggle_bottom_maximize(&self) { + let maximized = !self.panel_bottom_maximized(false); + self.styles.update(|styles| { + if let Some(style) = styles.get_mut(&PanelPosition::BottomLeft) { + style.maximized = maximized; + } + if let Some(style) = styles.get_mut(&PanelPosition::BottomRight) { + style.maximized = maximized; + } + }); + } + + pub fn panel_bottom_maximized(&self, tracked: bool) -> bool { + let styles = if tracked { + self.styles.get() + } else { + self.styles.get_untracked() + }; + styles + .get(&PanelPosition::BottomLeft) + .map(|p| p.maximized) + .unwrap_or(false) + || styles + .get(&PanelPosition::BottomRight) + .map(|p| p.maximized) + .unwrap_or(false) + } + + pub fn toggle_container_visual(&self, position: &PanelContainerPosition) { + let is_hidden = !self.is_container_shown(position, false); + if is_hidden { + self.styles.update(|styles| { + let style = styles.entry(position.first()).or_default(); + style.shown = true; + let style = styles.entry(position.second()).or_default(); + style.shown = true; + }); + } else { + if let Some((kind, _)) = + self.active_panel_at_position(&position.second(), false) + { + self.hide_panel(&kind); + } + if let Some((kind, _)) = + self.active_panel_at_position(&position.first(), false) + { + self.hide_panel(&kind); + } + self.styles.update(|styles| { + let style = styles.entry(position.first()).or_default(); + style.shown = false; + let style = styles.entry(position.second()).or_default(); + style.shown = false; + }); + } + } + + pub fn move_panel_to_position(&self, kind: PanelKind, position: &PanelPosition) { + let current_position = self.panel_position(&kind); + if current_position.as_ref().map(|(_, pos)| pos) == Some(position) { + return; + } + + let mut new_index_at_old_position = None; + let index = self + .panels + .try_update(|panels| { + if let Some((index, current_position)) = current_position { + if let Some(panels) = panels.get_mut(¤t_position) { + panels.remove(index); + + let max_index = panels.len().saturating_sub(1); + if index > max_index { + new_index_at_old_position = Some(max_index); + } + } + } + let panels = panels.entry(*position).or_default(); + panels.push_back(kind); + panels.len() - 1 + }) + .unwrap(); + self.styles.update(|styles| { + if let Some((_, current_position)) = current_position { + if let Some(new_index) = new_index_at_old_position { + let style = styles.entry(current_position).or_default(); + style.active = new_index; + } + } + + let style = styles.entry(*position).or_default(); + style.active = index; + style.shown = true; + }); + + let db: Arc = use_context().unwrap(); + db.save_panel_orders(self.panels.get_untracked()); + } + + pub fn section_open(&self, section: PanelSection) -> RwSignal { + let open = self + .sections + .with_untracked(|sections| sections.get(§ion).cloned()); + if let Some(open) = open { + return open; + } + + let open = self.common.scope.create_rw_signal(true); + self.sections.update(|sections| { + sections.insert(section, open); + }); + open + } +} + +pub fn panel_position( + order: &PanelOrder, + kind: &PanelKind, +) -> Option<(usize, PanelPosition)> { + for (pos, panels) in order.iter() { + let index = panels.iter().position(|k| k == kind); + if let Some(index) = index { + return Some((index, *pos)); + } + } + None +} diff --git a/lapce-app/src/panel/debug_view.rs b/lapce-app/src/panel/debug_view.rs new file mode 100644 index 0000000..d23e740 --- /dev/null +++ b/lapce-app/src/panel/debug_view.rs @@ -0,0 +1,758 @@ +use std::{rc::Rc, sync::Arc}; + +use floem::{ + View, + event::EventListener, + peniko::Color, + reactive::{ + ReadSignal, RwSignal, SignalGet, SignalUpdate, SignalWith, create_rw_signal, + }, + style::CursorStyle, + text::Style as FontStyle, + views::{ + Decorators, container, dyn_stack, label, scroll, stack, svg, text, + virtual_stack, + }, +}; +use lapce_rpc::{ + dap_types::{DapId, ThreadId}, + terminal::TermId, +}; + +use super::{data::PanelSection, position::PanelPosition, view::PanelBuilder}; +use crate::{ + app::clickable_icon, + command::InternalCommand, + config::{LapceConfig, color::LapceColor, icon::LapceIcons}, + debug::{DapVariable, RunDebugMode, StackTraceData}, + editor::location::{EditorLocation, EditorPosition}, + listener::Listener, + settings::checkbox, + terminal::panel::TerminalPanelData, + window_tab::WindowTabData, +}; + +pub fn debug_panel( + window_tab_data: Rc, + position: PanelPosition, +) -> impl View { + let config = window_tab_data.common.config; + let terminal = window_tab_data.terminal.clone(); + let internal_command = window_tab_data.common.internal_command; + + PanelBuilder::new(config, position) + .add_height( + "Processes", + 150.0, + debug_processes(terminal.clone(), config), + window_tab_data.panel.section_open(PanelSection::Process), + ) + .add( + "Variables", + variables_view(window_tab_data.clone()), + window_tab_data.panel.section_open(PanelSection::Variable), + ) + .add( + "Stack Frames", + debug_stack_traces(terminal.clone(), internal_command, config), + window_tab_data.panel.section_open(PanelSection::StackFrame), + ) + .add_height( + "Breakpoints", + 150.0, + breakpoints_view(window_tab_data.clone()), + window_tab_data.panel.section_open(PanelSection::Breakpoint), + ) + .build() + .debug_name("Debug Panel") +} + +fn debug_process_icons( + terminal: TerminalPanelData, + term_id: TermId, + dap_id: DapId, + mode: RunDebugMode, + stopped: bool, + config: ReadSignal>, +) -> impl View { + let paused = move || { + let stopped = terminal + .debug + .daps + .with_untracked(|daps| daps.get(&dap_id).map(|dap| dap.stopped)); + stopped.map(|stopped| stopped.get()).unwrap_or(false) + }; + match mode { + RunDebugMode::Run => container(stack(( + { + let terminal = terminal.clone(); + clickable_icon( + || LapceIcons::DEBUG_RESTART, + move || { + terminal.restart_run_debug(term_id); + }, + || false, + || false, + || "Restart", + config, + ) + .style(|s| s.margin_horiz(4.0)) + }, + { + let terminal = terminal.clone(); + clickable_icon( + || LapceIcons::DEBUG_STOP, + move || { + terminal.stop_run_debug(term_id); + }, + || false, + move || stopped, + || "Stop", + config, + ) + .style(|s| s.margin_right(4.0)) + }, + { + let terminal = terminal.clone(); + clickable_icon( + || LapceIcons::CLOSE, + move || { + terminal.close_terminal(&term_id); + }, + || false, + || false, + || "Close", + config, + ) + .style(|s| s.margin_right(4.0)) + }, + ))), + RunDebugMode::Debug => container(stack(( + { + let terminal = terminal.clone(); + clickable_icon( + || LapceIcons::DEBUG_CONTINUE, + move || { + terminal.dap_continue(term_id); + }, + || false, + move || !paused() || stopped, + || "Continue", + config, + ) + .style(|s| s.margin_horiz(6.0)) + }, + { + let terminal = terminal.clone(); + clickable_icon( + || LapceIcons::DEBUG_PAUSE, + move || { + terminal.dap_pause(term_id); + }, + || false, + move || paused() || stopped, + || "Pause", + config, + ) + .style(|s| s.margin_right(4.0)) + }, + { + let terminal = terminal.clone(); + clickable_icon( + || LapceIcons::DEBUG_STEP_OVER, + move || { + terminal.dap_step_over(term_id); + }, + || false, + move || !paused() || stopped, + || "Step Over", + config, + ) + .style(|s| s.margin_right(4.0)) + }, + { + let terminal = terminal.clone(); + clickable_icon( + || LapceIcons::DEBUG_STEP_INTO, + move || { + terminal.dap_step_into(term_id); + }, + || false, + move || !paused() || stopped, + || "Step Into", + config, + ) + .style(|s| s.margin_right(4.0)) + }, + { + let terminal = terminal.clone(); + clickable_icon( + || LapceIcons::DEBUG_STEP_OUT, + move || { + terminal.dap_step_out(term_id); + }, + || false, + move || !paused() || stopped, + || "Step Out", + config, + ) + .style(|s| s.margin_right(4.0)) + }, + { + let terminal = terminal.clone(); + clickable_icon( + || LapceIcons::DEBUG_RESTART, + move || { + terminal.restart_run_debug(term_id); + }, + || false, + || false, + || "Restart", + config, + ) + .style(|s| s.margin_right(4.0)) + }, + { + let terminal = terminal.clone(); + clickable_icon( + || LapceIcons::DEBUG_STOP, + move || { + terminal.stop_run_debug(term_id); + }, + || false, + move || stopped, + || "Stop", + config, + ) + .style(|s| s.margin_right(4.0)) + }, + { + let terminal = terminal.clone(); + clickable_icon( + || LapceIcons::CLOSE, + move || { + terminal.close_terminal(&term_id); + }, + || false, + || false, + || "Close", + config, + ) + .style(|s| s.margin_right(4.0)) + }, + ))), + } +} + +fn debug_processes( + terminal: TerminalPanelData, + config: ReadSignal>, +) -> impl View { + scroll({ + let terminal = terminal.clone(); + let local_terminal = terminal.clone(); + dyn_stack( + move || local_terminal.run_debug_process(true), + |(term_id, p)| (*term_id, p.stopped), + move |(term_id, p)| { + let terminal = terminal.clone(); + let is_active = + move || terminal.debug.active_term.get() == Some(term_id); + let local_terminal = terminal.clone(); + let is_hovered = create_rw_signal(false); + stack(( + { + let svg_str = match (&p.mode, p.stopped) { + (RunDebugMode::Run, false) => LapceIcons::START, + (RunDebugMode::Run, true) => LapceIcons::RUN_ERRORS, + (RunDebugMode::Debug, false) => LapceIcons::DEBUG, + (RunDebugMode::Debug, true) => { + LapceIcons::DEBUG_DISCONNECT + } + }; + svg(move || config.get().ui_svg(svg_str)).style(move |s| { + let config = config.get(); + let size = config.ui.icon_size() as f32; + s.size(size, size) + .margin_vert(5.0) + .margin_horiz(10.0) + .color(config.color(LapceColor::LAPCE_ICON_ACTIVE)) + }) + }, + label(move || p.config.name.clone()).style(|s| { + s.flex_grow(1.0) + .flex_basis(0.0) + .min_width(0.0) + .text_ellipsis() + }), + debug_process_icons( + terminal.clone(), + term_id, + p.config.dap_id, + p.mode, + p.stopped, + config, + ) + .style(move |s| { + s.apply_if(!is_hovered.get() && !is_active(), |s| s.hide()) + }), + )) + .on_click_stop(move |_| { + local_terminal.debug.active_term.set(Some(term_id)); + local_terminal.focus_terminal(term_id); + }) + .on_event_stop(EventListener::PointerEnter, move |_| { + is_hovered.set(true); + }) + .on_event_stop(EventListener::PointerLeave, move |_| { + is_hovered.set(false); + }) + .style(move |s| { + let config = config.get(); + s.padding_vert(6.0) + .width_pct(100.0) + .items_center() + .apply_if(is_active(), |s| { + s.background( + config.color(LapceColor::PANEL_CURRENT_BACKGROUND), + ) + }) + .hover(|s| { + s.cursor(CursorStyle::Pointer).background( + (config.color(LapceColor::PANEL_HOVERED_BACKGROUND)) + .multiply_alpha(0.3), + ) + }) + }) + }, + ) + .style(|s| s.width_pct(100.0).flex_col()) + }) +} + +fn variables_view(window_tab_data: Rc) -> impl View { + let terminal = window_tab_data.terminal.clone(); + let local_terminal = window_tab_data.terminal.clone(); + let ui_line_height = window_tab_data.common.ui_line_height; + let config = window_tab_data.common.config; + container( + scroll( + virtual_stack( + move || { + let dap = terminal.get_active_dap(true); + dap.map(|dap| { + if !dap.stopped.get() { + return DapVariable::default(); + } + let process_stopped = terminal + .get_terminal(&dap.term_id) + .and_then(|t| { + t.run_debug.with(|r| r.as_ref().map(|r| r.stopped)) + }) + .unwrap_or(true); + if process_stopped { + return DapVariable::default(); + } + dap.variables.get() + }) + .unwrap_or_default() + }, + |node| { + ( + node.item.name().to_string(), + node.item.value().map(|v| v.to_string()), + node.item.reference(), + node.expanded, + node.level, + ) + }, + move |node| { + let local_terminal = local_terminal.clone(); + let level = node.level; + let reference = node.item.reference(); + let name = node.item.name(); + let ty = node.item.ty(); + let type_exists = ty.map(|ty| !ty.is_empty()).unwrap_or(false); + stack(( + svg(move || { + let config = config.get(); + let svg_str = match node.expanded { + true => LapceIcons::ITEM_OPENED, + false => LapceIcons::ITEM_CLOSED, + }; + config.ui_svg(svg_str) + }) + .style(move |s| { + let config = config.get(); + let size = config.ui.icon_size() as f32; + + let color = if reference > 0 { + config.color(LapceColor::LAPCE_ICON_ACTIVE) + } else { + Color::TRANSPARENT + }; + s.size(size, size).margin_left(10.0).color(color) + }), + text(name), + text(": ").style(move |s| { + s.apply_if(!type_exists || reference == 0, |s| s.hide()) + }), + text(node.item.ty().unwrap_or("")).style(move |s| { + s.color(config.get().style_color("type").unwrap()) + .apply_if(!type_exists || reference == 0, |s| { + s.hide() + }) + }), + text(format!(" = {}", node.item.value().unwrap_or(""))) + .style(move |s| s.apply_if(reference > 0, |s| s.hide())), + )) + .on_click_stop(move |_| { + if reference > 0 { + let dap = local_terminal.get_active_dap(false); + if let Some(dap) = dap { + let process_stopped = local_terminal + .get_terminal(&dap.term_id) + .and_then(|t| { + t.run_debug + .with(|r| r.as_ref().map(|r| r.stopped)) + }) + .unwrap_or(true); + if !process_stopped { + dap.toggle_expand( + node.parent.clone(), + reference, + ); + } + } + } + }) + .style(move |s| { + s.items_center() + .padding_right(10.0) + .padding_left((level * 10) as f32) + .min_width_pct(100.0) + .hover(|s| { + s.apply_if(reference > 0, |s| { + s.background( + config.get().color( + LapceColor::PANEL_HOVERED_BACKGROUND, + ), + ) + }) + }) + }) + }, + ) + .item_size_fixed(move || ui_line_height.get()) + .style(|s| s.flex_col().min_width_full()), + ) + .style(|s| s.absolute().size_full()), + ) + .style(|s| s.width_full().line_height(1.6).flex_grow(1.0).flex_basis(0)) +} + +fn debug_stack_frames( + dap_id: DapId, + thread_id: ThreadId, + stack_trace: StackTraceData, + stopped: RwSignal, + internal_command: Listener, + config: ReadSignal>, +) -> impl View { + let expanded = stack_trace.expanded; + stack(( + container(label(move || thread_id.to_string())) + .on_click_stop(move |_| { + expanded.update(|expanded| { + *expanded = !*expanded; + }); + }) + .style(move |s| { + s.padding_horiz(10.0).min_width_pct(100.0).hover(move |s| { + s.cursor(CursorStyle::Pointer).background( + config.get().color(LapceColor::PANEL_HOVERED_BACKGROUND), + ) + }) + }), + dyn_stack( + move || { + let expanded = stack_trace.expanded.get() && stopped.get(); + if expanded { + stack_trace.frames.get() + } else { + im::Vector::new() + } + }, + |frame| frame.id, + move |frame| { + let full_path = frame.source.as_ref().and_then(|s| s.path.clone()); + let line = frame.line.saturating_sub(1); + let col = frame.column.saturating_sub(1); + + let source_path = frame + .source + .as_ref() + .and_then(|s| s.path.as_ref()) + .and_then(|p| p.file_name()) + .and_then(|s| s.to_str()) + .unwrap_or("") + .to_string(); + let has_source = !source_path.is_empty(); + let source_path = format!("{source_path}:{}", frame.line); + + container(stack(( + label(move || frame.name.clone()).style(move |s| { + s.hover(|s| { + s.background( + config + .get() + .color(LapceColor::PANEL_HOVERED_BACKGROUND), + ) + }) + }), + label(move || source_path.clone()).style(move |s| { + s.margin_left(10.0) + .color(config.get().color(LapceColor::EDITOR_DIM)) + .font_style(FontStyle::Italic) + .apply_if(!has_source, |s| s.hide()) + }), + ))) + .on_click_stop(move |_| { + if let Some(path) = full_path.clone() { + internal_command.send(InternalCommand::JumpToLocation { + location: EditorLocation { + path, + position: Some(EditorPosition::Position( + lsp_types::Position { + line: line as u32, + character: col as u32, + }, + )), + scroll_offset: None, + ignore_unconfirmed: false, + same_editor_tab: false, + }, + }); + } + internal_command.send(InternalCommand::DapFrameScopes { + dap_id, + frame_id: frame.id, + }); + }) + .style(move |s| { + let config = config.get(); + s.padding_left(20.0) + .padding_right(10.0) + .min_width_pct(100.0) + .apply_if(!has_source, |s| { + s.color(config.color(LapceColor::EDITOR_DIM)) + }) + .hover(|s| { + s.background( + config.color(LapceColor::PANEL_HOVERED_BACKGROUND), + ) + .apply_if(has_source, |s| s.cursor(CursorStyle::Pointer)) + }) + }) + }, + ) + .style(|s| s.flex_col().min_width_pct(100.0)), + )) + .style(|s| s.flex_col().min_width_pct(100.0)) +} + +fn debug_stack_traces( + terminal: TerminalPanelData, + internal_command: Listener, + config: ReadSignal>, +) -> impl View { + container( + scroll({ + let local_terminal = terminal.clone(); + dyn_stack( + move || { + let dap = local_terminal.get_active_dap(true); + if let Some(dap) = dap { + let process_stopped = local_terminal + .get_terminal(&dap.term_id) + .and_then(|t| { + t.run_debug.with(|r| r.as_ref().map(|r| r.stopped)) + }) + .unwrap_or(true); + if process_stopped { + return Vec::new(); + } + let main_thread = dap.thread_id.get(); + let stack_traces = dap.stack_traces.get(); + let mut traces = stack_traces + .into_iter() + .map(|(thread_id, stack_trace)| { + (dap.dap_id, dap.stopped, thread_id, stack_trace) + }) + .collect::>(); + traces.sort_by_key(|(_, _, id, _)| main_thread != Some(*id)); + traces + } else { + Vec::new() + } + }, + |(dap_id, stopped, thread_id, _)| { + (*dap_id, *thread_id, stopped.get_untracked()) + }, + move |(dap_id, stopped, thread_id, stack_trace)| { + debug_stack_frames( + dap_id, + thread_id, + stack_trace, + stopped, + internal_command, + config, + ) + }, + ) + .style(|s| s.flex_col().min_width_pct(100.0)) + }) + .style(|s| s.absolute().size_pct(100.0, 100.0)), + ) + .style(|s| { + s.width_pct(100.0) + .line_height(1.6) + .flex_grow(1.0) + .flex_basis(0.0) + }) +} + +fn breakpoints_view(window_tab_data: Rc) -> impl View { + let breakpoints = window_tab_data.terminal.debug.breakpoints; + let config = window_tab_data.common.config; + let workspace = window_tab_data.common.workspace.clone(); + let available_width = create_rw_signal(0.0); + let internal_command = window_tab_data.common.internal_command; + container( + scroll( + dyn_stack( + move || { + breakpoints + .get() + .into_iter() + .flat_map(|(path, breakpoints)| { + breakpoints.into_values().map(move |b| (path.clone(), b)) + }) + }, + move |(path, breakpoint)| { + (path.clone(), breakpoint.line, breakpoint.active) + }, + move |(path, breakpoint)| { + let line = breakpoint.line; + let full_path = path.clone(); + let full_path_for_jump = path.clone(); + let full_path_for_close = path.clone(); + let path = if let Some(workspace_path) = workspace.path.as_ref() + { + path.strip_prefix(workspace_path) + .unwrap_or(&full_path) + .to_path_buf() + } else { + path + }; + + let file_name = + path.file_name().and_then(|s| s.to_str()).unwrap_or(""); + let folder = + path.parent().and_then(|s| s.to_str()).unwrap_or(""); + let folder_empty = folder.is_empty(); + + stack(( + clickable_icon( + move || LapceIcons::CLOSE, + move || { + breakpoints.update(|breakpoints| { + if let Some(breakpoints) = + breakpoints.get_mut(&full_path_for_close) + { + breakpoints.remove(&line); + } + }); + }, + || false, + || false, + || "Remove", + config, + ) + .on_event_stop(EventListener::PointerDown, |_| {}), + checkbox(move || breakpoint.active, config) + .style(|s| { + s.margin_right(6.0).cursor(CursorStyle::Pointer) + }) + .on_click_stop(move |_| { + breakpoints.update(|breakpoints| { + if let Some(breakpoints) = + breakpoints.get_mut(&full_path) + { + if let Some(breakpoint) = + breakpoints.get_mut(&line) + { + breakpoint.active = !breakpoint.active; + } + } + }); + }), + text(format!("{file_name}:{}", breakpoint.line + 1)).style( + move |s| { + let size = config.get().ui.icon_size() as f32; + s.text_ellipsis().max_width( + available_width.get() as f32 + - 20.0 + - size + - 6.0 + - size + - 8.0, + ) + }, + ), + text(folder).style(move |s| { + s.text_ellipsis() + .flex_grow(1.0) + .flex_basis(0.0) + .color(config.get().color(LapceColor::EDITOR_DIM)) + .min_width(0.0) + .margin_left(6.0) + .apply_if(folder_empty, |s| s.hide()) + }), + )) + .style(move |s| { + s.items_center().padding_horiz(10.0).width_pct(100.0).hover( + |s| { + s.background( + config + .get() + .color(LapceColor::PANEL_HOVERED_BACKGROUND), + ) + }, + ) + }) + .on_click_stop(move |_| { + internal_command.send(InternalCommand::JumpToLocation { + location: EditorLocation { + path: full_path_for_jump.clone(), + position: Some(EditorPosition::Line(line)), + scroll_offset: None, + ignore_unconfirmed: false, + same_editor_tab: false, + }, + }); + }) + }, + ) + .style(|s| s.flex_col().line_height(1.6).width_pct(100.0)), + ) + .on_resize(move |rect| { + let width = rect.width(); + if available_width.get_untracked() != width { + available_width.set(width); + } + }) + .style(|s| s.absolute().size_pct(100.0, 100.0)), + ) + .style(|s| s.size_pct(100.0, 100.0)) +} diff --git a/lapce-app/src/panel/document_symbol.rs b/lapce-app/src/panel/document_symbol.rs new file mode 100644 index 0000000..62c8c9d --- /dev/null +++ b/lapce-app/src/panel/document_symbol.rs @@ -0,0 +1,321 @@ +use std::{ops::AddAssign, path::PathBuf, rc::Rc}; + +use floem::{ + View, + peniko::Color, + reactive::{RwSignal, Scope, SignalGet, SignalUpdate, SignalWith}, + style::CursorStyle, + views::{ + Decorators, VirtualVector, container, editor::id::Id, label, scroll, stack, + svg, virtual_stack, + }, +}; +use lsp_types::{DocumentSymbol, SymbolKind}; + +use super::position::PanelPosition; +use crate::{ + command::InternalCommand, + config::{color::LapceColor, icon::LapceIcons}, + editor::location::EditorLocation, + window_tab::WindowTabData, +}; + +#[derive(Clone, Debug)] +pub struct SymbolData { + pub path: PathBuf, + pub file: RwSignal, +} + +impl SymbolData { + pub fn new( + items: Vec>, + path: PathBuf, + cx: Scope, + ) -> Self { + let name = path + .file_name() + .and_then(|x| x.to_str()) + .map(|x| x.to_string()) + .unwrap_or_default(); + #[allow(deprecated)] + let file_ds = DocumentSymbol { + name: name.clone(), + detail: None, + kind: SymbolKind::FILE, + tags: None, + deprecated: None, + range: Default::default(), + selection_range: Default::default(), + children: None, + }; + let file = cx.create_rw_signal(SymbolInformationItemData { + id: Id::next(), + name, + detail: None, + item: file_ds, + open: cx.create_rw_signal(true), + children: items, + }); + Self { path, file } + } + fn get_children( + &self, + min: usize, + max: usize, + ) -> Vec<( + usize, + usize, + Rc, + RwSignal, + )> { + let path = Rc::new(self.path.clone()); + let level: usize = 0; + let mut next = 0; + get_children(self.file, &mut next, min, max, level, path.clone()) + } +} + +#[derive(Debug, Clone)] +pub struct SymbolInformationItemData { + pub id: Id, + pub name: String, + pub detail: Option, + pub item: DocumentSymbol, + pub open: RwSignal, + pub children: Vec>, +} + +impl From<(DocumentSymbol, Scope)> for SymbolInformationItemData { + fn from((mut item, cx): (DocumentSymbol, Scope)) -> Self { + let children = if let Some(children) = item.children.take() { + children + .into_iter() + .map(|x| cx.create_rw_signal(Self::from((x, cx)))) + .collect() + } else { + Vec::with_capacity(0) + }; + Self { + id: Id::next(), + name: item.name.clone(), + detail: item.detail.clone(), + item, + open: cx.create_rw_signal(true), + children, + } + } +} + +impl SymbolInformationItemData { + pub fn child_count(&self) -> usize { + let mut count = 1; + if self.open.get() { + for child in &self.children { + count += child.with(|x| x.child_count()) + } + } + count + } +} + +fn get_children( + data: RwSignal, + next: &mut usize, + min: usize, + max: usize, + level: usize, + path: Rc, +) -> Vec<( + usize, + usize, + Rc, + RwSignal, +)> { + let mut children = Vec::new(); + if *next >= min && *next < max { + children.push((*next, level, path.clone(), data)); + } else if *next >= max { + return children; + } + next.add_assign(1); + if data.get_untracked().open.get() { + for child in data.get().children { + let child_children = + get_children(child, next, min, max, level + 1, path.clone()); + children.extend(child_children); + if *next > max { + break; + } + } + } + children +} + +pub struct VirtualList { + root: Option>>, +} + +impl VirtualList { + pub fn new(root: Option>>) -> Self { + Self { root } + } +} + +impl + VirtualVector<( + usize, + usize, + Rc, + RwSignal, + )> for VirtualList +{ + fn total_len(&self) -> usize { + if let Some(root) = self.root.as_ref().and_then(|x| x.get()) { + root.file.get_untracked().child_count() + } else { + 0 + } + } + + fn slice( + &mut self, + range: std::ops::Range, + ) -> impl Iterator< + Item = ( + usize, + usize, + Rc, + RwSignal, + ), + > { + if let Some(root) = self.root.as_ref().and_then(|x| x.get()) { + let min = range.start; + let max = range.end; + let children = root.get_children(min, max); + children.into_iter() + } else { + Vec::new().into_iter() + } + } +} + +pub fn symbol_panel( + window_tab_data: Rc, + _position: PanelPosition, +) -> impl View { + let config = window_tab_data.common.config; + let ui_line_height = window_tab_data.common.ui_line_height; + scroll( + virtual_stack( + { + let window_tab_data = window_tab_data.clone(); + move || { + let editor = window_tab_data.main_split.get_active_editor(); + VirtualList::new(editor.map(|x| x.doc().document_symbol_data)) + } + }, + move |(_, _, _, item)| item.get_untracked().id, + move |(_, level, path, rw_data)| { + let data = rw_data.get_untracked(); + let open = data.open; + let has_child = !data.children.is_empty(); + let kind = data.item.kind; + stack(( + container( + svg(move || { + let config = config.get(); + let svg_str = match open.get() { + true => LapceIcons::ITEM_OPENED, + false => LapceIcons::ITEM_CLOSED, + }; + config.ui_svg(svg_str) + }) + .style(move |s| { + let config = config.get(); + let color = if has_child { + config.color(LapceColor::LAPCE_ICON_ACTIVE) + } else { + Color::TRANSPARENT + }; + let size = config.ui.icon_size() as f32; + s.size(size, size) + .color(color) + }) + ).style(|s| s.padding(4.0).margin_left(6.0).margin_right(2.0)) + .on_click_stop({ + move |_x| { + if has_child { + open.update(|x| { + *x = !*x; + }); + } + } + }), + svg(move || { + let config = config.get(); + config + .symbol_svg(&kind) + .unwrap_or_else(|| config.ui_svg(LapceIcons::FILE)) + }).style(move |s| { + let config = config.get(); + let size = config.ui.icon_size() as f32; + s.min_width(size) + .size(size, size) + .margin_right(5.0) + .color(config.symbol_color(&kind).unwrap_or_else(|| { + config.color(LapceColor::LAPCE_ICON_ACTIVE) + })) + }), + label(move || { + data.name.replace('\n', "↵") + }) + .style(move |s| { + s.selectable(false) + }), + label(move || { + data.detail.clone().unwrap_or_default() + }).style(move |s| s.margin_left(6.0) + .color(config.get().color(LapceColor::EDITOR_DIM)) + .selectable(false) + .apply_if( + data.item.detail.clone().is_none(), + |s| s.hide()) + ), + )) + .style(move |s| { + s.padding_right(5.0) + .padding_left((level * 10) as f32) + .items_center() + .height(ui_line_height.get()) + .hover(|s| { + s.background( + config + .get() + .color(LapceColor::PANEL_HOVERED_BACKGROUND), + ) + .cursor(CursorStyle::Pointer) + }) + }) + .on_click_stop({ + let window_tab_data = window_tab_data.clone(); + let data = rw_data; + move |_| { + let data = data.get_untracked(); + window_tab_data + .common + .internal_command + .send(InternalCommand::JumpToLocation { location: EditorLocation { + path: path.to_path_buf(), + position: Some(crate::editor::location::EditorPosition::Position(data.item.selection_range.start)), + scroll_offset: None, + ignore_unconfirmed: false, + same_editor_tab: false, + } }); + } + }) + }, + ).item_size_fixed(move || ui_line_height.get()) + .style(|s| s.flex_col().absolute().min_width_full()), + ) + .style(|s| s.absolute().size_full()) +} diff --git a/lapce-app/src/panel/global_search_view.rs b/lapce-app/src/panel/global_search_view.rs new file mode 100644 index 0000000..0cc1edd --- /dev/null +++ b/lapce-app/src/panel/global_search_view.rs @@ -0,0 +1,309 @@ +use std::{path::PathBuf, rc::Rc, sync::Arc}; + +use floem::{ + View, + event::EventListener, + reactive::{ReadSignal, SignalGet, SignalUpdate}, + style::{CursorStyle, Style}, + views::{Decorators, container, label, scroll, stack, svg, virtual_stack}, +}; +use lapce_xi_rope::find::CaseMatching; + +use super::{kind::PanelKind, position::PanelPosition}; +use crate::{ + app::clickable_icon, + command::InternalCommand, + config::{LapceConfig, color::LapceColor, icon::LapceIcons}, + editor::location::{EditorLocation, EditorPosition}, + focus_text::focus_text, + global_search::{GlobalSearchData, SearchMatchData}, + listener::Listener, + text_input::TextInputBuilder, + window_tab::{Focus, WindowTabData}, + workspace::LapceWorkspace, +}; + +pub fn global_search_panel( + window_tab_data: Rc, + _position: PanelPosition, +) -> impl View { + let global_search = window_tab_data.global_search.clone(); + let editor = global_search.editor.clone(); + let config = global_search.common.config; + let workspace = global_search.common.workspace.clone(); + let internal_command = global_search.common.internal_command; + let case_matching = global_search.common.find.case_matching; + let whole_word = global_search.common.find.whole_words; + let is_regex = global_search.common.find.is_regex; + + let focus = global_search.common.focus; + let is_focused = move || focus.get() == Focus::Panel(PanelKind::Search); + + stack(( + container( + stack(( + TextInputBuilder::new() + .is_focused(is_focused) + .build_editor(editor.clone()) + .style(|s| s.width_pct(100.0)), + clickable_icon( + || LapceIcons::SEARCH_CASE_SENSITIVE, + move || { + let new = match case_matching.get_untracked() { + CaseMatching::Exact => CaseMatching::CaseInsensitive, + CaseMatching::CaseInsensitive => CaseMatching::Exact, + }; + case_matching.set(new); + }, + move || case_matching.get() == CaseMatching::Exact, + || false, + || "Case Sensitive", + config, + ) + .style(|s| s.padding_vert(4.0)), + clickable_icon( + || LapceIcons::SEARCH_WHOLE_WORD, + move || { + whole_word.update(|whole_word| { + *whole_word = !*whole_word; + }); + }, + move || whole_word.get(), + || false, + || "Whole Word", + config, + ) + .style(|s| s.padding_left(6.0)), + clickable_icon( + || LapceIcons::SEARCH_REGEX, + move || { + is_regex.update(|is_regex| { + *is_regex = !*is_regex; + }); + }, + move || is_regex.get(), + || false, + || "Use Regex", + config, + ) + .style(|s| s.padding_left(6.0)), + )) + .on_event_cont(EventListener::PointerDown, move |_| { + focus.set(Focus::Panel(PanelKind::Search)); + }) + .style(move |s| { + s.width_pct(100.0) + .padding_right(6.0) + .items_center() + .border(1.0) + .border_radius(6.0) + .border_color(config.get().color(LapceColor::LAPCE_BORDER)) + }), + ) + .style(|s| s.width_pct(100.0).padding(10.0)), + search_result(workspace, global_search, internal_command, config), + )) + .style(|s| s.absolute().size_pct(100.0, 100.0).flex_col()) + .debug_name("Global Search Panel") +} + +fn search_result( + workspace: Arc, + global_search_data: GlobalSearchData, + internal_command: Listener, + config: ReadSignal>, +) -> impl View { + let ui_line_height = global_search_data.common.ui_line_height; + container({ + scroll({ + virtual_stack( + move || global_search_data.clone(), + move |(path, _)| path.to_owned(), + move |(path, match_data)| { + let full_path = path.clone(); + let path = if let Some(workspace_path) = workspace.path.as_ref() + { + path.strip_prefix(workspace_path) + .unwrap_or(&full_path) + .to_path_buf() + } else { + path + }; + let style_path = path.clone(); + + let file_name = path + .file_name() + .and_then(|s| s.to_str()) + .unwrap_or("") + .to_string(); + + let folder = path + .parent() + .and_then(|s| s.to_str()) + .unwrap_or("") + .to_string(); + + let expanded = match_data.expanded; + + stack(( + stack(( + svg(move || { + config.get().ui_svg(if expanded.get() { + LapceIcons::ITEM_OPENED + } else { + LapceIcons::ITEM_CLOSED + }) + }) + .style(move |s| { + let config = config.get(); + let size = config.ui.icon_size() as f32; + s.margin_left(10.0) + .margin_right(6.0) + .size(size, size) + .min_size(size, size) + .color( + config.color(LapceColor::LAPCE_ICON_ACTIVE), + ) + }), + svg(move || config.get().file_svg(&path).0).style( + move |s| { + let config = config.get(); + let size = config.ui.icon_size() as f32; + let color = config.file_svg(&style_path).1; + s.margin_right(6.0) + .size(size, size) + .min_size(size, size) + .apply_opt(color, Style::color) + }, + ), + stack(( + label(move || file_name.clone()).style(|s| { + s.margin_right(6.0) + .max_width_pct(100.0) + .text_ellipsis() + }), + label(move || folder.clone()).style(move |s| { + s.color( + config.get().color(LapceColor::EDITOR_DIM), + ) + .min_width(0.0) + .text_ellipsis() + }), + )) + .style(move |s| s.min_width(0.0).items_center()), + )) + .on_click_stop(move |_| { + expanded.update(|expanded| *expanded = !*expanded); + }) + .style(move |s| { + s.width_pct(100.0) + .min_width_pct(100.0) + .items_center() + .hover(|s| { + s.cursor(CursorStyle::Pointer).background( + config.get().color( + LapceColor::PANEL_HOVERED_BACKGROUND, + ), + ) + }) + }), + virtual_stack( + move || { + if expanded.get() { + match_data.matches.get() + } else { + im::Vector::new() + } + }, + |m| (m.line, m.start, m.end), + move |m| { + let path = full_path.clone(); + let line_number = m.line; + let start = m.start; + let end = m.end; + let line_content = m.line_content.clone(); + + focus_text( + move || { + let config = config.get(); + let content = if config + .ui + .trim_search_results_whitespace + { + m.line_content.trim() + } else { + &m.line_content + }; + format!("{}: {content}", m.line,) + }, + move || { + let config = config.get(); + let mut offset = if config + .ui + .trim_search_results_whitespace + { + line_content.trim_start().len() as i32 + - line_content.len() as i32 + } else { + 0 + }; + offset += + line_number.to_string().len() as i32 + 2; + + ((start as i32 + offset) as usize + ..(end as i32 + offset) as usize) + .collect() + }, + move || { + config.get().color(LapceColor::EDITOR_FOCUS) + }, + ) + .style(move |s| { + let config = config.get(); + let icon_size = config.ui.icon_size() as f32; + s.margin_left(10.0 + icon_size + 6.0).hover( + |s| { + s.cursor(CursorStyle::Pointer) + .background(config.color( + LapceColor::PANEL_HOVERED_BACKGROUND, + )) + }, + ) + }) + .on_click_stop( + move |_| { + internal_command.send( + InternalCommand::JumpToLocation { + location: EditorLocation { + path: path.clone(), + position: Some( + EditorPosition::Line( + line_number + .saturating_sub(1), + ), + ), + scroll_offset: None, + ignore_unconfirmed: false, + same_editor_tab: false, + }, + }, + ); + }, + ) + }, + ) + .item_size_fixed(move || ui_line_height.get()) + .style(|s| s.flex_col()), + )) + .style(|s| s.flex_col()) + }, + ) + .item_size_fn(|(_, match_data): &(PathBuf, SearchMatchData)| { + match_data.height() + }) + .style(|s| s.flex_col().min_width_pct(100.0).line_height(1.8)) + }) + .style(|s| s.absolute().size_pct(100.0, 100.0)) + }) + .style(|s| s.size_pct(100.0, 100.0)) +} diff --git a/lapce-app/src/panel/implementation_view.rs b/lapce-app/src/panel/implementation_view.rs new file mode 100644 index 0000000..7666397 --- /dev/null +++ b/lapce-app/src/panel/implementation_view.rs @@ -0,0 +1,384 @@ +use std::{ops::AddAssign, path::PathBuf, rc::Rc}; + +use floem::{ + IntoView, View, ViewId, + reactive::{RwSignal, Scope, SignalGet, SignalUpdate}, + style::CursorStyle, + views::{ + Decorators, VirtualVector, container, label, scroll, stack, svg, + virtual_stack, + }, +}; +use im::HashMap; +use itertools::Itertools; +use lapce_rpc::file_line::FileLine; +use lsp_types::{Location, SymbolKind, request::GotoImplementationResponse}; + +use super::position::PanelPosition; +use crate::{ + command::InternalCommand, + config::{color::LapceColor, icon::LapceIcons}, + editor::location::EditorLocation, + window_tab::WindowTabData, +}; + +pub fn implementation_panel( + window_tab_data: Rc, + _position: PanelPosition, +) -> impl View { + common_reference_panel(window_tab_data.clone(), _position, move || { + window_tab_data.main_split.implementations.get() + }) + .debug_name("implementation panel") +} +pub fn common_reference_panel( + window_tab_data: Rc, + _position: PanelPosition, + each_fn: impl Fn() -> ReferencesRoot + 'static, +) -> impl View { + let config = window_tab_data.common.config; + let ui_line_height = window_tab_data.common.ui_line_height; + scroll( + virtual_stack( + each_fn, + move |(_, _, data)| data.view_id(), + move |(_, level, rw_data)| { + match rw_data { + ReferenceLocation::File { path, open, .. } => stack(( + container( + svg(move || { + let config = config.get(); + let svg_str = match open.get() { + true => LapceIcons::ITEM_OPENED, + false => LapceIcons::ITEM_CLOSED, + }; + config.ui_svg(svg_str) + }) + .style(move |s| { + let config = config.get(); + let size = config.ui.icon_size() as f32; + s.size(size, size).color( + config.color(LapceColor::LAPCE_ICON_ACTIVE), + ) + }), + ) + .style(|s| s.padding(4.0).margin_left(6.0).margin_right(2.0)) + .on_click_stop({ + move |_x| { + open.update(|x| { + *x = !*x; + }); + } + }), + svg(move || { + let config = config.get(); + config + .symbol_svg(&SymbolKind::FILE) + .unwrap_or_else(|| config.ui_svg(LapceIcons::FILE)) + }) + .style(move |s| { + let config = config.get(); + let size = config.ui.icon_size() as f32; + s.min_width(size) + .size(size, size) + .margin_right(5.0) + .color( + config + .symbol_color(&SymbolKind::FILE) + .unwrap_or_else(|| { + config + .color(LapceColor::LAPCE_ICON_ACTIVE) + }), + ) + }), + label(move || format!("{:?}", path)) + .style(move |s| { + s.margin_left(6.0).color( + config.get().color(LapceColor::EDITOR_DIM), + ) + }) + .into_any(), + )) + .style(move |s| { + s.padding_right(5.0) + .height(ui_line_height.get()) + .padding_left((level * 10) as f32) + .items_center() + .hover(|s| { + s.background( + config + .get() + .color(LapceColor::PANEL_HOVERED_BACKGROUND), + ) + .cursor(CursorStyle::Pointer) + }) + }), + ReferenceLocation::Line { file_line, .. } => stack((container( + label(move || format!("{} {}", file_line.position.line + 1, file_line.content)) + .style(move |s| { + s.margin_left(6.0).color( + config.get().color(LapceColor::EDITOR_DIM), + ) + }) + .into_any(), + ) + .style(move |s| { + s.padding_right(5.0) + .height(ui_line_height.get()) + .padding_left((level * 20) as f32) + .items_center() + .hover(|s| { + s.background( + config + .get() + .color(LapceColor::PANEL_HOVERED_BACKGROUND), + ) + .cursor(CursorStyle::Pointer) + }) + }),)) + .on_click_stop({ + let window_tab_data = window_tab_data.clone(); + let position = file_line.position; + move |_| { + window_tab_data.common.internal_command.send( + InternalCommand::JumpToLocation { + location: EditorLocation { + path: file_line.path.clone(), + position: Some( + crate::editor::location::EditorPosition::Position( + position, + ), + ), + scroll_offset: None, + ignore_unconfirmed: false, + same_editor_tab: false, + }, + }, + ); + } + }), + } + .style(move |s| { + s.padding_right(5.0) + .height(ui_line_height.get()) + .padding_left((level * 10) as f32) + .items_center() + .hover(|s| { + s.background( + config + .get() + .color(LapceColor::PANEL_HOVERED_BACKGROUND), + ) + .cursor(CursorStyle::Pointer) + }) + }) + }, + ).item_size_fixed(move || ui_line_height.get()) + .style(|s| s.flex_col().absolute().min_width_full()), + ) + .style(|s| s.absolute().size_full()) +} + +pub fn map_to_location(resp: Option) -> Vec { + let Some(resp) = resp else { + return Vec::new(); + }; + match resp { + GotoImplementationResponse::Scalar(local) => { + vec![local] + } + GotoImplementationResponse::Array(items) => items, + GotoImplementationResponse::Link(items) => items + .into_iter() + .map(|x| Location { + uri: x.target_uri, + range: x.target_range, + }) + .collect(), + } +} + +pub fn init_implementation_root( + items: Vec, + scope: Scope, +) -> ReferencesRoot { + let mut refs_map: HashMap> = HashMap::new(); + for item in items { + let entry = refs_map.entry(item.path.clone()).or_default(); + (*entry).insert( + item.position.line, + Reference::Line { + location: ReferenceLocation::Line { + file_line: item, + view_id: ViewId::new(), + }, + }, + ); + } + + let mut refs = Vec::new(); + for (path, items) in refs_map { + let open = scope.create_rw_signal(true); + let children = items + .into_iter() + .sorted_by(|x, y| x.0.cmp(&y.0)) + .map(|x| x.1) + .collect(); + let ref_item = Reference::File { + location: ReferenceLocation::File { + open, + path, + view_id: ViewId::new(), + }, + children, + open, + }; + refs.push(ref_item); + } + ReferencesRoot { children: refs } +} + +#[derive(Clone, Default)] +pub struct ReferencesRoot { + pub(crate) children: Vec, +} + +impl ReferencesRoot { + pub fn total(&self) -> usize { + let mut total = 0; + for child in &self.children { + total += child.total_len() + } + total + } + + fn get_children( + &self, + next: &mut usize, + min: usize, + max: usize, + level: usize, + ) -> Vec<(usize, usize, ReferenceLocation)> { + let mut children = Vec::new(); + for child in &self.children { + let child_children = child.get_children(next, min, max, level + 1); + if !child_children.is_empty() { + children.extend(child_children); + } + if *next > max { + break; + } + } + children + } +} + +impl VirtualVector<(usize, usize, ReferenceLocation)> for ReferencesRoot { + fn total_len(&self) -> usize { + self.total() + } + + fn slice( + &mut self, + range: std::ops::Range, + ) -> impl Iterator { + let min = range.start; + let max = range.end; + let children = self.get_children(&mut 0, min, max, 0); + children.into_iter() + } +} + +#[derive(Clone)] +pub enum Reference { + File { + location: ReferenceLocation, + open: RwSignal, + children: Vec, + }, + Line { + location: ReferenceLocation, + }, +} + +#[derive(Clone)] +pub enum ReferenceLocation { + File { + path: PathBuf, + open: RwSignal, + view_id: ViewId, + }, + Line { + file_line: FileLine, + view_id: ViewId, + }, +} + +impl ReferenceLocation { + pub fn view_id(&self) -> ViewId { + match self { + ReferenceLocation::File { view_id, .. } => *view_id, + ReferenceLocation::Line { view_id, .. } => *view_id, + } + } +} + +impl Reference { + pub fn location(&self) -> ReferenceLocation { + match self { + Reference::File { location, .. } => location.clone(), + Reference::Line { location } => location.clone(), + } + } + pub fn total_len(&self) -> usize { + match self { + Reference::File { children, .. } => { + let mut total = 1; + for child in children { + total += child.total_len() + } + total + } + Reference::Line { .. } => 1, + } + } + pub fn children(&self) -> Option<&Vec> { + match self { + Reference::File { children, open, .. } => { + if open.get() { + return Some(children); + } + None + } + Reference::Line { .. } => None, + } + } + + fn get_children( + &self, + next: &mut usize, + min: usize, + max: usize, + level: usize, + ) -> Vec<(usize, usize, ReferenceLocation)> { + let mut children = Vec::new(); + if *next >= min && *next < max { + children.push((*next, level, self.location())); + } else if *next >= max { + return children; + } + next.add_assign(1); + if let Some(children_tmp) = self.children() { + for child in children_tmp { + let child_children = child.get_children(next, min, max, level + 1); + if !child_children.is_empty() { + children.extend(child_children); + } + if *next > max { + break; + } + } + } + children + } +} diff --git a/lapce-app/src/panel/kind.rs b/lapce-app/src/panel/kind.rs new file mode 100644 index 0000000..695990e --- /dev/null +++ b/lapce-app/src/panel/kind.rs @@ -0,0 +1,66 @@ +use serde::{Deserialize, Serialize}; +use strum_macros::EnumIter; + +use super::{data::PanelOrder, position::PanelPosition}; +use crate::config::icon::LapceIcons; + +#[derive( + Clone, Copy, PartialEq, Serialize, Deserialize, Hash, Eq, Debug, EnumIter, +)] +pub enum PanelKind { + Terminal, + FileExplorer, + SourceControl, + Plugin, + Search, + Problem, + Debug, + CallHierarchy, + DocumentSymbol, + References, + Implementation, +} + +impl PanelKind { + pub fn svg_name(&self) -> &'static str { + match &self { + PanelKind::Terminal => LapceIcons::TERMINAL, + PanelKind::FileExplorer => LapceIcons::FILE_EXPLORER, + PanelKind::SourceControl => LapceIcons::SCM, + PanelKind::Plugin => LapceIcons::EXTENSIONS, + PanelKind::Search => LapceIcons::SEARCH, + PanelKind::Problem => LapceIcons::PROBLEM, + PanelKind::Debug => LapceIcons::DEBUG, + PanelKind::CallHierarchy => LapceIcons::TYPE_HIERARCHY, + PanelKind::DocumentSymbol => LapceIcons::DOCUMENT_SYMBOL, + PanelKind::References => LapceIcons::REFERENCES, + PanelKind::Implementation => LapceIcons::IMPLEMENTATION, + } + } + + pub fn position(&self, order: &PanelOrder) -> Option<(usize, PanelPosition)> { + for (pos, panels) in order.iter() { + let index = panels.iter().position(|k| k == self); + if let Some(index) = index { + return Some((index, *pos)); + } + } + None + } + + pub fn default_position(&self) -> PanelPosition { + match self { + PanelKind::Terminal => PanelPosition::BottomLeft, + PanelKind::FileExplorer => PanelPosition::LeftTop, + PanelKind::SourceControl => PanelPosition::LeftTop, + PanelKind::Plugin => PanelPosition::LeftTop, + PanelKind::Search => PanelPosition::BottomLeft, + PanelKind::Problem => PanelPosition::BottomLeft, + PanelKind::Debug => PanelPosition::LeftTop, + PanelKind::CallHierarchy => PanelPosition::BottomLeft, + PanelKind::DocumentSymbol => PanelPosition::RightTop, + PanelKind::References => PanelPosition::BottomLeft, + PanelKind::Implementation => PanelPosition::BottomLeft, + } + } +} diff --git a/lapce-app/src/panel/mod.rs b/lapce-app/src/panel/mod.rs new file mode 100644 index 0000000..016192e --- /dev/null +++ b/lapce-app/src/panel/mod.rs @@ -0,0 +1,15 @@ +pub mod call_hierarchy_view; +pub mod data; +pub mod debug_view; +pub mod document_symbol; +pub mod global_search_view; +pub mod implementation_view; +pub mod kind; +pub mod plugin_view; +pub mod position; +pub mod problem_view; +pub mod references_view; +pub mod source_control_view; +pub mod style; +pub mod terminal_view; +pub mod view; diff --git a/lapce-app/src/panel/plugin_view.rs b/lapce-app/src/panel/plugin_view.rs new file mode 100644 index 0000000..4067761 --- /dev/null +++ b/lapce-app/src/panel/plugin_view.rs @@ -0,0 +1,407 @@ +use std::{ops::Range, rc::Rc}; + +use floem::{ + IntoView, View, + event::EventListener, + peniko::kurbo::{Point, Rect, Size}, + reactive::{ + RwSignal, SignalGet, SignalUpdate, SignalWith, create_memo, create_rw_signal, + }, + style::CursorStyle, + views::{ + Decorators, VirtualVector, container, dyn_container, img, label, + scroll::scroll, stack, svg, virtual_stack, + }, +}; +use indexmap::IndexMap; +use lapce_rpc::{ + core::CoreRpcHandler, + plugin::{VoltID, VoltInfo}, +}; + +use super::{ + data::PanelSection, kind::PanelKind, position::PanelPosition, view::PanelBuilder, +}; +use crate::{ + app::not_clickable_icon, + command::InternalCommand, + config::{color::LapceColor, icon::LapceIcons}, + plugin::{AvailableVoltData, InstalledVoltData, PluginData, VoltIcon}, + text_input::TextInputBuilder, + window_tab::{Focus, WindowTabData}, +}; + +pub const VOLT_DEFAULT_PNG: &[u8] = include_bytes!("../../../extra/images/volt.png"); + +struct IndexMapItems(IndexMap); + +impl IndexMapItems { + fn items(&self, range: Range) -> Vec<(K, V)> { + let mut items = Vec::new(); + for i in range { + if let Some((k, v)) = self.0.get_index(i) { + items.push((k.clone(), v.clone())); + } + } + items + } +} + +impl VirtualVector<(usize, K, V)> + for IndexMapItems +{ + fn total_len(&self) -> usize { + self.0.len() + } + + fn slice(&mut self, range: Range) -> impl Iterator { + let start = range.start; + Box::new( + self.items(range) + .into_iter() + .enumerate() + .map(move |(i, (k, v))| (i + start, k, v)), + ) + } +} + +pub fn plugin_panel( + window_tab_data: Rc, + position: PanelPosition, +) -> impl View { + let config = window_tab_data.common.config; + let plugin = window_tab_data.plugin.clone(); + let core_rpc = window_tab_data.proxy.core_rpc.clone(); + + PanelBuilder::new(config, position) + .add( + "Installed", + installed_view(plugin.clone()), + window_tab_data.panel.section_open(PanelSection::Installed), + ) + .add( + "Available", + available_view(plugin.clone(), core_rpc), + window_tab_data.panel.section_open(PanelSection::Available), + ) + .build() + .debug_name("Plugin Panel") +} + +fn installed_view(plugin: PluginData) -> impl View { + let ui_line_height = plugin.common.ui_line_height; + let volts = plugin.installed; + let config = plugin.common.config; + let disabled = plugin.disabled; + let workspace_disabled = plugin.workspace_disabled; + let internal_command = plugin.common.internal_command; + + let view_fn = move |volt: InstalledVoltData, plugin: PluginData| { + let meta = volt.meta.get_untracked(); + let volt_id = meta.id(); + let local_volt_id = volt_id.clone(); + let icon = volt.icon; + stack(( + dyn_container( + move || icon.get(), + move |icon| match icon { + None => img(move || VOLT_DEFAULT_PNG.to_vec()) + .style(|s| s.size_full()) + .into_any(), + Some(VoltIcon::Svg(svg_str)) => svg(move || svg_str.clone()) + .style(|s| s.size_full()) + .into_any(), + Some(VoltIcon::Img(buf)) => { + img(move || buf.clone()).style(|s| s.size_full()).into_any() + } + }, + ) + .style(|s| { + s.min_size(50.0, 50.0) + .size(50.0, 50.0) + .margin_top(5.0) + .margin_right(10.0) + .padding(5) + }), + stack(( + label(move || meta.display_name.clone()).style(|s| { + s.font_bold() + .text_ellipsis() + .min_width(0.0) + .selectable(false) + }), + label(move || meta.description.clone()) + .style(|s| s.text_ellipsis().min_width(0.0).selectable(false)), + stack(( + stack(( + label(move || meta.author.clone()).style(|s| { + s.text_ellipsis().max_width_pct(100.0).selectable(false) + }), + label(move || { + if disabled.with(|d| d.contains(&volt_id)) + || workspace_disabled.with(|d| d.contains(&volt_id)) + { + "Disabled".to_string() + } else if volt.meta.with(|m| { + volt.latest.with(|i| i.version != m.version) + }) { + "Upgrade".to_string() + } else { + format!("v{}", volt.meta.with(|m| m.version.clone())) + } + }) + .style(|s| s.text_ellipsis().selectable(false)), + )) + .style(|s| { + s.justify_between() + .flex_grow(1.0) + .flex_basis(0.0) + .min_width(0.0) + }), + not_clickable_icon( + || LapceIcons::SETTINGS, + || false, + || false, + || "Options", + config, + ) + .style(|s| s.padding_left(6.0)) + .popout_menu(move || { + plugin.plugin_controls(volt.meta.get(), volt.latest.get()) + }), + )) + .style(|s| s.width_pct(100.0).items_center()), + )) + .style(|s| s.flex_col().flex_grow(1.0).flex_basis(0.0).min_width(0.0)), + )) + .on_click_stop(move |_| { + internal_command.send(InternalCommand::OpenVoltView { + volt_id: local_volt_id.clone(), + }); + }) + .style(move |s| { + s.width_pct(100.0) + .padding_horiz(10.0) + .padding_vert(5.0) + .cursor(CursorStyle::Pointer) + .hover(|s| { + s.background( + config.get().color(LapceColor::PANEL_HOVERED_BACKGROUND), + ) + }) + }) + }; + + container( + scroll( + virtual_stack( + move || IndexMapItems(volts.get()), + move |(_, id, _)| id.clone(), + move |(_, _, volt)| view_fn(volt, plugin.clone()), + ) + .item_size_fixed(move || ui_line_height.get() * 3.0 + 10.0) + .style(|s| s.flex_col().width_pct(100.0)), + ) + .style(|s| s.absolute().size_pct(100.0, 100.0)), + ) + .style(|s| { + s.width_pct(100.0) + .line_height(1.6) + .flex_grow(1.0) + .flex_basis(0.0) + }) +} + +fn available_view(plugin: PluginData, core_rpc: CoreRpcHandler) -> impl View { + let ui_line_height = plugin.common.ui_line_height; + let volts = plugin.available.volts; + let installed = plugin.installed; + let config = plugin.common.config; + let internal_command = plugin.common.internal_command; + + let local_plugin = plugin.clone(); + let install_button = + move |id: VoltID, info: RwSignal, installing: RwSignal| { + let plugin = local_plugin.clone(); + let installed = create_memo(move |_| { + installed.with(|installed| installed.contains_key(&id)) + }); + label(move || { + if installed.get() { + "Installed".to_string() + } else if installing.get() { + "Installing".to_string() + } else { + "Install".to_string() + } + }) + .disabled(move || installed.get() || installing.get()) + .on_click_stop(move |_| { + plugin.install_volt(info.get_untracked()); + }) + .style(move |s| { + let config = config.get(); + s.color(config.color(LapceColor::LAPCE_BUTTON_PRIMARY_FOREGROUND)) + .background( + config.color(LapceColor::LAPCE_BUTTON_PRIMARY_BACKGROUND), + ) + .margin_left(6.0) + .padding_horiz(6.0) + .border_radius(6.0) + .hover(|s| { + s.cursor(CursorStyle::Pointer).background( + config + .color(LapceColor::LAPCE_BUTTON_PRIMARY_BACKGROUND) + .multiply_alpha(0.8), + ) + }) + .active(|s| { + s.background( + config + .color(LapceColor::LAPCE_BUTTON_PRIMARY_BACKGROUND) + .multiply_alpha(0.6), + ) + }) + .disabled(|s| s.background(config.color(LapceColor::EDITOR_DIM))) + }) + }; + + let view_fn = move |(_, id, volt): (usize, VoltID, AvailableVoltData)| { + let info = volt.info.get_untracked(); + let icon = volt.icon; + let volt_id = info.id(); + stack(( + dyn_container( + move || icon.get(), + move |icon| match icon { + None => img(move || VOLT_DEFAULT_PNG.to_vec()) + .style(|s| s.size_full()) + .into_any(), + Some(VoltIcon::Svg(svg_str)) => svg(move || svg_str.clone()) + .style(|s| s.size_full()) + .into_any(), + Some(VoltIcon::Img(buf)) => { + img(move || buf.clone()).style(|s| s.size_full()).into_any() + } + }, + ) + .style(|s| { + s.min_size(50.0, 50.0) + .size(50.0, 50.0) + .margin_top(5.0) + .margin_right(10.0) + .padding(5) + }), + stack(( + label(move || info.display_name.clone()).style(|s| { + s.font_bold() + .text_ellipsis() + .min_width(0.0) + .selectable(false) + }), + label(move || info.description.clone()) + .style(|s| s.text_ellipsis().min_width(0.0).selectable(false)), + stack(( + label(move || info.author.clone()).style(|s| { + s.text_ellipsis() + .min_width(0.0) + .flex_grow(1.0) + .flex_basis(0.0) + .selectable(false) + }), + install_button(id, volt.info, volt.installing), + )) + .style(|s| s.width_pct(100.0).items_center()), + )) + .style(|s| s.flex_col().flex_grow(1.0).flex_basis(0.0).min_width(0.0)), + )) + .on_click_stop(move |_| { + internal_command.send(InternalCommand::OpenVoltView { + volt_id: volt_id.clone(), + }); + }) + .style(move |s| { + s.width_pct(100.0) + .padding_horiz(10.0) + .padding_vert(5.0) + .cursor(CursorStyle::Pointer) + .hover(|s| { + s.background( + config.get().color(LapceColor::PANEL_HOVERED_BACKGROUND), + ) + }) + }) + }; + + let content_rect = create_rw_signal(Rect::ZERO); + + let editor = plugin.available.query_editor.clone(); + let focus = plugin.common.focus; + let is_focused = move || focus.get() == Focus::Panel(PanelKind::Plugin); + let cursor_x = create_rw_signal(0.0); + + stack(( + container({ + scroll( + TextInputBuilder::new() + .is_focused(is_focused) + .build_editor(editor.clone()) + .placeholder(|| "Search extensions".to_string()) + .on_cursor_pos(move |point| { + cursor_x.set(point.x); + }) + .style(|s| { + s.padding_vert(4.0).padding_horiz(10.0).min_width_pct(100.0) + }), + ) + .ensure_visible(move || { + Size::new(20.0, 0.0) + .to_rect() + .with_origin(Point::new(cursor_x.get(), 0.0)) + }) + .on_event_cont(EventListener::PointerDown, move |_| { + focus.set(Focus::Panel(PanelKind::Plugin)); + }) + .scroll_style(|s| s.hide_bars(true)) + .style(move |s| { + let config = config.get(); + s.width_pct(100.0) + .cursor(CursorStyle::Text) + .items_center() + .background(config.color(LapceColor::EDITOR_BACKGROUND)) + .border(1.0) + .border_radius(6.0) + .border_color(config.color(LapceColor::LAPCE_BORDER)) + }) + }) + .style(|s| s.padding(10.0).width_pct(100.0)), + container({ + scroll({ + virtual_stack( + move || IndexMapItems(volts.get()), + move |(_, id, _)| id.clone(), + view_fn, + ) + .item_size_fixed(move || ui_line_height.get() * 3.0 + 10.0) + .on_resize(move |rect| { + content_rect.set(rect); + }) + .style(|s| s.flex_col().width_pct(100.0)) + }) + .on_scroll(move |rect| { + if rect.y1 + 30.0 > content_rect.get_untracked().y1 { + plugin.load_more_available(core_rpc.clone()); + } + }) + .style(|s| s.absolute().size_pct(100.0, 100.0)) + }) + .style(|s| s.size_pct(100.0, 100.0)), + )) + .style(|s| { + s.width_pct(100.0) + .line_height(1.6) + .flex_grow(1.0) + .flex_basis(0.0) + .flex_col() + }) +} diff --git a/lapce-app/src/panel/position.rs b/lapce-app/src/panel/position.rs new file mode 100644 index 0000000..553319a --- /dev/null +++ b/lapce-app/src/panel/position.rs @@ -0,0 +1,74 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Eq, PartialEq, Hash, Clone, Copy, Debug, Serialize, Deserialize)] +pub enum PanelPosition { + LeftTop, + LeftBottom, + BottomLeft, + BottomRight, + RightTop, + RightBottom, +} + +impl PanelPosition { + pub fn is_bottom(&self) -> bool { + matches!(self, PanelPosition::BottomLeft | PanelPosition::BottomRight) + } + + pub fn is_right(&self) -> bool { + matches!(self, PanelPosition::RightTop | PanelPosition::RightBottom) + } + + pub fn is_left(&self) -> bool { + matches!(self, PanelPosition::LeftTop | PanelPosition::LeftBottom) + } + + pub fn is_first(&self) -> bool { + matches!( + self, + PanelPosition::LeftTop + | PanelPosition::BottomLeft + | PanelPosition::RightTop + ) + } + + pub fn peer(&self) -> PanelPosition { + match &self { + PanelPosition::LeftTop => PanelPosition::LeftBottom, + PanelPosition::LeftBottom => PanelPosition::LeftTop, + PanelPosition::BottomLeft => PanelPosition::BottomRight, + PanelPosition::BottomRight => PanelPosition::BottomLeft, + PanelPosition::RightTop => PanelPosition::RightBottom, + PanelPosition::RightBottom => PanelPosition::RightTop, + } + } +} + +#[derive(Eq, PartialEq, Hash, Clone, Copy, Debug)] +pub enum PanelContainerPosition { + Left, + Bottom, + Right, +} + +impl PanelContainerPosition { + pub fn is_bottom(&self) -> bool { + matches!(self, PanelContainerPosition::Bottom) + } + + pub fn first(&self) -> PanelPosition { + match self { + PanelContainerPosition::Left => PanelPosition::LeftTop, + PanelContainerPosition::Bottom => PanelPosition::BottomLeft, + PanelContainerPosition::Right => PanelPosition::RightTop, + } + } + + pub fn second(&self) -> PanelPosition { + match self { + PanelContainerPosition::Left => PanelPosition::LeftBottom, + PanelContainerPosition::Bottom => PanelPosition::BottomRight, + PanelContainerPosition::Right => PanelPosition::RightBottom, + } + } +} diff --git a/lapce-app/src/panel/problem_view.rs b/lapce-app/src/panel/problem_view.rs new file mode 100644 index 0000000..1ce8819 --- /dev/null +++ b/lapce-app/src/panel/problem_view.rs @@ -0,0 +1,399 @@ +use std::{path::PathBuf, rc::Rc, sync::Arc}; + +use floem::{ + View, + peniko::Color, + reactive::{ + ReadSignal, SignalGet, SignalUpdate, SignalWith, create_effect, + create_rw_signal, + }, + style::{CursorStyle, Style}, + views::{Decorators, container, dyn_stack, label, scroll, stack, svg}, +}; +use lsp_types::{DiagnosticRelatedInformation, DiagnosticSeverity}; + +use super::{data::PanelSection, position::PanelPosition, view::PanelBuilder}; +use crate::{ + command::InternalCommand, + config::{LapceConfig, color::LapceColor, icon::LapceIcons}, + doc::{DiagnosticData, EditorDiagnostic}, + editor::location::{EditorLocation, EditorPosition}, + listener::Listener, + lsp::path_from_url, + window_tab::WindowTabData, + workspace::LapceWorkspace, +}; + +pub fn problem_panel( + window_tab_data: Rc, + position: PanelPosition, +) -> impl View { + let config = window_tab_data.common.config; + let is_bottom = position.is_bottom(); + PanelBuilder::new(config, position) + .add_style( + "Errors", + problem_section(window_tab_data.clone(), DiagnosticSeverity::ERROR), + window_tab_data.panel.section_open(PanelSection::Error), + move |s| { + s.border_color(config.get().color(LapceColor::LAPCE_BORDER)) + .apply_if(is_bottom, |s| s.border_right(1.0)) + .apply_if(!is_bottom, |s| s.border_bottom(1.0)) + }, + ) + .add( + "Warnings", + problem_section(window_tab_data.clone(), DiagnosticSeverity::WARNING), + window_tab_data.panel.section_open(PanelSection::Warn), + ) + .build() + .debug_name("Problem Panel") +} + +fn problem_section( + window_tab_data: Rc, + severity: DiagnosticSeverity, +) -> impl View { + let config = window_tab_data.common.config; + let main_split = window_tab_data.main_split.clone(); + let internal_command = window_tab_data.common.internal_command; + container({ + scroll( + dyn_stack( + move || main_split.diagnostics.get(), + |(p, _)| p.clone(), + move |(path, diagnostic_data)| { + file_view( + main_split.common.workspace.clone(), + path, + diagnostic_data, + severity, + internal_command, + config, + ) + }, + ) + .style(|s| s.flex_col().width_pct(100.0).line_height(1.8)), + ) + .style(|s| s.absolute().size_pct(100.0, 100.0)) + }) + .style(|s| s.size_pct(100.0, 100.0)) +} + +fn file_view( + workspace: Arc, + path: PathBuf, + diagnostic_data: DiagnosticData, + severity: DiagnosticSeverity, + internal_command: Listener, + config: ReadSignal>, +) -> impl View { + let collapsed = create_rw_signal(false); + + let diagnostics = create_rw_signal(im::Vector::new()); + create_effect(move |_| { + let span = diagnostic_data.diagnostics_span.get(); + let d = if !span.is_empty() { + span.iter() + .filter_map(|(iv, diag)| { + if diag.severity == Some(severity) { + Some(EditorDiagnostic { + range: Some((iv.start, iv.end)), + diagnostic: diag.to_owned(), + }) + } else { + None + } + }) + .collect::>() + } else { + let diagnostics = diagnostic_data.diagnostics.get(); + let diagnostics: im::Vector = diagnostics + .into_iter() + .filter_map(|d| { + if d.severity == Some(severity) { + Some(EditorDiagnostic { + range: None, + diagnostic: d, + }) + } else { + None + } + }) + .collect(); + diagnostics + }; + diagnostics.set(d); + }); + + let full_path = path.clone(); + let path = if let Some(workspace_path) = workspace.path.as_ref() { + path.strip_prefix(workspace_path) + .unwrap_or(&full_path) + .to_path_buf() + } else { + path + }; + let style_path = path.clone(); + + let icon = match severity { + DiagnosticSeverity::ERROR => LapceIcons::ERROR, + _ => LapceIcons::WARNING, + }; + let icon_color = move || { + let config = config.get(); + match severity { + DiagnosticSeverity::ERROR => config.color(LapceColor::LAPCE_ERROR), + _ => config.color(LapceColor::LAPCE_WARN), + } + }; + + let file_name = path + .file_name() + .and_then(|s| s.to_str()) + .unwrap_or("") + .to_string(); + + let folder = path + .parent() + .and_then(|s| s.to_str()) + .unwrap_or("") + .to_string(); + + stack(( + stack(( + container( + stack(( + label(move || file_name.clone()).style(|s| { + s.margin_right(6.0) + .max_width_pct(100.0) + .text_ellipsis() + .selectable(false) + }), + label(move || folder.clone()).style(move |s| { + s.color(config.get().color(LapceColor::EDITOR_DIM)) + .min_width(0.0) + .text_ellipsis() + .selectable(false) + }), + )) + .style(move |s| s.width_pct(100.0).min_width(0.0)), + ) + .on_click_stop(move |_| { + collapsed.update(|collapsed| *collapsed = !*collapsed); + }) + .style(move |s| { + let config = config.get(); + s.width_pct(100.0) + .min_width(0.0) + .padding_left(10.0 + (config.ui.icon_size() as f32 + 6.0) * 2.0) + .padding_right(10.0) + .hover(|s| { + s.cursor(CursorStyle::Pointer).background( + config.color(LapceColor::PANEL_HOVERED_BACKGROUND), + ) + }) + }), + stack(( + svg(move || { + config.get().ui_svg(if collapsed.get() { + LapceIcons::ITEM_CLOSED + } else { + LapceIcons::ITEM_OPENED + }) + }) + .style(move |s| { + let config = config.get(); + let size = config.ui.icon_size() as f32; + s.margin_right(6.0) + .size(size, size) + .color(config.color(LapceColor::LAPCE_ICON_ACTIVE)) + }), + svg(move || config.get().file_svg(&path).0).style(move |s| { + let config = config.get(); + let size = config.ui.icon_size() as f32; + let color = config.file_svg(&style_path).1; + s.min_width(size) + .size(size, size) + .apply_opt(color, Style::color) + }), + label(|| " ".to_string()).style(move |s| s.selectable(false)), + )) + .style(|s| s.absolute().items_center().margin_left(10.0)), + )) + .style(move |s| s.width_pct(100.0).min_width(0.0)), + dyn_stack( + move || { + if collapsed.get() { + im::Vector::new() + } else { + diagnostics.get() + } + }, + |d| (d.range, d.diagnostic.range), + move |d| { + item_view( + full_path.clone(), + d, + icon, + icon_color, + internal_command, + config, + ) + }, + ) + .style(|s| s.flex_col().width_pct(100.0).min_width_pct(0.0)), + )) + .style(move |s| { + s.width_pct(100.0) + .items_start() + .flex_col() + .apply_if(diagnostics.with(|d| d.is_empty()), |s| s.hide()) + }) +} + +fn item_view( + path: PathBuf, + d: EditorDiagnostic, + icon: &'static str, + icon_color: impl Fn() -> Color + 'static, + internal_command: Listener, + config: ReadSignal>, +) -> impl View { + let related = d.diagnostic.related_information.unwrap_or_default(); + let position = if let Some((start, _)) = d.range { + EditorPosition::Offset(start) + } else { + EditorPosition::Position(d.diagnostic.range.start) + }; + let location = EditorLocation { + path, + position: Some(position), + scroll_offset: None, + ignore_unconfirmed: false, + same_editor_tab: false, + }; + stack(( + container({ + stack(( + label(move || d.diagnostic.message.clone()).style(move |s| { + s.width_pct(100.0) + .min_width(0.0) + .padding_left( + 10.0 + (config.get().ui.icon_size() as f32 + 6.0) * 3.0, + ) + .padding_right(10.0) + }), + stack(( + svg(move || config.get().ui_svg(icon)).style(move |s| { + let config = config.get(); + let size = config.ui.icon_size() as f32; + s.size(size, size).color(icon_color()) + }), + label(|| " ".to_string()).style(move |s| s.selectable(false)), + )) + .style(move |s| { + s.absolute().items_center().margin_left( + 10.0 + (config.get().ui.icon_size() as f32 + 6.0) * 2.0, + ) + }), + )) + .style(move |s| { + s.width_pct(100.0).min_width(0.0).hover(|s| { + s.cursor(CursorStyle::Pointer).background( + config.get().color(LapceColor::PANEL_HOVERED_BACKGROUND), + ) + }) + }) + }) + .on_click_stop(move |_| { + internal_command.send(InternalCommand::JumpToLocation { + location: location.clone(), + }); + }) + .style(|s| s.width_pct(100.0).min_width_pct(0.0)), + related_view(related, internal_command, config), + )) + .style(|s| s.width_pct(100.0).min_width_pct(0.0).flex_col()) +} + +fn related_view( + related: Vec, + internal_command: Listener, + config: ReadSignal>, +) -> impl View { + let is_empty = related.is_empty(); + stack(( + dyn_stack( + move || related.clone(), + |_| 0, + move |related| { + let full_path = path_from_url(&related.location.uri); + let path = full_path + .file_name() + .and_then(|f| f.to_str()) + .map(|f| { + format!( + "{f} [{}, {}]: ", + related.location.range.start.line, + related.location.range.start.character + ) + }) + .unwrap_or_default(); + let location = EditorLocation { + path: full_path, + position: Some(EditorPosition::Position( + related.location.range.start, + )), + scroll_offset: None, + ignore_unconfirmed: false, + same_editor_tab: false, + }; + let message = format!("{path}{}", related.message); + container( + label(move || message.clone()) + .style(move |s| s.width_pct(100.0).min_width(0.0)), + ) + .on_click_stop(move |_| { + internal_command.send(InternalCommand::JumpToLocation { + location: location.clone(), + }); + }) + .style(move |s| { + let config = config.get(); + s.padding_left(10.0 + (config.ui.icon_size() as f32 + 6.0) * 4.0) + .padding_right(10.0) + .width_pct(100.0) + .min_width(0.0) + .hover(|s| { + s.cursor(CursorStyle::Pointer).background( + config.color(LapceColor::PANEL_HOVERED_BACKGROUND), + ) + }) + }) + }, + ) + .style(|s| s.width_pct(100.0).min_width(0.0).flex_col()), + stack(( + svg(move || config.get().ui_svg(LapceIcons::LINK)).style(move |s| { + let config = config.get(); + let size = config.ui.icon_size() as f32; + s.size(size, size) + .color(config.color(LapceColor::EDITOR_DIM)) + }), + label(|| " ".to_string()).style(move |s| s.selectable(false)), + )) + .style(move |s| { + s.absolute() + .items_center() + .margin_left(10.0 + (config.get().ui.icon_size() as f32 + 6.0) * 3.0) + }), + )) + .style(move |s| { + s.width_pct(100.0) + .min_width(0.0) + .items_start() + .color(config.get().color(LapceColor::EDITOR_DIM)) + .apply_if(is_empty, |s| s.hide()) + }) +} diff --git a/lapce-app/src/panel/references_view.rs b/lapce-app/src/panel/references_view.rs new file mode 100644 index 0000000..6e4871a --- /dev/null +++ b/lapce-app/src/panel/references_view.rs @@ -0,0 +1,18 @@ +use std::rc::Rc; + +use floem::{View, reactive::SignalGet, views::Decorators}; + +use super::position::PanelPosition; +use crate::{ + panel::implementation_view::common_reference_panel, window_tab::WindowTabData, +}; + +pub fn references_panel( + window_tab_data: Rc, + _position: PanelPosition, +) -> impl View { + common_reference_panel(window_tab_data.clone(), _position, move || { + window_tab_data.main_split.references.get() + }) + .debug_name("references panel") +} diff --git a/lapce-app/src/panel/source_control_view.rs b/lapce-app/src/panel/source_control_view.rs new file mode 100644 index 0000000..504981e --- /dev/null +++ b/lapce-app/src/panel/source_control_view.rs @@ -0,0 +1,364 @@ +use std::{path::PathBuf, rc::Rc}; + +use floem::{ + View, + action::show_context_menu, + event::{Event, EventListener}, + menu::{Menu, MenuItem}, + peniko::kurbo::Rect, + prelude::SignalTrack, + reactive::{SignalGet, SignalUpdate, SignalWith, create_memo, create_rw_signal}, + style::{CursorStyle, Style}, + views::{ + Decorators, container, dyn_stack, + editor::view::{LineRegion, cursor_caret}, + label, scroll, stack, svg, text, + }, +}; +use lapce_core::buffer::rope_text::RopeText; +use lapce_rpc::source_control::FileDiff; + +use super::{ + data::PanelSection, kind::PanelKind, position::PanelPosition, + view::foldable_panel_section, +}; +use crate::{ + command::{CommandKind, InternalCommand, LapceCommand, LapceWorkbenchCommand}, + config::{color::LapceColor, icon::LapceIcons}, + editor::view::editor_view, + settings::checkbox, + source_control::SourceControlData, + window_tab::{Focus, WindowTabData}, +}; + +pub fn source_control_panel( + window_tab_data: Rc, + _position: PanelPosition, +) -> impl View { + let config = window_tab_data.common.config; + let source_control = window_tab_data.source_control.clone(); + let focus = source_control.common.focus; + let editor = source_control.editor.clone(); + let doc = editor.doc_signal(); + let cursor = editor.cursor(); + let viewport = editor.viewport(); + let window_origin = editor.window_origin(); + let editor = create_rw_signal(editor); + let is_active = move |tracked| { + let focus = if tracked { + focus.get() + } else { + focus.get_untracked() + }; + focus == Focus::Panel(PanelKind::SourceControl) + }; + let is_empty = create_memo(move |_| { + let doc = doc.get(); + doc.buffer.with(|b| b.len() == 0) + }); + let debug_breakline = create_memo(move |_| None); + + stack(( + stack(( + container({ + scroll({ + let view = stack(( + editor_view( + editor.get_untracked(), + debug_breakline, + is_active, + ), + label(|| "Commit Message".to_string()).style(move |s| { + let config = config.get(); + s.absolute() + .items_center() + .height(config.editor.line_height() as f32) + .color(config.color(LapceColor::EDITOR_DIM)) + .apply_if(!is_empty.get(), |s| s.hide()) + .selectable(false) + }), + )) + .style(|s| { + s.absolute() + .min_size_pct(100.0, 100.0) + .padding_left(10.0) + .padding_vert(6.0) + .hover(|s| s.cursor(CursorStyle::Text)) + }); + let id = view.id(); + view.on_event_cont(EventListener::PointerDown, move |event| { + let event = event.clone().offset((10.0, 6.0)); + if let Event::PointerDown(pointer_event) = event { + id.request_active(); + editor.get_untracked().pointer_down(&pointer_event); + } + }) + .on_event_stop(EventListener::PointerMove, move |event| { + let event = event.clone().offset((10.0, 6.0)); + if let Event::PointerMove(pointer_event) = event { + editor.get_untracked().pointer_move(&pointer_event); + } + }) + .on_event_stop( + EventListener::PointerUp, + move |event| { + let event = event.clone().offset((10.0, 6.0)); + if let Event::PointerUp(pointer_event) = event { + editor.get_untracked().pointer_up(&pointer_event); + } + }, + ) + }) + .on_move(move |pos| { + window_origin.set(pos + (10.0, 6.0)); + }) + .on_scroll(move |rect| { + viewport.set(rect); + }) + .ensure_visible(move || { + let cursor = cursor.get(); + let offset = cursor.offset(); + let e_data = editor.get_untracked(); + e_data.doc_signal().track(); + e_data.kind.track(); + let LineRegion { x, width, rvline } = cursor_caret( + &e_data.editor, + offset, + !cursor.is_insert(), + cursor.affinity, + ); + let config = config.get_untracked(); + let line_height = config.editor.line_height(); + // TODO: is there a way to avoid the calculation of the vline here? + let vline = e_data.editor.vline_of_rvline(rvline); + Rect::from_origin_size( + (x, (vline.get() * line_height) as f64), + (width, line_height as f64), + ) + .inflate(30.0, 10.0) + }) + .style(|s| s.absolute().size_pct(100.0, 100.0)) + }) + .style(move |s| { + let config = config.get(); + s.width_pct(100.0) + .height(120.0) + .border(1.0) + .padding(-1.0) + .border_radius(6.0) + .border_color(config.color(LapceColor::LAPCE_BORDER)) + .background(config.color(LapceColor::EDITOR_BACKGROUND)) + }), + { + let source_control = source_control.clone(); + label(|| "Commit".to_string()) + .on_click_stop(move |_| { + source_control.commit(); + }) + .style(move |s| { + let config = config.get(); + s.margin_top(10.0) + .line_height(1.6) + .width_pct(100.0) + .justify_center() + .border(1.0) + .border_radius(6.0) + .border_color(config.color(LapceColor::LAPCE_BORDER)) + .hover(|s| { + s.cursor(CursorStyle::Pointer).background( + config + .color(LapceColor::PANEL_HOVERED_BACKGROUND), + ) + }) + .active(|s| { + s.background(config.color( + LapceColor::PANEL_HOVERED_ACTIVE_BACKGROUND, + )) + }) + .selectable(false) + }) + }, + )) + .style(|s| s.flex_col().width_pct(100.0).padding(10.0)), + foldable_panel_section( + text("Changes"), + file_diffs_view(source_control), + window_tab_data.panel.section_open(PanelSection::Changes), + config, + ) + .style(|s| s.flex_col().size_pct(100.0, 100.0)), + )) + .on_event_stop(EventListener::PointerDown, move |_| { + if focus.get_untracked() != Focus::Panel(PanelKind::SourceControl) { + focus.set(Focus::Panel(PanelKind::SourceControl)); + } + }) + .style(|s| s.flex_col().size_pct(100.0, 100.0)) + .debug_name("Source Control Panel") +} + +fn file_diffs_view(source_control: SourceControlData) -> impl View { + let file_diffs = source_control.file_diffs; + let config = source_control.common.config; + let workspace = source_control.common.workspace.clone(); + let panel_rect = create_rw_signal(Rect::ZERO); + let panel_width = create_memo(move |_| panel_rect.get().width()); + let lapce_command = source_control.common.lapce_command; + let internal_command = source_control.common.internal_command; + + let view_fn = move |(path, (diff, checked)): (PathBuf, (FileDiff, bool))| { + let diff_for_style = diff.clone(); + let full_path = path.clone(); + let diff_for_menu = diff.clone(); + let path_for_click = full_path.clone(); + + let path = if let Some(workspace_path) = workspace.path.as_ref() { + path.strip_prefix(workspace_path) + .unwrap_or(&full_path) + .to_path_buf() + } else { + path + }; + let file_name = path + .file_name() + .and_then(|s| s.to_str()) + .unwrap_or("") + .to_string(); + let folder = path + .parent() + .and_then(|s| s.to_str()) + .unwrap_or("") + .to_string(); + let style_path = path.clone(); + stack(( + checkbox(move || checked, config).on_click_stop(move |_| { + file_diffs.update(|diffs| { + if let Some((_, checked)) = diffs.get_mut(&full_path) { + *checked = !*checked; + } + }); + }), + svg(move || config.get().file_svg(&path).0).style(move |s| { + let config = config.get(); + let size = config.ui.icon_size() as f32; + let color = config.file_svg(&style_path).1; + s.min_width(size) + .size(size, size) + .margin(6.0) + .apply_opt(color, Style::color) + }), + label(move || file_name.clone()).style(move |s| { + let config = config.get(); + let size = config.ui.icon_size() as f32; + let max_width = panel_width.get() as f32 + - 10.0 + - size + - 6.0 + - size + - 6.0 + - 10.0 + - size + - 6.0; + s.text_ellipsis() + .margin_right(6.0) + .max_width(max_width) + .selectable(false) + }), + label(move || folder.clone()).style(move |s| { + s.text_ellipsis() + .flex_grow(1.0) + .flex_basis(0.0) + .color(config.get().color(LapceColor::EDITOR_DIM)) + .min_width(0.0) + .selectable(false) + }), + container({ + svg(move || { + let svg = match &diff { + FileDiff::Modified(_) => LapceIcons::SCM_DIFF_MODIFIED, + FileDiff::Added(_) => LapceIcons::SCM_DIFF_ADDED, + FileDiff::Deleted(_) => LapceIcons::SCM_DIFF_REMOVED, + FileDiff::Renamed(_, _) => LapceIcons::SCM_DIFF_RENAMED, + }; + config.get().ui_svg(svg) + }) + .style(move |s| { + let config = config.get(); + let size = config.ui.icon_size() as f32; + let color = match &diff_for_style { + FileDiff::Modified(_) => LapceColor::SOURCE_CONTROL_MODIFIED, + FileDiff::Added(_) => LapceColor::SOURCE_CONTROL_ADDED, + FileDiff::Deleted(_) => LapceColor::SOURCE_CONTROL_REMOVED, + FileDiff::Renamed(_, _) => { + LapceColor::SOURCE_CONTROL_MODIFIED + } + }; + let color = config.color(color); + s.min_width(size).size(size, size).color(color) + }) + }) + .style(|s| { + s.absolute() + .size_pct(100.0, 100.0) + .padding_right(20.0) + .items_center() + .justify_end() + }), + )) + .on_click_stop(move |_| { + internal_command.send(InternalCommand::OpenFileChanges { + path: path_for_click.clone(), + }); + }) + .on_event_cont(EventListener::PointerDown, move |event| { + let diff_for_menu = diff_for_menu.clone(); + + let discard = move || { + lapce_command.send(LapceCommand { + kind: CommandKind::Workbench( + LapceWorkbenchCommand::SourceControlDiscardTargetFileChanges, + ), + data: Some(serde_json::json!(diff_for_menu.clone())), + }); + }; + + if let Event::PointerDown(pointer_event) = event { + if pointer_event.button.is_secondary() { + let menu = Menu::new("") + .entry(MenuItem::new("Discard Changes").action(discard)); + show_context_menu(menu, None); + } + } + }) + .style(move |s| { + let config = config.get(); + let size = config.ui.icon_size() as f32; + s.padding_left(10.0) + .padding_right(10.0 + size + 6.0) + .width_pct(100.0) + .items_center() + .cursor(CursorStyle::Pointer) + .hover(|s| { + s.background(config.color(LapceColor::PANEL_HOVERED_BACKGROUND)) + }) + }) + }; + + container({ + scroll({ + dyn_stack( + move || file_diffs.get(), + |(path, (diff, checked))| { + (path.to_path_buf(), diff.clone(), *checked) + }, + view_fn, + ) + .style(|s| s.line_height(1.6).flex_col().width_pct(100.0)) + }) + .style(|s| s.absolute().size_pct(100.0, 100.0)) + }) + .on_resize(move |rect| { + panel_rect.set(rect); + }) + .style(|s| s.size_pct(100.0, 100.0)) +} diff --git a/lapce-app/src/panel/style.rs b/lapce-app/src/panel/style.rs new file mode 100644 index 0000000..3123155 --- /dev/null +++ b/lapce-app/src/panel/style.rs @@ -0,0 +1,8 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Serialize, Deserialize, Default)] +pub struct PanelStyle { + pub active: usize, + pub shown: bool, + pub maximized: bool, +} diff --git a/lapce-app/src/panel/terminal_view.rs b/lapce-app/src/panel/terminal_view.rs new file mode 100644 index 0000000..ea1dc9b --- /dev/null +++ b/lapce-app/src/panel/terminal_view.rs @@ -0,0 +1,387 @@ +use std::rc::Rc; + +use floem::{ + View, ViewId, + action::show_context_menu, + event::{Event, EventListener, EventPropagation}, + kurbo::Size, + menu::{Menu, MenuItem}, + reactive::{SignalGet, SignalUpdate, SignalWith, create_rw_signal}, + style::CursorStyle, + views::{ + Decorators, container, dyn_stack, empty, label, + scroll::{Thickness, VerticalScrollAsHorizontal, scroll}, + stack, svg, tab, + }, +}; +use lapce_rpc::terminal::TermId; + +use super::kind::PanelKind; +use crate::{ + app::clickable_icon, + command::{InternalCommand, LapceWorkbenchCommand}, + config::{color::LapceColor, icon::LapceIcons}, + debug::RunDebugMode, + listener::Listener, + terminal::{ + panel::TerminalPanelData, tab::TerminalTabData, view::terminal_view, + }, + window_tab::{Focus, WindowTabData}, +}; + +pub fn terminal_panel(window_tab_data: Rc) -> impl View { + let focus = window_tab_data.common.focus; + stack(( + terminal_tab_header(window_tab_data.clone()), + terminal_tab_content(window_tab_data.clone()), + )) + .on_event_cont(EventListener::PointerDown, move |_| { + if focus.get_untracked() != Focus::Panel(PanelKind::Terminal) { + focus.set(Focus::Panel(PanelKind::Terminal)); + } + }) + .style(|s| s.absolute().size_pct(100.0, 100.0).flex_col()) + .debug_name("Terminal Panel") +} + +fn terminal_tab_header(window_tab_data: Rc) -> impl View { + let terminal = window_tab_data.terminal.clone(); + let config = window_tab_data.common.config; + let focus = window_tab_data.common.focus; + let active_index = move || terminal.tab_info.with(|info| info.active); + let tab_info = terminal.tab_info; + let header_width = create_rw_signal(0.0); + let header_height = create_rw_signal(0.0); + let icon_width = create_rw_signal(0.0); + let scroll_size = create_rw_signal(Size::ZERO); + let workbench_command = window_tab_data.common.workbench_command; + + stack(( + scroll(dyn_stack( + move || { + let tabs = terminal.tab_info.with(|info| info.tabs.clone()); + for (i, (index, _)) in tabs.iter().enumerate() { + if index.get_untracked() != i { + index.set(i); + } + } + tabs + }, + |(_, tab)| tab.terminal_tab_id, + move |(index, tab)| { + let terminal = terminal.clone(); + let local_terminal = terminal.clone(); + let terminal_tab_id = tab.terminal_tab_id; + + let title = { + let tab = tab.clone(); + move || { + let terminal = tab.active_terminal(true); + let run_debug = terminal.as_ref().map(|t| t.run_debug); + if let Some(run_debug) = run_debug { + if let Some(name) = run_debug.with(|run_debug| { + run_debug.as_ref().map(|r| r.config.name.clone()) + }) { + return name; + } + } + + let title = terminal.map(|t| t.title); + let title = title.map(|t| t.get()); + title.unwrap_or_default() + } + }; + + let svg_string = move || { + let terminal = tab.active_terminal(true); + let run_debug = terminal.as_ref().map(|t| t.run_debug); + if let Some(run_debug) = run_debug { + if let Some((mode, stopped)) = run_debug.with(|run_debug| { + run_debug.as_ref().map(|r| (r.mode, r.stopped)) + }) { + let svg = match (mode, stopped) { + (RunDebugMode::Run, false) => LapceIcons::START, + (RunDebugMode::Run, true) => LapceIcons::RUN_ERRORS, + (RunDebugMode::Debug, false) => LapceIcons::DEBUG, + (RunDebugMode::Debug, true) => { + LapceIcons::DEBUG_DISCONNECT + } + }; + return svg; + } + } + LapceIcons::TERMINAL + }; + stack(( + container({ + stack(( + container( + svg(move || config.get().ui_svg(svg_string())) + .style(move |s| { + let config = config.get(); + let size = config.ui.icon_size() as f32; + s.size(size, size).color( + config.color( + LapceColor::LAPCE_ICON_ACTIVE, + ), + ) + }), + ) + .style(|s| s.padding_horiz(10.0).padding_vert(12.0)), + label(title).style(|s| { + s.min_width(0.0) + .flex_basis(0.0) + .flex_grow(1.0) + .text_ellipsis() + .selectable(false) + }), + clickable_icon( + || LapceIcons::CLOSE, + move || { + terminal.close_tab(Some(terminal_tab_id)); + }, + || false, + || false, + || "Close", + config, + ) + .style(|s| s.margin_horiz(6.0)), + empty().style(move |s| { + s.absolute() + .width_full() + .height(header_height.get() - 15.0) + .border_right(1.0) + .border_color( + config.get().color(LapceColor::LAPCE_BORDER), + ) + .pointer_events_none() + }), + )) + .style(move |s| { + s.items_center().width(200.0).border_color( + config.get().color(LapceColor::LAPCE_BORDER), + ) + }) + }) + .style(|s| s.items_center()), + container({ + label(|| "".to_string()).style(move |s| { + s.size_pct(100.0, 100.0) + .border_bottom(if active_index() == index.get() { + 2.0 + } else { + 0.0 + }) + .border_color(config.get().color( + if focus.get() + == Focus::Panel(PanelKind::Terminal) + { + LapceColor::LAPCE_TAB_ACTIVE_UNDERLINE + } else { + LapceColor::LAPCE_TAB_INACTIVE_UNDERLINE + }, + )) + }) + }) + .style(|s| { + s.absolute() + .padding_horiz(3.0) + .size_pct(100.0, 100.0) + .pointer_events_none() + }), + )) + .style(|s| s.cursor(CursorStyle::Pointer)) + .on_event_cont( + EventListener::PointerDown, + move |_| { + if tab_info.with_untracked(|tab| tab.active) + != index.get_untracked() + { + tab_info.update(|tab| { + tab.active = index.get_untracked(); + }); + local_terminal.update_debug_active_term(); + } + }, + ) + }, + )) + .on_resize(move |rect| { + if rect.size() != scroll_size.get_untracked() { + scroll_size.set(rect.size()); + } + }) + .style(move |s| { + let header_width = header_width.get(); + let icon_width = icon_width.get(); + s.set(VerticalScrollAsHorizontal, true) + .absolute() + .max_width(header_width - icon_width) + .set(Thickness, 3) + }), + empty().style(move |s| { + let size = scroll_size.get(); + s.size(size.width, size.height).pointer_events_none() + }), + container(clickable_icon( + || LapceIcons::ADD, + move || { + workbench_command.send(LapceWorkbenchCommand::NewTerminalTab); + }, + || false, + || false, + || "New Terminal", + config, + )) + .on_resize(move |rect| { + let width = rect.size().width; + if icon_width.get_untracked() != width { + icon_width.set(width); + } + }) + .style(|s| s.padding_horiz(10)), + )) + .on_resize(move |rect| { + let size = rect.size(); + if header_width.get_untracked() != size.width { + header_width.set(size.width); + } + if header_height.get_untracked() != size.height { + header_height.set(size.height); + } + }) + .on_double_click(move |_| { + window_tab_data + .panel + .toggle_maximize(&crate::panel::kind::PanelKind::Terminal); + EventPropagation::Stop + }) + .style(move |s| { + let config = config.get(); + s.width_pct(100.0) + .items_center() + .border_bottom(1.0) + .border_color(config.color(LapceColor::LAPCE_BORDER)) + }) +} + +fn terminal_tab_split( + terminal_panel_data: TerminalPanelData, + terminal_tab_data: TerminalTabData, + tab_index: usize, +) -> impl View { + let config = terminal_panel_data.common.config; + let internal_command = terminal_panel_data.common.internal_command; + let workspace = terminal_panel_data.workspace.clone(); + let active = terminal_tab_data.active; + let terminal_tab_scope = terminal_tab_data.scope; + dyn_stack( + move || { + let terminals = terminal_tab_data.terminals.get(); + for (i, (index, _)) in terminals.iter().enumerate() { + if index.get_untracked() != i { + index.set(i); + } + } + terminals + }, + |(_, terminal)| terminal.term_id, + move |(index, terminal)| { + let terminal_panel_data = terminal_panel_data.clone(); + let terminal_scope = terminal.scope; + container({ + let terminal_view = terminal_view( + terminal.term_id, + terminal.raw.read_only(), + terminal.mode.read_only(), + terminal.run_debug.read_only(), + terminal_panel_data, + terminal.launch_error, + internal_command, + workspace.clone(), + ); + let view_id = terminal_view.id(); + let have_task = terminal.run_debug.get_untracked().is_some(); + terminal_view + .on_event_cont(EventListener::PointerDown, move |_| { + active.set(index.get_untracked()); + }) + .on_secondary_click_stop(move |_| { + if have_task { + tab_secondary_click( + internal_command, + view_id, + tab_index, + index.get_untracked(), + terminal.term_id, + ); + } + }) + .on_event(EventListener::PointerWheel, move |event| { + if let Event::PointerWheel(pointer_event) = event { + terminal.clone().wheel_scroll(pointer_event.delta.y); + EventPropagation::Stop + } else { + EventPropagation::Continue + } + }) + .on_cleanup(move || { + terminal_scope.dispose(); + }) + .style(|s| s.size_pct(100.0, 100.0)) + }) + .style(move |s| { + s.size_pct(100.0, 100.0).padding_horiz(10.0).apply_if( + index.get() > 0, + |s| { + s.border_left(1.0).border_color( + config.get().color(LapceColor::LAPCE_BORDER), + ) + }, + ) + }) + }, + ) + .on_cleanup(move || { + terminal_tab_scope.dispose(); + }) + .style(|s| s.size_pct(100.0, 100.0)) +} + +fn terminal_tab_content(window_tab_data: Rc) -> impl View { + let terminal = window_tab_data.terminal.clone(); + tab( + move || terminal.tab_info.with(|info| info.active), + move || terminal.tab_info.with(|info| info.tabs.clone()), + |(_, tab)| tab.terminal_tab_id, + move |(tab_index, tab)| { + terminal_tab_split(terminal.clone(), tab, tab_index.get_untracked()) + }, + ) + .style(|s| s.size_pct(100.0, 100.0)) +} + +fn tab_secondary_click( + internal_command: Listener, + view_id: ViewId, + tab_index: usize, + terminal_index: usize, + term_id: TermId, +) { + let mut menu = Menu::new(""); + menu = menu + .entry(MenuItem::new("Stop").action(move || { + internal_command.send(InternalCommand::StopTerminal { term_id }); + })) + .entry(MenuItem::new("Restart").action(move || { + internal_command.send(InternalCommand::RestartTerminal { term_id }); + })) + .entry(MenuItem::new("Clear All").action(move || { + internal_command.send(InternalCommand::ClearTerminalBuffer { + view_id, + tab_index, + terminal_index, + }); + })); + show_context_menu(menu, None); +} diff --git a/lapce-app/src/panel/view.rs b/lapce-app/src/panel/view.rs new file mode 100644 index 0000000..fa1966b --- /dev/null +++ b/lapce-app/src/panel/view.rs @@ -0,0 +1,655 @@ +use std::{rc::Rc, sync::Arc}; + +use floem::{ + AnyView, IntoView, View, + event::{Event, EventListener, EventPropagation}, + kurbo::{Point, Size}, + reactive::{ + ReadSignal, RwSignal, SignalGet, SignalUpdate, SignalWith, create_rw_signal, + }, + style::{CursorStyle, Style}, + taffy::AlignItems, + unit::PxPctAuto, + views::{ + Decorators, container, dyn_stack, empty, h_stack, label, stack, + stack_from_iter, tab, text, + }, +}; + +use super::{ + debug_view::debug_panel, + global_search_view::global_search_panel, + kind::PanelKind, + plugin_view::plugin_panel, + position::{PanelContainerPosition, PanelPosition}, + problem_view::problem_panel, + source_control_view::source_control_panel, + terminal_view::terminal_panel, +}; +use crate::{ + app::{clickable_icon, clickable_icon_base}, + config::{LapceConfig, color::LapceColor, icon::LapceIcons}, + file_explorer::view::file_explorer_panel, + panel::{ + call_hierarchy_view::show_hierarchy_panel, document_symbol::symbol_panel, + implementation_view::implementation_panel, + references_view::references_panel, + }, + window_tab::{DragContent, WindowTabData}, +}; + +pub fn foldable_panel_section( + header: impl View + 'static, + child: impl View + 'static, + open: RwSignal, + config: ReadSignal>, +) -> impl View { + stack(( + h_stack(( + clickable_icon_base( + move || { + if open.get() { + LapceIcons::PANEL_FOLD_DOWN + } else { + LapceIcons::PANEL_FOLD_UP + } + }, + None::>, + || false, + || false, + config, + ), + header.style(|s| s.align_items(AlignItems::Center).padding_left(3.0)), + )) + .style(move |s| { + s.padding_horiz(10.0) + .padding_vert(6.0) + .width_pct(100.0) + .cursor(CursorStyle::Pointer) + .background(config.get().color(LapceColor::EDITOR_BACKGROUND)) + }) + .on_click_stop(move |_| { + open.update(|open| *open = !*open); + }), + child.style(move |s| s.apply_if(!open.get(), |s| s.hide())), + )) +} + +/// A builder for creating a foldable panel out of sections +pub struct PanelBuilder { + views: Vec, + config: ReadSignal>, + position: PanelPosition, +} +impl PanelBuilder { + pub fn new( + config: ReadSignal>, + position: PanelPosition, + ) -> Self { + Self { + views: Vec::new(), + config, + position, + } + } + + fn add_general( + mut self, + name: &'static str, + height: Option, + view: impl View + 'static, + open: RwSignal, + style: impl Fn(Style) -> Style + 'static, + ) -> Self { + let position = self.position; + let view = foldable_panel_section( + text(name).style(move |s| s.selectable(false)), + view, + open, + self.config, + ) + .style(move |s| { + let s = s.width_full().flex_col(); + // Use the manual height if given, otherwise if we're open behave flex, + // otherwise, do nothing so that there's no height + let s = if open.get() { + if let Some(height) = height { + s.height(height) + } else { + s.flex_grow(1.0).flex_basis(0.0) + } + } else if position.is_bottom() { + s.flex_grow(0.3).flex_basis(0.0) + } else { + s + }; + + style(s) + }); + self.views.push(view.into_any()); + self + } + + /// Add a view to the panel + pub fn add( + self, + name: &'static str, + view: impl View + 'static, + open: RwSignal, + ) -> Self { + self.add_general(name, None, view, open, std::convert::identity) + } + + /// Add a view to the panel with a custom style applied to the overall header+section-content + pub fn add_style( + self, + name: &'static str, + view: impl View + 'static, + open: RwSignal, + style: impl Fn(Style) -> Style + 'static, + ) -> Self { + self.add_general(name, None, view, open, style) + } + + /// Add a view to the panel with a custom height that is only used when the panel is open + pub fn add_height( + self, + name: &'static str, + height: impl Into, + view: impl View + 'static, + open: RwSignal, + ) -> Self { + self.add_general( + name, + Some(height.into()), + view, + open, + std::convert::identity, + ) + } + + /// Add a view to the panel with a custom height that is only used when the panel is open + /// and a custom style applied to the overall header+section-content + pub fn add_height_style( + self, + name: &'static str, + height: impl Into, + view: impl View + 'static, + open: RwSignal, + style: impl Fn(Style) -> Style + 'static, + ) -> Self { + self.add_general(name, Some(height.into()), view, open, style) + } + + /// Add a view to the panel with a custom height that is only used when the panel is open + pub fn add_height_pct( + self, + name: &'static str, + height: f64, + view: impl View + 'static, + open: RwSignal, + ) -> Self { + self.add_general( + name, + Some(PxPctAuto::Pct(height)), + view, + open, + std::convert::identity, + ) + } + + /// Build the panel into a view + pub fn build(self) -> impl View { + stack_from_iter(self.views).style(move |s| { + s.width_full() + .apply_if(!self.position.is_bottom(), |s| s.flex_col()) + }) + } +} + +pub fn panel_container_view( + window_tab_data: Rc, + position: PanelContainerPosition, +) -> impl View { + let panel = window_tab_data.panel.clone(); + let config = window_tab_data.common.config; + let dragging = window_tab_data.common.dragging; + let current_size = create_rw_signal(Size::ZERO); + let available_size = window_tab_data.panel.available_size; + let is_dragging_panel = move || { + dragging + .with(|d| d.as_ref().map(|d| d.is_panel())) + .unwrap_or(false) + }; + let drop_view = { + let panel = panel.clone(); + move |position: PanelPosition| { + let panel = panel.clone(); + let dragging_over = create_rw_signal(false); + empty() + .on_event(EventListener::DragEnter, move |_| { + if is_dragging_panel() { + dragging_over.set(true); + EventPropagation::Stop + } else { + EventPropagation::Continue + } + }) + .on_event(EventListener::DragLeave, move |_| { + if is_dragging_panel() { + dragging_over.set(false); + EventPropagation::Stop + } else { + EventPropagation::Continue + } + }) + .on_event(EventListener::Drop, move |_| { + if let Some(DragContent::Panel(kind)) = dragging.get_untracked() + { + dragging_over.set(false); + panel.move_panel_to_position(kind, &position); + EventPropagation::Stop + } else { + EventPropagation::Continue + } + }) + .style(move |s| { + s.size_pct(100.0, 100.0).apply_if(dragging_over.get(), |s| { + s.background( + config + .get() + .color(LapceColor::EDITOR_DRAG_DROP_BACKGROUND), + ) + }) + }) + } + }; + + let resize_drag_view = { + let panel = panel.clone(); + let panel_size = panel.size; + move |position: PanelContainerPosition| { + panel.panel_info(); + let view = empty(); + let view_id = view.id(); + let drag_start: RwSignal> = create_rw_signal(None); + view.on_event_stop(EventListener::PointerDown, move |event| { + view_id.request_active(); + if let Event::PointerDown(pointer_event) = event { + drag_start.set(Some(pointer_event.pos)); + } + }) + .on_event_stop(EventListener::PointerMove, move |event| { + if let Event::PointerMove(pointer_event) = event { + if let Some(drag_start_point) = drag_start.get_untracked() { + let current_size = current_size.get_untracked(); + let available_size = available_size.get_untracked(); + match position { + PanelContainerPosition::Left => { + let new_size = current_size.width + + pointer_event.pos.x + - drag_start_point.x; + let current_panel_size = panel_size.get_untracked(); + let new_size = new_size + .max(150.0) + .min(available_size.width - 150.0 - 150.0); + if new_size != current_panel_size.left { + panel_size.update(|size| { + size.left = new_size; + size.right = size.right.min( + available_size.width - new_size - 150.0, + ) + }) + } + } + PanelContainerPosition::Bottom => { + let new_size = current_size.height + - (pointer_event.pos.y - drag_start_point.y); + let maximized = panel.panel_bottom_maximized(false); + if (maximized + && new_size < available_size.height - 50.0) + || (!maximized + && new_size > available_size.height - 50.0) + { + panel.toggle_bottom_maximize(); + } + + let new_size = new_size + .max(100.0) + .min(available_size.height - 100.0); + let current_size = + panel_size.with_untracked(|s| s.bottom); + if current_size != new_size { + panel_size.update(|size| { + size.bottom = new_size; + }) + } + } + PanelContainerPosition::Right => { + let new_size = current_size.width + - (pointer_event.pos.x - drag_start_point.x); + let current_panel_size = panel_size.get_untracked(); + let new_size = new_size + .max(150.0) + .min(available_size.width - 150.0 - 150.0); + if new_size != current_panel_size.right { + panel_size.update(|size| { + size.right = new_size; + size.left = size.left.min( + available_size.width - new_size - 150.0, + ) + }) + } + } + } + } + } + }) + .on_event_stop(EventListener::PointerUp, move |_| { + drag_start.set(None); + }) + .style(move |s| { + let is_dragging = drag_start.get().is_some(); + let current_size = current_size.get(); + let config = config.get(); + s.absolute() + .apply_if(position == PanelContainerPosition::Bottom, |s| { + s.width_pct(100.0).height(4.0).margin_top(-2.0) + }) + .apply_if(position == PanelContainerPosition::Left, |s| { + s.width(4.0) + .margin_left(current_size.width as f32 - 2.0) + .height_pct(100.0) + }) + .apply_if(position == PanelContainerPosition::Right, |s| { + s.width(4.0).margin_left(-2.0).height_pct(100.0) + }) + .apply_if(is_dragging, |s| { + s.background(config.color(LapceColor::EDITOR_CARET)) + .apply_if( + position == PanelContainerPosition::Bottom, + |s| s.cursor(CursorStyle::RowResize), + ) + .apply_if( + position != PanelContainerPosition::Bottom, + |s| s.cursor(CursorStyle::ColResize), + ) + .z_index(2) + }) + .hover(|s| { + s.background(config.color(LapceColor::EDITOR_CARET)) + .apply_if( + position == PanelContainerPosition::Bottom, + |s| s.cursor(CursorStyle::RowResize), + ) + .apply_if( + position != PanelContainerPosition::Bottom, + |s| s.cursor(CursorStyle::ColResize), + ) + .z_index(2) + }) + }) + } + }; + + let is_bottom = position.is_bottom(); + stack(( + panel_picker(window_tab_data.clone(), position.first()), + panel_view(window_tab_data.clone(), position.first()), + panel_view(window_tab_data.clone(), position.second()), + panel_picker(window_tab_data.clone(), position.second()), + resize_drag_view(position), + stack((drop_view(position.first()), drop_view(position.second()))).style( + move |s| { + let is_dragging_panel = is_dragging_panel(); + s.absolute() + .size_pct(100.0, 100.0) + .apply_if(!is_bottom, |s| s.flex_col()) + .apply_if(!is_dragging_panel, |s| s.pointer_events_none()) + }, + ), + )) + .on_resize(move |rect| { + let size = rect.size(); + if size != current_size.get_untracked() { + current_size.set(size); + } + }) + .style(move |s| { + let size = panel.size.with(|s| match position { + PanelContainerPosition::Left => s.left, + PanelContainerPosition::Bottom => s.bottom, + PanelContainerPosition::Right => s.right, + }); + let is_maximized = panel.panel_bottom_maximized(true); + let config = config.get(); + s.apply_if(!panel.is_container_shown(&position, true), |s| s.hide()) + .apply_if(position == PanelContainerPosition::Bottom, |s| { + s.width_pct(100.0) + .apply_if(!is_maximized, |s| { + s.border_top(1.0).height(size as f32) + }) + .apply_if(is_maximized, |s| s.flex_grow(1.0)) + }) + .apply_if(position == PanelContainerPosition::Left, |s| { + s.border_right(1.0) + .width(size as f32) + .height_pct(100.0) + .background(config.color(LapceColor::PANEL_BACKGROUND)) + }) + .apply_if(position == PanelContainerPosition::Right, |s| { + s.border_left(1.0) + .width(size as f32) + .height_pct(100.0) + .background(config.color(LapceColor::PANEL_BACKGROUND)) + }) + .apply_if(!is_bottom, |s| s.flex_col()) + .border_color(config.color(LapceColor::LAPCE_BORDER)) + .color(config.color(LapceColor::PANEL_FOREGROUND)) + }) + .debug_name(format!("{:?} Panel Container View", position)) +} + +fn panel_view( + window_tab_data: Rc, + position: PanelPosition, +) -> impl View { + let panel = window_tab_data.panel.clone(); + let panels = move || { + panel + .panels + .with(|p| p.get(&position).cloned().unwrap_or_default()) + }; + let active_fn = move || { + panel + .styles + .with(|s| s.get(&position).map(|s| s.active).unwrap_or(0)) + }; + tab( + active_fn, + panels, + |p| *p, + move |kind| { + let view = match kind { + PanelKind::Terminal => { + terminal_panel(window_tab_data.clone()).into_any() + } + PanelKind::FileExplorer => { + file_explorer_panel(window_tab_data.clone(), position).into_any() + } + PanelKind::SourceControl => { + source_control_panel(window_tab_data.clone(), position) + .into_any() + } + PanelKind::Plugin => { + plugin_panel(window_tab_data.clone(), position).into_any() + } + PanelKind::Search => { + global_search_panel(window_tab_data.clone(), position).into_any() + } + PanelKind::Problem => { + problem_panel(window_tab_data.clone(), position).into_any() + } + PanelKind::Debug => { + debug_panel(window_tab_data.clone(), position).into_any() + } + PanelKind::CallHierarchy => { + show_hierarchy_panel(window_tab_data.clone(), position) + .into_any() + } + PanelKind::DocumentSymbol => { + symbol_panel(window_tab_data.clone(), position).into_any() + } + PanelKind::References => { + references_panel(window_tab_data.clone(), position).into_any() + } + PanelKind::Implementation => { + implementation_panel(window_tab_data.clone(), position) + .into_any() + } + }; + view.style(|s| s.size_pct(100.0, 100.0)) + }, + ) + .style(move |s| { + s.size_pct(100.0, 100.0).apply_if( + !panel.is_position_shown(&position, true) + || panel.is_position_empty(&position, true), + |s| s.hide(), + ) + }) +} + +pub fn panel_header( + header: String, + config: ReadSignal>, +) -> impl View { + container(label(move || header.clone())).style(move |s| { + s.padding_horiz(10.0) + .padding_vert(6.0) + .width_pct(100.0) + .background(config.get().color(LapceColor::EDITOR_BACKGROUND)) + }) +} + +fn panel_picker( + window_tab_data: Rc, + position: PanelPosition, +) -> impl View { + let panel = window_tab_data.panel.clone(); + let panels = panel.panels; + let config = window_tab_data.common.config; + let dragging = window_tab_data.common.dragging; + let is_bottom = position.is_bottom(); + let is_first = position.is_first(); + dyn_stack( + move || { + panel + .panels + .with(|panels| panels.get(&position).cloned().unwrap_or_default()) + }, + |p| *p, + move |p| { + let window_tab_data = window_tab_data.clone(); + let tooltip = match p { + PanelKind::Terminal => "Terminal", + PanelKind::FileExplorer => "File Explorer", + PanelKind::SourceControl => "Source Control", + PanelKind::Plugin => "Plugins", + PanelKind::Search => "Search", + PanelKind::Problem => "Problems", + PanelKind::Debug => "Debug", + PanelKind::CallHierarchy => "Call Hierarchy", + PanelKind::DocumentSymbol => "Document Symbol", + PanelKind::References => "References", + PanelKind::Implementation => "Implementation", + }; + let icon = p.svg_name(); + let is_active = { + let window_tab_data = window_tab_data.clone(); + move || { + if let Some((active_panel, shown)) = window_tab_data + .panel + .active_panel_at_position(&position, true) + { + shown && active_panel == p + } else { + false + } + } + }; + container(stack(( + clickable_icon( + || icon, + move || { + window_tab_data.toggle_panel_visual(p); + }, + || false, + || false, + move || tooltip, + config, + ) + .draggable() + .on_event_stop(EventListener::DragStart, move |_| { + dragging.set(Some(DragContent::Panel(p))); + }) + .on_event_stop(EventListener::DragEnd, move |_| { + dragging.set(None); + }) + .dragging_style(move |s| { + let config = config.get(); + s.border(1.0) + .border_radius(6.0) + .border_color(config.color(LapceColor::LAPCE_BORDER)) + .padding(6.0) + .background( + config + .color(LapceColor::PANEL_BACKGROUND) + .multiply_alpha(0.7), + ) + }) + .style(|s| s.padding(1.0)), + label(|| "".to_string()).style(move |s| { + s.selectable(false) + .pointer_events_none() + .absolute() + .size_pct(100.0, 100.0) + .apply_if(!is_bottom && is_first, |s| s.margin_top(2.0)) + .apply_if(!is_bottom && !is_first, |s| s.margin_top(-2.0)) + .apply_if(is_bottom && is_first, |s| s.margin_left(-2.0)) + .apply_if(is_bottom && !is_first, |s| s.margin_left(2.0)) + .apply_if(is_active(), |s| { + s.apply_if(!is_bottom && is_first, |s| { + s.border_bottom(2.0) + }) + .apply_if(!is_bottom && !is_first, |s| s.border_top(2.0)) + .apply_if(is_bottom && is_first, |s| s.border_left(2.0)) + .apply_if(is_bottom && !is_first, |s| { + s.border_right(2.0) + }) + }) + .border_color( + config + .get() + .color(LapceColor::LAPCE_TAB_ACTIVE_UNDERLINE), + ) + }), + ))) + .style(|s| s.padding(6.0)) + }, + ) + .style(move |s| { + s.border_color(config.get().color(LapceColor::LAPCE_BORDER)) + .apply_if( + panels.with(|p| { + p.get(&position).map(|p| p.is_empty()).unwrap_or(true) + }), + |s| s.hide(), + ) + .apply_if(is_bottom, |s| s.flex_col()) + .apply_if(is_bottom && is_first, |s| s.border_right(1.0)) + .apply_if(is_bottom && !is_first, |s| s.border_left(1.0)) + .apply_if(!is_bottom && is_first, |s| s.border_bottom(1.0)) + .apply_if(!is_bottom && !is_first, |s| s.border_top(1.0)) + }) +} diff --git a/lapce-app/src/plugin.rs b/lapce-app/src/plugin.rs new file mode 100644 index 0000000..c229528 --- /dev/null +++ b/lapce-app/src/plugin.rs @@ -0,0 +1,1023 @@ +use std::{ + collections::HashSet, + rc::Rc, + sync::{Arc, atomic::AtomicU64}, +}; + +use anyhow::Result; +use floem::{ + IntoView, View, + action::show_context_menu, + ext_event::create_ext_action, + keyboard::Modifiers, + kurbo::Rect, + menu::{Menu, MenuItem}, + reactive::{ + RwSignal, Scope, SignalGet, SignalUpdate, SignalWith, create_effect, + create_memo, create_rw_signal, use_context, + }, + style::CursorStyle, + views::{ + Decorators, container, dyn_container, dyn_stack, empty, img, label, + rich_text, scroll, stack, svg, text, + }, +}; +use indexmap::IndexMap; +use lapce_core::{command::EditCommand, directory::Directory, mode::Mode}; +use lapce_proxy::plugin::{download_volt, volt_icon, wasi::find_all_volts}; +use lapce_rpc::{ + core::{CoreNotification, CoreRpcHandler}, + plugin::{VoltID, VoltInfo, VoltMetadata}, +}; +use lsp_types::MessageType; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; + +use crate::{ + command::{CommandExecuted, CommandKind}, + config::{LapceConfig, color::LapceColor}, + db::LapceDb, + editor::EditorData, + keypress::{KeyPressFocus, condition::Condition}, + main_split::Editors, + markdown::{MarkdownContent, parse_markdown}, + panel::plugin_view::VOLT_DEFAULT_PNG, + web_link::web_link, + window_tab::CommonData, +}; + +type PluginInfo = Option<( + Option, + VoltInfo, + Option, + Option, + Option>, +)>; + +#[derive(Clone, PartialEq, Eq)] +pub enum VoltIcon { + Svg(String), + Img(Vec), +} + +impl VoltIcon { + pub fn from_bytes(buf: &[u8]) -> Result { + if let Ok(s) = std::str::from_utf8(buf) { + Ok(VoltIcon::Svg(s.to_string())) + } else { + Ok(VoltIcon::Img(buf.to_vec())) + } + } +} + +#[derive(Deserialize, Serialize)] +pub struct VoltsInfo { + pub plugins: Vec, + pub total: usize, +} + +#[derive(Clone)] +pub struct InstalledVoltData { + pub meta: RwSignal, + pub icon: RwSignal>, + pub latest: RwSignal, +} + +#[derive(Clone, PartialEq)] +pub struct AvailableVoltData { + pub info: RwSignal, + pub icon: RwSignal>, + pub installing: RwSignal, +} + +#[derive(Clone, Debug)] +pub struct AvailableVoltList { + pub loading: RwSignal, + pub query_id: RwSignal, + pub query_editor: EditorData, + pub volts: RwSignal>, + pub total: RwSignal, +} + +#[derive(Clone, Debug)] +pub struct PluginData { + pub installed: RwSignal>, + pub available: AvailableVoltList, + pub all: RwSignal>, + pub disabled: RwSignal>, + pub workspace_disabled: RwSignal>, + pub common: Rc, +} + +impl KeyPressFocus for PluginData { + fn get_mode(&self) -> Mode { + Mode::Insert + } + + fn check_condition(&self, condition: Condition) -> bool { + matches!(condition, Condition::PanelFocus) + } + + fn run_command( + &self, + command: &crate::command::LapceCommand, + count: Option, + mods: Modifiers, + ) -> CommandExecuted { + match &command.kind { + CommandKind::Workbench(_) => {} + CommandKind::Scroll(_) => {} + CommandKind::Focus(_) => {} + CommandKind::Edit(_) + | CommandKind::Move(_) + | CommandKind::MultiSelection(_) => { + #[allow(clippy::single_match)] + match command.kind { + CommandKind::Edit(EditCommand::InsertNewLine) => { + return CommandExecuted::Yes; + } + _ => {} + } + + return self + .available + .query_editor + .run_command(command, count, mods); + } + CommandKind::MotionMode(_) => {} + } + CommandExecuted::No + } + + fn receive_char(&self, c: &str) { + self.available.query_editor.receive_char(c); + } +} + +impl PluginData { + pub fn new( + cx: Scope, + disabled: HashSet, + workspace_disabled: HashSet, + editors: Editors, + common: Rc, + core_rpc: CoreRpcHandler, + ) -> Self { + let installed = cx.create_rw_signal(IndexMap::new()); + let available = AvailableVoltList { + loading: cx.create_rw_signal(false), + volts: cx.create_rw_signal(IndexMap::new()), + total: cx.create_rw_signal(0), + query_id: cx.create_rw_signal(0), + query_editor: editors.make_local(cx, common.clone()), + }; + let disabled = cx.create_rw_signal(disabled); + let workspace_disabled = cx.create_rw_signal(workspace_disabled); + + let plugin = Self { + installed, + available, + all: cx.create_rw_signal(im::HashMap::new()), + disabled, + workspace_disabled, + common, + }; + + plugin.load_available_volts("", 0, core_rpc.clone()); + + { + let plugin = plugin.clone(); + let extra_plugin_paths = + plugin.common.window_common.extra_plugin_paths.clone(); + + let send = create_ext_action( + cx, + move |volts: Vec<(Option>, VoltMetadata)>| { + for (icon, meta) in volts { + plugin.volt_installed(&meta, &icon); + } + }, + ); + std::thread::Builder::new() + .name("FindAllVolts".to_owned()) + .spawn(move || { + let volts = find_all_volts(&extra_plugin_paths); + let volts = volts + .into_iter() + .filter_map(|meta| { + if meta.wasm.is_none() { + Some((volt_icon(&meta), meta)) + } else { + None + } + }) + .collect::>(); + send(volts); + }) + .unwrap(); + } + + { + let plugin = plugin.clone(); + cx.create_effect(move |s| { + let query = plugin + .available + .query_editor + .doc_signal() + .get() + .buffer + .with(|buffer| buffer.to_string()); + if s.as_ref() == Some(&query) { + return query; + } + plugin.available.query_id.update(|id| *id += 1); + plugin.available.loading.set(false); + plugin.available.volts.update(|v| v.clear()); + plugin.load_available_volts(&query, 0, core_rpc.clone()); + query + }); + } + + plugin + } + + pub fn volt_installed(&self, volt: &VoltMetadata, icon: &Option>) { + let volt_id = volt.id(); + let (existing, is_latest, volt_data) = self + .installed + .try_update(|installed| { + if let Some(v) = installed.get(&volt_id) { + (true, true, v.to_owned()) + } else { + let (info, is_latest) = if let Some(volt) = self + .available + .volts + .with_untracked(|all| all.get(&volt_id).cloned()) + { + (volt.info.get_untracked(), true) + } else { + (volt.info(), false) + }; + + let latest = self.common.scope.create_rw_signal(info); + let data = InstalledVoltData { + meta: self.common.scope.create_rw_signal(volt.clone()), + icon: self.common.scope.create_rw_signal( + icon.as_ref() + .and_then(|icon| VoltIcon::from_bytes(icon).ok()), + ), + latest, + }; + installed.insert(volt_id, data.clone()); + + (false, is_latest, data) + } + }) + .unwrap(); + + if existing { + volt_data.meta.set(volt.clone()); + volt_data.icon.set( + icon.as_ref() + .and_then(|icon| VoltIcon::from_bytes(icon).ok()), + ); + } + + let latest = volt_data.latest; + if !is_latest { + let url = format!( + "https://plugins.lapce.dev/api/v1/plugins/{}/{}/latest", + volt.author, volt.name + ); + let send = create_ext_action(self.common.scope, move |info| { + if let Some(info) = info { + latest.set(info); + } + }); + std::thread::spawn(move || { + let info: Option = lapce_proxy::get_url(url, None) + .ok() + .and_then(|r| r.json().ok()); + send(info); + }); + } + } + + pub fn volt_removed(&self, volt: &VoltInfo) { + let id = volt.id(); + self.installed.update(|installed| { + installed.swap_remove(&id); + }); + + if self.disabled.with_untracked(|d| d.contains(&id)) { + self.disabled.update(|d| { + d.remove(&id); + }); + let db: Arc = use_context().unwrap(); + db.save_disabled_volts( + self.disabled.get_untracked().into_iter().collect(), + ); + } + + if self.workspace_disabled.with_untracked(|d| d.contains(&id)) { + self.workspace_disabled.update(|d| { + d.remove(&id); + }); + let db: Arc = use_context().unwrap(); + db.save_workspace_disabled_volts( + self.common.workspace.clone(), + self.workspace_disabled + .get_untracked() + .into_iter() + .collect(), + ); + } + } + + fn load_available_volts( + &self, + query: &str, + offset: usize, + core_rpc: CoreRpcHandler, + ) { + if self.available.loading.get_untracked() { + return; + } + self.available.loading.set(true); + + let volts = self.available.volts; + let volts_total = self.available.total; + let cx = self.common.scope; + let loading = self.available.loading; + let query_id = self.available.query_id; + let current_query_id = self.available.query_id.get_untracked(); + let all = self.all; + let send = + create_ext_action(self.common.scope, move |new: Result| { + loading.set(false); + if query_id.get_untracked() != current_query_id { + return; + } + + match new { + Ok(new) => { + volts.update(|volts| { + volts.extend(new.plugins.into_iter().map(|volt| { + let icon = cx.create_rw_signal(None); + let send = create_ext_action(cx, move |result| { + if let Ok(i) = result { + icon.set(Some(i)); + } + }); + { + let volt = volt.clone(); + std::thread::spawn(move || { + let result = Self::load_icon(&volt); + send(result); + }); + } + + let data = AvailableVoltData { + info: cx.create_rw_signal(volt.clone()), + icon, + installing: cx.create_rw_signal(false), + }; + all.update(|all| { + all.insert(volt.id(), data.clone()); + }); + + (volt.id(), data) + })); + }); + volts_total.set(new.total); + } + Err(err) => { + tracing::error!("{:?}", err); + core_rpc.notification(CoreNotification::ShowMessage { + title: "Request Available Plugins".to_string(), + message: lsp_types::ShowMessageParams { + typ: MessageType::ERROR, + message: err.to_string(), + }, + }); + } + } + }); + + let query = query.to_string(); + std::thread::spawn(move || { + let volts = Self::query_volts(&query, offset); + send(volts); + }); + } + + fn load_icon(volt: &VoltInfo) -> Result { + let url = format!( + "https://plugins.lapce.dev/api/v1/plugins/{}/{}/{}/icon?id={}", + volt.author, volt.name, volt.version, volt.updated_at_ts + ); + + let cache_file_path = Directory::cache_directory().map(|cache_dir| { + let mut hasher = Sha256::new(); + hasher.update(url.as_bytes()); + let filename = format!("{:x}", hasher.finalize()); + cache_dir.join(filename) + }); + + let cache_content = + cache_file_path.as_ref().and_then(|p| std::fs::read(p).ok()); + + let content = match cache_content { + Some(content) => content, + None => { + let resp = lapce_proxy::get_url(&url, None)?; + if !resp.status().is_success() { + return Err(anyhow::anyhow!("can't download icon")); + } + let buf = resp.bytes()?.to_vec(); + + if let Some(path) = cache_file_path.as_ref() { + if let Err(err) = std::fs::write(path, &buf) { + tracing::error!("{:?}", err); + } + } + + buf + } + }; + + VoltIcon::from_bytes(&content) + } + + fn download_readme( + volt: &VoltInfo, + config: &LapceConfig, + ) -> Result> { + let url = format!( + "https://plugins.lapce.dev/api/v1/plugins/{}/{}/{}/readme", + volt.author, volt.name, volt.version + ); + let resp = lapce_proxy::get_url(&url, None)?; + if resp.status() != 200 { + let text = parse_markdown("Plugin doesn't have a README", 2.0, config); + return Ok(text); + } + let text = resp.text()?; + let text = parse_markdown(&text, 2.0, config); + Ok(text) + } + + fn query_volts(query: &str, offset: usize) -> Result { + let url = format!( + "https://plugins.lapce.dev/api/v1/plugins?q={query}&offset={offset}" + ); + let plugins: VoltsInfo = lapce_proxy::get_url(url, None)?.json()?; + Ok(plugins) + } + + fn all_loaded(&self) -> bool { + self.available.volts.with_untracked(|v| v.len()) + >= self.available.total.get_untracked() + } + + pub fn load_more_available(&self, core_rpc: CoreRpcHandler) { + if self.all_loaded() { + return; + } + + let query = self + .available + .query_editor + .doc() + .buffer + .with_untracked(|buffer| buffer.to_string()); + let offset = self.available.volts.with_untracked(|v| v.len()); + self.load_available_volts(&query, offset, core_rpc); + } + + pub fn install_volt(&self, info: VoltInfo) { + self.available.volts.with_untracked(|volts| { + if let Some(volt) = volts.get(&info.id()) { + volt.installing.set(true); + }; + }); + if info.wasm { + self.common.proxy.install_volt(info); + } else { + let plugin = self.clone(); + let send = create_ext_action(self.common.scope, move |result| { + if let Ok((meta, icon)) = result { + plugin.volt_installed(&meta, &icon); + } + }); + std::thread::spawn(move || { + let download = || -> Result<(VoltMetadata, Option>)> { + let download_volt_result = download_volt(&info); + let meta = download_volt_result?; + let icon = volt_icon(&meta); + Ok((meta, icon)) + }; + send(download()); + }); + } + } + + pub fn plugin_disabled(&self, id: &VoltID) -> bool { + self.disabled.with_untracked(|d| d.contains(id)) + || self.workspace_disabled.with_untracked(|d| d.contains(id)) + } + + pub fn enable_volt(&self, volt: VoltInfo) { + let id = volt.id(); + self.disabled.update(|d| { + d.remove(&id); + }); + if !self.plugin_disabled(&id) { + self.common.proxy.enable_volt(volt); + } + let db: Arc = use_context().unwrap(); + db.save_disabled_volts(self.disabled.get_untracked().into_iter().collect()); + } + + pub fn disable_volt(&self, volt: VoltInfo) { + let id = volt.id(); + self.disabled.update(|d| { + d.insert(id); + }); + self.common.proxy.disable_volt(volt); + let db: Arc = use_context().unwrap(); + db.save_disabled_volts(self.disabled.get_untracked().into_iter().collect()); + } + + pub fn enable_volt_for_ws(&self, volt: VoltInfo) { + let id = volt.id(); + self.workspace_disabled.update(|d| { + d.remove(&id); + }); + if !self.plugin_disabled(&id) { + self.common.proxy.enable_volt(volt); + } + let db: Arc = use_context().unwrap(); + db.save_workspace_disabled_volts( + self.common.workspace.clone(), + self.disabled.get_untracked().into_iter().collect(), + ); + } + + pub fn disable_volt_for_ws(&self, volt: VoltInfo) { + let id = volt.id(); + self.workspace_disabled.update(|d| { + d.insert(id); + }); + self.common.proxy.disable_volt(volt); + let db: Arc = use_context().unwrap(); + db.save_workspace_disabled_volts( + self.common.workspace.clone(), + self.disabled.get_untracked().into_iter().collect(), + ); + } + + pub fn uninstall_volt(&self, volt: VoltMetadata) { + if volt.wasm.is_some() { + self.common.proxy.remove_volt(volt); + } else { + let plugin = self.clone(); + let info = volt.info(); + let send = + create_ext_action(self.common.scope, move |result: Result<()>| { + if let Ok(()) = result { + plugin.volt_removed(&info); + } + }); + std::thread::spawn(move || { + let uninstall = || -> Result<()> { + let path = volt + .dir + .as_ref() + .ok_or_else(|| anyhow::anyhow!("don't have dir"))?; + std::fs::remove_dir_all(path)?; + Ok(()) + }; + send(uninstall()); + }); + } + } + + pub fn reload_volt(&self, volt: VoltMetadata) { + self.common.proxy.reload_volt(volt); + } + + pub fn plugin_controls(&self, meta: VoltMetadata, latest: VoltInfo) -> Menu { + let volt_id = meta.id(); + let mut menu = Menu::new(""); + if meta.version != latest.version { + menu = menu + .entry(MenuItem::new("Upgrade Plugin").action({ + let plugin = self.clone(); + let info = latest.clone(); + move || { + plugin.install_volt(info.clone()); + } + })) + .separator(); + } + menu = menu + .entry(MenuItem::new("Reload Plugin").action({ + let plugin = self.clone(); + let meta = meta.clone(); + move || { + plugin.reload_volt(meta.clone()); + } + })) + .separator() + .entry( + MenuItem::new("Enable") + .enabled( + self.disabled + .with_untracked(|disabled| disabled.contains(&volt_id)), + ) + .action({ + let plugin = self.clone(); + let volt = meta.info(); + move || { + plugin.enable_volt(volt.clone()); + } + }), + ) + .entry( + MenuItem::new("Disable") + .enabled( + self.disabled + .with_untracked(|disabled| !disabled.contains(&volt_id)), + ) + .action({ + let plugin = self.clone(); + let volt = meta.info(); + move || { + plugin.disable_volt(volt.clone()); + } + }), + ) + .separator() + .entry( + MenuItem::new("Enable For Workspace") + .enabled( + self.workspace_disabled + .with_untracked(|disabled| disabled.contains(&volt_id)), + ) + .action({ + let plugin = self.clone(); + let volt = meta.info(); + move || { + plugin.enable_volt_for_ws(volt.clone()); + } + }), + ) + .entry( + MenuItem::new("Disable For Workspace") + .enabled( + self.workspace_disabled + .with_untracked(|disabled| !disabled.contains(&volt_id)), + ) + .action({ + let plugin = self.clone(); + let volt = meta.info(); + move || { + plugin.disable_volt_for_ws(volt.clone()); + } + }), + ) + .separator() + .entry(MenuItem::new("Uninstall").action({ + let plugin = self.clone(); + move || { + plugin.uninstall_volt(meta.clone()); + } + })); + menu + } +} + +pub fn plugin_info_view(plugin: PluginData, volt: VoltID) -> impl View { + let config = plugin.common.config; + let header_rect = create_rw_signal(Rect::ZERO); + let scroll_width: RwSignal = create_rw_signal(0.0); + let internal_command = plugin.common.internal_command; + let local_plugin = plugin.clone(); + let plugin_info = create_memo(move |_| { + plugin + .installed + .with(|volts| { + volts.get(&volt).map(|v| { + ( + Some(v.meta.get()), + v.meta.get().info(), + v.icon.get(), + Some(v.latest.get()), + None, + ) + }) + }) + .or_else(|| { + plugin.all.with(|volts| { + volts.get(&volt).map(|v| { + (None, v.info.get(), v.icon.get(), None, Some(v.installing)) + }) + }) + }) + }); + + let version_view = move |plugin: PluginData, plugin_info: PluginInfo| { + let version_info = plugin_info.as_ref().map(|(_, volt, _, latest, _)| { + ( + volt.version.clone(), + latest.as_ref().map(|i| i.version.clone()), + ) + }); + let installing = plugin_info + .as_ref() + .and_then(|(_, _, _, _, installing)| *installing); + let local_version_info = version_info.clone(); + let control = { + move |version_info: Option<(String, Option)>| match version_info + .as_ref() + .map(|(v, l)| match l { + Some(l) => (true, l == v), + None => (false, false), + }) { + Some((true, true)) => "Installed ▼", + Some((true, false)) => "Upgrade ▼", + _ => { + if installing.map(|i| i.get()).unwrap_or(false) { + "Installing" + } else { + "Install" + } + } + } + }; + let local_plugin_info = plugin_info.clone(); + let local_plugin = plugin.clone(); + stack(( + text( + version_info + .as_ref() + .map(|(v, _)| format!("v{v}")) + .unwrap_or_default(), + ), + label(move || control(local_version_info.clone())) + .style(move |s| { + let config = config.get(); + s.margin_left(10) + .padding_horiz(10) + .border_radius(6.0) + .color( + config + .color(LapceColor::LAPCE_BUTTON_PRIMARY_FOREGROUND), + ) + .background( + config + .color(LapceColor::LAPCE_BUTTON_PRIMARY_BACKGROUND), + ) + .hover(|s| { + s.cursor(CursorStyle::Pointer).background( + config + .color( + LapceColor::LAPCE_BUTTON_PRIMARY_BACKGROUND, + ) + .multiply_alpha(0.8), + ) + }) + .active(|s| { + s.background( + config + .color( + LapceColor::LAPCE_BUTTON_PRIMARY_BACKGROUND, + ) + .multiply_alpha(0.6), + ) + }) + .disabled(|s| { + s.background(config.color(LapceColor::EDITOR_DIM)) + }) + .selectable(false) + }) + .disabled(move || installing.map(|i| i.get()).unwrap_or(false)) + .on_click_stop(move |_| { + if let Some((meta, info, _, latest, _)) = + local_plugin_info.as_ref() + { + if let Some(meta) = meta { + let menu = local_plugin.plugin_controls( + meta.to_owned(), + latest.clone().unwrap_or_else(|| info.to_owned()), + ); + show_context_menu(menu, None); + } else { + local_plugin.install_volt(info.to_owned()); + } + } + }), + )) + }; + + scroll( + dyn_container( + move || plugin_info.get(), + move |plugin_info| { + stack(( + stack(( + match plugin_info + .as_ref() + .and_then(|(_, _, icon, _, _)| icon.clone()) + { + None => container( + img(move || VOLT_DEFAULT_PNG.to_vec()) + .style(|s| s.size_full()), + ), + Some(VoltIcon::Svg(svg_str)) => container( + svg(move || svg_str.clone()) + .style(|s| s.size_full()), + ), + Some(VoltIcon::Img(buf)) => container( + img(move || buf.clone()).style(|s| s.size_full()), + ), + } + .style(|s| { + s.min_size(150.0, 150.0).size(150.0, 150.0).padding(20) + }), + stack(( + text( + plugin_info + .as_ref() + .map(|(_, volt, _, _, _)| { + volt.display_name.as_str() + }) + .unwrap_or(""), + ) + .style(move |s| { + s.font_bold().font_size( + (config.get().ui.font_size() as f32 * 1.6) + .round(), + ) + }), + text( + plugin_info + .as_ref() + .map(|(_, volt, _, _, _)| { + volt.description.as_str() + }) + .unwrap_or(""), + ) + .style(move |s| { + let scroll_width = scroll_width.get(); + s.max_width( + scroll_width + .clamp(200.0 + 60.0 * 2.0 + 200.0, 800.0) + - 60.0 * 2.0 + - 200.0, + ) + }), + { + let repo = plugin_info + .as_ref() + .and_then(|(_, volt, _, _, _)| { + volt.repository.as_deref() + }) + .unwrap_or("") + .to_string(); + let local_repo = repo.clone(); + stack(( + text("Repository: "), + web_link( + move || repo.clone(), + move || local_repo.clone(), + move || { + config + .get() + .color(LapceColor::EDITOR_LINK) + }, + internal_command, + ), + )) + }, + text( + plugin_info + .as_ref() + .map(|(_, volt, _, _, _)| volt.author.as_str()) + .unwrap_or(""), + ) + .style(move |s| { + s.color(config.get().color(LapceColor::EDITOR_DIM)) + }), + version_view(local_plugin.clone(), plugin_info.clone()), + )) + .style(|s| s.flex_col().line_height(1.6)), + )) + .style(|s| s.absolute()) + .on_resize(move |rect| { + if header_rect.get_untracked() != rect { + header_rect.set(rect); + } + }), + empty().style(move |s| { + let rect = header_rect.get(); + s.size(rect.width(), rect.height()).pointer_events_none() + }), + empty().style(move |s| { + s.margin_vert(6) + .height(1) + .width_full() + .background(config.get().color(LapceColor::LAPCE_BORDER)) + }), + { + let readme = create_rw_signal(None); + let info = plugin_info + .as_ref() + .map(|(_, info, _, _, _)| info.to_owned()); + create_effect(move |_| { + let config = config.get(); + let info = info.clone(); + if let Some(info) = info { + let cx = Scope::current(); + let send = create_ext_action(cx, move |result| { + if let Ok(md) = result { + readme.set(Some(md)); + } + }); + std::thread::spawn(move || { + let result = + PluginData::download_readme(&info, &config); + send(result); + }); + } + }); + { + let id = AtomicU64::new(0); + dyn_stack( + move || { + readme.get().unwrap_or_else(|| { + parse_markdown( + "Loading README", + 2.0, + &config.get(), + ) + }) + }, + move |_| { + id.fetch_add( + 1, + std::sync::atomic::Ordering::Relaxed, + ) + }, + move |content| match content { + MarkdownContent::Text(text_layout) => container( + rich_text(move || text_layout.clone()) + .style(|s| s.width_full()), + ) + .style(|s| s.width_full()), + MarkdownContent::Image { .. } => { + container(empty()) + } + MarkdownContent::Separator => { + container(empty().style(move |s| { + s.width_full() + .margin_vert(5.0) + .height(1.0) + .background( + config.get().color( + LapceColor::LAPCE_BORDER, + ), + ) + })) + } + }, + ) + .style(|s| s.flex_col().width_full()) + } + }, + )) + .style(move |s| { + let padding = 60.0; + s.flex_col() + .width( + scroll_width + .get() + .min(800.0) + .max(header_rect.get().width() + padding * 2.0), + ) + .padding(padding) + }) + .into_any() + }, + ) + .style(|s| s.min_width_full().justify_center()), + ) + .on_resize(move |rect| { + if scroll_width.get_untracked() != rect.width() { + scroll_width.set(rect.width()); + } + }) + .style(|s| s.absolute().size_full()) + .debug_name("Plugin Info") +} diff --git a/lapce-app/src/proxy.rs b/lapce-app/src/proxy.rs new file mode 100644 index 0000000..c417224 --- /dev/null +++ b/lapce-app/src/proxy.rs @@ -0,0 +1,171 @@ +use std::{ + collections::HashMap, + path::PathBuf, + process::Command, + sync::{Arc, mpsc::Sender}, +}; + +use floem::{ext_event::create_signal_from_channel, reactive::ReadSignal}; +use lapce_proxy::dispatch::Dispatcher; +use lapce_rpc::{ + core::{CoreHandler, CoreNotification, CoreRpcHandler}, + plugin::VoltID, + proxy::{ProxyRpcHandler, ProxyStatus}, + terminal::TermId, +}; +use tracing::error; + +use self::{remote::start_remote, ssh::SshRemote}; +use crate::{ + terminal::event::TermEvent, + workspace::{LapceWorkspace, LapceWorkspaceType}, +}; + +mod remote; +mod ssh; +#[cfg(windows)] +mod wsl; + +pub struct Proxy { + pub tx: Sender, + pub term_tx: Sender<(TermId, TermEvent)>, +} + +#[derive(Clone)] +pub struct ProxyData { + pub proxy_rpc: ProxyRpcHandler, + pub core_rpc: CoreRpcHandler, + pub notification: ReadSignal>, +} + +impl ProxyData { + pub fn shutdown(&self) { + self.proxy_rpc.shutdown(); + self.core_rpc.shutdown(); + } +} + +pub fn new_proxy( + workspace: Arc, + disabled_volts: Vec, + extra_plugin_paths: Vec, + plugin_configurations: HashMap>, + term_tx: Sender<(TermId, TermEvent)>, +) -> ProxyData { + let proxy_rpc = ProxyRpcHandler::new(); + let core_rpc = CoreRpcHandler::new(); + + { + let core_rpc = core_rpc.clone(); + let proxy_rpc = proxy_rpc.clone(); + std::thread::Builder::new() + .name("ProxyRpcHandler".to_owned()) + .spawn(move || { + core_rpc.notification(CoreNotification::ProxyStatus { + status: ProxyStatus::Connecting, + }); + proxy_rpc.initialize( + workspace.path.clone(), + disabled_volts, + extra_plugin_paths, + plugin_configurations, + 1, + 1, + ); + + match &workspace.kind { + LapceWorkspaceType::Local => { + let core_rpc = core_rpc.clone(); + let proxy_rpc = proxy_rpc.clone(); + let mut dispatcher = Dispatcher::new(core_rpc, proxy_rpc); + let proxy_rpc = dispatcher.proxy_rpc.clone(); + proxy_rpc.mainloop(&mut dispatcher); + } + LapceWorkspaceType::RemoteSSH(remote) => { + if let Err(e) = start_remote( + SshRemote { + ssh: remote.clone(), + }, + core_rpc.clone(), + proxy_rpc.clone(), + ) { + error!("Failed to start SSH remote: {e}"); + } + } + #[cfg(windows)] + LapceWorkspaceType::RemoteWSL(remote) => { + if let Err(e) = start_remote( + wsl::WslRemote { + wsl: remote.clone(), + }, + core_rpc.clone(), + proxy_rpc.clone(), + ) { + error!("Failed to start SSH remote: {e}"); + } + } + } + core_rpc.notification(CoreNotification::ProxyStatus { + status: ProxyStatus::Disconnected, + }); + }) + .unwrap(); + } + + let (tx, rx) = std::sync::mpsc::channel(); + { + let core_rpc = core_rpc.clone(); + std::thread::Builder::new() + .name("CoreRpcHandler".to_owned()) + .spawn(move || { + let mut proxy = Proxy { tx, term_tx }; + core_rpc.mainloop(&mut proxy); + core_rpc.notification(CoreNotification::ProxyStatus { + status: ProxyStatus::Disconnected, + }); + }) + .unwrap() + }; + + let notification = create_signal_from_channel(rx); + + ProxyData { + proxy_rpc, + core_rpc, + notification, + } +} + +impl CoreHandler for Proxy { + fn handle_notification(&mut self, rpc: lapce_rpc::core::CoreNotification) { + if let CoreNotification::UpdateTerminal { term_id, content } = &rpc { + if let Err(err) = self + .term_tx + .send((*term_id, TermEvent::UpdateContent(content.to_vec()))) + { + tracing::error!("{:?}", err); + } + return; + } + if let Err(err) = self.tx.send(rpc) { + tracing::error!("{:?}", err); + } + } + + fn handle_request( + &mut self, + _id: lapce_rpc::RequestId, + _rpc: lapce_rpc::core::CoreRequest, + ) { + } +} + +pub fn new_command(program: &str) -> Command { + #[allow(unused_mut)] + let mut cmd = Command::new(program); + #[cfg(target_os = "windows")] + use std::os::windows::process::CommandExt; + #[cfg(target_os = "windows")] + cmd.creation_flags(0x08000000); + cmd +} diff --git a/lapce-app/src/proxy/remote.rs b/lapce-app/src/proxy/remote.rs new file mode 100644 index 0000000..be9e64c --- /dev/null +++ b/lapce-app/src/proxy/remote.rs @@ -0,0 +1,497 @@ +use std::{ + io::{BufReader, Write}, + path::Path, + process::{Command, Stdio}, +}; + +use anyhow::{Result, anyhow}; +use flate2::read::GzDecoder; +use lapce_core::{ + directory::Directory, + meta::{self, ReleaseType}, +}; +use lapce_rpc::{ + RpcMessage, + core::CoreRpcHandler, + proxy::{ProxyRpc, ProxyRpcHandler}, + stdio_transport, +}; +use thiserror::Error; +use tracing::{debug, error}; + +const UNIX_PROXY_SCRIPT: &[u8] = include_bytes!("../../../extra/proxy.sh"); +const WINDOWS_PROXY_SCRIPT: &[u8] = include_bytes!("../../../extra/proxy.ps1"); + +#[derive(Clone, Copy, Error, Debug, PartialEq, Eq, strum_macros::Display)] +#[strum(ascii_case_insensitive)] +enum HostPlatform { + UnknownOS, + #[strum(serialize = "windows")] + Windows, + #[strum(serialize = "linux")] + Linux, + #[strum(serialize = "darwin")] + Darwin, + #[strum(serialize = "bsd")] + Bsd, +} + +/// serialise via strum to arch name that is used +/// in CI artefacts +#[derive(Clone, Copy, Error, Debug, PartialEq, Eq, strum_macros::Display)] +#[strum(ascii_case_insensitive)] +enum HostArchitecture { + UnknownArch, + #[strum(serialize = "x86_64")] + AMD64, + #[strum(serialize = "x86")] + X86, + #[strum(serialize = "aarch64")] + ARM64, + #[strum(serialize = "armv7")] + ARM32v7, + #[strum(serialize = "armhf")] + ARM32v6, +} + +pub trait Remote: Sized { + #[allow(unused)] + fn home_dir(&self) -> Result { + let cmd = self + .command_builder() + .arg("echo") + .arg("-n") + .arg("$HOME") + .stdout(Stdio::piped()) + .output()?; + + Ok(String::from_utf8(cmd.stdout)?) + } + + fn upload_file(&self, local: impl AsRef, remote: &str) -> Result<()>; + + fn command_builder(&self) -> Command; +} + +pub fn start_remote( + remote: impl Remote, + core_rpc: CoreRpcHandler, + proxy_rpc: ProxyRpcHandler, +) -> Result<()> { + // Note about platforms: + // Windows can use either cmd.exe, powershell.exe or pwsh.exe as + // SSH shell, syntax logic varies significantly that's why we bet on + // cmd.exe as it doesn't add unwanted newlines and use powershell only + // for proxy install + // + // Unix-like systems due to POSIX, always have /bin/sh which should not + // be necessary to use explicitly most of the time, as many wide-spread + // shells retain similar syntax, although shells like Nushell might not + // work (hopefully no one uses it as login shell) + use HostPlatform::*; + let (platform, architecture) = host_specification(&remote).unwrap(); + + if platform == UnknownOS || architecture == HostArchitecture::UnknownArch { + error!("detected remote host: {platform}/{architecture}"); + return Err(anyhow!("Unknown OS and/or architecture")); + } + + // ! Below paths have to be synced with what is + // ! returned by Config::proxy_directory() + let remote_proxy_path = match platform { + Windows => format!( + "%HOMEDRIVE%%HOMEPATH%\\AppData\\Local\\lapce\\{}\\data\\proxy", + meta::NAME + ), + Darwin => format!( + "~/Library/Application\\ Support/dev.lapce.{}/proxy", + meta::NAME + ), + _ => { + format!("~/.local/share/{}/proxy", meta::NAME.to_lowercase()) + } + }; + + let remote_proxy_file = match platform { + Windows => format!("{remote_proxy_path}\\lapce.exe"), + _ => format!("{remote_proxy_path}/lapce"), + }; + + if !remote + .command_builder() + .args([&remote_proxy_file, "--version"]) + .output() + .map(|output| { + if meta::RELEASE == ReleaseType::Debug { + String::from_utf8_lossy(&output.stdout).starts_with("Lapce-proxy") + } else { + String::from_utf8_lossy(&output.stdout).trim() + == format!("Lapce-proxy {}", meta::VERSION) + } + }) + .unwrap_or(false) + { + download_remote( + &remote, + &platform, + &architecture, + &remote_proxy_path, + &remote_proxy_file, + )?; + }; + + debug!("remote proxy path: {remote_proxy_path}"); + + let mut child = match platform { + // Force cmd.exe usage to resolve %envvar% variables + Windows => remote + .command_builder() + .args(["cmd", "/c"]) + .arg(&remote_proxy_file) + .arg("--proxy") + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn()?, + _ => remote + .command_builder() + .arg(&remote_proxy_file) + .arg("--proxy") + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn()?, + }; + let stdin = child + .stdin + .take() + .ok_or_else(|| anyhow!("can't find stdin"))?; + let stdout = BufReader::new( + child + .stdout + .take() + .ok_or_else(|| anyhow!("can't find stdout"))?, + ); + debug!("process id: {}", child.id()); + + let (writer_tx, writer_rx) = crossbeam_channel::unbounded(); + let (reader_tx, reader_rx) = crossbeam_channel::unbounded(); + stdio_transport(stdin, writer_rx, stdout, reader_tx); + + let local_proxy_rpc = proxy_rpc.clone(); + let local_writer_tx = writer_tx.clone(); + std::thread::Builder::new() + .name("ProxyRpcHandler".to_owned()) + .spawn(move || { + for msg in local_proxy_rpc.rx() { + match msg { + ProxyRpc::Request(id, rpc) => { + if let Err(err) = + local_writer_tx.send(RpcMessage::Request(id, rpc)) + { + tracing::error!("{:?}", err); + } + } + ProxyRpc::Notification(rpc) => { + if let Err(err) = + local_writer_tx.send(RpcMessage::Notification(rpc)) + { + tracing::error!("{:?}", err); + } + } + ProxyRpc::Shutdown => { + if let Err(err) = child.kill() { + tracing::error!("{:?}", err); + } + if let Err(err) = child.wait() { + tracing::error!("{:?}", err); + } + return; + } + } + } + }) + .unwrap(); + + for msg in reader_rx { + match msg { + RpcMessage::Request(id, req) => { + let writer_tx = writer_tx.clone(); + let core_rpc = core_rpc.clone(); + std::thread::spawn(move || match core_rpc.request(req) { + Ok(resp) => { + if let Err(err) = + writer_tx.send(RpcMessage::Response(id, resp)) + { + tracing::error!("{:?}", err); + } + } + Err(e) => { + if let Err(err) = writer_tx.send(RpcMessage::Error(id, e)) { + tracing::error!("{:?}", err); + } + } + }); + } + RpcMessage::Notification(n) => { + core_rpc.notification(n); + } + RpcMessage::Response(id, resp) => { + proxy_rpc.handle_response(id, Ok(resp)); + } + RpcMessage::Error(id, err) => { + proxy_rpc.handle_response(id, Err(err)); + } + } + } + + Ok(()) +} + +fn download_remote( + remote: &impl Remote, + platform: &HostPlatform, + architecture: &HostArchitecture, + remote_proxy_path: &str, + remote_proxy_file: &str, +) -> Result<()> { + use base64::{Engine as _, engine::general_purpose}; + + let script_install = match platform { + HostPlatform::Windows => { + let local_proxy_script = + Directory::proxy_directory().unwrap().join("proxy.ps1"); + + let mut proxy_script = std::fs::OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(&local_proxy_script)?; + proxy_script.write_all(WINDOWS_PROXY_SCRIPT)?; + + let remote_proxy_script = "${env:TEMP}\\lapce-proxy.ps1"; + remote.upload_file(local_proxy_script, remote_proxy_script)?; + + let cmd = remote + .command_builder() + .args([ + "powershell", + "-c", + remote_proxy_script, + "-version", + meta::VERSION, + "-directory", + remote_proxy_path, + ]) + .output()?; + debug!("{}", String::from_utf8_lossy(&cmd.stderr)); + debug!("{}", String::from_utf8_lossy(&cmd.stdout)); + + cmd.status + } + _ => { + let proxy_script = general_purpose::STANDARD.encode(UNIX_PROXY_SCRIPT); + + let version = match meta::RELEASE { + ReleaseType::Debug => "nightly".to_string(), + ReleaseType::Nightly => "nightly".to_string(), + ReleaseType::Stable => format!("v{}", meta::VERSION), + }; + let cmd = remote + .command_builder() + .args([ + "echo", + &proxy_script, + "|", + "base64", + "-d", + "|", + "sh", + "/dev/stdin", + &version, + remote_proxy_path, + ]) + .output()?; + + debug!("{}", String::from_utf8_lossy(&cmd.stderr)); + debug!("{}", String::from_utf8_lossy(&cmd.stdout)); + + cmd.status + } + }; + + if !script_install.success() { + let cmd = match platform { + HostPlatform::Windows => remote + .command_builder() + .args(["dir", remote_proxy_file]) + .status()?, + _ => remote + .command_builder() + .arg("test") + .arg("-e") + .arg(remote_proxy_file) + .status()?, + }; + if !cmd.success() { + let proxy_filename = format!("lapce-proxy-{platform}-{architecture}"); + let local_proxy_file = Directory::proxy_directory() + .ok_or_else(|| anyhow!("can't find proxy directory"))? + .join(&proxy_filename); + // remove possibly outdated proxy + if local_proxy_file.exists() { + // TODO: add proper proxy version detection and update proxy + // when needed + std::fs::remove_file(&local_proxy_file)?; + } + let proxy_version = match meta::RELEASE { + meta::ReleaseType::Stable => meta::VERSION, + _ => "nightly", + }; + let url = format!( + "https://github.com/lapce/lapce/releases/download/{proxy_version}/{proxy_filename}.gz" + ); + debug!("proxy download URI: {url}"); + let mut resp = lapce_proxy::get_url(url, None).expect("request failed"); + if resp.status().is_success() { + let mut out = std::fs::File::create(&local_proxy_file) + .expect("failed to create file"); + let mut gz = GzDecoder::new(&mut resp); + std::io::copy(&mut gz, &mut out).expect("failed to copy content"); + } else { + error!("proxy download failed with: {}", resp.status()); + } + + match platform { + // Windows creates all dirs in provided path + HostPlatform::Windows => remote + .command_builder() + .arg("mkdir") + .arg(remote_proxy_path) + .status()?, + // Unix needs -p to do same + _ => remote + .command_builder() + .arg("mkdir") + .arg("-p") + .arg(remote_proxy_path) + .status()?, + }; + + remote.upload_file(&local_proxy_file, remote_proxy_file)?; + if platform != &HostPlatform::Windows { + remote + .command_builder() + .arg("chmod") + .arg("+x") + .arg(remote_proxy_file) + .status()?; + } + } + } + + Ok(()) +} + +fn host_specification( + remote: &impl Remote, +) -> Result<(HostPlatform, HostArchitecture)> { + use HostArchitecture::*; + use HostPlatform::*; + + let cmd = remote.command_builder().args(["uname", "-sm"]).output(); + + let spec = match cmd { + Ok(cmd) => { + let stdout = String::from_utf8_lossy(&cmd.stdout).to_lowercase(); + let stdout = stdout.trim(); + debug!("{}", &stdout); + match stdout { + // If empty, then we probably deal with Windows and not Unix + // or something went wrong with command output + "" => { + // Try cmd explicitly + let cmd = remote + .command_builder() + .args(["cmd", "/c", "echo %OS% %PROCESSOR_ARCHITECTURE%"]) + .output(); + match cmd { + Ok(cmd) => { + let stdout = + String::from_utf8_lossy(&cmd.stdout).to_lowercase(); + let stdout = stdout.trim(); + debug!("{}", &stdout); + match stdout.split_once(' ') { + Some((os, arch)) => (parse_os(os), parse_arch(arch)), + None => { + // PowerShell fallback + let cmd = remote + .command_builder() + .args(["echo", "\"${env:OS} ${env:PROCESSOR_ARCHITECTURE}\""]) + .output(); + match cmd { + Ok(cmd) => { + let stdout = + String::from_utf8_lossy(&cmd.stdout) + .to_lowercase(); + let stdout = stdout.trim(); + debug!("{}", &stdout); + match stdout.split_once(' ') { + Some((os, arch)) => { + (parse_os(os), parse_arch(arch)) + } + None => (UnknownOS, UnknownArch), + } + } + Err(e) => { + error!("{e}"); + (UnknownOS, UnknownArch) + } + } + } + } + } + Err(e) => { + error!("{e}"); + (UnknownOS, UnknownArch) + } + } + } + v => { + if let Some((os, arch)) = v.split_once(' ') { + (parse_os(os), parse_arch(arch)) + } else { + (UnknownOS, UnknownArch) + } + } + } + } + Err(e) => { + error!("{e}"); + (UnknownOS, UnknownArch) + } + }; + Ok(spec) +} + +fn parse_arch(arch: &str) -> HostArchitecture { + use HostArchitecture::*; + // processor architectures be like that + match arch { + "amd64" | "x64" | "x86_64" => AMD64, + "x86" | "i386" | "i586" | "i686" => X86, + "arm" | "armhf" | "armv6" => ARM32v6, + "armv7" | "armv7l" => ARM32v7, + "arm64" | "armv8" | "aarch64" => ARM64, + _ => UnknownArch, + } +} + +fn parse_os(os: &str) -> HostPlatform { + use HostPlatform::*; + match os { + "linux" => Linux, + "darwin" => Darwin, + "windows_nt" => Windows, + v if v.ends_with("bsd") => Bsd, + _ => UnknownOS, + } +} diff --git a/lapce-app/src/proxy/ssh.rs b/lapce-app/src/proxy/ssh.rs new file mode 100644 index 0000000..f22bd39 --- /dev/null +++ b/lapce-app/src/proxy/ssh.rs @@ -0,0 +1,67 @@ +use std::{path::Path, process::Command}; + +use anyhow::Result; +use tracing::debug; + +use super::remote::Remote; +use crate::{proxy::new_command, workspace::SshHost}; + +pub struct SshRemote { + pub ssh: SshHost, +} + +impl SshRemote { + #[cfg(windows)] + const SSH_ARGS: &'static [&'static str] = &[]; + + #[cfg(unix)] + const SSH_ARGS: &'static [&'static str] = &[ + "-o", + "ControlMaster=auto", + "-o", + "ControlPath=~/.ssh/cm_%C", + "-o", + "ControlPersist=30m", + "-o", + "ConnectTimeout=15", + ]; +} + +impl Remote for SshRemote { + fn upload_file(&self, local: impl AsRef, remote: &str) -> Result<()> { + let mut cmd = new_command("scp"); + + cmd.args(Self::SSH_ARGS); + + if let Some(port) = self.ssh.port { + cmd.arg("-P").arg(port.to_string()); + } + + let output = cmd + .arg(local.as_ref()) + .arg(dbg!(format!("{}:{remote}", self.ssh.user_host()))) + .output()?; + + debug!("{}", String::from_utf8_lossy(&output.stderr)); + debug!("{}", String::from_utf8_lossy(&output.stdout)); + + Ok(()) + } + + fn command_builder(&self) -> Command { + let mut cmd = new_command("ssh"); + cmd.args(Self::SSH_ARGS); + + if let Some(port) = self.ssh.port { + cmd.arg("-p").arg(port.to_string()); + } + + cmd.arg(self.ssh.user_host()); + + if !std::env::var("LAPCE_DEBUG").unwrap_or_default().is_empty() { + cmd.arg("-v"); + } + + cmd + } +} diff --git a/lapce-app/src/proxy/wsl.rs b/lapce-app/src/proxy/wsl.rs new file mode 100644 index 0000000..8eff13c --- /dev/null +++ b/lapce-app/src/proxy/wsl.rs @@ -0,0 +1,33 @@ +use std::{path::Path, process::Command}; + +use anyhow::Result; + +use super::{new_command, remote::Remote}; +use crate::workspace::WslHost; + +pub struct WslRemote { + pub wsl: WslHost, +} + +impl Remote for WslRemote { + fn upload_file(&self, local: impl AsRef, remote: &str) -> Result<()> { + let mut wsl_path = Path::new(r"\\wsl.localhost\").join(&self.wsl.host); + if !wsl_path.exists() { + wsl_path = Path::new(r"\\wsl$").join(&self.wsl.host); + } + wsl_path = if remote.starts_with('~') { + let home_dir = self.home_dir()?; + wsl_path.join(remote.replacen('~', &home_dir, 1)) + } else { + wsl_path.join(remote) + }; + std::fs::copy(local, wsl_path)?; + Ok(()) + } + + fn command_builder(&self) -> Command { + let mut cmd = new_command("wsl"); + cmd.arg("-d").arg(&self.wsl.host).arg("--"); + cmd + } +} diff --git a/lapce-app/src/rename.rs b/lapce-app/src/rename.rs new file mode 100644 index 0000000..7d1c706 --- /dev/null +++ b/lapce-app/src/rename.rs @@ -0,0 +1,154 @@ +use std::{path::PathBuf, rc::Rc}; + +use floem::{ + ext_event::create_ext_action, + keyboard::Modifiers, + peniko::kurbo::Rect, + reactive::{RwSignal, Scope, SignalGet, SignalUpdate, SignalWith}, +}; +use lapce_core::{command::FocusCommand, mode::Mode, selection::Selection}; +use lapce_rpc::proxy::ProxyResponse; +use lapce_xi_rope::Rope; +use lsp_types::Position; + +use crate::{ + command::{CommandExecuted, CommandKind, InternalCommand, LapceCommand}, + editor::EditorData, + keypress::{KeyPressFocus, condition::Condition}, + main_split::Editors, + window_tab::{CommonData, Focus}, +}; + +#[derive(Clone, Debug)] +pub struct RenameData { + pub active: RwSignal, + pub editor: EditorData, + pub start: RwSignal, + pub position: RwSignal, + pub path: RwSignal, + pub layout_rect: RwSignal, + pub common: Rc, +} + +impl KeyPressFocus for RenameData { + fn get_mode(&self) -> Mode { + Mode::Insert + } + + fn check_condition(&self, condition: Condition) -> bool { + matches!(condition, Condition::RenameFocus | Condition::ModalFocus) + } + + fn run_command( + &self, + command: &LapceCommand, + count: Option, + mods: Modifiers, + ) -> CommandExecuted { + match &command.kind { + CommandKind::Workbench(_) => {} + CommandKind::Scroll(_) => {} + CommandKind::Focus(cmd) => { + self.run_focus_command(cmd); + } + CommandKind::Edit(_) + | CommandKind::Move(_) + | CommandKind::MultiSelection(_) => { + self.editor.run_command(command, count, mods); + } + CommandKind::MotionMode(_) => {} + } + CommandExecuted::Yes + } + + fn receive_char(&self, c: &str) { + self.editor.receive_char(c); + } +} + +impl RenameData { + pub fn new(cx: Scope, editors: Editors, common: Rc) -> Self { + let active = cx.create_rw_signal(false); + let start = cx.create_rw_signal(0); + let position = cx.create_rw_signal(Position::default()); + let layout_rect = cx.create_rw_signal(Rect::ZERO); + let path = cx.create_rw_signal(PathBuf::new()); + let editor = editors.make_local(cx, common.clone()); + Self { + active, + editor, + start, + position, + layout_rect, + path, + common, + } + } + + pub fn start( + &self, + path: PathBuf, + placeholder: String, + start: usize, + position: Position, + ) { + self.editor.doc().reload(Rope::from(&placeholder), true); + self.editor.cursor().update(|cursor| { + cursor.set_insert(Selection::region(0, placeholder.len())) + }); + self.path.set(path); + self.start.set(start); + self.position.set(position); + self.active.set(true); + self.common.focus.set(Focus::Rename); + } + + fn run_focus_command(&self, cmd: &FocusCommand) -> CommandExecuted { + match cmd { + FocusCommand::ModalClose => { + self.cancel(); + } + FocusCommand::ConfirmRename => { + self.confirm(); + } + _ => return CommandExecuted::No, + } + CommandExecuted::Yes + } + + fn cancel(&self) { + self.active.set(false); + if let Focus::Rename = self.common.focus.get_untracked() { + self.common.focus.set(Focus::Workbench); + } + } + + fn confirm(&self) { + let new_name = self + .editor + .doc() + .buffer + .with_untracked(|buffer| buffer.to_string()); + let new_name = new_name.trim(); + if !new_name.is_empty() { + let path = self.path.get_untracked(); + let position = self.position.get_untracked(); + let internal_command = self.common.internal_command; + let send = create_ext_action(self.common.scope, move |result| { + if let Ok(ProxyResponse::Rename { edit }) = result { + internal_command + .send(InternalCommand::ApplyWorkspaceEdit { edit }); + } + }); + self.common.proxy.rename( + path, + position, + new_name.to_string(), + move |result| { + send(result); + }, + ); + } + self.cancel(); + } +} diff --git a/lapce-app/src/settings.rs b/lapce-app/src/settings.rs new file mode 100644 index 0000000..f688fd0 --- /dev/null +++ b/lapce-app/src/settings.rs @@ -0,0 +1,1400 @@ +use std::{collections::BTreeMap, rc::Rc, sync::Arc, time::Duration}; + +use floem::{ + IntoView, View, + action::{TimerToken, add_overlay, exec_after, remove_overlay}, + event::EventListener, + keyboard::Modifiers, + peniko::kurbo::{Point, Rect, Size}, + reactive::{ + Memo, ReadSignal, RwSignal, Scope, SignalGet, SignalUpdate, SignalWith, + create_effect, create_memo, create_rw_signal, + }, + style::CursorStyle, + text::{Attrs, AttrsList, FamilyOwned, TextLayout}, + views::{ + Decorators, VirtualVector, container, dyn_stack, empty, label, + scroll::{PropagatePointerWheel, scroll}, + stack, svg, text, virtual_stack, + }, +}; +use indexmap::IndexMap; +use inflector::Inflector; +use lapce_core::{buffer::rope_text::RopeText, mode::Mode}; +use lapce_rpc::plugin::VoltID; +use lapce_xi_rope::Rope; +use serde::Serialize; +use serde_json::Value; + +use crate::{ + command::CommandExecuted, + config::{ + DropdownInfo, LapceConfig, color::LapceColor, core::CoreConfig, + editor::EditorConfig, icon::LapceIcons, terminal::TerminalConfig, + ui::UIConfig, + }, + keypress::KeyPressFocus, + main_split::Editors, + plugin::InstalledVoltData, + text_input::TextInputBuilder, + window_tab::CommonData, +}; + +#[derive(Debug, Clone)] +pub enum SettingsValue { + Float(f64), + Integer(i64), + String(String), + Bool(bool), + Dropdown(DropdownInfo), + Empty, +} + +impl From for SettingsValue { + fn from(v: serde_json::Value) -> Self { + match v { + serde_json::Value::Number(n) => { + if n.is_f64() { + SettingsValue::Float(n.as_f64().unwrap()) + } else { + SettingsValue::Integer(n.as_i64().unwrap()) + } + } + serde_json::Value::String(s) => SettingsValue::String(s), + serde_json::Value::Bool(b) => SettingsValue::Bool(b), + _ => SettingsValue::Empty, + } + } +} + +#[derive(Clone, Debug)] +struct SettingsItem { + kind: String, + name: String, + field: String, + description: String, + filter_text: String, + value: SettingsValue, + serde_value: Value, + pos: RwSignal, + size: RwSignal, + // this is only the header that give an visual separation between different type of settings + header: bool, +} + +#[derive(Clone, Debug)] +struct SettingsData { + items: RwSignal>, + kinds: RwSignal)>>, + plugin_items: RwSignal>, + plugin_kinds: RwSignal)>>, + filtered_items: RwSignal>, + common: Rc, +} + +impl KeyPressFocus for SettingsData { + fn get_mode(&self) -> lapce_core::mode::Mode { + Mode::Insert + } + + fn check_condition( + &self, + _condition: crate::keypress::condition::Condition, + ) -> bool { + false + } + + fn run_command( + &self, + _command: &crate::command::LapceCommand, + _count: Option, + _mods: Modifiers, + ) -> crate::command::CommandExecuted { + CommandExecuted::No + } + + fn receive_char(&self, _c: &str) {} +} + +impl VirtualVector for SettingsData { + fn total_len(&self) -> usize { + self.filtered_items.get_untracked().len() + } + + fn slice( + &mut self, + _range: std::ops::Range, + ) -> impl Iterator { + Box::new(self.filtered_items.get().into_iter()) + } +} + +impl SettingsData { + pub fn new( + cx: Scope, + installed_plugin: RwSignal>, + common: Rc, + ) -> Self { + fn into_settings_map( + data: &impl Serialize, + ) -> serde_json::Map { + match serde_json::to_value(data).unwrap() { + serde_json::Value::Object(h) => h, + _ => serde_json::Map::default(), + } + } + + let config = common.config; + let plugin_items = cx.create_rw_signal(im::Vector::new()); + let plugin_kinds = cx.create_rw_signal(im::Vector::new()); + let filtered_items = cx.create_rw_signal(im::Vector::new()); + let items = cx.create_rw_signal(im::Vector::new()); + let kinds = cx.create_rw_signal(im::Vector::new()); + cx.create_effect(move |_| { + let config = config.get(); + + let mut data_items = im::Vector::new(); + let mut data_kinds = im::Vector::new(); + let mut item_height_accum = 0.0; + for (kind, fields, descs, mut settings_map) in [ + ( + "Core", + &CoreConfig::FIELDS[..], + &CoreConfig::DESCS[..], + into_settings_map(&config.core), + ), + ( + "Editor", + &EditorConfig::FIELDS[..], + &EditorConfig::DESCS[..], + into_settings_map(&config.editor), + ), + ( + "UI", + &UIConfig::FIELDS[..], + &UIConfig::DESCS[..], + into_settings_map(&config.ui), + ), + ( + "Terminal", + &TerminalConfig::FIELDS[..], + &TerminalConfig::DESCS[..], + into_settings_map(&config.terminal), + ), + ] { + let pos = cx.create_rw_signal(Point::new(0.0, item_height_accum)); + data_items.push_back(SettingsItem { + kind: kind.to_string(), + name: "".to_string(), + field: "".to_string(), + filter_text: "".to_string(), + description: "".to_string(), + value: SettingsValue::Empty, + serde_value: Value::Null, + pos, + size: cx.create_rw_signal(Size::ZERO), + header: true, + }); + data_kinds.push_back((kind.to_string(), pos)); + for (name, desc) in fields.iter().zip(descs.iter()) { + let field = name.replace('_', "-"); + + let (value, serde_value) = if let Some(dropdown) = + config.get_dropdown_info(&kind.to_lowercase(), &field) + { + let index = dropdown.active_index; + ( + SettingsValue::Dropdown(dropdown), + Value::Number(index.into()), + ) + } else { + let value = settings_map.remove(&field).unwrap(); + (SettingsValue::from(value.clone()), value) + }; + + let name = format!( + "{kind}: {}", + name.replace('_', " ").to_title_case() + ); + let kind = kind.to_lowercase(); + let filter_text = format!("{kind} {name} {desc}").to_lowercase(); + let filter_text = + format!("{filter_text}{}", filter_text.replace(' ', "")); + data_items.push_back(SettingsItem { + kind, + name, + field, + filter_text, + description: desc.to_string(), + value, + pos: cx.create_rw_signal(Point::ZERO), + size: cx.create_rw_signal(Size::ZERO), + serde_value, + header: false, + }); + item_height_accum += 50.0; + } + } + + filtered_items.set(data_items.clone()); + items.set(data_items); + + let plugins = installed_plugin.get(); + let mut setting_items = im::Vector::new(); + let mut plugin_kinds_tmp = im::Vector::new(); + for (_, volt) in plugins { + let meta = volt.meta.get(); + let kind = meta.name; + let plugin_config = config.plugins.get(&kind); + if let Some(config) = meta.config { + let pos = + cx.create_rw_signal(Point::new(0.0, item_height_accum)); + setting_items.push_back(SettingsItem { + kind: meta.display_name.clone(), + name: "".to_string(), + field: "".to_string(), + filter_text: "".to_string(), + description: "".to_string(), + value: SettingsValue::Empty, + serde_value: Value::Null, + pos, + size: cx.create_rw_signal(Size::ZERO), + header: true, + }); + plugin_kinds_tmp.push_back((meta.display_name.clone(), pos)); + + { + let mut local_items = Vec::new(); + for (name, config) in config { + let field = name.clone(); + + let name = format!( + "{}: {}", + meta.display_name, + name.replace('_', " ").to_title_case() + ); + let desc = config.description; + let filter_text = + format!("{kind} {name} {desc}").to_lowercase(); + let filter_text = format!( + "{filter_text}{}", + filter_text.replace(' ', "") + ); + + let value = plugin_config + .and_then(|config| config.get(&field).cloned()) + .unwrap_or(config.default); + let value = SettingsValue::from(value); + + let item = SettingsItem { + kind: kind.clone(), + name, + field, + filter_text, + description: desc.to_string(), + value, + pos: cx.create_rw_signal(Point::ZERO), + size: cx.create_rw_signal(Size::ZERO), + serde_value: Value::Null, + header: false, + }; + local_items.push(item); + item_height_accum += 50.0; + } + local_items.sort_by_key(|i| i.name.clone()); + setting_items.extend(local_items.into_iter()); + } + } + } + plugin_items.set(setting_items); + plugin_kinds.set(plugin_kinds_tmp); + kinds.set(data_kinds); + }); + + Self { + filtered_items, + plugin_items, + plugin_kinds, + items, + kinds, + common, + } + } +} + +pub fn settings_view( + installed_plugins: RwSignal>, + editors: Editors, + common: Rc, +) -> impl View { + let config = common.config; + + let cx = Scope::current(); + let settings_data = SettingsData::new(cx, installed_plugins, common.clone()); + let view_settings_data = settings_data.clone(); + let plugin_kinds = settings_data.plugin_kinds; + + let search_editor = editors.make_local(cx, common); + let doc = search_editor.doc_signal(); + + let items = settings_data.items; + let kinds = settings_data.kinds; + let filtered_items_signal = settings_data.filtered_items; + create_effect(move |_| { + let doc = doc.get(); + let pattern = doc.buffer.with(|b| b.to_string().to_lowercase()); + let plugin_items = settings_data.plugin_items.get(); + let mut items = items.get(); + if pattern.is_empty() { + items.extend(plugin_items); + filtered_items_signal.set(items); + return; + } + + let mut filtered_items = im::Vector::new(); + for item in &items { + if item.header || item.filter_text.contains(&pattern) { + filtered_items.push_back(item.clone()); + } + } + for item in plugin_items { + if item.header || item.filter_text.contains(&pattern) { + filtered_items.push_back(item); + } + } + filtered_items_signal.set(filtered_items); + }); + + let ensure_visible = create_rw_signal(Rect::ZERO); + let settings_content_size = create_rw_signal(Size::ZERO); + let scroll_pos = create_rw_signal(Point::ZERO); + + let current_kind = { + create_memo(move |_| { + let scroll_pos = scroll_pos.get(); + let scroll_y = scroll_pos.y + 30.0; + + let plugin_kinds = plugin_kinds.get_untracked(); + for (kind, pos) in plugin_kinds.iter().rev() { + if pos.get_untracked().y < scroll_y { + return kind.to_string(); + } + } + + let kinds = kinds.get(); + for (kind, pos) in kinds.iter().rev() { + if pos.get_untracked().y < scroll_y { + return kind.to_string(); + } + } + + kinds.get(0).unwrap().0.to_string() + }) + }; + + let switcher_item = move |k: String, + pos: Box Option>>, + margin: f32| { + let kind = k.clone(); + container( + label(move || k.clone()) + .style(move |s| s.text_ellipsis().padding_left(margin)), + ) + .on_click_stop(move |_| { + if let Some(pos) = pos() { + ensure_visible.set( + settings_content_size + .get_untracked() + .to_rect() + .with_origin(pos.get_untracked()), + ); + } + }) + .style(move |s| { + let config = config.get(); + s.padding_horiz(20.0) + .width_pct(100.0) + .apply_if(kind == current_kind.get(), |s| { + s.background(config.color(LapceColor::PANEL_CURRENT_BACKGROUND)) + }) + .hover(|s| { + s.cursor(CursorStyle::Pointer).background( + config.color(LapceColor::PANEL_HOVERED_BACKGROUND), + ) + }) + .active(|s| { + s.background( + config.color(LapceColor::PANEL_HOVERED_ACTIVE_BACKGROUND), + ) + }) + }) + }; + + let switcher = || { + stack(( + dyn_stack( + move || kinds.get().clone(), + |(k, _)| k.clone(), + move |(k, pos)| switcher_item(k, Box::new(move || Some(pos)), 0.0), + ) + .style(|s| s.flex_col().width_pct(100.0)), + stack(( + switcher_item( + "Plugin Settings".to_string(), + Box::new(move || { + plugin_kinds + .with_untracked(|k| k.get(0).map(|(_, pos)| *pos)) + }), + 0.0, + ), + dyn_stack( + move || plugin_kinds.get(), + |(k, _)| k.clone(), + move |(k, pos)| { + switcher_item(k, Box::new(move || Some(pos)), 10.0) + }, + ) + .style(|s| s.flex_col().width_pct(100.0)), + )) + .style(move |s| { + s.width_pct(100.0) + .flex_col() + .apply_if(plugin_kinds.with(|k| k.is_empty()), |s| s.hide()) + }), + )) + .style(move |s| { + s.width_pct(100.0) + .flex_col() + .line_height(1.8) + .font_size(config.get().ui.font_size() as f32 + 1.0) + }) + }; + + stack(( + container({ + scroll({ + container(switcher()) + .style(|s| s.padding_vert(20.0).width_pct(100.0)) + }) + .style(|s| s.absolute().size_pct(100.0, 100.0)) + }) + .style(move |s| { + s.height_pct(100.0) + .width(200.0) + .border_right(1.0) + .border_color(config.get().color(LapceColor::LAPCE_BORDER)) + }), + stack(( + container({ + TextInputBuilder::new() + .build_editor(search_editor) + .placeholder(|| "Search Settings".to_string()) + .keyboard_navigable() + .style(move |s| { + s.width_pct(100.0) + .border_radius(6.0) + .border(1.0) + .border_color( + config.get().color(LapceColor::LAPCE_BORDER), + ) + }) + .request_focus(|| {}) + }) + .style(|s| s.padding_horiz(50.0).padding_vert(20.0)), + container({ + scroll({ + dyn_stack( + move || filtered_items_signal.get(), + |item| { + ( + item.kind.clone(), + item.name.clone(), + item.serde_value.clone(), + ) + }, + move |item| { + settings_item_view( + editors, + view_settings_data.clone(), + item, + ) + }, + ) + .style(|s| { + s.flex_col() + .padding_horiz(50.0) + .min_width_pct(100.0) + .max_width(400.0) + }) + }) + .on_scroll(move |rect| { + scroll_pos.set(rect.origin()); + }) + .ensure_visible(move || ensure_visible.get()) + .on_resize(move |rect| { + settings_content_size.set(rect.size()); + }) + .style(|s| s.absolute().size_pct(100.0, 100.0)) + }) + .style(|s| s.size_pct(100.0, 100.0)), + )) + .style(|s| s.flex_col().size_pct(100.0, 100.0)), + )) + .style(|s| s.absolute().size_pct(100.0, 100.0)) + .debug_name("Settings") +} + +fn settings_item_view( + editors: Editors, + settings_data: SettingsData, + item: SettingsItem, +) -> impl View + use<> { + let config = settings_data.common.config; + + let is_ticked = if let SettingsValue::Bool(is_ticked) = &item.value { + Some(*is_ticked) + } else { + None + }; + + let timer = create_rw_signal(TimerToken::INVALID); + + let editor_value = match &item.value { + SettingsValue::Float(n) => Some(n.to_string()), + SettingsValue::Integer(n) => Some(n.to_string()), + SettingsValue::String(s) => Some(s.to_string()), + SettingsValue::Bool(_) => None, + SettingsValue::Dropdown(_) => None, + SettingsValue::Empty => None, + }; + + let view = { + let item = item.clone(); + move || { + let cx = Scope::current(); + if let Some(editor_value) = editor_value { + let text_input_view = TextInputBuilder::new() + .value(editor_value) + .build(cx, editors, settings_data.common); + + let doc = text_input_view.doc_signal(); + + let kind = item.kind.clone(); + let field = item.field.clone(); + let item_value = item.value.clone(); + create_effect(move |last| { + let doc = doc.get_untracked(); + let rev = doc.buffer.with(|b| b.rev()); + if last.is_none() { + return rev; + } + if last == Some(rev) { + return rev; + } + let kind = kind.clone(); + let field = field.clone(); + let buffer = doc.buffer; + let item_value = item_value.clone(); + let token = + exec_after(Duration::from_millis(500), move |token| { + let Some(timer) = timer.try_get_untracked() else { + return; + }; + if timer != token { + return; + } + + let value = buffer.with_untracked(|b| b.to_string()); + // FIXME: Figure out how to block certain keys in inputs and not hate myself + let value = value.trim(); + let value = match &item_value { + SettingsValue::Float(_) => { + value.parse::().ok().and_then(|v| { + serde::Serialize::serialize( + &v, + toml_edit::ser::ValueSerializer::new(), + ) + .ok() + }) + } + SettingsValue::Integer(_) => { + value.parse::().ok().and_then(|v| { + serde::Serialize::serialize( + &v, + toml_edit::ser::ValueSerializer::new(), + ) + .ok() + }) + } + _ => serde::Serialize::serialize( + &value, + toml_edit::ser::ValueSerializer::new(), + ) + .ok(), + }; + + if let Some(value) = value { + LapceConfig::update_file(&kind, &field, value); + } + }); + timer.set(token); + + rev + }); + + text_input_view + .keyboard_navigable() + .style(move |s| { + s.width(300.0).border(1.0).border_radius(6.0).border_color( + config.get().color(LapceColor::LAPCE_BORDER), + ) + }) + .into_any() + } else if let SettingsValue::Dropdown(dropdown) = &item.value { + let expanded = create_rw_signal(false); + let current_value = dropdown + .items + .get(dropdown.active_index) + .or_else(|| dropdown.items.last()) + .map(|s| s.to_string()) + .unwrap_or_default(); + let current_value = create_rw_signal(current_value); + + dropdown_view( + &item, + current_value, + dropdown, + expanded, + settings_data.common.window_common.size, + config, + ) + .into_any() + } else if item.header { + label(move || item.kind.clone()) + .style(move |s| { + let config = config.get(); + s.line_height(2.0) + .font_bold() + .width_pct(100.0) + .padding_horiz(10.0) + .font_size(config.ui.font_size() as f32 + 2.0) + .background(config.color(LapceColor::PANEL_BACKGROUND)) + }) + .into_any() + } else { + empty().into_any() + } + } + }; + + stack(( + label(move || item.name.clone()).style(move |s| { + s.font_bold() + .text_ellipsis() + .min_width(0.0) + .max_width_pct(100.0) + .line_height(1.8) + .font_size(config.get().ui.font_size() as f32 + 1.0) + }), + stack(( + label(move || item.description.clone()).style(move |s| { + s.min_width(0.0) + .max_width_pct(100.0) + .line_height(1.8) + .apply_if(is_ticked.is_some(), |s| { + s.margin_left(config.get().ui.font_size() as f32 + 8.0) + }) + .apply_if(item.header, |s| s.hide()) + }), + if let Some(is_ticked) = is_ticked { + let checked = create_rw_signal(is_ticked); + + let kind = item.kind.clone(); + let field = item.field.clone(); + create_effect(move |last| { + let checked = checked.get(); + if last.is_none() { + return; + } + if let Ok(value) = serde::Serialize::serialize( + &checked, + toml_edit::ser::ValueSerializer::new(), + ) { + LapceConfig::update_file(&kind, &field, value); + } + }); + + container( + stack(( + checkbox(move || checked.get(), config), + label(|| " ".to_string()).style(|s| s.line_height(1.8)), + )) + .style(|s| s.items_center()), + ) + .on_click_stop(move |_| { + checked.update(|checked| { + *checked = !*checked; + }); + }) + .style(|s| { + s.absolute() + .cursor(CursorStyle::Pointer) + .size_pct(100.0, 100.0) + .items_start() + }) + } else { + container(empty()).style(|s| s.hide()) + }, + )), + view().style(move |s| s.apply_if(!item.header, |s| s.margin_top(6.0))), + )) + .on_resize(move |rect| { + if item.header { + item.pos.set(rect.origin()); + } + let old_size = item.size.get_untracked(); + let new_size = rect.size(); + if old_size != new_size { + item.size.set(new_size); + } + }) + .style(|s| { + s.flex_col() + .padding_vert(10.0) + .min_width_pct(100.0) + .max_width(300.0) + }) +} + +pub fn checkbox( + checked: impl Fn() -> bool + 'static, + config: ReadSignal>, +) -> impl View { + const CHECKBOX_SVG: &str = r#""#; + let svg_str = move || if checked() { CHECKBOX_SVG } else { "" }.to_string(); + + svg(svg_str).style(move |s| { + let config = config.get(); + let size = config.ui.font_size() as f32; + let color = config.color(LapceColor::EDITOR_FOREGROUND); + + s.min_width(size) + .size(size, size) + .color(color) + .border_color(color) + .border(1.) + .border_radius(2.) + }) +} + +struct BTreeMapVirtualList(BTreeMap); + +impl VirtualVector<(String, String)> for BTreeMapVirtualList { + fn total_len(&self) -> usize { + self.0.len() + } + + fn slice( + &mut self, + range: std::ops::Range, + ) -> impl Iterator { + Box::new( + self.0 + .iter() + .enumerate() + .filter_map(|(index, (k, v))| { + if range.contains(&index) { + Some((k.to_string(), v.to_string())) + } else { + None + } + }) + .collect::>() + .into_iter(), + ) + } +} + +fn color_section_list( + kind: &str, + header: &str, + list: impl Fn() -> BTreeMap + 'static, + max_width: Memo, + text_height: Memo, + editors: Editors, + common: Rc, +) -> impl View { + let config = common.config; + + let kind = kind.to_string(); + stack(( + text(header).style(|s| { + s.margin_top(10) + .margin_horiz(20) + .font_bold() + .line_height(2.0) + }), + virtual_stack( + move || BTreeMapVirtualList(list()), + move |(key, _)| key.to_owned(), + move |(key, value)| { + let cx = Scope::current(); + let text_input_view = TextInputBuilder::new() + .value(value.clone()) + .build(cx, editors, common.clone()); + let doc = text_input_view.doc_signal(); + + { + let kind = kind.clone(); + let key = key.clone(); + let doc = text_input_view.doc_signal(); + create_effect(move |_| { + let doc = doc.get_untracked(); + let config = config.get(); + let current = doc.buffer.with_untracked(|b| b.to_string()); + + let value = match kind.as_str() { + "base" => config.color_theme.base.get(&key), + "ui" => config.color_theme.ui.get(&key), + "syntax" => config.color_theme.syntax.get(&key), + _ => None, + }; + + if let Some(value) = value { + if value != ¤t { + doc.reload(Rope::from(value.to_string()), true); + } + } + }); + } + + { + let timer = create_rw_signal(TimerToken::INVALID); + let kind = kind.clone(); + let field = key.clone(); + create_effect(move |last| { + let doc = doc.get_untracked(); + + let rev = doc.buffer.with(|b| b.rev()); + if last.is_none() { + return rev; + } + if last == Some(rev) { + return rev; + } + let kind = kind.clone(); + let field = field.clone(); + let buffer = doc.buffer; + let token = + exec_after(Duration::from_millis(500), move |token| { + if let Some(timer) = timer.try_get_untracked() { + if timer == token { + let value = + buffer.with_untracked(|b| b.to_string()); + + let config = config.get_untracked(); + let default = match kind.as_str() { + "base" => config + .default_color_theme() + .base + .get(&field), + "ui" => config + .default_color_theme() + .ui + .get(&field), + "syntax" => config + .default_color_theme() + .syntax + .get(&field), + _ => None, + }; + + if default != Some(&value) { + let value = serde::Serialize::serialize( + &value, + toml_edit::ser::ValueSerializer::new( + ), + ) + .ok(); + + if let Some(value) = value { + LapceConfig::update_file( + &format!("color-theme.{kind}"), + &field, + value, + ); + } + } else { + LapceConfig::reset_setting( + &format!("color-theme.{kind}"), + &field, + ); + } + } + } + }); + timer.set(token); + + rev + }); + } + + let local_kind = kind.clone(); + let local_key = key.clone(); + stack(( + text(&key).style(move |s| { + s.width(max_width.get()).margin_left(20).margin_right(10) + }), + text_input_view.keyboard_navigable().style(move |s| { + s.width(150.0) + .margin_vert(6) + .border(1) + .border_radius(6) + .border_color( + config.get().color(LapceColor::LAPCE_BORDER), + ) + }), + empty().style(move |s| { + let size = text_height.get() + 12.0; + let config = config.get(); + let color = match local_kind.as_str() { + "base" => config.color.base.get(&local_key), + "ui" => config.color.ui.get(&local_key).copied(), + "syntax" => config.color.syntax.get(&local_key).copied(), + _ => None, + }; + s.border(1) + .border_radius(6) + .size(size, size) + .margin_left(10) + .border_color(config.color(LapceColor::LAPCE_BORDER)) + .background(color.unwrap_or_else(|| { + config.color(LapceColor::EDITOR_FOREGROUND) + })) + }), + { + let kind = kind.clone(); + let key = key.clone(); + let local_key = key.clone(); + let local_kind = kind.clone(); + text("Reset") + .on_click_stop(move |_| { + LapceConfig::reset_setting( + &format!("color-theme.{local_kind}"), + &local_key, + ); + }) + .style(move |s| { + let doc = doc.get_untracked(); + let config = config.get(); + let buffer = doc.buffer; + let content = buffer.with(|b| b.to_string()); + + let same = match kind.as_str() { + "base" => { + config.default_color_theme().base.get(&key) + == Some(&content) + } + "ui" => { + config.default_color_theme().ui.get(&key) + == Some(&content) + } + "syntax" => { + config.default_color_theme().syntax.get(&key) + == Some(&content) + } + _ => false, + }; + + s.margin_left(10) + .padding(6) + .cursor(CursorStyle::Pointer) + .border(1) + .border_radius(6) + .border_color( + config.color(LapceColor::LAPCE_BORDER), + ) + .apply_if(same, |s| s.hide()) + .active(|s| { + s.background( + config + .color(LapceColor::PANEL_BACKGROUND), + ) + }) + }) + }, + )) + .style(|s| s.items_center()) + }, + ) + .item_size_fixed(move || text_height.get() + 24.0) + .style(|s| s.flex_col().padding_right(20)), + )) + .style(|s| s.flex_col()) +} + +pub fn theme_color_settings_view( + editors: Editors, + common: Rc, +) -> impl View { + let config = common.config; + + let text_height = create_memo(move |_| { + let mut text_layout = TextLayout::new(); + let config = config.get(); + let family: Vec = + FamilyOwned::parse_list(&config.ui.font_family).collect(); + let attrs = Attrs::new() + .family(&family) + .font_size(config.ui.font_size() as f32); + let attrs_list = AttrsList::new(attrs); + text_layout.set_text("W", attrs_list, None); + text_layout.size().height + }); + + let max_width = create_memo(move |_| { + let mut text_layout = TextLayout::new(); + let config = config.get(); + let family: Vec = + FamilyOwned::parse_list(&config.ui.font_family).collect(); + let attrs = Attrs::new() + .family(&family) + .font_size(config.ui.font_size() as f32); + let attrs_list = AttrsList::new(attrs); + + let mut max_width = 0.0; + for key in config.color_theme.ui.keys() { + text_layout.set_text(key, attrs_list.clone(), None); + let width = text_layout.size().width; + if width > max_width { + max_width = width; + } + } + for key in config.color_theme.syntax.keys() { + text_layout.set_text(key, attrs_list.clone(), None); + let width = text_layout.size().width; + if width > max_width { + max_width = width; + } + } + max_width + }); + + let cx = Scope::current(); + let search_editor = editors.make_local(cx, common.clone()); + let buffer = search_editor.doc_signal().get_untracked().buffer; + + scroll( + stack(( + container({ + TextInputBuilder::new() + .build_editor(search_editor) + .placeholder(|| "Search Settings".to_string()) + .keyboard_navigable() + .style(move |s| { + s.width_pct(100.0) + .border_radius(6.0) + .border(1.0) + .border_color( + config.get().color(LapceColor::LAPCE_BORDER), + ) + }) + .request_focus(|| {}) + }) + .style(|s| s.padding_vert(20.0).padding_horiz(20.0)), + color_section_list( + "base", + "Base Colors", + move || { + let filter = buffer.get().text().to_string(); + config.with(|c| { + c.color_theme + .base + .0 + .iter() + .filter_map(|x| { + if x.0.contains(&filter) { + Some((x.0.clone(), x.1.clone())) + } else { + None + } + }) + .collect::>() + }) + }, + max_width, + text_height, + editors, + common.clone(), + ), + color_section_list( + "syntax", + "Syntax Colors", + move || { + let filter = buffer.get().text().to_string(); + config.with(|c| { + c.color_theme + .syntax + .iter() + .filter_map(|x| { + if x.0.contains(&filter) { + Some((x.0.clone(), x.1.clone())) + } else { + None + } + }) + .collect::>() + }) + }, + max_width, + text_height, + editors, + common.clone(), + ), + color_section_list( + "ui", + "UI Colors", + move || { + let filter = buffer.get().text().to_string(); + config.with(|c| { + c.color_theme + .ui + .iter() + .filter_map(|x| { + if x.0.contains(&filter) { + Some((x.0.clone(), x.1.clone())) + } else { + None + } + }) + .collect::>() + }) + }, + max_width, + text_height, + editors, + common.clone(), + ), + )) + .style(|s| s.flex_col()), + ) + .style(|s| s.absolute().size_full()) + .debug_name("Theme Color Settings") +} + +fn dropdown_view( + item: &SettingsItem, + current_value: RwSignal, + dropdown: &DropdownInfo, + expanded: RwSignal, + window_size: RwSignal, + config: ReadSignal>, +) -> impl View + use<> { + let window_origin = create_rw_signal(Point::ZERO); + let size = create_rw_signal(Size::ZERO); + let overlay_id = create_rw_signal(None); + let dropdown_input_focus = create_rw_signal(false); + let dropdown_scroll_focus = create_rw_signal(true); + + { + let item = item.to_owned(); + let dropdown = dropdown.to_owned(); + create_effect(move |_| { + if expanded.get() { + let item = item.clone(); + let dropdown = dropdown.clone(); + let id = add_overlay(Point::ZERO, move |_| { + dropdown_scroll( + &item.clone(), + current_value, + &dropdown.clone(), + expanded, + dropdown_scroll_focus, + dropdown_input_focus, + window_origin, + size, + window_size, + config, + ) + }); + overlay_id.set(Some(id)); + } else if let Some(id) = overlay_id.get_untracked() { + remove_overlay(id); + overlay_id.set(None); + } + }); + } + + stack(( + label(move || current_value.get()).style(move |s| { + s.text_ellipsis() + .width_pct(100.0) + .padding_horiz(10.0) + .selectable(false) + }), + container( + svg(move || { + if expanded.get() { + config.get().ui_svg(LapceIcons::CLOSE) + } else { + config.get().ui_svg(LapceIcons::DROPDOWN_ARROW) + } + }) + .style(move |s| { + let config = config.get(); + let size = config.ui.icon_size() as f32; + s.size(size, size) + .color(config.color(LapceColor::LAPCE_ICON_ACTIVE)) + }), + ) + .style(|s| s.padding_right(4.0)), + )) + .on_click_stop(move |_| { + expanded.update(|expanded| { + *expanded = !*expanded; + }); + }) + .on_move(move |point| { + window_origin.set(point); + if expanded.get_untracked() { + expanded.set(false); + } + }) + .on_resize(move |rect| { + size.set(rect.size()); + }) + .style(move |s| { + s.items_center() + .cursor(CursorStyle::Pointer) + .border_color(config.get().color(LapceColor::LAPCE_BORDER)) + .border(1.0) + .border_radius(6.0) + .width(250.0) + .line_height(1.8) + }) + .keyboard_navigable() + .on_event_stop(EventListener::FocusGained, move |_| { + dropdown_input_focus.set(true); + }) + .on_event_stop(EventListener::FocusLost, move |_| { + dropdown_input_focus.set(false); + if expanded.get_untracked() && !dropdown_scroll_focus.get_untracked() { + expanded.set(false); + } + }) + .on_cleanup(move || { + if let Some(id) = overlay_id.get_untracked() { + remove_overlay(id); + } + }) +} + +#[allow(clippy::too_many_arguments)] +fn dropdown_scroll( + item: &SettingsItem, + current_value: RwSignal, + dropdown: &DropdownInfo, + expanded: RwSignal, + dropdown_scroll_focus: RwSignal, + dropdown_input_focus: RwSignal, + window_origin: RwSignal, + input_size: RwSignal, + window_size: RwSignal, + config: ReadSignal>, +) -> impl View + use<> { + dropdown_scroll_focus.set(true); + + let kind = item.kind.clone(); + let field = item.field.clone(); + let view_fn = move |item_string: String| { + let kind = kind.clone(); + let field = field.clone(); + let local_item_string = item_string.clone(); + label(move || local_item_string.clone()) + .on_click_stop(move |_| { + current_value.set(item_string.clone()); + if let Ok(value) = serde::Serialize::serialize( + &item_string, + toml_edit::ser::ValueSerializer::new(), + ) { + LapceConfig::update_file(&kind, &field, value); + } + expanded.set(false); + }) + .style(move |s| { + s.text_ellipsis().padding_horiz(10.0).hover(|s| { + s.cursor(CursorStyle::Pointer).background( + config.get().color(LapceColor::PANEL_HOVERED_BACKGROUND), + ) + }) + }) + }; + + let items = dropdown.items.clone(); + + let scroll_size = create_rw_signal(Size::ZERO); + + scroll({ + dyn_stack(move || items.clone(), |item| item.to_string(), view_fn) + .style(|s| s.flex_col().width_pct(100.0).cursor(CursorStyle::Pointer)) + }) + .style(move |s| { + s.width_pct(100.0) + .max_height(200.0) + .set(PropagatePointerWheel, false) + }) + .keyboard_navigable() + .request_focus(|| {}) + .on_event_stop(EventListener::FocusGained, move |_| { + dropdown_scroll_focus.set(true); + }) + .on_event_stop(EventListener::FocusLost, move |_| { + dropdown_scroll_focus.set(false); + if expanded.get_untracked() && !dropdown_input_focus.get_untracked() { + expanded.set(false); + } + }) + .on_event_stop(EventListener::PointerMove, move |_| {}) + .on_event_stop(EventListener::PointerDown, move |_| {}) + .on_resize(move |rect| { + scroll_size.set(rect.size()); + }) + .style(move |s| { + let config = config.get(); + let window_origin = window_origin.get(); + let window_size = window_size.get(); + let input_size = input_size.get(); + let scroll_size = scroll_size.get(); + + let x = if window_origin.x + scroll_size.width + 5.0 > window_size.width { + window_size.width - scroll_size.width - 5.0 + } else { + window_origin.x + }; + + let y = if window_origin.y + input_size.height + scroll_size.height + 5.0 + > window_size.height + { + window_origin.y - scroll_size.height + 1.0 + } else { + window_origin.y + input_size.height - 1.0 + }; + + s.width(250.0) + .line_height(1.8) + .font_size(config.ui.font_size() as f32) + .font_family(config.ui.font_family.clone()) + .color(config.color(LapceColor::EDITOR_FOREGROUND)) + .background(config.color(LapceColor::EDITOR_BACKGROUND)) + .class(floem::views::scroll::Handle, |s| { + s.background(config.color(LapceColor::LAPCE_SCROLL_BAR)) + }) + .border(1) + .border_radius(6.0) + .border_color(config.color(LapceColor::LAPCE_BORDER)) + .box_shadow_blur(3.0) + .box_shadow_color(config.color(LapceColor::LAPCE_DROPDOWN_SHADOW)) + .inset_left(x) + .inset_top(y) + }) +} diff --git a/lapce-app/src/snippet.rs b/lapce-app/src/snippet.rs new file mode 100644 index 0000000..1980bc5 --- /dev/null +++ b/lapce-app/src/snippet.rs @@ -0,0 +1,859 @@ +use core::fmt; +use std::{fmt::Display, str::FromStr}; + +use anyhow::Error; +use once_cell::sync::Lazy; +use regex::Regex; + +#[derive(Debug, PartialEq)] +pub enum SnippetElement { + Text(String), + PlaceHolder(usize, Vec), + Tabstop(usize), +} + +impl Display for SnippetElement { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match &self { + SnippetElement::Text(text) => f.write_str(text), + SnippetElement::PlaceHolder(tab, elements) => { + // Trying to write to the provided buffer in the form "${tab:text}" + write!(f, "${{{tab}:")?; + for child_snippet_elm in elements { + // call ourselves recursively + fmt::Display::fmt(child_snippet_elm, f)?; + } + f.write_str("}") + } + SnippetElement::Tabstop(tab) => write!(f, "${tab}"), + } + } +} + +impl SnippetElement { + pub fn len(&self) -> usize { + match &self { + SnippetElement::Text(text) => text.len(), + SnippetElement::PlaceHolder(_, elements) => { + elements.iter().map(|e| e.len()).sum() + } + SnippetElement::Tabstop(_) => 0, + } + } + + #[inline] + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + #[inline] + pub fn text(&self) -> String { + let mut buf = String::new(); + self.write_text_to(&mut buf) + .expect("a write_to function returned an error unexpectedly"); + buf + } + + fn write_text_to(&self, buf: &mut Buffer) -> fmt::Result { + match self { + SnippetElement::Text(text) => buf.write_str(text), + SnippetElement::PlaceHolder(_, elements) => { + for child_snippet_elm in elements { + // call ourselves recursively + child_snippet_elm.write_text_to(buf)?; + } + fmt::Result::Ok(()) + } + SnippetElement::Tabstop(_) => fmt::Result::Ok(()), + } + } +} + +#[derive(Debug, PartialEq)] +pub struct Snippet { + elements: Vec, +} + +impl FromStr for Snippet { + type Err = Error; + + #[inline] + fn from_str(s: &str) -> Result { + let (elements, _) = Self::extract_elements(s, 0, &['$', '\\'], &['}']); + Ok(Snippet { elements }) + } +} + +impl Display for Snippet { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for snippet_element in self.elements.iter() { + fmt::Display::fmt(snippet_element, f)?; + } + fmt::Result::Ok(()) + } +} + +impl Snippet { + #[inline] + fn extract_elements( + s: &str, + pos: usize, + escs: &[char], + loose_escs: &[char], + ) -> (Vec, usize) { + let mut elements = Vec::new(); + let mut pos = pos; + loop { + if s.len() == pos { + break; + } else if let Some((ele, end)) = Self::extract_tabstop(s, pos) { + elements.push(ele); + pos = end; + } else if let Some((ele, end)) = Self::extract_placeholder(s, pos) { + elements.push(ele); + pos = end; + } else if let Some((ele, end)) = + Self::extract_text(s, pos, escs, loose_escs) + { + elements.push(ele); + pos = end; + } else { + break; + } + } + (elements, pos) + } + + #[inline] + fn extract_tabstop(str: &str, pos: usize) -> Option<(SnippetElement, usize)> { + // Regex for `$...` pattern, where `...` is some number (for example `$1`) + static REGEX_FIRST: Lazy = + Lazy::new(|| Regex::new(r"^\$(\d+)").unwrap()); + // Regex for `${...}` pattern, where `...` is some number (for example `${1}`) + static REGEX_SECOND: Lazy = + Lazy::new(|| Regex::new(r"^\$\{(\d+)\}").unwrap()); + + let str = &str[pos..]; + if let Some(matched) = REGEX_FIRST.find(str) { + // SAFETY: + // * The start index is guaranteed not to exceed the end index, since we + // compare with the `$ ...` pattern, and, therefore, the first element + // is always equal to the symbol `$`; + // * The indices are within the bounds of the original slice and lie on + // UTF-8 sequence boundaries, since we take the entire slice, with the + // exception of the first `$` char which is 1 byte in accordance with + // the UTF-8 standard. + let n = unsafe { + matched.as_str().get_unchecked(1..).parse::().ok()? + }; + let end = pos + matched.end(); + return Some((SnippetElement::Tabstop(n), end)); + } + if let Some(matched) = REGEX_SECOND.find(str) { + let matched = matched.as_str(); + // SAFETY: + // * The start index is guaranteed not to exceed the end index, since we + // compare with the `${...}` pattern, and, therefore, the first two elements + // are always equal to the `${` and the last one is equal to `}`; + // * The indices are within the bounds of the original slice and lie on UTF-8 + // sequence boundaries, since we take the entire slice, with the exception + // of the first two `${` and last one `}` chars each of which is 1 byte in + // accordance with the UTF-8 standard. + let n = unsafe { + matched + .get_unchecked(2..matched.len() - 1) + .parse::() + .ok()? + }; + let end = pos + matched.len(); + return Some((SnippetElement::Tabstop(n), end)); + } + None + } + + #[inline] + fn extract_placeholder(s: &str, pos: usize) -> Option<(SnippetElement, usize)> { + // Regex for `${num:text}` pattern, where text can be empty (for example `${1:first}` + // and `${2:}`) + static REGEX: Lazy = + Lazy::new(|| Regex::new(r"^\$\{(\d+):(.*?)\}").unwrap()); + + let caps = REGEX.captures(&s[pos..])?; + + let tab = caps.get(1)?.as_str().parse::().ok()?; + + let m = caps.get(2)?; + let content = m.as_str(); + if content.is_empty() { + return Some(( + SnippetElement::PlaceHolder( + tab, + vec![SnippetElement::Text(String::new())], + ), + pos + caps.get(0).unwrap().end(), + )); + } + let (els, pos) = + Self::extract_elements(s, pos + m.start(), &['$', '}', '\\'], &[]); + Some((SnippetElement::PlaceHolder(tab, els), pos + 1)) + } + + #[inline] + fn extract_text( + s: &str, + pos: usize, + escs: &[char], + loose_escs: &[char], + ) -> Option<(SnippetElement, usize)> { + let mut ele = String::new(); + let mut end = pos; + let mut chars_iter = s[pos..].chars().peekable(); + + while let Some(char) = chars_iter.next() { + if char == '\\' { + if let Some(&next) = chars_iter.peek() { + if escs.iter().chain(loose_escs.iter()).any(|c| *c == next) { + chars_iter.next(); + ele.push(next); + end += 1 + next.len_utf8(); + continue; + } + } + } + if escs.contains(&char) { + break; + } + ele.push(char); + end += char.len_utf8(); + } + if ele.is_empty() { + return None; + } + Some((SnippetElement::Text(ele), end)) + } + + #[inline] + pub fn text(&self) -> String { + let mut buf = String::new(); + self.write_text_to(&mut buf) + .expect("Snippet::write_text_to function unexpectedly return error"); + buf + } + + #[inline] + fn write_text_to(&self, buf: &mut Buffer) -> fmt::Result { + for snippet_element in self.elements.iter() { + snippet_element.write_text_to(buf)? + } + fmt::Result::Ok(()) + } + + #[inline] + pub fn tabs(&self, pos: usize) -> Vec<(usize, (usize, usize))> { + Self::elements_tabs(&self.elements, pos) + } + + pub fn elements_tabs( + elements: &[SnippetElement], + start: usize, + ) -> Vec<(usize, (usize, usize))> { + let mut tabs = Vec::new(); + let mut pos = start; + for el in elements { + match el { + SnippetElement::Text(t) => { + pos += t.len(); + } + SnippetElement::PlaceHolder(tab, els) => { + let placeholder_tabs = Self::elements_tabs(els, pos); + let end = pos + els.iter().map(|e| e.len()).sum::(); + tabs.push((*tab, (pos, end))); + tabs.extend_from_slice(&placeholder_tabs); + pos = end; + } + SnippetElement::Tabstop(tab) => { + tabs.push((*tab, (pos, pos))); + } + } + } + tabs + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_snippet() { + use SnippetElement::*; + + let s = "start $1${2:second ${3:third}} $0"; + let parsed = Snippet::from_str(s).unwrap(); + assert_eq!(s, parsed.to_string()); + + let text = "start second third "; + assert_eq!(text, parsed.text()); + + assert_eq!( + vec![(1, (6, 6)), (2, (6, 18)), (3, (13, 18)), (0, (19, 19))], + parsed.tabs(0) + ); + + let s = "start ${1}${2:second ${3:third}} $0and ${4}fourth"; + + let parsed = Snippet::from_str(s).unwrap(); + assert_eq!( + "start $1${2:second ${3:third}} $0and $4fourth", + parsed.to_string() + ); + + let text = "start second third and fourth"; + assert_eq!(text, parsed.text()); + + assert_eq!( + vec![ + (1, (6, 6)), + (2, (6, 18)), + (3, (13, 18)), + (0, (19, 19)), + (4, (23, 23)) + ], + parsed.tabs(0) + ); + + let s = "${1:first $6${2:second ${7}${3:third ${4:fourth ${5:fifth}}}}}"; + + let parsed = Snippet::from_str(s).unwrap(); + assert_eq!( + "${1:first $6${2:second $7${3:third ${4:fourth ${5:fifth}}}}}", + parsed.to_string() + ); + + let text = "first second third fourth fifth"; + assert_eq!(text, parsed.text()); + + assert_eq!( + vec![ + (1, (0, 31)), + (6, (6, 6)), + (2, (6, 31)), + (7, (13, 13)), + (3, (13, 31)), + (4, (19, 31)), + (5, (26, 31)) + ], + parsed.tabs(0) + ); + + assert_eq!( + Snippet { + elements: vec![PlaceHolder( + 1, + vec![ + Text("first ".into()), + Tabstop(6), + PlaceHolder( + 2, + vec![ + Text("second ".into()), + Tabstop(7), + PlaceHolder( + 3, + vec![ + Text("third ".into()), + PlaceHolder( + 4, + vec![ + Text("fourth ".into()), + PlaceHolder( + 5, + vec![Text("fifth".into())] + ) + ] + ) + ] + ) + ] + ) + ] + )] + }, + parsed + ); + + let s = "\\$1 start \\$2$3${4}${5:some text\\${6:third\\} $7}"; + + let parsed = Snippet::from_str(s).unwrap(); + assert_eq!( + "$1 start $2$3$4${5:some text${6:third} $7}", + parsed.to_string() + ); + + let text = "$1 start $2some text${6:third} "; + assert_eq!(text, parsed.text()); + + assert_eq!( + vec![(3, (11, 11)), (4, (11, 11)), (5, (11, 31)), (7, (31, 31))], + parsed.tabs(0) + ); + + assert_eq!( + Snippet { + elements: vec![ + Text("$1 start $2".into()), + Tabstop(3), + Tabstop(4), + PlaceHolder( + 5, + vec![Text("some text${6:third} ".into()), Tabstop(7)] + ) + ] + }, + parsed + ); + } + + #[test] + fn test_extract_tabstop() { + fn vec_of_tab_elms(s: &str) -> Vec<(usize, usize)> { + let mut pos = 0; + let mut vec = Vec::new(); + for char in s.chars() { + if let Some((SnippetElement::Tabstop(stop), end)) = + Snippet::extract_tabstop(s, pos) + { + vec.push((stop, end)); + } + pos += char.len_utf8(); + } + vec + } + + let s = "start $1${2:second ${3:third}} $0"; + assert_eq!(&[(1, 8), (0, 33)][..], &vec_of_tab_elms(s)[..]); + + let s = "start ${1}${2:second ${3:third}} $0and ${4}fourth"; + assert_eq!(&[(1, 10), (0, 35), (4, 43)][..], &vec_of_tab_elms(s)[..]); + + let s = "$s$1first${2}$second$3${4}${5}$6and${7}$8fourth$9$$$10$$${11}$$$12$$$13$$${14}$$${15}"; + assert_eq!( + &[ + (1, 4), + (2, 13), + (3, 22), + (4, 26), + (5, 30), + (6, 32), + (7, 39), + (8, 41), + (9, 49), + (10, 54), + (11, 61), + (12, 66), + (13, 71), + (14, 78), + (15, 85) + ][..], + &vec_of_tab_elms(s)[..] + ); + + let s = "$s$1ένα${2}$τρία$3${4}${5}$6τέσσερα${7}$8πέντε$9$$$10$$${11}$$$12$$$13$$${14}$$${15}"; + assert_eq!( + &[ + (1, 4), + (2, 14), + (3, 25), + (4, 29), + (5, 33), + (6, 35), + (7, 53), + (8, 55), + (9, 67), + (10, 72), + (11, 79), + (12, 84), + (13, 89), + (14, 96), + (15, 103) + ][..], + &vec_of_tab_elms(s)[..] + ); + } + + #[test] + fn test_extract_placeholder() { + use super::SnippetElement::*; + let s1 = "${1:first ${2:second ${3:third ${4:fourth ${5:fifth}}}}}"; + + assert_eq!( + ( + PlaceHolder( + 1, + vec![ + Text("first ".into()), + PlaceHolder( + 2, + vec![ + Text("second ".into()), + PlaceHolder( + 3, + vec![ + Text("third ".into()), + PlaceHolder( + 4, + vec![ + Text("fourth ".into()), + PlaceHolder( + 5, + vec![Text("fifth".into())] + ) + ] + ) + ] + ) + ] + ) + ] + ), + 56 + ), + Snippet::extract_placeholder(s1, 0).unwrap() + ); + + let s1 = "${1:first}${2:second}${3:third }${4:fourth ${5:fifth}}}}}"; + assert_eq!( + (PlaceHolder(1, vec![Text("first".to_owned())]), 10), + Snippet::extract_placeholder(s1, 0).unwrap() + ); + assert_eq!( + (PlaceHolder(2, vec![Text("second".to_owned())]), 21), + Snippet::extract_placeholder(s1, 10).unwrap() + ); + assert_eq!( + (PlaceHolder(3, vec![Text("third ".to_owned())]), 32), + Snippet::extract_placeholder(s1, 21).unwrap() + ); + + assert_eq!( + ( + PlaceHolder( + 4, + vec![ + Text("fourth ".into()), + PlaceHolder(5, vec![Text("fifth".into())]) + ] + ), + 54 + ), + Snippet::extract_placeholder(s1, 32).unwrap() + ); + } + + #[test] + fn test_extract_text() { + use SnippetElement::*; + + // 1. ==================================================================================== + + let s = "start $1${2:second ${3:third}} $0"; + let (snip_elm, end) = Snippet::extract_text(s, 0, &['$'], &[]).unwrap(); + assert_eq!((Text("start ".to_owned()), 6), (snip_elm, end)); + + let (snip_elm, end) = + Snippet::extract_text(s, end + 1, &['$'], &[]).unwrap(); + assert_eq!((Text("1".to_owned()), 8), (snip_elm, end)); + + let (snip_elm, end) = + Snippet::extract_text(s, end + 1, &['$'], &[]).unwrap(); + assert_eq!((Text("{2:second ".to_owned()), 19), (snip_elm, end)); + + let (snip_elm, end) = + Snippet::extract_text(s, end + 1, &['$'], &[]).unwrap(); + assert_eq!((Text("{3:third}} ".to_owned()), 31), (snip_elm, end)); + + let (snip_elm, end) = + Snippet::extract_text(s, end + 1, &['$'], &[]).unwrap(); + assert_eq!((Text("0".to_owned()), 33), (snip_elm, end)); + + // 2. ==================================================================================== + + let s = "start $1${2:second ${3:third}} $0"; + + let (snip_elm, end) = Snippet::extract_text(s, 0, &['{'], &[]).unwrap(); + assert_eq!((Text("start $1$".to_owned()), 9), (snip_elm, end)); + + let (snip_elm, end) = + Snippet::extract_text(s, end + 1, &['{'], &[]).unwrap(); + assert_eq!((Text("2:second $".to_owned()), 20), (snip_elm, end)); + + let (snip_elm, end) = + Snippet::extract_text(s, end + 1, &['{'], &[]).unwrap(); + assert_eq!((Text("3:third}} $0".to_owned()), 33), (snip_elm, end)); + + // 3. ==================================================================================== + + let s = "start $1${2:second ${3:third}} $0"; + + let (snip_elm, end) = Snippet::extract_text(s, 0, &['}'], &[]).unwrap(); + assert_eq!( + (Text("start $1${2:second ${3:third".to_owned()), 28), + (snip_elm, end) + ); + + assert_eq!(None, Snippet::extract_text(s, end + 1, &['}'], &[])); + + let (snip_elm, end) = + Snippet::extract_text(s, end + 2, &['}'], &[]).unwrap(); + assert_eq!((Text(" $0".to_owned()), 33), (snip_elm, end)); + + // 4. ==================================================================================== + + let s = "start $1${2:second ${3:third}} $0"; + + let (snip_elm, end) = Snippet::extract_text(s, 0, &['\\'], &[]).unwrap(); + assert_eq!((Text(s.to_owned()), 33), (snip_elm, end)); + + // 5. ==================================================================================== + + let s = "start \\$1${2:second \\${3:third}} $0"; + + let (snip_elm, end) = + Snippet::extract_text(s, 0, &['$', '\\'], &[]).unwrap(); + assert_eq!((Text("start $1".to_owned()), 9), (snip_elm, end)); + + let (snip_elm, end) = + Snippet::extract_text(s, end + 1, &['$', '\\'], &[]).unwrap(); + assert_eq!( + (Text("{2:second ${3:third}} ".to_owned()), 33), + (snip_elm, end) + ); + + let (snip_elm, end) = + Snippet::extract_text(s, end + 1, &['$', '\\'], &[]).unwrap(); + assert_eq!((Text("0".to_owned()), 35), (snip_elm, end)); + + // 6. ==================================================================================== + + let s = "\\{start $1${2:second $\\{3:third}} $0}"; + + let (snip_elm, end) = + Snippet::extract_text(s, 0, &['{', '\\'], &[]).unwrap(); + assert_eq!((Text("{start $1$".to_owned()), 11), (snip_elm, end)); + + let (snip_elm, end) = + Snippet::extract_text(s, end + 1, &['{', '\\'], &[]).unwrap(); + assert_eq!( + (Text("2:second ${3:third}} $0}".to_owned()), 37), + (snip_elm, end) + ); + + // 7. ==================================================================================== + + let s = "{start $1${2}:second $\\{3:third}} $0}"; + + let (snip_elm, end) = + Snippet::extract_text(s, 0, &['}', '\\'], &[]).unwrap(); + assert_eq!((Text("{start $1${2".to_owned()), 12), (snip_elm, end)); + + let (snip_elm, end) = + Snippet::extract_text(s, end + 1, &['}', '\\'], &[]).unwrap(); + assert_eq!((Text(":second $".to_owned()), 22), (snip_elm, end)); + + assert_eq!(None, Snippet::extract_text(s, end, &['}', '\\'], &[])); + + let (snip_elm, end) = + Snippet::extract_text(s, end + 1, &['}', '\\'], &[]).unwrap(); + assert_eq!((Text("{3:third".to_owned()), 31), (snip_elm, end)); + + assert_eq!(None, Snippet::extract_text(s, end + 1, &['}', '\\'], &[])); + + let (snip_elm, end) = + Snippet::extract_text(s, end + 2, &['}', '\\'], &[]).unwrap(); + assert_eq!((Text(" $0".to_owned()), 36), (snip_elm, end)); + + // 8. ==================================================================================== + + let s = "{start $1${2}:second $\\{3:third}} $0}"; + + let (snip_elm, end) = + Snippet::extract_text(s, 0, &['$', '\\'], &['}']).unwrap(); + assert_eq!((Text("{start ".to_owned()), 7), (snip_elm, end)); + + let (snip_elm, end) = + Snippet::extract_text(s, end + 1, &['$', '\\'], &['}']).unwrap(); + assert_eq!((Text("1".to_owned()), 9), (snip_elm, end)); + + let (snip_elm, end) = + Snippet::extract_text(s, end + 1, &['$', '\\'], &['}']).unwrap(); + assert_eq!((Text("{2}:second ".to_owned()), 21), (snip_elm, end)); + + assert_eq!( + None, + Snippet::extract_text(s, end + 1, &['$', '\\'], &['}']) + ); + + let (snip_elm, end) = + Snippet::extract_text(s, end + 2, &['$', '\\'], &['}']).unwrap(); + assert_eq!((Text("{3:third}} ".to_owned()), 34), (snip_elm, end)); + + let (snip_elm, end) = + Snippet::extract_text(s, end + 1, &['$', '\\'], &['}']).unwrap(); + assert_eq!((Text("0}".to_owned()), 37), (snip_elm, end)); + + // 9. ==================================================================================== + + let s = "{start $1${2}:second $\\{3:third}} $0}"; + + let (snip_elm, end) = + Snippet::extract_text(s, 0, &['$', '}', '\\'], &[]).unwrap(); + assert_eq!((Text("{start ".to_owned()), 7), (snip_elm, end)); + + let (snip_elm, end) = + Snippet::extract_text(s, end + 1, &['$', '}', '\\'], &[]).unwrap(); + assert_eq!((Text("1".to_owned()), 9), (snip_elm, end)); + + let (snip_elm, end) = + Snippet::extract_text(s, end + 1, &['$', '}', '\\'], &[]).unwrap(); + assert_eq!((Text("{2".to_owned()), 12), (snip_elm, end)); + + let (snip_elm, end) = + Snippet::extract_text(s, end + 1, &['$', '}', '\\'], &[]).unwrap(); + assert_eq!((Text(":second ".to_owned()), 21), (snip_elm, end)); + + assert_eq!( + None, + Snippet::extract_text(s, end + 1, &['$', '}', '\\'], &[]) + ); + + let (snip_elm, end) = + Snippet::extract_text(s, end + 2, &['$', '}', '\\'], &[]).unwrap(); + assert_eq!((Text("{3:third".to_owned()), 31), (snip_elm, end)); + + assert_eq!( + None, + Snippet::extract_text(s, end + 1, &['$', '}', '\\'], &[]) + ); + + let (snip_elm, end) = + Snippet::extract_text(s, end + 2, &['$', '}', '\\'], &[]).unwrap(); + assert_eq!((Text(" ".to_owned()), 34), (snip_elm, end)); + + let (snip_elm, end) = + Snippet::extract_text(s, end + 1, &['$', '}', '\\'], &[]).unwrap(); + assert_eq!((Text("0".to_owned()), 36), (snip_elm, end)); + + assert_eq!( + None, + Snippet::extract_text(s, end + 1, &['$', '}', '\\'], &[]) + ); + + // 10. ==================================================================================== + + let s = "{start $1${2}:second $\\{3:third}} $0}"; + + assert_eq!( + None, + Snippet::extract_text(s, 0, &['$', '{', '}', '\\'], &[]) + ); + + let (snip_elm, end) = + Snippet::extract_text(s, 1, &['$', '{', '}', '\\'], &[]).unwrap(); + assert_eq!((Text("start ".to_owned()), 7), (snip_elm, end)); + + let (snip_elm, end) = + Snippet::extract_text(s, end + 1, &['$', '{', '}', '\\'], &[]).unwrap(); + assert_eq!((Text("1".to_owned()), 9), (snip_elm, end)); + + assert_eq!( + None, + Snippet::extract_text(s, end + 1, &['$', '{', '}', '\\'], &[]) + ); + + let (snip_elm, end) = + Snippet::extract_text(s, end + 2, &['$', '{', '}', '\\'], &[]).unwrap(); + assert_eq!((Text("2".to_owned()), 12), (snip_elm, end)); + + let (snip_elm, end) = + Snippet::extract_text(s, end + 1, &['$', '{', '}', '\\'], &[]).unwrap(); + assert_eq!((Text(":second ".to_owned()), 21), (snip_elm, end)); + + let (snip_elm, end) = + Snippet::extract_text(s, end + 1, &['$', '{', '}', '\\'], &[]).unwrap(); + assert_eq!((Text("{3:third".to_owned()), 31), (snip_elm, end)); + + assert_eq!( + None, + Snippet::extract_text(s, end + 1, &['$', '{', '}', '\\'], &[]) + ); + + let (snip_elm, end) = + Snippet::extract_text(s, end + 2, &['$', '{', '}', '\\'], &[]).unwrap(); + assert_eq!((Text(" ".to_owned()), 34), (snip_elm, end)); + + let (snip_elm, end) = + Snippet::extract_text(s, end + 1, &['$', '{', '}', '\\'], &[]).unwrap(); + assert_eq!((Text("0".to_owned()), 36), (snip_elm, end)); + + assert_eq!( + None, + Snippet::extract_text(s, end + 1, &['$', '{', '}', '\\'], &[]) + ); + + // 11. ==================================================================================== + + let s = "{start\\\\ $1${2}:second\\ $\\{3:third}} $0}"; + + assert_eq!( + None, + Snippet::extract_text(s, 0, &['$', '{', '}', '\\'], &[]) + ); + + let (snip_elm, end) = + Snippet::extract_text(s, 1, &['$', '{', '}', '\\'], &[]).unwrap(); + assert_eq!((Text("start\\ ".to_owned()), 9), (snip_elm, end)); + + let (snip_elm, end) = + Snippet::extract_text(s, end + 1, &['$', '{', '}', '\\'], &[]).unwrap(); + assert_eq!((Text("1".to_owned()), 11), (snip_elm, end)); + + assert_eq!( + None, + Snippet::extract_text(s, end + 1, &['$', '{', '}', '\\'], &[]) + ); + + let (snip_elm, end) = + Snippet::extract_text(s, end + 2, &['$', '{', '}', '\\'], &[]).unwrap(); + assert_eq!((Text("2".to_owned()), 14), (snip_elm, end)); + + let (snip_elm, end) = + Snippet::extract_text(s, end + 1, &['$', '{', '}', '\\'], &[]).unwrap(); + assert_eq!((Text(":second".to_owned()), 22), (snip_elm, end)); + + let (snip_elm, end) = + Snippet::extract_text(s, end + 1, &['$', '{', '}', '\\'], &[]).unwrap(); + assert_eq!((Text(" ".to_owned()), 24), (snip_elm, end)); + + let (snip_elm, end) = + Snippet::extract_text(s, end + 1, &['$', '{', '}', '\\'], &[]).unwrap(); + assert_eq!((Text("{3:third".to_owned()), 34), (snip_elm, end)); + + assert_eq!( + None, + Snippet::extract_text(s, end + 1, &['$', '{', '}', '\\'], &[]) + ); + + let (snip_elm, end) = + Snippet::extract_text(s, end + 2, &['$', '{', '}', '\\'], &[]).unwrap(); + assert_eq!((Text(" ".to_owned()), 37), (snip_elm, end)); + + let (snip_elm, end) = + Snippet::extract_text(s, end + 1, &['$', '{', '}', '\\'], &[]).unwrap(); + assert_eq!((Text("0".to_owned()), 39), (snip_elm, end)); + + assert_eq!( + None, + Snippet::extract_text(s, end + 1, &['$', '{', '}', '\\'], &[]) + ); + } +} diff --git a/lapce-app/src/source_control.rs b/lapce-app/src/source_control.rs new file mode 100644 index 0000000..fc27b55 --- /dev/null +++ b/lapce-app/src/source_control.rs @@ -0,0 +1,104 @@ +use std::{path::PathBuf, rc::Rc}; + +use floem::{ + keyboard::Modifiers, + reactive::{RwSignal, Scope, SignalWith}, +}; +use indexmap::IndexMap; +use lapce_core::mode::Mode; +use lapce_rpc::source_control::FileDiff; + +use crate::{ + command::{CommandExecuted, CommandKind}, + editor::EditorData, + keypress::{KeyPressFocus, condition::Condition}, + main_split::Editors, + window_tab::CommonData, +}; + +#[derive(Clone, Debug)] +pub struct SourceControlData { + // VCS modified files & whether they should be included in the next commit + pub file_diffs: RwSignal>, + pub branch: RwSignal, + pub branches: RwSignal>, + pub tags: RwSignal>, + pub editor: EditorData, + pub common: Rc, +} + +impl KeyPressFocus for SourceControlData { + fn get_mode(&self) -> Mode { + Mode::Insert + } + + fn check_condition(&self, condition: Condition) -> bool { + matches!( + condition, + Condition::PanelFocus | Condition::SourceControlFocus + ) + } + + fn run_command( + &self, + command: &crate::command::LapceCommand, + count: Option, + mods: Modifiers, + ) -> CommandExecuted { + match &command.kind { + CommandKind::Edit(_) + | CommandKind::Move(_) + | CommandKind::MultiSelection(_) => { + self.editor.run_command(command, count, mods) + } + _ => CommandExecuted::No, + } + } + + fn receive_char(&self, c: &str) { + self.editor.receive_char(c); + } +} + +impl SourceControlData { + pub fn new(cx: Scope, editors: Editors, common: Rc) -> Self { + Self { + file_diffs: cx.create_rw_signal(IndexMap::new()), + branch: cx.create_rw_signal("".to_string()), + branches: cx.create_rw_signal(im::Vector::new()), + tags: cx.create_rw_signal(im::Vector::new()), + editor: editors.make_local(cx, common.clone()), + common, + } + } + + pub fn commit(&self) { + let diffs: Vec = self.file_diffs.with_untracked(|file_diffs| { + file_diffs + .iter() + .filter_map( + |(_, (diff, checked))| { + if *checked { Some(diff) } else { None } + }, + ) + .cloned() + .collect() + }); + if diffs.is_empty() { + return; + } + + let message = self + .editor + .doc() + .buffer + .with_untracked(|buffer| buffer.to_string()); + let message = message.trim(); + if message.is_empty() { + return; + } + + self.editor.reset(); + self.common.proxy.git_commit(message.to_string(), diffs); + } +} diff --git a/lapce-app/src/status.rs b/lapce-app/src/status.rs new file mode 100644 index 0000000..707affd --- /dev/null +++ b/lapce-app/src/status.rs @@ -0,0 +1,473 @@ +use std::{ + rc::Rc, + sync::{Arc, atomic::AtomicU64}, +}; + +use floem::{ + View, + event::EventPropagation, + reactive::{ + Memo, ReadSignal, RwSignal, SignalGet, SignalUpdate, SignalWith, create_memo, + }, + style::{AlignItems, CursorStyle, Display}, + views::{Decorators, dyn_stack, label, stack, svg}, +}; +use indexmap::IndexMap; +use lapce_core::mode::{Mode, VisualMode}; +use lsp_types::{DiagnosticSeverity, ProgressToken}; + +use crate::{ + app::clickable_icon, + command::LapceWorkbenchCommand, + config::{LapceConfig, color::LapceColor, icon::LapceIcons}, + editor::EditorData, + listener::Listener, + palette::kind::PaletteKind, + panel::{kind::PanelKind, position::PanelContainerPosition}, + source_control::SourceControlData, + window_tab::{WindowTabData, WorkProgress}, +}; + +pub fn status( + window_tab_data: Rc, + source_control: SourceControlData, + workbench_command: Listener, + status_height: RwSignal, + _config: ReadSignal>, +) -> impl View { + let config = window_tab_data.common.config; + let diagnostics = window_tab_data.main_split.diagnostics; + let editor = window_tab_data.main_split.active_editor; + let panel = window_tab_data.panel.clone(); + let palette = window_tab_data.palette.clone(); + let diagnostic_count = create_memo(move |_| { + let mut errors = 0; + let mut warnings = 0; + for (_, diagnostics) in diagnostics.get().iter() { + for diagnostic in diagnostics.diagnostics.get().iter() { + if let Some(severity) = diagnostic.severity { + match severity { + DiagnosticSeverity::ERROR => errors += 1, + DiagnosticSeverity::WARNING => warnings += 1, + _ => (), + } + } + } + } + (errors, warnings) + }); + let branch = source_control.branch; + let file_diffs = source_control.file_diffs; + let branch = move || { + format!( + "{}{}", + branch.get(), + if file_diffs.with(|diffs| diffs.is_empty()) { + "" + } else { + "*" + } + ) + }; + + let progresses = window_tab_data.progresses; + let mode = create_memo(move |_| window_tab_data.mode()); + let pointer_down = floem::reactive::create_rw_signal(false); + + stack(( + stack(( + label(move || match mode.get() { + Mode::Normal => "Normal".to_string(), + Mode::Insert => "Insert".to_string(), + Mode::Visual(mode) => match mode { + VisualMode::Normal => "Visual".to_string(), + VisualMode::Linewise => "Visual Line".to_string(), + VisualMode::Blockwise => "Visual Block".to_string(), + }, + Mode::Terminal => "Terminal".to_string(), + }) + .style(move |s| { + let config = config.get(); + let display = if config.core.modal { + Display::Flex + } else { + Display::None + }; + + let (bg, fg) = match mode.get() { + Mode::Normal => ( + LapceColor::STATUS_MODAL_NORMAL_BACKGROUND, + LapceColor::STATUS_MODAL_NORMAL_FOREGROUND, + ), + Mode::Insert => ( + LapceColor::STATUS_MODAL_INSERT_BACKGROUND, + LapceColor::STATUS_MODAL_INSERT_FOREGROUND, + ), + Mode::Visual(_) => ( + LapceColor::STATUS_MODAL_VISUAL_BACKGROUND, + LapceColor::STATUS_MODAL_VISUAL_FOREGROUND, + ), + Mode::Terminal => ( + LapceColor::STATUS_MODAL_TERMINAL_BACKGROUND, + LapceColor::STATUS_MODAL_TERMINAL_FOREGROUND, + ), + }; + + let bg = config.color(bg); + let fg = config.color(fg); + + s.display(display) + .padding_horiz(10.0) + .color(fg) + .background(bg) + .height_pct(100.0) + .align_items(Some(AlignItems::Center)) + .selectable(false) + }), + stack(( + svg(move || config.get().ui_svg(LapceIcons::SCM)).style(move |s| { + let config = config.get(); + let icon_size = config.ui.icon_size() as f32; + s.size(icon_size, icon_size) + .color(config.color(LapceColor::LAPCE_ICON_ACTIVE)) + }), + label(branch).style(move |s| { + s.margin_left(10.0) + .color(config.get().color(LapceColor::STATUS_FOREGROUND)) + .selectable(false) + }), + )) + .style(move |s| { + s.display(if branch().is_empty() { + Display::None + } else { + Display::Flex + }) + .height_pct(100.0) + .padding_horiz(10.0) + .align_items(Some(AlignItems::Center)) + .hover(|s| { + s.cursor(CursorStyle::Pointer).background( + config.get().color(LapceColor::PANEL_HOVERED_BACKGROUND), + ) + }) + }) + .on_event_cont(floem::event::EventListener::PointerDown, move |_| { + pointer_down.set(true); + }) + .on_event( + floem::event::EventListener::PointerUp, + move |_| { + if pointer_down.get() { + workbench_command + .send(LapceWorkbenchCommand::PaletteSCMReferences); + } + pointer_down.set(false); + EventPropagation::Continue + }, + ), + { + let panel = panel.clone(); + stack(( + svg(move || config.get().ui_svg(LapceIcons::ERROR)).style( + move |s| { + let config = config.get(); + let size = config.ui.icon_size() as f32; + s.size(size, size) + .color(config.color(LapceColor::LAPCE_ICON_ACTIVE)) + }, + ), + label(move || diagnostic_count.get().0.to_string()).style( + move |s| { + s.margin_left(5.0) + .color( + config + .get() + .color(LapceColor::STATUS_FOREGROUND), + ) + .selectable(false) + }, + ), + svg(move || config.get().ui_svg(LapceIcons::WARNING)).style( + move |s| { + let config = config.get(); + let size = config.ui.icon_size() as f32; + s.size(size, size) + .margin_left(5.0) + .color(config.color(LapceColor::LAPCE_ICON_ACTIVE)) + }, + ), + label(move || diagnostic_count.get().1.to_string()).style( + move |s| { + s.margin_left(5.0) + .color( + config + .get() + .color(LapceColor::STATUS_FOREGROUND), + ) + .selectable(false) + }, + ), + )) + .on_click_stop(move |_| { + panel.show_panel(&PanelKind::Problem); + }) + .style(move |s| { + s.height_pct(100.0) + .padding_horiz(10.0) + .items_center() + .hover(|s| { + s.cursor(CursorStyle::Pointer).background( + config + .get() + .color(LapceColor::PANEL_HOVERED_BACKGROUND), + ) + }) + }) + }, + progress_view(config, progresses), + )) + .style(|s| { + s.height_pct(100.0) + .min_width(0.0) + .flex_basis(0.0) + .flex_grow(1.0) + .items_center() + }), + stack(( + { + let panel = panel.clone(); + let icon = { + let panel = panel.clone(); + move || { + if panel + .is_container_shown(&PanelContainerPosition::Left, true) + { + LapceIcons::SIDEBAR_LEFT + } else { + LapceIcons::SIDEBAR_LEFT_OFF + } + } + }; + clickable_icon( + icon, + move || { + panel.toggle_container_visual(&PanelContainerPosition::Left) + }, + || false, + || false, + || "Toggle Left Panel", + config, + ) + }, + { + let panel = panel.clone(); + let icon = { + let panel = panel.clone(); + move || { + if panel.is_container_shown( + &PanelContainerPosition::Bottom, + true, + ) { + LapceIcons::LAYOUT_PANEL + } else { + LapceIcons::LAYOUT_PANEL_OFF + } + } + }; + clickable_icon( + icon, + move || { + panel + .toggle_container_visual(&PanelContainerPosition::Bottom) + }, + || false, + || false, + || "Toggle Bottom Panel", + config, + ) + }, + { + let panel = panel.clone(); + let icon = { + let panel = panel.clone(); + move || { + if panel + .is_container_shown(&PanelContainerPosition::Right, true) + { + LapceIcons::SIDEBAR_RIGHT + } else { + LapceIcons::SIDEBAR_RIGHT_OFF + } + } + }; + clickable_icon( + icon, + move || { + panel.toggle_container_visual(&PanelContainerPosition::Right) + }, + || false, + || false, + || "Toggle Right Panel", + config, + ) + }, + )) + .style(move |s| { + s.height_pct(100.0) + .items_center() + .color(config.get().color(LapceColor::STATUS_FOREGROUND)) + }), + stack({ + let palette_clone = palette.clone(); + let cursor_info = status_text(config, editor, move || { + if let Some(editor) = editor.get() { + let mut status = String::new(); + let cursor = editor.cursor().get(); + if let Some((line, column, character)) = editor + .doc_signal() + .get() + .buffer + .with(|buffer| cursor.get_line_col_char(buffer)) + { + status = format!( + "Ln {}, Col {}, Char {}", + line + 1, + column + 1, + character, + ); + } + if let Some(selection) = cursor.get_selection() { + let selection_range = selection.0.abs_diff(selection.1); + + if selection.0 != selection.1 { + status = + format!("{status} ({selection_range} selected)"); + } + } + let selection_count = cursor.get_selection_count(); + if selection_count > 1 { + status = format!("{status} {selection_count} selections"); + } + return status; + } + String::new() + }) + .on_click_stop(move |_| { + palette_clone.run(PaletteKind::Line); + }); + let palette_clone = palette.clone(); + let line_ending_info = status_text(config, editor, move || { + if let Some(editor) = editor.get() { + let doc = editor.doc_signal().get(); + doc.buffer.with(|b| b.line_ending()).as_str() + } else { + "" + } + }) + .on_click_stop(move |_| { + palette_clone.run(PaletteKind::LineEnding); + }); + let palette_clone = palette.clone(); + let language_info = status_text(config, editor, move || { + if let Some(editor) = editor.get() { + let doc = editor.doc_signal().get(); + doc.syntax().with(|s| s.language.name()) + } else { + "unknown" + } + }) + .on_click_stop(move |_| { + palette_clone.run(PaletteKind::Language); + }); + (cursor_info, line_ending_info, language_info) + }) + .style(|s| { + s.height_pct(100.0) + .flex_basis(0.0) + .flex_grow(1.0) + .justify_end() + }), + )) + .on_resize(move |rect| { + let height = rect.height(); + if height != status_height.get_untracked() { + status_height.set(height); + } + }) + .style(move |s| { + let config = config.get(); + s.border_top(1.0) + .border_color(config.color(LapceColor::LAPCE_BORDER)) + .background(config.color(LapceColor::STATUS_BACKGROUND)) + .flex_basis(config.ui.status_height() as f32) + .flex_grow(0.0) + .flex_shrink(0.0) + .items_center() + }) + .debug_name("Status/Bottom Bar") +} + +fn progress_view( + config: ReadSignal>, + progresses: RwSignal>, +) -> impl View { + let id = AtomicU64::new(0); + dyn_stack( + move || progresses.get(), + move |_| id.fetch_add(1, std::sync::atomic::Ordering::Relaxed), + move |(_, p)| { + let progress = match p.message { + Some(message) if !message.is_empty() => { + format!("{}: {}", p.title, message) + } + _ => p.title, + }; + label(move || progress.clone()).style(move |s| { + s.height_pct(100.0) + .min_width(0.0) + .margin_left(10.0) + .text_ellipsis() + .selectable(false) + .items_center() + .color(config.get().color(LapceColor::STATUS_FOREGROUND)) + }) + }, + ) + .style(move |s| s.flex_row().height_pct(100.0).min_width(0.0)) +} + +fn status_text( + config: ReadSignal>, + editor: Memo>, + text: impl Fn() -> S + 'static, +) -> impl View { + label(text).style(move |s| { + let config = config.get(); + let display = if editor + .get() + .map(|editor| { + editor.doc_signal().get().content.with(|c| { + use crate::doc::DocContent; + matches!(c, DocContent::File { .. } | DocContent::Scratch { .. }) + }) + }) + .unwrap_or(false) + { + Display::Flex + } else { + Display::None + }; + + s.display(display) + .height_full() + .padding_horiz(10.0) + .items_center() + .color(config.color(LapceColor::STATUS_FOREGROUND)) + .hover(|s| { + s.cursor(CursorStyle::Pointer) + .background(config.color(LapceColor::PANEL_HOVERED_BACKGROUND)) + }) + .selectable(false) + }) +} diff --git a/lapce-app/src/terminal/data.rs b/lapce-app/src/terminal/data.rs new file mode 100644 index 0000000..51badf7 --- /dev/null +++ b/lapce-app/src/terminal/data.rs @@ -0,0 +1,861 @@ +use std::{collections::HashMap, path::PathBuf, rc::Rc, sync::Arc}; + +use alacritty_terminal::{ + Term, + grid::{Dimensions, Scroll}, + selection::{Selection, SelectionType}, + term::{TermMode, test::TermSize}, + vi_mode::ViMotion, +}; +use anyhow::anyhow; +use floem::{ + keyboard::{Key, KeyEvent, Modifiers, NamedKey}, + reactive::{RwSignal, Scope, SignalGet, SignalUpdate, SignalWith}, + views::editor::text::SystemClipboard, +}; +use lapce_core::{ + command::{EditCommand, FocusCommand, ScrollCommand}, + mode::{Mode, VisualMode}, + movement::{LinePosition, Movement}, + register::Clipboard, +}; +use lapce_rpc::{ + dap_types::RunDebugConfig, + terminal::{TermId, TerminalProfile}, +}; +use parking_lot::RwLock; +use url::Url; + +use super::{ + event::TermEvent, + raw::{EventProxy, RawTerminal}, +}; +use crate::{ + command::{CommandExecuted, CommandKind, InternalCommand}, + debug::{RunDebugMode, RunDebugProcess}, + keypress::{KeyPressFocus, condition::Condition}, + window_tab::CommonData, + workspace::LapceWorkspace, +}; + +#[derive(Clone, Debug)] +pub struct TerminalData { + pub scope: Scope, + pub term_id: TermId, + pub workspace: Arc, + pub title: RwSignal, + pub launch_error: RwSignal>, + pub mode: RwSignal, + pub visual_mode: RwSignal, + pub raw: RwSignal>>, + pub run_debug: RwSignal>, + pub common: Rc, +} + +impl KeyPressFocus for TerminalData { + fn get_mode(&self) -> Mode { + self.mode.get_untracked() + } + + fn check_condition(&self, condition: Condition) -> bool { + matches!(condition, Condition::TerminalFocus | Condition::PanelFocus) + } + + fn run_command( + &self, + command: &crate::command::LapceCommand, + count: Option, + _mods: Modifiers, + ) -> crate::command::CommandExecuted { + self.common.view_id.get_untracked().request_paint(); + let config = self.common.config.get_untracked(); + match &command.kind { + CommandKind::Move(cmd) => { + let movement = cmd.to_movement(count); + let raw = self.raw.get_untracked(); + let mut raw = raw.write(); + let term = &mut raw.term; + match movement { + Movement::Left => { + term.vi_motion(ViMotion::Left); + } + Movement::Right => { + term.vi_motion(ViMotion::Right); + } + Movement::Up => { + term.vi_motion(ViMotion::Up); + } + Movement::Down => { + term.vi_motion(ViMotion::Down); + } + Movement::FirstNonBlank => { + term.vi_motion(ViMotion::FirstOccupied); + } + Movement::StartOfLine => { + term.vi_motion(ViMotion::First); + } + Movement::EndOfLine => { + term.vi_motion(ViMotion::Last); + } + Movement::WordForward => { + term.vi_motion(ViMotion::SemanticRight); + } + Movement::WordEndForward => { + term.vi_motion(ViMotion::SemanticRightEnd); + } + Movement::WordBackward => { + term.vi_motion(ViMotion::SemanticLeft); + } + Movement::Line(line) => { + match line { + LinePosition::First => { + term.scroll_display(Scroll::Top); + term.vi_mode_cursor.point.line = term.topmost_line(); + } + LinePosition::Last => { + term.scroll_display(Scroll::Bottom); + term.vi_mode_cursor.point.line = + term.bottommost_line(); + } + LinePosition::Line(_) => {} + }; + } + _ => (), + }; + } + CommandKind::Edit(cmd) => match cmd { + EditCommand::NormalMode => { + if !config.core.modal { + return CommandExecuted::Yes; + } + self.mode.set(Mode::Normal); + let raw = self.raw.get_untracked(); + let mut raw = raw.write(); + let term = &mut raw.term; + if !term.mode().contains(TermMode::VI) { + term.toggle_vi_mode(); + } + term.selection = None; + } + EditCommand::ToggleVisualMode => { + self.toggle_visual(VisualMode::Normal); + } + EditCommand::ToggleLinewiseVisualMode => { + self.toggle_visual(VisualMode::Linewise); + } + EditCommand::ToggleBlockwiseVisualMode => { + self.toggle_visual(VisualMode::Blockwise); + } + EditCommand::InsertMode => { + self.mode.set(Mode::Terminal); + let raw = self.raw.get_untracked(); + let mut raw = raw.write(); + let term = &mut raw.term; + if term.mode().contains(TermMode::VI) { + term.toggle_vi_mode(); + } + let scroll = alacritty_terminal::grid::Scroll::Bottom; + term.scroll_display(scroll); + term.selection = None; + } + EditCommand::ClipboardCopy => { + let mut clipboard = SystemClipboard::new(); + if matches!(self.mode.get_untracked(), Mode::Visual(_)) { + self.mode.set(Mode::Normal); + } + let raw = self.raw.get_untracked(); + let mut raw = raw.write(); + let term = &mut raw.term; + if let Some(content) = term.selection_to_string() { + clipboard.put_string(content); + } + if self.mode.get_untracked() != Mode::Terminal { + term.selection = None; + } + } + EditCommand::ClipboardPaste => { + let mut clipboard = SystemClipboard::new(); + let mut check_bracketed_paste: bool = false; + if self.mode.get_untracked() == Mode::Terminal { + let raw = self.raw.get_untracked(); + let mut raw = raw.write(); + let term = &mut raw.term; + term.selection = None; + if term.mode().contains(TermMode::BRACKETED_PASTE) { + check_bracketed_paste = true; + } + } + if let Some(s) = clipboard.get_string() { + if check_bracketed_paste { + self.receive_char("\x1b[200~"); + self.receive_char(&s.replace('\x1b', "")); + self.receive_char("\x1b[201~"); + } else { + self.receive_char(&s); + } + } + } + _ => return CommandExecuted::No, + }, + CommandKind::Scroll(cmd) => match cmd { + ScrollCommand::PageUp => { + let raw = self.raw.get_untracked(); + let mut raw = raw.write(); + let term = &mut raw.term; + let scroll_lines = term.screen_lines() as i32 / 2; + term.vi_mode_cursor = + term.vi_mode_cursor.scroll(term, scroll_lines); + + term.scroll_display(alacritty_terminal::grid::Scroll::Delta( + scroll_lines, + )); + } + ScrollCommand::PageDown => { + let raw = self.raw.get_untracked(); + let mut raw = raw.write(); + let term = &mut raw.term; + let scroll_lines = -(term.screen_lines() as i32 / 2); + term.vi_mode_cursor = + term.vi_mode_cursor.scroll(term, scroll_lines); + + term.scroll_display(alacritty_terminal::grid::Scroll::Delta( + scroll_lines, + )); + } + _ => return CommandExecuted::No, + }, + CommandKind::Focus(cmd) => match cmd { + FocusCommand::SplitVertical => { + self.common.internal_command.send( + InternalCommand::SplitTerminal { + term_id: self.term_id, + }, + ); + } + FocusCommand::SplitHorizontal => { + self.common.internal_command.send( + InternalCommand::SplitTerminal { + term_id: self.term_id, + }, + ); + } + FocusCommand::SplitLeft => { + self.common.internal_command.send( + InternalCommand::SplitTerminalPrevious { + term_id: self.term_id, + }, + ); + } + FocusCommand::SplitRight => { + self.common.internal_command.send( + InternalCommand::SplitTerminalNext { + term_id: self.term_id, + }, + ); + } + FocusCommand::SplitExchange => { + self.common.internal_command.send( + InternalCommand::SplitTerminalExchange { + term_id: self.term_id, + }, + ); + } + FocusCommand::SearchForward => { + // if let Some(search_string) = self.find.search_string.as_ref() { + // let mut raw = self.terminal.raw.lock(); + // let term = &mut raw.term; + // self.terminal.search_next( + // term, + // search_string, + // Direction::Right, + // ); + // } + } + FocusCommand::SearchBackward => { + // if let Some(search_string) = self.find.search_string.as_ref() { + // let mut raw = self.terminal.raw.lock(); + // let term = &mut raw.term; + // self.terminal.search_next( + // term, + // search_string, + // Direction::Left, + // ); + // } + } + _ => return CommandExecuted::No, + }, + _ => return CommandExecuted::No, + }; + CommandExecuted::Yes + } + + fn receive_char(&self, c: &str) { + if self.mode.get_untracked() == Mode::Terminal { + self.common + .proxy + .terminal_write(self.term_id, c.to_string()); + self.raw + .get_untracked() + .write() + .term + .scroll_display(Scroll::Bottom); + } + } +} + +impl TerminalData { + pub fn new( + cx: Scope, + workspace: Arc, + profile: Option, + common: Rc, + ) -> Self { + Self::new_run_debug(cx, workspace, None, profile, common) + } + + pub fn new_run_debug( + cx: Scope, + workspace: Arc, + run_debug: Option, + profile: Option, + common: Rc, + ) -> Self { + let cx = cx.create_child(); + let term_id = TermId::next(); + + let title = if let Some(profile) = &profile { + cx.create_rw_signal(profile.name.to_owned()) + } else { + cx.create_rw_signal(String::from("Default")) + }; + + let launch_error = cx.create_rw_signal(None); + + let raw = Self::new_raw_terminal( + &workspace, + term_id, + run_debug.as_ref(), + profile, + common.clone(), + launch_error, + ); + + let run_debug = cx.create_rw_signal(run_debug); + let mode = cx.create_rw_signal(Mode::Terminal); + let visual_mode = cx.create_rw_signal(VisualMode::Normal); + let raw = cx.create_rw_signal(raw); + + Self { + scope: cx, + term_id, + workspace, + raw, + title, + run_debug, + mode, + visual_mode, + common, + launch_error, + } + } + + fn new_raw_terminal( + workspace: &LapceWorkspace, + term_id: TermId, + run_debug: Option<&RunDebugProcess>, + profile: Option, + common: Rc, + launch_error: RwSignal>, + ) -> Arc> { + let raw = Arc::new(RwLock::new(RawTerminal::new( + term_id, + common.proxy.clone(), + common.term_notification_tx.clone(), + ))); + + let mut profile = profile.unwrap_or_default(); + + if profile.workdir.is_none() { + profile.workdir = url::Url::from_file_path( + workspace.path.as_ref().cloned().unwrap_or_default(), + ) + .ok(); + } + + let exp_run_debug = run_debug + .as_ref() + .map(|run_debug| { + ExpandedRunDebug::expand( + workspace, + &run_debug.config, + run_debug.is_prelaunch, + ) + }) + .transpose(); + + let exp_run_debug = exp_run_debug.unwrap_or_else(|e| { + let r_name = run_debug + .as_ref() + .map(|r| r.config.name.as_str()) + .unwrap_or("Unknown"); + launch_error.set(Some(format!( + "Failed to expand variables in run debug definition {r_name}: {e}" + ))); + None + }); + + if let Some(run_debug) = exp_run_debug { + if let Some(work_dir) = run_debug.work_dir { + profile.workdir = Some(work_dir); + } + + profile.environment = run_debug.env; + + profile.command = Some(run_debug.program); + profile.arguments = run_debug.args; + } + + { + let raw = raw.clone(); + if let Err(err) = + common.term_tx.send((term_id, TermEvent::NewTerminal(raw))) + { + tracing::error!("{:?}", err); + } + common.proxy.new_terminal(term_id, profile); + } + + raw + } + + pub fn send_keypress(&self, key: &KeyEvent) -> bool { + if let Some(command) = Self::resolve_key_event(key) { + self.receive_char(command); + true + } else if key.modifiers == Modifiers::ALT + && matches!(&key.key.logical_key, Key::Character(_)) + { + if let Key::Character(c) = &key.key.logical_key { + // In terminal emulators, when the Alt key is combined with another character + // (such as Alt+a), a leading ESC (Escape, ASCII code 0x1B) character is usually + // sent followed by a sequence of that character. For example, + // Alt+a sends \x1Ba. + self.receive_char("\x1b"); + self.receive_char(c.as_str()); + } + true + } else { + false + } + } + + pub fn resolve_key_event(key: &KeyEvent) -> Option<&str> { + let key = key.clone(); + + // Generates a `Modifiers` value to check against. + macro_rules! modifiers { + (ctrl) => { + Modifiers::CONTROL + }; + + (alt) => { + Modifiers::ALT + }; + + (shift) => { + Modifiers::SHIFT + }; + + ($mod:ident $(| $($mods:ident)|+)?) => { + modifiers!($mod) $(| modifiers!($($mods)|+) )? + }; + } + + // Generates modifier values for ANSI sequences. + macro_rules! modval { + (shift) => { + // 1 + "2" + }; + (alt) => { + // 2 + "3" + }; + (alt | shift) => { + // 1 + 2 + "4" + }; + (ctrl) => { + // 4 + "5" + }; + (ctrl | shift) => { + // 1 + 4 + "6" + }; + (alt | ctrl) => { + // 2 + 4 + "7" + }; + (alt | ctrl | shift) => { + // 1 + 2 + 4 + "8" + }; + } + + // Generates ANSI sequences to move the cursor by one position. + macro_rules! term_sequence { + // Generate every modifier combination (except meta) + ([all], $evt:ident, $no_mod:literal, $pre:literal, $post:literal) => { + { + term_sequence!([], $evt, $no_mod); + term_sequence!([shift, alt, ctrl], $evt, $pre, $post); + term_sequence!([alt | shift, ctrl | shift, alt | ctrl], $evt, $pre, $post); + term_sequence!([alt | ctrl | shift], $evt, $pre, $post); + return None; + } + }; + // No modifiers + ([], $evt:ident, $no_mod:literal) => { + if $evt.modifiers.is_empty() { + return Some($no_mod); + } + }; + // A single modifier combination + ([$($mod:ident)|+], $evt:ident, $pre:literal, $post:literal) => { + if $evt.modifiers == modifiers!($($mod)|+) { + return Some(concat!($pre, modval!($($mod)|+), $post)); + } + }; + // Break down multiple modifiers into a series of single combination branches + ([$($($mod:ident)|+),+], $evt:ident, $pre:literal, $post:literal) => { + $( + term_sequence!([$($mod)|+], $evt, $pre, $post); + )+ + }; + } + + match key.key.logical_key { + Key::Character(ref c) => { + if key.modifiers == Modifiers::CONTROL { + // Convert the character into its index (into a control character). + // In essence, this turns `ctrl+h` into `^h` + let str = match c.as_str() { + "@" => "\x00", + "a" => "\x01", + "b" => "\x02", + "c" => "\x03", + "d" => "\x04", + "e" => "\x05", + "f" => "\x06", + "g" => "\x07", + "h" => "\x08", + "i" => "\x09", + "j" => "\x0a", + "k" => "\x0b", + "l" => "\x0c", + "m" => "\x0d", + "n" => "\x0e", + "o" => "\x0f", + "p" => "\x10", + "q" => "\x11", + "r" => "\x12", + "s" => "\x13", + "t" => "\x14", + "u" => "\x15", + "v" => "\x16", + "w" => "\x17", + "x" => "\x18", + "y" => "\x19", + "z" => "\x1a", + "[" => "\x1b", + "\\" => "\x1c", + "]" => "\x1d", + "^" => "\x1e", + "_" => "\x1f", + _ => return None, + }; + + Some(str) + } else { + None + } + } + Key::Named(NamedKey::Backspace) => { + Some(if key.modifiers.control() { + "\x08" // backspace + } else if key.modifiers.alt() { + "\x1b\x7f" + } else { + "\x7f" + }) + } + + Key::Named(NamedKey::Tab) => Some("\x09"), + Key::Named(NamedKey::Enter) => Some("\r"), + Key::Named(NamedKey::Escape) => Some("\x1b"), + + // The following either expands to `\x1b[X` or `\x1b[1;NX` where N is a modifier value + Key::Named(NamedKey::ArrowUp) => { + term_sequence!([all], key, "\x1b[A", "\x1b[1;", "A") + } + Key::Named(NamedKey::ArrowDown) => { + term_sequence!([all], key, "\x1b[B", "\x1b[1;", "B") + } + Key::Named(NamedKey::ArrowRight) => { + term_sequence!([all], key, "\x1b[C", "\x1b[1;", "C") + } + Key::Named(NamedKey::ArrowLeft) => { + term_sequence!([all], key, "\x1b[D", "\x1b[1;", "D") + } + Key::Named(NamedKey::Home) => { + term_sequence!([all], key, "\x1bOH", "\x1b[1;", "H") + } + Key::Named(NamedKey::End) => { + term_sequence!([all], key, "\x1bOF", "\x1b[1;", "F") + } + Key::Named(NamedKey::Insert) => { + term_sequence!([all], key, "\x1b[2~", "\x1b[2;", "~") + } + Key::Named(NamedKey::Delete) => { + term_sequence!([all], key, "\x1b[3~", "\x1b[3;", "~") + } + Key::Named(NamedKey::PageUp) => { + term_sequence!([all], key, "\x1b[5~", "\x1b[5;", "~") + } + Key::Named(NamedKey::PageDown) => { + term_sequence!([all], key, "\x1b[6~", "\x1b[6;", "~") + } + _ => None, + } + } + + pub fn wheel_scroll(&self, delta: f64) { + let config = self.common.config.get_untracked(); + let step = config.terminal_line_height() as f64; + let raw = self.raw.get_untracked(); + let mut raw = raw.write(); + raw.scroll_delta -= delta; + let delta = (raw.scroll_delta / step) as i32; + raw.scroll_delta -= delta as f64 * step; + if delta != 0 { + let scroll = alacritty_terminal::grid::Scroll::Delta(delta); + raw.term.scroll_display(scroll); + } + } + + fn toggle_visual(&self, visual_mode: VisualMode) { + let config = self.common.config.get_untracked(); + if !config.core.modal { + return; + } + + match self.mode.get_untracked() { + Mode::Normal => { + self.mode.set(Mode::Visual(visual_mode)); + self.visual_mode.set(visual_mode); + } + Mode::Visual(_) => { + if self.visual_mode.get_untracked() == visual_mode { + self.mode.set(Mode::Normal); + } else { + self.visual_mode.set(visual_mode); + } + } + _ => (), + } + + let raw = self.raw.get_untracked(); + let mut raw = raw.write(); + let term = &mut raw.term; + if !term.mode().contains(TermMode::VI) { + term.toggle_vi_mode(); + } + let ty = match visual_mode { + VisualMode::Normal => SelectionType::Simple, + VisualMode::Linewise => SelectionType::Lines, + VisualMode::Blockwise => SelectionType::Block, + }; + let point = term.renderable_content().cursor.point; + self.toggle_selection( + term, + ty, + point, + alacritty_terminal::index::Side::Left, + ); + if let Some(selection) = term.selection.as_mut() { + selection.include_all(); + } + } + + pub fn toggle_selection( + &self, + term: &mut Term, + ty: SelectionType, + point: alacritty_terminal::index::Point, + side: alacritty_terminal::index::Side, + ) { + match &mut term.selection { + Some(selection) if selection.ty == ty && !selection.is_empty() => { + term.selection = None; + } + Some(selection) if !selection.is_empty() => { + selection.ty = ty; + } + _ => self.start_selection(term, ty, point, side), + } + } + + fn start_selection( + &self, + term: &mut Term, + ty: SelectionType, + point: alacritty_terminal::index::Point, + side: alacritty_terminal::index::Side, + ) { + term.selection = Some(Selection::new(ty, point, side)); + } + + pub fn new_process(&self, run_debug: Option) { + let (width, height) = { + let raw = self.raw.get_untracked(); + let raw = raw.read(); + let width = raw.term.columns(); + let height = raw.term.screen_lines(); + (width, height) + }; + + let raw = Self::new_raw_terminal( + &self.workspace, + self.term_id, + run_debug.as_ref(), + None, + self.common.clone(), + self.launch_error, + ); + + self.raw.set(raw); + self.run_debug.set(run_debug); + + let term_size = TermSize::new(width, height); + self.raw.get_untracked().write().term.resize(term_size); + self.common + .proxy + .terminal_resize(self.term_id, width, height); + } + + pub fn stop(&self) { + if let Some(dap_id) = self.run_debug.with_untracked(|x| { + if let Some(process) = x { + if !process.is_prelaunch && process.mode == RunDebugMode::Debug { + return Some(process.config.dap_id); + } + } + None + }) { + self.common.proxy.dap_stop(dap_id); + } + self.common.proxy.terminal_close(self.term_id); + } +} + +/// [`RunDebugConfig`] with expanded out program/arguments/etc. Used for creating the terminal. +#[derive(Debug, Clone)] +pub struct ExpandedRunDebug { + pub work_dir: Option, + pub env: Option>, + pub program: String, + pub args: Option>, +} +impl ExpandedRunDebug { + pub fn expand( + workspace: &LapceWorkspace, + run_debug: &RunDebugConfig, + is_prelaunch: bool, + ) -> anyhow::Result { + // Get the current working directory variable, which can container ${workspace} + let work_dir = Self::expand_work_dir(workspace, run_debug); + + let prelaunch = is_prelaunch + .then_some(run_debug.prelaunch.as_ref()) + .flatten(); + + let env = run_debug.env.clone(); + + // TODO: replace some variables in the args + let (program, mut args) = + if let Some(debug_command) = run_debug.debug_command.as_ref() { + let mut args = debug_command.to_owned(); + let command = args.first().cloned().unwrap_or_default(); + if !args.is_empty() { + args.remove(0); + } + + let args = if !args.is_empty() { Some(args) } else { None }; + (command, args) + } else if let Some(prelaunch) = prelaunch { + (prelaunch.program.clone(), prelaunch.args.clone()) + } else { + (run_debug.program.clone(), run_debug.args.clone()) + }; + let mut program = if program == "${lapce}" { + std::env::current_exe() + .map_err(|e| { + anyhow!( + "Failed to get current exe for ${{lapce}} run and debug: {e}" + ) + })? + .to_str() + .ok_or_else(|| anyhow!("Failed to convert ${{lapce}} path to str"))? + .to_string() + } else { + program + }; + + if program.contains("${workspace}") { + if let Some(workspace) = workspace.path.as_ref().and_then(|x| x.to_str()) + { + program = program.replace("${workspace}", workspace); + } + } + + if let Some(args) = &mut args { + for arg in args { + // Replace all mentions of ${workspace} with the current workspace path + if arg.contains("${workspace}") { + if let Some(workspace) = + workspace.path.as_ref().and_then(|x| x.to_str()) + { + *arg = arg.replace("${workspace}", workspace); + } + } + } + } + + Ok(ExpandedRunDebug { + work_dir, + env, + program, + args, + }) + } + + fn expand_work_dir( + workspace: &LapceWorkspace, + run_debug: &RunDebugConfig, + ) -> Option { + let path = run_debug.cwd.as_ref()?; + + if path.contains("${workspace}") { + if let Some(workspace) = workspace.path.as_ref().and_then(|x| x.to_str()) + { + let path = path.replace("${workspace}", workspace); + if let Ok(as_url) = Url::from_file_path(PathBuf::from(path)) { + return Some(as_url); + } + } + } + + Url::from_file_path(PathBuf::from(path)).ok() + } +} diff --git a/lapce-app/src/terminal/event.rs b/lapce-app/src/terminal/event.rs new file mode 100644 index 0000000..9dd45a1 --- /dev/null +++ b/lapce-app/src/terminal/event.rs @@ -0,0 +1,77 @@ +use std::{ + collections::HashMap, + sync::{ + Arc, + mpsc::{Receiver, Sender}, + }, + time::Instant, +}; + +use lapce_rpc::terminal::TermId; +use parking_lot::RwLock; + +use super::raw::RawTerminal; + +/// The notifications for terminals to send back to main thread +pub enum TermNotification { + SetTitle { term_id: TermId, title: String }, + RequestPaint, +} + +pub enum TermEvent { + NewTerminal(Arc>), + UpdateContent(Vec), + CloseTerminal, +} + +pub fn terminal_update_process( + receiver: Receiver<(TermId, TermEvent)>, + term_notification_tx: Sender, +) { + let mut terminals = HashMap::new(); + let mut last_redraw = Instant::now(); + let mut last_event = None; + loop { + let (term_id, event) = if let Some((term_id, event)) = last_event.take() { + (term_id, event) + } else { + match receiver.recv() { + Ok((term_id, event)) => (term_id, event), + Err(_) => return, + } + }; + match event { + TermEvent::CloseTerminal => { + terminals.remove(&term_id); + } + TermEvent::NewTerminal(raw) => { + terminals.insert(term_id, raw); + } + TermEvent::UpdateContent(content) => { + if let Some(raw) = terminals.get(&term_id) { + { + raw.write().update_content(content); + } + last_event = receiver.try_recv().ok(); + if last_event.is_some() { + if last_redraw.elapsed().as_millis() > 10 { + last_redraw = Instant::now(); + if let Err(err) = term_notification_tx + .send(TermNotification::RequestPaint) + { + tracing::error!("{:?}", err); + } + } + } else { + last_redraw = Instant::now(); + if let Err(err) = + term_notification_tx.send(TermNotification::RequestPaint) + { + tracing::error!("{:?}", err); + } + } + } + } + } + } +} diff --git a/lapce-app/src/terminal/mod.rs b/lapce-app/src/terminal/mod.rs new file mode 100644 index 0000000..455305f --- /dev/null +++ b/lapce-app/src/terminal/mod.rs @@ -0,0 +1,6 @@ +pub mod data; +pub mod event; +pub mod panel; +pub mod raw; +pub mod tab; +pub mod view; diff --git a/lapce-app/src/terminal/panel.rs b/lapce-app/src/terminal/panel.rs new file mode 100644 index 0000000..81455b2 --- /dev/null +++ b/lapce-app/src/terminal/panel.rs @@ -0,0 +1,820 @@ +use std::{collections::HashMap, path::PathBuf, rc::Rc, sync::Arc}; + +use floem::{ + ext_event::create_ext_action, + reactive::{Memo, RwSignal, Scope, SignalGet, SignalUpdate, SignalWith}, +}; +use lapce_core::mode::Mode; +use lapce_rpc::{ + dap_types::{ + self, DapId, RunDebugConfig, StackFrame, Stopped, ThreadId, Variable, + }, + proxy::ProxyResponse, + terminal::{TermId, TerminalProfile}, +}; + +use super::{data::TerminalData, tab::TerminalTabData}; +use crate::{ + debug::{ + DapData, DapVariable, RunDebugConfigs, RunDebugData, RunDebugMode, + RunDebugProcess, ScopeOrVar, + }, + id::TerminalTabId, + keypress::{EventRef, KeyPressData, KeyPressFocus, KeyPressHandle}, + main_split::MainSplitData, + panel::kind::PanelKind, + window_tab::{CommonData, Focus}, + workspace::LapceWorkspace, +}; + +pub struct TerminalTabInfo { + pub active: usize, + pub tabs: im::Vector<(RwSignal, TerminalTabData)>, +} + +#[derive(Clone)] +pub struct TerminalPanelData { + pub cx: Scope, + pub workspace: Arc, + pub tab_info: RwSignal, + pub debug: RunDebugData, + pub breakline: Memo>, + pub common: Rc, + pub main_split: MainSplitData, +} + +impl TerminalPanelData { + pub fn new( + workspace: Arc, + profile: Option, + common: Rc, + main_split: MainSplitData, + ) -> Self { + let terminal_tab = + TerminalTabData::new(workspace.clone(), profile, common.clone()); + + let cx = common.scope; + + let tabs = + im::vector![(terminal_tab.scope.create_rw_signal(0), terminal_tab)]; + let tab_info = TerminalTabInfo { active: 0, tabs }; + let tab_info = cx.create_rw_signal(tab_info); + + let debug = RunDebugData::new(cx, common.breakpoints); + + let breakline = { + let active_term = debug.active_term; + let daps = debug.daps; + cx.create_memo(move |_| { + let active_term = active_term.get(); + let active_term = match active_term { + Some(active_term) => active_term, + None => return None, + }; + + let term = tab_info.with_untracked(|info| { + for (_, tab) in &info.tabs { + let terminal = tab.terminals.with_untracked(|terminals| { + terminals + .iter() + .find(|(_, t)| t.term_id == active_term) + .cloned() + }); + if let Some(terminal) = terminal { + return Some(terminal.1); + } + } + None + }); + let term = match term { + Some(term) => term, + None => return None, + }; + let stopped = term + .run_debug + .with(|run_debug| run_debug.as_ref().map(|r| r.stopped)) + .unwrap_or(true); + if stopped { + return None; + } + + let daps = daps.get(); + let dap = daps.values().find(|d| d.term_id == active_term); + dap.and_then(|dap| dap.breakline.get()) + }) + }; + + Self { + cx, + workspace, + tab_info, + debug, + breakline, + common, + main_split, + } + } + + pub fn active_tab(&self, tracked: bool) -> Option { + if tracked { + self.tab_info.with(|info| { + info.tabs + .get(info.active) + .or_else(|| info.tabs.last()) + .cloned() + .map(|(_, tab)| tab) + }) + } else { + self.tab_info.with_untracked(|info| { + info.tabs + .get(info.active) + .or_else(|| info.tabs.last()) + .cloned() + .map(|(_, tab)| tab) + }) + } + } + + pub fn key_down<'a>( + &self, + event: impl Into> + Copy, + keypress: &KeyPressData, + ) -> Option { + if self.tab_info.with_untracked(|info| info.tabs.is_empty()) { + self.new_tab(None); + } + + let tab = self.active_tab(false); + let terminal = tab.and_then(|tab| tab.active_terminal(false)); + if let Some(terminal) = terminal { + let handle = keypress.key_down(event, &terminal); + let mode = terminal.get_mode(); + + if !handle.handled && mode == Mode::Terminal { + if let EventRef::Keyboard(key_event) = event.into() { + if terminal.send_keypress(key_event) { + return Some(KeyPressHandle { + handled: true, + keymatch: handle.keymatch, + keypress: handle.keypress, + }); + } + } + } + Some(handle) + } else { + None + } + } + + pub fn new_tab(&self, profile: Option) { + self.new_tab_run_debug(None, profile); + } + + /// Create a new terminal tab with the given run debug process. + /// Errors if expanding out the run debug process failed. + pub fn new_tab_run_debug( + &self, + run_debug: Option, + profile: Option, + ) -> TerminalTabData { + let terminal_tab = TerminalTabData::new_run_debug( + self.workspace.clone(), + run_debug, + profile, + self.common.clone(), + ); + + self.tab_info.update(|info| { + info.tabs.insert( + if info.tabs.is_empty() { + 0 + } else { + (info.active + 1).min(info.tabs.len()) + }, + (terminal_tab.scope.create_rw_signal(0), terminal_tab.clone()), + ); + let new_active = (info.active + 1).min(info.tabs.len() - 1); + info.active = new_active; + }); + + terminal_tab + } + + pub fn next_tab(&self) { + self.tab_info.update(|info| { + if info.active >= info.tabs.len().saturating_sub(1) { + info.active = 0; + } else { + info.active += 1; + } + }); + self.update_debug_active_term(); + } + + pub fn previous_tab(&self) { + self.tab_info.update(|info| { + if info.active == 0 { + info.active = info.tabs.len().saturating_sub(1); + } else { + info.active -= 1; + } + }); + self.update_debug_active_term(); + } + + pub fn close_tab(&self, terminal_tab_id: Option) { + if let Some(close_tab) = self + .tab_info + .try_update(|info| { + let mut close_tab = None; + if let Some(terminal_tab_id) = terminal_tab_id { + if let Some(index) = + info.tabs.iter().enumerate().find_map(|(index, (_, t))| { + if t.terminal_tab_id == terminal_tab_id { + Some(index) + } else { + None + } + }) + { + close_tab = Some( + info.tabs.remove(index).1.terminals.get_untracked(), + ); + } + } else { + let active = info.active.min(info.tabs.len().saturating_sub(1)); + if !info.tabs.is_empty() { + info.tabs.remove(active); + } + } + let new_active = info.active.min(info.tabs.len().saturating_sub(1)); + info.active = new_active; + close_tab + }) + .flatten() + { + for (_, data) in close_tab { + data.stop(); + } + } + self.update_debug_active_term(); + } + + pub fn set_title(&self, term_id: &TermId, title: &str) { + if let Some(t) = self.get_terminal(term_id) { + t.title.set(title.to_string()); + } + } + + pub fn get_terminal(&self, term_id: &TermId) -> Option { + self.tab_info.with_untracked(|info| { + for (_, tab) in &info.tabs { + let terminal = tab.terminals.with_untracked(|terminals| { + terminals + .iter() + .find(|(_, t)| &t.term_id == term_id) + .cloned() + }); + if let Some(terminal) = terminal { + return Some(terminal.1); + } + } + None + }) + } + + fn get_terminal_in_tab( + &self, + term_id: &TermId, + ) -> Option<(usize, TerminalTabData, usize, TerminalData)> { + self.tab_info.with_untracked(|info| { + for (tab_index, (_, tab)) in info.tabs.iter().enumerate() { + let result = tab.terminals.with_untracked(|terminals| { + terminals + .iter() + .enumerate() + .find(|(_, (_, t))| &t.term_id == term_id) + .map(|(i, (_, terminal))| (i, terminal.clone())) + }); + if let Some((index, terminal)) = result { + return Some((tab_index, tab.clone(), index, terminal)); + } + } + None + }) + } + + pub fn split(&self, term_id: TermId) { + if let Some((_, tab, index, _)) = self.get_terminal_in_tab(&term_id) { + let terminal_data = TerminalData::new( + tab.scope, + self.workspace.clone(), + None, + self.common.clone(), + ); + let i = terminal_data.scope.create_rw_signal(0); + tab.terminals.update(|terminals| { + terminals.insert(index + 1, (i, terminal_data)); + }); + } + } + + pub fn split_next(&self, term_id: TermId) { + if let Some((_, tab, index, _)) = self.get_terminal_in_tab(&term_id) { + let max = tab.terminals.with_untracked(|t| t.len() - 1); + let new_index = (index + 1).min(max); + if new_index != index { + tab.active.set(new_index); + self.update_debug_active_term(); + } + } + } + + pub fn split_previous(&self, term_id: TermId) { + if let Some((_, tab, index, _)) = self.get_terminal_in_tab(&term_id) { + let new_index = index.saturating_sub(1); + if new_index != index { + tab.active.set(new_index); + self.update_debug_active_term(); + } + } + } + + pub fn split_exchange(&self, term_id: TermId) { + if let Some((_, tab, index, _)) = self.get_terminal_in_tab(&term_id) { + let max = tab.terminals.with_untracked(|t| t.len() - 1); + if index < max { + tab.terminals.update(|terminals| { + terminals.swap(index, index + 1); + }); + self.update_debug_active_term(); + } + } + } + + pub fn close_terminal(&self, term_id: &TermId) { + if let Some((_, tab, index, _)) = self.get_terminal_in_tab(term_id) { + let active = tab.active.get_untracked(); + let len = tab + .terminals + .try_update(|terminals| { + terminals.remove(index); + terminals.len() + }) + .unwrap(); + if len == 0 { + self.close_tab(Some(tab.terminal_tab_id)); + } else { + let new_active = active.min(len.saturating_sub(1)); + if new_active != active { + tab.active.set(new_active); + self.update_debug_active_term(); + } + } + } + } + + pub fn launch_failed(&self, term_id: &TermId, error: &str) { + if let Some(terminal) = self.get_terminal(term_id) { + terminal.launch_error.set(Some(error.to_string())); + } + } + + pub fn terminal_stopped(&self, term_id: &TermId, exit_code: Option) { + if let Some(terminal) = self.get_terminal(term_id) { + if terminal.run_debug.with_untracked(|r| r.is_some()) { + let was_prelaunch = terminal + .run_debug + .try_update(|run_debug| { + if let Some(run_debug) = run_debug.as_mut() { + if run_debug.is_prelaunch + && run_debug.config.prelaunch.is_some() + { + run_debug.is_prelaunch = false; + if run_debug.mode == RunDebugMode::Debug { + // set it to be stopped so that the dap can pick the same terminal session + run_debug.stopped = true; + } + Some(true) + } else { + run_debug.stopped = true; + Some(false) + } + } else { + None + } + }) + .unwrap(); + let exit_code = exit_code.unwrap_or(0); + if was_prelaunch == Some(true) && exit_code == 0 { + let run_debug = terminal.run_debug.get_untracked(); + if let Some(run_debug) = run_debug { + if run_debug.mode == RunDebugMode::Debug { + self.common.proxy.dap_start( + run_debug.config, + self.debug.source_breakpoints(), + ) + } else { + terminal.new_process(Some(run_debug)); + } + } + } + } else { + self.close_terminal(term_id); + } + } + } + + pub fn get_stopped_run_debug_terminal( + &self, + mode: &RunDebugMode, + config: &RunDebugConfig, + ) -> Option { + self.tab_info.with_untracked(|info| { + for (_, tab) in &info.tabs { + let terminal = tab.terminals.with_untracked(|terminals| { + for (_, terminal) in terminals { + if let Some(run_debug) = + terminal.run_debug.get_untracked().as_ref() + { + if run_debug.stopped && &run_debug.mode == mode { + match run_debug.mode { + RunDebugMode::Run => { + if run_debug.config.name == config.name { + return Some(terminal.clone()); + } + } + RunDebugMode::Debug => { + if run_debug.config.dap_id == config.dap_id { + return Some(terminal.clone()); + } + } + } + } + } + } + None + }); + if let Some(terminal) = terminal { + return Some(terminal); + } + } + None + }) + } + + /// Return whether it is in debug mode. + pub fn restart_run_debug(&self, term_id: TermId) -> Option { + let (_, terminal_tab, index, terminal) = + self.get_terminal_in_tab(&term_id)?; + let mut run_debug = terminal.run_debug.get_untracked()?; + if run_debug.config.config_source.from_palette() { + if let Some(new_config) = + self.get_run_config_by_name(&run_debug.config.name) + { + run_debug.config = new_config; + } + } + let mut is_debug = false; + let new_term_id = match run_debug.mode { + RunDebugMode::Run => { + self.common.proxy.terminal_close(term_id); + let mut run_debug = run_debug; + run_debug.stopped = false; + run_debug.is_prelaunch = true; + let new_terminal = TerminalData::new_run_debug( + terminal_tab.scope, + self.workspace.clone(), + Some(run_debug), + None, + self.common.clone(), + ); + let new_term_id = new_terminal.term_id; + terminal_tab.terminals.update(|terminals| { + terminals[index] = + (new_terminal.scope.create_rw_signal(0), new_terminal); + }); + self.debug.active_term.set(Some(new_term_id)); + new_term_id + } + RunDebugMode::Debug => { + is_debug = true; + let dap_id = + terminal.run_debug.get_untracked().as_ref()?.config.dap_id; + let daps = self.debug.daps.get_untracked(); + let dap = daps.get(&dap_id)?; + self.common + .proxy + .dap_restart(dap.dap_id, self.debug.source_breakpoints()); + term_id + } + }; + + self.focus_terminal(new_term_id); + + Some(is_debug) + } + + fn get_run_config_by_name(&self, name: &str) -> Option { + if let Some(workspace) = self.common.workspace.path.as_deref() { + let run_toml = workspace.join(".lapce").join("run.toml"); + let (doc, new_doc) = self.main_split.get_doc(run_toml.clone(), None); + if !new_doc { + let content = doc.buffer.with_untracked(|b| b.to_string()); + match toml::from_str::(&content) { + Ok(configs) => { + return configs.configs.into_iter().find(|x| x.name == name); + } + Err(err) => { + // todo show message window + tracing::error!("deser fail {:?}", err); + } + } + } + } + None + } + + pub fn focus_terminal(&self, term_id: TermId) { + if let Some((tab_index, terminal_tab, index, _terminal)) = + self.get_terminal_in_tab(&term_id) + { + self.tab_info.update(|info| { + info.active = tab_index; + }); + terminal_tab.active.set(index); + self.common.focus.set(Focus::Panel(PanelKind::Terminal)); + + self.update_debug_active_term(); + } + } + + pub fn update_debug_active_term(&self) { + let tab = self.active_tab(false); + let terminal = tab.and_then(|tab| tab.active_terminal(false)); + if let Some(terminal) = terminal { + let term_id = terminal.term_id; + let is_run_debug = + terminal.run_debug.with_untracked(|run| run.is_some()); + let current_active = self.debug.active_term.get_untracked(); + if is_run_debug { + if current_active != Some(term_id) { + self.debug.active_term.set(Some(term_id)); + } + } else if let Some(active) = current_active { + if self.get_terminal(&active).is_none() { + self.debug.active_term.set(None); + } + } + } else { + self.debug.active_term.set(None); + } + } + + pub fn stop_run_debug(&self, term_id: TermId) -> Option<()> { + let terminal = self.get_terminal(&term_id)?; + let run_debug = terminal.run_debug.get_untracked()?; + + match run_debug.mode { + RunDebugMode::Run => { + self.common.proxy.terminal_close(term_id); + } + RunDebugMode::Debug => { + let dap_id = run_debug.config.dap_id; + let daps = self.debug.daps.get_untracked(); + let dap = daps.get(&dap_id)?; + self.common.proxy.dap_stop(dap.dap_id); + } + } + + self.focus_terminal(term_id); + Some(()) + } + + pub fn run_debug_process( + &self, + tracked: bool, + ) -> Vec<(TermId, RunDebugProcess)> { + let mut processes = Vec::new(); + if tracked { + self.tab_info.with(|info| { + for (_, tab) in &info.tabs { + tab.terminals.with(|terminals| { + for (_, terminal) in terminals { + if let Some(run_debug) = terminal.run_debug.get() { + processes.push((terminal.term_id, run_debug)); + } + } + }) + } + }); + } else { + self.tab_info.with_untracked(|info| { + for (_, tab) in &info.tabs { + tab.terminals.with_untracked(|terminals| { + for (_, terminal) in terminals { + if let Some(run_debug) = terminal.run_debug.get() { + processes.push((terminal.term_id, run_debug)); + } + } + }) + } + }); + } + processes.sort_by_key(|(_, process)| process.created); + processes + } + + pub fn set_process_id(&self, term_id: &TermId, process_id: Option) { + if let Some(terminal) = self.get_terminal(term_id) { + terminal.run_debug.with_untracked(|run_debug| { + if let Some(run_debug) = run_debug.as_ref() { + if run_debug.config.debug_command.is_some() { + let dap_id = run_debug.config.dap_id; + self.common + .proxy + .dap_process_id(dap_id, process_id, *term_id); + } + } + }); + } + } + + pub fn dap_continued(&self, dap_id: &DapId) { + let dap = self + .debug + .daps + .with_untracked(|daps| daps.get(dap_id).cloned()); + if let Some(dap) = dap { + dap.thread_id.set(None); + dap.stopped.set(false); + } + } + + pub fn dap_stopped( + &self, + dap_id: &DapId, + stopped: &Stopped, + stack_frames: &HashMap>, + variables: &[(dap_types::Scope, Vec)], + ) { + let dap = self + .debug + .daps + .with_untracked(|daps| daps.get(dap_id).cloned()); + if let Some(dap) = dap { + dap.stopped(self.cx, stopped, stack_frames, variables); + } + floem::action::focus_window(); + } + + pub fn dap_continue(&self, term_id: TermId) -> Option<()> { + let terminal = self.get_terminal(&term_id)?; + let dap_id = terminal + .run_debug + .with_untracked(|r| r.as_ref().map(|r| r.config.dap_id))?; + let thread_id = self.debug.daps.with_untracked(|daps| { + daps.get(&dap_id) + .and_then(|dap| dap.thread_id.get_untracked()) + }); + let thread_id = thread_id.unwrap_or_default(); + self.common.proxy.dap_continue(dap_id, thread_id); + Some(()) + } + + pub fn dap_pause(&self, term_id: TermId) -> Option<()> { + let terminal = self.get_terminal(&term_id)?; + let dap_id = terminal + .run_debug + .with_untracked(|r| r.as_ref().map(|r| r.config.dap_id))?; + let thread_id = self.debug.daps.with_untracked(|daps| { + daps.get(&dap_id) + .and_then(|dap| dap.thread_id.get_untracked()) + }); + let thread_id = thread_id.unwrap_or_default(); + self.common.proxy.dap_pause(dap_id, thread_id); + Some(()) + } + + pub fn dap_step_over(&self, term_id: TermId) -> Option<()> { + let terminal = self.get_terminal(&term_id)?; + let dap_id = terminal + .run_debug + .with_untracked(|r| r.as_ref().map(|r| r.config.dap_id))?; + let thread_id = self.debug.daps.with_untracked(|daps| { + daps.get(&dap_id) + .and_then(|dap| dap.thread_id.get_untracked()) + }); + let thread_id = thread_id.unwrap_or_default(); + self.common.proxy.dap_step_over(dap_id, thread_id); + Some(()) + } + + pub fn dap_step_into(&self, term_id: TermId) -> Option<()> { + let terminal = self.get_terminal(&term_id)?; + let dap_id = terminal + .run_debug + .with_untracked(|r| r.as_ref().map(|r| r.config.dap_id))?; + let thread_id = self.debug.daps.with_untracked(|daps| { + daps.get(&dap_id) + .and_then(|dap| dap.thread_id.get_untracked()) + }); + let thread_id = thread_id.unwrap_or_default(); + self.common.proxy.dap_step_into(dap_id, thread_id); + Some(()) + } + + pub fn dap_step_out(&self, term_id: TermId) -> Option<()> { + let terminal = self.get_terminal(&term_id)?; + let dap_id = terminal + .run_debug + .with_untracked(|r| r.as_ref().map(|r| r.config.dap_id))?; + let thread_id = self.debug.daps.with_untracked(|daps| { + daps.get(&dap_id) + .and_then(|dap| dap.thread_id.get_untracked()) + }); + let thread_id = thread_id.unwrap_or_default(); + self.common.proxy.dap_step_out(dap_id, thread_id); + Some(()) + } + + pub fn get_active_dap(&self, tracked: bool) -> Option { + let active_term = if tracked { + self.debug.active_term.get()? + } else { + self.debug.active_term.get_untracked()? + }; + self.get_dap(active_term, tracked) + } + + pub fn get_dap(&self, term_id: TermId, tracked: bool) -> Option { + let terminal = self.get_terminal(&term_id)?; + let dap_id = if tracked { + terminal + .run_debug + .with(|r| r.as_ref().map(|r| r.config.dap_id))? + } else { + terminal + .run_debug + .with_untracked(|r| r.as_ref().map(|r| r.config.dap_id))? + }; + + if tracked { + self.debug.daps.with(|daps| daps.get(&dap_id).cloned()) + } else { + self.debug + .daps + .with_untracked(|daps| daps.get(&dap_id).cloned()) + } + } + + pub fn dap_frame_scopes(&self, dap_id: DapId, frame_id: usize) { + if let Some(dap) = self.debug.daps.get_untracked().get(&dap_id) { + let variables = dap.variables; + let send = create_ext_action(self.common.scope, move |result| { + if let Ok(ProxyResponse::DapGetScopesResponse { scopes }) = result { + variables.update(|dap_var| { + dap_var.children = scopes + .iter() + .enumerate() + .map(|(i, (scope, vars))| DapVariable { + item: ScopeOrVar::Scope(scope.to_owned()), + parent: Vec::new(), + expanded: i == 0, + read: i == 0, + children: vars + .iter() + .map(|var| DapVariable { + item: ScopeOrVar::Var(var.to_owned()), + parent: vec![scope.variables_reference], + expanded: false, + read: false, + children: Vec::new(), + children_expanded_count: 0, + }) + .collect(), + children_expanded_count: if i == 0 { + vars.len() + } else { + 0 + }, + }) + .collect(); + dap_var.children_expanded_count = dap_var + .children + .iter() + .map(|v| v.children_expanded_count + 1) + .sum::(); + }); + } + }); + + self.common + .proxy + .dap_get_scopes(dap_id, frame_id, move |result| { + send(result); + }); + } + } +} diff --git a/lapce-app/src/terminal/raw.rs b/lapce-app/src/terminal/raw.rs new file mode 100644 index 0000000..f37db8d --- /dev/null +++ b/lapce-app/src/terminal/raw.rs @@ -0,0 +1,149 @@ +use std::sync::mpsc::Sender; + +use alacritty_terminal::{ + Term, + event::EventListener, + grid::Dimensions, + index::{Column, Direction, Line, Point}, + term::{ + cell::{Flags, LineLength}, + search::{Match, RegexIter, RegexSearch}, + test::TermSize, + }, + vte::ansi, +}; +use lapce_rpc::{proxy::ProxyRpcHandler, terminal::TermId}; + +use super::event::TermNotification; + +pub struct EventProxy { + term_id: TermId, + proxy: ProxyRpcHandler, + term_notification_tx: Sender, +} + +impl EventListener for EventProxy { + fn send_event(&self, event: alacritty_terminal::event::Event) { + match event { + alacritty_terminal::event::Event::PtyWrite(s) => { + self.proxy.terminal_write(self.term_id, s); + } + alacritty_terminal::event::Event::MouseCursorDirty => { + if let Err(err) = self + .term_notification_tx + .send(TermNotification::RequestPaint) + { + tracing::error!("{:?}", err); + } + } + alacritty_terminal::event::Event::Title(s) => { + if let Err(err) = + self.term_notification_tx.send(TermNotification::SetTitle { + term_id: self.term_id, + title: s, + }) + { + tracing::error!("{:?}", err); + } + } + _ => (), + } + } +} + +pub struct RawTerminal { + pub parser: ansi::Processor, + pub term: Term, + pub scroll_delta: f64, +} + +impl RawTerminal { + pub fn new( + term_id: TermId, + proxy: ProxyRpcHandler, + term_notification_tx: Sender, + ) -> Self { + let config = alacritty_terminal::term::Config { + semantic_escape_chars: ",│`|\"' ()[]{}<>\t".to_string(), + ..Default::default() + }; + let event_proxy = EventProxy { + term_id, + proxy, + term_notification_tx, + }; + + let size = TermSize::new(50, 30); + let term = Term::new(config, &size, event_proxy); + let parser = ansi::Processor::new(); + + Self { + parser, + term, + scroll_delta: 0.0, + } + } + + pub fn update_content(&mut self, content: Vec) { + for byte in content { + self.parser.advance(&mut self.term, byte); + } + } + + pub fn output(&self, line_num: usize) -> Vec { + let grid = self.term.grid(); + let mut lines = Vec::with_capacity(5); + let mut rows = Vec::new(); + for line in (grid.topmost_line().0..=grid.bottommost_line().0) + .map(Line) + .rev() + { + let row_cell = &grid[line]; + if row_cell[Column(row_cell.len() - 1)] + .flags + .contains(Flags::WRAPLINE) + { + rows.push(row_cell); + } else { + if !rows.is_empty() { + let mut new_line = Vec::new(); + std::mem::swap(&mut rows, &mut new_line); + let line_str: String = new_line + .into_iter() + .rev() + .flat_map(|x| { + x.into_iter().take(x.line_length().0).map(|x| x.c) + }) + .collect(); + lines.push(line_str); + if lines.len() >= line_num { + break; + } + } + rows.push(row_cell); + } + } + for line in &lines { + tracing::info!("{}", line); + } + lines + } +} + +pub fn visible_regex_match_iter<'a, EventProxy>( + term: &'a Term, + regex: &'a mut RegexSearch, +) -> impl Iterator + 'a { + let viewport_start = Line(-(term.grid().display_offset() as i32)); + let viewport_end = viewport_start + term.bottommost_line(); + let mut start = term.line_search_left(Point::new(viewport_start, Column(0))); + let mut end = term.line_search_right(Point::new(viewport_end, Column(0))); + start.line = start.line.max(viewport_start - MAX_SEARCH_LINES); + end.line = end.line.min(viewport_end + MAX_SEARCH_LINES); + + RegexIter::new(start, end, Direction::Right, term, regex) + .skip_while(move |rm| rm.end().line < viewport_start) + .take_while(move |rm| rm.start().line <= viewport_end) +} +/// todo:should be improved +pub const MAX_SEARCH_LINES: usize = 100; diff --git a/lapce-app/src/terminal/tab.rs b/lapce-app/src/terminal/tab.rs new file mode 100644 index 0000000..32214b7 --- /dev/null +++ b/lapce-app/src/terminal/tab.rs @@ -0,0 +1,68 @@ +use std::{rc::Rc, sync::Arc}; + +use floem::reactive::{RwSignal, Scope, SignalGet, SignalWith}; +use lapce_rpc::terminal::TerminalProfile; + +use super::data::TerminalData; +use crate::{ + debug::RunDebugProcess, id::TerminalTabId, window_tab::CommonData, + workspace::LapceWorkspace, +}; + +#[derive(Clone)] +pub struct TerminalTabData { + pub scope: Scope, + pub terminal_tab_id: TerminalTabId, + pub active: RwSignal, + pub terminals: RwSignal, TerminalData)>>, +} + +impl TerminalTabData { + pub fn new( + workspace: Arc, + profile: Option, + common: Rc, + ) -> Self { + TerminalTabData::new_run_debug(workspace, None, profile, common) + } + + /// Create the information for a terminal tab, which can contain multiple terminals. + pub fn new_run_debug( + workspace: Arc, + run_debug: Option, + profile: Option, + common: Rc, + ) -> Self { + let cx = common.scope.create_child(); + let terminal_data = + TerminalData::new_run_debug(cx, workspace, run_debug, profile, common); + let terminals = im::vector![(cx.create_rw_signal(0), terminal_data)]; + let terminals = cx.create_rw_signal(terminals); + let active = cx.create_rw_signal(0); + let terminal_tab_id = TerminalTabId::next(); + Self { + scope: cx, + terminal_tab_id, + active, + terminals, + } + } + + pub fn active_terminal(&self, tracked: bool) -> Option { + let active = if tracked { + self.active.get() + } else { + self.active.get_untracked() + }; + + if tracked { + self.terminals + .with(|t| t.get(active).or_else(|| t.last()).cloned()) + .map(|(_, t)| t) + } else { + self.terminals + .with_untracked(|t| t.get(active).or_else(|| t.last()).cloned()) + .map(|(_, t)| t) + } + } +} diff --git a/lapce-app/src/terminal/view.rs b/lapce-app/src/terminal/view.rs new file mode 100644 index 0000000..09b1310 --- /dev/null +++ b/lapce-app/src/terminal/view.rs @@ -0,0 +1,799 @@ +use std::{sync::Arc, time::SystemTime}; + +use alacritty_terminal::{ + grid::Dimensions, + index::Side, + selection::{Selection, SelectionType}, + term::{RenderableContent, cell::Flags, test::TermSize}, +}; +use floem::{ + Renderer, View, ViewId, + context::{EventCx, PaintCx}, + event::{Event, EventPropagation}, + kurbo::Stroke, + peniko::{ + Color, + kurbo::{Point, Rect, Size}, + }, + pointer::PointerInputEvent, + prelude::SignalTrack, + reactive::{ReadSignal, RwSignal, SignalGet, SignalWith, create_effect}, + text::{Attrs, AttrsList, FamilyOwned, TextLayout, Weight}, + views::editor::{core::register::Clipboard, text::SystemClipboard}, +}; +use lapce_core::mode::Mode; +use lapce_rpc::{proxy::ProxyRpcHandler, terminal::TermId}; +use lsp_types::Position; +use parking_lot::RwLock; +use regex::Regex; +use unicode_width::UnicodeWidthChar; + +use super::{panel::TerminalPanelData, raw::RawTerminal}; +use crate::{ + command::InternalCommand, + config::{LapceConfig, color::LapceColor}, + debug::RunDebugProcess, + editor::location::{EditorLocation, EditorPosition}, + listener::Listener, + panel::kind::PanelKind, + window_tab::Focus, + workspace::LapceWorkspace, +}; + +/// Threshold used for double_click/triple_click. +const CLICK_THRESHOLD: u128 = 400; + +enum TerminalViewState { + Config, + Focus(bool), + Raw(Arc>), +} + +struct TerminalLineContent<'a> { + y: f64, + bg: Vec<(usize, usize, Color)>, + underline: Vec<(usize, usize, Color, f64)>, + chars: Vec<(char, Attrs<'a>, f64, f64)>, + cursor: Option<(char, f64)>, +} + +pub struct TerminalView { + id: ViewId, + term_id: TermId, + raw: Arc>, + mode: ReadSignal, + size: Size, + is_focused: bool, + config: ReadSignal>, + run_config: ReadSignal>, + proxy: ProxyRpcHandler, + launch_error: RwSignal>, + internal_command: Listener, + workspace: Arc, + hyper_regs: Vec, + previous_mouse_action: MouseAction, + current_mouse_action: MouseAction, +} + +#[allow(clippy::too_many_arguments)] +pub fn terminal_view( + term_id: TermId, + raw: ReadSignal>>, + mode: ReadSignal, + run_config: ReadSignal>, + terminal_panel_data: TerminalPanelData, + launch_error: RwSignal>, + internal_command: Listener, + workspace: Arc, +) -> TerminalView { + let id = ViewId::new(); + + create_effect(move |_| { + let raw = raw.get(); + id.update_state(TerminalViewState::Raw(raw)); + }); + + create_effect(move |_| { + launch_error.track(); + id.request_paint(); + }); + + let config = terminal_panel_data.common.config; + create_effect(move |_| { + config.with(|_c| {}); + id.update_state(TerminalViewState::Config); + }); + + let proxy = terminal_panel_data.common.proxy.clone(); + + create_effect(move |last| { + let focus = terminal_panel_data.common.focus.get(); + + let mut is_focused = false; + if let Focus::Panel(PanelKind::Terminal) = focus { + let tab = terminal_panel_data.active_tab(true); + if let Some(tab) = tab { + let terminal = tab.active_terminal(true); + is_focused = terminal.map(|t| t.term_id) == Some(term_id); + } + } + + if last != Some(is_focused) { + id.update_state(TerminalViewState::Focus(is_focused)); + } + + is_focused + }); + + // for rust + let reg = regex::Regex::new("[\\w\\\\/-]+\\.(rs)?(toml)?:\\d+(:\\d+)?").unwrap(); + + TerminalView { + id, + term_id, + raw: raw.get_untracked(), + mode, + config, + proxy, + run_config, + size: Size::ZERO, + is_focused: false, + launch_error, + internal_command, + workspace, + hyper_regs: vec![reg], + previous_mouse_action: Default::default(), + current_mouse_action: Default::default(), + } +} + +impl TerminalView { + fn char_size(&self) -> Size { + let config = self.config.get_untracked(); + let font_family = config.terminal_font_family(); + let font_size = config.terminal_font_size(); + let family: Vec = + FamilyOwned::parse_list(font_family).collect(); + let attrs = Attrs::new().family(&family).font_size(font_size as f32); + let attrs_list = AttrsList::new(attrs); + let mut text_layout = TextLayout::new(); + text_layout.set_text("W", attrs_list, None); + text_layout.size() + } + + fn terminal_size(&self) -> (usize, usize) { + let config = self.config.get_untracked(); + let line_height = config.terminal_line_height() as f64; + let char_width = self.char_size().width; + let width = (self.size.width / char_width).floor() as usize; + let height = (self.size.height / line_height).floor() as usize; + (width.max(1), height.max(1)) + } + + fn click(&self, pos: Point) -> Option<()> { + let raw = self.raw.read(); + let position = self.get_terminal_point(pos); + let start_point = raw.term.semantic_search_left(position); + let end_point = raw.term.semantic_search_right(position); + let mut selection = + Selection::new(SelectionType::Simple, start_point, Side::Left); + selection.update(end_point, Side::Right); + selection.include_all(); + if let Some(selection) = selection.to_range(&raw.term) { + let content = raw.term.bounds_to_string(selection.start, selection.end); + if let Some(match_str) = + self.hyper_regs.iter().find_map(|x| x.find(&content)) + { + let hyperlink = match_str.as_str(); + let content: Vec<&str> = hyperlink.split(':').collect(); + let (file, line, col) = ( + content.first()?, + content.get(1).and_then(|x: &&str| x.parse::().ok())?, + content + .get(2) + .and_then(|x: &&str| x.parse::().ok()) + .unwrap_or(0), + ); + let parent_path = self.workspace.path.as_ref()?; + self.internal_command.send(InternalCommand::JumpToLocation { + location: EditorLocation { + path: parent_path.join(file), + position: Some(EditorPosition::Position(Position::new( + line.saturating_sub(1), + col.saturating_sub(1), + ))), + scroll_offset: None, + ignore_unconfirmed: false, + same_editor_tab: false, + }, + }); + return Some(()); + } + } + None + } + + fn update_mouse_action_by_down(&mut self, mouse: &PointerInputEvent) { + let mut next_action = MouseAction::None; + match self.current_mouse_action { + MouseAction::None + | MouseAction::LeftDouble { .. } + | MouseAction::LeftSelect { .. } + | MouseAction::RightOnce { .. } => { + if mouse.button.is_primary() { + next_action = MouseAction::LeftDown { pos: mouse.pos }; + } else if mouse.button.is_secondary() { + next_action = MouseAction::RightDown { pos: mouse.pos }; + } + } + MouseAction::LeftOnce { pos, time } => { + let during_mills = + time.elapsed().map(|x| x.as_millis()).unwrap_or(u128::MAX); + match ( + mouse.button.is_primary(), + mouse.pos == pos, + during_mills < CLICK_THRESHOLD, + ) { + (true, true, true) => { + next_action = MouseAction::LeftOnceAndDown { pos, time }; + } + (true, _, _) => { + next_action = MouseAction::LeftDown { pos: mouse.pos }; + } + _ => {} + } + } + MouseAction::LeftOnceAndDown { .. } + | MouseAction::LeftDown { .. } + | MouseAction::RightDown { .. } => {} + } + self.current_mouse_action = next_action; + } + + fn update_mouse_action_by_up(&mut self, mouse: &PointerInputEvent) { + let mut next_action = MouseAction::None; + match self.current_mouse_action { + MouseAction::None => {} + MouseAction::LeftDown { pos } => { + match (mouse.button.is_primary(), mouse.pos == pos) { + (true, true) => { + next_action = MouseAction::LeftOnce { + pos, + time: SystemTime::now(), + } + } + (true, false) => { + next_action = MouseAction::LeftSelect { + start_pos: pos, + end_pos: mouse.pos, + }; + } + _ => {} + } + } + MouseAction::LeftOnce { .. } => {} + MouseAction::LeftSelect { .. } => {} + MouseAction::LeftOnceAndDown { pos, time } => { + let during_mills = + time.elapsed().map(|x| x.as_millis()).unwrap_or(u128::MAX); + match ( + mouse.button.is_primary(), + mouse.pos == pos, + during_mills < CLICK_THRESHOLD, + ) { + (true, true, true) => { + next_action = MouseAction::LeftDouble { pos }; + } + (true, true, false) => { + next_action = MouseAction::LeftOnce { + pos: mouse.pos, + time: SystemTime::now(), + }; + } + (true, false, _) => { + next_action = MouseAction::LeftSelect { + start_pos: pos, + end_pos: mouse.pos, + }; + } + _ => {} + } + } + MouseAction::LeftDouble { .. } => {} + MouseAction::RightDown { pos } => { + if mouse.button.is_secondary() && mouse.pos == pos { + next_action = MouseAction::RightOnce { pos }; + } + } + MouseAction::RightOnce { .. } => {} + } + self.previous_mouse_action = self.current_mouse_action; + self.current_mouse_action = next_action; + } + + fn get_terminal_point(&self, pos: Point) -> alacritty_terminal::index::Point { + let raw = self.raw.read(); + let col = (pos.x / self.char_size().width) as usize; + let line_no = pos.y as i32 + / (self.config.get().terminal_line_height() as i32) + - raw.term.grid().display_offset() as i32; + alacritty_terminal::index::Point::new( + alacritty_terminal::index::Line(line_no), + alacritty_terminal::index::Column(col), + ) + } + + fn paint_content( + &self, + cx: &mut PaintCx, + content: RenderableContent, + line_height: f64, + char_size: Size, + config: &LapceConfig, + ) { + let term_bg = config.color(LapceColor::TERMINAL_BACKGROUND); + + let font_size = config.terminal_font_size(); + let font_family = config.terminal_font_family(); + let family: Vec = + FamilyOwned::parse_list(font_family).collect(); + let attrs = Attrs::new().family(&family).font_size(font_size as f32); + + let char_width = char_size.width; + + let cursor_point = &content.cursor.point; + + let mut line_content = TerminalLineContent { + y: 0.0, + bg: Vec::new(), + underline: Vec::new(), + chars: Vec::new(), + cursor: None, + }; + for item in content.display_iter { + let point = item.point; + let cell = item.cell; + let inverse = cell.flags.contains(Flags::INVERSE); + + let x = point.column.0 as f64 * char_width; + let y = + (point.line.0 as f64 + content.display_offset as f64) * line_height; + let char_y = y + (line_height - char_size.height) / 2.0; + if y != line_content.y { + self.paint_line_content( + cx, + &line_content, + line_height, + char_width, + config, + ); + line_content.y = y; + line_content.bg.clear(); + line_content.underline.clear(); + line_content.chars.clear(); + line_content.cursor = None; + } + + let mut bg = config.terminal_get_color(&cell.bg, content.colors); + let mut fg = config.terminal_get_color(&cell.fg, content.colors); + if cell.flags.contains(Flags::DIM) + || cell.flags.contains(Flags::DIM_BOLD) + { + fg = fg.multiply_alpha(0.66); + } + + if inverse { + std::mem::swap(&mut fg, &mut bg); + } + + if term_bg != bg { + let mut extend = false; + if let Some((_, end, color)) = line_content.bg.last_mut() { + if color == &bg && *end == point.column.0 { + *end += 1; + extend = true; + } + } + if !extend { + line_content + .bg + .push((point.column.0, point.column.0 + 1, bg)); + } + } + + if cursor_point == &point { + line_content.cursor = Some((cell.c, x)); + } + + let bold = cell.flags.contains(Flags::BOLD) + || cell.flags.contains(Flags::DIM_BOLD); + + if &point == cursor_point && self.is_focused { + fg = term_bg; + } + + if cell.c != ' ' && cell.c != '\t' { + let mut attrs = attrs.clone().color(fg); + if bold { + attrs = attrs.weight(Weight::BOLD); + } + line_content.chars.push((cell.c, attrs, x, char_y)); + } + } + self.paint_line_content(cx, &line_content, line_height, char_width, config); + } + + fn paint_line_content( + &self, + cx: &mut PaintCx, + line_content: &TerminalLineContent, + line_height: f64, + char_width: f64, + config: &LapceConfig, + ) { + for (start, end, bg) in &line_content.bg { + let rect = Size::new( + char_width * (end.saturating_sub(*start) as f64), + line_height, + ) + .to_rect() + .with_origin(Point::new(*start as f64 * char_width, line_content.y)); + cx.fill(&rect, bg, 0.0); + } + + for (start, end, fg, y) in &line_content.underline { + let rect = + Size::new(char_width * (end.saturating_sub(*start) as f64), 1.0) + .to_rect() + .with_origin(Point::new(*start as f64 * char_width, y - 1.0)); + cx.fill(&rect, fg, 0.0); + } + + if let Some((c, x)) = line_content.cursor { + let rect = + Size::new(char_width * c.width().unwrap_or(1) as f64, line_height) + .to_rect() + .with_origin(Point::new(x, line_content.y)); + let mode = self.mode.get_untracked(); + let cursor_color = if mode == Mode::Terminal { + if self.run_config.with_untracked(|run_config| { + run_config.as_ref().map(|r| r.stopped).unwrap_or(false) + }) { + config.color(LapceColor::LAPCE_ERROR) + } else { + config.color(LapceColor::TERMINAL_CURSOR) + } + } else { + config.color(LapceColor::EDITOR_CARET) + }; + if self.is_focused { + cx.fill(&rect, cursor_color, 0.0); + } else { + cx.stroke(&rect, cursor_color, &Stroke::new(1.0)); + } + } + + for (char, attr, x, y) in &line_content.chars { + let mut text_layout = TextLayout::new(); + text_layout.set_text( + &char.to_string(), + AttrsList::new(attr.clone()), + None, + ); + cx.draw_text(&text_layout, Point::new(*x, *y)); + } + } +} + +impl Drop for TerminalView { + fn drop(&mut self) { + self.proxy.terminal_close(self.term_id); + } +} + +impl View for TerminalView { + fn id(&self) -> ViewId { + self.id + } + + fn event_before_children( + &mut self, + _cx: &mut EventCx, + event: &Event, + ) -> EventPropagation { + match event { + Event::PointerDown(e) => { + self.update_mouse_action_by_down(e); + } + Event::PointerUp(e) => { + self.update_mouse_action_by_up(e); + let mut clear_selection = false; + match self.current_mouse_action { + MouseAction::LeftOnce { pos, .. } => { + clear_selection = true; + if e.modifiers.control() && self.click(pos).is_some() { + return EventPropagation::Stop; + } + } + MouseAction::LeftSelect { start_pos, end_pos } => { + let mut selection = Selection::new( + SelectionType::Simple, + self.get_terminal_point(start_pos), + Side::Left, + ); + selection + .update(self.get_terminal_point(end_pos), Side::Right); + selection.include_all(); + self.raw.write().term.selection = Some(selection); + _cx.app_state_mut().request_paint(self.id); + } + MouseAction::LeftDouble { pos } => { + let position = self.get_terminal_point(pos); + let mut raw = self.raw.write(); + let start_point = raw.term.semantic_search_left(position); + let end_point = raw.term.semantic_search_right(position); + + let mut selection = Selection::new( + SelectionType::Simple, + start_point, + Side::Left, + ); + selection.update(end_point, Side::Right); + selection.include_all(); + raw.term.selection = Some(selection); + _cx.app_state_mut().request_paint(self.id); + } + MouseAction::RightOnce { pos } => { + let position = self.get_terminal_point(pos); + let raw = self.raw.read(); + if let Some(selection) = &raw + .term + .selection + .as_ref() + .and_then(|x| x.to_range(&raw.term)) + { + if selection.contains(position) { + let mut clipboard = SystemClipboard::new(); + let content = raw.term.bounds_to_string( + selection.start, + selection.end, + ); + if !content.is_empty() { + clipboard.put_string(content); + } + } + clear_selection = true; + } + } + _ => { + clear_selection = true; + } + } + if clear_selection { + self.raw.write().term.selection = None; + _cx.app_state_mut().request_paint(self.id); + } + } + _ => {} + } + EventPropagation::Continue + } + + fn update( + &mut self, + cx: &mut floem::context::UpdateCx, + state: Box, + ) { + if let Ok(state) = state.downcast() { + match *state { + TerminalViewState::Config => {} + TerminalViewState::Focus(is_focused) => { + self.is_focused = is_focused; + } + TerminalViewState::Raw(raw) => { + self.raw = raw; + } + } + cx.app_state_mut().request_paint(self.id); + } + } + + fn layout( + &mut self, + cx: &mut floem::context::LayoutCx, + ) -> floem::taffy::prelude::NodeId { + cx.layout_node(self.id, false, |_cx| Vec::new()) + } + + fn compute_layout( + &mut self, + _cx: &mut floem::context::ComputeLayoutCx, + ) -> Option { + let layout = self.id.get_layout().unwrap_or_default(); + let size = layout.size; + let size = Size::new(size.width as f64, size.height as f64); + if size.is_zero_area() { + return None; + } + if size != self.size { + self.size = size; + let (width, height) = self.terminal_size(); + let term_size = TermSize::new(width, height); + self.raw.write().term.resize(term_size); + self.proxy.terminal_resize(self.term_id, width, height); + } + + None + } + + fn paint(&mut self, cx: &mut floem::context::PaintCx) { + let config = self.config.get_untracked(); + let mode = self.mode.get_untracked(); + let line_height = config.terminal_line_height() as f64; + let font_family = config.terminal_font_family(); + let font_size = config.terminal_font_size(); + let char_size = self.char_size(); + let char_width = char_size.width; + + let family: Vec = + FamilyOwned::parse_list(font_family).collect(); + let attrs = Attrs::new().family(&family).font_size(font_size as f32); + + if let Some(error) = self.launch_error.get() { + let mut text_layout = TextLayout::new(); + text_layout.set_text( + &format!("Terminal failed to launch. Error: {error}"), + AttrsList::new( + attrs.color(config.color(LapceColor::EDITOR_FOREGROUND)), + ), + None, + ); + cx.draw_text( + &text_layout, + Point::new(6.0, 0.0 + (line_height - char_size.height) / 2.0), + ); + return; + } + + let raw = self.raw.read(); + let term = &raw.term; + let content = term.renderable_content(); + + // let mut search = RegexSearch::new("[\\w\\\\?]+\\.rs:\\d+:\\d+").unwrap(); + // self.hyper_matches = visible_regex_match_iter(term, &mut search).collect(); + + if let Some(selection) = content.selection.as_ref() { + let start_line = selection.start.line.0 + content.display_offset as i32; + let start_line = if start_line < 0 { + 0 + } else { + start_line as usize + }; + let start_col = selection.start.column.0; + + let end_line = selection.end.line.0 + content.display_offset as i32; + let end_line = if end_line < 0 { 0 } else { end_line as usize }; + let end_col = selection.end.column.0; + + for line in start_line..end_line + 1 { + let left_col = if selection.is_block || line == start_line { + start_col + } else { + 0 + }; + let right_col = if selection.is_block || line == end_line { + end_col + 1 + } else { + term.last_column().0 + }; + let x0 = left_col as f64 * char_width; + let x1 = right_col as f64 * char_width; + let y0 = line as f64 * line_height; + let y1 = y0 + line_height; + cx.fill( + &Rect::new(x0, y0, x1, y1), + config.color(LapceColor::EDITOR_SELECTION), + 0.0, + ); + } + } else if mode != Mode::Terminal { + let y = (content.cursor.point.line.0 as f64 + + content.display_offset as f64) + * line_height; + cx.fill( + &Rect::new(0.0, y, self.size.width, y + line_height), + config.color(LapceColor::EDITOR_CURRENT_LINE), + 0.0, + ); + } + + self.paint_content(cx, content, line_height, char_size, &config); + // if data.find.visual { + // if let Some(search_string) = data.find.search_string.as_ref() { + // if let Ok(dfas) = RegexSearch::new(®ex::escape(search_string)) { + // let mut start = alacritty_terminal::index::Point::new( + // alacritty_terminal::index::Line( + // -(content.display_offset as i32), + // ), + // alacritty_terminal::index::Column(0), + // ); + // let end_line = (start.line + term.screen_lines()) + // .min(term.bottommost_line()); + // let mut max_lines = (end_line.0 - start.line.0) as usize; + + // while let Some(m) = term.search_next( + // &dfas, + // start, + // Direction::Right, + // Side::Left, + // Some(max_lines), + // ) { + // let match_start = m.start(); + // if match_start.line.0 < start.line.0 + // || (match_start.line.0 == start.line.0 + // && match_start.column.0 < start.column.0) + // { + // break; + // } + // let x = match_start.column.0 as f64 * char_width; + // let y = (match_start.line.0 as f64 + // + content.display_offset as f64) + // * line_height; + // let rect = Rect::ZERO + // .with_origin(Point::new(x, y)) + // .with_size(Size::new( + // (m.end().column.0 - m.start().column.0 + // + term.grid()[*m.end()].c.width().unwrap_or(1)) + // as f64 + // * char_width, + // line_height, + // )); + // cx.stroke( + // &rect, + // config.get_color(LapceColor::TERMINAL_FOREGROUND), + // 1.0, + // ); + // start = *m.end(); + // if start.column.0 < term.last_column() { + // start.column.0 += 1; + // } else if start.line.0 < term.bottommost_line() { + // start.column.0 = 0; + // start.line.0 += 1; + // } + // max_lines = (end_line.0 - start.line.0) as usize; + // } + // } + // } + // } + } +} + +#[derive(Debug, Default, Copy, Clone)] +enum MouseAction { + #[default] + None, + LeftDown { + pos: Point, + }, + LeftOnce { + pos: Point, + time: SystemTime, + }, + LeftSelect { + start_pos: Point, + end_pos: Point, + }, + LeftOnceAndDown { + pos: Point, + time: SystemTime, + }, + LeftDouble { + pos: Point, + }, + RightDown { + pos: Point, + }, + RightOnce { + pos: Point, + }, +} diff --git a/lapce-app/src/text_area.rs b/lapce-app/src/text_area.rs new file mode 100644 index 0000000..747dc00 --- /dev/null +++ b/lapce-app/src/text_area.rs @@ -0,0 +1,121 @@ +use floem::{ + View, + peniko::kurbo::Rect, + reactive::{ + SignalGet, SignalUpdate, SignalWith, create_effect, create_rw_signal, + }, + text::{Attrs, AttrsList, LineHeightValue, TextLayout}, + views::{Decorators, container, label, rich_text, scroll, stack}, +}; +use lapce_core::buffer::rope_text::RopeText; + +use crate::{config::color::LapceColor, editor::EditorData}; + +pub fn text_area( + editor: EditorData, + is_active: impl Fn() -> bool + 'static, +) -> impl View { + let config = editor.common.config; + let doc = editor.doc_signal(); + let cursor = editor.cursor(); + let text_area_rect = create_rw_signal(Rect::ZERO); + let text_layout = create_rw_signal(TextLayout::new()); + let line_height = 1.2; + + create_effect(move |_| { + let config = config.get(); + let font_size = config.ui.font_size(); + let font_family = config.ui.font_family(); + let color = config.color(LapceColor::EDITOR_FOREGROUND); + let attrs = Attrs::new() + .color(color) + .family(&font_family) + .font_size(font_size as f32) + .line_height(LineHeightValue::Normal(line_height)); + let attrs_list = AttrsList::new(attrs); + let doc = doc.get(); + let text = doc.buffer.with(|b| b.to_string()); + text_layout.update(|text_layout| { + text_layout.set_text(&text, attrs_list, None); + }); + }); + + create_effect(move |last_rev| { + let rev = doc.with(|doc| doc.rev()); + if last_rev == Some(rev) { + return rev; + } + + let config = config.get_untracked(); + let font_size = config.ui.font_size(); + let font_family = config.ui.font_family(); + let color = config.color(LapceColor::EDITOR_FOREGROUND); + let attrs = Attrs::new() + .color(color) + .family(&font_family) + .font_size(font_size as f32) + .line_height(LineHeightValue::Normal(1.2)); + let attrs_list = AttrsList::new(attrs); + let doc = doc.get(); + let text = doc.buffer.with(|b| b.to_string()); + text_layout.update(|text_layout| { + text_layout.set_text(&text, attrs_list, None); + }); + + rev + }); + + create_effect(move |last_width| { + let width = text_area_rect.get().width(); + if last_width == Some(width) { + return width; + } + + text_layout.update(|text_layout| { + text_layout.set_size(width as f32, f32::MAX); + }); + + width + }); + + let cursor_pos = move || { + let offset = cursor.with(|c| c.offset()); + let (line, col) = doc + .with_untracked(|doc| doc.buffer.with(|b| b.offset_to_line_col(offset))); + text_layout.with(|text_layout| { + let pos = text_layout.line_col_position(line, col); + pos.point - (0.0, pos.glyph_ascent) + }) + }; + + container( + scroll( + stack(( + rich_text(move || text_layout.get()) + .on_resize(move |rect| { + text_area_rect.set(rect); + }) + .style(|s| s.width_pct(100.0)), + label(|| " ".to_string()).style(move |s| { + let cursor_pos = cursor_pos(); + s.absolute() + .line_height(line_height) + .margin_left(cursor_pos.x as f32 - 1.0) + .margin_top(cursor_pos.y as f32) + .border_left(2.0) + .border_color(config.get().color(LapceColor::EDITOR_CARET)) + .apply_if(!is_active(), |s| s.hide()) + }), + )) + .style(|s| s.width_pct(100.0).padding(6.0)), + ) + .style(|s| s.absolute().size_pct(100.0, 100.0)), + ) + .style(move |s| { + let config = config.get(); + s.border(1.0) + .border_radius(6.0) + .border_color(config.color(LapceColor::LAPCE_BORDER)) + .background(config.color(LapceColor::EDITOR_BACKGROUND)) + }) +} diff --git a/lapce-app/src/text_input.rs b/lapce-app/src/text_input.rs new file mode 100644 index 0000000..b6cbbdb --- /dev/null +++ b/lapce-app/src/text_input.rs @@ -0,0 +1,801 @@ +use std::{rc::Rc, sync::Arc}; + +use floem::{ + Renderer, View, ViewId, + action::{set_ime_allowed, set_ime_cursor_area}, + context::EventCx, + event::{Event, EventListener, EventPropagation}, + kurbo::Stroke, + peniko::{ + Color, + kurbo::{Line, Point, Rect, Size, Vec2}, + }, + prop_extractor, + reactive::{ + Memo, ReadSignal, RwSignal, Scope, SignalGet, SignalUpdate, SignalWith, + create_effect, create_memo, create_rw_signal, + }, + style::{ + CursorStyle, FontFamily, FontSize, FontStyle, FontWeight, LineHeight, + PaddingLeft, Style, TextColor, + }, + taffy::prelude::NodeId, + text::{Attrs, AttrsList, FamilyOwned, TextLayout}, + unit::PxPct, + views::Decorators, +}; +use lapce_core::{ + buffer::rope_text::RopeText, + cursor::{Cursor, CursorMode}, + selection::Selection, +}; +use lapce_xi_rope::Rope; + +use crate::{ + config::{LapceConfig, color::LapceColor}, + doc::Doc, + editor::{DocSignal, EditorData, view::editor_style}, + keypress::KeyPressFocus, + main_split::Editors, + window_tab::CommonData, +}; + +prop_extractor! { + Extractor { + color: TextColor, + font_size: FontSize, + font_family: FontFamily, + font_weight: FontWeight, + font_style: FontStyle, + line_height: LineHeight, + } +} + +/// Builder for creating a [`TextInput`] easily. +pub struct TextInputBuilder { + is_focused: Option>, + // TODO: it'd be nice to not need to box this + key_focus: Option>, + value: Option, + keyboard_focus: RwSignal, +} + +impl Default for TextInputBuilder { + fn default() -> Self { + Self::new() + } +} + +impl TextInputBuilder { + pub fn new() -> Self { + Self { + is_focused: None, + key_focus: None, + value: None, + keyboard_focus: create_rw_signal(false), + } + } + + pub fn is_focused(mut self, is_focused: impl Fn() -> bool + 'static) -> Self { + let keyboard_focus = self.keyboard_focus; + self.is_focused = + Some(create_memo(move |_| is_focused() || keyboard_focus.get())); + self + } + + /// Initialize with a specific value. + /// If this is set it will apply the value via reloading the editor's doc as pristine. + pub fn value(mut self, value: impl Into) -> Self { + self.value = Some(value.into()); + self + } + + pub fn key_focus(mut self, key_focus: impl KeyPressFocus + 'static) -> Self { + self.key_focus = Some(Box::new(key_focus)); + self + } + + pub fn build( + self, + cx: Scope, + editors: Editors, + common: Rc, + ) -> TextInput { + let editor = editors.make_local(cx, common); + let id = editor.id(); + + self.build_editor(editor).on_cleanup(move || { + editors.remove(id); + }) + } + + /// Build the text input with a specific editor. + /// This function does *not* perform add/cleanup the editor to/from [`Editors`] + pub fn build_editor(self, editor: EditorData) -> TextInput { + let keyboard_focus = self.keyboard_focus; + let is_focused = if let Some(is_focused) = self.is_focused { + is_focused + } else { + create_memo(move |_| keyboard_focus.get()) + }; + + if let Some(value) = self.value { + editor.doc().reload(value, true); + } + + text_input_full(editor, self.key_focus, is_focused, keyboard_focus) + } +} + +/// Create a basic single line text input +/// `e_data` is the editor data that this input is associated with. +/// `supplied_editor` +/// `key_focus` is what receives the keydown events, leave as `None` to default to editor. +/// `is_focused` is a function that returns if the input is focused, used for certain events. +fn text_input_full( + e_data: EditorData, + key_focus: Option, + is_focused: Memo, + keyboard_focus: RwSignal, +) -> TextInput { + let id = ViewId::new(); + + let doc = e_data.doc_signal(); + let cursor = e_data.cursor(); + let config = e_data.common.config; + let keypress = e_data.common.keypress; + let window_origin = create_rw_signal(Point::ZERO); + let cursor_line = create_rw_signal(Line::new(Point::ZERO, Point::ZERO)); + let local_editor = e_data.clone(); + let editor = local_editor.editor.clone(); + + { + let doc = doc.get(); + create_effect(move |_| { + let offset = cursor.with(|c| c.offset()); + let (content, offset, preedit_range) = { + let content = doc.buffer.with(|b| b.to_string()); + if let Some(preedit) = doc.preedit.preedit.get().as_ref() { + let mut new_content = String::new(); + new_content.push_str(&content[..offset]); + new_content.push_str(&preedit.text); + new_content.push_str(&content[offset..]); + let range = (offset, offset + preedit.text.len()); + let offset = preedit + .cursor + .as_ref() + .map(|(_, end)| offset + *end) + .unwrap_or(offset); + (new_content, offset, Some(range)) + } else { + (content, offset, None) + } + }; + id.update_state(TextInputState::Content { + text: content, + offset, + preedit_range, + }); + }); + } + + { + create_effect(move |_| { + let focus = is_focused.get(); + id.update_state(TextInputState::Focus(focus)); + }); + + let editor = editor.clone(); + let ime_allowed = editor.ime_allowed; + create_effect(move |_| { + let focus = is_focused.get(); + if focus { + if !ime_allowed.get_untracked() { + ime_allowed.set(true); + set_ime_allowed(true); + } + let cursor_line = cursor_line.get(); + + let window_origin = window_origin.get(); + let viewport = editor.viewport.get(); + let origin = window_origin + + Vec2::new( + cursor_line.p1.x - viewport.x0, + cursor_line.p1.y - viewport.y0, + ); + set_ime_cursor_area(origin, Size::new(800.0, 600.0)); + } + }); + } + + let common_keyboard_focus = e_data.common.keyboard_focus; + + let ed1 = editor.clone(); + let ed2 = editor.clone(); + TextInput { + id, + config, + offset: 0, + preedit_range: None, + layout_rect: Rect::ZERO, + content: "".to_string(), + focus: false, + text_node: None, + text_layout: create_rw_signal(None), + text_rect: Rect::ZERO, + text_viewport: Rect::ZERO, + cursor_line, + placeholder: "".to_string(), + placeholder_text_layout: None, + editor: e_data.clone(), + cursor_pos: Point::ZERO, + on_cursor_pos: None, + hide_cursor: editor.cursor_info.hidden, + style: Default::default(), + } + .style(move |s| { + editor_style(config, doc, s) + .cursor(CursorStyle::Text) + .padding_horiz(10.0) + .padding_vert(6.0) + }) + .on_move(move |pos| { + window_origin.set(pos); + }) + .on_event_stop(EventListener::FocusGained, move |_| { + keyboard_focus.set(true); + common_keyboard_focus.set(Some(id)); + }) + .on_event_stop(EventListener::FocusLost, move |_| { + keyboard_focus.set(false); + if common_keyboard_focus.get_untracked() == Some(id) { + common_keyboard_focus.set(None); + } + }) + .on_event(EventListener::KeyDown, move |event| { + if let Event::KeyDown(key_event) = event { + let keypress = keypress.get_untracked(); + let key_focus = key_focus + .as_ref() + .map(|k| k as &dyn KeyPressFocus) + .unwrap_or(&e_data); + if keypress.key_down(key_event, key_focus).handled { + EventPropagation::Stop + } else { + EventPropagation::Continue + } + } else { + EventPropagation::Continue + } + }) + .on_event(EventListener::ImePreedit, move |event| { + if !is_focused.get_untracked() { + return EventPropagation::Continue; + } + + if let Event::ImePreedit { + text, + cursor: ime_cursor, + } = event + { + if text.is_empty() { + ed1.clear_preedit(); + } else { + let offset = cursor.with_untracked(|c| c.offset()); + ed1.set_preedit(text.clone(), ime_cursor.to_owned(), offset); + } + } + EventPropagation::Stop + }) + .on_event(EventListener::ImeCommit, move |event| { + if !is_focused.get_untracked() { + return EventPropagation::Continue; + } + + if let Event::ImeCommit(text) = event { + ed2.clear_preedit(); + ed2.receive_char(text.as_str()); + } + EventPropagation::Stop + }) +} + +enum TextInputState { + Content { + text: String, + offset: usize, + preedit_range: Option<(usize, usize)>, + }, + Focus(bool), + Placeholder(String), +} + +pub struct TextInput { + id: ViewId, + content: String, + offset: usize, + preedit_range: Option<(usize, usize)>, + editor: EditorData, + focus: bool, + text_node: Option, + text_layout: RwSignal>, + text_rect: Rect, + text_viewport: Rect, + layout_rect: Rect, + cursor_line: RwSignal, + placeholder: String, + placeholder_text_layout: Option, + cursor_pos: Point, + on_cursor_pos: Option>, + hide_cursor: RwSignal, + config: ReadSignal>, + style: Extractor, +} + +impl TextInput { + pub fn placeholder(self, placeholder: impl Fn() -> String + 'static) -> Self { + let id = self.id; + create_effect(move |_| { + let placeholder = placeholder(); + id.update_state(TextInputState::Placeholder(placeholder)); + }); + self + } + + pub fn on_cursor_pos(mut self, cursor_pos: impl Fn(Point) + 'static) -> Self { + self.on_cursor_pos = Some(Box::new(cursor_pos)); + self + } + + pub fn editor(&self) -> EditorData { + self.editor.clone() + } + + pub fn doc_signal(&self) -> DocSignal { + self.editor.doc_signal() + } + + pub fn doc(&self) -> Rc { + self.editor.doc() + } + + pub fn cursor(&self) -> RwSignal { + self.editor.cursor() + } + + fn set_text_layout(&mut self) { + let mut text_layout = TextLayout::new(); + let mut attrs = + Attrs::new().color(self.style.color().unwrap_or(Color::BLACK)); + if let Some(font_size) = self.style.font_size() { + attrs = attrs.font_size(font_size); + } + if let Some(font_style) = self.style.font_style() { + attrs = attrs.style(font_style); + } + let font_family = self.style.font_family().as_ref().map(|font_family| { + let family: Vec = + FamilyOwned::parse_list(font_family).collect(); + family + }); + if let Some(font_family) = font_family.as_ref() { + attrs = attrs.family(font_family); + } + if let Some(font_weight) = self.style.font_weight() { + attrs = attrs.weight(font_weight); + } + if let Some(line_height) = self.style.line_height() { + attrs = attrs.line_height(line_height); + } + text_layout.set_text( + if self.content.is_empty() { + " " + } else { + self.content.as_str() + }, + AttrsList::new(attrs.clone()), + None, + ); + self.text_layout.set(Some(text_layout)); + + let mut placeholder_text_layout = TextLayout::new(); + attrs = attrs.color( + self.style + .color() + .unwrap_or(Color::BLACK) + .multiply_alpha(0.5), + ); + placeholder_text_layout.set_text( + &self.placeholder, + AttrsList::new(attrs), + None, + ); + self.placeholder_text_layout = Some(placeholder_text_layout); + } + + fn hit_index(&self, _cx: &mut EventCx, point: Point) -> usize { + self.text_layout.with_untracked(|text_layout| { + if let Some(text_layout) = text_layout.as_ref() { + let padding_left = + match self.id.get_combined_style().get(PaddingLeft) { + PxPct::Px(v) => v, + PxPct::Pct(pct) => { + let layout = self.id.get_layout().unwrap_or_default(); + pct * layout.size.width as f64 + } + }; + let hit = + text_layout.hit_point(Point::new(point.x - padding_left, 0.0)); + hit.index.min(self.content.len()) + } else { + 0 + } + }) + } + + fn clamp_text_viewport(&mut self, text_viewport: Rect) { + let text_rect = self.text_rect; + let actual_size = text_rect.size(); + let width = text_rect.width(); + let height = text_rect.height(); + let child_size = self + .text_layout + .with_untracked(|text_layout| text_layout.as_ref().unwrap().size()); + + let mut text_viewport = text_viewport; + if width >= child_size.width { + text_viewport.x0 = 0.0; + } else if text_viewport.x0 > child_size.width - width { + text_viewport.x0 = child_size.width - width; + } else if text_viewport.x0 < 0.0 { + text_viewport.x0 = 0.0; + } + + if height >= child_size.height { + text_viewport.y0 = 0.0; + } else if text_viewport.y0 > child_size.height - height { + text_viewport.y0 = child_size.height - height; + } else if text_viewport.y0 < 0.0 { + text_viewport.y0 = 0.0; + } + + let text_viewport = text_viewport.with_size(actual_size); + if text_viewport != self.text_viewport { + self.text_viewport = text_viewport; + self.id.request_paint(); + } + } + + fn ensure_cursor_visible(&mut self) { + fn closest_on_axis(val: f64, min: f64, max: f64) -> f64 { + assert!(min <= max); + if val > min && val < max { + 0.0 + } else if val <= min { + val - min + } else { + val - max + } + } + + let rect = Rect::ZERO.with_origin(self.cursor_pos).inflate(10.0, 0.0); + // clamp the target region size to our own size. + // this means we will show the portion of the target region that + // includes the origin. + let target_size = Size::new( + rect.width().min(self.text_viewport.width()), + rect.height().min(self.text_viewport.height()), + ); + let rect = rect.with_size(target_size); + + let x0 = closest_on_axis( + rect.min_x(), + self.text_viewport.min_x(), + self.text_viewport.max_x(), + ); + let x1 = closest_on_axis( + rect.max_x(), + self.text_viewport.min_x(), + self.text_viewport.max_x(), + ); + let y0 = closest_on_axis( + rect.min_y(), + self.text_viewport.min_y(), + self.text_viewport.max_y(), + ); + let y1 = closest_on_axis( + rect.max_y(), + self.text_viewport.min_y(), + self.text_viewport.max_y(), + ); + + let delta_x = if x0.abs() > x1.abs() { x0 } else { x1 }; + let delta_y = if y0.abs() > y1.abs() { y0 } else { y1 }; + let new_origin = self.text_viewport.origin() + Vec2::new(delta_x, delta_y); + self.clamp_text_viewport(self.text_viewport.with_origin(new_origin)); + } +} + +impl View for TextInput { + fn id(&self) -> ViewId { + self.id + } + + fn update( + &mut self, + _cx: &mut floem::context::UpdateCx, + state: Box, + ) { + if let Ok(state) = state.downcast() { + match *state { + TextInputState::Content { + text, + offset, + preedit_range, + } => { + self.content = text; + self.offset = offset; + self.preedit_range = preedit_range; + self.text_layout.set(None); + } + TextInputState::Focus(focus) => { + self.focus = focus; + } + TextInputState::Placeholder(placeholder) => { + self.placeholder = placeholder; + self.placeholder_text_layout = None; + } + } + self.id.request_layout(); + } + } + + fn style_pass(&mut self, cx: &mut floem::context::StyleCx<'_>) { + if self.style.read(cx) { + self.set_text_layout(); + self.id.request_layout(); + } + } + + fn layout( + &mut self, + cx: &mut floem::context::LayoutCx, + ) -> floem::taffy::prelude::NodeId { + cx.layout_node(self.id, true, |_cx| { + if self + .text_layout + .with_untracked(|text_layout| text_layout.is_none()) + || self.placeholder_text_layout.is_none() + { + self.set_text_layout(); + } + + let text_layout = self.text_layout; + text_layout.with_untracked(|text_layout| { + let text_layout = text_layout.as_ref().unwrap(); + + let offset = self.cursor().get_untracked().offset(); + let cursor_point = text_layout.hit_position(offset).point; + if cursor_point != self.cursor_pos { + self.cursor_pos = cursor_point; + self.ensure_cursor_visible(); + } + + let size = text_layout.size(); + let height = size.height as f32; + + if self.text_node.is_none() { + self.text_node = Some(self.id.new_taffy_node()); + } + + let text_node = self.text_node.unwrap(); + + let style = Style::new().height(height).to_taffy_style(); + self.id.set_taffy_style(text_node, style); + }); + + vec![self.text_node.unwrap()] + }) + } + + fn compute_layout( + &mut self, + _cx: &mut floem::context::ComputeLayoutCx, + ) -> Option { + let layout = self.id.get_layout().unwrap_or_default(); + + let style = self.id.get_combined_style(); + let style = style.builtin(); + let padding_left = match style.padding_left() { + PxPct::Px(padding) => padding, + PxPct::Pct(pct) => pct * layout.size.width as f64, + }; + let padding_right = match style.padding_right() { + PxPct::Px(padding) => padding, + PxPct::Pct(pct) => pct * layout.size.width as f64, + }; + + let size = Size::new(layout.size.width as f64, layout.size.height as f64); + let mut text_rect = size.to_rect(); + text_rect.x0 += padding_left; + text_rect.x1 -= padding_right; + self.text_rect = text_rect; + + self.clamp_text_viewport(self.text_viewport); + + let text_node = self.text_node.unwrap(); + let location = self.id.taffy_layout(text_node).unwrap_or_default().location; + self.layout_rect = size + .to_rect() + .with_origin(Point::new(location.x as f64, location.y as f64)); + let offset = self.cursor().with_untracked(|c| c.offset()); + let cursor_line = self.text_layout.with_untracked(|text_layout| { + let hit_position = text_layout.as_ref().unwrap().hit_position(offset); + let point = Point::new(location.x as f64, location.y as f64) + - self.text_viewport.origin().to_vec2(); + let cursor_point = hit_position.point + point.to_vec2(); + + Line::new( + Point::new( + cursor_point.x, + cursor_point.y - hit_position.glyph_ascent, + ), + Point::new( + cursor_point.x, + cursor_point.y + hit_position.glyph_descent, + ), + ) + }); + self.cursor_line.set(cursor_line); + + None + } + + fn event_before_children( + &mut self, + cx: &mut floem::context::EventCx, + event: &floem::event::Event, + ) -> EventPropagation { + let text_offset = self.text_viewport.origin(); + let event = event.clone().offset((-text_offset.x, -text_offset.y)); + match event { + Event::PointerDown(pointer) => { + let offset = self.hit_index(cx, pointer.pos); + self.cursor().update(|cursor| { + cursor.set_insert(Selection::caret(offset)); + }); + if pointer.button.is_primary() && pointer.count == 2 { + let offset = self.hit_index(cx, pointer.pos); + let (start, end) = self + .doc() + .buffer + .with_untracked(|buffer| buffer.select_word(offset)); + self.cursor().update(|cursor| { + cursor.set_insert(Selection::region(start, end)); + }); + } else if pointer.button.is_primary() && pointer.count == 3 { + self.cursor().update(|cursor| { + cursor.set_insert(Selection::region(0, self.content.len())); + }); + } + cx.update_active(self.id); + } + Event::PointerMove(pointer) => { + if cx.is_active(self.id) { + let offset = self.hit_index(cx, pointer.pos); + self.cursor().update(|cursor| { + cursor.set_offset(offset, true, false); + }); + } + } + Event::PointerWheel(pointer_event) => { + let delta = pointer_event.delta; + let delta = if delta.x == 0.0 && delta.y != 0.0 { + Vec2::new(delta.y, delta.x) + } else { + delta + }; + self.clamp_text_viewport(self.text_viewport + delta); + return EventPropagation::Continue; + } + _ => {} + } + EventPropagation::Continue + } + + fn paint(&mut self, cx: &mut floem::context::PaintCx) { + cx.save(); + cx.clip(&self.text_rect.inflate(1.0, 0.0)); + let text_node = self.text_node.unwrap(); + let location = self.id.taffy_layout(text_node).unwrap_or_default().location; + let point = Point::new(location.x as f64, location.y as f64) + - self.text_viewport.origin().to_vec2(); + + self.text_layout.with_untracked(|text_layout| { + let text_layout = text_layout.as_ref().unwrap(); + let height = text_layout.size().height; + let config = self.config.get_untracked(); + + let cursor = self.cursor().get_untracked(); + + if let CursorMode::Insert(selection) = &cursor.mode { + for region in selection.regions() { + if !region.is_caret() { + let min = text_layout.hit_position(region.min()).point.x; + let max = text_layout.hit_position(region.max()).point.x; + cx.fill( + &Rect::ZERO + .with_size(Size::new(max - min, height)) + .with_origin(Point::new(min + point.x, point.y)), + config.color(LapceColor::EDITOR_SELECTION), + 0.0, + ); + } + } + } + + if !self.content.is_empty() { + cx.draw_text(text_layout, point); + } else if !self.placeholder.is_empty() { + cx.draw_text(self.placeholder_text_layout.as_ref().unwrap(), point); + } + + if let Some((start, end)) = self.preedit_range { + let start_position = text_layout.hit_position(start); + let start_point = start_position.point + + self.layout_rect.origin().to_vec2() + - self.text_viewport.origin().to_vec2(); + let end_position = text_layout.hit_position(end); + let end_point = end_position.point + + self.layout_rect.origin().to_vec2() + - self.text_viewport.origin().to_vec2(); + + let line = Line::new( + Point::new( + start_point.x, + start_point.y + start_position.glyph_descent, + ), + Point::new( + end_point.x, + end_point.y + end_position.glyph_descent, + ), + ); + cx.stroke( + &line, + config.color(LapceColor::EDITOR_FOREGROUND), + &Stroke::new(1.0), + ); + } + + if !self.hide_cursor.get_untracked() + && (self.focus || cx.is_focused(self.id)) + { + cx.clip(&self.text_rect.inflate(2.0, 2.0)); + + let hit_position = text_layout.hit_position(self.offset); + let cursor_point = hit_position.point + + self.layout_rect.origin().to_vec2() + - self.text_viewport.origin().to_vec2(); + + let line = Line::new( + Point::new( + cursor_point.x, + cursor_point.y - hit_position.glyph_ascent, + ), + Point::new( + cursor_point.x, + cursor_point.y + hit_position.glyph_descent, + ), + ); + + cx.stroke( + &line, + self.config.get_untracked().color(LapceColor::EDITOR_CARET), + &Stroke::new(2.0), + ); + } + + cx.restore(); + }); + } +} diff --git a/lapce-app/src/title.rs b/lapce-app/src/title.rs new file mode 100644 index 0000000..5933603 --- /dev/null +++ b/lapce-app/src/title.rs @@ -0,0 +1,533 @@ +use std::{rc::Rc, sync::Arc}; + +use floem::{ + View, + event::EventListener, + menu::{Menu, MenuItem}, + peniko::Color, + reactive::{ + Memo, ReadSignal, RwSignal, SignalGet, SignalUpdate, SignalWith, create_memo, + }, + style::{AlignItems, CursorStyle, JustifyContent}, + views::{Decorators, container, drag_window_area, empty, label, stack, svg}, +}; +use lapce_core::meta; +use lapce_rpc::proxy::ProxyStatus; + +use crate::{ + app::{clickable_icon, not_clickable_icon, tooltip_label, window_menu}, + command::{LapceCommand, LapceWorkbenchCommand, WindowCommand}, + config::{LapceConfig, color::LapceColor, icon::LapceIcons}, + listener::Listener, + main_split::MainSplitData, + update::ReleaseInfo, + window_tab::WindowTabData, + workspace::LapceWorkspace, +}; + +fn left( + workspace: Arc, + lapce_command: Listener, + workbench_command: Listener, + config: ReadSignal>, + proxy_status: RwSignal>, + num_window_tabs: Memo, +) -> impl View { + let is_local = workspace.kind.is_local(); + let is_macos = cfg!(target_os = "macos"); + stack(( + empty().style(move |s| { + let should_hide = if is_macos { + num_window_tabs.get() > 1 + } else { + true + }; + s.width(75.0).apply_if(should_hide, |s| s.hide()) + }), + container(svg(move || config.get().ui_svg(LapceIcons::LOGO)).style( + move |s| { + let config = config.get(); + s.size(16.0, 16.0) + .color(config.color(LapceColor::LAPCE_ICON_ACTIVE)) + }, + )) + .style(move |s| s.margin_horiz(10.0).apply_if(is_macos, |s| s.hide())), + not_clickable_icon( + || LapceIcons::MENU, + || false, + || false, + || "Menu", + config, + ) + .popout_menu(move || window_menu(lapce_command, workbench_command)) + .style(move |s| { + s.margin_left(4.0) + .margin_right(6.0) + .apply_if(is_macos, |s| s.hide()) + }), + tooltip_label( + config, + container(svg(move || config.get().ui_svg(LapceIcons::REMOTE)).style( + move |s| { + let config = config.get(); + let size = (config.ui.icon_size() as f32 + 2.0).min(30.0); + s.size(size, size).color(if is_local { + config.color(LapceColor::LAPCE_ICON_ACTIVE) + } else { + match proxy_status.get() { + Some(_) => Color::WHITE, + None => config.color(LapceColor::LAPCE_ICON_ACTIVE), + } + }) + }, + )), + || "Connect to Remote", + ) + .popout_menu(move || { + #[allow(unused_mut)] + let mut menu = Menu::new("").entry( + MenuItem::new("Connect to SSH Host").action(move || { + workbench_command.send(LapceWorkbenchCommand::ConnectSshHost); + }), + ); + if !is_local + && proxy_status.get().is_some_and(|p| { + matches!(p, ProxyStatus::Connecting | ProxyStatus::Connected) + }) + { + menu = menu.entry(MenuItem::new("Disconnect remote").action( + move || { + workbench_command + .send(LapceWorkbenchCommand::DisconnectRemote); + }, + )); + } + #[cfg(windows)] + { + menu = menu.entry(MenuItem::new("Connect to WSL Host").action( + move || { + workbench_command + .send(LapceWorkbenchCommand::ConnectWslHost); + }, + )); + } + menu + }) + .style(move |s| { + let config = config.get(); + let color = if is_local { + Color::TRANSPARENT + } else { + match proxy_status.get() { + Some(ProxyStatus::Connected) => { + config.color(LapceColor::LAPCE_REMOTE_CONNECTED) + } + Some(ProxyStatus::Connecting) => { + config.color(LapceColor::LAPCE_REMOTE_CONNECTING) + } + Some(ProxyStatus::Disconnected) => { + config.color(LapceColor::LAPCE_REMOTE_DISCONNECTED) + } + None => Color::TRANSPARENT, + } + }; + s.height_pct(100.0) + .padding_horiz(10.0) + .items_center() + .background(color) + .hover(|s| { + s.cursor(CursorStyle::Pointer).background( + config.color(LapceColor::PANEL_HOVERED_BACKGROUND), + ) + }) + .active(|s| { + s.cursor(CursorStyle::Pointer).background( + config.color(LapceColor::PANEL_HOVERED_ACTIVE_BACKGROUND), + ) + }) + }), + drag_window_area(empty()) + .style(|s| s.height_pct(100.0).flex_basis(0.0).flex_grow(1.0)), + )) + .style(move |s| { + s.height_pct(100.0) + .flex_basis(0.0) + .flex_grow(1.0) + .items_center() + }) + .debug_name("Left Side of Top Bar") +} + +fn middle( + workspace: Arc, + main_split: MainSplitData, + workbench_command: Listener, + config: ReadSignal>, +) -> impl View { + let local_workspace = workspace.clone(); + let can_jump_backward = { + let main_split = main_split.clone(); + create_memo(move |_| main_split.can_jump_location_backward(true)) + }; + let can_jump_forward = + create_memo(move |_| main_split.can_jump_location_forward(true)); + + let jump_backward = move || { + clickable_icon( + || LapceIcons::LOCATION_BACKWARD, + move || { + workbench_command.send(LapceWorkbenchCommand::JumpLocationBackward); + }, + || false, + move || !can_jump_backward.get(), + || "Jump Backward", + config, + ) + .style(move |s| s.margin_horiz(6.0)) + }; + let jump_forward = move || { + clickable_icon( + || LapceIcons::LOCATION_FORWARD, + move || { + workbench_command.send(LapceWorkbenchCommand::JumpLocationForward); + }, + || false, + move || !can_jump_forward.get(), + || "Jump Forward", + config, + ) + .style(move |s| s.margin_right(6.0)) + }; + + let open_folder = move || { + not_clickable_icon( + || LapceIcons::PALETTE_MENU, + || false, + || false, + || "Open Folder / Recent Workspace", + config, + ) + .popout_menu(move || { + Menu::new("") + .entry(MenuItem::new("Open Folder").action(move || { + workbench_command.send(LapceWorkbenchCommand::OpenFolder); + })) + .entry(MenuItem::new("Open Recent Workspace").action(move || { + workbench_command.send(LapceWorkbenchCommand::PaletteWorkspace); + })) + }) + }; + + stack(( + stack(( + drag_window_area(empty()) + .style(|s| s.height_pct(100.0).flex_basis(0.0).flex_grow(1.0)), + jump_backward(), + jump_forward(), + )) + .style(|s| { + s.flex_basis(0) + .flex_grow(1.0) + .justify_content(Some(JustifyContent::FlexEnd)) + }), + container( + stack(( + svg(move || config.get().ui_svg(LapceIcons::SEARCH)).style( + move |s| { + let config = config.get(); + let icon_size = config.ui.icon_size() as f32; + s.size(icon_size, icon_size) + .color(config.color(LapceColor::LAPCE_ICON_ACTIVE)) + }, + ), + label(move || { + if let Some(s) = local_workspace.display() { + s + } else { + "Open Folder".to_string() + } + }) + .style(|s| s.padding_left(10).padding_right(5).selectable(false)), + open_folder(), + )) + .style(|s| s.align_items(Some(AlignItems::Center))), + ) + .on_event_stop(EventListener::PointerDown, |_| {}) + .on_click_stop(move |_| { + if workspace.clone().path.is_some() { + workbench_command.send(LapceWorkbenchCommand::PaletteHelpAndFile); + } else { + workbench_command.send(LapceWorkbenchCommand::PaletteWorkspace); + } + }) + .style(move |s| { + let config = config.get(); + s.flex_basis(0) + .flex_grow(10.0) + .min_width(200.0) + .max_width(500.0) + .height(26.0) + .justify_content(Some(JustifyContent::Center)) + .align_items(Some(AlignItems::Center)) + .border(1.0) + .border_color(config.color(LapceColor::LAPCE_BORDER)) + .border_radius(6.0) + .background(config.color(LapceColor::EDITOR_BACKGROUND)) + }), + stack(( + clickable_icon( + || LapceIcons::START, + move || { + workbench_command.send(LapceWorkbenchCommand::PaletteRunAndDebug) + }, + || false, + || false, + || "Run and Debug", + config, + ) + .style(move |s| s.margin_horiz(6.0)), + drag_window_area(empty()) + .style(|s| s.height_pct(100.0).flex_basis(0.0).flex_grow(1.0)), + )) + .style(move |s| { + s.flex_basis(0) + .flex_grow(1.0) + .justify_content(Some(JustifyContent::FlexStart)) + }), + )) + .style(|s| { + s.flex_basis(0) + .flex_grow(2.0) + .align_items(Some(AlignItems::Center)) + .justify_content(Some(JustifyContent::Center)) + }) + .debug_name("Middle of Top Bar") +} + +fn right( + window_command: Listener, + workbench_command: Listener, + latest_release: ReadSignal>>, + update_in_progress: RwSignal, + num_window_tabs: Memo, + window_maximized: RwSignal, + config: ReadSignal>, +) -> impl View { + let latest_version = create_memo(move |_| { + let latest_release = latest_release.get(); + let latest_version = + latest_release.as_ref().as_ref().map(|r| r.version.clone()); + if latest_version.is_some() + && latest_version.as_deref() != Some(meta::VERSION) + { + latest_version + } else { + None + } + }); + + let has_update = move || latest_version.with(|v| v.is_some()); + + stack(( + drag_window_area(empty()) + .style(|s| s.height_pct(100.0).flex_basis(0.0).flex_grow(1.0)), + stack(( + not_clickable_icon( + || LapceIcons::SETTINGS, + || false, + || false, + || "Settings", + config, + ) + .popout_menu(move || { + Menu::new("") + .entry(MenuItem::new("Command Palette").action(move || { + workbench_command.send(LapceWorkbenchCommand::PaletteCommand) + })) + .separator() + .entry(MenuItem::new("Open Settings").action(move || { + workbench_command.send(LapceWorkbenchCommand::OpenSettings) + })) + .entry(MenuItem::new("Open Keyboard Shortcuts").action( + move || { + workbench_command + .send(LapceWorkbenchCommand::OpenKeyboardShortcuts) + }, + )) + .entry(MenuItem::new("Open Theme Color Settings").action( + move || { + workbench_command + .send(LapceWorkbenchCommand::OpenThemeColorSettings) + }, + )) + .separator() + .entry(if let Some(v) = latest_version.get_untracked() { + if update_in_progress.get_untracked() { + MenuItem::new(format!("Update in progress ({v})")) + .enabled(false) + } else { + MenuItem::new(format!("Restart to update ({v})")).action( + move || { + workbench_command + .send(LapceWorkbenchCommand::RestartToUpdate) + }, + ) + } + } else { + MenuItem::new("No update available").enabled(false) + }) + .separator() + .entry(MenuItem::new("About Lapce").action(move || { + workbench_command.send(LapceWorkbenchCommand::ShowAbout) + })) + }), + container(label(|| "1".to_string()).style(move |s| { + let config = config.get(); + s.font_size(10.0) + .color(config.color(LapceColor::EDITOR_BACKGROUND)) + .border_radius(100.0) + .margin_left(5.0) + .margin_top(10.0) + .background(config.color(LapceColor::EDITOR_CARET)) + })) + .style(move |s| { + let has_update = has_update(); + s.absolute() + .size_pct(100.0, 100.0) + .justify_end() + .items_end() + .pointer_events_none() + .apply_if(!has_update, |s| s.hide()) + }), + )) + .style(move |s| s.margin_horiz(6.0)), + window_controls_view( + window_command, + true, + num_window_tabs, + window_maximized, + config, + ), + )) + .style(|s| { + s.flex_basis(0) + .flex_grow(1.0) + .justify_content(Some(JustifyContent::FlexEnd)) + }) + .debug_name("Right of top bar") +} + +pub fn title(window_tab_data: Rc) -> impl View { + let workspace = window_tab_data.workspace.clone(); + let lapce_command = window_tab_data.common.lapce_command; + let workbench_command = window_tab_data.common.workbench_command; + let window_command = window_tab_data.common.window_common.window_command; + let latest_release = window_tab_data.common.window_common.latest_release; + let proxy_status = window_tab_data.common.proxy_status; + let num_window_tabs = window_tab_data.common.window_common.num_window_tabs; + let window_maximized = window_tab_data.common.window_common.window_maximized; + let title_height = window_tab_data.title_height; + let update_in_progress = window_tab_data.update_in_progress; + let config = window_tab_data.common.config; + stack(( + left( + workspace.clone(), + lapce_command, + workbench_command, + config, + proxy_status, + num_window_tabs, + ), + middle( + workspace, + window_tab_data.main_split.clone(), + workbench_command, + config, + ), + right( + window_command, + workbench_command, + latest_release, + update_in_progress, + num_window_tabs, + window_maximized, + config, + ), + )) + .on_resize(move |rect| { + let height = rect.height(); + if height != title_height.get_untracked() { + title_height.set(height); + } + }) + .style(move |s| { + let config = config.get(); + s.width_pct(100.0) + .height(37.0) + .items_center() + .background(config.color(LapceColor::PANEL_BACKGROUND)) + .border_bottom(1.0) + .border_color(config.color(LapceColor::LAPCE_BORDER)) + }) + .debug_name("Title / Top Bar") +} + +pub fn window_controls_view( + window_command: Listener, + is_title: bool, + num_window_tabs: Memo, + window_maximized: RwSignal, + config: ReadSignal>, +) -> impl View { + stack(( + clickable_icon( + || LapceIcons::WINDOW_MINIMIZE, + || { + floem::action::minimize_window(); + }, + || false, + || false, + || "Minimize", + config, + ) + .style(|s| s.margin_right(16.0).margin_left(10.0)), + clickable_icon( + move || { + if window_maximized.get() { + LapceIcons::WINDOW_RESTORE + } else { + LapceIcons::WINDOW_MAXIMIZE + } + }, + move || { + floem::action::set_window_maximized( + !window_maximized.get_untracked(), + ); + }, + || false, + || false, + || "Maximize", + config, + ) + .style(|s| s.margin_right(16.0)), + clickable_icon( + || LapceIcons::WINDOW_CLOSE, + move || { + window_command.send(WindowCommand::CloseWindow); + }, + || false, + || false, + || "Close Window", + config, + ) + .style(|s| s.margin_right(6.0)), + )) + .style(move |s| { + s.apply_if( + cfg!(target_os = "macos") + || !config.get_untracked().core.custom_titlebar + || (is_title && num_window_tabs.get() > 1), + |s| s.hide(), + ) + }) +} diff --git a/lapce-app/src/tracing.rs b/lapce-app/src/tracing.rs new file mode 100644 index 0000000..24db7f4 --- /dev/null +++ b/lapce-app/src/tracing.rs @@ -0,0 +1,4 @@ +// Re-export `tracing` crate under own name to not collide and as convenient import +pub use tracing::{ + self, Instrument, Level as TraceLevel, event as trace, instrument, +}; diff --git a/lapce-app/src/update.rs b/lapce-app/src/update.rs new file mode 100644 index 0000000..0924cc6 --- /dev/null +++ b/lapce-app/src/update.rs @@ -0,0 +1,233 @@ +use std::path::{Path, PathBuf}; + +use anyhow::{Result, anyhow}; +use lapce_core::{directory::Directory, meta}; +use serde::Deserialize; + +#[derive(Clone, Deserialize, Debug)] +pub struct ReleaseInfo { + pub tag_name: String, + pub target_commitish: String, + pub assets: Vec, + #[serde(skip)] + pub version: String, +} + +#[derive(Clone, Deserialize, Debug)] +pub struct ReleaseAsset { + pub name: String, + pub browser_download_url: String, +} + +pub fn get_latest_release() -> Result { + let url = match meta::RELEASE { + meta::ReleaseType::Debug => { + return Err(anyhow!("no release for debug")); + } + meta::ReleaseType::Nightly => { + "https://api.github.com/repos/lapce/lapce/releases/tags/nightly" + } + _ => "https://api.github.com/repos/lapce/lapce/releases/latest", + }; + + let resp = lapce_proxy::get_url(url, Some("Lapce"))?; + if !resp.status().is_success() { + return Err(anyhow!("get release info failed {}", resp.text()?)); + } + let mut release: ReleaseInfo = serde_json::from_str(&resp.text()?)?; + + release.version = match release.tag_name.as_str() { + "nightly" => format!( + "{}+Nightly.{}", + env!("CARGO_PKG_VERSION"), + &release.target_commitish[..7] + ), + _ => release + .tag_name + .strip_prefix('v') + .unwrap_or(&release.tag_name) + .to_owned(), + }; + + Ok(release) +} + +pub fn download_release(release: &ReleaseInfo) -> Result { + let dir = + Directory::updates_directory().ok_or_else(|| anyhow!("no directory"))?; + let name = match std::env::consts::OS { + "macos" => "Lapce-macos.dmg", + "linux" => match std::env::consts::ARCH { + "aarch64" => "lapce-linux-arm64.tar.gz", + "x86_64" => "lapce-linux-amd64.tar.gz", + _ => return Err(anyhow!("arch not supported")), + }, + #[cfg(feature = "portable")] + "windows" => "Lapce-windows-portable.zip", + #[cfg(not(feature = "portable"))] + "windows" => "Lapce-windows.msi", + _ => return Err(anyhow!("os not supported")), + }; + let file_path = dir.join(name); + + for asset in &release.assets { + if asset.name == name { + let mut resp = lapce_proxy::get_url(&asset.browser_download_url, None)?; + if !resp.status().is_success() { + return Err(anyhow!("download file error {}", resp.text()?)); + } + let mut out = std::fs::File::create(&file_path)?; + resp.copy_to(&mut out)?; + return Ok(file_path); + } + } + + Err(anyhow!("can't download release")) +} + +#[cfg(target_os = "macos")] +pub fn extract(src: &Path, process_path: &Path) -> Result { + let info = dmg::Attach::new(src).with()?; + let dest = process_path.parent().ok_or_else(|| anyhow!("no parent"))?; + let dest = if dest.file_name().and_then(|s| s.to_str()) == Some("MacOS") { + dest.parent().unwrap().parent().unwrap().parent().unwrap() + } else { + dest + }; + std::fs::remove_dir_all(dest.join("Lapce.app"))?; + fs_extra::copy_items( + &[info.mount_point.join("Lapce.app")], + dest, + &fs_extra::dir::CopyOptions { + overwrite: true, + skip_exist: false, + buffer_size: 64000, + copy_inside: true, + content_only: false, + depth: 0, + }, + )?; + Ok(dest.join("Lapce.app")) +} + +#[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "openbsd"))] +pub fn extract(src: &Path, process_path: &Path) -> Result { + let tar_gz = std::fs::File::open(src)?; + let tar = flate2::read::GzDecoder::new(tar_gz); + let mut archive = tar::Archive::new(tar); + let parent = src.parent().ok_or_else(|| anyhow::anyhow!("no parent"))?; + archive.unpack(parent)?; + std::fs::remove_file(process_path)?; + std::fs::copy(parent.join("Lapce").join("lapce"), process_path)?; + Ok(process_path.to_path_buf()) +} + +#[cfg(all(target_os = "windows", feature = "portable"))] +pub fn extract(src: &Path, process_path: &Path) -> Result { + let parent = src + .parent() + .ok_or_else(|| anyhow::anyhow!("src has no parent"))?; + let dst_parent = process_path + .parent() + .ok_or_else(|| anyhow::anyhow!("process_path has no parent"))?; + + { + let mut archive = zip::ZipArchive::new(std::fs::File::open(src)?)?; + archive.extract(parent)?; + } + + // TODO(dbuga): instead of replacing the exe, run the msi installer for non-portable + // TODO(dbuga): there's a very slight chance the user might end up with a backup file without a working .exe + std::fs::rename(process_path, dst_parent.join("lapce.exe.bak"))?; + std::fs::copy(parent.join("lapce.exe"), process_path)?; + + Ok(process_path.to_path_buf()) +} + +#[cfg(all(target_os = "windows", not(feature = "portable")))] +pub fn extract(src: &Path, _process_path: &Path) -> Result { + // We downloaded an uncompressed msi installer, nothing to extract. + // On the other hand, we need to run this msi so pass its path back out. + Ok(src.to_path_buf()) +} + +#[cfg(target_os = "macos")] +pub fn restart(path: &Path) -> Result<()> { + use std::os::unix::process::CommandExt; + let _ = std::process::Command::new("open") + .arg("-n") + .arg(path) + .arg("--args") + .arg("-n") + .exec(); + Ok(()) +} + +#[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "openbsd"))] +pub fn restart(path: &Path) -> Result<()> { + use std::os::unix::process::CommandExt; + let _ = std::process::Command::new(path).arg("-n").exec(); + Ok(()) +} + +#[cfg(all(target_os = "windows", feature = "portable"))] +pub fn restart(path: &Path) -> Result<()> { + use std::os::windows::process::CommandExt; + const DETACHED_PROCESS: u32 = 0x00000008; + let process_id = std::process::id(); + let path = path + .to_str() + .ok_or_else(|| anyhow!("can't get path to str"))?; + std::process::Command::new("cmd") + .raw_arg(format!( + r#"/C taskkill /PID {process_id} & start "" "{path}""# + )) + .creation_flags(DETACHED_PROCESS) + .spawn()?; + Ok(()) +} + +#[cfg(all(target_os = "windows", not(feature = "portable")))] +pub fn restart(path: &Path) -> Result<()> { + use std::os::windows::process::CommandExt; + const DETACHED_PROCESS: u32 = 0x00000008; + let process_id = std::process::id(); + let path = path + .to_str() + .ok_or_else(|| anyhow!("can't get path to str"))?; + + let lapce_exe = std::env::current_exe() + .map_err(|err| anyhow!("can't get path to exe").context(err))?; + let lapce_exe = lapce_exe + .to_str() + .ok_or_else(|| anyhow!("can't convert exe path to str"))?; + + std::process::Command::new("cmd") + .raw_arg(format!( + r#"/C taskkill /PID {process_id} & msiexec /i "{path}" /qb & start "" "{lapce_exe}""#, + )) + .creation_flags(DETACHED_PROCESS) + .spawn()?; + Ok(()) +} + +#[cfg(all(target_os = "windows", feature = "portable"))] +pub fn cleanup() { + // Clean up backup exe after an update + if let Ok(process_path) = std::env::current_exe() { + if let Some(dst_parent) = process_path.parent() { + if let Err(err) = std::fs::remove_file(dst_parent.join("lapce.exe.bak")) + { + tracing::error!("{:?}", err); + } + } + } +} + +#[cfg(any( + not(target_os = "windows"), + all(target_os = "windows", not(feature = "portable")) +))] +pub fn cleanup() { + // Nothing to do yet +} diff --git a/lapce-app/src/wave.rs b/lapce-app/src/wave.rs new file mode 100644 index 0000000..9382cd0 --- /dev/null +++ b/lapce-app/src/wave.rs @@ -0,0 +1,59 @@ +use floem::{ + Renderer, View, ViewId, + peniko::kurbo::{BezPath, Point, Size}, + style::TextColor, +}; + +pub fn wave_box() -> WaveBox { + WaveBox { id: ViewId::new() } +} + +pub struct WaveBox { + id: ViewId, +} + +impl View for WaveBox { + fn id(&self) -> ViewId { + self.id + } + + fn paint(&mut self, cx: &mut floem::context::PaintCx) { + if let Some(color) = self.id.get_combined_style().get(TextColor) { + let layout = self.id.get_layout().unwrap_or_default(); + let size = layout.size; + let size = Size::new(size.width as f64, size.height as f64); + let radius = 4.0; + + let origin = Point::new(0.0, size.height); + let mut path = BezPath::new(); + path.move_to(origin); + + let mut x = 0.0; + let mut direction = -1.0; + while x < size.width { + let point = origin + (x, 0.0); + let p1 = point + (radius * 1.5, -radius * direction); + let p2 = point + (radius * 3.0, 0.0); + path.quad_to(p1, p2); + x += radius * 3.0; + direction *= -1.0; + } + { + let origin = Point::new(0.0, 0.0); + path.line_to(origin + (x, 0.0)); + direction *= -1.0; + while x >= 0.0 { + x -= radius * 3.0; + let point = origin + (x, 0.0); + let p1 = point + (radius * 1.5, -radius * direction); + let p2 = point; + path.quad_to(p1, p2); + direction *= -1.0; + } + } + path.line_to(origin); + + cx.fill(&path, color, 0.0); + } + } +} diff --git a/lapce-app/src/web_link.rs b/lapce-app/src/web_link.rs new file mode 100644 index 0000000..96a54a9 --- /dev/null +++ b/lapce-app/src/web_link.rs @@ -0,0 +1,24 @@ +use floem::{ + View, + peniko::Color, + style::CursorStyle, + views::{Decorators, label}, +}; + +use crate::{command::InternalCommand, listener::Listener}; + +pub fn web_link( + text: impl Fn() -> String + 'static, + uri: impl Fn() -> String + 'static, + color: impl Fn() -> Color + 'static, + internal_command: Listener, +) -> impl View { + label(text) + .on_click_stop(move |_| { + internal_command.send(InternalCommand::OpenWebUri { uri: uri() }); + }) + .style(move |s| { + s.color(color()) + .hover(move |s| s.cursor(CursorStyle::Pointer)) + }) +} diff --git a/lapce-app/src/window.rs b/lapce-app/src/window.rs new file mode 100644 index 0000000..0eda897 --- /dev/null +++ b/lapce-app/src/window.rs @@ -0,0 +1,394 @@ +use std::{path::PathBuf, rc::Rc, sync::Arc}; + +use floem::{ + ViewId, + action::TimerToken, + peniko::kurbo::{Point, Size}, + reactive::{ + Memo, ReadSignal, RwSignal, Scope, SignalGet, SignalUpdate, SignalWith, + use_context, + }, + window::WindowId, +}; +use serde::{Deserialize, Serialize}; + +use crate::{ + app::AppCommand, + command::{InternalCommand, WindowCommand}, + config::LapceConfig, + db::LapceDb, + keypress::EventRef, + listener::Listener, + update::ReleaseInfo, + window_tab::WindowTabData, + workspace::LapceWorkspace, +}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TabsInfo { + pub active_tab: usize, + pub workspaces: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WindowInfo { + pub size: Size, + pub pos: Point, + pub maximised: bool, + pub tabs: TabsInfo, +} + +#[derive(Clone)] +pub struct WindowCommonData { + pub window_command: Listener, + pub window_scale: RwSignal, + pub size: RwSignal, + pub num_window_tabs: Memo, + pub window_maximized: RwSignal, + pub window_tab_header_height: RwSignal, + pub latest_release: ReadSignal>>, + pub ime_allowed: RwSignal, + pub cursor_blink_timer: RwSignal, + // the value to be update by cursor blinking + pub hide_cursor: RwSignal, + pub app_view_id: RwSignal, + pub extra_plugin_paths: Arc>, +} + +/// `WindowData` is the application model for a top-level window. +/// +/// A top-level window can be independently moved around and +/// resized using your window manager. Normally Lapce has only one +/// top-level window, but new ones can be created using the "New Window" +/// command. +/// +/// Each window has its own collection of "window tabs" (again, there is +/// normally only one window tab), size, position etc. +#[derive(Clone)] +pub struct WindowData { + pub window_id: WindowId, + pub scope: Scope, + /// The set of tabs within the window. These tabs are high-level + /// constructs for workspaces, in particular they are not **editor tabs**. + pub window_tabs: RwSignal, Rc)>>, + pub num_window_tabs: Memo, + /// The index of the active window tab. + pub active: RwSignal, + pub app_command: Listener, + pub position: RwSignal, + pub root_view_id: RwSignal, + pub window_scale: RwSignal, + pub config: RwSignal>, + pub ime_enabled: RwSignal, + pub common: Rc, +} + +impl WindowData { + pub fn new( + window_id: WindowId, + app_view_id: RwSignal, + info: WindowInfo, + window_scale: RwSignal, + latest_release: ReadSignal>>, + extra_plugin_paths: Arc>, + app_command: Listener, + ) -> Self { + let cx = Scope::new(); + let config = + LapceConfig::load(&LapceWorkspace::default(), &[], &extra_plugin_paths); + let config = cx.create_rw_signal(Arc::new(config)); + let root_view_id = cx.create_rw_signal(ViewId::new()); + + let window_tabs = cx.create_rw_signal(im::Vector::new()); + let num_window_tabs = + cx.create_memo(move |_| window_tabs.with(|tabs| tabs.len())); + let active = info.tabs.active_tab; + let window_command = Listener::new_empty(cx); + let ime_allowed = cx.create_rw_signal(false); + let window_maximized = cx.create_rw_signal(false); + let size = cx.create_rw_signal(Size::ZERO); + let window_tab_header_height = cx.create_rw_signal(0.0); + let cursor_blink_timer = cx.create_rw_signal(TimerToken::INVALID); + let hide_cursor = cx.create_rw_signal(false); + + let common = Rc::new(WindowCommonData { + window_command, + window_scale, + size, + num_window_tabs, + window_maximized, + window_tab_header_height, + latest_release, + ime_allowed, + cursor_blink_timer, + hide_cursor, + app_view_id, + extra_plugin_paths, + }); + + for w in info.tabs.workspaces { + let window_tab = + Rc::new(WindowTabData::new(cx, Arc::new(w), common.clone())); + window_tabs.update(|window_tabs| { + window_tabs.push_back((cx.create_rw_signal(0), window_tab)); + }); + } + + if window_tabs.with_untracked(|window_tabs| window_tabs.is_empty()) { + let window_tab = Rc::new(WindowTabData::new( + cx, + Arc::new(LapceWorkspace::default()), + common.clone(), + )); + window_tabs.update(|window_tabs| { + window_tabs.push_back((cx.create_rw_signal(0), window_tab)); + }); + } + + let active = cx.create_rw_signal(active); + let position = cx.create_rw_signal(info.pos); + + let window_data = Self { + window_id, + scope: cx, + window_tabs, + num_window_tabs, + active, + position, + root_view_id, + window_scale, + app_command, + config, + ime_enabled: cx.create_rw_signal(false), + common, + }; + + { + let window_data = window_data.clone(); + window_data.common.window_command.listen(move |cmd| { + window_data.run_window_command(cmd); + }); + } + + { + cx.create_effect(move |_| { + let active = active.get(); + let tab = window_tabs + .with(|tabs| tabs.get(active).map(|(_, tab)| tab.clone())); + if let Some(tab) = tab { + tab.common + .internal_command + .send(InternalCommand::ResetBlinkCursor); + } + }) + } + + window_data + } + + pub fn reload_config(&self) { + let config = LapceConfig::load( + &LapceWorkspace::default(), + &[], + &self.common.extra_plugin_paths, + ); + self.config.set(Arc::new(config)); + let window_tabs = self.window_tabs.get_untracked(); + for (_, window_tab) in window_tabs { + window_tab.reload_config(); + } + } + + pub fn run_window_command(&self, cmd: WindowCommand) { + match cmd { + WindowCommand::SetWorkspace { workspace } => { + let db: Arc = use_context().unwrap(); + if let Err(err) = db.update_recent_workspace(&workspace) { + tracing::error!("{:?}", err); + } + + let active = self.active.get_untracked(); + self.window_tabs.with_untracked(|window_tabs| { + if !window_tabs.is_empty() { + let active = window_tabs.len().saturating_sub(1).min(active); + if let Err(err) = + db.insert_window_tab(window_tabs[active].1.clone()) + { + tracing::error!("{:?}", err); + } + } + }); + + let window_tab = Rc::new(WindowTabData::new( + self.scope, + Arc::new(workspace), + self.common.clone(), + )); + self.window_tabs.update(|window_tabs| { + if window_tabs.is_empty() { + window_tabs + .push_back((self.scope.create_rw_signal(0), window_tab)); + } else { + let active = window_tabs.len().saturating_sub(1).min(active); + let (_, old_window_tab) = window_tabs.set( + active, + (self.scope.create_rw_signal(0), window_tab), + ); + old_window_tab.proxy.shutdown(); + } + }) + } + WindowCommand::NewWorkspaceTab { workspace, end } => { + let db: Arc = use_context().unwrap(); + if let Err(err) = db.update_recent_workspace(&workspace) { + tracing::error!("{:?}", err); + } + + let window_tab = Rc::new(WindowTabData::new( + self.scope, + Arc::new(workspace), + self.common.clone(), + )); + let active = self.active.get_untracked(); + let active = self + .window_tabs + .try_update(|tabs| { + if end || tabs.is_empty() { + tabs.push_back(( + self.scope.create_rw_signal(0), + window_tab, + )); + tabs.len() - 1 + } else { + let index = tabs.len().min(active + 1); + tabs.insert( + index, + (self.scope.create_rw_signal(0), window_tab), + ); + index + } + }) + .unwrap(); + self.active.set(active); + } + WindowCommand::CloseWorkspaceTab { index } => { + let active = self.active.get_untracked(); + let index = index.unwrap_or(active); + self.window_tabs.update(|window_tabs| { + if window_tabs.len() < 2 { + return; + } + + if index < window_tabs.len() { + let (_, old_window_tab) = window_tabs.remove(index); + old_window_tab.proxy.shutdown(); + let db: Arc = use_context().unwrap(); + if let Err(err) = db.save_window_tab(old_window_tab) { + tracing::error!("{:?}", err); + } + } + }); + + let tabs_len = self.window_tabs.with_untracked(|tabs| tabs.len()); + + if active > index && active > 0 { + self.active.set(active - 1); + } else if active >= tabs_len.saturating_sub(1) { + self.active.set(tabs_len.saturating_sub(1)); + } + } + WindowCommand::NextWorkspaceTab => { + let active = self.active.get_untracked(); + let tabs_len = self.window_tabs.with_untracked(|tabs| tabs.len()); + if tabs_len > 1 { + let active = if active >= tabs_len - 1 { + 0 + } else { + active + 1 + }; + self.active.set(active); + } + } + WindowCommand::PreviousWorkspaceTab => { + let active = self.active.get_untracked(); + let tabs_len = self.window_tabs.with_untracked(|tabs| tabs.len()); + if tabs_len > 1 { + let active = if active == 0 { + tabs_len - 1 + } else { + active - 1 + }; + self.active.set(active); + } + } + WindowCommand::NewWindow => { + self.app_command + .send(AppCommand::NewWindow { folder: None }); + } + WindowCommand::CloseWindow => { + self.app_command + .send(AppCommand::CloseWindow(self.window_id)); + } + } + self.app_command.send(AppCommand::SaveApp); + } + + pub fn key_down<'a>(&self, event: impl Into> + Copy) -> bool { + let active = self.active.get_untracked(); + let window_tab = self.window_tabs.with_untracked(|window_tabs| { + window_tabs + .get(active) + .or_else(|| window_tabs.last()) + .cloned() + }); + if let Some((_, window_tab)) = window_tab { + window_tab.key_down(event) + } else { + false + } + } + + pub fn info(&self) -> WindowInfo { + let workspaces: Vec = self + .window_tabs + .get_untracked() + .iter() + .map(|(_, t)| (*t.workspace).clone()) + .collect(); + WindowInfo { + size: self.common.size.get_untracked(), + pos: self.position.get_untracked(), + maximised: false, + tabs: TabsInfo { + active_tab: self.active.get_untracked(), + workspaces, + }, + } + } + + pub fn active_window_tab(&self) -> Option> { + let window_tabs = self.window_tabs.get_untracked(); + let active = self + .active + .get_untracked() + .min(window_tabs.len().saturating_sub(1)); + window_tabs.get(active).map(|(_, tab)| tab.clone()) + } + + pub fn move_tab(&self, from_index: usize, to_index: usize) { + if from_index == to_index { + return; + } + + let to_index = if from_index < to_index { + to_index - 1 + } else { + to_index + }; + self.window_tabs.update(|tabs| { + let tab = tabs.remove(from_index); + tabs.insert(to_index, tab); + }); + self.active.set(to_index); + } +} diff --git a/lapce-app/src/window_tab.rs b/lapce-app/src/window_tab.rs new file mode 100644 index 0000000..f21ead0 --- /dev/null +++ b/lapce-app/src/window_tab.rs @@ -0,0 +1,2989 @@ +use std::{ + collections::{BTreeMap, HashSet}, + env, + path::{Path, PathBuf}, + rc::Rc, + sync::{ + Arc, + mpsc::{Sender, channel}, + }, + time::Instant, +}; + +use alacritty_terminal::vte::ansi::Handler; +use floem::{ + ViewId, + action::{TimerToken, open_file, remove_overlay}, + ext_event::{create_ext_action, create_signal_from_channel}, + file::FileDialogOptions, + keyboard::Modifiers, + kurbo::Size, + peniko::kurbo::{Point, Rect, Vec2}, + prelude::SignalTrack, + reactive::{ + Memo, ReadSignal, RwSignal, Scope, SignalGet, SignalUpdate, SignalWith, + WriteSignal, use_context, + }, + text::{Attrs, AttrsList, FamilyOwned, LineHeightValue, TextLayout}, + views::editor::core::buffer::rope_text::RopeText, +}; +use im::HashMap; +use indexmap::IndexMap; +use itertools::Itertools; +use lapce_core::{ + command::FocusCommand, cursor::CursorAffinity, directory::Directory, meta, + mode::Mode, register::Register, +}; +use lapce_rpc::{ + RpcError, + core::CoreNotification, + dap_types::{ConfigSource, RunDebugConfig}, + file::{Naming, PathObject}, + plugin::PluginId, + proxy::{ProxyResponse, ProxyRpcHandler, ProxyStatus}, + source_control::FileDiff, + terminal::TermId, +}; +use lsp_types::{ + CodeActionOrCommand, CodeLens, Diagnostic, ProgressParams, ProgressToken, + ShowMessageParams, +}; +use serde_json::Value; +use tracing::{Level, debug, error, event}; + +use crate::{ + about::AboutData, + alert::{AlertBoxData, AlertButton}, + code_action::{CodeActionData, CodeActionStatus}, + command::{ + CommandExecuted, CommandKind, InternalCommand, LapceCommand, + LapceWorkbenchCommand, WindowCommand, + }, + completion::{CompletionData, CompletionStatus}, + config::LapceConfig, + db::LapceDb, + debug::{DapData, LapceBreakpoint, RunDebugMode, RunDebugProcess}, + doc::DocContent, + editor::location::{EditorLocation, EditorPosition}, + editor_tab::EditorTabChild, + file_explorer::data::FileExplorerData, + find::Find, + global_search::GlobalSearchData, + hover::HoverData, + id::WindowTabId, + inline_completion::InlineCompletionData, + keypress::{EventRef, KeyPressData, KeyPressFocus, condition::Condition}, + listener::Listener, + lsp::path_from_url, + main_split::{MainSplitData, SplitData, SplitDirection, SplitMoveDirection}, + palette::{DEFAULT_RUN_TOML, PaletteData, PaletteStatus, kind::PaletteKind}, + panel::{ + call_hierarchy_view::{CallHierarchyData, CallHierarchyItemData}, + data::{PanelData, PanelSection, default_panel_order}, + kind::PanelKind, + position::PanelContainerPosition, + }, + plugin::PluginData, + proxy::{ProxyData, new_proxy}, + rename::RenameData, + source_control::SourceControlData, + terminal::{ + event::{TermEvent, TermNotification, terminal_update_process}, + panel::TerminalPanelData, + }, + tracing::*, + window::WindowCommonData, + workspace::{LapceWorkspace, LapceWorkspaceType, WorkspaceInfo}, +}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Focus { + Workbench, + Palette, + CodeAction, + Rename, + AboutPopup, + Panel(PanelKind), +} + +#[derive(Clone)] +pub enum DragContent { + Panel(PanelKind), + EditorTab(EditorTabChild), +} + +impl DragContent { + pub fn is_panel(&self) -> bool { + matches!(self, DragContent::Panel(_)) + } +} + +#[derive(Clone)] +pub struct WorkProgress { + pub token: ProgressToken, + pub title: String, + pub message: Option, + pub percentage: Option, +} + +#[derive(Clone)] +pub struct CommonData { + pub workspace: Arc, + pub scope: Scope, + pub focus: RwSignal, + pub keypress: RwSignal, + pub completion: RwSignal, + pub inline_completion: RwSignal, + pub hover: HoverData, + pub register: RwSignal, + pub find: Find, + pub workbench_size: RwSignal, + pub window_origin: RwSignal, + pub internal_command: Listener, + pub lapce_command: Listener, + pub workbench_command: Listener, + pub term_tx: Sender<(TermId, TermEvent)>, + pub term_notification_tx: Sender, + pub proxy: ProxyRpcHandler, + pub view_id: RwSignal, + pub ui_line_height: Memo, + pub dragging: RwSignal>, + pub config: ReadSignal>, + pub proxy_status: RwSignal>, + pub mouse_hover_timer: RwSignal, + pub breakpoints: RwSignal>>, + // the current focused view which will receive keyboard events + pub keyboard_focus: RwSignal>, + pub window_common: Rc, +} + +impl std::fmt::Debug for CommonData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("CommonData") + .field("workspace", &self.workspace) + .finish() + } +} + +#[derive(Clone)] +pub struct WindowTabData { + pub scope: Scope, + pub window_tab_id: WindowTabId, + pub workspace: Arc, + pub palette: PaletteData, + pub main_split: MainSplitData, + pub file_explorer: FileExplorerData, + pub panel: PanelData, + pub terminal: TerminalPanelData, + pub plugin: PluginData, + pub code_action: RwSignal, + pub code_lens: RwSignal>, + pub source_control: SourceControlData, + pub rename: RenameData, + pub global_search: GlobalSearchData, + pub call_hierarchy_data: CallHierarchyData, + pub about_data: AboutData, + pub alert_data: AlertBoxData, + pub layout_rect: RwSignal, + pub title_height: RwSignal, + pub status_height: RwSignal, + pub proxy: ProxyData, + pub set_config: WriteSignal>, + pub update_in_progress: RwSignal, + pub progresses: RwSignal>, + pub messages: RwSignal>, + pub common: Rc, +} + +impl std::fmt::Debug for WindowTabData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("WindowTabData") + .field("window_tab_id", &self.window_tab_id) + .finish() + } +} + +impl KeyPressFocus for WindowTabData { + fn get_mode(&self) -> Mode { + Mode::Normal + } + + fn check_condition(&self, condition: Condition) -> bool { + match condition { + Condition::PanelFocus => { + matches!(self.common.focus.get_untracked(), Focus::Panel(_)) + } + Condition::SourceControlFocus => { + self.common.focus.get_untracked() + == Focus::Panel(PanelKind::SourceControl) + } + _ => false, + } + } + + fn run_command( + &self, + command: &LapceCommand, + _count: Option, + _mods: Modifiers, + ) -> CommandExecuted { + match &command.kind { + CommandKind::Workbench(cmd) => { + self.run_workbench_command(cmd.clone(), None); + } + CommandKind::Focus(cmd) => { + if self.common.focus.get_untracked() == Focus::Workbench { + match cmd { + FocusCommand::SplitClose => { + self.main_split.editor_tab_child_close_active(); + } + FocusCommand::SplitVertical => { + self.main_split.split_active(SplitDirection::Vertical); + } + FocusCommand::SplitHorizontal => { + self.main_split.split_active(SplitDirection::Horizontal); + } + FocusCommand::SplitRight => { + self.main_split + .split_move_active(SplitMoveDirection::Right); + } + FocusCommand::SplitLeft => { + self.main_split + .split_move_active(SplitMoveDirection::Left); + } + FocusCommand::SplitUp => { + self.main_split + .split_move_active(SplitMoveDirection::Up); + } + FocusCommand::SplitDown => { + self.main_split + .split_move_active(SplitMoveDirection::Down); + } + FocusCommand::SplitExchange => { + self.main_split.split_exchange_active(); + } + _ => { + return CommandExecuted::No; + } + } + } + } + _ => { + return CommandExecuted::No; + } + } + + CommandExecuted::Yes + } + + fn receive_char(&self, _c: &str) {} +} + +impl WindowTabData { + #[allow(clippy::too_many_arguments)] + pub fn new( + cx: Scope, + workspace: Arc, + window_common: Rc, + ) -> Self { + let cx = cx.create_child(); + let db: Arc = use_context().unwrap(); + + let disabled_volts = db.get_disabled_volts().unwrap_or_default(); + let workspace_disabled_volts = db + .get_workspace_disabled_volts(&workspace) + .unwrap_or_default(); + let mut all_disabled_volts = disabled_volts.clone(); + all_disabled_volts.extend(workspace_disabled_volts.clone()); + + let workspace_info = if workspace.path.is_some() { + db.get_workspace_info(&workspace).ok() + } else { + let mut info = db.get_workspace_info(&workspace).ok(); + if let Some(info) = info.as_mut() { + info.split.children.clear(); + } + info + }; + + let config = LapceConfig::load( + &workspace, + &all_disabled_volts, + &window_common.extra_plugin_paths, + ); + let lapce_command = Listener::new_empty(cx); + let workbench_command = Listener::new_empty(cx); + let internal_command = Listener::new_empty(cx); + let keypress = cx.create_rw_signal(KeyPressData::new(cx, &config)); + let proxy_status = cx.create_rw_signal(None); + + let (term_tx, term_rx) = channel(); + let (term_notification_tx, term_notification_rx) = channel(); + { + let term_notification_tx = term_notification_tx.clone(); + std::thread::Builder::new() + .name("terminal update process".to_owned()) + .spawn(move || { + terminal_update_process(term_rx, term_notification_tx); + }) + .unwrap(); + } + + let proxy = new_proxy( + workspace.clone(), + all_disabled_volts, + window_common.extra_plugin_paths.as_ref().clone(), + config.plugins.clone(), + term_tx.clone(), + ); + let (config, set_config) = cx.create_signal(Arc::new(config)); + + let focus = cx.create_rw_signal(Focus::Workbench); + let completion = cx.create_rw_signal(CompletionData::new(cx, config)); + let inline_completion = cx.create_rw_signal(InlineCompletionData::new(cx)); + let hover = HoverData::new(cx); + + let register = cx.create_rw_signal(Register::default()); + let view_id = cx.create_rw_signal(ViewId::new()); + let find = Find::new(cx); + + let ui_line_height = cx.create_memo(move |_| { + let config = config.get(); + let mut text_layout = TextLayout::new(); + + let family: Vec = + FamilyOwned::parse_list(&config.ui.font_family).collect(); + let attrs = Attrs::new() + .family(&family) + .font_size(config.ui.font_size() as f32) + .line_height(LineHeightValue::Normal(1.8)); + let attrs_list = AttrsList::new(attrs); + text_layout.set_text("W", attrs_list, None); + text_layout.size().height + }); + + let common = Rc::new(CommonData { + workspace: workspace.clone(), + scope: cx, + keypress, + focus, + completion, + inline_completion, + hover, + register, + find, + internal_command, + lapce_command, + workbench_command, + term_tx, + term_notification_tx, + proxy: proxy.proxy_rpc.clone(), + view_id, + ui_line_height, + dragging: cx.create_rw_signal(None), + workbench_size: cx.create_rw_signal(Size::ZERO), + config, + proxy_status, + mouse_hover_timer: cx.create_rw_signal(TimerToken::INVALID), + window_origin: cx.create_rw_signal(Point::ZERO), + breakpoints: cx.create_rw_signal(BTreeMap::new()), + keyboard_focus: cx.create_rw_signal(None), + window_common: window_common.clone(), + }); + + let main_split = MainSplitData::new(cx, common.clone()); + let code_action = + cx.create_rw_signal(CodeActionData::new(cx, common.clone())); + let source_control = + SourceControlData::new(cx, main_split.editors, common.clone()); + let file_explorer = + FileExplorerData::new(cx, main_split.editors, common.clone()); + + if let Some(info) = workspace_info.as_ref() { + let root_split = main_split.root_split; + info.split.to_data(main_split.clone(), None, root_split); + } else { + let root_split = main_split.root_split; + let root_split_data = { + let cx = cx.create_child(); + let root_split_data = SplitData { + scope: cx, + parent_split: None, + split_id: root_split, + children: Vec::new(), + direction: SplitDirection::Horizontal, + window_origin: Point::ZERO, + layout_rect: Rect::ZERO, + }; + cx.create_rw_signal(root_split_data) + }; + main_split.splits.update(|splits| { + splits.insert(root_split, root_split_data); + }); + } + + let palette = PaletteData::new( + cx, + workspace.clone(), + main_split.clone(), + keypress.read_only(), + source_control.clone(), + common.clone(), + ); + + let title_height = cx.create_rw_signal(0.0); + let status_height = cx.create_rw_signal(0.0); + let panel_available_size = cx.create_memo(move |_| { + let title_height = title_height.get(); + let status_height = status_height.get(); + let num_window_tabs = window_common.num_window_tabs.get(); + let window_size = window_common.size.get(); + Size::new( + window_size.width, + window_size.height + - title_height + - status_height + - if num_window_tabs > 1 { + window_common.window_tab_header_height.get() + } else { + 0.0 + }, + ) + }); + let panel = workspace_info + .as_ref() + .map(|i| { + let panel_order = db + .get_panel_orders() + .unwrap_or_else(|_| default_panel_order()); + PanelData { + panels: cx.create_rw_signal(panel_order), + styles: cx.create_rw_signal(i.panel.styles.clone()), + size: cx.create_rw_signal(i.panel.size.clone()), + available_size: panel_available_size, + sections: cx.create_rw_signal( + i.panel + .sections + .iter() + .map(|(key, value)| (*key, cx.create_rw_signal(*value))) + .collect(), + ), + common: common.clone(), + } + }) + .unwrap_or_else(|| { + let panel_order = db + .get_panel_orders() + .unwrap_or_else(|_| default_panel_order()); + PanelData::new( + cx, + panel_order, + panel_available_size, + im::HashMap::new(), + common.clone(), + ) + }); + + let terminal = TerminalPanelData::new( + workspace.clone(), + common.config.get_untracked().terminal.get_default_profile(), + common.clone(), + main_split.clone(), + ); + if let Some(workspace_info) = workspace_info.as_ref() { + terminal.debug.breakpoints.set( + workspace_info + .breakpoints + .clone() + .into_iter() + .map(|(path, breakpoints)| { + ( + path, + breakpoints + .into_iter() + .map(|b| (b.line, b)) + .collect::>(), + ) + }) + .collect(), + ); + } + + let rename = RenameData::new(cx, main_split.editors, common.clone()); + let global_search = GlobalSearchData::new(cx, main_split.clone()); + + let plugin = PluginData::new( + cx, + HashSet::from_iter(disabled_volts), + HashSet::from_iter(workspace_disabled_volts), + main_split.editors, + common.clone(), + proxy.core_rpc.clone(), + ); + + { + let notification = create_signal_from_channel(term_notification_rx); + let terminal = terminal.clone(); + cx.create_effect(move |_| { + notification.with(|notification| { + if let Some(notification) = notification.as_ref() { + match notification { + TermNotification::SetTitle { term_id, title } => { + terminal.set_title(term_id, title); + } + TermNotification::RequestPaint => { + view_id.get_untracked().request_paint(); + } + } + } + }); + }); + } + + let about_data = AboutData::new(cx, common.focus); + let alert_data = AlertBoxData::new(cx, common.clone()); + + let window_tab_data = Self { + scope: cx, + window_tab_id: WindowTabId::next(), + workspace, + palette, + main_split, + terminal, + panel, + file_explorer, + code_action, + code_lens: cx.create_rw_signal(None), + source_control, + plugin, + rename, + global_search, + call_hierarchy_data: CallHierarchyData { + root: cx.create_rw_signal(None), + common: common.clone(), + scroll_to_line: cx.create_rw_signal(None), + }, + about_data, + alert_data, + layout_rect: cx.create_rw_signal(Rect::ZERO), + title_height, + status_height, + proxy, + set_config, + update_in_progress: cx.create_rw_signal(false), + progresses: cx.create_rw_signal(IndexMap::new()), + messages: cx.create_rw_signal(Vec::new()), + common, + }; + + { + let focus = window_tab_data.common.focus; + let active_editor = window_tab_data.main_split.active_editor; + let rename_active = window_tab_data.rename.active; + let internal_command = window_tab_data.common.internal_command; + cx.create_effect(move |_| { + let focus = focus.get(); + active_editor.track(); + internal_command.send(InternalCommand::ResetBlinkCursor); + + if focus != Focus::Rename && rename_active.get_untracked() { + rename_active.set(false); + } + }); + } + + { + let window_tab_data = window_tab_data.clone(); + window_tab_data.common.lapce_command.listen(move |cmd| { + window_tab_data.run_lapce_command(cmd); + }); + } + + { + let window_tab_data = window_tab_data.clone(); + window_tab_data.common.workbench_command.listen(move |cmd| { + window_tab_data.run_workbench_command(cmd, None); + }); + } + + { + let window_tab_data = window_tab_data.clone(); + let internal_command = window_tab_data.common.internal_command; + internal_command.listen(move |cmd| { + window_tab_data.run_internal_command(cmd); + }); + } + + { + let window_tab_data = window_tab_data.clone(); + let notification = window_tab_data.proxy.notification; + cx.create_effect(move |_| { + notification.with(|rpc| { + if let Some(rpc) = rpc.as_ref() { + window_tab_data.handle_core_notification(rpc); + } + }); + }); + } + + window_tab_data + } + + pub fn reload_config(&self) { + let db: Arc = use_context().unwrap(); + + let disabled_volts = db.get_disabled_volts().unwrap_or_default(); + let workspace_disabled_volts = db + .get_workspace_disabled_volts(&self.workspace) + .unwrap_or_default(); + let mut all_disabled_volts = disabled_volts; + all_disabled_volts.extend(workspace_disabled_volts); + + let config = LapceConfig::load( + &self.workspace, + &all_disabled_volts, + &self.common.window_common.extra_plugin_paths, + ); + self.common.keypress.update(|keypress| { + keypress.update_keymaps(&config); + }); + + let mut change_plugins = Vec::new(); + for (key, configs) in self.common.config.get_untracked().plugins.iter() { + if config + .plugins + .get(key) + .map(|x| x != configs) + .unwrap_or_default() + { + change_plugins.push(key.clone()); + } + } + self.set_config.set(Arc::new(config.clone())); + if !change_plugins.is_empty() { + self.common + .proxy + .update_plugin_configs(config.plugins.clone()); + if config.core.auto_reload_plugin { + let mut plugin_metas: HashMap< + String, + lapce_rpc::plugin::VoltMetadata, + > = self + .plugin + .installed + .get_untracked() + .values() + .map(|x| { + let meta = x.meta.get_untracked(); + (meta.name.clone(), meta) + }) + .collect(); + for name in change_plugins { + if let Some(meta) = plugin_metas.remove(&name) { + self.common.proxy.reload_volt(meta); + } else { + tracing::error!("not found volt metadata of {}", name); + } + } + } + } + } + + pub fn run_lapce_command(&self, cmd: LapceCommand) { + match cmd.kind { + CommandKind::Workbench(command) => { + self.run_workbench_command(command, cmd.data); + } + CommandKind::Scroll(_) + | CommandKind::Focus(_) + | CommandKind::Edit(_) + | CommandKind::Move(_) => { + if self.palette.status.get_untracked() != PaletteStatus::Inactive { + self.palette.run_command(&cmd, None, Modifiers::empty()); + } else if let Some(editor_data) = + self.main_split.active_editor.get_untracked() + { + editor_data.run_command(&cmd, None, Modifiers::empty()); + } else { + // TODO: dispatch to current focused view? + } + } + CommandKind::MotionMode(_) => {} + CommandKind::MultiSelection(_) => {} + } + } + + pub fn run_workbench_command( + &self, + cmd: LapceWorkbenchCommand, + data: Option, + ) { + use LapceWorkbenchCommand::*; + match cmd { + // ==== Modal ==== + EnableModal => { + let internal_command = self.common.internal_command; + internal_command.send(InternalCommand::SetModal { modal: true }); + } + DisableModal => { + let internal_command = self.common.internal_command; + internal_command.send(InternalCommand::SetModal { modal: false }); + } + + // ==== Files / Folders ==== + OpenFolder => { + if !self.workspace.kind.is_remote() { + let window_command = self.common.window_common.window_command; + let mut options = FileDialogOptions::new().title("Choose a folder").select_directories(); + options = if let Some(parent) = self.workspace.path.as_ref().and_then(|x| x.parent()) { + options.force_starting_directory(parent) + } else { + options + }; + open_file(options, move |file| { + if let Some(mut file) = file { + let workspace = LapceWorkspace { + kind: LapceWorkspaceType::Local, + path: Some(if let Some(path) = file.path.pop() { + path + } else { + tracing::error!("No path"); + return; + }), + last_open: std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(), + }; + window_command + .send(WindowCommand::SetWorkspace { workspace }); + } + }); + } + } + CloseFolder => { + if !self.workspace.kind.is_remote() { + let window_command = self.common.window_common.window_command; + let workspace = LapceWorkspace { + kind: LapceWorkspaceType::Local, + path: None, + last_open: 0, + }; + window_command.send(WindowCommand::SetWorkspace { workspace }); + } + } + OpenFile => { + if !self.workspace.kind.is_remote() { + let internal_command = self.common.internal_command; + let options = FileDialogOptions::new().title("Choose a file"); + open_file(options, move |file| { + if let Some(mut file) = file { + internal_command.send(InternalCommand::OpenFile { + path: if let Some(path) = file.path.pop() { + path + } else { + tracing::error!("No path"); + return; + }, + }) + } + }); + } + } + NewFile => { + self.main_split.new_file(); + } + RevealActiveFileInFileExplorer => { + if let Some(editor_data) = self.main_split.active_editor.get() { + let doc = editor_data.doc(); + let path = if let DocContent::File { path, .. } = + doc.content.get_untracked() + { + Some(path) + } else { + None + }; + let Some(path) = path else { return }; + let path = path.parent().unwrap_or(&path); + + open_uri(path); + } + } + + SaveAll => { + self.main_split.editors.with_editors_untracked(|editors| { + let mut paths = HashSet::new(); + for (_, editor_data) in editors.iter() { + let doc = editor_data.doc(); + let should_save = if let DocContent::File { path, .. } = + doc.content.get_untracked() + { + if paths.contains(&path) { + false + } else { + paths.insert(path.clone()); + + true + } + } else { + false + }; + + if should_save { + editor_data.save(true, || {}); + } + } + }); + } + + // ==== Configuration / Info Files and Folders ==== + OpenSettings => { + self.main_split.open_settings(); + } + OpenSettingsFile => { + if let Some(path) = LapceConfig::settings_file() { + self.main_split.jump_to_location( + EditorLocation { + path, + position: None, + scroll_offset: None, + ignore_unconfirmed: false, + same_editor_tab: false, + }, + None, + ); + } + } + OpenSettingsDirectory => { + if let Some(dir) = Directory::config_directory() { + open_uri(&dir); + } + } + OpenThemeColorSettings => { + self.main_split.open_theme_color_settings(); + } + OpenKeyboardShortcuts => { + self.main_split.open_keymap(); + } + OpenKeyboardShortcutsFile => { + if let Some(path) = LapceConfig::keymaps_file() { + self.main_split.jump_to_location( + EditorLocation { + path, + position: None, + scroll_offset: None, + ignore_unconfirmed: false, + same_editor_tab: false, + }, + None, + ); + } + } + OpenLogFile => { + if let Some(dir) = Directory::logs_directory() { + self.open_paths(&[PathObject::from_path( + dir.join(format!( + "lapce.{}.log", + chrono::prelude::Local::now().format("%Y-%m-%d") + )), + false, + )]) + } + } + OpenLogsDirectory => { + if let Some(dir) = Directory::logs_directory() { + open_uri(&dir); + } + } + OpenProxyDirectory => { + if let Some(dir) = Directory::proxy_directory() { + open_uri(&dir); + } + } + OpenThemesDirectory => { + if let Some(dir) = Directory::themes_directory() { + open_uri(&dir); + } + } + OpenPluginsDirectory => { + if let Some(dir) = Directory::plugins_directory() { + open_uri(&dir); + } + } + OpenGrammarsDirectory => { + if let Some(dir) = Directory::grammars_directory() { + open_uri(&dir); + } + } + OpenQueriesDirectory => { + if let Some(dir) = Directory::queries_directory() { + open_uri(&dir); + } + } + + InstallTheme => {} + ExportCurrentThemeSettings => { + self.main_split.export_theme(); + } + ToggleInlayHints => {} + + // ==== Window ==== + ReloadWindow => { + self.common.window_common.window_command.send( + WindowCommand::SetWorkspace { + workspace: (*self.workspace).clone(), + }, + ); + } + NewWindow => { + self.common + .window_common + .window_command + .send(WindowCommand::NewWindow); + } + CloseWindow => { + self.common + .window_common + .window_command + .send(WindowCommand::CloseWindow); + } + // ==== Window Tabs ==== + NewWindowTab => { + self.common.window_common.window_command.send( + WindowCommand::NewWorkspaceTab { + workspace: LapceWorkspace::default(), + end: false, + }, + ); + } + CloseWindowTab => { + self.common + .window_common + .window_command + .send(WindowCommand::CloseWorkspaceTab { index: None }); + } + NextWindowTab => { + self.common + .window_common + .window_command + .send(WindowCommand::NextWorkspaceTab); + } + PreviousWindowTab => { + self.common + .window_common + .window_command + .send(WindowCommand::PreviousWorkspaceTab); + } + + // ==== Editor Tabs ==== + NextEditorTab => { + if let Some(editor_tab_id) = + self.main_split.active_editor_tab.get_untracked() + { + self.main_split.editor_tabs.with_untracked(|editor_tabs| { + let Some(editor_tab) = editor_tabs.get(&editor_tab_id) + else { + return; + }; + + let new_index = editor_tab.with_untracked(|editor_tab| { + if editor_tab.children.is_empty() { + None + } else if editor_tab.active + == editor_tab.children.len() - 1 + { + Some(0) + } else { + Some(editor_tab.active + 1) + } + }); + + if let Some(new_index) = new_index { + editor_tab.update(|editor_tab| { + editor_tab.active = new_index; + }); + } + }); + } + } + PreviousEditorTab => { + if let Some(editor_tab_id) = + self.main_split.active_editor_tab.get_untracked() + { + self.main_split.editor_tabs.with_untracked(|editor_tabs| { + let Some(editor_tab) = editor_tabs.get(&editor_tab_id) + else { + return; + }; + + let new_index = editor_tab.with_untracked(|editor_tab| { + if editor_tab.children.is_empty() { + None + } else if editor_tab.active == 0 { + Some(editor_tab.children.len() - 1) + } else { + Some(editor_tab.active - 1) + } + }); + + if let Some(new_index) = new_index { + editor_tab.update(|editor_tab| { + editor_tab.active = new_index; + }); + } + }); + } + } + + // ==== Terminal ==== + NewTerminalTab => { + self.terminal.new_tab( + self.common + .config + .get_untracked() + .terminal + .get_default_profile(), + ); + if !self.panel.is_panel_visible(&PanelKind::Terminal) { + self.panel.show_panel(&PanelKind::Terminal); + } + self.common.focus.set(Focus::Panel(PanelKind::Terminal)); + } + CloseTerminalTab => { + self.terminal.close_tab(None); + if self + .terminal + .tab_info + .with_untracked(|info| info.tabs.is_empty()) + { + if self.panel.is_panel_visible(&PanelKind::Terminal) { + self.panel.hide_panel(&PanelKind::Terminal); + } + self.common.focus.set(Focus::Workbench); + } else { + if !self.panel.is_panel_visible(&PanelKind::Terminal) { + self.panel.show_panel(&PanelKind::Terminal); + } + self.common.focus.set(Focus::Panel(PanelKind::Terminal)); + } + } + NextTerminalTab => { + self.terminal.next_tab(); + if !self.panel.is_panel_visible(&PanelKind::Terminal) { + self.panel.show_panel(&PanelKind::Terminal); + } + self.common.focus.set(Focus::Panel(PanelKind::Terminal)); + } + PreviousTerminalTab => { + self.terminal.previous_tab(); + if !self.panel.is_panel_visible(&PanelKind::Terminal) { + self.panel.show_panel(&PanelKind::Terminal); + } + self.common.focus.set(Focus::Panel(PanelKind::Terminal)); + } + + // ==== Remote ==== + ConnectSshHost => { + self.palette.run(PaletteKind::SshHost); + } + #[cfg(windows)] + ConnectWslHost => { + self.palette.run(PaletteKind::WslHost); + } + DisconnectRemote => { + self.common.window_common.window_command.send( + WindowCommand::SetWorkspace { + workspace: LapceWorkspace { + kind: LapceWorkspaceType::Local, + path: None, + last_open: 0, + }, + }, + ); + } + + // ==== Palette Commands ==== + PaletteHelp => self.palette.run(PaletteKind::PaletteHelp), + PaletteHelpAndFile => self.palette.run(PaletteKind::HelpAndFile), + PaletteLine => { + self.palette.run(PaletteKind::Line); + } + Palette => { + self.palette.run(PaletteKind::File); + } + PaletteSymbol => { + self.palette.run(PaletteKind::DocumentSymbol); + } + PaletteWorkspaceSymbol => {} + PaletteCommand => { + self.palette.run(PaletteKind::Command); + } + PaletteWorkspace => { + self.palette.run(PaletteKind::Workspace); + } + PaletteRunAndDebug => { + self.palette.run(PaletteKind::RunAndDebug); + } + PaletteSCMReferences => { + self.palette.run(PaletteKind::SCMReferences); + } + ChangeColorTheme => { + self.palette.run(PaletteKind::ColorTheme); + } + ChangeIconTheme => { + self.palette.run(PaletteKind::IconTheme); + } + ChangeFileLanguage => { + self.palette.run(PaletteKind::Language); + } + ChangeFileLineEnding => { + self.palette.run(PaletteKind::LineEnding); + } + DiffFiles => self.palette.run(PaletteKind::DiffFiles), + + // ==== Running / Debugging ==== + RunAndDebugRestart => { + let active_term = self.terminal.debug.active_term.get_untracked(); + if let Some(is_debug) = active_term + .and_then(|term_id| self.terminal.restart_run_debug(term_id)) + { + self.panel.show_panel(&PanelKind::Terminal); + if is_debug { + self.panel.show_panel(&PanelKind::Debug); + } + } else { + self.palette.run(PaletteKind::RunAndDebug); + } + } + RunAndDebugStop => { + let active_term = self.terminal.debug.active_term.get_untracked(); + if let Some(term_id) = active_term { + self.terminal.stop_run_debug(term_id); + } + } + + // ==== UI ==== + ZoomIn => { + let mut scale = + self.common.window_common.window_scale.get_untracked(); + scale += 0.1; + if scale > 4.0 { + scale = 4.0 + } + self.common.window_common.window_scale.set(scale); + + LapceConfig::update_file( + "ui", + "scale", + toml_edit::Value::from(scale), + ); + } + ZoomOut => { + let mut scale = + self.common.window_common.window_scale.get_untracked(); + scale -= 0.1; + if scale < 0.1 { + scale = 0.1 + } + self.common.window_common.window_scale.set(scale); + + LapceConfig::update_file( + "ui", + "scale", + toml_edit::Value::from(scale), + ); + } + ZoomReset => { + self.common.window_common.window_scale.set(1.0); + + LapceConfig::update_file( + "ui", + "scale", + toml_edit::Value::from(1.0), + ); + } + + ToggleMaximizedPanel => { + if let Some(data) = data { + if let Ok(kind) = serde_json::from_value::(data) { + self.panel.toggle_maximize(&kind); + } + } else { + self.panel.toggle_active_maximize(); + } + } + HidePanel => { + if let Some(data) = data { + if let Ok(kind) = serde_json::from_value::(data) { + self.hide_panel(kind); + } + } + } + ShowPanel => { + if let Some(data) = data { + if let Ok(kind) = serde_json::from_value::(data) { + self.show_panel(kind); + } + } + } + TogglePanelFocus => { + if let Some(data) = data { + if let Ok(kind) = serde_json::from_value::(data) { + self.toggle_panel_focus(kind); + } + } + } + TogglePanelVisual => { + if let Some(data) = data { + if let Ok(kind) = serde_json::from_value::(data) { + self.toggle_panel_visual(kind); + } + } + } + TogglePanelLeftVisual => { + self.toggle_container_visual(&PanelContainerPosition::Left); + } + TogglePanelRightVisual => { + self.toggle_container_visual(&PanelContainerPosition::Right); + } + TogglePanelBottomVisual => { + self.toggle_container_visual(&PanelContainerPosition::Bottom); + } + ToggleTerminalFocus => { + self.toggle_panel_focus(PanelKind::Terminal); + } + ToggleSourceControlFocus => { + self.toggle_panel_focus(PanelKind::SourceControl); + } + TogglePluginFocus => { + self.toggle_panel_focus(PanelKind::Plugin); + } + ToggleFileExplorerFocus => { + self.toggle_panel_focus(PanelKind::FileExplorer); + } + ToggleProblemFocus => { + self.toggle_panel_focus(PanelKind::Problem); + } + ToggleSearchFocus => { + self.toggle_panel_focus(PanelKind::Search); + } + ToggleTerminalVisual => { + self.toggle_panel_visual(PanelKind::Terminal); + } + ToggleSourceControlVisual => { + self.toggle_panel_visual(PanelKind::SourceControl); + } + TogglePluginVisual => { + self.toggle_panel_visual(PanelKind::Plugin); + } + ToggleFileExplorerVisual => { + self.toggle_panel_visual(PanelKind::FileExplorer); + } + ToggleProblemVisual => { + self.toggle_panel_visual(PanelKind::Problem); + } + ToggleDebugVisual => { + self.toggle_panel_visual(PanelKind::Debug); + } + ToggleSearchVisual => { + self.toggle_panel_visual(PanelKind::Search); + } + FocusEditor => { + self.common.focus.set(Focus::Workbench); + } + FocusTerminal => { + self.common.focus.set(Focus::Panel(PanelKind::Terminal)); + } + OpenUIInspector => { + self.common.view_id.get_untracked().inspect(); + } + ShowEnvironment => { + self.main_split.show_env(); + } + + // ==== Source Control ==== + SourceControlInit => { + self.proxy.proxy_rpc.git_init(); + } + CheckoutReference => match data { + Some(reference) => { + if let Some(reference) = reference.as_str() { + self.proxy.proxy_rpc.git_checkout(reference.to_string()); + } + } + None => error!("No ref provided"), + }, + SourceControlCommit => { + self.source_control.commit(); + } + SourceControlCopyActiveFileRemoteUrl => {} + SourceControlDiscardActiveFileChanges => {} + SourceControlDiscardTargetFileChanges => { + if let Some(diff) = data + .and_then(|data| serde_json::from_value::(data).ok()) + { + match diff { + FileDiff::Added(path) => { + self.common.proxy.trash_path(path, Box::new(|_| {})); + } + FileDiff::Modified(path) | FileDiff::Deleted(path) => { + self.common.proxy.git_discard_files_changes(vec![path]); + } + FileDiff::Renamed(old_path, new_path) => { + self.common + .proxy + .git_discard_files_changes(vec![old_path]); + self.common.proxy.trash_path(new_path, Box::new(|_| {})); + } + } + } + } + SourceControlDiscardWorkspaceChanges => {} + + // ==== UI ==== + ShowAbout => { + self.about_data.open(); + } + + // ==== Updating ==== + RestartToUpdate => { + if let Some(release) = self + .common + .window_common + .latest_release + .get_untracked() + .as_ref() + { + let release = release.clone(); + let update_in_progress = self.update_in_progress; + if release.version != *meta::VERSION { + if let Ok(process_path) = env::current_exe() { + update_in_progress.set(true); + let send = create_ext_action( + self.common.scope, + move |_started| { + update_in_progress.set(false); + }, + ); + std::thread::Builder::new().name("RestartToUpdate".to_owned()).spawn(move || { + let do_update = || -> anyhow::Result<()> { + let src = + crate::update::download_release(&release)?; + + let path = + crate::update::extract(&src, &process_path)?; + + crate::update::restart(&path)?; + + Ok(()) + }; + + if let Err(err) = do_update() { + error!("Failed to update: {err}"); + } + + send(false); + }).unwrap(); + } + } + } + } + + // ==== Movement ==== + #[cfg(target_os = "macos")] + InstallToPATH => { + self.common.internal_command.send( + InternalCommand::ExecuteProcess { + program: String::from("osascript"), + arguments: vec![String::from("-e"), format!(r#"do shell script "ln -sf '{}' /usr/local/bin/lapce" with administrator privileges"#, std::env::args().next().unwrap())], + } + ) + } + #[cfg(target_os = "macos")] + UninstallFromPATH => { + self.common.internal_command.send( + InternalCommand::ExecuteProcess { + program: String::from("osascript"), + arguments: vec![String::from("-e"), String::from(r#"do shell script "rm /usr/local/bin/lapce" with administrator privileges"#)], + } + ) + } + JumpLocationForward => { + self.main_split.jump_location_forward(false); + } + JumpLocationBackward => { + self.main_split.jump_location_backward(false); + } + JumpLocationForwardLocal => { + self.main_split.jump_location_forward(true); + } + JumpLocationBackwardLocal => { + self.main_split.jump_location_backward(true); + } + NextError => { + self.main_split.next_error(); + } + PreviousError => {} + Quit => { + floem::quit_app(); + } + RevealInPanel => { + if let Some(editor_data) = + self.main_split.active_editor.get_untracked() + { + self.show_panel(PanelKind::FileExplorer); + self.panel + .section_open(PanelSection::FileExplorer).update(|x| { + *x = true; + }); + if let DocContent::File {path, ..} = editor_data.doc().content.get_untracked() { + self.file_explorer.reveal_in_file_tree(path); + } + } + } + SourceControlOpenActiveFileRemoteUrl => { + if let Some(editor_data) = + self.main_split.active_editor.get_untracked() + { + if let DocContent::File {path, ..} = editor_data.doc().content.get_untracked() { + let offset = editor_data.cursor().with_untracked(|c| c.offset()); + let line = editor_data.doc() + .buffer + .with_untracked(|buffer| buffer.line_of_offset(offset)); + self.common.proxy.git_get_remote_file_url( + path, + create_ext_action(self.scope, move |result| { + if let Ok(ProxyResponse::GitGetRemoteFileUrl { + file_url + }) = result + { + if let Err(err) = open::that(format!("{}#L{}", file_url, line)) { + error!("Failed to open file in github: {}", err); + } + } + }), + ); + + } + } + } + RevealInFileExplorer => { + if let Some(editor_data) = + self.main_split.active_editor.get_untracked() + { + if let DocContent::File {path, ..} = editor_data.doc().content.get_untracked() { + let path = path.parent().unwrap_or(&path); + if !path.exists() { + return; + } + if let Err(err) = open::that(path) { + error!( + "Failed to reveal file in system file explorer: {}", + err + ); + } + } + } + } + ShowCallHierarchy => { + if let Some(editor_data) = + self.main_split.active_editor.get_untracked() + { + editor_data.call_hierarchy(self.clone()); + } + } + FindReferences => { + if let Some(editor_data) = + self.main_split.active_editor.get_untracked() + { + editor_data.find_reference(self.clone()); + } + } + GoToImplementation => { + if let Some(editor_data) = + self.main_split.active_editor.get_untracked() + { + editor_data.go_to_implementation(self.clone()); + } + } + RunInTerminal => { + if let Some(editor_data) = + self.main_split.active_editor.get_untracked() + { + let name = editor_data.word_at_cursor(); + if !name.is_empty() { + let mut args_str = name.split(" "); + let program = args_str.next().map(|x| x.to_string()).unwrap(); + let args: Vec = args_str.map(|x| x.to_string()).collect(); + let args = if args.is_empty() { + None + } else { + Some(args) + }; + + let config = RunDebugConfig { + ty: None, + name, + program, + args, + cwd: None, + env: None, + prelaunch: None, + debug_command: None, + dap_id: Default::default(), + tracing_output: false, + config_source: ConfigSource::RunInTerminal, + }; + self.common + .internal_command + .send(InternalCommand::RunAndDebug { mode: RunDebugMode::Run, config }); + } + } + } + GoToLocation => { + if let Some(editor_data) = + self.main_split.active_editor.get_untracked() + { + let doc = editor_data.doc(); + let path = match if doc.loaded() { + doc.content.with_untracked(|c| c.path().cloned()) + } else { + None + } { + Some(path) => path, + None => return, + }; + let offset = editor_data.cursor().with_untracked(|c| c.offset()); + let internal_command = self.common.internal_command; + + internal_command.send(InternalCommand::MakeConfirmed); + internal_command.send(InternalCommand::GoToLocation { location: EditorLocation { + path, + position: Some(EditorPosition::Offset(offset)), + scroll_offset: None, + ignore_unconfirmed: false, + same_editor_tab: false, + } }); + } + } + AddRunDebugConfig => { + if let Some(editor_data) = + self.main_split.active_editor.get_untracked() + { + editor_data.receive_char(DEFAULT_RUN_TOML); + } + } + + } + } + + pub fn run_internal_command(&self, cmd: InternalCommand) { + let cx = self.scope; + match cmd { + InternalCommand::ReloadConfig => { + self.reload_config(); + } + InternalCommand::UpdateLogLevel { level } => { + // TODO: implement logging panel, runtime log level change + debug!("{level}"); + } + InternalCommand::MakeConfirmed => { + if let Some(editor) = self.main_split.active_editor.get_untracked() { + editor.confirmed.set(true); + } + } + InternalCommand::OpenFile { path } => { + self.main_split.jump_to_location( + EditorLocation { + path, + position: None, + scroll_offset: None, + ignore_unconfirmed: false, + same_editor_tab: false, + }, + None, + ); + } + InternalCommand::OpenAndConfirmedFile { path } => { + self.main_split.jump_to_location( + EditorLocation { + path, + position: None, + scroll_offset: None, + ignore_unconfirmed: false, + same_editor_tab: false, + }, + None, + ); + if let Some(editor) = self.main_split.active_editor.get_untracked() { + editor.confirmed.set(true); + } + } + InternalCommand::OpenFileInNewTab { path } => { + self.main_split.jump_to_location( + EditorLocation { + path, + position: None, + scroll_offset: None, + ignore_unconfirmed: true, + same_editor_tab: false, + }, + None, + ); + } + InternalCommand::OpenFileChanges { path } => { + self.main_split.open_file_changes(path); + } + InternalCommand::ReloadFileExplorer => { + self.file_explorer.reload(); + } + InternalCommand::TestPathCreation { new_path } => { + let naming = self.file_explorer.naming; + + let send = create_ext_action( + self.scope, + move |response: Result| match response { + Ok(_) => { + naming.update(Naming::set_ok); + } + Err(err) => { + naming.update(|naming| naming.set_err(err.message)); + } + }, + ); + + self.common.proxy.test_create_at_path(new_path, send); + } + InternalCommand::FinishRenamePath { + current_path, + new_path, + } => { + let send_current_path = current_path.clone(); + let send_new_path = new_path.clone(); + let file_explorer = self.file_explorer.clone(); + let editors = self.main_split.editors; + + let send = create_ext_action( + self.scope, + move |response: Result| match response { + Ok(response) => { + // Get the canonicalized new path from the proxy. + let new_path = + if let ProxyResponse::CreatePathResponse { path } = + response + { + path + } else { + send_new_path + }; + + // If the renamed item is a file, update any editors the file is open + // in to use the new path. + // If the renamed item is a directory, update any editors in which a + // file the renamed directory is an ancestor of is open to use the + // file's new path. + let renamed_editors_content: Vec<_> = editors + .with_editors_untracked(|editors| { + editors + .values() + .map(|editor| editor.doc().content) + .filter(|content| { + content.with_untracked(|content| { + match content { + DocContent::File { + path, + .. + } => path.starts_with( + &send_current_path, + ), + _ => false, + } + }) + }) + .collect() + }); + + for content in renamed_editors_content { + content.update(|content| { + if let DocContent::File { path, .. } = content { + if let Ok(suffix) = + path.strip_prefix(&send_current_path) + { + *path = new_path.join(suffix); + } + } + }); + } + + file_explorer.reload(); + file_explorer.naming.set(Naming::None); + } + Err(err) => { + file_explorer + .naming + .update(|naming| naming.set_err(err.message)); + } + }, + ); + + self.file_explorer.naming.update(Naming::set_pending); + self.common + .proxy + .rename_path(current_path.clone(), new_path, send); + } + InternalCommand::FinishNewNode { is_dir, path } => { + let file_explorer = self.file_explorer.clone(); + let internal_command = self.common.internal_command; + + let send = create_ext_action( + self.scope, + move |response: Result| { + match response { + Ok(response) => { + file_explorer.reload(); + file_explorer.naming.set(Naming::None); + + // Open a new file in the editor + if let ProxyResponse::CreatePathResponse { path } = + response + { + if !is_dir { + internal_command.send( + InternalCommand::OpenFile { path }, + ); + } + } + } + Err(err) => { + file_explorer + .naming + .update(|naming| naming.set_err(err.message)); + } + } + }, + ); + + self.file_explorer.naming.update(Naming::set_pending); + if is_dir { + self.common.proxy.create_directory(path, send); + } else { + self.common.proxy.create_file(path, send); + } + } + InternalCommand::FinishDuplicate { source, path } => { + let file_explorer = self.file_explorer.clone(); + + let send = create_ext_action( + self.scope, + move |response: Result<_, RpcError>| { + if let Err(err) = response { + file_explorer + .naming + .update(|naming| naming.set_err(err.message)); + } else { + file_explorer.reload(); + file_explorer.naming.set(Naming::None); + } + }, + ); + + self.file_explorer.naming.update(Naming::set_pending); + self.common.proxy.duplicate_path(source, path, send); + } + InternalCommand::GoToLocation { location } => { + self.main_split.go_to_location(location, None); + } + InternalCommand::JumpToLocation { location } => { + self.main_split.jump_to_location(location, None); + } + InternalCommand::PaletteReferences { references } => { + self.palette.references.set(references); + self.palette.run(PaletteKind::Reference); + } + InternalCommand::Split { + direction, + editor_tab_id, + } => { + self.main_split.split(direction, editor_tab_id); + } + InternalCommand::SplitMove { + direction, + editor_tab_id, + } => { + self.main_split.split_move(direction, editor_tab_id); + } + InternalCommand::SplitExchange { editor_tab_id } => { + self.main_split.split_exchange(editor_tab_id); + } + InternalCommand::EditorTabClose { editor_tab_id } => { + self.main_split.editor_tab_close(editor_tab_id); + } + InternalCommand::EditorTabChildClose { + editor_tab_id, + child, + } => { + self.main_split + .editor_tab_child_close(editor_tab_id, child, false); + } + InternalCommand::EditorTabCloseByKind { + editor_tab_id, + child, + kind, + } => { + self.main_split.editor_tab_child_close_by_kind( + editor_tab_id, + child, + kind, + ); + } + InternalCommand::ShowCodeActions { + offset, + mouse_click, + plugin_id, + code_actions, + } => { + let mut code_action = self.code_action.get_untracked(); + code_action.show(plugin_id, code_actions, offset, mouse_click); + self.code_action.set(code_action); + } + InternalCommand::RunCodeAction { plugin_id, action } => { + self.main_split.run_code_action(plugin_id, action); + } + InternalCommand::ApplyWorkspaceEdit { edit } => { + self.main_split.apply_workspace_edit(&edit); + } + InternalCommand::SaveJumpLocation { + path, + offset, + scroll_offset, + } => { + self.main_split + .save_jump_location(path, offset, scroll_offset); + } + InternalCommand::NewTerminal { profile } => { + self.terminal.new_tab(profile); + } + InternalCommand::SplitTerminal { term_id } => { + self.terminal.split(term_id); + } + InternalCommand::SplitTerminalNext { term_id } => { + self.terminal.split_next(term_id); + } + InternalCommand::SplitTerminalPrevious { term_id } => { + self.terminal.split_previous(term_id); + } + InternalCommand::SplitTerminalExchange { term_id } => { + self.terminal.split_exchange(term_id); + } + InternalCommand::RunAndDebug { mode, config } => { + self.run_and_debug(cx, &mode, &config); + } + InternalCommand::StartRename { + path, + placeholder, + position, + start, + } => { + self.rename.start(path, placeholder, start, position); + } + InternalCommand::Search { pattern } => { + self.main_split.set_find_pattern(pattern); + } + InternalCommand::FindEditorReceiveChar { s } => { + self.main_split.find_editor.receive_char(&s); + } + InternalCommand::ReplaceEditorReceiveChar { s } => { + self.main_split.replace_editor.receive_char(&s); + } + InternalCommand::FindEditorCommand { + command, + count, + mods, + } => { + self.main_split + .find_editor + .run_command(&command, count, mods); + } + InternalCommand::ReplaceEditorCommand { + command, + count, + mods, + } => { + self.main_split + .replace_editor + .run_command(&command, count, mods); + } + InternalCommand::FocusEditorTab { editor_tab_id } => { + self.main_split.active_editor_tab.set(Some(editor_tab_id)); + } + InternalCommand::SetColorTheme { name, save } => { + if save { + // The config file is watched + LapceConfig::update_file( + "core", + "color-theme", + toml_edit::Value::from(name), + ); + } else { + let mut new_config = self.common.config.get_untracked(); + Arc::make_mut(&mut new_config) + .set_color_theme(&self.workspace, &name); + self.set_config.set(new_config); + } + } + InternalCommand::SetIconTheme { name, save } => { + if save { + // The config file is watched + LapceConfig::update_file( + "core", + "icon-theme", + toml_edit::Value::from(name), + ); + } else { + let mut new_config = self.common.config.get_untracked(); + Arc::make_mut(&mut new_config) + .set_icon_theme(&self.workspace, &name); + self.set_config.set(new_config); + } + } + InternalCommand::SetModal { modal } => { + LapceConfig::update_file( + "core", + "modal", + toml_edit::Value::from(modal), + ); + } + InternalCommand::OpenWebUri { uri } => { + if !uri.is_empty() { + match open::that(&uri) { + Ok(_) => { + trace!(TraceLevel::TRACE, "opened web uri: {uri:?}"); + } + Err(e) => { + trace!( + TraceLevel::ERROR, + "failed to open web uri: {uri:?}, error: {e}" + ); + } + } + } + } + InternalCommand::ShowAlert { + title, + msg, + buttons, + } => { + self.show_alert(title, msg, buttons); + } + InternalCommand::HideAlert => { + self.alert_data.active.set(false); + } + InternalCommand::SaveScratchDoc { doc } => { + self.main_split.save_scratch_doc(doc); + } + InternalCommand::SaveScratchDoc2 { doc } => { + self.main_split.save_scratch_doc2(doc); + } + InternalCommand::UpdateProxyStatus { status } => { + self.common.proxy_status.set(Some(status)); + } + InternalCommand::DapFrameScopes { dap_id, frame_id } => { + self.terminal.dap_frame_scopes(dap_id, frame_id); + } + InternalCommand::OpenVoltView { volt_id } => { + self.main_split.open_volt_view(volt_id); + } + InternalCommand::ResetBlinkCursor => { + // All the editors share the blinking information and logic, so we can just reset + // one of them. + if let Some(e_data) = self.main_split.active_editor.get_untracked() { + e_data.editor.cursor_info.reset(); + } + } + InternalCommand::OpenDiffFiles { + left_path, + right_path, + } => self.main_split.open_diff_files(left_path, right_path), + InternalCommand::ExecuteProcess { program, arguments } => { + let mut cmd = match std::process::Command::new(program) + .args(arguments) + .spawn() + { + Ok(v) => v, + Err(e) => { + return event!(Level::ERROR, "Failed to spawn process: {e}"); + } + }; + + match cmd.wait() { + Ok(v) => event!(Level::TRACE, "Process exited with status {v}"), + Err(e) => { + event!(Level::ERROR, "Process exited with an error: {e}") + } + }; + } + InternalCommand::ClearTerminalBuffer { + view_id, + tab_index, + terminal_index, + } => { + let Some(tab) = self.terminal.tab_info.with_untracked(|x| { + x.tabs.iter().find_map(|(index, data)| { + if index.get_untracked() == tab_index { + Some(data.clone()) + } else { + None + } + }) + }) else { + error!("could not find terminal tab data: index={tab_index}"); + return; + }; + let Some(raw) = tab.terminals.with_untracked(|x| { + x.iter().find_map(|(index, data)| { + if index.get_untracked() == terminal_index { + Some(data.raw.get_untracked()) + } else { + None + } + }) + }) else { + error!("could not find terminal data: index={terminal_index}"); + return; + }; + raw.write().term.reset_state(); + view_id.request_paint(); + } + InternalCommand::StopTerminal { term_id } => { + self.terminal.stop_run_debug(term_id); + } + InternalCommand::RestartTerminal { term_id } => { + if let Some(is_debug) = self.terminal.restart_run_debug(term_id) { + self.panel.show_panel(&PanelKind::Terminal); + if is_debug { + self.panel.show_panel(&PanelKind::Debug); + } + } else { + self.palette.run(PaletteKind::RunAndDebug); + } + } + InternalCommand::CallHierarchyIncoming { item_id } => { + self.call_hierarchy_incoming(item_id); + } + } + } + + fn handle_core_notification(&self, rpc: &CoreNotification) { + let cx = self.scope; + match rpc { + CoreNotification::ProxyStatus { status } => { + self.common.proxy_status.set(Some(status.to_owned())); + } + CoreNotification::DiffInfo { diff } => { + self.source_control.branch.set(diff.head.clone()); + self.source_control + .branches + .set(diff.branches.iter().cloned().collect()); + self.source_control + .tags + .set(diff.tags.iter().cloned().collect()); + self.source_control.file_diffs.update(|file_diffs| { + *file_diffs = diff + .diffs + .iter() + .cloned() + .map(|diff| { + let checked = + file_diffs.get(diff.path()).is_none_or(|(_, c)| *c); + (diff.path().clone(), (diff, checked)) + }) + .collect(); + }); + + let docs = self.main_split.docs.get_untracked(); + for (_, doc) in docs { + doc.retrieve_head(); + } + } + CoreNotification::CompletionResponse { + request_id, + input, + resp, + plugin_id, + } => { + self.common.completion.update(|completion| { + completion.receive(*request_id, input, resp, *plugin_id); + }); + + let completion = self.common.completion.get_untracked(); + let editor_data = completion + .latest_editor_id + .and_then(|id| self.main_split.editors.editor_untracked(id)); + if let Some(editor_data) = editor_data { + let cursor_offset = + editor_data.cursor().with_untracked(|c| c.offset()); + completion + .update_document_completion(&editor_data, cursor_offset); + } + } + CoreNotification::PublishDiagnostics { diagnostics } => { + let path = path_from_url(&diagnostics.uri); + let diagnostics: im::Vector = diagnostics + .diagnostics + .clone() + .into_iter() + .sorted_by_key(|d| d.range.start) + .collect(); + + self.main_split + .get_diagnostic_data(&path) + .diagnostics + .set(diagnostics); + + // inform the document about the diagnostics + if let Some(doc) = self + .main_split + .docs + .with_untracked(|docs| docs.get(&path).cloned()) + { + doc.init_diagnostics(); + } + } + CoreNotification::ServerStatus { params } => { + if params.is_ok() { + // todo filter by language + self.main_split.docs.with_untracked(|x| { + for doc in x.values() { + doc.get_code_lens(); + doc.get_document_symbol(); + doc.get_semantic_styles(); + doc.get_folding_range(); + doc.get_inlay_hints(); + } + }); + } + } + CoreNotification::TerminalProcessStopped { term_id, exit_code } => { + debug!("TerminalProcessStopped {:?}, {:?}", term_id, exit_code); + if let Err(err) = self + .common + .term_tx + .send((*term_id, TermEvent::CloseTerminal)) + { + tracing::error!("{:?}", err); + } + self.terminal.terminal_stopped(term_id, *exit_code); + if self + .terminal + .tab_info + .with_untracked(|info| info.tabs.is_empty()) + { + if self.panel.is_panel_visible(&PanelKind::Terminal) { + self.panel.hide_panel(&PanelKind::Terminal); + } + self.common.focus.set(Focus::Workbench); + } + } + CoreNotification::TerminalLaunchFailed { term_id, error } => { + self.terminal.launch_failed(term_id, error); + } + CoreNotification::RunInTerminal { config } => { + self.run_in_terminal(cx, &RunDebugMode::Debug, config, true); + } + CoreNotification::TerminalProcessId { + term_id, + process_id, + } => { + self.terminal.set_process_id(term_id, *process_id); + } + CoreNotification::DapStopped { + dap_id, + stopped, + stack_frames, + variables, + } => { + self.show_panel(PanelKind::Debug); + self.terminal + .dap_stopped(dap_id, stopped, stack_frames, variables); + } + CoreNotification::OpenPaths { paths } => { + self.open_paths(paths); + } + CoreNotification::DapContinued { dap_id } => { + self.terminal.dap_continued(dap_id); + } + CoreNotification::DapBreakpointsResp { + path, breakpoints, .. + } => { + self.terminal.debug.breakpoints.update(|all_breakpoints| { + if let Some(current_breakpoints) = all_breakpoints.get_mut(path) + { + let mut line_changed = HashSet::new(); + let mut i = 0; + for (_, current_breakpoint) in current_breakpoints.iter_mut() + { + if !current_breakpoint.active { + continue; + } + if let Some(breakpoint) = breakpoints.get(i) { + current_breakpoint.id = breakpoint.id; + current_breakpoint.verified = breakpoint.verified; + current_breakpoint + .message + .clone_from(&breakpoint.message); + if let Some(new_line) = breakpoint.line { + if current_breakpoint.line + 1 != new_line { + line_changed.insert(current_breakpoint.line); + current_breakpoint.line = + new_line.saturating_sub(1); + } + } + } + i += 1; + } + for line in line_changed { + if let Some(changed) = current_breakpoints.remove(&line) + { + current_breakpoints.insert(changed.line, changed); + } + } + } + }); + } + CoreNotification::OpenFileChanged { path, content } => { + self.main_split.open_file_changed(path, content); + } + CoreNotification::VoltInstalled { volt, icon } => { + self.plugin.volt_installed(volt, icon); + } + CoreNotification::VoltRemoved { volt, .. } => { + self.plugin.volt_removed(volt); + } + CoreNotification::WorkDoneProgress { progress } => { + self.update_progress(progress); + } + CoreNotification::ShowMessage { title, message } => { + self.show_message(title, message); + } + CoreNotification::Log { + level, + message, + target, + } => { + use lapce_rpc::core::LogLevel; + use tracing_log::log::{Level, log}; + + let target = target.clone().unwrap_or(String::from("unknown")); + + match level { + LogLevel::Trace => { + log!(target: &target, Level::Trace, "{}", message); + } + LogLevel::Debug => { + log!(target: &target, Level::Debug, "{}", message); + } + LogLevel::Info => { + log!(target: &target, Level::Info, "{}", message); + } + LogLevel::Warn => { + log!(target: &target, Level::Warn, "{}", message); + } + LogLevel::Error => { + log!(target: &target, Level::Error, "{}", message); + } + } + } + CoreNotification::LogMessage { message, target } => { + use lsp_types::MessageType; + use tracing_log::log::{Level, log}; + match message.typ { + MessageType::ERROR => { + log!(target: target, Level::Error, "{}", message.message) + } + MessageType::WARNING => { + log!(target: target, Level::Warn, "{}", message.message) + } + MessageType::INFO => { + log!(target: target, Level::Info, "{}", message.message) + } + MessageType::DEBUG => { + log!(target: target, Level::Debug, "{}", message.message) + } + MessageType::LOG => { + log!(target: target, Level::Debug, "{}", message.message) + } + _ => {} + } + } + CoreNotification::WorkspaceFileChange => { + self.file_explorer.reload(); + } + _ => {} + } + } + + pub fn key_down<'a>(&self, event: impl Into> + Copy) -> bool { + if self.alert_data.active.get_untracked() { + return false; + } + let focus = self.common.focus.get_untracked(); + let keypress = self.common.keypress.get_untracked(); + let handle = match focus { + Focus::Workbench => self.main_split.key_down(event, &keypress), + Focus::Palette => Some(keypress.key_down(event, &self.palette)), + Focus::CodeAction => { + let code_action = self.code_action.get_untracked(); + Some(keypress.key_down(event, &code_action)) + } + Focus::Rename => Some(keypress.key_down(event, &self.rename)), + Focus::AboutPopup => Some(keypress.key_down(event, &self.about_data)), + Focus::Panel(PanelKind::Terminal) => { + self.terminal.key_down(event, &keypress) + } + Focus::Panel(PanelKind::Search) => { + Some(keypress.key_down(event, &self.global_search)) + } + Focus::Panel(PanelKind::Plugin) => { + Some(keypress.key_down(event, &self.plugin)) + } + Focus::Panel(PanelKind::SourceControl) => { + Some(keypress.key_down(event, &self.source_control)) + } + _ => None, + }; + + if let Some(handle) = &handle { + if handle.handled { + true + } else { + keypress + .handle_keymatch( + self, + handle.keymatch.clone(), + handle.keypress.clone(), + ) + .handled + } + } else { + keypress.key_down(event, self).handled + } + } + + pub fn workspace_info(&self) -> WorkspaceInfo { + let main_split_data = self + .main_split + .splits + .get_untracked() + .get(&self.main_split.root_split) + .cloned() + .unwrap(); + WorkspaceInfo { + split: main_split_data.get_untracked().split_info(self), + panel: self.panel.panel_info(), + breakpoints: self + .terminal + .debug + .breakpoints + .get_untracked() + .into_iter() + .map(|(path, breakpoints)| { + (path, breakpoints.into_values().collect::>()) + }) + .collect(), + } + } + + pub fn hover_origin(&self) -> Option { + if !self.common.hover.active.get_untracked() { + return None; + } + + let editor_id = self.common.hover.editor_id.get_untracked(); + let editor_data = self.main_split.editors.editor(editor_id)?; + + let (window_origin, viewport, editor) = ( + editor_data.window_origin(), + editor_data.viewport(), + &editor_data.editor, + ); + + // TODO(minor): affinity should be gotten from where the hover was started at. + let (point_above, point_below) = editor.points_of_offset( + self.common.hover.offset.get_untracked(), + CursorAffinity::Forward, + ); + + let window_origin = + window_origin.get() - self.common.window_origin.get().to_vec2(); + let viewport = viewport.get(); + let hover_size = self.common.hover.layout_rect.get().size(); + let tab_size = self.layout_rect.get().size(); + + let mut origin = window_origin + + Vec2::new( + point_below.x - viewport.x0, + (point_above.y - viewport.y0) - hover_size.height, + ); + if origin.y < 0.0 { + origin.y = window_origin.y + point_below.y - viewport.y0; + } + if origin.x + hover_size.width + 1.0 > tab_size.width { + origin.x = tab_size.width - hover_size.width - 1.0; + } + if origin.x <= 0.0 { + origin.x = 0.0; + } + + Some(origin) + } + + pub fn completion_origin(&self) -> Point { + let completion = self.common.completion.get(); + if completion.status == CompletionStatus::Inactive { + return Point::ZERO; + } + let config = self.common.config.get(); + let editor_data = + if let Some(editor) = self.main_split.active_editor.get_untracked() { + editor + } else { + return Point::ZERO; + }; + + let (window_origin, viewport, editor) = ( + editor_data.window_origin(), + editor_data.viewport(), + &editor_data.editor, + ); + + // TODO(minor): What affinity should we use for this? Probably just use the cursor's + // original affinity.. + let (point_above, point_below) = + editor.points_of_offset(completion.offset, CursorAffinity::Forward); + + let window_origin = + window_origin.get() - self.common.window_origin.get().to_vec2(); + let viewport = viewport.get(); + let completion_size = completion.layout_rect.size(); + let tab_size = self.layout_rect.get().size(); + + let mut origin = window_origin + + Vec2::new( + point_below.x + - viewport.x0 + - config.editor.line_height() as f64 + - 5.0, + point_below.y - viewport.y0, + ); + if origin.y + completion_size.height > tab_size.height { + origin.y = window_origin.y + (point_above.y - viewport.y0) + - completion_size.height; + } + if origin.x + completion_size.width + 1.0 > tab_size.width { + origin.x = tab_size.width - completion_size.width - 1.0; + } + if origin.x <= 0.0 { + origin.x = 0.0; + } + + origin + } + + pub fn code_action_origin(&self) -> Point { + let code_action = self.code_action.get(); + let config = self.common.config.get(); + if code_action.status.get_untracked() == CodeActionStatus::Inactive { + return Point::ZERO; + } + + let tab_size = self.layout_rect.get().size(); + let code_action_size = code_action.layout_rect.size(); + + let editor_data = + if let Some(editor) = self.main_split.active_editor.get_untracked() { + editor + } else { + return Point::ZERO; + }; + + let (window_origin, viewport, editor) = ( + editor_data.window_origin(), + editor_data.viewport(), + &editor_data.editor, + ); + + // TODO(minor): What affinity should we use for this? + let (_point_above, point_below) = + editor.points_of_offset(code_action.offset, CursorAffinity::Forward); + + let window_origin = + window_origin.get() - self.common.window_origin.get().to_vec2(); + let viewport = viewport.get(); + + let mut origin = window_origin + + Vec2::new( + if code_action.mouse_click { + 0.0 + } else { + point_below.x - viewport.x0 + }, + point_below.y - viewport.y0, + ); + + if origin.y + code_action_size.height > tab_size.height { + origin.y = origin.y + - config.editor.line_height() as f64 + - code_action_size.height; + } + if origin.x + code_action_size.width + 1.0 > tab_size.width { + origin.x = tab_size.width - code_action_size.width - 1.0; + } + if origin.x <= 0.0 { + origin.x = 0.0; + } + + origin + } + + pub fn rename_origin(&self) -> Point { + let config = self.common.config.get(); + if !self.rename.active.get() { + return Point::ZERO; + } + + let tab_size = self.layout_rect.get().size(); + let rename_size = self.rename.layout_rect.get().size(); + + let editor_data = + if let Some(editor) = self.main_split.active_editor.get_untracked() { + editor + } else { + return Point::ZERO; + }; + + let (window_origin, viewport, editor) = ( + editor_data.window_origin(), + editor_data.viewport(), + &editor_data.editor, + ); + + // TODO(minor): What affinity should we use for this? + let (_point_above, point_below) = editor.points_of_offset( + self.rename.start.get_untracked(), + CursorAffinity::Forward, + ); + + let window_origin = + window_origin.get() - self.common.window_origin.get().to_vec2(); + let viewport = viewport.get(); + + let mut origin = window_origin + + Vec2::new(point_below.x - viewport.x0, point_below.y - viewport.y0); + + if origin.y + rename_size.height > tab_size.height { + origin.y = + origin.y - config.editor.line_height() as f64 - rename_size.height; + } + if origin.x + rename_size.width + 1.0 > tab_size.width { + origin.x = tab_size.width - rename_size.width - 1.0; + } + if origin.x <= 0.0 { + origin.x = 0.0; + } + + origin + } + + /// Get the mode for the current editor or terminal + pub fn mode(&self) -> Mode { + if self.common.config.get().core.modal { + let mode = if self.common.focus.get() == Focus::Workbench { + self.main_split + .active_editor + .get() + .map(|editor| editor.cursor().with(|c| c.get_mode())) + } else { + None + }; + + mode.unwrap_or(Mode::Normal) + } else { + Mode::Insert + } + } + + pub fn toggle_panel_visual(&self, kind: PanelKind) { + if self.panel.is_panel_visible(&kind) { + self.hide_panel(kind); + } else { + self.show_panel(kind); + } + } + + /// Toggle a specific kind of panel. + fn toggle_panel_focus(&self, kind: PanelKind) { + let should_hide = match kind { + PanelKind::FileExplorer + | PanelKind::Plugin + | PanelKind::Problem + | PanelKind::Debug + | PanelKind::CallHierarchy + | PanelKind::DocumentSymbol + | PanelKind::References + | PanelKind::Implementation => { + // Some panels don't accept focus (yet). Fall back to visibility check + // in those cases. + self.panel.is_panel_visible(&kind) + } + PanelKind::Terminal | PanelKind::SourceControl | PanelKind::Search => { + self.is_panel_focused(kind) + } + }; + if should_hide { + self.hide_panel(kind); + } else { + self.show_panel(kind); + } + } + + /// Toggle a panel on one of the sides. + fn toggle_container_visual(&self, position: &PanelContainerPosition) { + let shown = !self.panel.is_container_shown(position, false); + self.panel.set_shown(&position.first(), shown); + self.panel.set_shown(&position.second(), shown); + + if shown { + if let Some((kind, _)) = self + .panel + .active_panel_at_position(&position.second(), false) + { + self.show_panel(kind); + } + + if let Some((kind, _)) = self + .panel + .active_panel_at_position(&position.first(), false) + { + self.show_panel(kind); + } + } else { + if let Some((kind, _)) = self + .panel + .active_panel_at_position(&position.second(), false) + { + self.hide_panel(kind); + } + + if let Some((kind, _)) = self + .panel + .active_panel_at_position(&position.first(), false) + { + self.hide_panel(kind); + } + } + } + + fn is_panel_focused(&self, kind: PanelKind) -> bool { + // Moving between e.g. Search and Problems doesn't affect focus, so we need to also check + // visibility. + self.common.focus.get_untracked() == Focus::Panel(kind) + && self.panel.is_panel_visible(&kind) + } + + fn hide_panel(&self, kind: PanelKind) { + self.panel.hide_panel(&kind); + self.common.focus.set(Focus::Workbench); + } + + pub fn show_panel(&self, kind: PanelKind) { + if kind == PanelKind::Terminal + && self + .terminal + .tab_info + .with_untracked(|info| info.tabs.is_empty()) + { + self.terminal.new_tab( + self.common + .config + .get_untracked() + .terminal + .get_default_profile(), + ); + } + self.panel.show_panel(&kind); + if kind == PanelKind::Search + && self.common.focus.get_untracked() == Focus::Workbench + { + let active_editor = self.main_split.active_editor.get_untracked(); + let word = active_editor.map(|editor| editor.word_at_cursor()); + if let Some(word) = word { + if !word.is_empty() { + self.global_search.set_pattern(word); + } + } + } + self.common.focus.set(Focus::Panel(kind)); + } + + fn run_and_debug( + &self, + cx: Scope, + mode: &RunDebugMode, + config: &RunDebugConfig, + ) { + debug!("{:?}", config); + match mode { + RunDebugMode::Run => { + self.run_in_terminal(cx, mode, config, false); + } + RunDebugMode::Debug => { + if config.prelaunch.is_some() { + self.run_in_terminal(cx, mode, config, false); + } else { + self.common.proxy.dap_start( + config.clone(), + self.terminal.debug.source_breakpoints(), + ) + }; + if !self.panel.is_panel_visible(&PanelKind::Debug) { + self.panel.show_panel(&PanelKind::Debug); + } + } + } + } + + fn run_in_terminal( + &self, + cx: Scope, + mode: &RunDebugMode, + config: &RunDebugConfig, + from_dap: bool, + ) { + // if not from dap, then run prelaunch first + let is_prelaunch = !from_dap; + let term_id = if let Some(terminal) = + self.terminal.get_stopped_run_debug_terminal(mode, config) + { + terminal.new_process(Some(RunDebugProcess { + mode: *mode, + config: config.clone(), + stopped: false, + created: Instant::now(), + is_prelaunch, + })); + + terminal.term_id + } else { + let new_terminal_tab = self.terminal.new_tab_run_debug( + Some(RunDebugProcess { + mode: *mode, + config: config.clone(), + stopped: false, + created: Instant::now(), + is_prelaunch, + }), + None, + ); + new_terminal_tab.active_terminal(false).unwrap().term_id + }; + self.common.focus.set(Focus::Panel(PanelKind::Terminal)); + self.terminal.focus_terminal(term_id); + + self.terminal.debug.active_term.set(Some(term_id)); + self.terminal.debug.daps.update(|daps| { + daps.insert( + config.dap_id, + DapData::new(cx, config.dap_id, term_id, self.common.clone()), + ); + }); + + if !self.panel.is_panel_visible(&PanelKind::Terminal) { + self.panel.show_panel(&PanelKind::Terminal); + } + } + + pub fn open_paths(&self, paths: &[PathObject]) { + let (folders, files): (Vec<&PathObject>, Vec<&PathObject>) = + paths.iter().partition(|p| p.is_dir); + + for folder in folders { + self.common.window_common.window_command.send( + WindowCommand::NewWorkspaceTab { + workspace: LapceWorkspace { + kind: self.workspace.kind.clone(), + path: Some(folder.path.clone()), + last_open: 0, + }, + end: false, + }, + ); + } + + for file in files { + let position = file.linecol.map(|pos| { + EditorPosition::Position(lsp_types::Position { + line: pos.line.saturating_sub(1) as u32, + character: pos.column.saturating_sub(1) as u32, + }) + }); + + self.common + .internal_command + .send(InternalCommand::GoToLocation { + location: EditorLocation { + path: file.path.clone(), + position, + scroll_offset: None, + // Create a new editor for the file, so we don't change any current unconfirmed + // editor + ignore_unconfirmed: true, + same_editor_tab: false, + }, + }); + } + } + + pub fn show_alert(&self, title: String, msg: String, buttons: Vec) { + self.alert_data.title.set(title); + self.alert_data.msg.set(msg); + self.alert_data.buttons.set(buttons); + self.alert_data.active.set(true); + } + + fn update_progress(&self, progress: &ProgressParams) { + let token = progress.token.clone(); + match &progress.value { + lsp_types::ProgressParamsValue::WorkDone(progress) => match progress { + lsp_types::WorkDoneProgress::Begin(progress) => { + let progress = WorkProgress { + token: token.clone(), + title: progress.title.clone(), + message: progress.message.clone(), + percentage: progress.percentage, + }; + self.progresses.update(|p| { + p.insert(token, progress); + }); + } + lsp_types::WorkDoneProgress::Report(report) => { + self.progresses.update(|p| { + if let Some(progress) = p.get_mut(&token) { + progress.message.clone_from(&report.message); + progress.percentage = report.percentage; + } + }) + } + lsp_types::WorkDoneProgress::End(_) => { + self.progresses.update(|p| { + p.swap_remove(&token); + }); + } + }, + } + } + + fn show_message(&self, title: &str, message: &ShowMessageParams) { + self.messages.update(|messages| { + messages.push((title.to_string(), message.clone())); + }); + } + + pub fn update_code_lens_id(&self, view_id: Option) { + if let Some(Some(old_id)) = self.code_lens.try_update(|x| { + let old = x.take(); + if let Some(id) = view_id { + let _ = x.insert(id); + } + old + }) { + remove_overlay(old_id); + } + } + + pub fn show_code_lens( + &self, + mouse_click: bool, + plugin_id: PluginId, + offset: usize, + lens: im::Vector, + ) { + self.common + .internal_command + .send(InternalCommand::ShowCodeActions { + offset, + mouse_click, + plugin_id, + code_actions: lens + .into_iter() + .filter_map(|lens| { + Some(CodeActionOrCommand::Command(lens.command?)) + }) + .collect(), + }); + } + + pub fn call_hierarchy_incoming(&self, item_id: ViewId) { + let Some(root) = self.call_hierarchy_data.root.get_untracked() else { + return; + }; + let Some(item) = CallHierarchyItemData::find_by_id(root, item_id) else { + return; + }; + let root_item = item; + let path: PathBuf = item.get_untracked().item.uri.to_file_path().unwrap(); + let scope = self.scope; + let send = + create_ext_action(scope, move |_rs: Result| { + match _rs { + Ok(ProxyResponse::CallHierarchyIncomingResponse { items }) => { + if let Some(items) = items { + let mut item_children = Vec::new(); + for x in items { + let item = Rc::new(x.from); + for range in x.from_ranges { + item_children.push(scope.create_rw_signal( + CallHierarchyItemData { + view_id: floem::ViewId::new(), + item: item.clone(), + from_range: range, + init: false, + open: scope.create_rw_signal(false), + children: + scope.create_rw_signal(Vec::new()), + }, + )) + } + } + root_item.update(|x| { + x.init = true; + x.children.update(|children| { + *children = item_children; + }) + }); + } + } + Err(err) => { + tracing::error!("{:?}", err); + } + Ok(_) => {} + } + }); + self.common.proxy.call_hierarchy_incoming( + path, + item.get_untracked().item.as_ref().clone(), + send, + ); + } +} + +/// Open path with the default application without blocking. +fn open_uri(path: &Path) { + match open::that(path) { + Ok(_) => { + debug!("opened active file: {path:?}"); + } + Err(e) => { + error!("failed to open active file: {path:?}, error: {e}"); + } + } +} diff --git a/lapce-app/src/workspace.rs b/lapce-app/src/workspace.rs new file mode 100644 index 0000000..7181ac0 --- /dev/null +++ b/lapce-app/src/workspace.rs @@ -0,0 +1,159 @@ +use std::{collections::HashMap, fmt::Display, path::PathBuf}; + +use serde::{Deserialize, Serialize}; + +use crate::{debug::LapceBreakpoint, main_split::SplitInfo, panel::data::PanelInfo}; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Hash)] +pub struct SshHost { + pub user: Option, + pub host: String, + pub port: Option, +} + +impl SshHost { + pub fn from_string(s: &str) -> Self { + let mut whole_splits = s.split(':'); + let splits = whole_splits + .next() + .unwrap() + .split('@') + .collect::>(); + let mut splits = splits.iter().rev(); + let host = splits.next().unwrap().to_string(); + let user = splits.next().map(|s| s.to_string()); + let port = whole_splits.next().and_then(|s| s.parse::().ok()); + Self { user, host, port } + } + + pub fn user_host(&self) -> String { + if let Some(user) = self.user.as_ref() { + format!("{user}@{}", self.host) + } else { + self.host.clone() + } + } +} + +impl Display for SshHost { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if let Some(user) = self.user.as_ref() { + write!(f, "{user}@")?; + } + write!(f, "{}", self.host)?; + if let Some(port) = self.port { + write!(f, ":{port}")?; + } + Ok(()) + } +} + +#[cfg(windows)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Hash)] +pub struct WslHost { + pub host: String, +} + +#[cfg(windows)] +impl Display for WslHost { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.host)?; + Ok(()) + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub enum LapceWorkspaceType { + Local, + RemoteSSH(SshHost), + #[cfg(windows)] + RemoteWSL(WslHost), +} + +impl LapceWorkspaceType { + pub fn is_local(&self) -> bool { + matches!(self, LapceWorkspaceType::Local) + } + + pub fn is_remote(&self) -> bool { + use LapceWorkspaceType::*; + + #[cfg(not(windows))] + return matches!(self, RemoteSSH(_)); + + #[cfg(windows)] + return matches!(self, RemoteSSH(_) | RemoteWSL(_)); + } +} + +impl std::fmt::Display for LapceWorkspaceType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + LapceWorkspaceType::Local => f.write_str("Local"), + LapceWorkspaceType::RemoteSSH(remote) => { + write!(f, "ssh://{remote}") + } + #[cfg(windows)] + LapceWorkspaceType::RemoteWSL(remote) => { + write!(f, "{remote} (WSL)") + } + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct LapceWorkspace { + pub kind: LapceWorkspaceType, + pub path: Option, + pub last_open: u64, +} + +impl LapceWorkspace { + pub fn display(&self) -> Option { + let path = self.path.as_ref()?; + let path = path + .file_name() + .unwrap_or(path.as_os_str()) + .to_string_lossy() + .to_string(); + let remote = match &self.kind { + LapceWorkspaceType::Local => String::new(), + LapceWorkspaceType::RemoteSSH(remote) => { + format!(" [SSH: {}]", remote.host) + } + #[cfg(windows)] + LapceWorkspaceType::RemoteWSL(remote) => { + format!(" [WSL: {}]", remote.host) + } + }; + Some(format!("{path}{remote}")) + } +} + +impl Default for LapceWorkspace { + fn default() -> Self { + Self { + kind: LapceWorkspaceType::Local, + path: None, + last_open: 0, + } + } +} + +impl std::fmt::Display for LapceWorkspace { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}:{}", + self.kind, + self.path.as_ref().and_then(|p| p.to_str()).unwrap_or("") + ) + } +} + +#[derive(Clone, Serialize, Deserialize)] +pub struct WorkspaceInfo { + pub split: SplitInfo, + pub panel: PanelInfo, + pub breakpoints: HashMap>, +} diff --git a/lapce-core/Cargo.toml b/lapce-core/Cargo.toml new file mode 100644 index 0000000..2182b7a --- /dev/null +++ b/lapce-core/Cargo.toml @@ -0,0 +1,43 @@ +[package] +name = "lapce-core" +license = { workspace = true } +version = { workspace = true } +authors = { workspace = true } +edition = { workspace = true } +rust-version = { workspace = true } + +[dependencies] +directories = { workspace = true } +itertools = { workspace = true } +once_cell = { workspace = true } +strum = { workspace = true } +strum_macros = { workspace = true } +thiserror = { workspace = true } +tracing = { workspace = true } +include_dir = { workspace = true } +regex = { workspace = true } + +lsp-types = { workspace = true } +lapce-xi-rope = { workspace = true } +lapce-rpc = { workspace = true } +floem-editor-core = { workspace = true } + +libloading = "0.8.1" +slotmap = "1.0" +arc-swap = "1.6.0" +tree-sitter = "0.22.6" +ahash = "0.8.11" +remain = "0.2" +hashbrown = { version = "0.14.5", features = ["raw"] } + + +[features] +default = [] +portable = [] +distribution = [] + +[build-dependencies] +anyhow = { workspace = true } + +[target.'cfg(not(target_os = "linux"))'.build-dependencies] +git2 = { workspace = true } diff --git a/lapce-core/build.rs b/lapce-core/build.rs new file mode 100644 index 0000000..d4a7d15 --- /dev/null +++ b/lapce-core/build.rs @@ -0,0 +1,114 @@ +use std::{env, fs, path::Path}; + +use anyhow::Result; + +#[derive(Debug)] +struct ReleaseInfo { + version: String, + branch: String, +} + +fn main() -> Result<()> { + println!("cargo:rerun-if-changed=build.rs"); + println!("cargo:rerun-if-changed=../.git/HEAD"); + println!("cargo:rerun-if-env-changed=CARGO_PKG_VERSION"); + println!("cargo:rerun-if-env-changed=CARGO_FEATURE_DISTRIBUTION"); + println!("cargo:rerun-if-env-changed=RELEASE_TAG_NAME"); + + let release_info = get_info()?; + + // Print info to terminal during compilation + println!("cargo::warning=Compiling meta: {release_info:?}"); + + let meta_file = Path::new(&env::var("OUT_DIR")?).join("meta.rs"); + + let ReleaseInfo { version, branch } = release_info; + + #[rustfmt::skip] + let meta = format!(r#" + pub const NAME: &str = "Lapce-{branch}"; + pub const VERSION: &str = "{version}"; + pub const RELEASE: ReleaseType = ReleaseType::{branch}; + "#); + + fs::write(meta_file, meta)?; + + Ok(()) +} + +fn get_info() -> Result { + // CARGO_PKG_* are always available, even in build scripts + let cargo_tag = env!("CARGO_PKG_VERSION"); + + // For any downstream that complains about us doing magic + if env::var("CARGO_FEATURE_DISTRIBUTION").is_ok() { + return Ok(ReleaseInfo { + version: cargo_tag.to_string(), + branch: String::from("Stable"), + }); + } + + let release_info = { + let release_tag = env::var("RELEASE_TAG_NAME").unwrap_or_default(); + + if release_tag.starts_with('v') { + ReleaseInfo { + version: cargo_tag.to_string(), + branch: "Stable".to_string(), + } + } else { + #[cfg(not(debug_assertions))] + let release = "Nightly"; + #[cfg(debug_assertions)] + let release = "Debug"; + + let tag = format!( + "{cargo_tag}+{release}.{}", + get_head().unwrap_or("unknown".to_string()) + ); + ReleaseInfo { + version: tag, + branch: release.to_string(), + } + } + }; + + Ok(release_info) +} + +#[cfg(not(target_os = "linux"))] +fn get_head() -> Option { + let repo = match git2::Repository::discover(format!( + "{}/..", + env::var("CARGO_MANIFEST_DIR").ok()? + )) { + Ok(v) => v, + Err(err) => { + println!("cargo::warning=Failed to obtain git repo: {err}"); + return None; + } + }; + let reference = match repo.head() { + Ok(v) => v, + Err(err) => { + println!("cargo::warning=Failed to obtain head: {err}"); + return None; + } + }; + let commit = reference.target(); + println!("cargo::warning=Commit found: {commit:?}"); + commit.map(|s| s.to_string().split_at(7).0.to_owned()) +} + +#[cfg(target_os = "linux")] +fn get_head() -> Option { + let cmd = std::process::Command::new("git") + .args(["show", "--pretty=format:%h", "--no-patch"]) + .output() + .ok()?; + + let commit = String::from_utf8_lossy(&cmd.stdout); + let commit = commit.trim(); + + Some(commit.to_string()) +} diff --git a/lapce-core/src/directory.rs b/lapce-core/src/directory.rs new file mode 100644 index 0000000..4284977 --- /dev/null +++ b/lapce-core/src/directory.rs @@ -0,0 +1,192 @@ +use std::path::PathBuf; + +use directories::{BaseDirs, ProjectDirs}; + +use crate::meta::NAME; + +pub struct Directory {} + +impl Directory { + pub fn home_dir() -> Option { + BaseDirs::new().map(|d| PathBuf::from(d.home_dir())) + } + + #[cfg(not(feature = "portable"))] + fn project_dirs() -> Option { + ProjectDirs::from("dev", "lapce", NAME) + } + + /// Return path adjacent to lapce executable when built as portable + #[cfg(feature = "portable")] + fn project_dirs() -> Option { + if let Ok(current_exe) = std::env::current_exe() { + if let Some(parent) = current_exe.parent() { + return ProjectDirs::from_path(parent.join("lapce-data")); + } + unreachable!("Couldn't obtain current process parent path"); + } + unreachable!("Couldn't obtain current process path"); + } + + // Get path of local data directory + // Local data directory differs from data directory + // on some platforms and is not transferred across + // machines + pub fn data_local_directory() -> Option { + match Self::project_dirs() { + Some(dir) => { + let dir = dir.data_local_dir(); + if !dir.exists() { + if let Err(err) = std::fs::create_dir_all(dir) { + tracing::error!("{:?}", err); + } + } + Some(dir.to_path_buf()) + } + None => None, + } + } + + /// Get the path to logs directory + /// Each log file is for individual application startup + pub fn logs_directory() -> Option { + if let Some(dir) = Self::data_local_directory() { + let dir = dir.join("logs"); + if !dir.exists() { + if let Err(err) = std::fs::create_dir(&dir) { + tracing::error!("{:?}", err); + } + } + Some(dir) + } else { + None + } + } + + /// Get the path to cache directory + pub fn cache_directory() -> Option { + if let Some(dir) = Self::data_local_directory() { + let dir = dir.join("cache"); + if !dir.exists() { + if let Err(err) = std::fs::create_dir(&dir) { + tracing::error!("{:?}", err); + } + } + Some(dir) + } else { + None + } + } + + /// Directory to store proxy executables used on local + /// host as well, as ones uploaded to remote host when + /// connecting + pub fn proxy_directory() -> Option { + if let Some(dir) = Self::data_local_directory() { + let dir = dir.join("proxy"); + if !dir.exists() { + if let Err(err) = std::fs::create_dir(&dir) { + tracing::error!("{:?}", err); + } + } + Some(dir) + } else { + None + } + } + /// Get the path to the themes folder + /// Themes are stored within as individual toml files + pub fn themes_directory() -> Option { + if let Some(dir) = Self::data_local_directory() { + let dir = dir.join("themes"); + if !dir.exists() { + if let Err(err) = std::fs::create_dir(&dir) { + tracing::error!("{:?}", err); + } + } + Some(dir) + } else { + None + } + } + // Get the path to plugins directory + // Each plugin has own directory that contains + // metadata file and plugin wasm + pub fn plugins_directory() -> Option { + if let Some(dir) = Self::data_local_directory() { + let dir = dir.join("plugins"); + if !dir.exists() { + if let Err(err) = std::fs::create_dir(&dir) { + tracing::error!("{:?}", err); + } + } + Some(dir) + } else { + None + } + } + + // Config directory contain only configuration files + pub fn config_directory() -> Option { + match Self::project_dirs() { + Some(dir) => { + let dir = dir.config_dir(); + if !dir.exists() { + if let Err(err) = std::fs::create_dir_all(dir) { + tracing::error!("{:?}", err); + } + } + Some(dir.to_path_buf()) + } + None => None, + } + } + + pub fn local_socket() -> Option { + Self::data_local_directory().map(|dir| dir.join("local.sock")) + } + + pub fn updates_directory() -> Option { + if let Some(dir) = Self::data_local_directory() { + let dir = dir.join("updates"); + if !dir.exists() { + if let Err(err) = std::fs::create_dir(&dir) { + tracing::error!("{:?}", err); + } + } + Some(dir) + } else { + None + } + } + + pub fn queries_directory() -> Option { + if let Some(dir) = Self::config_directory() { + let dir = dir.join("queries"); + if !dir.exists() { + if let Err(err) = std::fs::create_dir(&dir) { + tracing::error!("{:?}", err); + } + } + + Some(dir) + } else { + None + } + } + + pub fn grammars_directory() -> Option { + if let Some(dir) = Self::data_local_directory() { + let dir = dir.join("grammars"); + if !dir.exists() { + if let Err(err) = std::fs::create_dir(&dir) { + tracing::error!("{:?}", err); + } + } + + Some(dir) + } else { + None + } + } +} diff --git a/lapce-core/src/encoding.rs b/lapce-core/src/encoding.rs new file mode 100644 index 0000000..f67c107 --- /dev/null +++ b/lapce-core/src/encoding.rs @@ -0,0 +1,163 @@ +/// Convert a utf8 offset into a utf16 offset, if possible +/// `text` is what the offsets are into +pub fn offset_utf8_to_utf16( + char_indices: impl Iterator, + offset: usize, +) -> usize { + if offset == 0 { + return 0; + } + + let mut utf16_offset = 0; + let mut last_ich = None; + for (utf8_offset, ch) in char_indices { + last_ich = Some((utf8_offset, ch)); + + match utf8_offset.cmp(&offset) { + std::cmp::Ordering::Less => {} + // We found the right offset + std::cmp::Ordering::Equal => { + return utf16_offset; + } + // Implies that the offset was inside of a character + std::cmp::Ordering::Greater => return utf16_offset, + } + + utf16_offset += ch.len_utf16(); + } + + // TODO: We could use TrustedLen when that is stabilized and it is impl'd on + // the iterators we use + + // We did not find the offset. This means that it is either at the end + // or past the end. + let text_len = last_ich.map(|(i, c)| i + c.len_utf8()); + if text_len == Some(offset) { + // Since the utf16 offset was being incremented each time, by now it is equivalent to the length + // but in utf16 characters + return utf16_offset; + } + + utf16_offset +} + +pub fn offset_utf8_to_utf16_str(text: &str, offset: usize) -> usize { + offset_utf8_to_utf16(text.char_indices(), offset) +} + +/// Convert a utf16 offset into a utf8 offset, if possible +/// `char_indices` is an iterator over utf8 offsets and the characters +/// It is cloneable so that it can be iterated multiple times. Though it should be cheaply cloneable. +pub fn offset_utf16_to_utf8( + char_indices: impl Iterator, + offset: usize, +) -> usize { + if offset == 0 { + return 0; + } + + // We accumulate the utf16 char lens until we find the utf8 offset that matches it + // or, we find out that it went into the middle of sometext + // We also keep track of the last offset and char in order to calculate the length of the text + // if we the index was at the end of the string + let mut utf16_offset = 0; + let mut last_ich = None; + for (utf8_offset, ch) in char_indices { + last_ich = Some((utf8_offset, ch)); + + let ch_utf16_len = ch.len_utf16(); + + match utf16_offset.cmp(&offset) { + std::cmp::Ordering::Less => {} + // We found the right offset + std::cmp::Ordering::Equal => { + return utf8_offset; + } + // This implies that the offset was in the middle of a character as we skipped over it + std::cmp::Ordering::Greater => return utf8_offset, + } + + utf16_offset += ch_utf16_len; + } + + // We did not find the offset, this means that it was either at the end + // or past the end + // Since we've iterated over all the char indices, the utf16_offset is now the + // utf16 length + if let Some((last_utf8_offset, last_ch)) = last_ich { + last_utf8_offset + last_ch.len_utf8() + } else { + 0 + } +} + +pub fn offset_utf16_to_utf8_str(text: &str, offset: usize) -> usize { + offset_utf16_to_utf8(text.char_indices(), offset) +} + +#[cfg(test)] +mod tests { + // TODO: more tests with unicode characters + + use crate::encoding::{offset_utf8_to_utf16_str, offset_utf16_to_utf8_str}; + + #[test] + fn utf8_to_utf16() { + let text = "hello world"; + + assert_eq!(offset_utf8_to_utf16_str(text, 0), 0); + assert_eq!(offset_utf8_to_utf16_str("", 0), 0); + + assert_eq!(offset_utf8_to_utf16_str("", 1), 0); + + assert_eq!(offset_utf8_to_utf16_str("h", 0), 0); + assert_eq!(offset_utf8_to_utf16_str("h", 1), 1); + + assert_eq!(offset_utf8_to_utf16_str(text, text.len()), text.len()); + + assert_eq!( + offset_utf8_to_utf16_str(text, text.len() - 1), + text.len() - 1 + ); + + assert_eq!(offset_utf8_to_utf16_str(text, text.len() + 1), text.len()); + + assert_eq!(offset_utf8_to_utf16_str("×", 0), 0); + assert_eq!(offset_utf8_to_utf16_str("×", 1), 1); + assert_eq!(offset_utf8_to_utf16_str("×", 2), 1); + assert_eq!(offset_utf8_to_utf16_str("a×", 0), 0); + assert_eq!(offset_utf8_to_utf16_str("a×", 1), 1); + assert_eq!(offset_utf8_to_utf16_str("a×", 2), 2); + assert_eq!(offset_utf8_to_utf16_str("a×", 3), 2); + } + + #[test] + fn utf16_to_utf8() { + let text = "hello world"; + + assert_eq!(offset_utf16_to_utf8_str(text, 0), 0); + assert_eq!(offset_utf16_to_utf8_str("", 0), 0); + + assert_eq!(offset_utf16_to_utf8_str("", 1), 0); + + assert_eq!(offset_utf16_to_utf8_str("h", 0), 0); + assert_eq!(offset_utf16_to_utf8_str("h", 1), 1); + + assert_eq!(offset_utf16_to_utf8_str(text, text.len()), text.len()); + + assert_eq!( + offset_utf16_to_utf8_str(text, text.len() - 1), + text.len() - 1 + ); + + assert_eq!(offset_utf16_to_utf8_str(text, text.len() + 1), text.len()); + + assert_eq!(offset_utf16_to_utf8_str("×", 0), 0); + assert_eq!(offset_utf16_to_utf8_str("×", 1), 2); + assert_eq!(offset_utf16_to_utf8_str("a×", 0), 0); + assert_eq!(offset_utf16_to_utf8_str("a×", 1), 1); + assert_eq!(offset_utf16_to_utf8_str("a×", 2), 3); + assert_eq!(offset_utf16_to_utf8_str("×a", 1), 2); + assert_eq!(offset_utf16_to_utf8_str("×a", 2), 3); + } +} diff --git a/lapce-core/src/language.rs b/lapce-core/src/language.rs new file mode 100644 index 0000000..6d4356d --- /dev/null +++ b/lapce-core/src/language.rs @@ -0,0 +1,2075 @@ +use std::{ + collections::{HashMap, HashSet, hash_map::Entry}, + fmt::Write, + path::Path, + str::FromStr, +}; + +use lapce_rpc::style::{LineStyle, Style}; +use once_cell::sync::Lazy; +use regex::Regex; +use strum_macros::{AsRefStr, Display, EnumMessage, EnumString, IntoStaticStr}; +use tracing::{Level, event}; +use tree_sitter::{Point, TreeCursor}; + +use crate::{ + directory::Directory, + syntax::highlight::{HighlightConfiguration, HighlightIssue}, +}; + +#[remain::sorted] +pub enum Indent { + Space(u8), + Tab, +} + +impl Indent { + const fn tab() -> &'static str { + Indent::Tab.as_str() + } + + const fn space(count: u8) -> &'static str { + Indent::Space(count).as_str() + } + + const fn as_str(&self) -> &'static str { + match self { + Indent::Tab => "\u{0009}", + #[allow(clippy::wildcard_in_or_patterns)] + Indent::Space(v) => match v { + 2 => "\u{0020}\u{0020}", + 4 => "\u{0020}\u{0020}\u{0020}\u{0020}", + 8 | _ => { + "\u{0020}\u{0020}\u{0020}\u{0020}\u{0020}\u{0020}\u{0020}\u{0020}" + } + }, + } + } +} + +const DEFAULT_CODE_GLANCE_LIST: &[&str] = &["source_file"]; +const DEFAULT_CODE_GLANCE_IGNORE_LIST: &[&str] = &["source_file"]; + +#[macro_export] +macro_rules! comment_properties { + () => { + CommentProperties { + single_line_start: None, + single_line_end: None, + + multi_line_start: None, + multi_line_end: None, + multi_line_prefix: None, + } + }; + ($s:expr) => { + CommentProperties { + single_line_start: Some($s), + single_line_end: None, + + multi_line_start: None, + multi_line_end: None, + multi_line_prefix: None, + } + }; + ($s:expr, $e:expr) => { + CommentProperties { + single_line_start: Some($s), + single_line_end: Some($e), + + multi_line_start: None, + multi_line_end: None, + multi_line_prefix: None, + } + }; + ($sl_s:expr, $sl_e:expr, $ml_s:expr, $ml_e:expr) => { + CommentProperties { + single_line_start: Some($sl_s), + single_line_end: Some($sl_e), + + multi_line_start: Some($sl_s), + multi_line_end: None, + multi_line_prefix: Some($sl_e), + } + }; +} + +#[derive(Eq, PartialEq, Hash, Clone, Copy, Debug, PartialOrd, Ord, Default)] +pub struct SyntaxProperties { + /// An extra check to make sure that the array elements are in the correct order. + /// If this id does not match the enum value, a panic will happen with a debug assertion message. + id: LapceLanguage, + + /// All tokens that can be used for comments in language + comment: CommentProperties, + /// The indent unit. + /// " " for bash, " " for rust, for example. + indent: &'static str, + /// Filenames that belong to this language + /// `["Dockerfile"]` for Dockerfile, `[".editorconfig"]` for EditorConfig + files: &'static [&'static str], + /// File name extensions to determine the language. + /// `["py"]` for python, `["rs"]` for rust, for example. + extensions: &'static [&'static str], + /// Tree-sitter properties + tree_sitter: TreeSitterProperties, +} + +#[derive(Eq, PartialEq, Hash, Clone, Copy, Debug, PartialOrd, Ord, Default)] +struct TreeSitterProperties { + /// the grammar name that's in the grammars folder + grammar: Option<&'static str>, + /// the grammar fn name + grammar_fn: Option<&'static str>, + /// the query folder name + query: Option<&'static str>, + /// Preface: Originally this feature was called "Code Lens", which is not + /// an LSP "Code Lens". It is renamed to "Code Glance", below doc text is + /// left unchanged. + /// + /// Lists of tree-sitter node types that control how code lenses are built. + /// The first is a list of nodes that should be traversed and included in + /// the lens, along with their children. The second is a list of nodes that + /// should be excluded from the lens, though they will still be traversed. + /// See `walk_tree` for more details. + /// + /// The tree-sitter playground may be useful when creating these lists: + /// https://tree-sitter.github.io/tree-sitter/playground + /// + /// If unsure, use `DEFAULT_CODE_GLANCE_LIST` and + /// `DEFAULT_CODE_GLANCE_IGNORE_LIST`. + code_glance: (&'static [&'static str], &'static [&'static str]), + /// the tree-sitter tag names that can be put in sticky headers + sticky_headers: &'static [&'static str], +} + +impl TreeSitterProperties { + const DEFAULT: Self = Self { + grammar: None, + grammar_fn: None, + query: None, + code_glance: (DEFAULT_CODE_GLANCE_LIST, DEFAULT_CODE_GLANCE_IGNORE_LIST), + sticky_headers: &[], + }; +} + +#[derive(Eq, PartialEq, Hash, Clone, Copy, Debug, PartialOrd, Ord, Default)] +struct CommentProperties { + /// Single line comment token used when commenting out one line. + /// "#" for python, "//" for rust for example. + single_line_start: Option<&'static str>, + single_line_end: Option<&'static str>, + + /// Multi line comment token used when commenting a selection of lines. + /// "#" for python, "//" for rust for example. + multi_line_start: Option<&'static str>, + multi_line_end: Option<&'static str>, + multi_line_prefix: Option<&'static str>, +} + +/// NOTE: Keep the enum variants "fieldless" so they can cast to usize as array +/// indices into the LANGUAGES array. See method `LapceLanguage::properties`. +/// +/// Do not assign values to the variants because the number of variants and +/// number of elements in the LANGUAGES array change as different features +/// selected by the cargo build command. +#[derive( + Eq, + PartialEq, + Ord, + PartialOrd, + Hash, + Clone, + Copy, + Debug, + Display, + AsRefStr, + IntoStaticStr, + EnumString, + EnumMessage, + Default, +)] +#[strum(ascii_case_insensitive)] +#[remain::sorted] +pub enum LapceLanguage { + // Do not move + #[remain::unsorted] + #[default] + #[strum(message = "Plain Text")] + PlainText, + + #[strum(message = "Ada")] + Ada, + #[strum(message = "Adl")] + Adl, + #[strum(message = "Agda")] + Agda, + #[strum(message = "Astro")] + Astro, + #[strum(message = "Bash")] + Bash, + #[strum(message = "Bass")] + Bass, + #[strum(message = "Beancount")] + Beancount, + #[strum(message = "Bibtex")] + Bibtex, + #[strum(message = "Bitbake")] + Bitbake, + #[strum(message = "Blade")] + Blade, + #[strum(message = "C")] + C, + #[strum(message = "Clojure")] + Clojure, + #[strum(message = "CMake")] + Cmake, + #[strum(message = "Comment")] + Comment, + #[strum(message = "C++")] + Cpp, + #[strum(message = "C#")] + Csharp, + #[strum(message = "CSS")] + Css, + #[strum(message = "Cue")] + Cue, + #[strum(message = "D")] + D, + #[strum(message = "Dart")] + Dart, + #[strum(message = "Dhall")] + Dhall, + #[strum(message = "Diff")] + Diff, + #[strum(message = "Dockerfile")] + Dockerfile, + #[strum(message = "Dot")] + Dot, + #[strum(message = "Elixir")] + Elixir, + #[strum(message = "Elm")] + Elm, + #[strum(message = "Erlang")] + Erlang, + #[strum(message = "Fish Shell")] + Fish, + #[strum(message = "Fluent")] + Fluent, + #[strum(message = "Forth")] + Forth, + #[strum(message = "Fortran")] + Fortran, + #[strum(message = "F#")] + FSharp, + #[strum(message = "Gitattributes")] + Gitattributes, + #[strum(message = "Git (commit)")] + GitCommit, + #[strum(message = "Git (config)")] + GitConfig, + #[strum(message = "Git (rebase)")] + GitRebase, + #[strum(message = "Gleam")] + Gleam, + #[strum(message = "Glimmer")] + Glimmer, + #[strum(message = "GLSL")] + Glsl, + #[strum(message = "Gn")] + Gn, + #[strum(message = "Go")] + Go, + #[strum(message = "Go (go.mod)")] + GoMod, + #[strum(message = "Go (template)")] + GoTemplate, + #[strum(message = "Go (go.work)")] + GoWork, + #[strum(message = "GraphQL")] + GraphQl, + #[strum(message = "Groovy")] + Groovy, + #[strum(message = "Hare")] + Hare, + #[strum(message = "Haskell")] + Haskell, + #[strum(message = "Haxe")] + Haxe, + #[strum(message = "HCL")] + Hcl, + #[strum(message = "Hosts file (/etc/hosts)")] + Hosts, + #[strum(message = "HTML")] + Html, + #[strum(message = "INI")] + Ini, + #[strum(message = "Java")] + Java, + #[strum(message = "JavaScript")] + Javascript, + #[strum(message = "JSDoc")] + Jsdoc, + #[strum(message = "JSON")] + Json, + #[strum(message = "JSON5")] + Json5, + #[strum(message = "Jsonnet")] + Jsonnet, + #[strum(message = "JavaScript React")] + Jsx, + #[strum(message = "Julia")] + Julia, + #[strum(message = "Just")] + Just, + #[strum(message = "KDL")] + Kdl, + #[strum(message = "Kotlin")] + Kotlin, + #[strum(message = "Kotlin Build Script")] + KotlinBuildScript, + #[strum(message = "LaTeX")] + Latex, + #[strum(message = "Linker Script")] + Ld, + #[strum(message = "LLVM")] + Llvm, + #[strum(message = "LLVM MIR")] + LlvmMir, + #[strum(message = "Log")] + Log, + #[strum(message = "Lua")] + Lua, + #[strum(message = "Makefile")] + Make, + #[strum(message = "Markdown")] + Markdown, + #[strum(serialize = "markdown.inline")] + MarkdownInline, + #[strum(message = "Meson")] + Meson, + #[strum(message = "NASM")] + Nasm, + #[strum(message = "Nix")] + Nix, + #[strum(message = "Nu (nushell)")] + Nushell, + #[strum(message = "Ocaml")] + Ocaml, + #[strum(serialize = "ocaml.interface")] + OcamlInterface, + #[strum(message = "Odin")] + Odin, + #[strum(message = "OpenCL")] + OpenCl, + #[strum(message = "Pascal")] + Pascal, + #[strum(message = "Password file (/etc/passwd)")] + Passwd, + #[strum(message = "PEM (RFC 1422)")] + Pem, + #[strum(message = "PHP")] + Php, + #[strum(message = "PKL")] + Pkl, + #[strum(message = "PowerShell")] + PowerShell, + #[strum(message = "Prisma")] + Prisma, + #[strum(message = "Proto")] + ProtoBuf, + #[strum(message = "Python")] + Python, + #[strum(message = "QL")] + Ql, + #[strum(message = "R")] + R, + #[strum(message = "RCL")] + Rcl, + #[strum(message = "RegEx")] + Regex, + #[strum(message = "REGO")] + Rego, + #[strum(message = "RON (Rust Object Notation)")] + Ron, + #[strum(message = "Rst")] + Rst, + #[strum(message = "Ruby")] + Ruby, + #[strum(message = "Rust")] + Rust, + #[strum(message = "Scala")] + Scala, + #[strum(message = "Scheme")] + Scheme, + #[strum(message = "SCSS")] + Scss, + #[strum(message = "Shell Script (POSIX)")] + ShellScript, + #[strum(message = "Smithy")] + Smithy, + #[strum(message = "SQL")] + Sql, + #[strum(message = "SSH Config")] + SshClientConfig, + #[strum(message = "Strace")] + Strace, + #[strum(message = "Svelte")] + Svelte, + #[strum(message = "Sway")] + Sway, + #[strum(message = "Swift")] + Swift, + #[strum(message = "TCL")] + Tcl, + #[strum(message = "TOML")] + Toml, + #[strum(message = "Tsx")] + Tsx, + #[strum(message = "TypeScript")] + Typescript, + #[strum(message = "Typst")] + Typst, + #[strum(message = "Verilog")] + Verilog, + #[strum(message = "Vue")] + Vue, + #[strum(message = "WASM")] + Wasm, + #[strum(message = "WGSL")] + Wgsl, + #[strum(message = "WIT")] + Wit, + #[strum(message = "XML")] + Xml, + #[strum(message = "YAML")] + Yaml, + #[strum(message = "Zig")] + Zig, +} + +/// NOTE: Elements in the array must be in the same order as the enum variants of +/// `LapceLanguage` as they will be accessed using the enum variants as indices. +const LANGUAGES: &[SyntaxProperties] = &[ + // Undetected/unmatched fallback or just plain file + SyntaxProperties { + id: LapceLanguage::PlainText, + indent: Indent::tab(), + files: &[], + extensions: &["txt"], + comment: comment_properties!(), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + // Languages + SyntaxProperties { + id: LapceLanguage::Ada, + indent: Indent::tab(), + files: &[], + extensions: &[], + comment: comment_properties!(), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Adl, + indent: Indent::tab(), + files: &[], + extensions: &[], + comment: comment_properties!(), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Agda, + indent: Indent::tab(), + files: &[], + extensions: &[], + comment: comment_properties!(), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Astro, + indent: Indent::tab(), + files: &[], + extensions: &[], + comment: comment_properties!(), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Bash, + indent: Indent::space(2), + files: &[], + extensions: &["bash", "sh"], + comment: comment_properties!("#"), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Bass, + indent: Indent::tab(), + files: &[], + extensions: &[], + comment: comment_properties!(), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Beancount, + indent: Indent::tab(), + files: &[], + extensions: &[], + comment: comment_properties!(), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Bibtex, + indent: Indent::tab(), + files: &[], + extensions: &[], + comment: comment_properties!(), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Bitbake, + indent: Indent::tab(), + files: &[], + extensions: &[], + comment: comment_properties!(), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Blade, + indent: Indent::tab(), + files: &[], + extensions: &[], + comment: comment_properties!(), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::C, + indent: Indent::space(4), + files: &[], + extensions: &["c", "h"], + comment: comment_properties!("//"), + tree_sitter: TreeSitterProperties { + grammar: None, + grammar_fn: None, + query: None, + code_glance: (DEFAULT_CODE_GLANCE_LIST, DEFAULT_CODE_GLANCE_IGNORE_LIST), + sticky_headers: &["function_definition", "struct_specifier"], + }, + }, + SyntaxProperties { + id: LapceLanguage::Clojure, + indent: Indent::space(2), + files: &[], + extensions: &[ + "clj", + "edn", // spellchecker:disable-line + "cljs", + "cljc", + "cljd", + "bb", + "clj_kondo", + ], + comment: comment_properties!(";"), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Cmake, + indent: Indent::space(2), + files: &["cmakelists"], + extensions: &["cmake"], + comment: comment_properties!("#"), + tree_sitter: TreeSitterProperties { + grammar: None, + grammar_fn: None, + query: None, + code_glance: (DEFAULT_CODE_GLANCE_LIST, DEFAULT_CODE_GLANCE_IGNORE_LIST), + sticky_headers: &["function_definition"], + }, + }, + SyntaxProperties { + id: LapceLanguage::Comment, + indent: Indent::tab(), + files: &[], + extensions: &[], + comment: comment_properties!(), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Cpp, + indent: Indent::space(4), + files: &[], + extensions: &["cpp", "cxx", "cc", "c++", "hpp", "hxx", "hh", "h++"], + comment: comment_properties!("//"), + tree_sitter: TreeSitterProperties { + grammar: None, + grammar_fn: None, + query: None, + code_glance: (DEFAULT_CODE_GLANCE_LIST, DEFAULT_CODE_GLANCE_IGNORE_LIST), + sticky_headers: &[ + "function_definition", + "class_specifier", + "struct_specifier", + ], + }, + }, + SyntaxProperties { + id: LapceLanguage::Csharp, + indent: Indent::space(2), + files: &[], + extensions: &["cs", "csx"], + comment: comment_properties!("#"), + tree_sitter: TreeSitterProperties { + grammar: None, + grammar_fn: None, + query: None, + code_glance: (DEFAULT_CODE_GLANCE_LIST, DEFAULT_CODE_GLANCE_IGNORE_LIST), + sticky_headers: &[ + "interface_declaration", + "class_declaration", + "enum_declaration", + "struct_declaration", + "record_declaration", + "record_struct_declaration", + "namespace_declaration", + "constructor_declaration", + "destructor_declaration", + "method_declaration", + ], + }, + }, + SyntaxProperties { + id: LapceLanguage::Css, + indent: Indent::space(2), + files: &[], + extensions: &["css"], + comment: comment_properties!("/*", "*/"), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Cue, + indent: Indent::tab(), + files: &[], + extensions: &[], + comment: comment_properties!(), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::D, + indent: Indent::space(4), + files: &[], + extensions: &["d", "di", "dlang"], + comment: CommentProperties { + single_line_start: Some("//"), + single_line_end: None, + multi_line_start: Some("/+"), + multi_line_prefix: None, + multi_line_end: Some("+/"), + }, + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Dart, + indent: Indent::space(2), + files: &[], + extensions: &["dart"], + comment: CommentProperties { + single_line_start: Some("//"), + single_line_end: None, + + multi_line_start: Some("/*"), + multi_line_prefix: None, + multi_line_end: Some("*/"), + }, + tree_sitter: TreeSitterProperties { + grammar: None, + grammar_fn: None, + query: None, + code_glance: ( + &["program", "class_definition"], + &[ + "program", + "import_or_export", + "comment", + "documentation_comment", + ], + ), + sticky_headers: &["class_definition"], + }, + }, + SyntaxProperties { + id: LapceLanguage::Dhall, + indent: Indent::tab(), + files: &[], + extensions: &[], + comment: comment_properties!(), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Diff, + indent: Indent::tab(), + files: &[], + extensions: &["diff", "patch"], + comment: comment_properties!(), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Dockerfile, + indent: Indent::space(2), + files: &["Dockerfile", "Containerfile"], + extensions: &["containerfile", "dockerfile"], + comment: comment_properties!("#"), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Dot, + indent: Indent::tab(), + files: &[], + extensions: &[], + comment: comment_properties!(), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Elixir, + indent: Indent::space(2), + files: &[], + extensions: &["ex", "exs", "eex", "heex", "sface"], + comment: comment_properties!("#"), + tree_sitter: TreeSitterProperties { + grammar: None, + grammar_fn: None, + query: None, + code_glance: (DEFAULT_CODE_GLANCE_LIST, DEFAULT_CODE_GLANCE_IGNORE_LIST), + sticky_headers: &["do_block"], + }, + }, + SyntaxProperties { + id: LapceLanguage::Elm, + indent: Indent::space(4), + files: &[], + extensions: &["elm"], + comment: comment_properties!("#"), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Erlang, + indent: Indent::space(4), + files: &[], + extensions: &["erl", "hrl"], + comment: comment_properties!("%"), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::FSharp, + indent: Indent::tab(), + files: &[], + extensions: &[], + comment: comment_properties!(), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Fish, + indent: Indent::tab(), + files: &[], + extensions: &["fish"], + comment: comment_properties!(), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Fluent, + indent: Indent::tab(), + files: &[], + extensions: &[], + comment: comment_properties!(), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Forth, + indent: Indent::tab(), + files: &[], + extensions: &[], + comment: comment_properties!(), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Fortran, + indent: Indent::tab(), + files: &[], + extensions: &[], + comment: comment_properties!(), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Gitattributes, + indent: Indent::tab(), + files: &[], + extensions: &[], + comment: comment_properties!(), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::GitCommit, + indent: Indent::tab(), + files: &[], + extensions: &[], + comment: comment_properties!(), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::GitConfig, + indent: Indent::tab(), + files: &[".gitconfig", ".git/config"], + extensions: &[], + comment: comment_properties!(), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::GitRebase, + indent: Indent::tab(), + files: &[], + extensions: &[], + comment: comment_properties!(), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Gleam, + indent: Indent::tab(), + files: &[], + extensions: &[], + comment: comment_properties!(), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Glimmer, + indent: Indent::space(2), + files: &[], + extensions: &["hbs"], + comment: comment_properties!("{{!", "!}}"), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Glsl, + indent: Indent::space(2), + files: &[], + // spellchecker:off + extensions: &[ + "glsl", "cs", "vs", "gs", "fs", "csh", "vsh", "gsh", "fsh", "cshader", + "vshader", "gshader", "fshader", "comp", "vert", "geom", "frag", "tesc", + "tese", "mesh", "task", "rgen", "rint", "rahit", "rchit", "rmiss", + "rcall", + ], + // spellchecker:on + comment: comment_properties!("//"), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Gn, + indent: Indent::tab(), + files: &[], + extensions: &[], + comment: comment_properties!(), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Go, + indent: Indent::tab(), + files: &[], + extensions: &["go"], + comment: comment_properties!("//"), + tree_sitter: TreeSitterProperties { + grammar: None, + grammar_fn: None, + query: None, + code_glance: ( + &[ + "source_file", + "type_declaration", + "type_spec", + "interface_type", + "method_spec_list", + ], + &["source_file", "comment", "line_comment"], + ), + sticky_headers: &[], + }, + }, + SyntaxProperties { + id: LapceLanguage::GoMod, + indent: Indent::tab(), + files: &[], + extensions: &[], + comment: comment_properties!(), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::GoTemplate, + indent: Indent::tab(), + files: &[], + extensions: &[], + comment: comment_properties!(), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::GoWork, + indent: Indent::tab(), + files: &[], + extensions: &[], + comment: comment_properties!(), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::GraphQl, + indent: Indent::tab(), + files: &[], + extensions: &[], + comment: comment_properties!(), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Groovy, + indent: Indent::tab(), + files: &[], + extensions: &[], + comment: comment_properties!(), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Hare, + indent: Indent::space(8), + files: &[], + extensions: &["ha"], + comment: comment_properties!("//"), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Haskell, + indent: Indent::space(2), + files: &[], + extensions: &["hs"], + comment: comment_properties!("--"), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Haxe, + indent: Indent::space(2), + files: &[], + extensions: &["hx"], + comment: comment_properties!("//"), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Hcl, + indent: Indent::space(2), + files: &[], + extensions: &["hcl", "tf"], + comment: comment_properties!("//"), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Hosts, + indent: Indent::tab(), + files: &[], + extensions: &[], + comment: comment_properties!(), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Html, + indent: Indent::space(4), + files: &[], + extensions: &["html", "htm"], + comment: comment_properties!(""), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Ini, + indent: Indent::tab(), + files: &[], + extensions: &[], + comment: comment_properties!(), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Java, + indent: Indent::space(4), + files: &[], + extensions: &["java"], + comment: comment_properties!("//"), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Javascript, + indent: Indent::space(2), + files: &[], + extensions: &["js", "cjs", "mjs"], + comment: comment_properties!("//"), + tree_sitter: TreeSitterProperties { + grammar: None, + grammar_fn: None, + query: None, + code_glance: (&["source_file", "program"], &["source_file"]), + sticky_headers: &[], + }, + }, + SyntaxProperties { + id: LapceLanguage::Jsdoc, + indent: Indent::tab(), + files: &[], + extensions: &[], + comment: comment_properties!(), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Json, + indent: Indent::space(4), + files: &[], + extensions: &["json", "har"], + comment: comment_properties!(), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Json5, + indent: Indent::tab(), + files: &[], + extensions: &[], + comment: comment_properties!(), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Jsonnet, + indent: Indent::tab(), + files: &[], + extensions: &[], + comment: comment_properties!(), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Jsx, + indent: Indent::space(2), + files: &[], + extensions: &["jsx"], + comment: comment_properties!("//"), + tree_sitter: TreeSitterProperties { + grammar: Some("javascript"), + grammar_fn: Some("javascript"), + query: Some("jsx"), + code_glance: (&["source_file", "program"], &["source_file"]), + sticky_headers: &[], + }, + }, + SyntaxProperties { + id: LapceLanguage::Julia, + indent: Indent::space(4), + files: &[], + extensions: &["julia", "jl"], + comment: CommentProperties { + single_line_start: Some("#"), + single_line_end: None, + multi_line_start: Some("#="), + multi_line_prefix: None, + multi_line_end: Some("=#"), + }, + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Just, + indent: Indent::tab(), + files: &["justfile", "Justfile", ".justfile", ".Justfile"], + extensions: &["just"], + comment: comment_properties!(), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Kdl, + indent: Indent::tab(), + files: &[], + extensions: &[], + comment: comment_properties!(), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Kotlin, + indent: Indent::space(2), + files: &[], + extensions: &["kt"], + comment: CommentProperties { + single_line_start: Some("//"), + single_line_end: None, + + multi_line_start: Some("/*"), + multi_line_prefix: None, + multi_line_end: Some("*/"), + }, + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::KotlinBuildScript, + indent: Indent::space(2), + files: &[], + extensions: &["kts"], + comment: CommentProperties { + single_line_start: Some("//"), + single_line_end: None, + + multi_line_start: Some("/*"), + multi_line_prefix: None, + multi_line_end: Some("*/"), + }, + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Latex, + indent: Indent::space(2), + files: &[], + extensions: &["tex"], + comment: comment_properties!("%"), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Ld, + indent: Indent::tab(), + files: &[], + extensions: &[], + comment: comment_properties!(), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Llvm, + indent: Indent::tab(), + files: &[], + extensions: &[], + comment: comment_properties!(), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::LlvmMir, + indent: Indent::tab(), + files: &[], + extensions: &[], + comment: comment_properties!(), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Log, + indent: Indent::tab(), + files: &["log.txt"], + extensions: &["log"], + comment: comment_properties!(), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Lua, + indent: Indent::space(2), + files: &[], + extensions: &["lua"], + comment: comment_properties!("--"), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Make, + indent: Indent::tab(), + files: &[], + extensions: &[], + comment: comment_properties!(), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Markdown, + indent: Indent::space(4), + files: &[], + extensions: &["md"], + comment: comment_properties!(), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::MarkdownInline, + indent: Indent::space(4), + // markdown inline is only used as an injection by the Markdown language + files: &[], + extensions: &[], + comment: comment_properties!(), + tree_sitter: TreeSitterProperties { + grammar: Some("markdown_inline"), + grammar_fn: Some("markdown_inline"), + query: Some("markdown.inline"), + code_glance: (DEFAULT_CODE_GLANCE_LIST, DEFAULT_CODE_GLANCE_IGNORE_LIST), + sticky_headers: &[], + }, + }, + SyntaxProperties { + id: LapceLanguage::Meson, + indent: Indent::tab(), + files: &[], + extensions: &[], + comment: comment_properties!(), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Nasm, + indent: Indent::tab(), + files: &[], + extensions: &[], + comment: comment_properties!(), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Nix, + indent: Indent::space(2), + files: &[], + extensions: &["nix"], + comment: CommentProperties { + single_line_start: Some("#"), + single_line_end: None, + + multi_line_start: Some("/*"), + multi_line_prefix: None, + multi_line_end: Some("*/"), + }, + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Nushell, + indent: Indent::tab(), + files: &[], + extensions: &[], + comment: comment_properties!(), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Ocaml, + indent: Indent::space(2), + files: &[], + extensions: &["ml"], + comment: CommentProperties { + single_line_start: Some("(*"), + single_line_end: Some("*)"), + + multi_line_start: Some("(*"), + multi_line_prefix: Some("*"), + multi_line_end: Some("*)"), + }, + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::OcamlInterface, + indent: Indent::space(2), + files: &[], + extensions: &["mli"], + comment: comment_properties!("(*"), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Odin, + indent: Indent::tab(), + files: &[], + extensions: &[], + comment: comment_properties!(), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::OpenCl, + indent: Indent::tab(), + files: &[], + extensions: &[], + comment: comment_properties!(), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Pascal, + indent: Indent::tab(), + files: &[], + extensions: &[], + comment: comment_properties!(), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Passwd, + indent: Indent::tab(), + files: &[], + extensions: &[], + comment: comment_properties!(), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Pem, + indent: Indent::tab(), + files: &[], + extensions: &[], + comment: comment_properties!(), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Php, + indent: Indent::space(2), + files: &[], + extensions: &["php"], + comment: comment_properties!("//"), + tree_sitter: TreeSitterProperties { + grammar: None, + grammar_fn: None, + query: None, + code_glance: ( + &[ + "program", + "class_declaration", + "trait_declaration", + "interface_declaration", + "declaration_list", + "method_declaration", + "function_declaration", + ], + &[ + "program", + "php_tag", + "comment", + "namespace_definition", + "namespace_use_declaration", + "use_declaration", + "const_declaration", + "property_declaration", + "expression_statement", + ], + ), + sticky_headers: &[], + }, + }, + SyntaxProperties { + id: LapceLanguage::Pkl, + indent: Indent::tab(), + files: &[], + extensions: &[], + comment: comment_properties!(), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::PowerShell, + indent: Indent::space(4), + files: &[], + extensions: &["ps1", "psm1", "psd1", "ps1xml"], + comment: CommentProperties { + single_line_start: Some("#"), + single_line_end: None, + multi_line_start: Some("<#"), + multi_line_end: Some("#>"), + multi_line_prefix: None, + }, + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Prisma, + indent: Indent::space(4), + files: &[], + extensions: &["prisma"], + comment: comment_properties!("//"), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::ProtoBuf, + indent: Indent::space(2), + files: &[], + extensions: &["proto"], + comment: comment_properties!("//"), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Python, + indent: Indent::space(4), + files: &[], + extensions: &["py", "pyi", "pyc", "pyd", "pyw"], + comment: comment_properties!("#"), + tree_sitter: TreeSitterProperties { + grammar: None, + grammar_fn: None, + query: None, + code_glance: ( + &[ + "source_file", + "module", + "class_definition", + "class", + "identifier", + "decorated_definition", + "block", + ], + &["source_file", "import_statement", "import_from_statement"], + ), + sticky_headers: &[], + }, + }, + SyntaxProperties { + id: LapceLanguage::Ql, + indent: Indent::space(2), + files: &[], + extensions: &["ql"], + comment: comment_properties!("//"), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::R, + indent: Indent::space(2), + files: &[], + extensions: &["r"], + comment: comment_properties!("#"), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Rcl, + indent: Indent::tab(), + files: &[], + extensions: &[], + comment: comment_properties!(), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Regex, + indent: Indent::tab(), + files: &[], + extensions: &[], + comment: comment_properties!(), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Rego, + indent: Indent::tab(), + files: &[], + extensions: &[], + comment: comment_properties!(), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Ron, + indent: Indent::tab(), + files: &[], + extensions: &[], + comment: comment_properties!(), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Rst, + indent: Indent::tab(), + files: &[], + extensions: &[], + comment: comment_properties!(), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Ruby, + indent: Indent::space(2), + files: &[], + extensions: &["rb"], + comment: comment_properties!("#"), + tree_sitter: TreeSitterProperties { + grammar: None, + grammar_fn: None, + query: None, + code_glance: (DEFAULT_CODE_GLANCE_LIST, DEFAULT_CODE_GLANCE_IGNORE_LIST), + sticky_headers: &["module", "class", "method", "do_block"], + }, + }, + SyntaxProperties { + id: LapceLanguage::Rust, + indent: Indent::space(4), + files: &[], + extensions: &["rs"], + comment: comment_properties!("//"), + tree_sitter: TreeSitterProperties { + grammar: None, + grammar_fn: None, + query: None, + code_glance: ( + &["source_file", "impl_item", "trait_item", "declaration_list"], + &["source_file", "use_declaration", "line_comment"], + ), + sticky_headers: &[ + "struct_item", + "enum_item", + "function_item", + "impl_item", + ], + }, + }, + SyntaxProperties { + id: LapceLanguage::Scala, + indent: Indent::tab(), + files: &[], + extensions: &[], + comment: comment_properties!(), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Scheme, + indent: Indent::space(2), + files: &[], + extensions: &["scm", "ss"], + comment: comment_properties!(";"), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Scss, + indent: Indent::space(2), + files: &[], + extensions: &["scss"], + comment: comment_properties!("//"), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::ShellScript, + indent: Indent::space(2), + files: &[], + extensions: &["sh"], + comment: comment_properties!("#"), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Smithy, + indent: Indent::tab(), + files: &[], + extensions: &[], + comment: comment_properties!(), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Sql, + indent: Indent::space(2), + files: &[], + extensions: &["sql"], + comment: comment_properties!("--"), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::SshClientConfig, + indent: Indent::tab(), + files: &[], + extensions: &[], + comment: comment_properties!(), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Strace, + indent: Indent::tab(), + files: &[], + extensions: &[], + comment: comment_properties!(), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Svelte, + indent: Indent::space(2), + files: &[], + extensions: &["svelte"], + comment: comment_properties!("//"), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Sway, + indent: Indent::tab(), + files: &[], + extensions: &[], + comment: comment_properties!(), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Swift, + indent: Indent::space(2), + files: &[], + extensions: &["swift"], + comment: comment_properties!("//"), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Tcl, + indent: Indent::tab(), + files: &[], + extensions: &["tcl"], + comment: comment_properties!(), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Toml, + indent: Indent::space(2), + files: &["Cargo.lock"], + extensions: &["toml"], + comment: comment_properties!("#"), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Tsx, + indent: Indent::space(4), + files: &[], + extensions: &["tsx"], + comment: comment_properties!("//"), + tree_sitter: TreeSitterProperties { + grammar: Some("tsx"), + grammar_fn: Some("tsx"), + query: Some("tsx"), + code_glance: (&["source_file", "program"], &["source_file"]), + sticky_headers: &[], + }, + }, + SyntaxProperties { + id: LapceLanguage::Typescript, + indent: Indent::space(4), + files: &[], + extensions: &["ts", "cts", "mts"], + comment: comment_properties!("//"), + tree_sitter: TreeSitterProperties { + grammar: Some("typescript"), + grammar_fn: Some("typescript"), + query: Some("typescript"), + code_glance: (&["source_file", "program"], &["source_file"]), + sticky_headers: &[], + }, + }, + SyntaxProperties { + id: LapceLanguage::Typst, + indent: Indent::tab(), + files: &[], + extensions: &[], + comment: comment_properties!(), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Verilog, + indent: Indent::tab(), + files: &[], + extensions: &[], + comment: comment_properties!(), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Vue, + indent: Indent::space(2), + files: &[], + extensions: &["vue"], + comment: comment_properties!("//"), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Wasm, + indent: Indent::space(4), + files: &[], + extensions: &["wasm"], + comment: comment_properties!(), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Wgsl, + indent: Indent::space(4), + files: &[], + extensions: &["wgsl"], + comment: comment_properties!("//"), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Wit, + indent: Indent::space(4), + files: &[], + extensions: &["wit"], + comment: comment_properties!(), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Xml, + indent: Indent::space(4), + files: &[], + extensions: &["xml", "csproj"], + comment: comment_properties!("//"), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Yaml, + indent: Indent::space(2), + files: &[], + extensions: &["yml", "yaml"], + comment: comment_properties!("#"), + tree_sitter: TreeSitterProperties::DEFAULT, + }, + SyntaxProperties { + id: LapceLanguage::Zig, + indent: Indent::space(4), + files: &[], + extensions: &["zig"], + comment: comment_properties!("//"), + tree_sitter: TreeSitterProperties::DEFAULT, + }, +]; + +impl LapceLanguage { + const HIGHLIGHTS_INJECTIONS_FILE_NAME: &'static str = "injections.scm"; + const HIGHLIGHTS_QUERIES_FILE_NAME: &'static str = "highlights.scm"; + + pub fn from_path(path: &Path) -> LapceLanguage { + Self::from_path_raw(path).unwrap_or(LapceLanguage::PlainText) + } + + pub fn from_path_raw(path: &Path) -> Option { + let filename = path.file_name().and_then(|s| s.to_str()); + let extension = path + .extension() + .and_then(|s| s.to_str().map(|s| s.to_lowercase())); + // NOTE: This is a linear search. It is assumed that this function + // isn't called in any tight loop. + for properties in LANGUAGES { + if properties.files.iter().any(|f| Some(*f) == filename) { + return Some(properties.id); + } + if properties + .extensions + .iter() + .any(|e| Some(*e) == extension.as_deref()) + { + return Some(properties.id); + } + } + + None + } + + pub fn from_name(name: &str) -> Option { + match LapceLanguage::from_str(name.to_lowercase().as_str()) { + Ok(v) => Some(v), + Err(e) => { + event!(Level::DEBUG, "failed parsing `{name}` LapceLanguage: {e}"); + None + } + } + } + + pub fn languages() -> Vec<&'static str> { + let mut langs = vec![]; + for l in LANGUAGES { + // Get only languages with display name to hide inline grammars + if let Some(lang) = strum::EnumMessage::get_message(&l.id) { + langs.push(lang) + } + } + langs + } + + // NOTE: Instead of using `&LANGUAGES[*self as usize]` directly, the + // `debug_assertion` gives better feedback should something has gone wrong + // badly. + fn properties(&self) -> &SyntaxProperties { + let i = *self as usize; + let l = &LANGUAGES[i]; + debug_assert!( + l.id == *self, + "LANGUAGES[{i}]: Setting::id mismatch: {:?} != {:?}", + l.id, + self + ); + l + } + + pub fn name(&self) -> &'static str { + strum::EnumMessage::get_message(self).unwrap_or(self.into()) + } + + pub fn sticky_header_tags(&self) -> &[&'static str] { + self.properties().tree_sitter.sticky_headers + } + + pub fn comment_token(&self) -> &'static str { + self.properties() + .comment + .single_line_start + .unwrap_or_default() + } + + pub fn indent_unit(&self) -> &str { + self.properties().indent + } + + fn get_grammar(&self) -> Option { + let grammar_name = self.grammar_name(); + let grammar_fn_name = self.grammar_fn_name(); + + if let Some(grammars_dir) = Directory::grammars_directory() { + match self::load_grammar(&grammar_name, &grammar_fn_name, &grammars_dir) + { + Ok(grammar) => { + return Some(grammar); + } + Err(err) => { + if self != &LapceLanguage::PlainText { + tracing::error!("{:?} {:?}", self, err); + } + } + } + }; + + None + } + + fn query_name(&self) -> String { + self.properties() + .tree_sitter + .query + .unwrap_or(self.properties().id.as_ref()) + .to_lowercase() + } + + fn grammar_name(&self) -> String { + self.properties() + .tree_sitter + .grammar + .unwrap_or(self.properties().id.as_ref()) + .to_lowercase() + } + + fn grammar_fn_name(&self) -> String { + self.properties() + .tree_sitter + .grammar_fn + .unwrap_or(self.properties().id.as_ref()) + .to_lowercase() + } + + fn get_grammar_query(&self) -> (String, String) { + let query_name = self.query_name(); + + // Try reading highlights from user config dir + if let Some(queries_dir) = Directory::queries_directory() { + return ( + read_grammar_query( + &queries_dir, + &query_name, + Self::HIGHLIGHTS_QUERIES_FILE_NAME, + ), + read_grammar_query( + &queries_dir, + &query_name, + Self::HIGHLIGHTS_INJECTIONS_FILE_NAME, + ), + ); + } + + ("".to_string(), "".to_string()) + } + + pub(crate) fn new_highlight_config( + &self, + ) -> Result { + let grammar = self.get_grammar().ok_or(HighlightIssue::NotAvailable)?; + let (query, injection) = self.get_grammar_query(); + + match HighlightConfiguration::new(grammar, &query, &injection, "") { + Ok(x) => Ok(x), + Err(x) => { + let str = format!( + "Encountered {x:?} while trying to construct HighlightConfiguration for {}", + self.name() + ); + event!(Level::ERROR, "{str}"); + Err(HighlightIssue::Error(str)) + } + } + } + + pub(crate) fn walk_tree( + &self, + cursor: &mut TreeCursor, + normal_lines: &mut HashSet, + ) { + let (list, ignore_list) = self.properties().tree_sitter.code_glance; + walk_tree(cursor, normal_lines, list, ignore_list); + } +} + +fn load_grammar( + grammar_name: &str, + grammar_fn_name: &str, + path: &Path, +) -> Result { + let mut library_path = path.join(format!("libtree-sitter-{grammar_name}")); + library_path.set_extension(std::env::consts::DLL_EXTENSION); + + if !library_path.exists() { + event!(Level::WARN, "Grammar not found at: {library_path:?}"); + + // Load backward compat libraries + library_path = path.join(format!("tree-sitter-{grammar_name}")); + library_path.set_extension(std::env::consts::DLL_EXTENSION); + + if !library_path.exists() { + event!(Level::WARN, "Grammar not found at: {library_path:?}"); + return Err(HighlightIssue::Error("grammar not found".to_string())); + } + } + + event!(Level::DEBUG, "Loading grammar from user grammar dir"); + let library = match unsafe { libloading::Library::new(&library_path) } { + Ok(v) => v, + Err(e) => { + let err = format!("Failed to load '{}': '{e}'", library_path.display()); + event!(Level::ERROR, err); + return Err(HighlightIssue::Error(err)); + } + }; + + let language_fn_name = + format!("tree_sitter_{}", grammar_fn_name.replace('-', "_")); + event!( + Level::DEBUG, + "Loading grammar with address: '{language_fn_name}'" + ); + let language = unsafe { + let language_fn: libloading::Symbol< + unsafe extern "C" fn() -> tree_sitter::Language, + > = match library.get(language_fn_name.as_bytes()) { + Ok(v) => v, + Err(e) => { + let err = format!("Failed to load '{language_fn_name}': '{e}'"); + event!(Level::ERROR, err); + if let Some(e) = library.close().err() { + event!(Level::ERROR, "Failed to drop loaded library: {e}"); + }; + return Err(HighlightIssue::Error(err)); + } + }; + language_fn() + }; + std::mem::forget(library); + + Ok(language) +} + +/// Walk an AST and determine which lines to include in the code glance. +/// +/// Node types listed in `list` will be walked, along with their children. All +/// nodes encountered will be included, unless they are listed in `ignore_list`. +fn walk_tree( + cursor: &mut TreeCursor, + normal_lines: &mut HashSet, + list: &[&str], + ignore_list: &[&str], +) { + let node = cursor.node(); + let start_pos = node.start_position(); + let end_pos = node.end_position(); + let kind = node.kind().trim(); + if !ignore_list.contains(&kind) && !kind.is_empty() { + normal_lines.insert(start_pos.row); + normal_lines.insert(end_pos.row); + } + + if list.contains(&kind) && cursor.goto_first_child() { + loop { + walk_tree(cursor, normal_lines, list, ignore_list); + if !cursor.goto_next_sibling() { + break; + } + } + cursor.goto_parent(); + } +} + +fn add_bracket_pos( + bracket_pos: &mut HashMap>, + start_pos: Point, + color: String, +) { + let line_style = LineStyle { + start: start_pos.column, + end: start_pos.column + 1, + style: Style { + fg_color: Some(color), + }, + }; + match bracket_pos.entry(start_pos.row) { + Entry::Vacant(v) => _ = v.insert(vec![line_style]), + Entry::Occupied(mut o) => o.get_mut().push(line_style), + } +} + +pub(crate) fn walk_tree_bracket_ast( + cursor: &mut TreeCursor, + level: &mut usize, + counter: &mut usize, + bracket_pos: &mut HashMap>, + palette: &Vec, +) { + if cursor.node().kind().ends_with('(') + || cursor.node().kind().ends_with('{') + || cursor.node().kind().ends_with('[') + { + let row = cursor.node().end_position().row; + let col = cursor.node().end_position().column - 1; + let start_pos = Point::new(row, col); + add_bracket_pos( + bracket_pos, + start_pos, + palette.get(*level % palette.len()).unwrap().clone(), + ); + *level += 1; + } else if cursor.node().kind().ends_with(')') + || cursor.node().kind().ends_with('}') + || cursor.node().kind().ends_with(']') + { + let (new_level, overflow) = (*level).overflowing_sub(1); + let row = cursor.node().end_position().row; + let col = cursor.node().end_position().column - 1; + let start_pos = Point::new(row, col); + if overflow { + add_bracket_pos(bracket_pos, start_pos, "bracket.unpaired".to_string()); + } else { + *level = new_level; + add_bracket_pos( + bracket_pos, + start_pos, + palette.get(*level % palette.len()).unwrap().clone(), + ); + } + } + *counter += 1; + if cursor.goto_first_child() { + loop { + walk_tree_bracket_ast(cursor, level, counter, bracket_pos, palette); + if !cursor.goto_next_sibling() { + break; + } + } + cursor.goto_parent(); + } +} + +fn read_grammar_query(queries_dir: &Path, name: &str, kind: &str) -> String { + static INHERITS_REGEX: Lazy = + Lazy::new(|| Regex::new(r";+\s*inherits\s*:?\s*([a-z_,()-]+)\s*").unwrap()); + + let file = queries_dir.join(name).join(kind); + let query = std::fs::read_to_string(&file).unwrap_or_else(|err| { + tracing::event!( + tracing::Level::WARN, + "Failed to read queries at: {file:?}, {err}" + ); + String::new() + }); + + INHERITS_REGEX + .replace_all(&query, |captures: ®ex::Captures| { + captures[1] + .split(',') + .fold(String::new(), |mut output, name| { + write!( + output, + "\n{}\n", + read_grammar_query(queries_dir, name, kind) + ) + .unwrap(); + output + }) + }) + .to_string() +} + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + + use super::LapceLanguage; + + #[test] + fn test_language_from_path() { + let l = LapceLanguage::from_path(&PathBuf::new().join("test.rs")); + assert_eq!(l, LapceLanguage::Rust); + } +} diff --git a/lapce-core/src/lens.rs b/lapce-core/src/lens.rs new file mode 100644 index 0000000..348de5f --- /dev/null +++ b/lapce-core/src/lens.rs @@ -0,0 +1,388 @@ +use std::mem; + +use lapce_xi_rope::{ + Cursor, Delta, Interval, Metric, + interval::IntervalBounds, + tree::{DefaultMetric, Leaf, Node, NodeInfo, TreeBuilder}, +}; + +const MIN_LEAF: usize = 5; +const MAX_LEAF: usize = 10; + +pub type LensNode = Node; + +#[derive(Clone)] +pub struct Lens(LensNode); + +#[derive(Clone, Debug)] +pub struct LensInfo(usize); + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct LensData { + len: usize, + line_height: usize, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct LensLeaf { + len: usize, + data: Vec, + total_height: usize, +} + +pub struct LensIter<'a> { + cursor: Cursor<'a, LensInfo>, + end: usize, +} + +impl Lens { + pub fn len(&self) -> usize { + self.0.len() + } + + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + pub fn line_of_height(&self, height: usize) -> usize { + let max_height = self.0.count::(self.0.len()); + if height >= max_height { + return self.0.len(); + } + self.0.count_base_units::(height) + } + + pub fn height_of_line(&self, line: usize) -> usize { + let line = self.0.len().min(line); + self.0.count::(line) + } + + pub fn iter(&self) -> LensIter<'_> { + LensIter { + cursor: Cursor::new(&self.0, 0), + end: self.len(), + } + } + + pub fn iter_chunks(&self, range: I) -> LensIter<'_> { + let Interval { start, end } = range.into_interval(self.len()); + + LensIter { + cursor: Cursor::new(&self.0, start), + end, + } + } + + pub fn apply_delta(&mut self, _delta: &Delta) {} +} + +impl NodeInfo for LensInfo { + type L = LensLeaf; + + fn accumulate(&mut self, other: &Self) { + self.0 += other.0; + } + + fn compute_info(l: &LensLeaf) -> LensInfo { + LensInfo(l.total_height) + } +} + +impl Leaf for LensLeaf { + fn len(&self) -> usize { + self.len + } + + fn is_ok_child(&self) -> bool { + self.data.len() >= MIN_LEAF + } + + fn push_maybe_split( + &mut self, + other: &LensLeaf, + iv: Interval, + ) -> Option { + let (iv_start, iv_end) = iv.start_end(); + let mut accum = 0; + let mut added_len = 0; + let mut added_height = 0; + for sec in &other.data { + if accum + sec.len < iv_start { + accum += sec.len; + continue; + } + + if accum + sec.len <= iv_end { + accum += sec.len; + self.data.push(LensData { + len: sec.len, + line_height: sec.line_height, + }); + added_len += sec.len; + added_height += sec.len * sec.line_height; + continue; + } + + let len = iv_end - (accum + sec.len); + self.data.push(LensData { + len, + line_height: sec.line_height, + }); + added_len += len; + added_height += sec.len * sec.line_height; + break; + } + self.len += added_len; + self.total_height += added_height; + + if self.data.len() <= MAX_LEAF { + None + } else { + let splitpoint = self.data.len() / 2; // number of spans + let new = self.data.split_off(splitpoint); + let new_len = new.iter().map(|d| d.len).sum(); + let new_height = new.iter().map(|d| d.len * d.line_height).sum(); + self.len -= new_len; + self.total_height -= new_height; + Some(LensLeaf { + len: new_len, + data: new, + total_height: new_height, + }) + } + } +} + +#[derive(Copy, Clone)] +pub struct LensMetric(()); + +impl Metric for LensMetric { + fn measure(info: &LensInfo, _len: usize) -> usize { + info.0 + } + + fn to_base_units(l: &LensLeaf, in_measured_units: usize) -> usize { + if in_measured_units > l.total_height { + l.len + } else if in_measured_units == 0 { + 0 + } else { + let mut line = 0; + let mut accum = 0; + for data in l.data.iter() { + let leaf_height = data.line_height * data.len; + let accum_height = accum + leaf_height; + if accum_height > in_measured_units { + return line + (in_measured_units - accum) / data.line_height; + } + accum = accum_height; + line += data.len; + } + line + } + } + + fn from_base_units(l: &LensLeaf, in_base_units: usize) -> usize { + let mut line = 0; + let mut accum = 0; + for data in l.data.iter() { + if in_base_units < line + data.len { + return accum + (in_base_units - line) * data.line_height; + } + accum += data.len * data.line_height; + line += data.len; + } + accum + } + + fn is_boundary(_l: &LensLeaf, _offset: usize) -> bool { + true + } + + fn prev(_l: &LensLeaf, offset: usize) -> Option { + if offset == 0 { None } else { Some(offset - 1) } + } + + fn next(l: &LensLeaf, offset: usize) -> Option { + if offset < l.len { + Some(offset + 1) + } else { + None + } + } + + fn can_fragment() -> bool { + false + } +} + +impl DefaultMetric for LensInfo { + type DefaultMetric = LensBaseMetric; +} + +#[derive(Copy, Clone)] +pub struct LensBaseMetric(()); + +impl Metric for LensBaseMetric { + fn measure(_: &LensInfo, len: usize) -> usize { + len + } + + fn to_base_units(_: &LensLeaf, in_measured_units: usize) -> usize { + in_measured_units + } + + fn from_base_units(_: &LensLeaf, in_base_units: usize) -> usize { + in_base_units + } + + fn is_boundary(l: &LensLeaf, offset: usize) -> bool { + LensMetric::is_boundary(l, offset) + } + + fn prev(l: &LensLeaf, offset: usize) -> Option { + LensMetric::prev(l, offset) + } + + fn next(l: &LensLeaf, offset: usize) -> Option { + LensMetric::next(l, offset) + } + + fn can_fragment() -> bool { + false + } +} + +pub struct LensBuilder { + b: TreeBuilder, + leaf: LensLeaf, +} + +impl Default for LensBuilder { + fn default() -> LensBuilder { + LensBuilder { + b: TreeBuilder::new(), + leaf: LensLeaf::default(), + } + } +} + +impl LensBuilder { + pub fn new() -> LensBuilder { + LensBuilder::default() + } + + pub fn add_section(&mut self, len: usize, line_height: usize) { + if self.leaf.data.len() == MAX_LEAF { + let leaf = mem::take(&mut self.leaf); + self.b.push(Node::from_leaf(leaf)); + } + self.leaf.len += len; + self.leaf.total_height += len * line_height; + self.leaf.data.push(LensData { len, line_height }); + } + + pub fn build(mut self) -> Lens { + self.b.push(Node::from_leaf(self.leaf)); + Lens(self.b.build()) + } +} + +impl Iterator for LensIter<'_> { + type Item = (usize, usize); + + fn next(&mut self) -> Option { + if self.cursor.pos() >= self.end { + return None; + } + if let Some((leaf, leaf_pos)) = self.cursor.get_leaf() { + if leaf.data.is_empty() { + return None; + } + let line = self.cursor.pos(); + self.cursor.next::(); + + let mut lines = 0; + for data in leaf.data.iter() { + if leaf_pos < data.len + lines { + return Some((line, data.line_height)); + } + lines += data.len; + } + return None; + } + None + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_lens_metric() { + let mut builder = LensBuilder::new(); + builder.add_section(10, 2); + builder.add_section(1, 25); + builder.add_section(20, 3); + let lens = builder.build(); + + assert_eq!(31, lens.len()); + assert_eq!(0, lens.height_of_line(0)); + assert_eq!(2, lens.height_of_line(1)); + assert_eq!(20, lens.height_of_line(10)); + assert_eq!(45, lens.height_of_line(11)); + assert_eq!(48, lens.height_of_line(12)); + assert_eq!(105, lens.height_of_line(31)); + assert_eq!(105, lens.height_of_line(32)); + assert_eq!(105, lens.height_of_line(62)); + + assert_eq!(0, lens.line_of_height(0)); + assert_eq!(0, lens.line_of_height(1)); + assert_eq!(1, lens.line_of_height(2)); + assert_eq!(1, lens.line_of_height(3)); + assert_eq!(2, lens.line_of_height(4)); + assert_eq!(2, lens.line_of_height(5)); + assert_eq!(3, lens.line_of_height(6)); + assert_eq!(10, lens.line_of_height(20)); + assert_eq!(10, lens.line_of_height(44)); + assert_eq!(11, lens.line_of_height(45)); + assert_eq!(11, lens.line_of_height(46)); + assert_eq!(31, lens.line_of_height(105)); + assert_eq!(31, lens.line_of_height(106)); + } + + #[test] + fn test_lens_iter() { + let mut builder = LensBuilder::new(); + builder.add_section(10, 2); + builder.add_section(1, 25); + builder.add_section(2, 3); + let lens = builder.build(); + + let mut iter = lens.iter(); + assert_eq!(Some((0, 2)), iter.next()); + assert_eq!(Some((1, 2)), iter.next()); + assert_eq!(Some((2, 2)), iter.next()); + for _ in 0..7 { + iter.next(); + } + assert_eq!(Some((10, 25)), iter.next()); + assert_eq!(Some((11, 3)), iter.next()); + assert_eq!(Some((12, 3)), iter.next()); + assert_eq!(None, iter.next()); + + let mut iter = lens.iter_chunks(9..12); + assert_eq!(Some((9, 2)), iter.next()); + assert_eq!(Some((10, 25)), iter.next()); + assert_eq!(Some((11, 3)), iter.next()); + assert_eq!(None, iter.next()); + + let mut iter = lens.iter_chunks(9..15); + assert_eq!(Some((9, 2)), iter.next()); + assert_eq!(Some((10, 25)), iter.next()); + assert_eq!(Some((11, 3)), iter.next()); + assert_eq!(Some((12, 3)), iter.next()); + assert_eq!(None, iter.next()); + } +} diff --git a/lapce-core/src/lib.rs b/lapce-core/src/lib.rs new file mode 100644 index 0000000..6c2f2c7 --- /dev/null +++ b/lapce-core/src/lib.rs @@ -0,0 +1,13 @@ +#![allow(clippy::manual_clamp)] + +pub mod directory; +pub mod encoding; +pub mod language; +pub mod lens; +pub mod meta; +pub mod rope_text_pos; +pub mod style; +pub mod syntax; +// This is primarily being re-exported to avoid changing every single usage +// in lapce-app. We should probably remove this at some point. +pub use floem_editor_core::*; diff --git a/lapce-core/src/meta.rs b/lapce-core/src/meta.rs new file mode 100644 index 0000000..1add227 --- /dev/null +++ b/lapce-core/src/meta.rs @@ -0,0 +1,8 @@ +#[derive(strum_macros::AsRefStr, PartialEq, Eq)] +pub enum ReleaseType { + Debug, + Stable, + Nightly, +} + +include!(concat!(env!("OUT_DIR"), "/meta.rs")); diff --git a/lapce-core/src/rope_text_pos.rs b/lapce-core/src/rope_text_pos.rs new file mode 100644 index 0000000..7b28305 --- /dev/null +++ b/lapce-core/src/rope_text_pos.rs @@ -0,0 +1,40 @@ +use floem_editor_core::buffer::rope_text::RopeText; +use lsp_types::Position; + +use crate::encoding::{offset_utf8_to_utf16, offset_utf16_to_utf8}; + +pub trait RopeTextPosition: RopeText { + /// Converts a UTF8 offset to a UTF16 LSP position + /// Returns None if it is not a valid UTF16 offset + fn offset_to_position(&self, offset: usize) -> Position { + let (line, col) = self.offset_to_line_col(offset); + let line_offset = self.offset_of_line(line); + + let utf16_col = + offset_utf8_to_utf16(self.char_indices_iter(line_offset..), col); + + Position { + line: line as u32, + character: utf16_col as u32, + } + } + + fn offset_of_position(&self, pos: &Position) -> usize { + let (line, column) = self.position_to_line_col(pos); + + self.offset_of_line_col(line, column) + } + + fn position_to_line_col(&self, pos: &Position) -> (usize, usize) { + let line = pos.line as usize; + let line_offset = self.offset_of_line(line); + + let column = offset_utf16_to_utf8( + self.char_indices_iter(line_offset..), + pos.character as usize, + ); + + (line, column) + } +} +impl RopeTextPosition for T {} diff --git a/lapce-core/src/style.rs b/lapce-core/src/style.rs new file mode 100644 index 0000000..775737e --- /dev/null +++ b/lapce-core/src/style.rs @@ -0,0 +1,75 @@ +use std::str; + +use lapce_rpc::style::{LineStyle, Style}; +use lapce_xi_rope::{LinesMetric, Rope, spans::Spans}; + +pub const SCOPES: &[&str] = &[ + "constant", + "type", + "type.builtin", + "property", + "comment", + "constructor", + "function", + "label", + "keyword", + "string", + "variable", + "variable.other.member", + "operator", + "attribute", + "escape", + "embedded", + "symbol", + "punctuation", + "punctuation.special", + "punctuation.delimiter", + "text", + "text.literal", + "text.title", + "text.uri", + "text.reference", + "string.escape", + "conceal", + "none", + "tag", + "markup.bold", + "markup.italic", + "markup.list", + "markup.quote", + "markup.heading", + "markup.link.url", + "markup.link.label", + "markup.link.text", +]; + +pub fn line_styles( + text: &Rope, + line: usize, + styles: &Spans