Compare commits
458 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 95f7c53ff9 | |||
| f2355aa0b8 | |||
| 21192b6ec1 | |||
| f2ab022735 | |||
| 4bc49aa8db | |||
| 9ed68160e1 | |||
| 65a99d5945 | |||
| 3251d3d05c | |||
| 3ee00cebb8 | |||
| 2e601e50ab | |||
| af72732768 | |||
| 6a64a5331e | |||
| 9d016feb26 | |||
| 11f36a7038 | |||
| 0ddc216245 | |||
| 0ff10e346f | |||
| 5263087173 | |||
| 3f69ff4c18 | |||
| c604f513ae | |||
| c9f5f8fd20 | |||
| 985b07cca2 | |||
| 09065bb7af | |||
| 1004b7fbf3 | |||
| cbca98e108 | |||
| 75cfa069aa | |||
| 3dc59aeb27 | |||
| 43bdb124e3 | |||
| 6fd83c49bc | |||
| e9a87c0b01 | |||
| 8985acd81b | |||
| 0bc1c97575 | |||
| 36627a93ec | |||
| 42b219e642 | |||
| b066d164d3 | |||
| d390b5870c | |||
| f49ff95eee | |||
| 8531d7abe9 | |||
| 7c99eca99e | |||
| 49f4f50dfd | |||
| ab52dbcd57 | |||
| a234ae75e8 | |||
| abd6688fc5 | |||
| e2ec4234b7 | |||
| cc90d1e0bc | |||
| 7ebdf7f5bb | |||
| 468d416704 | |||
| 46f29719dd | |||
| d94668cea5 | |||
| 78b90a4ccd | |||
| 9fb61d0a72 | |||
| 3466d465f8 | |||
| 5e2a31e09f | |||
| d75cc7bf12 | |||
| 9ab8f6d919 | |||
| ad0a719e02 | |||
| f0d1fba177 | |||
| 38a6961987 | |||
| 659e4ce870 | |||
| c1ce13d1ce | |||
| f5cdedc0b2 | |||
| 6b38f76161 | |||
| c2321a963b | |||
| fc40496e51 | |||
| 6fa0f4c4b9 | |||
| e189ead0c1 | |||
| 76c6aa24f7 | |||
| 10f12074f7 | |||
| 8d32ebd43a | |||
| fe93911306 | |||
| d219fec54f | |||
| cbf4c78a98 | |||
| 2dca24cbdc | |||
| 3efd11a8ae | |||
| cb968ab523 | |||
| f64ed5bd86 | |||
| 9f4c589e9d | |||
| 54d9a7b7b2 | |||
| f9742f9e0f | |||
| d66b3c7cff | |||
| e8e0d4e0c2 | |||
| 2202343cdc | |||
| bc617f8995 | |||
| 819bdcb972 | |||
| cfc88c434b | |||
| 7b5ef24132 | |||
| 8507ad9eb5 | |||
| 350aa48b6f | |||
| 40e1631b1d | |||
| 0f9090640b | |||
| 6c68a01761 | |||
| 21e6719af2 | |||
| cf81aaad3c | |||
| 39ef70df34 | |||
| cb8b8b9728 | |||
| bfa61fd2e2 | |||
| 571485f1a6 | |||
| 9cd3bcc611 | |||
| 3ac55f9508 | |||
| aabb7f27f2 | |||
| 383fdf8e1a | |||
| b99a208471 | |||
| 29cc333460 | |||
| 823651e7dd | |||
| b3aa8aefa6 | |||
| 80ccc709df | |||
| 14dd01bd2a | |||
| cf1a33398a | |||
| 8b9a94d617 | |||
| 6a02307208 | |||
| 354a23c470 | |||
| 716125ddb6 | |||
| ed098beac2 | |||
| 5dceb340dc | |||
| 3873b88b10 | |||
| f2b49fda2f | |||
| 6ca7df9e91 | |||
| b9f1382f85 | |||
| 987bfb8b28 | |||
| 19cdabc84b | |||
| d8dc78c7f2 | |||
| 78e3c52ba2 | |||
| 51a56ac085 | |||
| 18e63a6345 | |||
| 1024a98506 | |||
| 16e0775297 | |||
| 30cb9f58dd | |||
| d7092d775a | |||
| 900591b774 | |||
| f4a89ead78 | |||
| b9d21e6003 | |||
| a7b76414e4 | |||
| 2ea1c8aba2 | |||
| 750b12242c | |||
| 8127fc25ce | |||
| 21c714d3f0 | |||
| 48ca2364e5 | |||
| 85de48d3f7 | |||
| f5e862c78c | |||
| 70cee3b1b0 | |||
| 3397ce7523 | |||
| 628b5eef71 | |||
| e4bef9d8d9 | |||
| c289599826 | |||
| 849ac6753f | |||
| 4f77923124 | |||
| 4e5ed2bef0 | |||
| fbe37339ae | |||
| 25045a2213 | |||
| 52de8302c1 | |||
| 54e6682755 | |||
| 1eefaf49ba | |||
| 995e4fdcc6 | |||
| a8b6e2d324 | |||
| c94607e5db | |||
| 631eebfb56 | |||
| d2dcf4cf98 | |||
| a5863e54b6 | |||
| 1ed42aef3d | |||
| 159c9cfc46 | |||
| b41cf0ca4e | |||
| 17bc13d4fa | |||
| 32d68b8a67 | |||
| 890dd0afe7 | |||
| 37b2773bac | |||
| e6a66cab8d | |||
| 49e4df0e89 | |||
| 8d1c65df98 | |||
| cea8a10a12 | |||
| a970532d47 | |||
| 1a22debaf5 | |||
| 43b54d022e | |||
| 797e42ac3a | |||
| 9ce38338fe | |||
| e9c3b96596 | |||
| 15ec2fe5c9 | |||
| 50b7152983 | |||
| 45951733ee | |||
| f9731ba74b | |||
| 37f2160a13 | |||
| d405ef2e2f | |||
| 59cc301050 | |||
| 2bb82948cb | |||
| f021173496 | |||
| 29ddb212fe | |||
| 4872588c80 | |||
| 9f944091e3 | |||
| ed3f599360 | |||
| f6328a72c0 | |||
| 993774f396 | |||
| 63d15f870a | |||
| 7b45a7504f | |||
| 823ff968d7 | |||
| f75143f23e | |||
| 3099fff025 | |||
| f8fa97b46b | |||
| 98fe5c0739 | |||
| 35f1d9f258 | |||
| 7acc4f5cda | |||
| 3c5713e013 | |||
| 6bc8dd0f3e | |||
| 4422838795 | |||
| 8044cdadba | |||
| d49d2f1f93 | |||
| 013ca94186 | |||
| 5dc17de070 | |||
| 2b97c5d618 | |||
| d448fe5f66 | |||
| fcccc0fb9d | |||
| 7258f38f62 | |||
| 0c82823218 | |||
| 47d8a18bda | |||
| 81a70dd545 | |||
| 26cbf7081d | |||
| ad5f47defb | |||
| b739064959 | |||
| e759fa88b4 | |||
| 2dcc07466c | |||
| 0b567128fe | |||
| 4f5318d519 | |||
| 94a6f8d374 | |||
| 0d5869fe96 | |||
| 2b9b9f7ba4 | |||
| 73e8ec44f9 | |||
| 0ae52f5b31 | |||
| e3cc7601a8 | |||
| 6e7aa1c5ab | |||
| bdc6351e6d | |||
| 2c08177d9b | |||
| 9bddebda16 | |||
| f3d1719ca7 | |||
| 8d51772988 | |||
| 2eb17a2150 | |||
| 86981f1624 | |||
| 4cc31611d8 | |||
| 5652efc39b | |||
| 65a73e0d24 | |||
| 9424a3336a | |||
| 83120215ad | |||
| e1f3892729 | |||
| 1d585b2bb5 | |||
| a4d805e77f | |||
| 0e0355122a | |||
| 5c09aa731d | |||
| 1f040084e4 | |||
| f7d2f4ba86 | |||
| b7d394408a | |||
| 048c5103e2 | |||
| 286d2d463d | |||
| e9435735f2 | |||
| f1a96106c1 | |||
| 7a330513af | |||
| c6cf1db4a8 | |||
| 87e0fc06f1 | |||
| cb96e5ac3f | |||
| 2cf6fa12cb | |||
| ede1159b7f | |||
| 111219cd49 | |||
| 95063498a3 | |||
| c68a00e19a | |||
| d8452a1e49 | |||
| 592cbc7b6f | |||
| 9009c97916 | |||
| e3de6269a4 | |||
| f26e045978 | |||
| 8a137c5b2c | |||
| c7e0474ebf | |||
| 8dc41a0df6 | |||
| a2ffa24d1c | |||
| b7cf8b03f8 | |||
| da290bf563 | |||
| bbe431a162 | |||
| d3e8ea8c6a | |||
| de2d7b11e1 | |||
| 07040dab7f | |||
| e574e9c89b | |||
| 6766930df5 | |||
| e4ebafca03 | |||
| a636ad0436 | |||
| 983d230393 | |||
| 79325cddfa | |||
| 1435dc89fd | |||
| d02ab6f866 | |||
| 94d55375c6 | |||
| c49e281422 | |||
| 73414962b1 | |||
| b4a6ec5b04 | |||
| c03153ba5e | |||
| 71a71a1905 | |||
| 980b025886 | |||
| 0b707bbc77 | |||
| 0e90d87e23 | |||
| d9094d926c | |||
| 692c4c359b | |||
| d7806565a1 | |||
| 87b13c987b | |||
| 25aae633c4 | |||
| 1f1020c3f9 | |||
| 6f87c66f3f | |||
| 76f6b78835 | |||
| b55cdac039 | |||
| fac0fd3bd8 | |||
| c12429f401 | |||
| bdae83292f | |||
| bd432eeccc | |||
| 04b8281dd8 | |||
| 460582929f | |||
| 64b2e9cb7e | |||
| db782db9b0 | |||
| eace7c16cf | |||
| 5ccc13bd03 | |||
| b57e2fa49c | |||
| 0ceafc53b3 | |||
| 59510d7c22 | |||
| 114fdd379a | |||
| 1ef665b881 | |||
| 8c85108f8b | |||
| ebc01a4367 | |||
| 948fa96cd5 | |||
| 01faff6387 | |||
| 7d98e8a28d | |||
| 72d84c0f6f | |||
| 27d932c1c2 | |||
| 3649e15ef0 | |||
| d9fa230005 | |||
| 0735f19ca0 | |||
| 553da3a5b8 | |||
| a81f352b18 | |||
| 2505c5b4eb | |||
| 5e17fa25d3 | |||
| a090c788cf | |||
| 1a3389ca1b | |||
| b93bf952cd | |||
| a6b22968ef | |||
| eb316a939b | |||
| 7a7931df85 | |||
| 88580f2247 | |||
| 4514221191 | |||
| 548ca85c25 | |||
| 78ccc8bc1f | |||
| 4f26af8668 | |||
| 0d5fab5e67 | |||
| f5fbf24333 | |||
| 82d30986e6 | |||
| cdc0ae805a | |||
| 938ee96a8e | |||
| 79fa5f3ffd | |||
| 640b7cfbea | |||
| 36e908ecf2 | |||
| 2cdd7af86f | |||
| 8e9bda9734 | |||
| 515d6404cd | |||
| 362a3bcf73 | |||
| 99a4f13da3 | |||
| 9b09c3a5df | |||
| 50e9a2170e | |||
| 17b8d65664 | |||
| f8caa220b9 | |||
| f2750f812f | |||
| 626f7b30f1 | |||
| ced5417173 | |||
| cd7b9a6657 | |||
| 3486db4543 | |||
| b9f6b54073 | |||
| 510dfd9fe6 | |||
| ccfae88211 | |||
| a760c24d9b | |||
| 8f596cc6f1 | |||
| 507e9e4de8 | |||
| c328533645 | |||
| 5b4c6c3868 | |||
| 7a87fb9d34 | |||
| 98facf246c | |||
| 89c64856d4 | |||
| 54b0850bd1 | |||
| f724016297 | |||
| d1c1aaf302 | |||
| d55a6ee9d7 | |||
| e295ce8d61 | |||
| 3ab7637b70 | |||
| 4ba0a9e54b | |||
| 7fc29b1baf | |||
| fa48091520 | |||
| 1f62c4a3e5 | |||
| 78e846ad89 | |||
| 32dd88c299 | |||
| 3cf1934484 | |||
| 7f8d7bdab9 | |||
| b288fcf02d | |||
| 4401d46528 | |||
| c539f7a205 | |||
| 83ca73b8f5 | |||
| b45eee137b | |||
| 4154fe3831 | |||
| 5bfc5975cd | |||
| dd9a6cff42 | |||
| 583e6713b9 | |||
| ec291ef33e | |||
| c166dac44e | |||
| 6eba1eb02e | |||
| 6fb6b58575 | |||
| b9dae22447 | |||
| a32a915788 | |||
| bf6f296235 | |||
| dc12d0f9a5 | |||
| 10c83e4320 | |||
| 5b177a7b8c | |||
| a67d1920ac | |||
| e368782880 | |||
| 8d7a1f1b7f | |||
| fe6b048ec2 | |||
| 9f4b608072 | |||
| 7a90970cea | |||
| 7cfe5e6890 | |||
| 5d1d3e8ab1 | |||
| 9918d5efc0 | |||
| b584250ea7 | |||
| 6ee9b8fcb7 | |||
| 7b33526a53 | |||
| 5f5f4a1357 | |||
| 6bd22fe837 | |||
| 36f27aec8f | |||
| 76f1fc2991 | |||
| 5e1a305471 | |||
| 27b7956bc8 | |||
| 558a951cf4 | |||
| a645336c18 | |||
| ab8af33373 | |||
| 56a98b9aaf | |||
| ea36dbcae7 | |||
| da1a57c833 | |||
| 4b847be66a | |||
| 520a6deb55 | |||
| 13a97ce6d8 | |||
| 30ffcebf3e | |||
| a8758d9e6b | |||
| c05ef6f1d8 | |||
| 5581fd8479 | |||
| bd195c77a4 | |||
| d4fe89f51a | |||
| d6c143b12e | |||
| 3a9d0739ad | |||
| 4d7553977f | |||
| 635917f910 | |||
| 9b064f5c81 | |||
| ddecf2c039 | |||
| ad9a8e178a | |||
| 857e61de1e | |||
| b1864360b8 | |||
| c0e77646c5 | |||
| f36c9612ed | |||
| aebbd81000 | |||
| 2454570020 | |||
| 1cbd4b9d4a | |||
| 069cc72cc9 | |||
| 8ef86c8f46 | |||
| 4d6a8c1a36 | |||
| cb644de14a | |||
| 6567b41390 |
@@ -0,0 +1,9 @@
|
||||
.github
|
||||
.vscode
|
||||
.lapce
|
||||
docs
|
||||
extra/linux/docker
|
||||
extra/macos
|
||||
extra/windows
|
||||
target
|
||||
lapce.spec
|
||||
@@ -7,5 +7,6 @@ updates:
|
||||
|
||||
- package-ecosystem: 'cargo'
|
||||
directory: '/'
|
||||
open-pull-requests-limit: 10
|
||||
schedule:
|
||||
interval: 'weekly'
|
||||
interval: 'weekly'
|
||||
|
||||
@@ -32,21 +32,20 @@ jobs:
|
||||
- lapce-app/portable
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install dependencies on Ubuntu
|
||||
if: startsWith(matrix.os, 'ubuntu')
|
||||
run: sudo apt-get update && sudo apt-get install clang libxkbcommon-x11-dev pkg-config libvulkan-dev libgtk-3-dev
|
||||
run: sudo make ubuntu-deps
|
||||
|
||||
- name: Update toolchain & add llvm-tools
|
||||
run: |
|
||||
rustup update
|
||||
rustup update --no-self-update
|
||||
rustup component add llvm-tools-preview
|
||||
|
||||
- name: Cache Rust dependencies
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
||||
|
||||
- name: Fetch dependencies
|
||||
run: cargo fetch --locked
|
||||
|
||||
@@ -58,6 +57,10 @@ jobs:
|
||||
if: ${{ join(matrix.features, ',') != '' }}
|
||||
run: cargo build --frozen --features ${{ join(matrix.features, ',') }}
|
||||
|
||||
- name: Free space
|
||||
if: startsWith(matrix.os, 'windows')
|
||||
run: cargo clean
|
||||
|
||||
- name: Install cargo-llvm-cov
|
||||
uses: taiki-e/install-action@cargo-llvm-cov
|
||||
|
||||
@@ -68,8 +71,7 @@ jobs:
|
||||
- name: Run doc tests
|
||||
run: cargo test --doc --workspace
|
||||
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v3
|
||||
- uses: codecov/codecov-action@v4
|
||||
with:
|
||||
files: lcov.info
|
||||
|
||||
@@ -77,8 +79,7 @@ jobs:
|
||||
name: Rustfmt
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Update toolchain & add rustfmt
|
||||
run: |
|
||||
@@ -96,17 +97,16 @@ jobs:
|
||||
os: [ ubuntu-latest, windows-latest, macos-latest ]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Update toolchain & add clippy
|
||||
run: |
|
||||
rustup update
|
||||
rustup update --no-self-update
|
||||
rustup component add clippy
|
||||
|
||||
- name: Install dependencies on Ubuntu
|
||||
if: startsWith(matrix.os, 'ubuntu')
|
||||
run: sudo apt-get update && sudo apt-get install clang libxkbcommon-x11-dev pkg-config libvulkan-dev
|
||||
run: sudo make ubuntu-deps
|
||||
|
||||
- name: Cache Rust dependencies
|
||||
uses: Swatinem/rust-cache@v2
|
||||
@@ -115,4 +115,4 @@ jobs:
|
||||
run: cargo fetch --locked
|
||||
|
||||
- name: Run clippy
|
||||
run: cargo clippy -- -D warnings
|
||||
run: cargo clippy
|
||||
|
||||
@@ -6,7 +6,7 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag_name:
|
||||
description: 'Tag name for release'
|
||||
description: "Tag name for release"
|
||||
required: false
|
||||
default: nightly
|
||||
push:
|
||||
@@ -16,10 +16,13 @@ on:
|
||||
# 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
|
||||
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
|
||||
|
||||
jobs:
|
||||
tagname:
|
||||
@@ -27,30 +30,24 @@ jobs:
|
||||
outputs:
|
||||
tag_name: ${{ steps.tag.outputs.tag }}
|
||||
steps:
|
||||
- if: github.event_name == 'workflow_dispatch'
|
||||
run: echo "TAG_NAME=${{ github.event.inputs.tag_name }}" >> $GITHUB_ENV
|
||||
- id: vars
|
||||
shell: bash
|
||||
run: echo "sha_short=${GITHUB_SHA::7}" | tee -a $GITHUB_OUTPUT
|
||||
|
||||
- if: github.event_name == 'schedule'
|
||||
run: echo 'TAG_NAME=nightly' >> $GITHUB_ENV
|
||||
- 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/}" >> $GITHUB_ENV
|
||||
|
||||
- if: github.event_name == 'pull_request'
|
||||
run: echo 'TAG_NAME=debug' >> $GITHUB_ENV
|
||||
|
||||
- id: vars
|
||||
shell: bash
|
||||
run: echo "sha_short=${GITHUB_SHA::7}" >> $GITHUB_OUTPUT
|
||||
|
||||
- if: env.TAG_NAME == 'nightly'
|
||||
run: echo 'TAG_NAME=nightly-${{ steps.vars.outputs.sha_short }}' >> $GITHUB_ENV
|
||||
echo "TAG_NAME=${TAG_NAME#refs/tags/}" | tee -a $GITHUB_ENV
|
||||
|
||||
- id: tag
|
||||
run: echo "tag=$TAG_NAME" >> $GITHUB_OUTPUT
|
||||
|
||||
run: echo "tag=$TAG_NAME" | tee -a $GITHUB_OUTPUT
|
||||
|
||||
windows:
|
||||
runs-on: windows-latest
|
||||
needs: tagname
|
||||
@@ -62,25 +59,23 @@ jobs:
|
||||
shell: bash
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Update rust
|
||||
run: rustup update
|
||||
|
||||
run: rustup update --no-self-update
|
||||
|
||||
- name: Fetch dependencies
|
||||
run: cargo fetch --locked
|
||||
|
||||
- name: Build
|
||||
run: cargo build --frozen --profile release-lto
|
||||
|
||||
- name: Install WiX
|
||||
run: nuget install WiX -Version 3.11.2
|
||||
|
||||
- name: Crate msi installer
|
||||
run: |
|
||||
./WiX.*/tools/candle.exe -arch "x64" -ext WixUIExtension -ext WixUtilExtension \
|
||||
candle.exe -arch "x64" -ext WixUIExtension -ext WixUtilExtension \
|
||||
-out "./lapce.wixobj" "extra/windows/wix/lapce.wxs"
|
||||
./WiX.*/tools/light.exe -ext WixUIExtension -ext WixUtilExtension \
|
||||
|
||||
light.exe -ext WixUIExtension -ext WixUtilExtension \
|
||||
-out "./Lapce-windows.msi" -sice:ICE61 -sice:ICE91 \
|
||||
"./lapce.wixobj"
|
||||
|
||||
@@ -100,7 +95,7 @@ jobs:
|
||||
Start-Sleep -Seconds 10
|
||||
$compressor.close()
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: lapce-windows
|
||||
path: |
|
||||
@@ -111,43 +106,113 @@ jobs:
|
||||
|
||||
linux:
|
||||
runs-on: ubuntu-latest
|
||||
container: ubuntu:18.04
|
||||
needs: tagname
|
||||
env:
|
||||
RELEASE_TAG_NAME: ${{ needs.tagname.outputs.tag_name }}
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install dependencies
|
||||
- uses: actions/checkout@v4
|
||||
- name: Build deb packages
|
||||
run: |
|
||||
apt-get -y update
|
||||
apt-get -y install curl wget clang libxkbcommon-x11-dev pkg-config libvulkan-dev
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
target: x86_64-unknown-linux-gnu
|
||||
profile: minimal
|
||||
|
||||
- name: Fetch dependencies
|
||||
run: cargo fetch --locked
|
||||
|
||||
- name: Build
|
||||
run: cargo build --frozen --profile release-lto --bin lapce
|
||||
docker buildx create --driver=docker-container --use
|
||||
docker buildx bake --pull ubuntu-focal-binary
|
||||
|
||||
- name: Gzip
|
||||
run: |
|
||||
mkdir Lapce
|
||||
cp ./target/release-lto/lapce Lapce/
|
||||
tar -zcvf ./Lapce-linux.tar.gz Lapce
|
||||
cp ./target/linux_amd64/lapce Lapce/
|
||||
tar -zcvf ./lapce-linux-amd64.tar.gz Lapce
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
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@v4
|
||||
with:
|
||||
name: lapce-linux
|
||||
path: |
|
||||
./Lapce-linux.tar.gz
|
||||
./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 }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os-name: debian
|
||||
os-version: bookworm
|
||||
- os-name: debian
|
||||
os-version: bullseye
|
||||
- os-name: ubuntu
|
||||
os-version: focal
|
||||
- os-name: ubuntu
|
||||
os-version: jammy
|
||||
- os-name: ubuntu
|
||||
os-version: lunar
|
||||
- os-name: ubuntu
|
||||
os-version: mantic
|
||||
# - os-name: ubuntu
|
||||
# os-version: noble
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- 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@v4
|
||||
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 }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os-name: fedora
|
||||
os-version: 39
|
||||
- os-name: fedora
|
||||
os-version: 40
|
||||
# - os-name: fedora
|
||||
# os-version: 41
|
||||
# - os-name: fedora
|
||||
# os-version: rawhide
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- 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@v4
|
||||
with:
|
||||
name: lapce-${{ matrix.os-name }}-${{ matrix.os-version }}
|
||||
path: |
|
||||
./target/*
|
||||
retention-days: 1
|
||||
|
||||
linux-musl:
|
||||
@@ -171,7 +236,7 @@ jobs:
|
||||
- triple: aarch64-unknown-linux-musl
|
||||
platform: aarch64
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Alpine Linux for ${{ matrix.platform }} (target arch)
|
||||
id: alpine-target
|
||||
@@ -208,11 +273,11 @@ jobs:
|
||||
PKG_CONFIG_LIBDIR: ${{ env.CROSS_SYSROOT }}/usr/lib/pkgconfig
|
||||
RUSTFLAGS: -C target-feature=+crt-static -C linker=/usr/bin/ld.lld # link runtime static, use universal linker
|
||||
CARGO_BUILD_TARGET: ${{ matrix.triple }}
|
||||
CARGO_PROFILE_RELEASE_LTO: 'true'
|
||||
CARGO_PROFILE_RELEASE_LTO: "true"
|
||||
CARGO_PROFILE_RELEASE_STRIP: symbols # remove unneeded debug stuff
|
||||
CARGO_PROFILE_RELEASE_OPT_LEVEL: 's' # optimise for size
|
||||
CARGO_PROFILE_RELEASE_CODEGEN_UNITS: '1' # optimise each crate
|
||||
SYSROOT: /dummy # workaround for https://github.com/rust-lang/pkg-config-rs/issues/102
|
||||
CARGO_PROFILE_RELEASE_OPT_LEVEL: "s" # optimise for size
|
||||
CARGO_PROFILE_RELEASE_CODEGEN_UNITS: "1" # optimise each crate
|
||||
SYSROOT: /dummy # workaround for https://github.com/rust-lang/pkg-config-rs/issues/102
|
||||
CC: clang
|
||||
run: |
|
||||
# Workaround for https://github.com/rust-lang/pkg-config-rs/issues/102
|
||||
@@ -231,7 +296,7 @@ jobs:
|
||||
run: |
|
||||
gzip -c ./target/${{ matrix.triple }}/release/lapce-proxy > ./lapce-proxy-linux-${{ matrix.platform }}.gz
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: lapce-proxy-linux-${{ matrix.platform }}
|
||||
path: |
|
||||
@@ -248,7 +313,7 @@ jobs:
|
||||
NOTARIZE_PASSWORD: ${{ secrets.NOTARIZE_PASSWORD }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install ARM target
|
||||
run: rustup update && rustup target add aarch64-apple-darwin
|
||||
@@ -274,16 +339,22 @@ jobs:
|
||||
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"
|
||||
run: |
|
||||
npx notarize-cli --file ./target/release-lto/macos/Lapce-macos.dmg --bundle-id io.lapce --asc-provider CYSGAZFR8D
|
||||
- name: Notarize Release Build
|
||||
uses: lando/notarize-action@v2
|
||||
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"
|
||||
|
||||
|
||||
- name: "Staple Release Build"
|
||||
uses: lapce/xcode-staple@062485d6eeafe841c18a412f012e80f49e23c517
|
||||
with:
|
||||
product-path: "./target/release-lto/macos/Lapce-macos.dmg"
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: lapce-macos
|
||||
path: |
|
||||
@@ -292,8 +363,13 @@ jobs:
|
||||
retention-days: 1
|
||||
|
||||
publish:
|
||||
if: github.event_name != 'pull_request'
|
||||
needs: [linux, linux-musl, windows, macos]
|
||||
needs:
|
||||
- linux
|
||||
- linux-musl
|
||||
- deb
|
||||
- rpm
|
||||
- windows
|
||||
- macos
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GH_REPO: ${{ github.repository }}
|
||||
@@ -303,35 +379,51 @@ jobs:
|
||||
steps:
|
||||
# Must perform checkout first, since it deletes the target directory
|
||||
# before running, and would therefore delete the downloaded artifacts
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
|
||||
- if: github.event_name == 'workflow_dispatch'
|
||||
run: echo "TAG_NAME=${{ github.event.inputs.tag_name }}" >> $GITHUB_ENV
|
||||
run: echo "TAG_NAME=${{ github.event.inputs.tag_name }}" | tee -a $GITHUB_ENV
|
||||
|
||||
- if: github.event_name == 'schedule'
|
||||
run: echo 'TAG_NAME=nightly' >> $GITHUB_ENV
|
||||
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/}" >> $GITHUB_ENV
|
||||
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') >> $GITHUB_ENV
|
||||
{
|
||||
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=') >> $GITHUB_ENV
|
||||
{
|
||||
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-proxy-linux-*/* lapce-windows/*
|
||||
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/*
|
||||
|
||||
@@ -6,6 +6,7 @@ shell.nix
|
||||
lib/
|
||||
target/
|
||||
core/languages/
|
||||
vendor/
|
||||
|
||||
.DS_Store
|
||||
*.patch
|
||||
*.patch
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
max_width = 85
|
||||
# below requires nightly
|
||||
# imports_granularity = "Crate"
|
||||
# group_imports = "StdExternalCrate"
|
||||
imports_granularity = "Crate"
|
||||
group_imports = "StdExternalCrate"
|
||||
|
||||
@@ -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
|
||||
@@ -3,9 +3,56 @@
|
||||
## Unreleased
|
||||
|
||||
### Features/Changes
|
||||
- [#2425](https://github.com/lapce/lapce/pull/2425): Reimplement completion lens
|
||||
|
||||
### Bug Fixes
|
||||
- [#3203](https://github.com/lapce/lapce/pull/3203): Fallback to default theme is theme key is not found
|
||||
|
||||
## 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
|
||||
|
||||
|
||||
@@ -1,22 +1,38 @@
|
||||
# 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).
|
||||
|
||||
A summary of how to contribute is below. If you are a _user_, also check out [this guide](docs/new-contributor-guide-user.md). If you are a _developer_, also check out [this guide](docs/new-contributor-guide-developer.md).
|
||||
## 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 _something you want to have included in Lapce_. We track feature requests on GitHub via [issues](https://github.com/lapce/lapce/issues). There are generally two kinds of features:
|
||||
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:
|
||||
|
||||
1. 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).
|
||||
2. A feature that is more suited to a Plugin. We currently have an issue to track requests specifically for Plugins! You can view the discussion and make suggestions [here](https://github.com/lapce/lapce/issues/558).
|
||||
### Core features
|
||||
|
||||
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!
|
||||
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 to indicate it should be fixed ASAP!
|
||||
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
|
||||
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
[package]
|
||||
name = "lapce"
|
||||
version = "0.2.8"
|
||||
authors = ["Dongdong Zhou <dzhou121@gmail.com>"]
|
||||
edition = "2021"
|
||||
rust-version = "1.65"
|
||||
default-run = "lapce"
|
||||
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-app = { path = "./lapce-app" }
|
||||
lapce-proxy = { path = "./lapce-proxy" }
|
||||
|
||||
[[bin]]
|
||||
@@ -19,79 +20,97 @@ name = "lapce-proxy"
|
||||
path = "lapce-proxy/src/bin/lapce-proxy.rs"
|
||||
|
||||
[workspace]
|
||||
members = [
|
||||
"lapce-app",
|
||||
"lapce-proxy",
|
||||
"lapce-rpc",
|
||||
"lapce-core",
|
||||
]
|
||||
members = ["lapce-app", "lapce-proxy", "lapce-rpc", "lapce-core"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.2.8"
|
||||
edition = "2021"
|
||||
rust-version = "1.64"
|
||||
homepage = "https://lapce.dev"
|
||||
authors = ["Dongdong Zhou <dzhou121@gmail.com>"]
|
||||
version = "0.4.0"
|
||||
edition = "2021"
|
||||
rust-version = "1.75"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://lapce.dev"
|
||||
authors = ["Dongdong Zhou <dzhou121@gmail.com>"]
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1.0"
|
||||
chrono = "0.4"
|
||||
clap = { version = "3.2.17", features = ["derive"] }
|
||||
crossbeam-channel = "0.5.0"
|
||||
directories = "4.0.1"
|
||||
flate2 = "1.0"
|
||||
hashbrown = { version = "0.12.3", features = ["serde"] }
|
||||
im = { version = "15.0.0", features = ["serde"] }
|
||||
include_dir = "0.6.2"
|
||||
indexmap = { version = "1.9", features = ["serde"] }
|
||||
interprocess = "1.1.1"
|
||||
itertools = "0.10.1"
|
||||
notify = { version = "5.0.0", features = ["serde"] }
|
||||
once_cell = "1.17"
|
||||
parking_lot = { version = "0.11.0", features = ["deadlock_detection"] }
|
||||
rayon = "1.5.1"
|
||||
regex = "1.7.0"
|
||||
reqwest = { version = "0.11", features = ["blocking", "json", "socks"] }
|
||||
serde = "1.0"
|
||||
serde_json = "1.0"
|
||||
smallvec = "1.10.0"
|
||||
strum = "0.21.0" # follow same version as system-deps in lockfile
|
||||
strum_macros = "0.21.1" # ditto
|
||||
tar = "0.4"
|
||||
thiserror = "1.0"
|
||||
toml_edit = { version = "0.14.4", features = ["easy"] }
|
||||
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" }
|
||||
globset = { version = "0.4.14" }
|
||||
hashbrown = { version = "0.14.5", features = ["serde"] }
|
||||
im = { version = "15.0.0", features = ["serde"] }
|
||||
include_dir = { version = "0.6.2" }
|
||||
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.4" }
|
||||
reqwest = { version = "0.11", features = ["blocking", "json", "socks"] }
|
||||
serde = { version = "1.0" }
|
||||
serde_json = { version = "1.0" }
|
||||
smallvec = { version = "1.13.2" }
|
||||
strum = { version = "0.26.2" }
|
||||
strum_macros = { version = "0.26.2" }
|
||||
tar = { version = "0.4" }
|
||||
thiserror = { version = "1.0" }
|
||||
toml = { version = "*" }
|
||||
toml_edit = { version = "0.20.2", features = ["serde"] }
|
||||
url = { version = "2.5.0" }
|
||||
|
||||
lsp-types = { version = "0.93", features = ["proposed"] }
|
||||
psp-types = { git = "https://github.com/lapce/psp-types" }
|
||||
dap-types = { git = "https://github.com/lapce/dap-types", rev = "913a2a520416a2a81a1f09a7946f595d5ae3ee4a", package = "dap" }
|
||||
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-core = { path = "./lapce-core" }
|
||||
lapce-rpc = { path = "./lapce-rpc" }
|
||||
lapce-proxy = { path = "./lapce-proxy" }
|
||||
|
||||
floem-editor-core = { git = "https://github.com/lapce/floem", rev = "c9e3d6a131c63286dc634b27c38b95bde43ed90d", features = ["serde"] }
|
||||
# floem-editor-core = { path = "../workspaces/floem/editor-core/", features = ["serde"] }
|
||||
|
||||
[patch.crates-io]
|
||||
# Temporarily patch lsp-types with a version that supports inline-completion
|
||||
lsp-types = { git = "https://github.com/lapce/lsp-types", rev = "feaa1e2ec80975c9dadd400a238ceacf071058e6" }
|
||||
regalloc2 = { rev = "5d79e12d0a93b10fc181f4da409b4671dd365228", git = "https://github.com/bytecodealliance/regalloc2" }
|
||||
|
||||
|
||||
[workspace.dependencies.tracing]
|
||||
git = "https://github.com/tokio-rs/tracing"
|
||||
rev = "c14525e1610db88986f849d46bd3e9795878b012"
|
||||
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 = "c14525e1610db88986f849d46bd3e9795878b012"
|
||||
git = "https://github.com/tokio-rs/tracing"
|
||||
rev = "908cc432a5994f6e17c8f36e13c217dc40085704"
|
||||
package = "tracing-subscriber"
|
||||
|
||||
[workspace.dependencies.tracing-appender]
|
||||
git = "https://github.com/tokio-rs/tracing"
|
||||
rev = "c14525e1610db88986f849d46bd3e9795878b012"
|
||||
git = "https://github.com/tokio-rs/tracing"
|
||||
rev = "908cc432a5994f6e17c8f36e13c217dc40085704"
|
||||
package = "tracing-appender"
|
||||
|
||||
[workspace.dependencies.alacritty_terminal]
|
||||
git = "https://github.com/lapce/alacritty"
|
||||
branch = "release/v0.12.1/lapce"
|
||||
version = "0.23.0"
|
||||
|
||||
[workspace.dependencies.windows-sys]
|
||||
version = "0"
|
||||
features = ["Win32_Foundation", "Win32_System_Threading"]
|
||||
|
||||
[profile.release-lto]
|
||||
inherits = "release"
|
||||
lto = true
|
||||
inherits = "release"
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
|
||||
# A profile which compiles all (non-workspace) dependencies in release mode
|
||||
@@ -107,3 +126,4 @@ opt-level = 3
|
||||
|
||||
[profile.fastdev]
|
||||
inherits = "dev"
|
||||
|
||||
|
||||
@@ -24,6 +24,10 @@ 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:
|
||||
|
||||
@@ -14,17 +14,17 @@
|
||||
<a href="https://discord.gg/n8tGJ6Rn6D" target="_blank">
|
||||
<img src="https://img.shields.io/discord/946858761413328946?logo=discord" />
|
||||
</a>
|
||||
<a href="https://matrix.to/#/#lapce-editor:matrix.org" target="_blank">
|
||||
<img src="https://img.shields.io/matrix/lapce-editor:matrix.org?color=turquoise&logo=Matrix" />
|
||||
</a>
|
||||
<a href="https://docs.lapce.dev" target="_blank">
|
||||
<img src="https://img.shields.io/static/v1?label=Docs&message=docs.lapce.dev&color=blue" alt="Lapce Docs">
|
||||
</a>
|
||||
<a href="https://wiki.mutable.ai/lapce/lapce" target="_blank">
|
||||
<img src="https://img.shields.io/static/v1?label=Code Wiki&message=Mutable.ai&color=blue" alt="Mutable.ai Auto Wiki">
|
||||
</a>
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
|
||||
Lapce (IPA: /læps/) is written in pure Rust with a UI in [Floem](https://github.com/lapce/floem) (which is also written in Rust). 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) which makes for 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/).
|
||||
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) which makes for 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/).
|
||||
|
||||

|
||||
|
||||
@@ -32,7 +32,7 @@ Lapce (IPA: /læps/) is written in pure Rust with a UI in [Floem](https://github
|
||||
|
||||
* 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.
|
||||
* 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://github.com/lapce/lapdev) 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.
|
||||
|
||||
|
||||
@@ -26,8 +26,15 @@ 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" = "#5C6370"
|
||||
"comment" = "$dim-text"
|
||||
"constant" = "$yellow"
|
||||
"type" = "$yellow"
|
||||
"typeAlias" = "$yellow"
|
||||
@@ -58,9 +65,14 @@ yellow = "#E5C07B"
|
||||
"text.uri" = "$cyan"
|
||||
"text.reference" = "$yellow"
|
||||
"variable" = "$red"
|
||||
"variable.other.member" = "$green"
|
||||
"variable.other.member" = "$red"
|
||||
"tag" = "$blue"
|
||||
|
||||
"bracket.color.1" = "$blue"
|
||||
"bracket.color.2" = "$yellow"
|
||||
"bracket.color.3" = "$purple"
|
||||
"bracket.unpaired" = "$red"
|
||||
|
||||
[color-theme.ui]
|
||||
"lapce.error" = "$red"
|
||||
"lapce.warn" = "$yellow"
|
||||
@@ -72,18 +84,18 @@ yellow = "#E5C07B"
|
||||
"lapce.button.primary.foreground" = "$black"
|
||||
|
||||
# tab
|
||||
"lapce.tab.active.background" = "$black"
|
||||
"lapce.tab.active.foreground" = "$white"
|
||||
"lapce.tab.active.background" = "$primary-background"
|
||||
"lapce.tab.active.foreground" = "$text"
|
||||
"lapce.tab.active.underline" = "#528BFF"
|
||||
|
||||
"lapce.tab.inactive.background" = "#21252b"
|
||||
"lapce.tab.inactive.foreground" = "$white"
|
||||
"lapce.tab.inactive.background" = "$secondary-background"
|
||||
"lapce.tab.inactive.foreground" = "$text"
|
||||
"lapce.tab.inactive.underline" = "#528BFF77"
|
||||
|
||||
"lapce.tab.separator" = ""
|
||||
|
||||
"lapce.icon.active" = "$white"
|
||||
"lapce.icon.inactive" = "#5C6370"
|
||||
"lapce.icon.active" = "$text"
|
||||
"lapce.icon.inactive" = "$dim-text"
|
||||
|
||||
"lapce.remote.icon" = "$black"
|
||||
"lapce.remote.local" = "#4078F2"
|
||||
@@ -92,66 +104,72 @@ yellow = "#E5C07B"
|
||||
"lapce.remote.disconnected" = "#E45649"
|
||||
|
||||
"lapce.plugin.name" = "#DDDDDD"
|
||||
"lapce.plugin.description" = "$white"
|
||||
"lapce.plugin.description" = "$text"
|
||||
"lapce.plugin.author" = "#B0B0B0"
|
||||
|
||||
"editor.background" = "$black"
|
||||
"editor.foreground" = "$white"
|
||||
"editor.dim" = "#5C6370"
|
||||
"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" = "$black"
|
||||
"editor.sticky_header_background" = "$primary-background"
|
||||
|
||||
"inlay_hint.foreground" = "$white"
|
||||
"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" = "#5C6370"
|
||||
"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"
|
||||
|
||||
"palette.background" = "#21252B"
|
||||
"palette.foreground" = "$white"
|
||||
"palette.current.background" = "#2C313A"
|
||||
"palette.current.foreground" = "$white"
|
||||
"tooltip.background" = "$primary-background"
|
||||
"tooltip.foreground" = "$text"
|
||||
|
||||
"completion.background" = "#21252B"
|
||||
"completion.current" = "#2C313A"
|
||||
"palette.background" = "$secondary-background"
|
||||
"palette.foreground" = "$text"
|
||||
"palette.current.background" = "$current-background"
|
||||
"palette.current.foreground" = "$text"
|
||||
|
||||
"hover.background" = "#21252B"
|
||||
"completion.background" = "$secondary-background"
|
||||
"completion.current" = "$current-background"
|
||||
|
||||
"activity.background" = "#21252B"
|
||||
"activity.current" = "$black"
|
||||
"hover.background" = "$secondary-background"
|
||||
|
||||
"activity.background" = "$secondary-background"
|
||||
"activity.current" = "$primary-background"
|
||||
|
||||
"debug.breakpoint" = "$red"
|
||||
"debug.breakpoint.hover" = "$grey"
|
||||
"debug.breakpoint.hover" = "#E06C7566"
|
||||
|
||||
"panel.background" = "#21252B"
|
||||
"panel.foreground" = "$white"
|
||||
"panel.foreground.dim" = "#5C6370"
|
||||
"panel.current.background" = "#2C313A"
|
||||
"panel.current.foreground" = "$white"
|
||||
"panel.current.foreground.dim" = "#5C6370"
|
||||
"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" = "#5C6370"
|
||||
"panel.hovered.foreground" = "$white"
|
||||
"panel.hovered.foreground.dim" = "#5C6370"
|
||||
"panel.hovered.active.background" = "$dim-text"
|
||||
"panel.hovered.foreground" = "$text"
|
||||
"panel.hovered.foreground.dim" = "$dim-text"
|
||||
|
||||
"status.background" = "#21252B"
|
||||
"status.foreground" = "$white"
|
||||
"status.background" = "$secondary-background"
|
||||
"status.foreground" = "$text"
|
||||
"status.modal.normal.background" = "$blue"
|
||||
"status.modal.normal.foreground" = "$black"
|
||||
"status.modal.insert.background" = "$red"
|
||||
@@ -163,9 +181,9 @@ yellow = "#E5C07B"
|
||||
|
||||
"markdown.blockquote" = "#898989"
|
||||
|
||||
"terminal.cursor" = "$white"
|
||||
"terminal.foreground" = "$white"
|
||||
"terminal.background" = "$black"
|
||||
"terminal.cursor" = "$text"
|
||||
"terminal.foreground" = "$text"
|
||||
"terminal.background" = "$primary-background"
|
||||
"terminal.white" = "$white"
|
||||
"terminal.black" = "$black"
|
||||
"terminal.red" = "$red"
|
||||
|
||||
@@ -4,6 +4,143 @@
|
||||
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"
|
||||
|
||||
"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"
|
||||
|
||||
"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]
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ mode = "i"
|
||||
|
||||
[[keymaps]]
|
||||
key = "shift+backspace"
|
||||
command = "delete_backward"
|
||||
command = "delete_forward"
|
||||
mode = "i"
|
||||
|
||||
[[keymaps]]
|
||||
@@ -109,7 +109,7 @@ command = "jump_location_backward"
|
||||
mode = "i"
|
||||
|
||||
[[keymaps]]
|
||||
key = "ctrl+_"
|
||||
key = "ctrl+shift+-"
|
||||
command = "jump_location_forward"
|
||||
mode = "i"
|
||||
|
||||
@@ -205,16 +205,36 @@ mode = "n"
|
||||
[[keymaps]]
|
||||
key = "esc"
|
||||
command = "modal.close"
|
||||
when = "modal_focus"
|
||||
when = "modal_focus || completion_focus"
|
||||
|
||||
[[keymaps]]
|
||||
key = "ctrl+b"
|
||||
command = "left"
|
||||
key = "tab"
|
||||
command = "inline_completion.select"
|
||||
when = "inline_completion_visible && !search_focus && !modal_focus && !list_focus && !search_active"
|
||||
mode = "i"
|
||||
|
||||
[[keymaps]]
|
||||
key = "ctrl+f"
|
||||
command = "right"
|
||||
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]]
|
||||
@@ -287,7 +307,7 @@ mode = "i"
|
||||
[[keymaps]]
|
||||
key = "tab"
|
||||
command = "insert_tab"
|
||||
when = "!in_snippet && !completion_focus && !search_focus && !replace_focus"
|
||||
when = "!in_snippet && !completion_focus && !inline_completion_visible && !search_focus && !replace_focus"
|
||||
mode = "i"
|
||||
|
||||
[[keymaps]]
|
||||
@@ -312,7 +332,7 @@ mode = "i"
|
||||
key = "esc"
|
||||
command = "normal_mode"
|
||||
mode = "niv"
|
||||
when = "!search_focus && !modal_focus && !search_active"
|
||||
when = "!search_focus && !modal_focus && !search_active && !inline_completion_visible"
|
||||
|
||||
[[keymaps]]
|
||||
key = "ctrl+c"
|
||||
@@ -327,7 +347,7 @@ mode = "niv"
|
||||
when = "!search_focus && !search_active"
|
||||
|
||||
[[keymaps]]
|
||||
key = ":"
|
||||
key = "shift+;"
|
||||
command = "palette.command"
|
||||
mode = "n"
|
||||
|
||||
@@ -487,12 +507,12 @@ command = "line_start"
|
||||
mode = "nv"
|
||||
|
||||
[[keymaps]]
|
||||
key = "^"
|
||||
key = "shift+6"
|
||||
command = "line_start_non_blank"
|
||||
mode = "nv"
|
||||
|
||||
[[keymaps]]
|
||||
key = "$"
|
||||
key = "shift+4"
|
||||
command = "line_end"
|
||||
mode = "nv"
|
||||
|
||||
@@ -587,12 +607,12 @@ command = "motion_mode_delete"
|
||||
mode = "n"
|
||||
|
||||
[[keymaps]]
|
||||
key = ">"
|
||||
key = "shift+."
|
||||
command = "motion_mode_indent"
|
||||
mode = "n"
|
||||
|
||||
[[keymaps]]
|
||||
key = "<"
|
||||
key = "shift+,"
|
||||
command = "motion_mode_outdent"
|
||||
mode = "n"
|
||||
|
||||
@@ -602,7 +622,7 @@ command = "motion_mode_yank"
|
||||
mode = "n"
|
||||
|
||||
[[keymaps]]
|
||||
key = "*"
|
||||
key = "shift+8"
|
||||
command = "search_whole_word_forward"
|
||||
mode = "nv"
|
||||
|
||||
@@ -617,27 +637,27 @@ command = "search_backward"
|
||||
mode = "nv"
|
||||
|
||||
[[keymaps]]
|
||||
key = "%"
|
||||
key = "shift+5"
|
||||
command = "match_pairs"
|
||||
mode = "nv"
|
||||
|
||||
[[keymaps]]
|
||||
key = "] )"
|
||||
key = "] shift+0"
|
||||
command = "next_unmatched_right_bracket"
|
||||
mode = "nv"
|
||||
|
||||
[[keymaps]]
|
||||
key = "[ ("
|
||||
key = "[ shift+9"
|
||||
command = "previous_unmatched_left_bracket"
|
||||
mode = "nv"
|
||||
|
||||
[[keymaps]]
|
||||
key = "] }"
|
||||
key = "] shift+]"
|
||||
command = "next_unmatched_right_curly_bracket"
|
||||
mode = "nv"
|
||||
|
||||
[[keymaps]]
|
||||
key = "[ {"
|
||||
key = "[ shift+["
|
||||
command = "previous_unmatched_left_curly_bracket"
|
||||
mode = "nv"
|
||||
|
||||
@@ -702,12 +722,12 @@ command = "toggle_code_lens"
|
||||
mode = "nv"
|
||||
|
||||
[[keymaps]]
|
||||
key = ">"
|
||||
key = "shift+."
|
||||
command = "indent_line"
|
||||
mode = "v"
|
||||
|
||||
[[keymaps]]
|
||||
key = "<"
|
||||
key = "shift+,"
|
||||
command = "outdent_line"
|
||||
mode = "v"
|
||||
|
||||
|
||||
@@ -130,7 +130,7 @@ command = "delete_word_forward"
|
||||
mode = "i"
|
||||
|
||||
[[keymaps]]
|
||||
key = "meta+|"
|
||||
key = "meta+shift+\\"
|
||||
command = "match_pairs"
|
||||
mode = "i"
|
||||
|
||||
@@ -275,6 +275,23 @@ command = "document_start"
|
||||
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"
|
||||
|
||||
@@ -125,7 +125,7 @@ command = "delete_word_forward"
|
||||
mode = "i"
|
||||
|
||||
[[keymaps]]
|
||||
key = "ctrl+|"
|
||||
key = "ctrl+shift+\\"
|
||||
command = "match_pairs"
|
||||
mode = "i"
|
||||
|
||||
@@ -275,6 +275,28 @@ command = "document_start"
|
||||
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"
|
||||
|
||||
@@ -26,8 +26,15 @@ 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" = "#A0A1A7"
|
||||
"comment" = "$dim-text"
|
||||
"constant" = "$yellow"
|
||||
"type" = "$yellow"
|
||||
"typeAlias" = "$yellow"
|
||||
@@ -64,9 +71,14 @@ yellow = "#C18401"
|
||||
"text.uri" = "$cyan"
|
||||
"text.reference" = "$yellow"
|
||||
"variable" = "$red"
|
||||
"variable.other.member" = "$green"
|
||||
"variable.other.member" = "$red"
|
||||
"tag" = "$blue"
|
||||
|
||||
"bracket.color.1" = "$blue"
|
||||
"bracket.color.2" = "$yellow"
|
||||
"bracket.color.3" = "$purple"
|
||||
"bracket.unpaired" = "$red"
|
||||
|
||||
[color-theme.ui]
|
||||
"lapce.error" = "#E51400"
|
||||
"lapce.warn" = "#E9A700"
|
||||
@@ -78,18 +90,18 @@ yellow = "#C18401"
|
||||
"lapce.button.primary.foreground" = "$white"
|
||||
|
||||
# tab
|
||||
"lapce.tab.active.background" = "$white"
|
||||
"lapce.tab.active.foreground" = "$black"
|
||||
"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" = "$black"
|
||||
"lapce.tab.inactive.foreground" = "$text"
|
||||
"lapce.tab.inactive.underline" = "#528BFF77"
|
||||
|
||||
"lapce.tab.separator" = "#B4B4B4"
|
||||
|
||||
"lapce.icon.active" = "$black"
|
||||
"lapce.icon.inactive" = "#A0A1A7"
|
||||
"lapce.icon.active" = "$text"
|
||||
"lapce.icon.inactive" = "$dim-text"
|
||||
|
||||
"lapce.remote.icon" = "$white"
|
||||
"lapce.remote.local" = "#4078F2"
|
||||
@@ -98,12 +110,12 @@ yellow = "#C18401"
|
||||
"lapce.remote.disconnected" = "#E45649"
|
||||
|
||||
"lapce.plugin.name" = "#444444"
|
||||
"lapce.plugin.description" = "$black"
|
||||
"lapce.plugin.description" = "$text"
|
||||
"lapce.plugin.author" = "#707070"
|
||||
|
||||
"terminal.cursor" = "$black"
|
||||
"terminal.foreground" = "$black"
|
||||
"terminal.background" = "$white"
|
||||
"terminal.cursor" = "$text"
|
||||
"terminal.foreground" = "$text"
|
||||
"terminal.background" = "$primary-background"
|
||||
"terminal.white" = "$white"
|
||||
"terminal.black" = "$black"
|
||||
"terminal.red" = "$red"
|
||||
@@ -121,63 +133,69 @@ yellow = "#C18401"
|
||||
"terminal.bright_magenta" = "$magenta"
|
||||
"terminal.bright_black" = "#A0A1A7"
|
||||
|
||||
"editor.background" = "$white"
|
||||
"editor.foreground" = "$black"
|
||||
"editor.dim" = "#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" = "$white"
|
||||
"editor.sticky_header_background" = "$primary-background"
|
||||
|
||||
"inlay_hint.foreground" = "$black"
|
||||
"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" = "#A0A1A7"
|
||||
"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"
|
||||
|
||||
"palette.background" = "#EAEAEB"
|
||||
"palette.foreground" = "$black"
|
||||
"palette.current.background" = "#DBDBDC"
|
||||
"palette.current.foreground" = "$black"
|
||||
"tooltip.background" = "$primary-background"
|
||||
"tooltip.foreground" = "$text"
|
||||
|
||||
"completion.background" = "#EAEAEB"
|
||||
"completion.current" = "#DBDBDC"
|
||||
"palette.background" = "$secondary-background"
|
||||
"palette.foreground" = "$text"
|
||||
"palette.current.background" = "$current-background"
|
||||
"palette.current.foreground" = "$text"
|
||||
|
||||
"hover.background" = "#EAEAEB"
|
||||
"completion.background" = "$secondary-background"
|
||||
"completion.current" = "$current-background"
|
||||
|
||||
"activity.background" = "#EAEAEB"
|
||||
"activity.current" = "$white"
|
||||
"hover.background" = "$secondary-background"
|
||||
|
||||
"activity.background" = "$secondary-background"
|
||||
"activity.current" = "$primary-background"
|
||||
|
||||
"debug.breakpoint" = "$red"
|
||||
"debug.breakpoint.hover" = "$grey"
|
||||
"debug.breakpoint.hover" = "#E4564966"
|
||||
|
||||
"panel.background" = "#EAEAEB"
|
||||
"panel.foreground" = "$black"
|
||||
"panel.foreground.dim" = "#A0A1A7"
|
||||
"panel.current.background" = "#DBDBDC"
|
||||
"panel.current.foreground" = "$black"
|
||||
"panel.current.foreground.dim" = "#A0A1A7"
|
||||
"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" = "#A0A1A7"
|
||||
"panel.hovered.foreground" = "$black"
|
||||
"panel.hovered.foreground.dim" = "#A0A1A7"
|
||||
"panel.hovered.active.background" = "$dim-text"
|
||||
"panel.hovered.foreground" = "$text"
|
||||
"panel.hovered.foreground.dim" = "$dim-text"
|
||||
|
||||
"status.background" = "#EAEAEB"
|
||||
"status.foreground" = "$black"
|
||||
"status.background" = "$secondary-background"
|
||||
"status.foreground" = "$text"
|
||||
"status.modal.normal.background" = "$blue"
|
||||
"status.modal.normal.foreground" = "$white"
|
||||
"status.modal.insert.background" = "$red"
|
||||
|
||||
@@ -4,16 +4,26 @@
|
||||
# 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
|
||||
program = ""
|
||||
|
||||
# the program arguments, e.g. args = ["arg1", "arg2"]
|
||||
args = []
|
||||
# the program arguments, e.g. args = ["arg1", "arg2"], optional
|
||||
# arguments = []
|
||||
|
||||
# current working directory, optional
|
||||
# cwd = "${workspace}"
|
||||
# working_directory = "${workspace}"
|
||||
|
||||
# enviroment variables, optional
|
||||
# [configs.env]
|
||||
# environment variables, optional
|
||||
# [configs.environment]
|
||||
# VAR1 = "VAL1"
|
||||
# VAR2 = "VAL2"
|
||||
|
||||
# task to run before the run/debug session is started, optional
|
||||
# [configs.prelaunch]
|
||||
# program = "cargo"
|
||||
# arguments = [
|
||||
# "build",
|
||||
# ]
|
||||
|
||||
@@ -7,21 +7,26 @@ icon-theme = "Lapce Codicons"
|
||||
custom-titlebar = true
|
||||
|
||||
[editor]
|
||||
font-family = "Cascadia Code"
|
||||
font-family = "Monospace"
|
||||
font-size = 13
|
||||
code-lens-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-show-documentation = true
|
||||
show-signature = true
|
||||
signature-label-code-block = true
|
||||
auto-closing-matching-pairs = true
|
||||
hover-delay = 300 # ms
|
||||
auto-surround = true
|
||||
hover-delay = 300 # ms
|
||||
modal-mode-relative-line-numbers = true
|
||||
format-on-save = false
|
||||
highlight-matching-brackets = true
|
||||
@@ -29,6 +34,7 @@ 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
|
||||
@@ -36,10 +42,12 @@ enable-error-lens = 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
|
||||
blink-interval = 500 # ms
|
||||
multicursor-case-sensitive = true
|
||||
multicursor-whole-words = true
|
||||
render-whitespace = "none"
|
||||
@@ -47,14 +55,30 @@ show-indent-guide = true
|
||||
atomic-soft-tabs = false
|
||||
double-click = "single"
|
||||
move-focus-while-search = true
|
||||
diff-context-lines=3
|
||||
scroll-speed-modifier=1
|
||||
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
|
||||
shell = ""
|
||||
|
||||
[terminal.default-profile]
|
||||
macos = "default"
|
||||
linux = "default"
|
||||
windows = "default"
|
||||
|
||||
[terminal.profiles]
|
||||
default = {}
|
||||
|
||||
# [terminal.profiles.example]
|
||||
# command = "cargo"
|
||||
# arguments = ["run"]
|
||||
# environemnt = { "KEY" = "VALUE" }
|
||||
# workdir = "/home/user"
|
||||
|
||||
[ui]
|
||||
font-family = ""
|
||||
@@ -65,318 +89,10 @@ status-height = 25
|
||||
tab-min-width = 100
|
||||
scroll-width = 10
|
||||
drop-shadow-width = 0
|
||||
preview-editor-width = 0
|
||||
palette-width = 500
|
||||
hover-font-family = ""
|
||||
hover-font-size = 0
|
||||
trim-search-results-whitespace = true
|
||||
list-line-height = 25
|
||||
|
||||
[color-theme]
|
||||
name = ""
|
||||
|
||||
[color-theme.base]
|
||||
white = "#ABB2BF"
|
||||
black = "#282C34"
|
||||
grey = "#3E4451"
|
||||
blue = "#61AFEF"
|
||||
red = "#E06C75"
|
||||
yellow = "#E5C07B"
|
||||
orange = "#D19A66"
|
||||
green = "#98C379"
|
||||
purple = "#C678DD"
|
||||
cyan = "#56B6C2"
|
||||
magenta = "#C678DD"
|
||||
|
||||
[color-theme.syntax]
|
||||
"comment" = "#5C6370"
|
||||
"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"
|
||||
"keyword.control" = "$purple"
|
||||
"selfKeyword" = "$purple"
|
||||
"field" = "$red"
|
||||
"property" = "$red"
|
||||
"enumMember" = "$red"
|
||||
"enum-member" = "$red"
|
||||
"variable.other.member" = "$red"
|
||||
"string" = "$green"
|
||||
"type.builtin" = "$cyan"
|
||||
"builtinType" = "$cyan"
|
||||
"escape" = "$cyan"
|
||||
"embedded" = "$cyan"
|
||||
"symbol" = "$yellow"
|
||||
|
||||
[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" = "$black"
|
||||
"lapce.tab.active.foreground" = "$white"
|
||||
"lapce.tab.active.underline" = "#528BFF"
|
||||
|
||||
"lapce.tab.inactive.background" = "#21252b"
|
||||
"lapce.tab.inactive.foreground" = "$white"
|
||||
"lapce.tab.inactive.underline" = "#528BFF77"
|
||||
|
||||
"lapce.tab.separator" = "#000000"
|
||||
|
||||
"lapce.icon.active" = "$white"
|
||||
"lapce.icon.inactive" = "#5C6370"
|
||||
|
||||
"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" = "$white"
|
||||
"lapce.plugin.author" = "#B0B0B0"
|
||||
|
||||
"editor.background" = "$black"
|
||||
"editor.foreground" = "$white"
|
||||
"editor.dim" = "#5C6370"
|
||||
"editor.focus" = "#CCCCCC"
|
||||
"editor.caret" = "#528bFF"
|
||||
"editor.selection" = "$grey"
|
||||
"editor.current_line" = "#2C313C"
|
||||
"editor.link" = "$blue"
|
||||
"editor.visible_whitespace" = "#5C6370"
|
||||
"editor.indent_guide" = "$grey"
|
||||
"editor.drag_drop_background" = "#79c1fc55"
|
||||
"editor.drag_drop_tab_background" = "#0b0e1455"
|
||||
"editor.sticky_header_background" = "$black"
|
||||
|
||||
"inlay_hint.foreground" = "$white"
|
||||
"inlay_hint.background" = "#528bFF88"
|
||||
|
||||
"error_lens.error.foreground" = "$red"
|
||||
"error_lens.error.background" = "#E06C7520"
|
||||
"error_lens.warning.foreground" = "$yellow"
|
||||
"error_lens.warning.background" = "#E5C07B20"
|
||||
"error_lens.other.foreground" = "#5C6370"
|
||||
"error_lens.other.background" = "#5C637020"
|
||||
|
||||
"completion_lens.foreground" = "#5C6370"
|
||||
|
||||
"source_control.added" = "#50A14FCC"
|
||||
"source_control.removed" = "#FF5266CC"
|
||||
"source_control.modified" = "#0184BCCC"
|
||||
|
||||
"palette.background" = "#21252B"
|
||||
"palette.foreground" = "$white"
|
||||
"palette.current.background" = "#2C313A"
|
||||
"palette.current.foreground" = "$white"
|
||||
|
||||
"completion.background" = "#21252B"
|
||||
"completion.current" = "#2C313A"
|
||||
|
||||
"hover.background" = "#21252B"
|
||||
|
||||
"activity.background" = "#21252B"
|
||||
"activity.current" = "$black"
|
||||
|
||||
"debug.breakpoint" = "$red"
|
||||
"debug.breakpoint.hover" = "$grey"
|
||||
|
||||
"panel.background" = "#21252B"
|
||||
"panel.foreground" = "$white"
|
||||
"panel.foreground.dim" = "#5C6370"
|
||||
"panel.current.background" = "#2C313A"
|
||||
"panel.current.foreground" = "$white"
|
||||
"panel.current.foreground.dim" = "#5C6370"
|
||||
"panel.hovered.background" = "#343A45"
|
||||
"panel.hovered.active.background" = "#5C6370"
|
||||
"panel.hovered.foreground" = "$white"
|
||||
"panel.hovered.foreground.dim" = "#5C6370"
|
||||
|
||||
"status.background" = "#21252B"
|
||||
"status.foreground" = "$white"
|
||||
"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" = "$white"
|
||||
"terminal.foreground" = "$white"
|
||||
"terminal.background" = "$black"
|
||||
"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"
|
||||
|
||||
[icon-theme]
|
||||
name = ""
|
||||
|
||||
[icon-theme.ui]
|
||||
"logo" = "lapce_logo.svg"
|
||||
"link" = "link.svg"
|
||||
"error" = "error.svg"
|
||||
"add" = "add.svg"
|
||||
"close" = "close.svg"
|
||||
"remote" = "lapce_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_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"
|
||||
|
||||
"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"
|
||||
|
||||
"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"
|
||||
|
||||
"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]
|
||||
tab-close-button = "Right"
|
||||
open-editors-visible = true
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
variable "RUST_VERSION" {
|
||||
default = "1.76"
|
||||
}
|
||||
|
||||
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 = "kinetic" }, # 22.10
|
||||
{ packages = null, platforms = null, type = "package", os_version = "lunar" }, # 23.04
|
||||
{ packages = null, platforms = null, type = "package", os_version = "mantic" }, # 23.10
|
||||
{ packages = null, platforms = null, type = "package", os_version = "noble" }, # 24.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 = "rawhide" },
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
target "cross-fedora" {
|
||||
inherits = ["fedora", "cross-package"]
|
||||
}
|
||||
|
||||
variable "APK_FAMILY_PACKAGES" {
|
||||
default = [
|
||||
"make",
|
||||
"clang",
|
||||
"git",
|
||||
"lld",
|
||||
"build-base",
|
||||
"rustup",
|
||||
"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 = "3.18", packages = null, platforms = null },
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
target "cross-alpine" {
|
||||
inherits = ["alpine-3-18", "cross-binary"]
|
||||
}
|
||||
@@ -8,15 +8,15 @@ It is easy to build Lapce from source on a GNU/Linux distribution. Cargo handles
|
||||
|
||||
#### Ubuntu
|
||||
```sh
|
||||
sudo apt install clang libxkbcommon-x11-dev pkg-config libvulkan-dev
|
||||
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
|
||||
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
|
||||
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):
|
||||
@@ -30,9 +30,21 @@ cd ~/lapce
|
||||
```
|
||||
|
||||
```sh
|
||||
cargo build --release
|
||||
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 `target/release/lapce`.
|
||||
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.
|
||||
|
||||
@@ -1,87 +1,20 @@
|
||||
## Installation With Package Manager
|
||||
|
||||
### Arch Linux
|
||||
Lapce is available in below software repositories:
|
||||
|
||||
There is an community package that can be installed with `pacman`:
|
||||
[](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 pacman -Syu lapce
|
||||
```
|
||||
|
||||
### Fedora
|
||||
|
||||
```bash
|
||||
sudo dnf copr enable titaniumtown/lapce
|
||||
sudo dnf copr enable titaniumtown/lapce
|
||||
sudo dnf install lapce
|
||||
```
|
||||
|
||||
### Flatpak
|
||||
|
||||
Lapce is available as a flatpak [here](https://flathub.org/apps/details/dev.lapce.lapce)
|
||||
|
||||
```bash
|
||||
flatpak install flathub dev.lapce.lapce
|
||||
```
|
||||
|
||||
### Gentoo
|
||||
|
||||
Lapce is available in Gentoos user repository GURU.
|
||||
If the GURU is not activated, it can be with:
|
||||
|
||||
```bash
|
||||
emerge --ask app-eselect/eselect-repository # install eselect repository
|
||||
eselect repository enable guru
|
||||
emaint sync -r guru
|
||||
```
|
||||
|
||||
After activating and syncing the GURU repository, lapce can be installed with
|
||||
|
||||
```bash
|
||||
emerge app-editors/lapce
|
||||
```
|
||||
|
||||
### Homebrew
|
||||
|
||||
```bash
|
||||
brew install lapce
|
||||
```
|
||||
|
||||
### nixpkgs
|
||||
|
||||
You can find the packages [here](https://search.nixos.org/packages?channel=unstable&show=lapce&from=0&size=50&sort=relevance&type=packages&query=lapce):
|
||||
|
||||
```bash
|
||||
# try with nix-shell
|
||||
nix-shell -p lapce
|
||||
|
||||
# on NixOS
|
||||
nix-env -iA nixos.lapce
|
||||
|
||||
# on non-NixOS installs, including macOS
|
||||
nix-env -iA nixpkgs.lapce
|
||||
|
||||
# only if `nix.settings.experimental-features` includes both `nix-command` and `flakes`.
|
||||
# WARNING: THIS BREAKS nix-env, PROCEED AT YOUR OWN RISK. THIS ALSO INSTALLS FROM UNSTABLE BRANCH.
|
||||
nix profile install nixpkgs#lapce
|
||||
```
|
||||
|
||||
### Scoop
|
||||
|
||||
```bash
|
||||
scoop install lapce
|
||||
```
|
||||
|
||||
### Void Linux
|
||||
|
||||
```bash
|
||||
sudo xbps-install -S lapce
|
||||
```
|
||||
|
||||
### winget
|
||||
|
||||
You can find the packages [here](https://github.com/microsoft/winget-pkgs/tree/master/manifests/l/Lapce/Lapce):
|
||||
|
||||
```bash
|
||||
winget install lapce
|
||||
```
|
||||
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
# New Contributor Guide **Developers**
|
||||
|
||||
This document contains some useful resources for anyone wishing to contribute to the Lapce project by contributing code.
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. Get an overview of the architecture from [`docs/why-lapce.md`](why-lapce.md) and [Architecture](https://docs.lapce.dev/development/architecture).
|
||||
|
||||
2. Explore the rope data structure, a key part of Lapce's architecture. The crate used is [xi_rope](https://crates.io/crates/xi-rope).
|
||||
|
||||
3. Review both open and closed [issues](https://github.com/lapce/lapce/issues), there may be others working on a similar feature.
|
||||
|
||||
4. [Get cracking](https://dictionary.cambridge.org/dictionary/english/get-cracking)!
|
||||
|
||||
5. Be aware that unless you explicitly state otherwise, contributions you make to this project will be under the Apache License Version 2.
|
||||
|
||||
## Resources
|
||||
|
||||
The [Wikipedia article on ropes](https://en.wikipedia.org/wiki/Rope_(data_structure)) provides an overview of the data structure.
|
||||
|
||||
For some context, also see [Ropes: an Alternative to Strings](https://www.cs.tufts.edu/comp/150FP/archive/hans-boehm/ropes.pdf) by Boehm, Atkinson, and Plass.
|
||||
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
# New Contributor Guide **Users**
|
||||
|
||||
This document contains some useful resources for anyone wishing to contribute to the Lapce project by suggesting improvements, requesting features, or supporting the community. Where it says _question_ in this document, it can also apply to _suggestions_, _requests_, etc.
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. Open a text editor
|
||||
|
||||
2. Write out your question (or request, or suggestion).
|
||||
|
||||
3. _Revise_ your question
|
||||
|
||||
4. Choose where to post your question
|
||||
|
||||
5. Post it!
|
||||
|
||||
## Resources
|
||||
|
||||
For general information about open source software, [see these FAQs](https://opensource.org/faq) by the Open Source Initiative.
|
||||
|
||||
We don't have a specified code of conduct yet, but for some reasonable guidelines see the [Mozilla Community Participation Guidelines](https://www.mozilla.org/en-US/about/governance/policies/participation/).
|
||||
|
||||
[This guide](http://www.catb.org/~esr/faqs/smart-questions.html), by Eric Steven Raymond, is about how to ask excellent questions. You can apply the same techniques (such as _Use meaningful, specific subject headers_) to suggestions and bug reports. The more specific (and helpful!) you can be with your question, the more it helps us to provide a solution.
|
||||
|
Before Width: | Height: | Size: 861 KiB After Width: | Height: | Size: 944 KiB |
@@ -9,7 +9,7 @@ GenericName=Code Editor
|
||||
StartupWMClass=lapce
|
||||
|
||||
Icon=dev.lapce.lapce
|
||||
Exec=lapce
|
||||
Exec=lapce %F
|
||||
Terminal=false
|
||||
MimeType=text/plain;inode/directory;
|
||||
Actions=new-window;
|
||||
|
||||
@@ -30,6 +30,6 @@
|
||||
</screenshot>
|
||||
</screenshots>
|
||||
<releases>
|
||||
<release version="0.2.8" date="2023-06-01"/>
|
||||
<release version="0.4.0" date="2024-04-24"/>
|
||||
</releases>
|
||||
</component>
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
# 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" ]
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# 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 \
|
||||
<<EOF
|
||||
|
||||
apk upgrade -Ua
|
||||
apk add --no-cache \
|
||||
bash clang lld llvm file git sccache \
|
||||
cmake pkgconf build-base abuild \
|
||||
${DISTRIBUTION_PACKAGES}
|
||||
EOF
|
||||
|
||||
WORKDIR /source
|
||||
COPY --link . .
|
||||
|
||||
FROM build-base AS build-prep
|
||||
|
||||
ENV CARGO_REGISTRIES_CRATES_IO_PROTOCOL='sparse'
|
||||
ENV CARGO_TARGET_DIR='/root/.cache/rust'
|
||||
RUN \
|
||||
--mount=type=cache,target=/cargo/git/db,sharing=locked \
|
||||
--mount=type=cache,target=/cargo/registry/cache,sharing=locked \
|
||||
--mount=type=cache,target=/cargo/registry/index,sharing=locked \
|
||||
<<EOF
|
||||
#!/usr/bin/env bash
|
||||
set -euxo pipefail
|
||||
|
||||
xx-cargo fetch --locked
|
||||
EOF
|
||||
|
||||
# Install target dependencies
|
||||
ARG TARGETPLATFORM
|
||||
ARG DISTRIBUTION_PACKAGES
|
||||
RUN \
|
||||
--mount=type=cache,target=/var/cache/apt,sharing=private \
|
||||
--mount=type=cache,target=/var/lib/apt,sharing=private \
|
||||
<<EOF
|
||||
#!/usr/bin/env bash
|
||||
set -euxo pipefail
|
||||
|
||||
xx-apk add \
|
||||
"xx-cxx-essentials" \
|
||||
${DISTRIBUTION_PACKAGES}
|
||||
EOF
|
||||
|
||||
FROM build-prep AS build
|
||||
|
||||
ARG PACKAGE_NAME
|
||||
ENV PACKAGE_NAME="${PACKAGE_NAME}"
|
||||
|
||||
ARG OUTPUT_DIR="/output"
|
||||
ENV OUTPUT_DIR="${OUTPUT_DIR}"
|
||||
|
||||
ARG CARGO_BUILD_INCREMENTAL='false'
|
||||
ENV CARGO_TARGET_DIR='/root/.cache/rust'
|
||||
|
||||
ENV CC='xx-clang'
|
||||
ENV CXX='xx-clang++'
|
||||
|
||||
ENV OPENSSL_NO_VENDOR="1"
|
||||
ENV ZSTD_SYS_USE_PKG_CONFIG="1"
|
||||
|
||||
ARG RELEASE_TAG_NAME
|
||||
ENV RELEASE_TAG_NAME="${RELEASE_TAG_NAME}"
|
||||
|
||||
ENV LIBZ_SYS_STATIC="1"
|
||||
ENV LIBSSH2_STATIC="1"
|
||||
ENV LIBGIT2_STATIC="1"
|
||||
ENV OPENSSL_STATIC="1"
|
||||
ENV PKG_CONFIG_ALL_STATIC="1"
|
||||
|
||||
RUN \
|
||||
--mount=type=cache,target=/cargo/git/db,sharing=locked \
|
||||
--mount=type=cache,target=/cargo/registry/cache,sharing=locked \
|
||||
--mount=type=cache,target=/cargo/registry/index,sharing=locked \
|
||||
--mount=type=cache,target=/root/.cache,sharing=private \
|
||||
<<EOF
|
||||
#!/usr/bin/env bash
|
||||
set -euxo pipefail
|
||||
|
||||
xx-clang --setup-target-triple
|
||||
xx-clang --wrap
|
||||
|
||||
export RUSTFLAGS="-C linker=clang -C link-arg=-fuse-ld=/usr/bin/ld.lld"
|
||||
export PKG_CONFIG="$(xx-clang --print-prog-name=pkg-config)"
|
||||
export CARGO_BUILD_TARGET="$(xx-cargo --print-target-triple)"
|
||||
|
||||
xx-cargo build \
|
||||
--frozen \
|
||||
--package lapce-app \
|
||||
--profile release-lto \
|
||||
--no-default-features \
|
||||
--features all-languages
|
||||
|
||||
export RUSTFLAGS="${RUSTFLAGS} -C target-feature=+crt-static"
|
||||
|
||||
xx-cargo build
|
||||
|
||||
xx-verify "${CARGO_TARGET_DIR}"/"$(xx-cargo --print-target-triple)"/release-lto/lapce
|
||||
|
||||
mkdir -p /target
|
||||
mv -v "${CARGO_TARGET_DIR}"/"$(xx-cargo --print-target-triple)"/release-lto/lapce /target/
|
||||
|
||||
cargo pkgid | cut -d'#' -f2 | cut -d'@' -f2 | cut -d':' -f2 | tee /target/lapce.version
|
||||
EOF
|
||||
|
||||
WORKDIR /output
|
||||
RUN <<EOF
|
||||
#!/usr/bin/env bash
|
||||
set -euxo pipefail
|
||||
|
||||
echo 'a'
|
||||
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 .
|
||||
@@ -0,0 +1,187 @@
|
||||
# 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}-${DISTRIBUTION_VERSION} AS build-base
|
||||
COPY --from=xx / /
|
||||
|
||||
SHELL [ "/bin/bash", "-c" ]
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# 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 \
|
||||
<<EOF
|
||||
|
||||
apt-get update -y
|
||||
apt-get install -y \
|
||||
bash clang lld llvm file cmake pkg-config dpkg-dev \
|
||||
${DISTRIBUTION_PACKAGES}
|
||||
EOF
|
||||
|
||||
WORKDIR /source
|
||||
COPY --link . .
|
||||
|
||||
FROM build-base AS build-prep
|
||||
|
||||
ENV CARGO_REGISTRIES_CRATES_IO_PROTOCOL='sparse'
|
||||
ENV CARGO_TARGET_DIR='/root/.cache/rust'
|
||||
RUN \
|
||||
--mount=type=cache,target=/cargo/git/db,sharing=locked \
|
||||
--mount=type=cache,target=/cargo/registry/cache,sharing=locked \
|
||||
--mount=type=cache,target=/cargo/registry/index,sharing=locked \
|
||||
<<EOF
|
||||
#!/usr/bin/env bash
|
||||
set -euxo pipefail
|
||||
|
||||
cargo fetch --locked
|
||||
EOF
|
||||
|
||||
# Install target dependencies
|
||||
ARG TARGETPLATFORM
|
||||
ARG DISTRIBUTION_PACKAGES
|
||||
RUN \
|
||||
--mount=type=cache,target=/var/cache/apt,sharing=private \
|
||||
--mount=type=cache,target=/var/lib/apt,sharing=private \
|
||||
<<EOF
|
||||
#!/usr/bin/env bash
|
||||
set -euxo pipefail
|
||||
|
||||
xx-apt-get install -y \
|
||||
"xx-cxx-essentials" \
|
||||
${DISTRIBUTION_PACKAGES}
|
||||
EOF
|
||||
|
||||
FROM build-prep AS build
|
||||
|
||||
ARG PACKAGE_NAME
|
||||
ENV PACKAGE_NAME="${PACKAGE_NAME}"
|
||||
|
||||
ARG OUTPUT_DIR="/output"
|
||||
ENV OUTPUT_DIR="${OUTPUT_DIR}"
|
||||
|
||||
# in bullseye arm64 target does not link with lld so configure it to use ld instead
|
||||
# RUN [ ! -f /etc/alpine-release ] && xx-info is-cross && [ "$(xx-info arch)" = "arm64" ] && XX_CC_PREFER_LINKER=ld xx-clang --setup-target-triple || true
|
||||
|
||||
ARG CARGO_BUILD_INCREMENTAL='false'
|
||||
ENV CARGO_TARGET_DIR='/root/.cache/rust'
|
||||
|
||||
ENV CC='xx-clang'
|
||||
ENV CXX='xx-clang++'
|
||||
|
||||
ENV OPENSSL_NO_VENDOR="1"
|
||||
ENV ZSTD_SYS_USE_PKG_CONFIG="1"
|
||||
|
||||
ARG RELEASE_TAG_NAME
|
||||
ENV RELEASE_TAG_NAME="${RELEASE_TAG_NAME}"
|
||||
|
||||
RUN \
|
||||
--mount=type=cache,target=/cargo/git/db,sharing=locked \
|
||||
--mount=type=cache,target=/cargo/registry/cache,sharing=locked \
|
||||
--mount=type=cache,target=/cargo/registry/index,sharing=locked \
|
||||
--mount=type=cache,target=/root/.cache,sharing=private \
|
||||
<<EOF
|
||||
#!/usr/bin/env bash
|
||||
set -euxo pipefail
|
||||
|
||||
xx-clang --setup-target-triple
|
||||
xx-clang --wrap
|
||||
|
||||
export RUSTFLAGS="-C linker=clang -C link-arg=-fuse-ld=/usr/bin/ld.lld"
|
||||
export PKG_CONFIG="$(xx-clang --print-prog-name=pkg-config)"
|
||||
export CARGO_BUILD_TARGET="$(xx-cargo --print-target-triple)"
|
||||
|
||||
xx-cargo build \
|
||||
--frozen \
|
||||
--package lapce-app \
|
||||
--profile release-lto \
|
||||
--no-default-features \
|
||||
--features all-languages
|
||||
|
||||
xx-verify "${CARGO_TARGET_DIR}"/"$(xx-cargo --print-target-triple)"/release-lto/lapce
|
||||
|
||||
mkdir -p /target
|
||||
mv -v "${CARGO_TARGET_DIR}"/"$(xx-cargo --print-target-triple)"/release-lto/lapce /target/
|
||||
|
||||
cargo pkgid | cut -d'#' -f2 | cut -d'@' -f2 | cut -d':' -f2 | tee /target/lapce.version
|
||||
EOF
|
||||
|
||||
WORKDIR /output
|
||||
RUN <<EOF
|
||||
#!/usr/bin/env bash
|
||||
set -euxo pipefail
|
||||
|
||||
export _PACKAGE_ARCHITECTURE=$(xx-info debian-arch)
|
||||
|
||||
mkdir -v -p ${PACKAGE_NAME}/{etc,usr/{bin,share/{applications,metainfo,pixmaps}},debian}
|
||||
cd ${PACKAGE_NAME}
|
||||
|
||||
cp '/source/extra/linux/dev.lapce.lapce.desktop' './usr/share/applications/dev.lapce.lapce.desktop'
|
||||
cp '/source/extra/linux/dev.lapce.lapce.metainfo.xml' './usr/share/metainfo/dev.lapce.lapce.metainfo.xml'
|
||||
cp '/source/extra/images/logo.png' './usr/share/pixmaps/dev.lapce.lapce.png'
|
||||
|
||||
mv '/target/lapce' './usr/bin/'
|
||||
|
||||
if [[ "${PACKAGE_NAME}" == "lapce" ]]; then
|
||||
conflicts="lapce-nightly"
|
||||
else
|
||||
conflicts="lapce"
|
||||
fi
|
||||
|
||||
case "${RELEASE_TAG_NAME}" in
|
||||
nightly-*)
|
||||
version=$(cat /target/lapce.version)
|
||||
commit=$(echo "${RELEASE_TAG_NAME}" | cut -d'-' -f2)
|
||||
# date=$(date +%Y%m%d%H%M)
|
||||
RELEASE_TAG_NAME="${version}+${commit}"
|
||||
;;
|
||||
debug|nightly)
|
||||
version=$(cat /target/lapce.version)
|
||||
date=$(date +%Y%m%d%H%M)
|
||||
RELEASE_TAG_NAME="${version}+${date}"
|
||||
;;
|
||||
*)
|
||||
RELEASE_TAG_NAME="${RELEASE_TAG_NAME//v/}"
|
||||
;;
|
||||
esac
|
||||
|
||||
cat <<- EOL > debian/control
|
||||
Package: ${PACKAGE_NAME}
|
||||
Version: ${RELEASE_TAG_NAME}
|
||||
Conflicts: ${conflicts}
|
||||
Maintainer: Jakub Panek <me+debian@panekj.dev>
|
||||
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 .
|
||||
@@ -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 \
|
||||
<<EOF
|
||||
#!/usr/bin/env bash
|
||||
set -euxo pipefail
|
||||
|
||||
printf "keepcache=True" >> /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 \
|
||||
<<EOF
|
||||
#!/usr/bin/env bash
|
||||
set -euxo pipefail
|
||||
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||
EOF
|
||||
|
||||
ENV PATH="${CARGO_HOME}/bin:${PATH}"
|
||||
|
||||
WORKDIR /source
|
||||
|
||||
FROM build-base AS build-prep
|
||||
|
||||
ENV CARGO_REGISTRIES_CRATES_IO_PROTOCOL='sparse'
|
||||
ENV CARGO_TARGET_DIR='/target'
|
||||
ENV CARGO_NET_GIT_FETCH_WITH_CLI="true"
|
||||
|
||||
COPY --link . .
|
||||
|
||||
RUN \
|
||||
--mount=type=cache,target=/cargo/git/db,sharing=locked \
|
||||
--mount=type=cache,target=/cargo/registry/cache,sharing=locked \
|
||||
--mount=type=cache,target=/cargo/registry/index,sharing=locked \
|
||||
<<EOF
|
||||
#!/usr/bin/env bash
|
||||
set -euxo pipefail
|
||||
|
||||
cargo fetch --locked
|
||||
EOF
|
||||
|
||||
# Install target dependencies
|
||||
ARG TARGETPLATFORM
|
||||
ARG DISTRIBUTION_PACKAGES
|
||||
RUN \
|
||||
--mount=type=cache,target=/var/cache/dnf,sharing=locked \
|
||||
<<EOF
|
||||
#!/usr/bin/env bash
|
||||
set -euxo pipefail
|
||||
|
||||
mkdir -p /run/lock
|
||||
mkdir -p /var/run/lock
|
||||
XX_DEBUG_DNF=1 \
|
||||
xx-dnf install -y \
|
||||
"xx-cxx-essentials" \
|
||||
${DISTRIBUTION_PACKAGES}
|
||||
EOF
|
||||
|
||||
FROM build-prep AS build
|
||||
|
||||
ARG PACKAGE_NAME
|
||||
ENV PACKAGE_NAME="${PACKAGE_NAME}"
|
||||
|
||||
ARG CARGO_BUILD_INCREMENTAL='false'
|
||||
|
||||
ENV CC='xx-clang'
|
||||
ENV CXX='xx-clang++'
|
||||
|
||||
ENV OPENSSL_NO_VENDOR="1"
|
||||
ENV ZSTD_SYS_USE_PKG_CONFIG="1"
|
||||
|
||||
ARG RELEASE_TAG_NAME
|
||||
ENV RELEASE_TAG_NAME="${RELEASE_TAG_NAME}"
|
||||
|
||||
RUN \
|
||||
--mount=type=cache,target=/cargo/git/db,readonly=true \
|
||||
--mount=type=cache,target=/cargo/registry/cache,readonly=true \
|
||||
--mount=type=cache,target=/cargo/registry/index,readonly=true \
|
||||
--mount=type=cache,target=/root/.cache,sharing=private \
|
||||
<<EOF
|
||||
#!/usr/bin/env bash
|
||||
set -euxo pipefail
|
||||
|
||||
LAPCE_VERSION=$(cargo pkgid | cut -d'#' -f2 | cut -d'@' -f2 | cut -d':' -f2)
|
||||
|
||||
case "${RELEASE_TAG_NAME}" in
|
||||
nightly-*)
|
||||
commit=$(echo "${RELEASE_TAG_NAME}" | cut -d'-' -f2)
|
||||
RELEASE_TAG_NAME="${LAPCE_VERSION}+${commit}"
|
||||
;;
|
||||
debug|nightly)
|
||||
date=$(date +%Y%m%d%H%M)
|
||||
RELEASE_TAG_NAME="${LAPCE_VERSION}+${date}"
|
||||
;;
|
||||
*)
|
||||
RELEASE_TAG_NAME="${RELEASE_TAG_NAME//v/}"
|
||||
;;
|
||||
esac
|
||||
|
||||
. /etc/os-release
|
||||
export DISTRO_ID="$(echo $ID | tr - _)"
|
||||
export DISTRO_VERSION="$(echo $VERSION_ID | tr - _)"
|
||||
|
||||
cat > lapce.spec <<EOL
|
||||
Name: ${PACKAGE_NAME}
|
||||
Version: ${RELEASE_TAG_NAME}
|
||||
Release: 1.${DISTRO_ID}${DISTRO_VERSION}
|
||||
Summary: Lightning-fast and Powerful Code Editor written in Rust
|
||||
License: Apache-2.0
|
||||
URL: https://github.com/lapce/lapce
|
||||
Packager: Jakub Panek
|
||||
|
||||
%description
|
||||
Lapce is written in pure Rust with a UI in Druid (which is also written in Rust).
|
||||
It is designed with Rope Science from the Xi-Editor which makes for lightning-fast computation, and leverages OpenGL for rendering.
|
||||
|
||||
%build
|
||||
xx-clang --setup-target-triple
|
||||
xx-clang --wrap
|
||||
export RUSTFLAGS="-C linker=clang -C link-arg=-fuse-ld=/usr/bin/ld.lld"
|
||||
export PKG_CONFIG="$(xx-clang --print-prog-name=pkg-config)"
|
||||
rustup target add "$(xx-cargo --print-target-triple)"
|
||||
export RELEASE_TAG_NAME="${RELEASE_TAG_NAME}"
|
||||
xx-cargo build --profile release-lto --bin lapce --frozen
|
||||
xx-verify "${CARGO_TARGET_DIR}"/"$(xx-cargo --print-target-triple)"/release-lto/lapce
|
||||
|
||||
%install
|
||||
install -Dm755 "${CARGO_TARGET_DIR}"/"$(xx-cargo --print-target-triple)"/release-lto/lapce %{buildroot}%{_bindir}/lapce
|
||||
install -Dm644 extra/linux/dev.lapce.lapce.desktop %{buildroot}/usr/share/applications/dev.lapce.lapce.desktop
|
||||
install -Dm644 extra/linux/dev.lapce.lapce.metainfo.xml %{buildroot}/usr/share/metainfo/dev.lapce.lapce.metainfo.xml
|
||||
install -Dm644 extra/images/logo.png %{buildroot}/usr/share/pixmaps/dev.lapce.lapce.png
|
||||
|
||||
%files
|
||||
%license LICENSE*
|
||||
%doc *.md
|
||||
%{_bindir}/lapce
|
||||
/usr/share/applications/dev.lapce.lapce.desktop
|
||||
/usr/share/metainfo/dev.lapce.lapce.metainfo.xml
|
||||
/usr/share/pixmaps/dev.lapce.lapce.png
|
||||
|
||||
%changelog
|
||||
* Thu Apr 25 2024 Jakub Panek
|
||||
- See GitHub for full changelog
|
||||
EOL
|
||||
|
||||
/usr/bin/rpmbuild --build-in-place --noclean --noprep -bb --rmspec lapce.spec --verbose
|
||||
ls -lahR $HOME/rpmbuild
|
||||
EOF
|
||||
|
||||
RUN \
|
||||
<<EOF
|
||||
#!/usr/bin/env bash
|
||||
set -euxo pipefail
|
||||
|
||||
mv -v $HOME/rpmbuild/RPMS/* /output/
|
||||
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/*.rpm .
|
||||
|
||||
FROM scratch AS cross-package
|
||||
COPY --from=build /output/*.rpm .
|
||||
@@ -0,0 +1,215 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
ARG DISTRIBUTION_VERSION
|
||||
ARG RUST_VERSION
|
||||
ARG XX_VERSION
|
||||
|
||||
FROM --platform=$BUILDPLATFORM tonistiigi/xx:${XX_VERSION} AS xx
|
||||
FROM --platform=$BUILDPLATFORM ubuntu:${DISTRIBUTION_VERSION} AS build-base
|
||||
COPY --from=xx / /
|
||||
|
||||
SHELL [ "/bin/bash", "-c" ]
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
RUN \
|
||||
<<EOF
|
||||
#!/usr/bin/env bash
|
||||
set -euxo pipefail
|
||||
|
||||
rm -f /etc/apt/apt.conf.d/docker-clean
|
||||
echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /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 \
|
||||
<<EOF
|
||||
#!/usr/bin/env bash
|
||||
set -euxo pipefail
|
||||
|
||||
apt-get update -y
|
||||
apt-get upgrade -y
|
||||
apt-get install -y \
|
||||
bash clang lld llvm file cmake pkg-config curl git dpkg-dev \
|
||||
${DISTRIBUTION_PACKAGES}
|
||||
EOF
|
||||
|
||||
ENV CARGO_HOME="/cargo"
|
||||
ENV RUSTUP_HOME="/rustup"
|
||||
|
||||
RUN \
|
||||
<<EOF
|
||||
#!/usr/bin/env bash
|
||||
set -euxo pipefail
|
||||
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||
EOF
|
||||
|
||||
ENV PATH="${CARGO_HOME}/bin:${PATH}"
|
||||
|
||||
WORKDIR /source
|
||||
COPY --link . .
|
||||
|
||||
FROM build-base AS build-prep
|
||||
|
||||
ENV CARGO_TARGET_DIR='/target'
|
||||
|
||||
RUN \
|
||||
--mount=type=cache,target=/cargo/git/db,sharing=locked \
|
||||
--mount=type=cache,target=/cargo/registry/cache,sharing=locked \
|
||||
--mount=type=cache,target=/cargo/registry/index,sharing=locked \
|
||||
<<EOF
|
||||
#!/usr/bin/env bash
|
||||
set -euxo pipefail
|
||||
|
||||
cargo fetch --locked
|
||||
EOF
|
||||
|
||||
# Install target dependencies
|
||||
ARG TARGETPLATFORM
|
||||
ARG DISTRIBUTION_PACKAGES
|
||||
RUN \
|
||||
--mount=type=cache,target=/var/cache/apt,sharing=private \
|
||||
--mount=type=cache,target=/var/lib/apt,sharing=private \
|
||||
<<EOF
|
||||
#!/usr/bin/env bash
|
||||
set -euxo pipefail
|
||||
|
||||
xx-apt-get install -y \
|
||||
"xx-cxx-essentials" \
|
||||
${DISTRIBUTION_PACKAGES}
|
||||
EOF
|
||||
|
||||
FROM build-prep AS build
|
||||
|
||||
ARG PACKAGE_NAME
|
||||
|
||||
ARG OUTPUT_DIR="/output"
|
||||
|
||||
ARG CARGO_BUILD_INCREMENTAL='false'
|
||||
|
||||
ENV CC='xx-clang'
|
||||
ENV CXX='xx-clang++'
|
||||
|
||||
ARG ZSTD_SYS_USE_PKG_CONFIG
|
||||
ARG LIBGIT2_STATIC
|
||||
ARG LIBSSH2_STATIC
|
||||
ARG LIBZ_SYS_STATIC
|
||||
ARG OPENSSL_STATIC
|
||||
ARG OPENSSL_NO_VENDOR
|
||||
ARG PKG_CONFIG_ALL_STATIC
|
||||
|
||||
ARG RELEASE_TAG_NAME
|
||||
|
||||
RUN \
|
||||
--mount=type=cache,target=/cargo/git/db,sharing=shared,readonly \
|
||||
--mount=type=cache,target=/cargo/registry/cache,sharing=shared,readonly \
|
||||
--mount=type=cache,target=/cargo/registry/index,sharing=shared,readonly \
|
||||
--mount=type=cache,target=/root/.cache,sharing=private \
|
||||
<<EOF
|
||||
#!/usr/bin/env bash
|
||||
set -euxo pipefail
|
||||
|
||||
xx-clang --setup-target-triple
|
||||
xx-clang --wrap
|
||||
|
||||
export RUSTFLAGS="-C linker=clang -C link-arg=-fuse-ld=/usr/bin/ld.lld"
|
||||
export PKG_CONFIG="$(xx-clang --print-prog-name=pkg-config)"
|
||||
export CARGO_BUILD_TARGET="$(xx-cargo --print-target-triple)"
|
||||
|
||||
env
|
||||
|
||||
xx-cargo build \
|
||||
--frozen \
|
||||
--package lapce-app \
|
||||
--profile release-lto \
|
||||
--no-default-features \
|
||||
--features all-languages
|
||||
|
||||
xx-verify "${CARGO_TARGET_DIR}"/"$(xx-cargo --print-target-triple)"/release-lto/lapce
|
||||
|
||||
mkdir -p /target
|
||||
mv -v "${CARGO_TARGET_DIR}"/"$(xx-cargo --print-target-triple)"/release-lto/lapce /target/
|
||||
|
||||
cargo pkgid | cut -d'#' -f2 | cut -d'@' -f2 | cut -d':' -f2 | tee /target/lapce.version
|
||||
EOF
|
||||
|
||||
WORKDIR /output
|
||||
RUN <<EOF
|
||||
#!/usr/bin/env bash
|
||||
set -euxo pipefail
|
||||
|
||||
export _PACKAGE_ARCHITECTURE=$(xx-info debian-arch)
|
||||
|
||||
mkdir -v -p ${PACKAGE_NAME}/{etc,usr/{bin,share/{applications,metainfo,pixmaps}},debian}
|
||||
cd ${PACKAGE_NAME}
|
||||
|
||||
cp '/source/extra/linux/dev.lapce.lapce.desktop' './usr/share/applications/dev.lapce.lapce.desktop'
|
||||
cp '/source/extra/linux/dev.lapce.lapce.metainfo.xml' './usr/share/metainfo/dev.lapce.lapce.metainfo.xml'
|
||||
cp '/source/extra/images/logo.png' './usr/share/pixmaps/dev.lapce.lapce.png'
|
||||
|
||||
cp '/target/lapce' './usr/bin/'
|
||||
|
||||
# single binary
|
||||
cp /target/lapce /output/lapce
|
||||
|
||||
if [[ "${PACKAGE_NAME}" == "lapce" ]]; then
|
||||
conflicts="lapce-nightly"
|
||||
else
|
||||
conflicts="lapce"
|
||||
fi
|
||||
|
||||
case "${RELEASE_TAG_NAME}" in
|
||||
nightly-*)
|
||||
version=$(cat /target/lapce.version)
|
||||
commit=$(echo "${RELEASE_TAG_NAME}" | cut -d'-' -f2)
|
||||
# date=$(date +%Y%m%d%H%M)
|
||||
RELEASE_TAG_NAME="${version}+${commit}"
|
||||
;;
|
||||
debug|nightly)
|
||||
version=$(cat /target/lapce.version)
|
||||
date=$(date +%Y%m%d%H%M)
|
||||
RELEASE_TAG_NAME="${version}+${date}"
|
||||
;;
|
||||
*)
|
||||
RELEASE_TAG_NAME="${RELEASE_TAG_NAME//v/}"
|
||||
;;
|
||||
esac
|
||||
|
||||
cat <<- EOL > debian/control
|
||||
Package: ${PACKAGE_NAME}
|
||||
Version: ${RELEASE_TAG_NAME}
|
||||
Conflicts: ${conflicts}
|
||||
Maintainer: Jakub Panek <me+debian@panekj.dev>
|
||||
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 .
|
||||
@@ -15,7 +15,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.2.8</string>
|
||||
<string>0.4.0</string>
|
||||
<key>CFBundleSupportedPlatforms</key>
|
||||
<array>
|
||||
<string>MacOSX</string>
|
||||
|
||||
@@ -8,21 +8,33 @@ test_cmd() {
|
||||
command -v "$1" >/dev/null
|
||||
}
|
||||
|
||||
tmp_dir='/tmp'
|
||||
# proxy version
|
||||
lapce_new_ver="${1}"
|
||||
# proxy directory
|
||||
# eval to resolve '~' into proper user dir
|
||||
eval lapce_dir="'${2}'"
|
||||
|
||||
if [ -e "${lapce_dir}/lapce" ]; then
|
||||
chmod +x "${lapce_dir}/lapce"
|
||||
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_ver}"
|
||||
if [ "${lapce_installed_ver}" = "${lapce_new_ver}" ]; then
|
||||
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
|
||||
@@ -43,7 +55,8 @@ case $(uname -s) in
|
||||
Linux) os_name=linux ;;
|
||||
Darwin) os_name=darwin ;;
|
||||
*)
|
||||
printf '[ERROR] unsupported os'
|
||||
printf '[ERROR] unsupported os\n'
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
@@ -53,15 +66,16 @@ case $(uname -m) in
|
||||
arm64|aarch64) arch_name=aarch64 ;;
|
||||
# riscv64) arch_name=riscv64 ;;
|
||||
*)
|
||||
printf '[ERROR] unsupported arch'
|
||||
printf '[ERROR] unsupported arch\n'
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
printf 'Switching to "%s"\n' "${tmp_dir}"
|
||||
cd "${tmp_dir}"
|
||||
lapce_download_url="https://github.com/lapce/lapce/releases/download/${lapce_new_ver_tag}/lapce-proxy-${os_name}-${arch_name}.gz"
|
||||
|
||||
lapce_new_ver_real_tag=$(echo ${lapce_new_ver} | cut -d '-' -f1)
|
||||
lapce_download_url="https://github.com/lapce/lapce/releases/download/${lapce_new_ver_real_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
|
||||
@@ -76,14 +90,11 @@ else
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf 'Creating "%s"\n' "${lapce_dir}"
|
||||
mkdir -p "${lapce_dir}"
|
||||
|
||||
printf 'Decompressing gzip\n'
|
||||
gzip -d "${tmp_dir}/lapce-proxy-${os_name}-${arch_name}.gz"
|
||||
gzip -df "${lapce_dir}/lapce-proxy-${os_name}-${arch_name}.gz"
|
||||
|
||||
printf 'Moving proxy to our dir\n'
|
||||
mv -v "${tmp_dir}/lapce-proxy-${os_name}-${arch_name}" "${lapce_dir}/lapce"
|
||||
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"
|
||||
|
||||
@@ -41,41 +41,8 @@
|
||||
},
|
||||
"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"
|
||||
}
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"required": [],
|
||||
"title": "Base"
|
||||
@@ -182,6 +149,18 @@
|
||||
},
|
||||
"tag": {
|
||||
"type": "string"
|
||||
},
|
||||
"bracket.color.1": {
|
||||
"type": "string"
|
||||
},
|
||||
"bracket.color.2": {
|
||||
"type": "string"
|
||||
},
|
||||
"bracket.color.3": {
|
||||
"type": "string"
|
||||
},
|
||||
"bracket.unpaired": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [],
|
||||
@@ -220,4 +199,4 @@
|
||||
"title": "UI"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -221,6 +221,9 @@
|
||||
"auto-closing-matching-pairs": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"auto-surround": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"hover-delay": {
|
||||
"type": "integer"
|
||||
},
|
||||
@@ -330,7 +333,7 @@
|
||||
"drop-shadow-width": {
|
||||
"type": "integer"
|
||||
},
|
||||
"preview-editor-width": {
|
||||
"palette-width": {
|
||||
"type": "integer"
|
||||
},
|
||||
"hover-font-family": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="windows-1252"?>
|
||||
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi" xmlns:util="http://schemas.microsoft.com/wix/UtilExtension">
|
||||
<Product Name="Lapce" Id="*" UpgradeCode="9c09a374-1135-4782-959f-2dec376a1dfa" Language="1033" Codepage="1252" Version="0.2.8" Manufacturer="Lapce">
|
||||
<Product Name="Lapce" Id="*" UpgradeCode="9c09a374-1135-4782-959f-2dec376a1dfa" Language="1033" Codepage="1252" Version="0.4.0" Manufacturer="Lapce">
|
||||
<Package InstallerVersion="200" Compressed="yes" InstallScope="perMachine"/>
|
||||
<MajorUpgrade AllowSameVersionUpgrades="yes" DowngradeErrorMessage="A newer version of [ProductName] is already installed."/>
|
||||
<Icon Id="lapce.exe" SourceFile=".\extra\windows\lapce.ico"/>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><path fill-rule="evenodd" clip-rule="evenodd" d="M8 9.532h.542l3.905-3.905-1.061-1.06-2.637 2.61V1H7.251v6.177l-2.637-2.61-1.061 1.06 3.905 3.905H8zm1.956 3.481a2 2 0 1 1-4 0 2 2 0 0 1 4 0z"/></svg>
|
||||
|
After Width: | Height: | Size: 301 B |
@@ -0,0 +1 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><path fill-rule="evenodd" clip-rule="evenodd" d="M8 1h-.542L3.553 4.905l1.061 1.06 2.637-2.61v6.177h1.498V3.355l2.637 2.61 1.061-1.06L8.542 1H8zm1.956 12.013a2 2 0 1 1-4 0 2 2 0 0 1 4 0z"/></svg>
|
||||
|
After Width: | Height: | Size: 298 B |
@@ -0,0 +1 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><path fill-rule="evenodd" clip-rule="evenodd" d="M14.25 5.75v-4h-1.5v2.542c-1.145-1.359-2.911-2.209-4.84-2.209-3.177 0-5.92 2.307-6.16 5.398l-.02.269h1.501l.022-.226c.212-2.195 2.202-3.94 4.656-3.94 1.736 0 3.244.875 4.05 2.166h-2.83v1.5h4.163l.962-.975V5.75h-.004zM8 14a2 2 0 1 0 0-4 2 2 0 0 0 0 4z"/></svg>
|
||||
|
After Width: | Height: | Size: 411 B |
@@ -0,0 +1 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><path d="M16 5H0V4h16v1zm0 8H0v-1h16v1zm0-4.008H0V8h16v.992z"/></svg>
|
||||
|
After Width: | Height: | Size: 172 B |
@@ -0,0 +1 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><path fill-rule="evenodd" clip-rule="evenodd" d="M12.904 9.57L8.928 5.596l3.976-3.976-.619-.62L8 5.286v.619l4.285 4.285.62-.618zM3 5.62l4.072 4.07L3 13.763l.619.618L8 10v-.619L3.619 5 3 5.619z"/></svg>
|
||||
|
After Width: | Height: | Size: 304 B |
@@ -0,0 +1,8 @@
|
||||
<svg version="1.1" id="svg2" width="2880" height="2880" viewBox="0 0 2880 2880" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs id="defs6"></defs>
|
||||
<g id="g10" transform="matrix(4.511777, 0, 0, -4.511777, -3433.283203, 6312.723633)" style="filter: none;">
|
||||
<g id="g12" transform="scale(0.1)">
|
||||
<path d="m 8680.16,13575.4 -567.42,327.6 c 0,-2068.7 -0.01,-4137.33 -0.01,-6205.98 551.01,318.11 1102.02,636.24 1653.02,954.37 V 9306.6 L 8680.16,8679.83 c 0,1631.87 0.02,3263.67 0,4895.57 z M 9768.37,9981 v 2310.9 c 861.33,-497.3 1722.73,-994.6 2584.03,-1491.9 -672.2,-388.1 -1344.4,-776.2 -2016.6,-1164.29 V 8980.5 c 861.3,497.3 1722.7,994.6 2584,1491.9 l 567.5,327.6 -567.5,327.6 c -1050.5,606.5 -2100.9,1213 -3151.43,1819.5 l -2.62,1.5 -567.43,327.6 c 0,-1208.1 0.01,-2416.2 0.01,-3624.32 z m 1447.33,835.6 -879.9,508.1 v -1016.1 z" style="fill:#1a1a1a;fill-opacity:1;fill-rule:evenodd;stroke:none" id="path14"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 944 B |
@@ -1,69 +1,85 @@
|
||||
[package]
|
||||
name = "lapce-app"
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
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
|
||||
chrono.workspace = true
|
||||
clap.workspace = true
|
||||
crossbeam-channel.workspace = true
|
||||
directories.workspace = true
|
||||
flate2.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
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
regex.workspace = true
|
||||
tar.workspace = true
|
||||
thiserror.workspace = true
|
||||
toml_edit.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
tracing-appender.workspace = true
|
||||
alacritty_terminal = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
backtrace = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
clap = { workspace = true }
|
||||
crossbeam-channel = { workspace = true }
|
||||
directories = { 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 }
|
||||
dap-types = { 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 }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
tar = { 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 }
|
||||
|
||||
Inflector = "0.11.4"
|
||||
open = "3.0.2"
|
||||
unicode-width = "0.1.10"
|
||||
fuzzy-matcher = "0.3.7"
|
||||
sled = "0.34.7"
|
||||
bytemuck = "1.8.0"
|
||||
tokio = { version = "1.21", features = ["full"] }
|
||||
futures = "0.3.26"
|
||||
floem = { git = "https://github.com/lapce/floem", rev = "57865513fc0538357ef7e0b23283423443234b9c" }
|
||||
# floem = { path = "../../workspaces/floem" }
|
||||
config = { version = "0.13.2", default-features = false, features = ["toml"] }
|
||||
structdesc = { git = "https://github.com/lapce/structdesc" }
|
||||
pulldown-cmark = "0.10.3"
|
||||
Inflector = "0.11.4"
|
||||
open = "5.1.2"
|
||||
unicode-width = "0.1.12"
|
||||
nucleo = "0.5.0"
|
||||
bytemuck = "1.15.0"
|
||||
config = { version = "=0.13.4", default-features = false, features = ["toml"] }
|
||||
structdesc = { git = "https://github.com/lapce/structdesc" }
|
||||
base64 = "0.21.7"
|
||||
sha2 = "0.10.8"
|
||||
zip = { version = "0.6.6", default-features = false, features = ["deflate"] }
|
||||
percent-encoding = { version = "2.3.1" }
|
||||
|
||||
[dependencies.floem]
|
||||
# path = "../../workspaces/floem"
|
||||
git = "https://github.com/lapce/floem"
|
||||
rev = "c9e3d6a131c63286dc634b27c38b95bde43ed90d"
|
||||
default-features = true
|
||||
features = ["editor", "serde", "default-image-formats", "rfd-async-std"]
|
||||
|
||||
[target.'cfg(target_os="macos")'.dependencies]
|
||||
fs_extra = "1.2.0"
|
||||
dmg = "0.1.1"
|
||||
dmg = "0.1.1"
|
||||
|
||||
[target.'cfg(target_os="windows")'.dependencies]
|
||||
zip = { version = "0.6.3", default-features = false, features = ["deflate"] }
|
||||
[target.'cfg(windows)'.dependencies.windows-sys]
|
||||
workspace = true
|
||||
features = ["Win32_Foundation", "Win32_System_Threading", "Win32_UI_WindowsAndMessaging"]
|
||||
|
||||
[features]
|
||||
default = ["all-languages", "updater"]
|
||||
default = ["all-languages", "updater"]
|
||||
portable = ["lapce-core/portable"]
|
||||
updater = []
|
||||
updater = []
|
||||
|
||||
# To build lapce with only some of the supported languages, for example:
|
||||
#
|
||||
@@ -71,14 +87,20 @@ updater = []
|
||||
# --features lang-rust,lang-toml,lang-markdown,lang-json
|
||||
#
|
||||
all-languages = [
|
||||
"lapce-core/lang-bash",
|
||||
"lapce-core/lang-c",
|
||||
"lapce-core/lang-cpp",
|
||||
"lapce-core/lang-javascript",
|
||||
"lapce-core/lang-json",
|
||||
"lapce-core/lang-markdown",
|
||||
"lapce-core/lang-rust",
|
||||
"lapce-core/lang-toml",
|
||||
"lapce-core/lang-yaml",
|
||||
"lapce-core/lang-bash",
|
||||
"lapce-core/lang-c",
|
||||
"lapce-core/lang-cpp",
|
||||
"lapce-core/lang-javascript",
|
||||
"lapce-core/lang-json",
|
||||
"lapce-core/lang-markdown",
|
||||
"lapce-core/lang-python",
|
||||
"lapce-core/lang-rust",
|
||||
"lapce-core/lang-toml",
|
||||
]
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = "0.5"
|
||||
|
||||
[[bench]]
|
||||
name = "visual_line"
|
||||
harness = false
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
use std::{cell::RefCell, collections::HashMap, rc::Rc, sync::Arc};
|
||||
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
use floem::{
|
||||
cosmic_text::{Attrs, AttrsList, FamilyOwned, TextLayout, Wrap},
|
||||
reactive::Scope,
|
||||
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<usize, PhantomTextLine>,
|
||||
font_family: Vec<FamilyOwned>,
|
||||
has_multiline_phantom: bool,
|
||||
}
|
||||
impl<'a> TextLayoutProvider for TLProv<'a> {
|
||||
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<TextLayoutLine> {
|
||||
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);
|
||||
|
||||
// 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;
|
||||
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);
|
||||
|
||||
// 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<usize, PhantomTextLine>,
|
||||
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);
|
||||
@@ -1,11 +1,12 @@
|
||||
use std::sync::Arc;
|
||||
use std::rc::Rc;
|
||||
|
||||
use floem::{
|
||||
event::EventListener,
|
||||
keyboard::Modifiers,
|
||||
reactive::{RwSignal, Scope},
|
||||
style::{CursorStyle, Display, Position, Style},
|
||||
view::View,
|
||||
style::{CursorStyle, Display, Position},
|
||||
views::{container, label, stack, svg, Decorators},
|
||||
View,
|
||||
};
|
||||
use lapce_core::{command::FocusCommand, meta::VERSION, mode::Mode};
|
||||
|
||||
@@ -20,11 +21,11 @@ use crate::{
|
||||
struct AboutUri {}
|
||||
|
||||
impl AboutUri {
|
||||
const LAPCE: &str = "https://lapce.dev";
|
||||
const GITHUB: &str = "https://github.com/lapce/lapce";
|
||||
const MATRIX: &str = "https://matrix.to/#/#lapce-editor:matrix.org";
|
||||
const DISCORD: &str = "https://discord.gg/n8tGJ6Rn6D";
|
||||
const CODICONS: &str = "https://github.com/microsoft/vscode-codicons";
|
||||
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)]
|
||||
@@ -53,7 +54,7 @@ impl AboutData {
|
||||
|
||||
impl KeyPressFocus for AboutData {
|
||||
fn get_mode(&self) -> Mode {
|
||||
Mode::Visual
|
||||
Mode::Insert
|
||||
}
|
||||
|
||||
fn check_condition(
|
||||
@@ -67,12 +68,13 @@ impl KeyPressFocus for AboutData {
|
||||
&self,
|
||||
command: &crate::command::LapceCommand,
|
||||
_count: Option<usize>,
|
||||
_mods: floem::glazier::Modifiers,
|
||||
_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();
|
||||
@@ -91,124 +93,120 @@ impl KeyPressFocus for AboutData {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn about_popup(window_tab_data: Arc<WindowTabData>) -> impl View {
|
||||
pub fn about_popup(window_tab_data: Rc<WindowTabData>) -> 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(move || {
|
||||
(
|
||||
svg(move || (*config.get()).logo_svg()).style(move || {
|
||||
Style::BASE.size_px(logo_size, logo_size).color(
|
||||
*config.get().get_color(LapceColor::EDITOR_FOREGROUND),
|
||||
)
|
||||
}),
|
||||
label(|| "Lapce".to_string()).style(move || {
|
||||
Style::BASE.font_bold().margin_top_px(10.0).color(
|
||||
*config.get().get_color(LapceColor::EDITOR_FOREGROUND),
|
||||
)
|
||||
}),
|
||||
label(|| format!("Version: {}", VERSION)).style(move || {
|
||||
Style::BASE
|
||||
.margin_top_px(10.0)
|
||||
.color(*config.get().get_color(LapceColor::EDITOR_DIM))
|
||||
}),
|
||||
web_link(
|
||||
|| "Website".to_string(),
|
||||
|| AboutUri::LAPCE.to_string(),
|
||||
move || *config.get().get_color(LapceColor::EDITOR_LINK),
|
||||
internal_command,
|
||||
)
|
||||
.style(|| Style::BASE.margin_top_px(20.0)),
|
||||
web_link(
|
||||
|| "GitHub".to_string(),
|
||||
|| AboutUri::GITHUB.to_string(),
|
||||
move || *config.get().get_color(LapceColor::EDITOR_LINK),
|
||||
internal_command,
|
||||
)
|
||||
.style(|| Style::BASE.margin_top_px(10.0)),
|
||||
web_link(
|
||||
|| "Discord".to_string(),
|
||||
|| AboutUri::DISCORD.to_string(),
|
||||
move || *config.get().get_color(LapceColor::EDITOR_LINK),
|
||||
internal_command,
|
||||
)
|
||||
.style(|| Style::BASE.margin_top_px(10.0)),
|
||||
web_link(
|
||||
|| "Matrix".to_string(),
|
||||
|| AboutUri::MATRIX.to_string(),
|
||||
move || *config.get().get_color(LapceColor::EDITOR_LINK),
|
||||
internal_command,
|
||||
)
|
||||
.style(|| Style::BASE.margin_top_px(10.0)),
|
||||
label(|| "Attributions".to_string()).style(move || {
|
||||
Style::BASE
|
||||
.font_bold()
|
||||
.color(*config.get().get_color(LapceColor::EDITOR_DIM))
|
||||
.margin_top_px(40.0)
|
||||
}),
|
||||
web_link(
|
||||
|| "Codicons (CC-BY-4.0)".to_string(),
|
||||
|| AboutUri::CODICONS.to_string(),
|
||||
move || *config.get().get_color(LapceColor::EDITOR_LINK),
|
||||
internal_command,
|
||||
)
|
||||
.style(|| Style::BASE.margin_top_px(10.0)),
|
||||
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(|| Style::BASE.flex_col().items_center())
|
||||
.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())
|
||||
})
|
||||
}
|
||||
|
||||
fn exclusive_popup<V: View>(
|
||||
window_tab_data: Arc<WindowTabData>,
|
||||
fn exclusive_popup<V: View + 'static>(
|
||||
window_tab_data: Rc<WindowTabData>,
|
||||
visibility: RwSignal<bool>,
|
||||
content: impl FnOnce() -> V,
|
||||
) -> impl View {
|
||||
let config = window_tab_data.common.config;
|
||||
|
||||
container(move || {
|
||||
container(move || {
|
||||
container(content)
|
||||
.style(move || {
|
||||
container(
|
||||
container(
|
||||
container(content())
|
||||
.style(move |s| {
|
||||
let config = config.get();
|
||||
Style::BASE
|
||||
.padding_vert_px(25.0)
|
||||
.padding_horiz_px(100.0)
|
||||
s.padding_vert(25.0)
|
||||
.padding_horiz(100.0)
|
||||
.border(1.0)
|
||||
.border_radius(6.0)
|
||||
.border_color(*config.get_color(LapceColor::LAPCE_BORDER))
|
||||
.background(*config.get_color(LapceColor::PANEL_BACKGROUND))
|
||||
.border_color(config.color(LapceColor::LAPCE_BORDER))
|
||||
.background(config.color(LapceColor::PANEL_BACKGROUND))
|
||||
})
|
||||
.on_event(EventListener::PointerDown, move |_| true)
|
||||
})
|
||||
.style(move || Style::BASE.flex_grow(1.0).flex_row().items_center())
|
||||
.hover_style(move || Style::BASE.cursor(CursorStyle::Default))
|
||||
})
|
||||
.on_event(EventListener::PointerDown, move |_| {
|
||||
.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();
|
||||
true
|
||||
})
|
||||
// Prevent things behind the grayed out area from being hovered.
|
||||
.on_event(EventListener::PointerMove, move |_| true)
|
||||
.style(move || {
|
||||
Style::BASE
|
||||
.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()
|
||||
.get_color(LapceColor::LAPCE_DROPDOWN_SHADOW)
|
||||
.with_alpha_factor(0.5),
|
||||
)
|
||||
.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)
|
||||
.with_alpha_factor(0.5),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -7,9 +7,9 @@ use std::{
|
||||
use floem::{
|
||||
event::EventListener,
|
||||
reactive::{ReadSignal, RwSignal, Scope},
|
||||
style::{CursorStyle, Style},
|
||||
view::View,
|
||||
views::{container, label, list, stack, svg, Decorators},
|
||||
style::CursorStyle,
|
||||
views::{container, dyn_stack, label, stack, svg, Decorators},
|
||||
View,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
@@ -41,7 +41,7 @@ pub struct AlertBoxData {
|
||||
}
|
||||
|
||||
impl AlertBoxData {
|
||||
pub fn new(cx: Scope, common: CommonData) -> Self {
|
||||
pub fn new(cx: Scope, common: Rc<CommonData>) -> Self {
|
||||
Self {
|
||||
active: cx.create_rw_signal(false),
|
||||
title: cx.create_rw_signal("".to_string()),
|
||||
@@ -60,125 +60,105 @@ pub fn alert_box(alert_data: AlertBoxData) -> impl View {
|
||||
let buttons = alert_data.buttons;
|
||||
let button_id = AtomicU64::new(0);
|
||||
|
||||
container(|| {
|
||||
container(|| {
|
||||
stack(|| {
|
||||
(
|
||||
svg(move || config.get().ui_svg(LapceIcons::WARNING)).style(
|
||||
move || {
|
||||
Style::BASE.size_px(50.0, 50.0).color(
|
||||
*config.get().get_color(LapceColor::LAPCE_WARN),
|
||||
)
|
||||
},
|
||||
),
|
||||
label(move || title.get()).style(move || {
|
||||
Style::BASE
|
||||
.margin_top_px(20.0)
|
||||
.width_pct(100.0)
|
||||
.font_bold()
|
||||
.font_size((config.get().ui.font_size() + 1) as f32)
|
||||
}),
|
||||
label(move || msg.get()).style(move || {
|
||||
Style::BASE.width_pct(100.0).margin_top_px(10.0)
|
||||
}),
|
||||
list(
|
||||
move || buttons.get(),
|
||||
move |_button| {
|
||||
button_id
|
||||
.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
|
||||
},
|
||||
move |button| {
|
||||
label(move || button.text.clone())
|
||||
.on_click(move |_| {
|
||||
(button.action)();
|
||||
true
|
||||
})
|
||||
.style(move || {
|
||||
let config = config.get();
|
||||
Style::BASE
|
||||
.margin_top_px(10.0)
|
||||
.width_pct(100.0)
|
||||
.justify_center()
|
||||
.font_size(
|
||||
(config.ui.font_size() + 1) as f32,
|
||||
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,
|
||||
),
|
||||
)
|
||||
.line_height(1.6)
|
||||
.border(1.0)
|
||||
.border_radius(6.0)
|
||||
.border_color(
|
||||
*config
|
||||
.get_color(LapceColor::LAPCE_BORDER),
|
||||
)
|
||||
})
|
||||
.hover_style(move || {
|
||||
Style::BASE
|
||||
.cursor(CursorStyle::Pointer)
|
||||
.background(*config.get().get_color(
|
||||
LapceColor::PANEL_HOVERED_BACKGROUND,
|
||||
))
|
||||
})
|
||||
.active_style(move || {
|
||||
Style::BASE.background(*config.get().get_color(
|
||||
LapceColor::PANEL_HOVERED_ACTIVE_BACKGROUND,
|
||||
))
|
||||
})
|
||||
},
|
||||
)
|
||||
.style(|| {
|
||||
Style::BASE.flex_col().width_pct(100.0).margin_top_px(10.0)
|
||||
}),
|
||||
label(|| "Cancel".to_string())
|
||||
.on_click(move |_| {
|
||||
active.set(false);
|
||||
true
|
||||
})
|
||||
.style(move || {
|
||||
let config = config.get();
|
||||
Style::BASE
|
||||
.margin_top_px(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.get_color(LapceColor::LAPCE_BORDER),
|
||||
)
|
||||
})
|
||||
.hover_style(move || {
|
||||
Style::BASE.cursor(CursorStyle::Pointer).background(
|
||||
*config
|
||||
.get()
|
||||
.get_color(LapceColor::PANEL_HOVERED_BACKGROUND),
|
||||
)
|
||||
})
|
||||
.active_style(move || {
|
||||
Style::BASE.background(*config.get().get_color(
|
||||
LapceColor::PANEL_HOVERED_ACTIVE_BACKGROUND,
|
||||
))
|
||||
}),
|
||||
})
|
||||
.active(|s| {
|
||||
s.background(config.color(
|
||||
LapceColor::PANEL_HOVERED_ACTIVE_BACKGROUND,
|
||||
))
|
||||
})
|
||||
})
|
||||
},
|
||||
)
|
||||
})
|
||||
.style(|| Style::BASE.flex_col().items_center().width_pct(100.0))
|
||||
.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(EventListener::PointerDown, |_| true)
|
||||
.style(move || {
|
||||
.on_event_stop(EventListener::PointerDown, |_| {})
|
||||
.style(move |s| {
|
||||
let config = config.get();
|
||||
Style::BASE
|
||||
.padding_px(20.0)
|
||||
.width_px(250.0)
|
||||
s.padding(20.0)
|
||||
.width(250.0)
|
||||
.border(1.0)
|
||||
.border_radius(6.0)
|
||||
.border_color(*config.get_color(LapceColor::LAPCE_BORDER))
|
||||
.color(*config.get_color(LapceColor::EDITOR_FOREGROUND))
|
||||
.background(*config.get_color(LapceColor::PANEL_BACKGROUND))
|
||||
.border_color(config.color(LapceColor::LAPCE_BORDER))
|
||||
.color(config.color(LapceColor::EDITOR_FOREGROUND))
|
||||
.background(config.color(LapceColor::PANEL_BACKGROUND))
|
||||
})
|
||||
})
|
||||
.on_event(EventListener::PointerDown, move |_| true)
|
||||
.style(move || {
|
||||
Style::BASE
|
||||
.absolute()
|
||||
.on_event_stop(EventListener::PointerDown, move |_| {})
|
||||
.style(move |s| {
|
||||
s.absolute()
|
||||
.size_pct(100.0, 100.0)
|
||||
.items_center()
|
||||
.justify_center()
|
||||
@@ -186,7 +166,7 @@ pub fn alert_box(alert_data: AlertBoxData) -> impl View {
|
||||
.background(
|
||||
config
|
||||
.get()
|
||||
.get_color(LapceColor::LAPCE_DROPDOWN_SHADOW)
|
||||
.color(LapceColor::LAPCE_DROPDOWN_SHADOW)
|
||||
.with_alpha_factor(0.5),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
use lapce_app::app;
|
||||
|
||||
pub fn main() {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::sync::Arc;
|
||||
use std::{rc::Rc, sync::Arc};
|
||||
|
||||
use floem::{
|
||||
keyboard::Modifiers,
|
||||
peniko::kurbo::Rect,
|
||||
reactive::{RwSignal, Scope},
|
||||
};
|
||||
@@ -48,7 +49,7 @@ pub struct CodeActionData {
|
||||
pub filtered_items: im::Vector<ScoredCodeActionItem>,
|
||||
pub layout_rect: Rect,
|
||||
pub mouse_click: bool,
|
||||
pub common: CommonData,
|
||||
pub common: Rc<CommonData>,
|
||||
}
|
||||
|
||||
impl KeyPressFocus for CodeActionData {
|
||||
@@ -64,12 +65,13 @@ impl KeyPressFocus for CodeActionData {
|
||||
&self,
|
||||
command: &crate::command::LapceCommand,
|
||||
_count: Option<usize>,
|
||||
_mods: floem::glazier::Modifiers,
|
||||
_mods: Modifiers,
|
||||
) -> crate::command::CommandExecuted {
|
||||
match &command.kind {
|
||||
CommandKind::Workbench(_) => {}
|
||||
CommandKind::Edit(_) => {}
|
||||
CommandKind::Move(_) => {}
|
||||
CommandKind::Scroll(_) => {}
|
||||
CommandKind::Focus(cmd) => {
|
||||
self.run_focus_command(cmd);
|
||||
}
|
||||
@@ -83,7 +85,7 @@ impl KeyPressFocus for CodeActionData {
|
||||
}
|
||||
|
||||
impl CodeActionData {
|
||||
pub fn new(cx: Scope, common: CommonData) -> Self {
|
||||
pub fn new(cx: Scope, common: Rc<CommonData>) -> Self {
|
||||
let status = cx.create_rw_signal(CodeActionStatus::Inactive);
|
||||
let active = cx.create_rw_signal(0);
|
||||
|
||||
@@ -192,7 +194,7 @@ impl CodeActionData {
|
||||
self.common.focus.set(Focus::Workbench);
|
||||
}
|
||||
|
||||
fn select(&self) {
|
||||
pub fn select(&self) {
|
||||
if let Some(item) = self.filtered_items.get(self.active.get_untracked()) {
|
||||
self.common
|
||||
.internal_command
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
use std::{path::PathBuf, rc::Rc, sync::Arc};
|
||||
|
||||
use floem::{peniko::kurbo::Vec2, reactive::RwSignal};
|
||||
use dap_types::types::{DapId, RunDebugConfig};
|
||||
pub use floem::views::editor::command::CommandExecuted;
|
||||
use floem::{
|
||||
keyboard::Modifiers, peniko::kurbo::Vec2, views::editor::command::Command,
|
||||
};
|
||||
use indexmap::IndexMap;
|
||||
use lapce_core::command::{
|
||||
EditCommand, FocusCommand, MotionModeCommand, MoveCommand, MultiSelectionCommand,
|
||||
EditCommand, FocusCommand, MotionModeCommand, MoveCommand,
|
||||
MultiSelectionCommand, ScrollCommand,
|
||||
};
|
||||
use lapce_rpc::{
|
||||
plugin::{PluginId, VoltID},
|
||||
proxy::ProxyStatus,
|
||||
terminal::{TermId, TerminalProfile},
|
||||
};
|
||||
use lapce_rpc::{dap_types::RunDebugConfig, plugin::PluginId, terminal::TermId};
|
||||
use lsp_types::{CodeActionOrCommand, Position, WorkspaceEdit};
|
||||
use serde_json::Value;
|
||||
use strum::{EnumMessage, IntoEnumIterator};
|
||||
@@ -14,7 +23,7 @@ use strum_macros::{Display, EnumIter, EnumMessage, EnumString, IntoStaticStr};
|
||||
use crate::{
|
||||
alert::AlertButton,
|
||||
debug::RunDebugMode,
|
||||
doc::Document,
|
||||
doc::Doc,
|
||||
editor::location::EditorLocation,
|
||||
editor_tab::EditorTabChild,
|
||||
id::EditorTabId,
|
||||
@@ -33,6 +42,7 @@ pub enum CommandKind {
|
||||
Workbench(LapceWorkbenchCommand),
|
||||
Edit(EditCommand),
|
||||
Move(MoveCommand),
|
||||
Scroll(ScrollCommand),
|
||||
Focus(FocusCommand),
|
||||
MotionMode(MotionModeCommand),
|
||||
MultiSelection(MultiSelectionCommand),
|
||||
@@ -44,6 +54,7 @@ impl CommandKind {
|
||||
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(),
|
||||
@@ -55,17 +66,26 @@ impl CommandKind {
|
||||
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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq)]
|
||||
pub enum CommandExecuted {
|
||||
Yes,
|
||||
No,
|
||||
impl From<Command> 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<String, LapceCommand> {
|
||||
@@ -95,6 +115,14 @@ pub fn lapce_internal_commands() -> IndexMap<String, LapceCommand> {
|
||||
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()),
|
||||
@@ -158,6 +186,10 @@ pub enum LapceWorkbenchCommand {
|
||||
#[strum(message = "Reveal Active File in File Explorer")]
|
||||
RevealActiveFileInFileExplorer,
|
||||
|
||||
#[strum(serialize = "open_ui_inspector")]
|
||||
#[strum(message = "Open Internal UI Inspector")]
|
||||
OpenUIInspector,
|
||||
|
||||
#[strum(serialize = "change_color_theme")]
|
||||
#[strum(message = "Change Color Theme")]
|
||||
ChangeColorTheme,
|
||||
@@ -178,6 +210,10 @@ pub enum LapceWorkbenchCommand {
|
||||
#[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,
|
||||
@@ -270,14 +306,16 @@ pub enum LapceWorkbenchCommand {
|
||||
#[strum(message = "Connect to SSH Host")]
|
||||
ConnectSshHost,
|
||||
|
||||
#[strum(serialize = "connect_wsl")]
|
||||
#[strum(message = "Connect to WSL")]
|
||||
ConnectWsl,
|
||||
#[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,
|
||||
|
||||
@@ -285,9 +323,11 @@ pub enum LapceWorkbenchCommand {
|
||||
#[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,
|
||||
|
||||
@@ -307,6 +347,10 @@ pub enum LapceWorkbenchCommand {
|
||||
#[strum(serialize = "palette.scm_references")]
|
||||
PaletteSCMReferences,
|
||||
|
||||
#[strum(message = "List Palette Types")]
|
||||
#[strum(serialize = "palette.palette_help")]
|
||||
PaletteHelp,
|
||||
|
||||
#[strum(message = "Run and Debug Restart Current Running")]
|
||||
#[strum(serialize = "palette.run_and_debug_restart")]
|
||||
RunAndDebugRestart,
|
||||
@@ -433,6 +477,10 @@ pub enum LapceWorkbenchCommand {
|
||||
#[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,
|
||||
@@ -486,6 +534,10 @@ pub enum LapceWorkbenchCommand {
|
||||
#[strum(serialize = "previous_error")]
|
||||
PreviousError,
|
||||
|
||||
#[strum(message = "Diff Files")]
|
||||
#[strum(serialize = "diff_files")]
|
||||
DiffFiles,
|
||||
|
||||
#[strum(serialize = "quit")]
|
||||
#[strum(message = "Quit Editor")]
|
||||
Quit,
|
||||
@@ -497,9 +549,30 @@ pub enum InternalCommand {
|
||||
OpenFile {
|
||||
path: PathBuf,
|
||||
},
|
||||
OpenFileInNewTab {
|
||||
path: PathBuf,
|
||||
},
|
||||
MakeConfirmed,
|
||||
OpenFileChanges {
|
||||
path: PathBuf,
|
||||
},
|
||||
ReloadFileExplorer,
|
||||
/// Test whether a file/directory can be created at that path
|
||||
TestPathCreation {
|
||||
new_path: Url,
|
||||
},
|
||||
FinishRenamePath {
|
||||
current_path: Url,
|
||||
new_path: Url,
|
||||
},
|
||||
FinishNewNode {
|
||||
is_dir: bool,
|
||||
path: Url,
|
||||
},
|
||||
FinishDuplicate {
|
||||
source: Url,
|
||||
path: Url,
|
||||
},
|
||||
GoToLocation {
|
||||
location: EditorLocation,
|
||||
},
|
||||
@@ -525,6 +598,9 @@ pub enum InternalCommand {
|
||||
SplitExchange {
|
||||
editor_tab_id: EditorTabId,
|
||||
},
|
||||
NewTerminal {
|
||||
profile: Option<TerminalProfile>,
|
||||
},
|
||||
SplitTerminal {
|
||||
term_id: TermId,
|
||||
},
|
||||
@@ -578,12 +654,12 @@ pub enum InternalCommand {
|
||||
FindEditorCommand {
|
||||
command: LapceCommand,
|
||||
count: Option<usize>,
|
||||
mods: floem::glazier::Modifiers,
|
||||
mods: Modifiers,
|
||||
},
|
||||
ReplaceEditorCommand {
|
||||
command: LapceCommand,
|
||||
count: Option<usize>,
|
||||
mods: floem::glazier::Modifiers,
|
||||
mods: Modifiers,
|
||||
},
|
||||
FocusEditorTab {
|
||||
editor_tab_id: EditorTabId,
|
||||
@@ -615,7 +691,29 @@ pub enum InternalCommand {
|
||||
},
|
||||
HideAlert,
|
||||
SaveScratchDoc {
|
||||
doc: RwSignal<Document>,
|
||||
doc: Rc<Doc>,
|
||||
},
|
||||
SaveScratchDoc2 {
|
||||
doc: Rc<Doc>,
|
||||
},
|
||||
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<String>,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -3,19 +3,19 @@ use std::{borrow::Cow, path::PathBuf, str::FromStr, sync::Arc};
|
||||
use floem::{
|
||||
peniko::kurbo::Rect,
|
||||
reactive::{ReadSignal, RwSignal, Scope},
|
||||
views::editor::{id::EditorId, text::Document},
|
||||
};
|
||||
use lapce_core::{
|
||||
buffer::rope_text::RopeText, movement::Movement, rope_text_pos::RopeTextPosition,
|
||||
};
|
||||
use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher};
|
||||
use lapce_core::{buffer::rope_text::RopeText, movement::Movement};
|
||||
use lapce_rpc::{plugin::PluginId, proxy::ProxyRpcHandler};
|
||||
use lsp_types::{
|
||||
CompletionItem, CompletionResponse, CompletionTextEdit, InsertTextFormat,
|
||||
Position,
|
||||
};
|
||||
use nucleo::Utf32Str;
|
||||
|
||||
use crate::{
|
||||
config::LapceConfig, doc::Document, editor::view_data::EditorViewData,
|
||||
id::EditorId, snippet::Snippet,
|
||||
};
|
||||
use crate::{config::LapceConfig, editor::EditorData, snippet::Snippet};
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub enum CompletionStatus {
|
||||
@@ -28,8 +28,8 @@ pub enum CompletionStatus {
|
||||
pub struct ScoredCompletionItem {
|
||||
pub item: CompletionItem,
|
||||
pub plugin_id: PluginId,
|
||||
pub score: i64,
|
||||
pub label_score: i64,
|
||||
pub score: u32,
|
||||
pub label_score: u32,
|
||||
pub indices: Vec<usize>,
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ pub struct CompletionData {
|
||||
/// The editor id that was most recently used to trigger a completion.
|
||||
pub latest_editor_id: Option<EditorId>,
|
||||
/// Matcher for filtering the completion items
|
||||
matcher: Arc<SkimMatcherV2>,
|
||||
matcher: RwSignal<nucleo::Matcher>,
|
||||
config: ReadSignal<Arc<LapceConfig>>,
|
||||
}
|
||||
|
||||
@@ -80,7 +80,8 @@ impl CompletionData {
|
||||
input_items: im::HashMap::new(),
|
||||
filtered_items: im::Vector::new(),
|
||||
layout_rect: Rect::ZERO,
|
||||
matcher: Arc::new(SkimMatcherV2::default().ignore_case()),
|
||||
matcher: cx
|
||||
.create_rw_signal(nucleo::Matcher::new(nucleo::Config::DEFAULT)),
|
||||
latest_editor_id: None,
|
||||
config,
|
||||
}
|
||||
@@ -179,41 +180,57 @@ impl CompletionData {
|
||||
|
||||
// Filter the items by the fuzzy matching with the input text.
|
||||
let mut items: im::Vector<ScoredCompletionItem> = 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);
|
||||
if let Some((score, mut indices)) =
|
||||
self.matcher.fuzzy_indices(filter_text, &self.input)
|
||||
{
|
||||
if shift > 0 {
|
||||
for idx in indices.iter_mut() {
|
||||
*idx += shift;
|
||||
.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
|
||||
}
|
||||
}
|
||||
let mut item = i.clone();
|
||||
item.score = score;
|
||||
item.label_score = score;
|
||||
item.indices = indices;
|
||||
if let Some(score) =
|
||||
self.matcher.fuzzy_match(&i.item.label, &self.input)
|
||||
{
|
||||
item.label_score = score;
|
||||
}
|
||||
Some(item)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.collect();
|
||||
.unwrap();
|
||||
// Sort all the items by their score, then their label score, then their length.
|
||||
items.sort_by(|a, b| {
|
||||
b.score
|
||||
@@ -282,60 +299,46 @@ impl CompletionData {
|
||||
/// Update the completion lens of the document with the active completion item.
|
||||
pub fn update_document_completion(
|
||||
&self,
|
||||
view: &EditorViewData,
|
||||
editor_data: &EditorData,
|
||||
cursor_offset: usize,
|
||||
) {
|
||||
let doc = view.doc;
|
||||
let doc = editor_data.doc();
|
||||
|
||||
if !doc.with_untracked(|doc| doc.content.is_file()) {
|
||||
if !doc.content.with_untracked(|content| content.is_file()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let config = self.config.get_untracked();
|
||||
|
||||
if !config.editor.enable_completion_lens {
|
||||
clear_completion_lens(doc);
|
||||
doc.clear_completion_lens();
|
||||
return;
|
||||
}
|
||||
|
||||
let completion_lens = doc.with_untracked(|doc| {
|
||||
completion_lens_text(
|
||||
view.rope_text(),
|
||||
cursor_offset,
|
||||
self,
|
||||
doc.completion_lens(),
|
||||
)
|
||||
});
|
||||
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) = view.offset_to_line_col(offset);
|
||||
let (line, col) = editor_data.editor.offset_to_line_col(offset);
|
||||
|
||||
doc.update(|doc| {
|
||||
doc.set_completion_lens(lens, line, col);
|
||||
});
|
||||
doc.set_completion_lens(lens, line, col);
|
||||
}
|
||||
// Unchanged
|
||||
Some(None) => {}
|
||||
None => {
|
||||
clear_completion_lens(doc);
|
||||
doc.clear_completion_lens();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear the current completion lens. Only `update`s if there is a completion lens.
|
||||
pub fn clear_completion_lens(doc: RwSignal<Document>) {
|
||||
let has_completion = doc.with_untracked(|doc| doc.completion_lens().is_some());
|
||||
if has_completion {
|
||||
doc.update(|doc| {
|
||||
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.
|
||||
@@ -356,7 +359,9 @@ fn completion_lens_text(
|
||||
.unwrap_or(InsertTextFormat::PLAIN_TEXT);
|
||||
|
||||
// We don't display insert and replace
|
||||
let CompletionTextEdit::Edit(edit) = edit else { return None };
|
||||
let CompletionTextEdit::Edit(edit) = edit else {
|
||||
return None;
|
||||
};
|
||||
// The completion offset can be different from the current cursor offset.
|
||||
let completion_offset = completion.offset;
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ use std::{
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use ::core::slice;
|
||||
use floem::peniko::Color;
|
||||
use itertools::Itertools;
|
||||
use lapce_core::directory::Directory;
|
||||
@@ -13,12 +14,14 @@ 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,
|
||||
editor::{EditorConfig, WrapStyle, SCALE_OR_SIZE_LIMIT},
|
||||
icon::LapceIcons,
|
||||
icon_theme::IconThemeConfig,
|
||||
svg::SvgStore,
|
||||
@@ -48,6 +51,38 @@ static DEFAULT_CONFIG: Lazy<config::Config> = Lazy::new(LapceConfig::default_con
|
||||
static DEFAULT_LAPCE_CONFIG: Lazy<LapceConfig> =
|
||||
Lazy::new(LapceConfig::default_lapce_config);
|
||||
|
||||
static DEFAULT_DARK_THEME_CONFIG: Lazy<config::Config> = 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<ColorThemeConfig> = Lazy::new(|| {
|
||||
let (_, theme) =
|
||||
LapceConfig::load_color_theme_from_str(DEFAULT_DARK_THEME).unwrap();
|
||||
theme.get::<ColorThemeConfig>("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<config::Config> = 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<IconThemeConfig> = Lazy::new(|| {
|
||||
DEFAULT_ICON_THEME_CONFIG.get::<IconThemeConfig>("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 {
|
||||
@@ -56,7 +91,7 @@ pub struct DropdownInfo {
|
||||
pub items: im::Vector<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
#[derive(Debug, Clone, Deserialize, Default)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct LapceConfig {
|
||||
#[serde(skip)]
|
||||
@@ -65,15 +100,13 @@ pub struct LapceConfig {
|
||||
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<String, HashMap<String, serde_json::Value>>,
|
||||
#[serde(skip)]
|
||||
pub default_color_theme: ColorThemeConfig,
|
||||
#[serde(skip)]
|
||||
pub default_icon_theme: IconThemeConfig,
|
||||
#[serde(skip)]
|
||||
pub color: ThemeColor,
|
||||
#[serde(skip)]
|
||||
pub available_color_themes: HashMap<String, (String, config::Config)>,
|
||||
@@ -90,18 +123,30 @@ pub struct LapceConfig {
|
||||
color_theme_list: im::Vector<String>,
|
||||
#[serde(skip)]
|
||||
icon_theme_list: im::Vector<String>,
|
||||
/// The couple names for the wrap style
|
||||
#[serde(skip)]
|
||||
wrap_style_list: im::Vector<String>,
|
||||
}
|
||||
|
||||
impl LapceConfig {
|
||||
pub fn load(workspace: &LapceWorkspace, disabled_volts: &[VoltID]) -> Self {
|
||||
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 = config
|
||||
.try_deserialize()
|
||||
.unwrap_or_else(|_| DEFAULT_LAPCE_CONFIG.clone());
|
||||
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);
|
||||
lapce_config.available_icon_themes = Self::load_icon_themes(disabled_volts);
|
||||
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
|
||||
@@ -120,6 +165,13 @@ impl LapceConfig {
|
||||
.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
|
||||
@@ -131,9 +183,13 @@ impl LapceConfig {
|
||||
icon_theme_config: Option<config::Config>,
|
||||
) -> 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());
|
||||
@@ -170,7 +226,7 @@ impl LapceConfig {
|
||||
}
|
||||
LapceWorkspaceType::RemoteSSH(_) => {}
|
||||
#[cfg(windows)]
|
||||
LapceWorkspaceType::RemoteWSL => {}
|
||||
LapceWorkspaceType::RemoteWSL(_) => {}
|
||||
}
|
||||
|
||||
config
|
||||
@@ -195,78 +251,66 @@ impl LapceConfig {
|
||||
|
||||
fn default_lapce_config() -> LapceConfig {
|
||||
let mut default_lapce_config: LapceConfig =
|
||||
DEFAULT_CONFIG.clone().try_deserialize().unwrap();
|
||||
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 mut default_lapce_config = DEFAULT_LAPCE_CONFIG.clone();
|
||||
if let Some((_, color_theme_config)) = self
|
||||
.available_color_themes
|
||||
.get(&self.core.color_theme.to_lowercase())
|
||||
{
|
||||
if let Ok(mut theme_lapce_config) = config::Config::builder()
|
||||
.add_source(DEFAULT_CONFIG.clone())
|
||||
.add_source(color_theme_config.clone())
|
||||
.build()
|
||||
.and_then(|theme| theme.try_deserialize::<LapceConfig>())
|
||||
{
|
||||
theme_lapce_config.resolve_colors(Some(&default_lapce_config));
|
||||
default_lapce_config = theme_lapce_config;
|
||||
}
|
||||
}
|
||||
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);
|
||||
.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);
|
||||
.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 color_theme_config.is_some() || icon_theme_config.is_some() {
|
||||
if let Ok(new) = Self::merge_config(
|
||||
workspace,
|
||||
color_theme_config.cloned(),
|
||||
icon_theme_config.cloned(),
|
||||
)
|
||||
.try_deserialize::<LapceConfig>()
|
||||
{
|
||||
self.core = new.core;
|
||||
self.ui = new.ui;
|
||||
self.editor = new.editor;
|
||||
self.terminal = new.terminal;
|
||||
self.terminal.get_indexed_colors();
|
||||
if let Ok(new) = Self::merge_config(
|
||||
workspace,
|
||||
Some(color_theme_config.clone()),
|
||||
Some(icon_theme_config.clone()),
|
||||
)
|
||||
.try_deserialize::<LapceConfig>()
|
||||
{
|
||||
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.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.default_color_theme = default_lapce_config.color_theme.clone();
|
||||
self.default_icon_theme = default_lapce_config.icon_theme.clone();
|
||||
self.update_id();
|
||||
}
|
||||
|
||||
fn load_color_themes(
|
||||
disabled_volts: &[VoltID],
|
||||
extra_plugin_paths: &[PathBuf],
|
||||
) -> HashMap<String, (String, config::Config)> {
|
||||
let mut themes = Self::load_local_themes().unwrap_or_default();
|
||||
|
||||
for (key, theme) in Self::load_plugin_color_themes(disabled_volts) {
|
||||
for (key, theme) in
|
||||
Self::load_plugin_color_themes(disabled_volts, extra_plugin_paths)
|
||||
{
|
||||
themes.insert(key, theme);
|
||||
}
|
||||
|
||||
@@ -280,6 +324,10 @@ impl LapceConfig {
|
||||
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) {
|
||||
@@ -303,16 +351,19 @@ impl LapceConfig {
|
||||
/// # 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 get_color(&self, name: &str) -> &Color {
|
||||
self.color
|
||||
.ui
|
||||
.get(name)
|
||||
.unwrap_or_else(|| panic!("Key not found: {name}"))
|
||||
pub fn color(&self, name: &str) -> Color {
|
||||
match self.color.ui.get(name) {
|
||||
Some(c) => *c,
|
||||
None => {
|
||||
error!("Failed to find key: {name}");
|
||||
Color::HOT_PINK
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieve a color value whose key starts with "style."
|
||||
pub fn get_style_color(&self, name: &str) -> Option<&Color> {
|
||||
self.color.syntax.get(name)
|
||||
pub fn style_color(&self, name: &str) -> Option<Color> {
|
||||
self.color.syntax.get(name).copied()
|
||||
}
|
||||
|
||||
pub fn completion_color(
|
||||
@@ -338,14 +389,14 @@ impl LapceConfig {
|
||||
_ => "string",
|
||||
};
|
||||
|
||||
self.get_style_color(theme_str).cloned()
|
||||
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.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));
|
||||
@@ -354,8 +405,8 @@ impl LapceConfig {
|
||||
default_config.map(|c| &c.color.syntax),
|
||||
);
|
||||
|
||||
let fg = self.get_color(LapceColor::EDITOR_FOREGROUND);
|
||||
let bg = self.get_color(LapceColor::EDITOR_BACKGROUND);
|
||||
let fg = self.color(LapceColor::EDITOR_FOREGROUND);
|
||||
let bg = self.color(LapceColor::EDITOR_BACKGROUND);
|
||||
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);
|
||||
@@ -394,6 +445,8 @@ impl LapceConfig {
|
||||
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))
|
||||
@@ -406,11 +459,12 @@ impl LapceConfig {
|
||||
|
||||
fn load_icon_themes(
|
||||
disabled_volts: &[VoltID],
|
||||
extra_plugin_paths: &[PathBuf],
|
||||
) -> HashMap<String, (String, config::Config, Option<PathBuf>)> {
|
||||
let mut themes = HashMap::new();
|
||||
|
||||
for (key, (name, theme, path)) in
|
||||
Self::load_plugin_icon_themes(disabled_volts)
|
||||
Self::load_plugin_icon_themes(disabled_volts, extra_plugin_paths)
|
||||
{
|
||||
themes.insert(key, (name, theme, Some(path)));
|
||||
}
|
||||
@@ -434,9 +488,10 @@ impl LapceConfig {
|
||||
|
||||
fn load_plugin_color_themes(
|
||||
disabled_volts: &[VoltID],
|
||||
extra_plugin_paths: &[PathBuf],
|
||||
) -> HashMap<String, (String, config::Config)> {
|
||||
let mut themes: HashMap<String, (String, config::Config)> = HashMap::new();
|
||||
for meta in find_all_volts() {
|
||||
for meta in find_all_volts(extra_plugin_paths) {
|
||||
if disabled_volts.contains(&meta.id()) {
|
||||
continue;
|
||||
}
|
||||
@@ -455,10 +510,11 @@ impl LapceConfig {
|
||||
|
||||
fn load_plugin_icon_themes(
|
||||
disabled_volts: &[VoltID],
|
||||
extra_plugin_paths: &[PathBuf],
|
||||
) -> HashMap<String, (String, config::Config, PathBuf)> {
|
||||
let mut themes: HashMap<String, (String, config::Config, PathBuf)> =
|
||||
HashMap::new();
|
||||
for meta in find_all_volts() {
|
||||
for meta in find_all_volts(extra_plugin_paths) {
|
||||
if disabled_volts.contains(&meta.id()) {
|
||||
continue;
|
||||
}
|
||||
@@ -493,6 +549,19 @@ impl LapceConfig {
|
||||
))
|
||||
}
|
||||
|
||||
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<PathBuf> {
|
||||
let path = Directory::config_directory()?.join("settings.toml");
|
||||
|
||||
@@ -526,19 +595,20 @@ impl LapceConfig {
|
||||
});
|
||||
|
||||
svg.unwrap_or_else(|| {
|
||||
let name = self.default_icon_theme.ui.get(icon).unwrap();
|
||||
let name = DEFAULT_ICON_THEME_ICON_CONFIG.ui.get(icon).unwrap();
|
||||
self.svg_store.write().get_default_svg(name)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn file_svg(&self, path: &Path) -> (String, Option<&Color>) {
|
||||
pub fn files_svg(&self, paths: &[&Path]) -> (String, Option<Color>) {
|
||||
let svg = self
|
||||
.icon_theme
|
||||
.resolve_path_to_icon(path)
|
||||
.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.get_color(LapceColor::LAPCE_ICON_ACTIVE))
|
||||
Some(self.color(LapceColor::LAPCE_ICON_ACTIVE))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -546,11 +616,15 @@ impl LapceConfig {
|
||||
} else {
|
||||
(
|
||||
self.ui_svg(LapceIcons::FILE),
|
||||
Some(self.get_color(LapceColor::LAPCE_ICON_ACTIVE)),
|
||||
Some(self.color(LapceColor::LAPCE_ICON_ACTIVE)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn file_svg(&self, path: &Path) -> (String, Option<Color>) {
|
||||
self.files_svg(slice::from_ref(&path))
|
||||
}
|
||||
|
||||
pub fn symbol_svg(&self, kind: &SymbolKind) -> Option<String> {
|
||||
let kind_str = match *kind {
|
||||
SymbolKind::ARRAY => LapceIcons::SYMBOL_KIND_ARRAY,
|
||||
@@ -612,8 +686,17 @@ impl LapceConfig {
|
||||
}
|
||||
|
||||
pub fn terminal_line_height(&self) -> usize {
|
||||
if self.terminal.line_height > 0 {
|
||||
self.terminal.line_height
|
||||
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()
|
||||
}
|
||||
@@ -621,37 +704,37 @@ impl LapceConfig {
|
||||
|
||||
pub fn terminal_get_color(
|
||||
&self,
|
||||
color: &alacritty_terminal::ansi::Color,
|
||||
color: &alacritty_terminal::vte::ansi::Color,
|
||||
colors: &alacritty_terminal::term::color::Colors,
|
||||
) -> Color {
|
||||
match color {
|
||||
alacritty_terminal::ansi::Color::Named(color) => {
|
||||
alacritty_terminal::vte::ansi::Color::Named(color) => {
|
||||
self.terminal_get_named_color(color)
|
||||
}
|
||||
alacritty_terminal::ansi::Color::Spec(rgb) => {
|
||||
alacritty_terminal::vte::ansi::Color::Spec(rgb) => {
|
||||
Color::rgb8(rgb.r, rgb.g, rgb.b)
|
||||
}
|
||||
alacritty_terminal::ansi::Color::Indexed(index) => {
|
||||
alacritty_terminal::vte::ansi::Color::Indexed(index) => {
|
||||
if let Some(rgb) = colors[*index as usize] {
|
||||
return Color::rgb8(rgb.r, rgb.g, rgb.b);
|
||||
}
|
||||
const NAMED_COLORS: [alacritty_terminal::ansi::NamedColor; 16] = [
|
||||
alacritty_terminal::ansi::NamedColor::Black,
|
||||
alacritty_terminal::ansi::NamedColor::Red,
|
||||
alacritty_terminal::ansi::NamedColor::Green,
|
||||
alacritty_terminal::ansi::NamedColor::Yellow,
|
||||
alacritty_terminal::ansi::NamedColor::Blue,
|
||||
alacritty_terminal::ansi::NamedColor::Magenta,
|
||||
alacritty_terminal::ansi::NamedColor::Cyan,
|
||||
alacritty_terminal::ansi::NamedColor::White,
|
||||
alacritty_terminal::ansi::NamedColor::BrightBlack,
|
||||
alacritty_terminal::ansi::NamedColor::BrightRed,
|
||||
alacritty_terminal::ansi::NamedColor::BrightGreen,
|
||||
alacritty_terminal::ansi::NamedColor::BrightYellow,
|
||||
alacritty_terminal::ansi::NamedColor::BrightBlue,
|
||||
alacritty_terminal::ansi::NamedColor::BrightMagenta,
|
||||
alacritty_terminal::ansi::NamedColor::BrightCyan,
|
||||
alacritty_terminal::ansi::NamedColor::BrightWhite,
|
||||
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])
|
||||
@@ -664,98 +747,98 @@ impl LapceConfig {
|
||||
|
||||
fn terminal_get_named_color(
|
||||
&self,
|
||||
color: &alacritty_terminal::ansi::NamedColor,
|
||||
color: &alacritty_terminal::vte::ansi::NamedColor,
|
||||
) -> Color {
|
||||
let (color, alpha) = match color {
|
||||
alacritty_terminal::ansi::NamedColor::Cursor => {
|
||||
alacritty_terminal::vte::ansi::NamedColor::Cursor => {
|
||||
(LapceColor::TERMINAL_CURSOR, 1.0)
|
||||
}
|
||||
alacritty_terminal::ansi::NamedColor::Foreground => {
|
||||
alacritty_terminal::vte::ansi::NamedColor::Foreground => {
|
||||
(LapceColor::TERMINAL_FOREGROUND, 1.0)
|
||||
}
|
||||
alacritty_terminal::ansi::NamedColor::Background => {
|
||||
alacritty_terminal::vte::ansi::NamedColor::Background => {
|
||||
(LapceColor::TERMINAL_BACKGROUND, 1.0)
|
||||
}
|
||||
alacritty_terminal::ansi::NamedColor::Blue => {
|
||||
alacritty_terminal::vte::ansi::NamedColor::Blue => {
|
||||
(LapceColor::TERMINAL_BLUE, 1.0)
|
||||
}
|
||||
alacritty_terminal::ansi::NamedColor::Green => {
|
||||
alacritty_terminal::vte::ansi::NamedColor::Green => {
|
||||
(LapceColor::TERMINAL_GREEN, 1.0)
|
||||
}
|
||||
alacritty_terminal::ansi::NamedColor::Yellow => {
|
||||
alacritty_terminal::vte::ansi::NamedColor::Yellow => {
|
||||
(LapceColor::TERMINAL_YELLOW, 1.0)
|
||||
}
|
||||
alacritty_terminal::ansi::NamedColor::Red => {
|
||||
alacritty_terminal::vte::ansi::NamedColor::Red => {
|
||||
(LapceColor::TERMINAL_RED, 1.0)
|
||||
}
|
||||
alacritty_terminal::ansi::NamedColor::White => {
|
||||
alacritty_terminal::vte::ansi::NamedColor::White => {
|
||||
(LapceColor::TERMINAL_WHITE, 1.0)
|
||||
}
|
||||
alacritty_terminal::ansi::NamedColor::Black => {
|
||||
alacritty_terminal::vte::ansi::NamedColor::Black => {
|
||||
(LapceColor::TERMINAL_BLACK, 1.0)
|
||||
}
|
||||
alacritty_terminal::ansi::NamedColor::Cyan => {
|
||||
alacritty_terminal::vte::ansi::NamedColor::Cyan => {
|
||||
(LapceColor::TERMINAL_CYAN, 1.0)
|
||||
}
|
||||
alacritty_terminal::ansi::NamedColor::Magenta => {
|
||||
alacritty_terminal::vte::ansi::NamedColor::Magenta => {
|
||||
(LapceColor::TERMINAL_MAGENTA, 1.0)
|
||||
}
|
||||
alacritty_terminal::ansi::NamedColor::BrightBlue => {
|
||||
alacritty_terminal::vte::ansi::NamedColor::BrightBlue => {
|
||||
(LapceColor::TERMINAL_BRIGHT_BLUE, 1.0)
|
||||
}
|
||||
alacritty_terminal::ansi::NamedColor::BrightGreen => {
|
||||
alacritty_terminal::vte::ansi::NamedColor::BrightGreen => {
|
||||
(LapceColor::TERMINAL_BRIGHT_GREEN, 1.0)
|
||||
}
|
||||
alacritty_terminal::ansi::NamedColor::BrightYellow => {
|
||||
alacritty_terminal::vte::ansi::NamedColor::BrightYellow => {
|
||||
(LapceColor::TERMINAL_BRIGHT_YELLOW, 1.0)
|
||||
}
|
||||
alacritty_terminal::ansi::NamedColor::BrightRed => {
|
||||
alacritty_terminal::vte::ansi::NamedColor::BrightRed => {
|
||||
(LapceColor::TERMINAL_BRIGHT_RED, 1.0)
|
||||
}
|
||||
alacritty_terminal::ansi::NamedColor::BrightWhite => {
|
||||
alacritty_terminal::vte::ansi::NamedColor::BrightWhite => {
|
||||
(LapceColor::TERMINAL_BRIGHT_WHITE, 1.0)
|
||||
}
|
||||
alacritty_terminal::ansi::NamedColor::BrightBlack => {
|
||||
alacritty_terminal::vte::ansi::NamedColor::BrightBlack => {
|
||||
(LapceColor::TERMINAL_BRIGHT_BLACK, 1.0)
|
||||
}
|
||||
alacritty_terminal::ansi::NamedColor::BrightCyan => {
|
||||
alacritty_terminal::vte::ansi::NamedColor::BrightCyan => {
|
||||
(LapceColor::TERMINAL_BRIGHT_CYAN, 1.0)
|
||||
}
|
||||
alacritty_terminal::ansi::NamedColor::BrightMagenta => {
|
||||
alacritty_terminal::vte::ansi::NamedColor::BrightMagenta => {
|
||||
(LapceColor::TERMINAL_BRIGHT_MAGENTA, 1.0)
|
||||
}
|
||||
alacritty_terminal::ansi::NamedColor::BrightForeground => {
|
||||
alacritty_terminal::vte::ansi::NamedColor::BrightForeground => {
|
||||
(LapceColor::TERMINAL_FOREGROUND, 1.0)
|
||||
}
|
||||
alacritty_terminal::ansi::NamedColor::DimBlack => {
|
||||
alacritty_terminal::vte::ansi::NamedColor::DimBlack => {
|
||||
(LapceColor::TERMINAL_BLACK, 0.66)
|
||||
}
|
||||
alacritty_terminal::ansi::NamedColor::DimRed => {
|
||||
alacritty_terminal::vte::ansi::NamedColor::DimRed => {
|
||||
(LapceColor::TERMINAL_RED, 0.66)
|
||||
}
|
||||
alacritty_terminal::ansi::NamedColor::DimGreen => {
|
||||
alacritty_terminal::vte::ansi::NamedColor::DimGreen => {
|
||||
(LapceColor::TERMINAL_GREEN, 0.66)
|
||||
}
|
||||
alacritty_terminal::ansi::NamedColor::DimYellow => {
|
||||
alacritty_terminal::vte::ansi::NamedColor::DimYellow => {
|
||||
(LapceColor::TERMINAL_YELLOW, 0.66)
|
||||
}
|
||||
alacritty_terminal::ansi::NamedColor::DimBlue => {
|
||||
alacritty_terminal::vte::ansi::NamedColor::DimBlue => {
|
||||
(LapceColor::TERMINAL_BLUE, 0.66)
|
||||
}
|
||||
alacritty_terminal::ansi::NamedColor::DimMagenta => {
|
||||
alacritty_terminal::vte::ansi::NamedColor::DimMagenta => {
|
||||
(LapceColor::TERMINAL_MAGENTA, 0.66)
|
||||
}
|
||||
alacritty_terminal::ansi::NamedColor::DimCyan => {
|
||||
alacritty_terminal::vte::ansi::NamedColor::DimCyan => {
|
||||
(LapceColor::TERMINAL_CYAN, 0.66)
|
||||
}
|
||||
alacritty_terminal::ansi::NamedColor::DimWhite => {
|
||||
alacritty_terminal::vte::ansi::NamedColor::DimWhite => {
|
||||
(LapceColor::TERMINAL_WHITE, 0.66)
|
||||
}
|
||||
alacritty_terminal::ansi::NamedColor::DimForeground => {
|
||||
alacritty_terminal::vte::ansi::NamedColor::DimForeground => {
|
||||
(LapceColor::TERMINAL_FOREGROUND, 0.66)
|
||||
}
|
||||
};
|
||||
(*self.get_color(color)).with_alpha_factor(alpha)
|
||||
self.color(color).with_alpha_factor(alpha)
|
||||
}
|
||||
|
||||
/// Get the dropdown information for the specific setting, used for the settings UI.
|
||||
@@ -779,6 +862,42 @@ impl LapceConfig {
|
||||
.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(),
|
||||
}),
|
||||
("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,
|
||||
}
|
||||
}
|
||||
@@ -790,6 +909,30 @@ impl LapceConfig {
|
||||
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(
|
||||
@@ -797,6 +940,16 @@ impl LapceConfig {
|
||||
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
|
||||
|
||||
@@ -11,6 +11,10 @@ pub enum LoadThemeError {
|
||||
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),
|
||||
}
|
||||
@@ -18,138 +22,160 @@ pub enum LoadThemeError {
|
||||
pub struct LapceColor {}
|
||||
|
||||
impl LapceColor {
|
||||
pub const LAPCE_WARN: &str = "lapce.warn";
|
||||
pub const LAPCE_ERROR: &str = "lapce.error";
|
||||
pub const LAPCE_DROPDOWN_SHADOW: &str = "lapce.dropdown_shadow";
|
||||
pub const LAPCE_BORDER: &str = "lapce.border";
|
||||
pub const LAPCE_SCROLL_BAR: &str = "lapce.scroll_bar";
|
||||
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: &str =
|
||||
pub const LAPCE_BUTTON_PRIMARY_BACKGROUND: &'static str =
|
||||
"lapce.button.primary.background";
|
||||
pub const LAPCE_BUTTON_PRIMARY_FOREGROUND: &str =
|
||||
pub const LAPCE_BUTTON_PRIMARY_FOREGROUND: &'static str =
|
||||
"lapce.button.primary.foreground";
|
||||
|
||||
pub const LAPCE_TAB_ACTIVE_BACKGROUND: &str = "lapce.tab.active.background";
|
||||
pub const LAPCE_TAB_ACTIVE_FOREGROUND: &str = "lapce.tab.active.foreground";
|
||||
pub const LAPCE_TAB_ACTIVE_UNDERLINE: &str = "lapce.tab.active.underline";
|
||||
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: &str = "lapce.tab.inactive.background";
|
||||
pub const LAPCE_TAB_INACTIVE_FOREGROUND: &str = "lapce.tab.inactive.foreground";
|
||||
pub const LAPCE_TAB_INACTIVE_UNDERLINE: &str = "lapce.tab.inactive.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: &str = "lapce.tab.separator";
|
||||
pub const LAPCE_TAB_SEPARATOR: &'static str = "lapce.tab.separator";
|
||||
|
||||
pub const LAPCE_ICON_ACTIVE: &str = "lapce.icon.active";
|
||||
pub const LAPCE_ICON_INACTIVE: &str = "lapce.icon.inactive";
|
||||
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: &str = "lapce.remote.icon";
|
||||
pub const LAPCE_REMOTE_LOCAL: &str = "lapce.remote.local";
|
||||
pub const LAPCE_REMOTE_CONNECTED: &str = "lapce.remote.connected";
|
||||
pub const LAPCE_REMOTE_CONNECTING: &str = "lapce.remote.connecting";
|
||||
pub const LAPCE_REMOTE_DISCONNECTED: &str = "lapce.remote.disconnected";
|
||||
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: &str = "lapce.plugin.name";
|
||||
pub const LAPCE_PLUGIN_DESCRIPTION: &str = "lapce.plugin.description";
|
||||
pub const LAPCE_PLUGIN_AUTHOR: &str = "lapce.plugin.author";
|
||||
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: &str = "editor.background";
|
||||
pub const EDITOR_FOREGROUND: &str = "editor.foreground";
|
||||
pub const EDITOR_DIM: &str = "editor.dim";
|
||||
pub const EDITOR_FOCUS: &str = "editor.focus";
|
||||
pub const EDITOR_CARET: &str = "editor.caret";
|
||||
pub const EDITOR_SELECTION: &str = "editor.selection";
|
||||
pub const EDITOR_CURRENT_LINE: &str = "editor.current_line";
|
||||
pub const EDITOR_LINK: &str = "editor.link";
|
||||
pub const EDITOR_VISIBLE_WHITESPACE: &str = "editor.visible_whitespace";
|
||||
pub const EDITOR_INDENT_GUIDE: &str = "editor.indent_guide";
|
||||
pub const EDITOR_DRAG_DROP_BACKGROUND: &str = "editor.drag_drop_background";
|
||||
pub const EDITOR_STICKY_HEADER_BACKGROUND: &str =
|
||||
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: &str =
|
||||
pub const EDITOR_DRAG_DROP_TAB_BACKGROUND: &'static str =
|
||||
"editor.drag_drop_tab_background";
|
||||
|
||||
pub const INLAY_HINT_FOREGROUND: &str = "inlay_hint.foreground";
|
||||
pub const INLAY_HINT_BACKGROUND: &str = "inlay_hint.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: &str = "error_lens.error.foreground";
|
||||
pub const ERROR_LENS_ERROR_BACKGROUND: &str = "error_lens.error.background";
|
||||
pub const ERROR_LENS_WARNING_FOREGROUND: &str = "error_lens.warning.foreground";
|
||||
pub const ERROR_LENS_WARNING_BACKGROUND: &str = "error_lens.warning.background";
|
||||
pub const ERROR_LENS_OTHER_FOREGROUND: &str = "error_lens.other.foreground";
|
||||
pub const ERROR_LENS_OTHER_BACKGROUND: &str = "error_lens.other.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: &str = "completion_lens.foreground";
|
||||
pub const COMPLETION_LENS_FOREGROUND: &'static str =
|
||||
"completion_lens.foreground";
|
||||
|
||||
pub const SOURCE_CONTROL_ADDED: &str = "source_control.added";
|
||||
pub const SOURCE_CONTROL_REMOVED: &str = "source_control.removed";
|
||||
pub const SOURCE_CONTROL_MODIFIED: &str = "source_control.modified";
|
||||
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: &str = "terminal.cursor";
|
||||
pub const TERMINAL_BACKGROUND: &str = "terminal.background";
|
||||
pub const TERMINAL_FOREGROUND: &str = "terminal.foreground";
|
||||
pub const TERMINAL_RED: &str = "terminal.red";
|
||||
pub const TERMINAL_BLUE: &str = "terminal.blue";
|
||||
pub const TERMINAL_GREEN: &str = "terminal.green";
|
||||
pub const TERMINAL_YELLOW: &str = "terminal.yellow";
|
||||
pub const TERMINAL_BLACK: &str = "terminal.black";
|
||||
pub const TERMINAL_WHITE: &str = "terminal.white";
|
||||
pub const TERMINAL_CYAN: &str = "terminal.cyan";
|
||||
pub const TERMINAL_MAGENTA: &str = "terminal.magenta";
|
||||
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: &str = "terminal.bright_red";
|
||||
pub const TERMINAL_BRIGHT_BLUE: &str = "terminal.bright_blue";
|
||||
pub const TERMINAL_BRIGHT_GREEN: &str = "terminal.bright_green";
|
||||
pub const TERMINAL_BRIGHT_YELLOW: &str = "terminal.bright_yellow";
|
||||
pub const TERMINAL_BRIGHT_BLACK: &str = "terminal.bright_black";
|
||||
pub const TERMINAL_BRIGHT_WHITE: &str = "terminal.bright_white";
|
||||
pub const TERMINAL_BRIGHT_CYAN: &str = "terminal.bright_cyan";
|
||||
pub const TERMINAL_BRIGHT_MAGENTA: &str = "terminal.bright_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: &str = "palette.background";
|
||||
pub const PALETTE_FOREGROUND: &str = "palette.foreground";
|
||||
pub const PALETTE_CURRENT_BACKGROUND: &str = "palette.current.background";
|
||||
pub const PALETTE_CURRENT_FOREGROUND: &str = "palette.current.foreground";
|
||||
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: &str = "completion.background";
|
||||
pub const COMPLETION_CURRENT: &str = "completion.current";
|
||||
pub const COMPLETION_BACKGROUND: &'static str = "completion.background";
|
||||
pub const COMPLETION_CURRENT: &'static str = "completion.current";
|
||||
|
||||
pub const HOVER_BACKGROUND: &str = "hover.background";
|
||||
pub const HOVER_BACKGROUND: &'static str = "hover.background";
|
||||
|
||||
pub const ACTIVITY_BACKGROUND: &str = "activity.background";
|
||||
pub const ACTIVITY_CURRENT: &str = "activity.current";
|
||||
pub const ACTIVITY_BACKGROUND: &'static str = "activity.background";
|
||||
pub const ACTIVITY_CURRENT: &'static str = "activity.current";
|
||||
|
||||
pub const DEBUG_BREAKPOINT: &str = "debug.breakpoint";
|
||||
pub const DEBUG_BREAKPOINT_HOVER: &str = "debug.breakpoint.hover";
|
||||
pub const DEBUG_BREAKPOINT: &'static str = "debug.breakpoint";
|
||||
pub const DEBUG_BREAKPOINT_HOVER: &'static str = "debug.breakpoint.hover";
|
||||
|
||||
pub const PANEL_BACKGROUND: &str = "panel.background";
|
||||
pub const PANEL_FOREGROUND: &str = "panel.foreground";
|
||||
pub const PANEL_FOREGROUND_DIM: &str = "panel.foreground.dim";
|
||||
pub const PANEL_CURRENT_BACKGROUND: &str = "panel.current.background";
|
||||
pub const PANEL_CURRENT_FOREGROUND: &str = "panel.current.foreground";
|
||||
pub const PANEL_CURRENT_FOREGROUND_DIM: &str = "panel.current.foreground.dim";
|
||||
pub const PANEL_HOVERED_BACKGROUND: &str = "panel.hovered.background";
|
||||
pub const PANEL_HOVERED_ACTIVE_BACKGROUND: &str =
|
||||
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: &str = "panel.hovered.foreground";
|
||||
pub const PANEL_HOVERED_FOREGROUND_DIM: &str = "panel.hovered.foreground.dim";
|
||||
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: &str = "status.background";
|
||||
pub const STATUS_FOREGROUND: &str = "status.foreground";
|
||||
pub const STATUS_MODAL_NORMAL_BACKGROUND: &str =
|
||||
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: &str =
|
||||
pub const STATUS_MODAL_NORMAL_FOREGROUND: &'static str =
|
||||
"status.modal.normal.foreground";
|
||||
pub const STATUS_MODAL_INSERT_BACKGROUND: &str =
|
||||
pub const STATUS_MODAL_INSERT_BACKGROUND: &'static str =
|
||||
"status.modal.insert.background";
|
||||
pub const STATUS_MODAL_INSERT_FOREGROUND: &str =
|
||||
pub const STATUS_MODAL_INSERT_FOREGROUND: &'static str =
|
||||
"status.modal.insert.foreground";
|
||||
pub const STATUS_MODAL_VISUAL_BACKGROUND: &str =
|
||||
pub const STATUS_MODAL_VISUAL_BACKGROUND: &'static str =
|
||||
"status.modal.visual.background";
|
||||
pub const STATUS_MODAL_VISUAL_FOREGROUND: &str =
|
||||
pub const STATUS_MODAL_VISUAL_FOREGROUND: &'static str =
|
||||
"status.modal.visual.foreground";
|
||||
pub const STATUS_MODAL_TERMINAL_BACKGROUND: &str =
|
||||
pub const STATUS_MODAL_TERMINAL_BACKGROUND: &'static str =
|
||||
"status.modal.terminal.background";
|
||||
pub const STATUS_MODAL_TERMINAL_FOREGROUND: &str =
|
||||
pub const STATUS_MODAL_TERMINAL_FOREGROUND: &'static str =
|
||||
"status.modal.terminal.foreground";
|
||||
|
||||
pub const MARKDOWN_BLOCKQUOTE: &'static str = "markdown.blockquote";
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
use std::{
|
||||
collections::{BTreeMap, HashMap},
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
use floem::peniko::Color;
|
||||
use indexmap::IndexMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::color::LoadThemeError;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub enum ThemeColorPreference {
|
||||
#[default]
|
||||
@@ -13,65 +17,17 @@ pub enum ThemeColorPreference {
|
||||
HighContrastLight,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ThemeBaseColor {
|
||||
pub white: Color,
|
||||
pub black: Color,
|
||||
pub grey: Color,
|
||||
pub blue: Color,
|
||||
pub red: Color,
|
||||
pub yellow: Color,
|
||||
pub orange: Color,
|
||||
pub green: Color,
|
||||
pub purple: Color,
|
||||
pub cyan: Color,
|
||||
pub magenta: Color,
|
||||
}
|
||||
|
||||
impl Default for ThemeBaseColor {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
white: Color::rgb8(0, 0, 0),
|
||||
black: Color::rgb8(0, 0, 0),
|
||||
grey: Color::rgb8(0, 0, 0),
|
||||
blue: Color::rgb8(0, 0, 0),
|
||||
red: Color::rgb8(0, 0, 0),
|
||||
yellow: Color::rgb8(0, 0, 0),
|
||||
orange: Color::rgb8(0, 0, 0),
|
||||
green: Color::rgb8(0, 0, 0),
|
||||
purple: Color::rgb8(0, 0, 0),
|
||||
cyan: Color::rgb8(0, 0, 0),
|
||||
magenta: Color::rgb8(0, 0, 0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Holds all the resolved theme variables
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ThemeBaseColor(HashMap<String, Color>);
|
||||
impl ThemeBaseColor {
|
||||
pub fn get(&self, name: &str) -> Option<&Color> {
|
||||
Some(match name {
|
||||
"white" => &self.white,
|
||||
"black" => &self.black,
|
||||
"grey" => &self.grey,
|
||||
"blue" => &self.blue,
|
||||
"red" => &self.red,
|
||||
"yellow" => &self.yellow,
|
||||
"orange" => &self.orange,
|
||||
"green" => &self.green,
|
||||
"purple" => &self.purple,
|
||||
"cyan" => &self.cyan,
|
||||
"magenta" => &self.magenta,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn keys(&self) -> [&'static str; 11] {
|
||||
[
|
||||
"white", "black", "grey", "blue", "red", "yellow", "orange", "green",
|
||||
"purple", "cyan", "magenta",
|
||||
]
|
||||
pub fn get(&self, name: &str) -> Option<Color> {
|
||||
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,
|
||||
@@ -81,71 +37,108 @@ pub struct ThemeColor {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
|
||||
pub struct ThemeBaseConfig {
|
||||
pub white: String,
|
||||
pub black: String,
|
||||
pub grey: String,
|
||||
pub blue: String,
|
||||
pub red: String,
|
||||
pub yellow: String,
|
||||
pub orange: String,
|
||||
pub green: String,
|
||||
pub purple: String,
|
||||
pub cyan: String,
|
||||
pub magenta: String,
|
||||
}
|
||||
pub struct ThemeBaseConfig(BTreeMap<String, String>);
|
||||
|
||||
impl ThemeBaseConfig {
|
||||
pub fn resolve(&self, default: Option<&ThemeBaseColor>) -> ThemeBaseColor {
|
||||
/// 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();
|
||||
ThemeBaseColor {
|
||||
white: Color::parse(&self.white).unwrap_or(default.white),
|
||||
black: Color::parse(&self.black).unwrap_or(default.black),
|
||||
grey: Color::parse(&self.grey).unwrap_or(default.grey),
|
||||
blue: Color::parse(&self.blue).unwrap_or(default.blue),
|
||||
red: Color::parse(&self.red).unwrap_or(default.red),
|
||||
yellow: Color::parse(&self.yellow).unwrap_or(default.yellow),
|
||||
orange: Color::parse(&self.orange).unwrap_or(default.orange),
|
||||
green: Color::parse(&self.green).unwrap_or(default.green),
|
||||
purple: Color::parse(&self.purple).unwrap_or(default.purple),
|
||||
cyan: Color::parse(&self.cyan).unwrap_or(default.cyan),
|
||||
magenta: Color::parse(&self.magenta).unwrap_or(default.magenta),
|
||||
|
||||
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::parse(color)
|
||||
.unwrap_or_else(|| {
|
||||
tracing::warn!(
|
||||
"Failed to parse color theme variable for ({key}: {value})"
|
||||
);
|
||||
Color::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<Option<&'a str>, 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> {
|
||||
Some(match name {
|
||||
"white" => &self.white,
|
||||
"black" => &self.black,
|
||||
"grey" => &self.grey,
|
||||
"blue" => &self.blue,
|
||||
"red" => &self.red,
|
||||
"yellow" => &self.yellow,
|
||||
"orange" => &self.orange,
|
||||
"green" => &self.green,
|
||||
"purple" => &self.purple,
|
||||
"cyan" => &self.cyan,
|
||||
"magenta" => &self.magenta,
|
||||
_ => return None,
|
||||
})
|
||||
self.0.get(name)
|
||||
}
|
||||
|
||||
pub fn key_values(&self) -> BTreeMap<String, String> {
|
||||
self.0.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[serde(rename_all = "kebab-case", default)]
|
||||
pub struct ColorThemeConfig {
|
||||
#[serde(skip)]
|
||||
pub path: PathBuf,
|
||||
pub name: String,
|
||||
pub high_contrast: Option<bool>,
|
||||
pub base: ThemeBaseConfig,
|
||||
pub syntax: IndexMap<String, String>,
|
||||
pub ui: IndexMap<String, String>,
|
||||
pub syntax: BTreeMap<String, String>,
|
||||
pub ui: BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
impl ColorThemeConfig {
|
||||
fn resolve_color(
|
||||
colors: &IndexMap<String, String>,
|
||||
colors: &BTreeMap<String, String>,
|
||||
base: &ThemeBaseColor,
|
||||
default: Option<&HashMap<String, Color>>,
|
||||
) -> HashMap<String, Color> {
|
||||
@@ -153,7 +146,7 @@ impl ColorThemeConfig {
|
||||
.iter()
|
||||
.map(|(name, hex)| {
|
||||
let color = if let Some(stripped) = hex.strip_prefix('$') {
|
||||
base.get(stripped).cloned()
|
||||
base.get(stripped)
|
||||
} else {
|
||||
Color::parse(hex)
|
||||
};
|
||||
@@ -185,3 +178,85 @@ impl ColorThemeConfig {
|
||||
Self::resolve_color(&self.syntax, base, default)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use config::Config;
|
||||
use floem::peniko::Color;
|
||||
|
||||
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: {:?}", Color::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::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::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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
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]
|
||||
@@ -12,6 +15,48 @@ pub enum ClickMode {
|
||||
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<Self> {
|
||||
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 {
|
||||
@@ -25,6 +70,10 @@ pub struct EditorConfig {
|
||||
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")]
|
||||
@@ -37,6 +86,12 @@ pub struct EditorConfig {
|
||||
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"
|
||||
)]
|
||||
@@ -54,9 +109,13 @@ pub struct EditorConfig {
|
||||
)]
|
||||
pub signature_label_code_block: bool,
|
||||
#[field_names(
|
||||
desc = "Whether the editor should disable automatic closing of matching pairs"
|
||||
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"
|
||||
)]
|
||||
@@ -70,6 +129,11 @@ pub struct EditorConfig {
|
||||
)]
|
||||
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,
|
||||
|
||||
@@ -93,6 +157,12 @@ pub struct EditorConfig {
|
||||
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."
|
||||
)]
|
||||
@@ -105,6 +175,8 @@ pub struct EditorConfig {
|
||||
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."
|
||||
)]
|
||||
@@ -116,7 +188,7 @@ pub struct EditorConfig {
|
||||
#[field_names(
|
||||
desc = "Set the cursor blink interval (in milliseconds). Set to 0 to completely disable."
|
||||
)]
|
||||
pub blink_interval: u64, // TODO: change to u128 when upgrading config-rs to >0.11
|
||||
blink_interval: u64,
|
||||
#[field_names(
|
||||
desc = "Whether the multiple cursor selection is case sensitive."
|
||||
)]
|
||||
@@ -128,7 +200,7 @@ pub struct EditorConfig {
|
||||
#[field_names(
|
||||
desc = "How the editor should render whitespace characters.\nOptions: none, all, boundary, trailing."
|
||||
)]
|
||||
pub render_whitespace: String,
|
||||
pub render_whitespace: RenderWhitespace,
|
||||
#[field_names(desc = "Whether the editor show indent guide.")]
|
||||
pub show_indent_guide: bool,
|
||||
#[field_names(
|
||||
@@ -153,6 +225,14 @@ pub struct EditorConfig {
|
||||
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 {
|
||||
@@ -161,8 +241,6 @@ impl EditorConfig {
|
||||
}
|
||||
|
||||
pub fn line_height(&self) -> usize {
|
||||
const SCALE_OR_SIZE_LIMIT: f64 = 5.0;
|
||||
|
||||
let line_height = if self.line_height < SCALE_OR_SIZE_LIMIT {
|
||||
self.line_height * self.font_size as f64
|
||||
} else {
|
||||
@@ -207,4 +285,11 @@ impl EditorConfig {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn blink_interval(&self) -> u64 {
|
||||
if self.blink_interval == 0 {
|
||||
return 0;
|
||||
}
|
||||
self.blink_interval.max(200)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,85 +1,94 @@
|
||||
pub struct LapceIcons {}
|
||||
|
||||
impl LapceIcons {
|
||||
pub const WINDOW_CLOSE: &str = "window.close";
|
||||
pub const WINDOW_RESTORE: &str = "window.restore";
|
||||
pub const WINDOW_MAXIMIZE: &str = "window.maximize";
|
||||
pub const WINDOW_MINIMIZE: &str = "window.minimize";
|
||||
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 LINK: &str = "link";
|
||||
pub const ERROR: &str = "error";
|
||||
pub const ADD: &str = "add";
|
||||
pub const CLOSE: &str = "close";
|
||||
pub const REMOTE: &str = "remote";
|
||||
pub const PROBLEM: &str = "error";
|
||||
pub const DEBUG: &str = "debug";
|
||||
pub const DEBUG_ALT: &str = "debug_alt";
|
||||
pub const DEBUG_BREAKPOINT: &str = "debug_breakpoint";
|
||||
pub const DEBUG_SMALL: &str = "debug_small";
|
||||
pub const DEBUG_RESTART: &str = "debug_restart";
|
||||
pub const DEBUG_CONTINUE: &str = "debug_continue";
|
||||
pub const DEBUG_PAUSE: &str = "debug_pause";
|
||||
pub const DEBUG_STOP: &str = "debug_stop";
|
||||
pub const DEBUG_CONSOLE: &str = "debug_console";
|
||||
pub const DEBUG_DISCONNECT: &str = "debug_disconnect";
|
||||
pub const START: &str = "start";
|
||||
pub const RUN_ERRORS: &str = "run_errors";
|
||||
pub const UNSAVED: &str = "unsaved";
|
||||
pub const WARNING: &str = "warning";
|
||||
pub const TERMINAL: &str = "terminal";
|
||||
pub const SETTINGS: &str = "settings";
|
||||
pub const LIGHTBULB: &str = "lightbulb";
|
||||
pub const EXTENSIONS: &str = "extensions";
|
||||
pub const KEYBOARD: &str = "keyboard";
|
||||
pub const BREADCRUMB_SEPARATOR: &str = "breadcrumb_separator";
|
||||
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 FILE: &str = "file";
|
||||
pub const FILE_EXPLORER: &str = "file_explorer";
|
||||
pub const FILE_PICKER_UP: &str = "file_picker_up";
|
||||
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: &str = "image_loading";
|
||||
pub const IMAGE_ERROR: &str = "image_error";
|
||||
pub const IMAGE_LOADING: &'static str = "image_loading";
|
||||
pub const IMAGE_ERROR: &'static str = "image_error";
|
||||
|
||||
pub const SCM: &str = "scm.icon";
|
||||
pub const SCM_DIFF_MODIFIED: &str = "scm.diff.modified";
|
||||
pub const SCM_DIFF_ADDED: &str = "scm.diff.added";
|
||||
pub const SCM_DIFF_REMOVED: &str = "scm.diff.removed";
|
||||
pub const SCM_DIFF_RENAMED: &str = "scm.diff.renamed";
|
||||
pub const SCM_CHANGE_ADD: &str = "scm.change.add";
|
||||
pub const SCM_CHANGE_REMOVE: &str = "scm.change.remove";
|
||||
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: &str = "fold";
|
||||
pub const FOLD_UP: &str = "fold.up";
|
||||
pub const FOLD_DOWN: &str = "fold.down";
|
||||
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: &str = "palette.menu";
|
||||
pub const PALETTE_MENU: &'static str = "palette.menu";
|
||||
|
||||
pub const DROPDOWN_ARROW: &str = "dropdown.arrow";
|
||||
pub const DROPDOWN_ARROW: &'static str = "dropdown.arrow";
|
||||
|
||||
pub const LOCATION_BACKWARD: &str = "location.backward";
|
||||
pub const LOCATION_FORWARD: &str = "location.forward";
|
||||
pub const PANEL_FOLD_UP: &'static str = "panel.fold-up";
|
||||
pub const PANEL_FOLD_DOWN: &'static str = "panel.fold-down";
|
||||
|
||||
pub const ITEM_OPENED: &str = "item.opened";
|
||||
pub const ITEM_CLOSED: &str = "item.closed";
|
||||
pub const LOCATION_BACKWARD: &'static str = "location.backward";
|
||||
pub const LOCATION_FORWARD: &'static str = "location.forward";
|
||||
|
||||
pub const DIRECTORY_CLOSED: &str = "directory.closed";
|
||||
pub const DIRECTORY_OPENED: &str = "directory.opened";
|
||||
pub const ITEM_OPENED: &'static str = "item.opened";
|
||||
pub const ITEM_CLOSED: &'static str = "item.closed";
|
||||
|
||||
pub const PANEL_RESTORE: &str = "panel.restore";
|
||||
pub const PANEL_MAXIMISE: &str = "panel.maximise";
|
||||
pub const DIRECTORY_CLOSED: &'static str = "directory.closed";
|
||||
pub const DIRECTORY_OPENED: &'static str = "directory.opened";
|
||||
|
||||
pub const SPLIT_HORIZONTAL: &str = "split.horizontal";
|
||||
pub const PANEL_RESTORE: &'static str = "panel.restore";
|
||||
pub const PANEL_MAXIMISE: &'static str = "panel.maximise";
|
||||
|
||||
pub const TAB_PREVIOUS: &str = "tab.previous";
|
||||
pub const TAB_NEXT: &str = "tab.next";
|
||||
pub const SPLIT_HORIZONTAL: &'static str = "split.horizontal";
|
||||
|
||||
pub const SIDEBAR_LEFT: &str = "sidebar.left.on";
|
||||
pub const SIDEBAR_LEFT_OFF: &str = "sidebar.left.off";
|
||||
pub const SIDEBAR_RIGHT: &str = "sidebar.right.on";
|
||||
pub const SIDEBAR_RIGHT_OFF: &str = "sidebar.right.off";
|
||||
pub const TAB_PREVIOUS: &'static str = "tab.previous";
|
||||
pub const TAB_NEXT: &'static str = "tab.next";
|
||||
|
||||
pub const LAYOUT_PANEL: &str = "layout.panel.on";
|
||||
pub const LAYOUT_PANEL_OFF: &str = "layout.panel.off";
|
||||
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";
|
||||
@@ -91,52 +100,65 @@ impl LapceIcons {
|
||||
pub const SEARCH_REPLACE: &'static str = "search.replace";
|
||||
pub const SEARCH_REPLACE_ALL: &'static str = "search.replace_all";
|
||||
|
||||
pub const FILE_TYPE_CODE: &str = "file-code";
|
||||
pub const FILE_TYPE_MEDIA: &str = "file-media";
|
||||
pub const FILE_TYPE_BINARY: &str = "file-binary";
|
||||
pub const FILE_TYPE_ARCHIVE: &str = "file-zip";
|
||||
pub const FILE_TYPE_SUBMODULE: &str = "file-submodule";
|
||||
pub const FILE_TYPE_SYMLINK_FILE: &str = "file-symlink-file";
|
||||
pub const FILE_TYPE_SYMLINK_DIRECTORY: &str = "file-symlink-directory";
|
||||
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 SYMBOL_KIND_ARRAY: &str = "symbol_kind.array";
|
||||
pub const SYMBOL_KIND_BOOLEAN: &str = "symbol_kind.boolean";
|
||||
pub const SYMBOL_KIND_CLASS: &str = "symbol_kind.class";
|
||||
pub const SYMBOL_KIND_CONSTANT: &str = "symbol_kind.constant";
|
||||
pub const SYMBOL_KIND_ENUM_MEMBER: &str = "symbol_kind.enum_member";
|
||||
pub const SYMBOL_KIND_ENUM: &str = "symbol_kind.enum";
|
||||
pub const SYMBOL_KIND_EVENT: &str = "symbol_kind.event";
|
||||
pub const SYMBOL_KIND_FIELD: &str = "symbol_kind.field";
|
||||
pub const SYMBOL_KIND_FILE: &str = "symbol_kind.file";
|
||||
pub const SYMBOL_KIND_FUNCTION: &str = "symbol_kind.function";
|
||||
pub const SYMBOL_KIND_INTERFACE: &str = "symbol_kind.interface";
|
||||
pub const SYMBOL_KIND_KEY: &str = "symbol_kind.key";
|
||||
pub const SYMBOL_KIND_METHOD: &str = "symbol_kind.method";
|
||||
pub const SYMBOL_KIND_NAMESPACE: &str = "symbol_kind.namespace";
|
||||
pub const SYMBOL_KIND_NUMBER: &str = "symbol_kind.number";
|
||||
pub const SYMBOL_KIND_OBJECT: &str = "symbol_kind.namespace";
|
||||
pub const SYMBOL_KIND_OPERATOR: &str = "symbol_kind.operator";
|
||||
pub const SYMBOL_KIND_PROPERTY: &str = "symbol_kind.property";
|
||||
pub const SYMBOL_KIND_STRING: &str = "symbol_kind.string";
|
||||
pub const SYMBOL_KIND_STRUCT: &str = "symbol_kind.struct";
|
||||
pub const SYMBOL_KIND_TYPE_PARAMETER: &str = "symbol_kind.type_parameter";
|
||||
pub const SYMBOL_KIND_VARIABLE: &str = "symbol_kind.variable";
|
||||
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: &str = "completion_item_kind.class";
|
||||
pub const COMPLETION_ITEM_KIND_CONSTANT: &str = "completion_item_kind.constant";
|
||||
pub const COMPLETION_ITEM_KIND_ENUM_MEMBER: &str =
|
||||
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: &str = "completion_item_kind.enum";
|
||||
pub const COMPLETION_ITEM_KIND_FIELD: &str = "completion_item_kind.field";
|
||||
pub const COMPLETION_ITEM_KIND_FUNCTION: &str = "completion_item_kind.function";
|
||||
pub const COMPLETION_ITEM_KIND_INTERFACE: &str =
|
||||
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: &str = "completion_item_kind.keyword";
|
||||
pub const COMPLETION_ITEM_KIND_METHOD: &str = "completion_item_kind.method";
|
||||
pub const COMPLETION_ITEM_KIND_MODULE: &str = "completion_item_kind.module";
|
||||
pub const COMPLETION_ITEM_KIND_PROPERTY: &str = "completion_item_kind.property";
|
||||
pub const COMPLETION_ITEM_KIND_SNIPPET: &str = "completion_item_kind.snippet";
|
||||
pub const COMPLETION_ITEM_KIND_STRING: &str = "completion_item_kind.string";
|
||||
pub const COMPLETION_ITEM_KIND_STRUCT: &str = "completion_item_kind.struct";
|
||||
pub const COMPLETION_ITEM_KIND_VARIABLE: &str = "completion_item_kind.variable";
|
||||
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";
|
||||
}
|
||||
|
||||
@@ -1,10 +1,26 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
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<T: PartialEq, I: IntoIterator<Item = Option<T>>>(
|
||||
items: I,
|
||||
) -> Option<T> {
|
||||
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")]
|
||||
#[serde(rename_all = "kebab-case", default)]
|
||||
pub struct IconThemeConfig {
|
||||
#[serde(skip)]
|
||||
pub path: PathBuf,
|
||||
@@ -17,23 +33,211 @@ pub struct IconThemeConfig {
|
||||
}
|
||||
|
||||
impl IconThemeConfig {
|
||||
pub fn resolve_path_to_icon(&self, path: &Path) -> Option<PathBuf> {
|
||||
if let Some((_, icon)) = self.filename.get_key_value(
|
||||
path.file_name()
|
||||
.unwrap_or_default()
|
||||
.to_str()
|
||||
.unwrap_or_default(),
|
||||
) {
|
||||
Some(self.path.join(icon))
|
||||
} else if let Some((_, icon)) = self.extension.get_key_value(
|
||||
path.extension()
|
||||
.unwrap_or_default()
|
||||
.to_str()
|
||||
.unwrap_or_default(),
|
||||
) {
|
||||
Some(self.path.join(icon))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
/// 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<PathBuf> {
|
||||
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::<u32>::None, try_all_equal_value([]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn try_all_equal_value_any_none_none() {
|
||||
assert_eq!(Option::<u32>::None, try_all_equal_value([None]));
|
||||
assert_eq!(
|
||||
Option::<i32>::None,
|
||||
try_all_equal_value([None, Some(1), Some(1)])
|
||||
);
|
||||
assert_eq!(Option::<u64>::None, try_all_equal_value([Some(0), None]));
|
||||
assert_eq!(
|
||||
Option::<u8>::None,
|
||||
try_all_equal_value([Some(3), Some(3), None, Some(3)])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn try_all_equal_value_any_different_none() {
|
||||
assert_eq!(Option::<u32>::None, try_all_equal_value([Some(1), Some(2)]));
|
||||
assert_eq!(
|
||||
Option::<u128>::None,
|
||||
try_all_equal_value([Some(1), Some(10), Some(1)])
|
||||
);
|
||||
assert_eq!(
|
||||
Option::<i16>::None,
|
||||
try_all_equal_value([Some(3), Some(3), Some(3), Some(3), Some(2)])
|
||||
);
|
||||
assert_eq!(
|
||||
Option::<i64>::None,
|
||||
try_all_equal_value([Some(5), Some(4), Some(4), Some(4), Some(4)])
|
||||
);
|
||||
assert_eq!(
|
||||
Option::<i128>::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::<u32>::Some(1), try_all_equal_value([Some(1)]));
|
||||
assert_eq!(Option::<i16>::Some(-2), try_all_equal_value([Some(-2); 2]));
|
||||
assert_eq!(Option::<i128>::Some(0), try_all_equal_value([Some(0); 3]));
|
||||
assert_eq!(Option::<u8>::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)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ use crate::config::LOGO;
|
||||
const CODICONS_ICONS_DIR: Dir = include_dir!("../icons/codicons");
|
||||
const LAPCE_ICONS_DIR: Dir = include_dir!("../icons/lapce");
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SvgStore {
|
||||
svgs: HashMap<String, String>,
|
||||
svgs_on_disk: HashMap<PathBuf, Option<String>>,
|
||||
@@ -39,7 +40,7 @@ impl SvgStore {
|
||||
|
||||
pub fn get_default_svg(&mut self, name: &str) -> String {
|
||||
if !self.svgs.contains_key(name) {
|
||||
let file = if name == "lapce_remote.svg" {
|
||||
let file = if name == "lapce_remote.svg" || name == "lapce_logo.svg" {
|
||||
LAPCE_ICONS_DIR.get_file(name).unwrap()
|
||||
} else {
|
||||
CODICONS_ICONS_DIR
|
||||
|
||||
@@ -18,15 +18,31 @@ pub struct TerminalConfig {
|
||||
#[field_names(
|
||||
desc = "Set the terminal line height, If 0, it uses editor line height"
|
||||
)]
|
||||
pub line_height: usize,
|
||||
#[field_names(desc = "Set the terminal Shell")]
|
||||
pub shell: String,
|
||||
pub line_height: f64,
|
||||
|
||||
#[field_names(skip)]
|
||||
pub profiles: HashMap<String, TerminalProfile>,
|
||||
#[field_names(skip)]
|
||||
pub default_profile: HashMap<String, String>,
|
||||
|
||||
#[serde(skip)]
|
||||
#[field_names(skip)]
|
||||
pub indexed_colors: Arc<HashMap<u8, Color>>,
|
||||
}
|
||||
|
||||
#[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<String>,
|
||||
#[field_names(desc = "Arguments passed to command")]
|
||||
pub arguments: Option<Vec<String>>,
|
||||
#[field_names(desc = "Command to execute when launching terminal")]
|
||||
pub workdir: Option<std::path::PathBuf>,
|
||||
#[field_names(desc = "Arguments passed to command")]
|
||||
pub environment: Option<HashMap<String, String>>,
|
||||
}
|
||||
|
||||
impl TerminalConfig {
|
||||
pub fn get_indexed_colors(&mut self) {
|
||||
let mut indexed_colors = HashMap::new();
|
||||
@@ -57,4 +73,29 @@ impl TerminalConfig {
|
||||
|
||||
self.indexed_colors = Arc::new(indexed_colors);
|
||||
}
|
||||
|
||||
pub fn get_default_profile(
|
||||
&self,
|
||||
) -> Option<lapce_rpc::terminal::TerminalProfile> {
|
||||
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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,8 +33,8 @@ pub struct UIConfig {
|
||||
#[field_names(desc = "Controls the width of drop shadow in the UI")]
|
||||
drop_shadow_width: usize,
|
||||
|
||||
#[field_names(desc = "Controls the width of the preview editor")]
|
||||
preview_editor_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"
|
||||
@@ -48,6 +48,32 @@ pub struct UIConfig {
|
||||
|
||||
#[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,
|
||||
}
|
||||
|
||||
impl UIConfig {
|
||||
@@ -76,4 +102,12 @@ impl UIConfig {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
@@ -8,6 +9,7 @@ use crossbeam_channel::{unbounded, Sender};
|
||||
use floem::peniko::kurbo::Vec2;
|
||||
use lapce_core::directory::Directory;
|
||||
use lapce_rpc::plugin::VoltID;
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
use crate::{
|
||||
app::{AppData, AppInfo},
|
||||
@@ -18,6 +20,14 @@ use crate::{
|
||||
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),
|
||||
@@ -30,24 +40,26 @@ pub enum SaveEvent {
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct LapceDb {
|
||||
folder: PathBuf,
|
||||
workspace_folder: PathBuf,
|
||||
save_tx: Sender<SaveEvent>,
|
||||
sled_db: Option<sled::Db>,
|
||||
}
|
||||
|
||||
impl LapceDb {
|
||||
pub fn new() -> Result<Self> {
|
||||
let path = Directory::config_directory()
|
||||
let folder = Directory::config_directory()
|
||||
.ok_or_else(|| anyhow!("can't get config directory"))?
|
||||
.join("lapce.db");
|
||||
.join("db");
|
||||
let workspace_folder = folder.join("workspaces");
|
||||
let _ = std::fs::create_dir_all(&workspace_folder);
|
||||
|
||||
let (save_tx, save_rx) = unbounded();
|
||||
|
||||
let sled_db = sled::Config::default()
|
||||
.path(path)
|
||||
.flush_every_ms(None)
|
||||
.open()
|
||||
.ok();
|
||||
|
||||
let db = Self { save_tx, sled_db };
|
||||
let db = Self {
|
||||
save_tx,
|
||||
workspace_folder,
|
||||
folder,
|
||||
};
|
||||
let local_db = db.clone();
|
||||
std::thread::spawn(move || -> Result<()> {
|
||||
loop {
|
||||
@@ -81,19 +93,9 @@ impl LapceDb {
|
||||
Ok(db)
|
||||
}
|
||||
|
||||
fn get_db(&self) -> Result<&sled::Db> {
|
||||
self.sled_db
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow!("didn't open sled db"))
|
||||
}
|
||||
|
||||
pub fn get_disabled_volts(&self) -> Result<Vec<VoltID>> {
|
||||
let sled_db = self.get_db()?;
|
||||
let volts = sled_db
|
||||
.get("disabled_volts")?
|
||||
.ok_or_else(|| anyhow!("can't find disable volts"))?;
|
||||
let volts = std::str::from_utf8(&volts)?;
|
||||
let volts: Vec<VoltID> = serde_json::from_str(volts)?;
|
||||
let volts = std::fs::read_to_string(self.folder.join(DISABLED_VOLTS))?;
|
||||
let volts: Vec<VoltID> = serde_json::from_str(&volts)?;
|
||||
Ok(volts)
|
||||
}
|
||||
|
||||
@@ -112,10 +114,8 @@ impl LapceDb {
|
||||
}
|
||||
|
||||
pub fn insert_disabled_volts(&self, volts: Vec<VoltID>) -> Result<()> {
|
||||
let sled_db = self.get_db()?;
|
||||
let volts = serde_json::to_string(&volts)?;
|
||||
sled_db.insert(b"disabled_volts", volts.as_str())?;
|
||||
sled_db.flush()?;
|
||||
let volts = serde_json::to_string_pretty(&volts)?;
|
||||
std::fs::write(self.folder.join(DISABLED_VOLTS), volts)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -124,10 +124,13 @@ impl LapceDb {
|
||||
workspace: Arc<LapceWorkspace>,
|
||||
volts: Vec<VoltID>,
|
||||
) -> Result<()> {
|
||||
let sled_db = self.get_db()?;
|
||||
let volts = serde_json::to_string(&volts)?;
|
||||
sled_db.insert(format!("disabled_volts:{workspace}"), volts.as_str())?;
|
||||
sled_db.flush()?;
|
||||
let folder = self
|
||||
.workspace_folder
|
||||
.join(workspace_folder_name(&workspace));
|
||||
let _ = std::fs::create_dir_all(&folder);
|
||||
|
||||
let volts = serde_json::to_string_pretty(&volts)?;
|
||||
std::fs::write(folder.join(DISABLED_VOLTS), volts)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -135,22 +138,16 @@ impl LapceDb {
|
||||
&self,
|
||||
workspace: &LapceWorkspace,
|
||||
) -> Result<Vec<VoltID>> {
|
||||
let sled_db = self.get_db()?;
|
||||
let volts = sled_db
|
||||
.get(format!("disabled_volts:{workspace}"))?
|
||||
.ok_or_else(|| anyhow!("can't find disable volts"))?;
|
||||
let volts = std::str::from_utf8(&volts)?;
|
||||
let volts: Vec<VoltID> = serde_json::from_str(volts)?;
|
||||
let folder = self.workspace_folder.join(workspace_folder_name(workspace));
|
||||
let volts = std::fs::read_to_string(folder.join(DISABLED_VOLTS))?;
|
||||
let volts: Vec<VoltID> = serde_json::from_str(&volts)?;
|
||||
Ok(volts)
|
||||
}
|
||||
|
||||
pub fn recent_workspaces(&self) -> Result<Vec<LapceWorkspace>> {
|
||||
let sled_db = self.get_db()?;
|
||||
let workspaces = sled_db
|
||||
.get("recent_workspaces")?
|
||||
.ok_or_else(|| anyhow!("can't find disable volts"))?;
|
||||
let workspaces = std::str::from_utf8(&workspaces)?;
|
||||
let workspaces: Vec<LapceWorkspace> = serde_json::from_str(workspaces)?;
|
||||
let workspaces =
|
||||
std::fs::read_to_string(self.folder.join(RECENT_WORKSPACES))?;
|
||||
let workspaces: Vec<LapceWorkspace> = serde_json::from_str(&workspaces)?;
|
||||
Ok(workspaces)
|
||||
}
|
||||
|
||||
@@ -163,18 +160,7 @@ impl LapceDb {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn insert_doc(&self, info: &DocInfo) -> Result<()> {
|
||||
let key = format!("{}:{}", info.workspace, info.path.to_str().unwrap_or(""));
|
||||
let info = serde_json::to_string(info)?;
|
||||
let sled_db = self.get_db()?;
|
||||
sled_db.insert(key.as_str(), info.as_str())?;
|
||||
sled_db.flush()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn insert_recent_workspace(&self, workspace: LapceWorkspace) -> Result<()> {
|
||||
let sled_db = self.get_db()?;
|
||||
|
||||
let mut workspaces = self.recent_workspaces().unwrap_or_default();
|
||||
|
||||
let mut exits = false;
|
||||
@@ -197,29 +183,13 @@ impl LapceDb {
|
||||
workspaces.push(workspace);
|
||||
}
|
||||
workspaces.sort_by_key(|w| -(w.last_open as i64));
|
||||
let workspaces = serde_json::to_string(&workspaces)?;
|
||||
|
||||
sled_db.insert("recent_workspaces", workspaces.as_str())?;
|
||||
sled_db.flush()?;
|
||||
let workspaces = serde_json::to_string_pretty(&workspaces)?;
|
||||
std::fs::write(self.folder.join(RECENT_WORKSPACES), workspaces)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_workspace_info(
|
||||
&self,
|
||||
workspace: &LapceWorkspace,
|
||||
) -> Result<WorkspaceInfo> {
|
||||
let workspace = workspace.to_string();
|
||||
let sled_db = self.get_db()?;
|
||||
let info = sled_db
|
||||
.get(workspace)?
|
||||
.ok_or_else(|| anyhow!("can't find workspace info"))?;
|
||||
let info = std::str::from_utf8(&info)?;
|
||||
let info: WorkspaceInfo = serde_json::from_str(info)?;
|
||||
Ok(info)
|
||||
}
|
||||
|
||||
pub fn save_window_tab(&self, data: Arc<WindowTabData>) -> Result<()> {
|
||||
pub fn save_window_tab(&self, data: Rc<WindowTabData>) -> Result<()> {
|
||||
let workspace = (*data.workspace).clone();
|
||||
let workspace_info = data.workspace_info();
|
||||
|
||||
@@ -230,16 +200,28 @@ impl LapceDb {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_workspace_info(
|
||||
&self,
|
||||
workspace: &LapceWorkspace,
|
||||
) -> Result<WorkspaceInfo> {
|
||||
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 workspace = workspace.to_string();
|
||||
let workspace_info = serde_json::to_string(info)?;
|
||||
let sled_db = self.get_db()?;
|
||||
sled_db.insert(workspace.as_str(), workspace_info.as_str())?;
|
||||
sled_db.flush()?;
|
||||
let folder = self.workspace_folder.join(workspace_folder_name(workspace));
|
||||
let _ = std::fs::create_dir_all(&folder);
|
||||
let workspace_info = serde_json::to_string_pretty(info)?;
|
||||
std::fs::write(folder.join(WORKSPACE_INFO), workspace_info)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -255,6 +237,9 @@ impl LapceDb {
|
||||
.map(|(_, window_data)| window_data.info())
|
||||
.collect(),
|
||||
};
|
||||
if info.windows.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.save_tx.send(SaveEvent::App(info))?;
|
||||
|
||||
@@ -262,15 +247,17 @@ impl LapceDb {
|
||||
}
|
||||
|
||||
pub fn insert_app_info(&self, info: AppInfo) -> Result<()> {
|
||||
let info = serde_json::to_string(&info)?;
|
||||
let sled_db = self.get_db()?;
|
||||
sled_db.insert("app", info.as_str())?;
|
||||
sled_db.flush()?;
|
||||
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 {
|
||||
let _ = self.insert_window(window.clone());
|
||||
}
|
||||
@@ -280,30 +267,33 @@ impl LapceDb {
|
||||
.map(|(_, window_data)| window_data.info())
|
||||
.collect(),
|
||||
};
|
||||
let info = serde_json::to_string(&info)?;
|
||||
let sled_db = self.get_db()?;
|
||||
sled_db.insert("app", info.as_str())?;
|
||||
sled_db.flush()?;
|
||||
self.insert_app_info(info)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_app(&self) -> Result<AppInfo> {
|
||||
let sled_db = self.get_db()?;
|
||||
let info = sled_db
|
||||
.get("app")?
|
||||
.ok_or_else(|| anyhow!("can't find app info"))?;
|
||||
let info = std::str::from_utf8(&info)?;
|
||||
let info: AppInfo = serde_json::from_str(info)?;
|
||||
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.width = 600.0;
|
||||
}
|
||||
}
|
||||
Ok(info)
|
||||
}
|
||||
|
||||
pub fn get_window(&self) -> Result<WindowInfo> {
|
||||
let sled_db = self.get_db()?;
|
||||
let info = sled_db
|
||||
.get("window")?
|
||||
.ok_or_else(|| anyhow!("can't find app info"))?;
|
||||
let info = std::str::from_utf8(&info)?;
|
||||
let info: WindowInfo = serde_json::from_str(info)?;
|
||||
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.width = 600.0;
|
||||
}
|
||||
Ok(info)
|
||||
}
|
||||
|
||||
@@ -319,14 +309,12 @@ impl LapceDb {
|
||||
let _ = self.insert_window_tab(window_tab);
|
||||
}
|
||||
let info = data.info();
|
||||
let info = serde_json::to_string(&info)?;
|
||||
let sled_db = self.get_db()?;
|
||||
sled_db.insert("window", info.as_str())?;
|
||||
sled_db.flush()?;
|
||||
let info = serde_json::to_string_pretty(&info)?;
|
||||
std::fs::write(self.folder.join(WINDOW), info)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn insert_window_tab(&self, data: Arc<WindowTabData>) -> Result<()> {
|
||||
pub fn insert_window_tab(&self, data: Rc<WindowTabData>) -> Result<()> {
|
||||
let workspace = (*data.workspace).clone();
|
||||
let workspace_info = data.workspace_info();
|
||||
|
||||
@@ -337,19 +325,13 @@ impl LapceDb {
|
||||
}
|
||||
|
||||
pub fn get_panel_orders(&self) -> Result<PanelOrder> {
|
||||
let sled_db = self.get_db()?;
|
||||
let panel_orders = sled_db
|
||||
.get("panel_orders")?
|
||||
.ok_or_else(|| anyhow!("can't find panel orders"))?;
|
||||
let panel_orders = std::str::from_utf8(&panel_orders)?;
|
||||
let mut panel_orders: PanelOrder = serde_json::from_str(panel_orders)?;
|
||||
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(PanelPosition::LeftTop)
|
||||
.or_insert_with(im::Vector::new);
|
||||
let panels = panel_orders.entry(PanelPosition::LeftTop).or_default();
|
||||
panels.push_back(kind);
|
||||
}
|
||||
}
|
||||
@@ -362,10 +344,8 @@ impl LapceDb {
|
||||
}
|
||||
|
||||
fn insert_panel_orders(&self, order: &PanelOrder) -> Result<()> {
|
||||
let info = serde_json::to_string(order)?;
|
||||
let sled_db = self.get_db()?;
|
||||
sled_db.insert("panel_orders", info.as_str())?;
|
||||
sled_db.flush()?;
|
||||
let info = serde_json::to_string_pretty(order)?;
|
||||
std::fs::write(self.folder.join(PANEL_ORDERS), info)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -385,18 +365,40 @@ impl LapceDb {
|
||||
let _ = self.save_tx.send(SaveEvent::Doc(info));
|
||||
}
|
||||
|
||||
fn insert_doc(&self, info: &DocInfo) -> Result<()> {
|
||||
let folder = self
|
||||
.workspace_folder
|
||||
.join(workspace_folder_name(&info.workspace))
|
||||
.join(WORKSPACE_FILES);
|
||||
let _ = std::fs::create_dir_all(&folder);
|
||||
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<DocInfo> {
|
||||
let key = format!("{}:{}", workspace, path.to_str().unwrap_or(""));
|
||||
let sled_db = self.get_db()?;
|
||||
let info = sled_db
|
||||
.get(key.as_str())?
|
||||
.ok_or_else(|| anyhow!("can't find workspace info"))?;
|
||||
let info = std::str::from_utf8(&info)?;
|
||||
let info: DocInfo = serde_json::from_str(info)?;
|
||||
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())
|
||||
}
|
||||
|
||||
@@ -1,20 +1,28 @@
|
||||
use std::{
|
||||
collections::{BTreeMap, HashMap},
|
||||
fmt::Display,
|
||||
path::{Path, PathBuf},
|
||||
path::PathBuf,
|
||||
rc::Rc,
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
use floem::reactive::{RwSignal, Scope};
|
||||
use lapce_rpc::{
|
||||
dap_types::{
|
||||
DapId, RunDebugConfig, SourceBreakpoint, StackFrame, Stopped, ThreadId,
|
||||
},
|
||||
terminal::TermId,
|
||||
use dap_types::types::{
|
||||
self, DapId, RunDebugConfig, SourceBreakpoint, StackFrame, Stopped, ThreadId,
|
||||
Variable,
|
||||
};
|
||||
use floem::{
|
||||
ext_event::create_ext_action,
|
||||
reactive::{Memo, RwSignal, Scope},
|
||||
views::VirtualVector,
|
||||
};
|
||||
use lapce_rpc::{proxy::ProxyResponse, terminal::TermId};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
const DEFAULT_RUN_TOML: &str = include_str!("../../defaults/run.toml");
|
||||
use crate::{
|
||||
command::InternalCommand,
|
||||
editor::location::{EditorLocation, EditorPosition},
|
||||
window_tab::CommonData,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum RunDebugMode {
|
||||
@@ -38,6 +46,7 @@ pub struct RunDebugProcess {
|
||||
pub config: RunDebugConfig,
|
||||
pub stopped: bool,
|
||||
pub created: Instant,
|
||||
pub is_prelaunch: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
@@ -45,33 +54,22 @@ pub struct RunDebugConfigs {
|
||||
pub configs: Vec<RunDebugConfig>,
|
||||
}
|
||||
|
||||
pub fn run_configs(workspace: Option<&Path>) -> Option<RunDebugConfigs> {
|
||||
let workspace = workspace?;
|
||||
let run_toml = workspace.join(".lapce").join("run.toml");
|
||||
if !run_toml.exists() {
|
||||
if !workspace.join(".lapce").exists() {
|
||||
let _ = std::fs::create_dir_all(workspace.join(".lapce"));
|
||||
}
|
||||
let _ = std::fs::write(&run_toml, DEFAULT_RUN_TOML);
|
||||
return None;
|
||||
}
|
||||
let content = std::fs::read_to_string(run_toml).ok()?;
|
||||
let configs: RunDebugConfigs = toml_edit::easy::from_str(&content).ok()?;
|
||||
Some(configs)
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RunDebugData {
|
||||
pub active_term: RwSignal<Option<TermId>>,
|
||||
pub daps: RwSignal<im::HashMap<DapId, DapData>>,
|
||||
pub breakpoints: RwSignal<BTreeMap<PathBuf, Vec<LapceBreakpoint>>>,
|
||||
pub breakpoints: RwSignal<BTreeMap<PathBuf, BTreeMap<usize, LapceBreakpoint>>>,
|
||||
}
|
||||
|
||||
impl RunDebugData {
|
||||
pub fn new(cx: Scope) -> Self {
|
||||
let active_term = cx.create_rw_signal(None);
|
||||
let daps = cx.create_rw_signal(im::HashMap::new());
|
||||
let breakpoints = cx.create_rw_signal(BTreeMap::new());
|
||||
pub fn new(
|
||||
cx: Scope,
|
||||
breakpoints: RwSignal<BTreeMap<PathBuf, BTreeMap<usize, LapceBreakpoint>>>,
|
||||
) -> Self {
|
||||
let active_term: RwSignal<Option<TermId>> = cx.create_rw_signal(None);
|
||||
let daps: RwSignal<im::HashMap<DapId, DapData>> =
|
||||
cx.create_rw_signal(im::HashMap::new());
|
||||
|
||||
Self {
|
||||
active_term,
|
||||
daps,
|
||||
@@ -88,12 +86,18 @@ impl RunDebugData {
|
||||
path.to_path_buf(),
|
||||
breakpoints
|
||||
.iter()
|
||||
.map(|b| SourceBreakpoint {
|
||||
line: b.line + 1,
|
||||
column: None,
|
||||
condition: None,
|
||||
hit_condition: None,
|
||||
log_message: None,
|
||||
.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(),
|
||||
)
|
||||
@@ -109,7 +113,7 @@ pub struct StackTraceData {
|
||||
pub frames_shown: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct LapceBreakpoint {
|
||||
pub id: Option<usize>,
|
||||
pub verified: bool,
|
||||
@@ -117,6 +121,60 @@ pub struct LapceBreakpoint {
|
||||
pub line: usize,
|
||||
pub offset: usize,
|
||||
pub dap_line: Option<usize>,
|
||||
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<usize>,
|
||||
pub expanded: bool,
|
||||
pub read: bool,
|
||||
pub children: Vec<DapVariable>,
|
||||
pub children_expanded_count: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -126,19 +184,63 @@ pub struct DapData {
|
||||
pub stopped: RwSignal<bool>,
|
||||
pub thread_id: RwSignal<Option<ThreadId>>,
|
||||
pub stack_traces: RwSignal<BTreeMap<ThreadId, StackTraceData>>,
|
||||
pub variables_id: RwSignal<usize>,
|
||||
pub variables: RwSignal<DapVariable>,
|
||||
pub breakline: Memo<Option<(usize, PathBuf)>>,
|
||||
pub common: Rc<CommonData>,
|
||||
}
|
||||
|
||||
impl DapData {
|
||||
pub fn new(cx: Scope, dap_id: DapId, term_id: TermId) -> Self {
|
||||
pub fn new(
|
||||
cx: Scope,
|
||||
dap_id: DapId,
|
||||
term_id: TermId,
|
||||
common: Rc<CommonData>,
|
||||
) -> Self {
|
||||
let stopped = cx.create_rw_signal(false);
|
||||
let thread_id = cx.create_rw_signal(None);
|
||||
let stack_traces = cx.create_rw_signal(BTreeMap::new());
|
||||
let stack_traces: RwSignal<BTreeMap<ThreadId, StackTraceData>> =
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,6 +249,7 @@ impl DapData {
|
||||
cx: Scope,
|
||||
stopped: &Stopped,
|
||||
stack_traces: &HashMap<ThreadId, Vec<StackFrame>>,
|
||||
variables: &[(dap_types::Scope, Vec<Variable>)],
|
||||
) {
|
||||
self.stopped.set(true);
|
||||
self.thread_id.update(|thread_id| {
|
||||
@@ -154,26 +257,381 @@ impl DapData {
|
||||
});
|
||||
|
||||
let main_thread_id = self.thread_id.get_untracked();
|
||||
self.stack_traces.update(|current_stack_traces| {
|
||||
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 let Some(current) = current_stack_traces.get_mut(thread_id) {
|
||||
current.frames.set(frames.into());
|
||||
if is_main_thread {
|
||||
current.expanded.set(true);
|
||||
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::<usize>();
|
||||
});
|
||||
}
|
||||
|
||||
pub fn toggle_expand(&self, parent: Vec<usize>, 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 {
|
||||
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,
|
||||
},
|
||||
);
|
||||
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 { varialbes }) = 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 = varialbes
|
||||
.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<usize>,
|
||||
pub expanded: bool,
|
||||
pub level: usize,
|
||||
}
|
||||
|
||||
impl VirtualVector<DapVariableViewdata> for DapVariable {
|
||||
fn total_len(&self) -> usize {
|
||||
self.children_expanded_count
|
||||
}
|
||||
|
||||
fn slice(
|
||||
&mut self,
|
||||
range: std::ops::Range<usize>,
|
||||
) -> impl Iterator<Item = DapVariableViewdata> {
|
||||
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<DapVariableViewdata>,
|
||||
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::<usize>();
|
||||
}
|
||||
|
||||
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::<usize>()
|
||||
} 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 = vec![
|
||||
(
|
||||
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::<usize>();
|
||||
assert_eq!(root.children_expanded_count, 9);
|
||||
|
||||
let var = root.get_var_mut(&[0], 3).unwrap();
|
||||
var.expanded = true;
|
||||
var.read = true;
|
||||
var.children = vec![
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
use floem::peniko::Color;
|
||||
use lsp_types::DiagnosticSeverity;
|
||||
use smallvec::SmallVec;
|
||||
|
||||
/// `PhantomText` is for text that is not in the actual document, but should be rendered with it.
|
||||
/// Ex: Inlay hints, IME text, error lens' diagnostics, etc
|
||||
pub struct PhantomText {
|
||||
/// The kind is currently used for sorting the phantom text on a line
|
||||
pub kind: PhantomTextKind,
|
||||
/// Column on the line that the phantom text should be displayed at
|
||||
pub col: usize,
|
||||
pub text: String,
|
||||
pub font_size: Option<usize>,
|
||||
// font_family: Option<FontFamily>,
|
||||
pub fg: Option<Color>,
|
||||
pub bg: Option<Color>,
|
||||
pub under_line: Option<Color>,
|
||||
}
|
||||
|
||||
#[derive(Ord, Eq, PartialEq, PartialOrd)]
|
||||
pub enum PhantomTextKind {
|
||||
/// Input methods
|
||||
Ime,
|
||||
/// Completion lens
|
||||
Completion,
|
||||
/// Inlay hints supplied by an LSP/PSP (like type annotations)
|
||||
InlayHint,
|
||||
/// Error lens
|
||||
Diagnostic,
|
||||
}
|
||||
|
||||
/// Information about the phantom text on a specific line.
|
||||
/// This has various utility functions for transforming a coordinate (typically a column) into the
|
||||
/// resulting coordinate after the phantom text is combined with the line's real content.
|
||||
#[derive(Default)]
|
||||
pub struct PhantomTextLine {
|
||||
/// This uses a smallvec because most lines rarely have more than a couple phantom texts
|
||||
pub text: SmallVec<[PhantomText; 6]>,
|
||||
/// Maximum diagnostic severity, so that we can color the background as an error if there is a
|
||||
/// warning and error on the line. (For error lens)
|
||||
pub max_severity: Option<DiagnosticSeverity>,
|
||||
}
|
||||
|
||||
impl PhantomTextLine {
|
||||
/// Translate a column position into the text into what it would be after combining
|
||||
pub fn col_at(&self, pre_col: usize) -> usize {
|
||||
let mut last = pre_col;
|
||||
for (col_shift, size, col, _) in self.offset_size_iter() {
|
||||
if pre_col >= col {
|
||||
last = pre_col + col_shift + size;
|
||||
}
|
||||
}
|
||||
|
||||
last
|
||||
}
|
||||
|
||||
/// Translate a column position into the text into what it would be after combining
|
||||
/// If before_cursor is false and the cursor is right at the start then it will stay there
|
||||
pub fn col_after(&self, pre_col: usize, before_cursor: bool) -> usize {
|
||||
let mut last = pre_col;
|
||||
for (col_shift, size, col, _) in self.offset_size_iter() {
|
||||
if pre_col > col || (pre_col == col && before_cursor) {
|
||||
last = pre_col + col_shift + size;
|
||||
}
|
||||
}
|
||||
|
||||
last
|
||||
}
|
||||
|
||||
/// Translate a column position into the position it would be before combining
|
||||
pub fn before_col(&self, col: usize) -> usize {
|
||||
let mut last = col;
|
||||
for (col_shift, size, hint_col, _) in self.offset_size_iter() {
|
||||
let shifted_start = hint_col + col_shift;
|
||||
let shifted_end = shifted_start + size;
|
||||
if col >= shifted_start {
|
||||
if col >= shifted_end {
|
||||
last = col - col_shift - size;
|
||||
} else {
|
||||
last = hint_col;
|
||||
}
|
||||
}
|
||||
}
|
||||
last
|
||||
}
|
||||
|
||||
/// Insert the hints at their positions in the text
|
||||
pub fn combine_with_text(&self, text: String) -> String {
|
||||
let mut text = text;
|
||||
let mut col_shift = 0;
|
||||
|
||||
for phantom in self.text.iter() {
|
||||
let location = phantom.col + col_shift;
|
||||
|
||||
// Stop iterating if the location is bad
|
||||
if text.get(location..).is_none() {
|
||||
return text;
|
||||
}
|
||||
|
||||
text.insert_str(location, &phantom.text);
|
||||
col_shift += phantom.text.len();
|
||||
}
|
||||
|
||||
text
|
||||
}
|
||||
|
||||
/// Iterator over (col_shift, size, hint, pre_column)
|
||||
/// Note that this only iterates over the ordered text, since those depend on the text for where
|
||||
/// they'll be positioned
|
||||
pub fn offset_size_iter(
|
||||
&self,
|
||||
) -> impl Iterator<Item = (usize, usize, usize, &PhantomText)> + '_ {
|
||||
let mut col_shift = 0;
|
||||
|
||||
self.text.iter().map(move |phantom| {
|
||||
let pre_col_shift = col_shift;
|
||||
col_shift += phantom.text.len();
|
||||
(
|
||||
pre_col_shift,
|
||||
col_shift - pre_col_shift,
|
||||
phantom.col,
|
||||
phantom,
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,14 @@
|
||||
use std::sync::atomic;
|
||||
use std::{rc::Rc, sync::atomic};
|
||||
|
||||
use floem::{
|
||||
event::EventListener,
|
||||
ext_event::create_ext_action,
|
||||
reactive::{RwSignal, Scope},
|
||||
style::{CursorStyle, Style},
|
||||
view::View,
|
||||
views::{clip, empty, label, list, stack, svg, Decorators},
|
||||
style::CursorStyle,
|
||||
views::{
|
||||
clip, dyn_stack, editor::id::EditorId, empty, label, stack, svg, Decorators,
|
||||
},
|
||||
View,
|
||||
};
|
||||
use lapce_core::buffer::{
|
||||
diff::{expand_diff_lines, rope_diff, DiffExpand, DiffLines},
|
||||
@@ -16,17 +18,16 @@ 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::{DocContent, Document},
|
||||
id::{DiffEditorId, EditorId, EditorTabId},
|
||||
main_split::MainSplitData,
|
||||
doc::{Doc, DocContent},
|
||||
id::{DiffEditorId, EditorTabId},
|
||||
main_split::{Editors, MainSplitData},
|
||||
wave::wave_box,
|
||||
window_tab::CommonData,
|
||||
};
|
||||
|
||||
use super::{EditorData, EditorViewKind};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DiffInfo {
|
||||
pub is_right: bool,
|
||||
@@ -53,43 +54,41 @@ impl DiffEditorInfo {
|
||||
let data = data.clone();
|
||||
let common = data.common.clone();
|
||||
move |content: &DocContent| match content {
|
||||
DocContent::File(path) => {
|
||||
let (doc, _) = data.get_doc(path.clone());
|
||||
DocContent::File { path, .. } => {
|
||||
let (doc, _) = data.get_doc(path.clone(), None);
|
||||
doc
|
||||
}
|
||||
DocContent::Local => cx.create_rw_signal(Document::new_local(
|
||||
cx,
|
||||
common.find.clone(),
|
||||
common.proxy.clone(),
|
||||
common.config,
|
||||
)),
|
||||
DocContent::Local => {
|
||||
Rc::new(Doc::new_local(cx, data.editors, common.clone()))
|
||||
}
|
||||
DocContent::History(history) => {
|
||||
let doc = Document::new_hisotry(
|
||||
let doc = Doc::new_history(
|
||||
cx,
|
||||
content.clone(),
|
||||
common.find.clone(),
|
||||
common.proxy.clone(),
|
||||
common.config,
|
||||
data.editors,
|
||||
common.clone(),
|
||||
);
|
||||
let doc = doc.scope.create_rw_signal(doc);
|
||||
let doc = Rc::new(doc);
|
||||
|
||||
let send = create_ext_action(cx, move |result| {
|
||||
if let Ok(ProxyResponse::BufferHeadResponse {
|
||||
content,
|
||||
..
|
||||
}) = result
|
||||
{
|
||||
doc.update(|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);
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
common.proxy.get_buffer_head(
|
||||
history.path.clone(),
|
||||
move |result| {
|
||||
send(result);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
doc
|
||||
}
|
||||
DocContent::Scratch { name, .. } => {
|
||||
@@ -97,16 +96,15 @@ impl DiffEditorInfo {
|
||||
id: BufferId::next(),
|
||||
name: name.to_string(),
|
||||
};
|
||||
let doc = Document::new_content(
|
||||
let doc = Doc::new_content(
|
||||
cx,
|
||||
doc_content,
|
||||
common.find.clone(),
|
||||
common.proxy.clone(),
|
||||
common.config,
|
||||
data.editors,
|
||||
common.clone(),
|
||||
);
|
||||
let doc = doc.scope.create_rw_signal(doc);
|
||||
let doc = Rc::new(doc);
|
||||
data.scratch_docs.update(|scratch_docs| {
|
||||
scratch_docs.insert(name.to_string(), doc);
|
||||
scratch_docs.insert(name.to_string(), doc.clone());
|
||||
});
|
||||
doc
|
||||
}
|
||||
@@ -122,6 +120,7 @@ impl DiffEditorInfo {
|
||||
editor_tab_id,
|
||||
left_doc,
|
||||
right_doc,
|
||||
data.editors,
|
||||
data.common.clone(),
|
||||
);
|
||||
|
||||
@@ -136,10 +135,11 @@ impl DiffEditorInfo {
|
||||
#[derive(Clone)]
|
||||
pub struct DiffEditorData {
|
||||
pub id: DiffEditorId,
|
||||
pub editor_tab_id: EditorTabId,
|
||||
pub editor_tab_id: RwSignal<EditorTabId>,
|
||||
pub scope: Scope,
|
||||
pub left: RwSignal<EditorData>,
|
||||
pub right: RwSignal<EditorData>,
|
||||
pub left: EditorData,
|
||||
pub right: EditorData,
|
||||
pub confirmed: RwSignal<bool>,
|
||||
pub focus_right: RwSignal<bool>,
|
||||
}
|
||||
|
||||
@@ -148,23 +148,33 @@ impl DiffEditorData {
|
||||
cx: Scope,
|
||||
id: DiffEditorId,
|
||||
editor_tab_id: EditorTabId,
|
||||
left_doc: RwSignal<Document>,
|
||||
right_doc: RwSignal<Document>,
|
||||
common: CommonData,
|
||||
left_doc: Rc<Doc>,
|
||||
right_doc: Rc<Doc>,
|
||||
editors: Editors,
|
||||
common: Rc<CommonData>,
|
||||
) -> Self {
|
||||
let cx = cx.create_child();
|
||||
let left =
|
||||
EditorData::new(cx, None, EditorId::next(), left_doc, common.clone());
|
||||
let left = left.scope.create_rw_signal(left);
|
||||
let right = EditorData::new(cx, None, EditorId::next(), right_doc, common);
|
||||
let right = right.scope.create_rw_signal(right);
|
||||
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,
|
||||
editor_tab_id: cx.create_rw_signal(editor_tab_id),
|
||||
scope: cx,
|
||||
left,
|
||||
right,
|
||||
confirmed,
|
||||
focus_right: cx.create_rw_signal(true),
|
||||
};
|
||||
|
||||
@@ -175,14 +185,8 @@ impl DiffEditorData {
|
||||
|
||||
pub fn diff_editor_info(&self) -> DiffEditorInfo {
|
||||
DiffEditorInfo {
|
||||
left_content: self.left.get_untracked().view.doc.get_untracked().content,
|
||||
right_content: self
|
||||
.right
|
||||
.get_untracked()
|
||||
.view
|
||||
.doc
|
||||
.get_untracked()
|
||||
.content,
|
||||
left_content: self.left.doc().content.get_untracked(),
|
||||
right_content: self.right.doc().content.get_untracked(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,24 +195,31 @@ impl DiffEditorData {
|
||||
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,
|
||||
editor_tab_id: cx.create_rw_signal(editor_tab_id),
|
||||
focus_right: cx.create_rw_signal(true),
|
||||
left: cx.create_rw_signal(self.left.get_untracked().copy(
|
||||
cx,
|
||||
None,
|
||||
EditorId::next(),
|
||||
)),
|
||||
right: cx.create_rw_signal(self.right.get_untracked().copy(
|
||||
cx,
|
||||
None,
|
||||
EditorId::next(),
|
||||
)),
|
||||
left,
|
||||
right,
|
||||
confirmed,
|
||||
};
|
||||
|
||||
diff_editor.listen_diff_changes();
|
||||
@@ -218,32 +229,38 @@ impl DiffEditorData {
|
||||
fn listen_diff_changes(&self) {
|
||||
let cx = self.scope;
|
||||
|
||||
let left = self.left;
|
||||
let left_doc_rev = cx.create_memo(move |_| {
|
||||
let left_doc = left.with(|editor| editor.view.doc);
|
||||
left_doc.with(|doc| (doc.content.clone(), doc.rev()))
|
||||
});
|
||||
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;
|
||||
let right_doc_rev = cx.create_memo(move |_| {
|
||||
let right_doc = right.with(|editor| editor.view.doc);
|
||||
right_doc.with(|doc| (doc.content.clone(), doc.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.with_untracked(|editor| (editor.view.kind, editor.view.doc));
|
||||
let (left_atomic_rev, left_rope) = left_doc.with_untracked(|doc| {
|
||||
(doc.buffer().atomic_rev(), doc.buffer().text().clone())
|
||||
});
|
||||
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.with_untracked(|editor| (editor.view.kind, editor.view.doc));
|
||||
let (right_atomic_rev, right_rope) = right_doc.with_untracked(|doc| {
|
||||
(doc.buffer().atomic_rev(), doc.buffer().text().clone())
|
||||
});
|
||||
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();
|
||||
@@ -296,18 +313,16 @@ struct DiffShowMoreSection {
|
||||
}
|
||||
|
||||
pub fn diff_show_more_section_view(
|
||||
left_editor: RwSignal<EditorData>,
|
||||
right_editor: RwSignal<EditorData>,
|
||||
left_editor: &EditorData,
|
||||
right_editor: &EditorData,
|
||||
) -> impl View {
|
||||
let left_editor_view = left_editor.with_untracked(|editor| editor.view.kind);
|
||||
let (right_editor_view, viewport, config) =
|
||||
right_editor.with_untracked(|editor| {
|
||||
(editor.view.kind, editor.viewport, editor.common.config)
|
||||
});
|
||||
let left_editor_view = left_editor.kind;
|
||||
let right_editor_view = right_editor.kind;
|
||||
let viewport = right_editor.viewport();
|
||||
let config = right_editor.common.config;
|
||||
|
||||
let each_fn = move || {
|
||||
let editor_view = right_editor.with(|editor| editor.view.kind);
|
||||
let editor_view = editor_view.get();
|
||||
let editor_view = right_editor_view.get();
|
||||
if let EditorViewKind::Diff(diff_info) = editor_view {
|
||||
let viewport = viewport.get();
|
||||
let config = config.get_untracked();
|
||||
@@ -373,184 +388,159 @@ pub fn diff_show_more_section_view(
|
||||
move |section: &DiffShowMoreSection| (section.visual_line, section.lines);
|
||||
|
||||
let view_fn = move |section: DiffShowMoreSection| {
|
||||
stack(|| {
|
||||
(
|
||||
wave_box().style(move || {
|
||||
Style::BASE
|
||||
.absolute()
|
||||
.size_pct(100.0, 100.0)
|
||||
.color(*config.get().get_color(LapceColor::PANEL_BACKGROUND))
|
||||
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(move || format!("{} Hidden Lines", section.lines)),
|
||||
label(|| "|".to_string()).style(|| Style::BASE.margin_left_px(10.0)),
|
||||
stack(|| {
|
||||
(
|
||||
svg(move || config.get().ui_svg(LapceIcons::FOLD)).style(
|
||||
move || {
|
||||
let config = config.get();
|
||||
let size = config.ui.icon_size() as f32;
|
||||
Style::BASE.size_px(size, size).color(
|
||||
*config.get_color(LapceColor::EDITOR_FOREGROUND),
|
||||
)
|
||||
},
|
||||
),
|
||||
label(|| "Expand All".to_string())
|
||||
.style(|| Style::BASE.margin_left_px(6.0)),
|
||||
)
|
||||
})
|
||||
.on_event(EventListener::PointerDown, move |_| true)
|
||||
.on_click(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,
|
||||
);
|
||||
}
|
||||
});
|
||||
true
|
||||
})
|
||||
.style(|| {
|
||||
Style::BASE
|
||||
.margin_left_px(10.0)
|
||||
.height_pct(100.0)
|
||||
.items_center()
|
||||
})
|
||||
.hover_style(|| Style::BASE.cursor(CursorStyle::Pointer)),
|
||||
label(|| "|".to_string()).style(|| Style::BASE.margin_left_px(10.0)),
|
||||
stack(|| {
|
||||
(
|
||||
svg(move || config.get().ui_svg(LapceIcons::FOLD_UP)).style(
|
||||
move || {
|
||||
let config = config.get();
|
||||
let size = config.ui.icon_size() as f32;
|
||||
Style::BASE.size_px(size, size).color(
|
||||
*config.get_color(LapceColor::EDITOR_FOREGROUND),
|
||||
)
|
||||
},
|
||||
),
|
||||
label(|| "Expand Up".to_string())
|
||||
.style(|| Style::BASE.margin_left_px(6.0)),
|
||||
)
|
||||
})
|
||||
.on_event(EventListener::PointerDown, move |_| true)
|
||||
.on_click(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,
|
||||
);
|
||||
}
|
||||
});
|
||||
true
|
||||
})
|
||||
.style(move || {
|
||||
Style::BASE
|
||||
.margin_left_px(10.0)
|
||||
.height_pct(100.0)
|
||||
.items_center()
|
||||
})
|
||||
.hover_style(|| Style::BASE.cursor(CursorStyle::Pointer)),
|
||||
label(|| "|".to_string()).style(|| Style::BASE.margin_left_px(10.0)),
|
||||
stack(|| {
|
||||
(
|
||||
svg(move || config.get().ui_svg(LapceIcons::FOLD_DOWN))
|
||||
.style(move || {
|
||||
let config = config.get();
|
||||
let size = config.ui.icon_size() as f32;
|
||||
Style::BASE.size_px(size, size).color(
|
||||
*config.get_color(LapceColor::EDITOR_FOREGROUND),
|
||||
)
|
||||
}),
|
||||
label(|| "Expand Down".to_string())
|
||||
.style(|| Style::BASE.margin_left_px(6.0)),
|
||||
)
|
||||
})
|
||||
.on_event(EventListener::PointerDown, move |_| true)
|
||||
.on_click(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,
|
||||
);
|
||||
}
|
||||
});
|
||||
true
|
||||
})
|
||||
.style(move || {
|
||||
Style::BASE
|
||||
.margin_left_px(10.0)
|
||||
.height_pct(100.0)
|
||||
.items_center()
|
||||
})
|
||||
.hover_style(|| Style::BASE.cursor(CursorStyle::Pointer)),
|
||||
)
|
||||
})
|
||||
.style(move || {
|
||||
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))
|
||||
}),
|
||||
))
|
||||
.style(move |s| {
|
||||
let config = config.get();
|
||||
Style::BASE
|
||||
.absolute()
|
||||
s.absolute()
|
||||
.width_pct(100.0)
|
||||
.height_px(config.editor.line_height() as f32)
|
||||
.height(config.editor.line_height() as f32)
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.margin_top_px(
|
||||
.margin_top(
|
||||
(section.visual_line * config.editor.line_height()) as f32
|
||||
- viewport.get().y0 as f32,
|
||||
)
|
||||
.hover(|s| s.cursor(CursorStyle::Default))
|
||||
})
|
||||
.hover_style(|| Style::BASE.cursor(CursorStyle::Default))
|
||||
};
|
||||
|
||||
stack(move || {
|
||||
(
|
||||
empty().style(move || {
|
||||
Style::BASE.height_px(config.get().editor.line_height() as f32 + 1.0)
|
||||
}),
|
||||
clip(|| {
|
||||
list(each_fn, key_fn, view_fn)
|
||||
.style(|| Style::BASE.flex_col().size_pct(100.0, 100.0))
|
||||
})
|
||||
.style(|| Style::BASE.size_pct(100.0, 100.0)),
|
||||
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(|| Style::BASE.absolute().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))
|
||||
}
|
||||
|
||||
@@ -1,30 +1,22 @@
|
||||
use floem::{
|
||||
context::PaintCx,
|
||||
cosmic_text::{Attrs, AttrsList, FamilyOwned, TextLayout},
|
||||
id::Id,
|
||||
peniko::kurbo::{Point, Rect, Size},
|
||||
reactive::RwSignal,
|
||||
view::{ChangeFlags, View},
|
||||
Renderer, ViewContext,
|
||||
Renderer, View, ViewId,
|
||||
};
|
||||
use lapce_core::{buffer::rope_text::RopeText, mode::Mode};
|
||||
|
||||
use crate::{
|
||||
config::{color::LapceColor, LapceConfig},
|
||||
doc::Document,
|
||||
};
|
||||
|
||||
use super::{view::changes_colors, EditorData};
|
||||
use super::{view::changes_colors_screen, EditorData};
|
||||
use crate::config::{color::LapceColor, LapceConfig};
|
||||
|
||||
pub struct EditorGutterView {
|
||||
id: Id,
|
||||
editor: RwSignal<EditorData>,
|
||||
id: ViewId,
|
||||
editor: EditorData,
|
||||
width: f64,
|
||||
}
|
||||
|
||||
pub fn editor_gutter_view(editor: RwSignal<EditorData>) -> EditorGutterView {
|
||||
let cx = ViewContext::get_current();
|
||||
let id = cx.new_id();
|
||||
pub fn editor_gutter_view(editor: EditorData) -> EditorGutterView {
|
||||
let id = ViewId::new();
|
||||
|
||||
EditorGutterView {
|
||||
id,
|
||||
@@ -37,7 +29,7 @@ impl EditorGutterView {
|
||||
fn paint_head_changes(
|
||||
&self,
|
||||
cx: &mut PaintCx,
|
||||
doc: RwSignal<Document>,
|
||||
e_data: &EditorData,
|
||||
viewport: Rect,
|
||||
is_normal: bool,
|
||||
config: &LapceConfig,
|
||||
@@ -46,21 +38,17 @@ impl EditorGutterView {
|
||||
return;
|
||||
}
|
||||
|
||||
let changes = doc.with_untracked(|doc| doc.head_changes);
|
||||
let changes = changes.get();
|
||||
let changes = e_data.doc().head_changes().get_untracked();
|
||||
let line_height = config.editor.line_height() as f64;
|
||||
|
||||
let min_line = (viewport.y0 / line_height).floor() as usize;
|
||||
let max_line = (viewport.y1 / line_height).ceil() as usize;
|
||||
|
||||
let changes = changes_colors(changes, min_line, max_line, config);
|
||||
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 as f64 * line_height - viewport.y0;
|
||||
let mut y = y - viewport.y0;
|
||||
if removed {
|
||||
y -= 5.0;
|
||||
}
|
||||
@@ -69,140 +57,133 @@ impl EditorGutterView {
|
||||
.to_rect()
|
||||
.with_origin(Point::new(self.width + 7.0, 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;
|
||||
let sticky_header_height = 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) -> Id {
|
||||
fn id(&self) -> ViewId {
|
||||
self.id
|
||||
}
|
||||
|
||||
fn child(&self, _id: Id) -> Option<&dyn View> {
|
||||
None
|
||||
}
|
||||
|
||||
fn child_mut(&mut self, _id: Id) -> Option<&mut dyn View> {
|
||||
None
|
||||
}
|
||||
|
||||
fn children(&self) -> Vec<&dyn View> {
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
fn children_mut(&mut self) -> Vec<&mut dyn View> {
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
fn update(
|
||||
&mut self,
|
||||
_cx: &mut floem::context::UpdateCx,
|
||||
_state: Box<dyn std::any::Any>,
|
||||
) -> ChangeFlags {
|
||||
ChangeFlags::default()
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
cx: &mut floem::context::LayoutCx,
|
||||
) -> floem::taffy::prelude::Node {
|
||||
cx.layout_node(self.id, false, |_| Vec::new())
|
||||
}
|
||||
|
||||
fn compute_layout(
|
||||
&mut self,
|
||||
cx: &mut floem::context::LayoutCx,
|
||||
_cx: &mut floem::context::ComputeLayoutCx,
|
||||
) -> Option<floem::peniko::kurbo::Rect> {
|
||||
if let Some(width) = cx.get_layout(self.id).map(|l| l.size.width as f64) {
|
||||
if let Some(width) = self.id.get_layout().map(|l| l.size.width as f64) {
|
||||
self.width = width;
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn event(
|
||||
&mut self,
|
||||
_cx: &mut floem::context::EventCx,
|
||||
_id_path: Option<&[Id]>,
|
||||
_event: floem::event::Event,
|
||||
) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn paint(&mut self, cx: &mut floem::context::PaintCx) {
|
||||
let (view, cursor, viewport, screen_lines, config) =
|
||||
self.editor.with_untracked(|editor| {
|
||||
(
|
||||
editor.view.clone(),
|
||||
editor.cursor,
|
||||
editor.viewport,
|
||||
editor.screen_lines(),
|
||||
editor.common.config,
|
||||
)
|
||||
});
|
||||
let viewport = viewport.get_untracked();
|
||||
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 = view.kind.with_untracked(|kind| kind.is_normal());
|
||||
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 = view.last_line();
|
||||
let current_line = view
|
||||
.doc
|
||||
.with_untracked(|doc| doc.buffer().line_of_offset(offset));
|
||||
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> =
|
||||
FamilyOwned::parse_list(&config.editor.font_family).collect();
|
||||
let attrs = Attrs::new()
|
||||
.family(&family)
|
||||
.color(*config.get_color(LapceColor::EDITOR_DIM))
|
||||
.color(config.color(LapceColor::EDITOR_DIM))
|
||||
.font_size(config.editor.font_size() as f32);
|
||||
let attrs_list = AttrsList::new(attrs);
|
||||
let current_line_attrs_list = AttrsList::new(
|
||||
attrs.color(*config.get_color(LapceColor::EDITOR_FOREGROUND)),
|
||||
);
|
||||
let current_line_attrs_list =
|
||||
AttrsList::new(attrs.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;
|
||||
|
||||
for line in &screen_lines.lines {
|
||||
let line = *line;
|
||||
if line > last_line {
|
||||
break;
|
||||
}
|
||||
|
||||
let text = if show_relative {
|
||||
if line == current_line {
|
||||
line + 1
|
||||
} else {
|
||||
line.abs_diff(current_line)
|
||||
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;
|
||||
}
|
||||
} else {
|
||||
line + 1
|
||||
|
||||
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());
|
||||
} else {
|
||||
text_layout.set_text(&text, attrs_list.clone());
|
||||
}
|
||||
let size = text_layout.size();
|
||||
let height = size.height;
|
||||
|
||||
cx.draw_text(
|
||||
&text_layout,
|
||||
Point::new(
|
||||
(self.width - (size.width)).max(0.0),
|
||||
y + (line_height - height) / 2.0 - viewport.y0,
|
||||
),
|
||||
);
|
||||
}
|
||||
.to_string();
|
||||
});
|
||||
|
||||
let info = screen_lines.info.get(&line).unwrap();
|
||||
let mut text_layout = TextLayout::new();
|
||||
if line == current_line {
|
||||
text_layout.set_text(&text, current_line_attrs_list.clone());
|
||||
} else {
|
||||
text_layout.set_text(&text, attrs_list.clone());
|
||||
}
|
||||
let size = text_layout.size();
|
||||
let height = size.height;
|
||||
let y = info.y;
|
||||
|
||||
cx.draw_text(
|
||||
&text_layout,
|
||||
Point::new(
|
||||
(self.width - (size.width)).max(0.0),
|
||||
y as f64 + (line_height - height) / 2.0 - viewport.y0,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
self.paint_head_changes(cx, view.doc, viewport, kind_is_normal, &config);
|
||||
self.paint_head_changes(cx, &self.editor, viewport, kind_is_normal, &config);
|
||||
self.paint_sticky_headers(cx, kind_is_normal, &config);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use floem::peniko::kurbo::Vec2;
|
||||
use lapce_core::buffer::{rope_text::RopeText, Buffer};
|
||||
use lapce_core::{buffer::rope_text::RopeText, rope_text_pos::RopeTextPosition};
|
||||
use lsp_types::Position;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
@@ -24,12 +24,10 @@ pub enum EditorPosition {
|
||||
}
|
||||
|
||||
impl EditorPosition {
|
||||
pub fn to_offset(&self, buffer: &Buffer) -> usize {
|
||||
pub fn to_offset(&self, text: &impl RopeText) -> usize {
|
||||
match self {
|
||||
EditorPosition::Line(n) => buffer.first_non_blank_character_on_line(*n),
|
||||
EditorPosition::Position(position) => {
|
||||
buffer.offset_of_position(position)
|
||||
}
|
||||
EditorPosition::Line(n) => text.first_non_blank_character_on_line(*n),
|
||||
EditorPosition::Position(position) => text.offset_of_position(position),
|
||||
EditorPosition::Offset(offset) => *offset,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,734 +0,0 @@
|
||||
//! Movement logic for the editor.
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
use lapce_core::{
|
||||
buffer::rope_text::RopeText,
|
||||
command::MultiSelectionCommand,
|
||||
cursor::{ColPosition, Cursor, CursorMode},
|
||||
editor::Editor,
|
||||
mode::{Mode, MotionMode},
|
||||
movement::{LinePosition, Movement},
|
||||
register::Register,
|
||||
selection::{SelRegion, Selection},
|
||||
soft_tab::{snap_to_soft_tab, SnapDirection},
|
||||
};
|
||||
|
||||
use crate::doc::Document;
|
||||
|
||||
use super::view_data::EditorViewData;
|
||||
|
||||
/// Move a selection region by a given movement.
|
||||
/// Much of the time, this will just be a matter of moving the cursor, but
|
||||
/// some movements may depend on the current selection.
|
||||
fn move_region(
|
||||
view: &EditorViewData,
|
||||
region: &SelRegion,
|
||||
count: usize,
|
||||
modify: bool,
|
||||
movement: &Movement,
|
||||
mode: Mode,
|
||||
) -> SelRegion {
|
||||
let (count, region) = if count >= 1 && !modify && !region.is_caret() {
|
||||
// If we're not a caret, and we are moving left/up or right/down, we want to move
|
||||
// the cursor to the left or right side of the selection.
|
||||
// Ex: `|abc|` -> left/up arrow key -> `|abc`
|
||||
// Ex: `|abc|` -> right/down arrow key -> `abc|`
|
||||
// and it doesn't matter which direction the selection is going, so we use min/max
|
||||
match movement {
|
||||
Movement::Left | Movement::Up => {
|
||||
let leftmost = region.min();
|
||||
(count - 1, SelRegion::new(leftmost, leftmost, region.horiz))
|
||||
}
|
||||
Movement::Right | Movement::Down => {
|
||||
let rightmost = region.max();
|
||||
(
|
||||
count - 1,
|
||||
SelRegion::new(rightmost, rightmost, region.horiz),
|
||||
)
|
||||
}
|
||||
_ => (count, *region),
|
||||
}
|
||||
} else {
|
||||
(count, *region)
|
||||
};
|
||||
|
||||
let (end, horiz) = move_offset(
|
||||
view,
|
||||
region.end,
|
||||
region.horiz.as_ref(),
|
||||
count,
|
||||
movement,
|
||||
mode,
|
||||
);
|
||||
let start = match modify {
|
||||
true => region.start,
|
||||
false => end,
|
||||
};
|
||||
SelRegion::new(start, end, horiz)
|
||||
}
|
||||
|
||||
pub fn move_selection(
|
||||
view: &EditorViewData,
|
||||
selection: &Selection,
|
||||
count: usize,
|
||||
modify: bool,
|
||||
movement: &Movement,
|
||||
mode: Mode,
|
||||
) -> Selection {
|
||||
let mut new_selection = Selection::new();
|
||||
for region in selection.regions() {
|
||||
new_selection
|
||||
.add_region(move_region(view, region, count, modify, movement, mode));
|
||||
}
|
||||
new_selection
|
||||
}
|
||||
|
||||
pub fn move_offset(
|
||||
view: &EditorViewData,
|
||||
offset: usize,
|
||||
horiz: Option<&ColPosition>,
|
||||
count: usize,
|
||||
movement: &Movement,
|
||||
mode: Mode,
|
||||
) -> (usize, Option<ColPosition>) {
|
||||
let config = view.config.get_untracked();
|
||||
|
||||
match movement {
|
||||
Movement::Left => {
|
||||
let new_offset = move_left(
|
||||
view.rope_text(),
|
||||
offset,
|
||||
mode,
|
||||
count,
|
||||
config.editor.atomic_soft_tab_width(),
|
||||
);
|
||||
|
||||
(new_offset, None)
|
||||
}
|
||||
Movement::Right => {
|
||||
let new_offset = move_right(
|
||||
view.rope_text(),
|
||||
offset,
|
||||
mode,
|
||||
count,
|
||||
config.editor.atomic_soft_tab_width(),
|
||||
);
|
||||
|
||||
(new_offset, None)
|
||||
}
|
||||
Movement::Up => {
|
||||
let font_size = config.editor.font_size();
|
||||
let (new_offset, horiz) =
|
||||
move_up(view, font_size, offset, horiz.cloned(), mode, count);
|
||||
|
||||
(new_offset, Some(horiz))
|
||||
}
|
||||
Movement::Down => {
|
||||
let font_size = config.editor.font_size();
|
||||
let (new_offset, horiz) =
|
||||
move_down(view, font_size, offset, horiz.cloned(), mode, count);
|
||||
|
||||
(new_offset, Some(horiz))
|
||||
}
|
||||
Movement::DocumentStart => (0, Some(ColPosition::Start)),
|
||||
Movement::DocumentEnd => {
|
||||
let (new_offset, horiz) = document_end(view.rope_text(), mode);
|
||||
|
||||
(new_offset, Some(horiz))
|
||||
}
|
||||
Movement::FirstNonBlank => {
|
||||
let (new_offset, horiz) = first_non_blank(view.rope_text(), offset);
|
||||
|
||||
(new_offset, Some(horiz))
|
||||
}
|
||||
Movement::StartOfLine => {
|
||||
let (new_offset, horiz) = start_of_line(view.rope_text(), offset);
|
||||
|
||||
(new_offset, Some(horiz))
|
||||
}
|
||||
Movement::EndOfLine => {
|
||||
let (new_offset, horiz) = end_of_line(view.rope_text(), offset, mode);
|
||||
|
||||
(new_offset, Some(horiz))
|
||||
}
|
||||
Movement::Line(position) => {
|
||||
let font_size = config.editor.font_size();
|
||||
let (new_offset, horiz) =
|
||||
to_line(view, font_size, offset, horiz.cloned(), mode, position);
|
||||
|
||||
(new_offset, Some(horiz))
|
||||
}
|
||||
Movement::Offset(offset) => {
|
||||
let new_offset = view.text().prev_grapheme_offset(*offset + 1).unwrap();
|
||||
(new_offset, None)
|
||||
}
|
||||
Movement::WordEndForward => {
|
||||
let new_offset = view.rope_text().move_n_wordends_forward(
|
||||
offset,
|
||||
count,
|
||||
mode == Mode::Insert,
|
||||
);
|
||||
(new_offset, None)
|
||||
}
|
||||
Movement::WordForward => {
|
||||
let new_offset = view.rope_text().move_n_words_forward(offset, count);
|
||||
(new_offset, None)
|
||||
}
|
||||
Movement::WordBackward => {
|
||||
let new_offset =
|
||||
view.rope_text().move_n_words_backward(offset, count, mode);
|
||||
(new_offset, None)
|
||||
}
|
||||
Movement::NextUnmatched(char) => {
|
||||
let new_offset = view.find_unmatched(offset, false, *char);
|
||||
|
||||
(new_offset, None)
|
||||
}
|
||||
Movement::PreviousUnmatched(char) => {
|
||||
let new_offset = view.find_unmatched(offset, true, *char);
|
||||
|
||||
(new_offset, None)
|
||||
}
|
||||
Movement::MatchPairs => {
|
||||
let new_offset = view.find_matching_pair(offset);
|
||||
|
||||
(new_offset, None)
|
||||
}
|
||||
Movement::ParagraphForward => {
|
||||
let new_offset =
|
||||
view.rope_text().move_n_paragraphs_forward(offset, count);
|
||||
|
||||
(new_offset, None)
|
||||
}
|
||||
Movement::ParagraphBackward => {
|
||||
let new_offset =
|
||||
view.rope_text().move_n_paragraphs_backward(offset, count);
|
||||
|
||||
(new_offset, None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Move the offset to the left by `count` amount.
|
||||
/// If `soft_tab_width` is `Some` (and greater than 1) then the offset will snap to the soft tab.
|
||||
fn move_left(
|
||||
rope_text: impl RopeText,
|
||||
offset: usize,
|
||||
mode: Mode,
|
||||
count: usize,
|
||||
soft_tab_width: Option<usize>,
|
||||
) -> usize {
|
||||
let mut new_offset = rope_text.move_left(offset, mode, count);
|
||||
|
||||
if let Some(soft_tab_width) = soft_tab_width {
|
||||
if soft_tab_width > 1 {
|
||||
new_offset = snap_to_soft_tab(
|
||||
rope_text.text(),
|
||||
new_offset,
|
||||
SnapDirection::Left,
|
||||
soft_tab_width,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
new_offset
|
||||
}
|
||||
|
||||
/// Move the offset to the right by `count` amount.
|
||||
/// If `soft_tab_width` is `Some` (and greater than 1) then the offset will snap to the soft tab.
|
||||
fn move_right(
|
||||
rope_text: impl RopeText,
|
||||
offset: usize,
|
||||
mode: Mode,
|
||||
count: usize,
|
||||
soft_tab_width: Option<usize>,
|
||||
) -> usize {
|
||||
let mut new_offset = rope_text.move_right(offset, mode, count);
|
||||
|
||||
if let Some(soft_tab_width) = soft_tab_width {
|
||||
if soft_tab_width > 1 {
|
||||
new_offset = snap_to_soft_tab(
|
||||
rope_text.text(),
|
||||
new_offset,
|
||||
SnapDirection::Right,
|
||||
soft_tab_width,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
new_offset
|
||||
}
|
||||
|
||||
// These could be abstracted away from `EditorViewData` by having some trait that has a 'get Rope' and 'get point information about text' functions. Would be useful for testing.
|
||||
/// Move the offset up by `count` amount.
|
||||
fn move_up(
|
||||
view: &EditorViewData,
|
||||
font_size: usize,
|
||||
offset: usize,
|
||||
horiz: Option<ColPosition>,
|
||||
mode: Mode,
|
||||
count: usize,
|
||||
) -> (usize, ColPosition) {
|
||||
let rope_text = view.rope_text();
|
||||
|
||||
let line = rope_text.line_of_offset(offset);
|
||||
|
||||
if line == 0 {
|
||||
let line = rope_text.line_of_offset(offset);
|
||||
let new_offset = rope_text.offset_of_line(line);
|
||||
let horiz = horiz.unwrap_or_else(|| {
|
||||
ColPosition::Col(view.line_point_of_offset(offset, font_size).x)
|
||||
});
|
||||
return (new_offset, horiz);
|
||||
}
|
||||
|
||||
let visual_line = view.visual_line(line).saturating_sub(count);
|
||||
let line = view.actual_line(visual_line, false);
|
||||
|
||||
let horiz = horiz.unwrap_or_else(|| {
|
||||
ColPosition::Col(view.line_point_of_offset(offset, font_size).x)
|
||||
});
|
||||
let col = view.line_horiz_col(line, font_size, &horiz, mode != Mode::Normal);
|
||||
let new_offset = rope_text.offset_of_line_col(line, col);
|
||||
|
||||
(new_offset, horiz)
|
||||
}
|
||||
|
||||
/// Move the offset down by `count` amount.
|
||||
fn move_down(
|
||||
view: &EditorViewData,
|
||||
font_size: usize,
|
||||
offset: usize,
|
||||
horiz: Option<ColPosition>,
|
||||
mode: Mode,
|
||||
count: usize,
|
||||
) -> (usize, ColPosition) {
|
||||
let rope_text = view.rope_text();
|
||||
|
||||
let last_line = rope_text.last_line();
|
||||
let line = rope_text.line_of_offset(offset);
|
||||
if line == last_line {
|
||||
let new_offset = rope_text.offset_line_end(offset, mode != Mode::Normal);
|
||||
let horiz = horiz.unwrap_or_else(|| {
|
||||
ColPosition::Col(view.line_point_of_offset(offset, font_size).x)
|
||||
});
|
||||
return (new_offset, horiz);
|
||||
}
|
||||
|
||||
let visual_line = view.visual_line(line);
|
||||
let line = view.actual_line(visual_line + count, true);
|
||||
let line = line.min(last_line);
|
||||
|
||||
let horiz = horiz.unwrap_or_else(|| {
|
||||
ColPosition::Col(view.line_point_of_offset(offset, font_size).x)
|
||||
});
|
||||
let col = view.line_horiz_col(line, font_size, &horiz, mode != Mode::Normal);
|
||||
let new_offset = rope_text.offset_of_line_col(line, col);
|
||||
|
||||
(new_offset, horiz)
|
||||
}
|
||||
|
||||
fn document_end(rope_text: impl RopeText, mode: Mode) -> (usize, ColPosition) {
|
||||
let last_offset =
|
||||
rope_text.offset_line_end(rope_text.len(), mode != Mode::Normal);
|
||||
|
||||
(last_offset, ColPosition::End)
|
||||
}
|
||||
|
||||
fn first_non_blank(rope_text: impl RopeText, offset: usize) -> (usize, ColPosition) {
|
||||
let line = rope_text.line_of_offset(offset);
|
||||
let non_blank_offset = rope_text.first_non_blank_character_on_line(line);
|
||||
let start_line_offset = rope_text.offset_of_line(line);
|
||||
if offset > non_blank_offset {
|
||||
// Jump to the first non-whitespace character if we're strictly after it
|
||||
(non_blank_offset, ColPosition::FirstNonBlank)
|
||||
} else {
|
||||
// If we're at the start of the line, also jump to the first not blank
|
||||
if start_line_offset == offset {
|
||||
(non_blank_offset, ColPosition::FirstNonBlank)
|
||||
} else {
|
||||
// Otherwise, jump to the start of the line
|
||||
(start_line_offset, ColPosition::Start)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn start_of_line(rope_text: impl RopeText, offset: usize) -> (usize, ColPosition) {
|
||||
let line = rope_text.line_of_offset(offset);
|
||||
let new_offset = rope_text.offset_of_line(line);
|
||||
|
||||
(new_offset, ColPosition::Start)
|
||||
}
|
||||
|
||||
fn end_of_line(
|
||||
rope_text: impl RopeText,
|
||||
offset: usize,
|
||||
mode: Mode,
|
||||
) -> (usize, ColPosition) {
|
||||
let new_offset = rope_text.offset_line_end(offset, mode != Mode::Normal);
|
||||
|
||||
(new_offset, ColPosition::End)
|
||||
}
|
||||
|
||||
fn to_line(
|
||||
view: &EditorViewData,
|
||||
font_size: usize,
|
||||
offset: usize,
|
||||
horiz: Option<ColPosition>,
|
||||
mode: Mode,
|
||||
position: &LinePosition,
|
||||
) -> (usize, ColPosition) {
|
||||
let rope_text = view.rope_text();
|
||||
|
||||
let line = match position {
|
||||
LinePosition::Line(line) => (line - 1).min(rope_text.last_line()),
|
||||
LinePosition::First => 0,
|
||||
LinePosition::Last => rope_text.last_line(),
|
||||
};
|
||||
let horiz = horiz.unwrap_or_else(|| {
|
||||
ColPosition::Col(view.line_point_of_offset(offset, font_size).x)
|
||||
});
|
||||
let col = view.line_horiz_col(line, font_size, &horiz, mode != Mode::Normal);
|
||||
let new_offset = rope_text.offset_of_line_col(line, col);
|
||||
|
||||
(new_offset, horiz)
|
||||
}
|
||||
|
||||
// TODO: passing a view with the document is kinda ehhh, because this would be used when you call
|
||||
// .update on the `RwSignal<Document>`, which presumably delinks it from the RwSignal the view has.
|
||||
// A solution might be to change the caller to only pass in the editor view and have this do the update, but only when it is needed.
|
||||
|
||||
/// Move the current cursor.
|
||||
/// This will signal-update the document for some motion modes.
|
||||
pub fn move_cursor(
|
||||
view: &EditorViewData,
|
||||
cursor: &mut Cursor,
|
||||
movement: &Movement,
|
||||
count: usize,
|
||||
modify: bool,
|
||||
register: &mut Register,
|
||||
) {
|
||||
match cursor.mode {
|
||||
CursorMode::Normal(offset) => {
|
||||
let (new_offset, horiz) = move_offset(
|
||||
view,
|
||||
offset,
|
||||
cursor.horiz.as_ref(),
|
||||
count,
|
||||
movement,
|
||||
Mode::Normal,
|
||||
);
|
||||
if let Some(motion_mode) = cursor.motion_mode.clone() {
|
||||
let (moved_new_offset, _) = move_offset(
|
||||
view,
|
||||
new_offset,
|
||||
None,
|
||||
1,
|
||||
&Movement::Right,
|
||||
Mode::Insert,
|
||||
);
|
||||
let (start, end) = match movement {
|
||||
Movement::EndOfLine | Movement::WordEndForward => {
|
||||
(offset, moved_new_offset)
|
||||
}
|
||||
Movement::MatchPairs => {
|
||||
if new_offset > offset {
|
||||
(offset, moved_new_offset)
|
||||
} else {
|
||||
(moved_new_offset, new_offset)
|
||||
}
|
||||
}
|
||||
_ => (offset, new_offset),
|
||||
};
|
||||
view.doc.update(|doc| {
|
||||
let deltas = Editor::execute_motion_mode(
|
||||
cursor,
|
||||
doc.buffer_mut(),
|
||||
motion_mode,
|
||||
start,
|
||||
end,
|
||||
movement.is_vertical(),
|
||||
register,
|
||||
);
|
||||
doc.apply_deltas(&deltas);
|
||||
});
|
||||
cursor.motion_mode = None;
|
||||
} else {
|
||||
cursor.mode = CursorMode::Normal(new_offset);
|
||||
cursor.horiz = horiz;
|
||||
}
|
||||
}
|
||||
CursorMode::Visual { start, end, mode } => {
|
||||
let (new_offset, horiz) = move_offset(
|
||||
view,
|
||||
end,
|
||||
cursor.horiz.as_ref(),
|
||||
count,
|
||||
movement,
|
||||
Mode::Visual,
|
||||
);
|
||||
cursor.mode = CursorMode::Visual {
|
||||
start,
|
||||
end: new_offset,
|
||||
mode,
|
||||
};
|
||||
cursor.horiz = horiz;
|
||||
}
|
||||
CursorMode::Insert(ref selection) => {
|
||||
let selection = move_selection(
|
||||
view,
|
||||
selection,
|
||||
count,
|
||||
modify,
|
||||
movement,
|
||||
Mode::Insert,
|
||||
);
|
||||
cursor.set_insert(selection);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn do_multi_selection(
|
||||
view: &EditorViewData,
|
||||
cursor: &mut Cursor,
|
||||
cmd: &MultiSelectionCommand,
|
||||
) {
|
||||
use MultiSelectionCommand::*;
|
||||
let rope_text = view.rope_text();
|
||||
|
||||
match cmd {
|
||||
SelectUndo => {
|
||||
if let CursorMode::Insert(_) = cursor.mode.clone() {
|
||||
if let Some(selection) = cursor.history_selections.last().cloned() {
|
||||
cursor.mode = CursorMode::Insert(selection);
|
||||
}
|
||||
cursor.history_selections.pop();
|
||||
}
|
||||
}
|
||||
InsertCursorAbove => {
|
||||
if let CursorMode::Insert(mut selection) = cursor.mode.clone() {
|
||||
let offset = selection.first().map(|s| s.end).unwrap_or(0);
|
||||
let (new_offset, _) = move_offset(
|
||||
view,
|
||||
offset,
|
||||
cursor.horiz.as_ref(),
|
||||
1,
|
||||
&Movement::Up,
|
||||
Mode::Insert,
|
||||
);
|
||||
if new_offset != offset {
|
||||
selection
|
||||
.add_region(SelRegion::new(new_offset, new_offset, None));
|
||||
}
|
||||
cursor.set_insert(selection);
|
||||
}
|
||||
}
|
||||
InsertCursorBelow => {
|
||||
if let CursorMode::Insert(mut selection) = cursor.mode.clone() {
|
||||
let offset = selection.last().map(|s| s.end).unwrap_or(0);
|
||||
let (new_offset, _) = move_offset(
|
||||
view,
|
||||
offset,
|
||||
cursor.horiz.as_ref(),
|
||||
1,
|
||||
&Movement::Down,
|
||||
Mode::Insert,
|
||||
);
|
||||
if new_offset != offset {
|
||||
selection
|
||||
.add_region(SelRegion::new(new_offset, new_offset, None));
|
||||
}
|
||||
cursor.set_insert(selection);
|
||||
}
|
||||
}
|
||||
InsertCursorEndOfLine => {
|
||||
if let CursorMode::Insert(selection) = cursor.mode.clone() {
|
||||
let mut new_selection = Selection::new();
|
||||
for region in selection.regions() {
|
||||
let (start_line, _) = rope_text.offset_to_line_col(region.min());
|
||||
let (end_line, end_col) =
|
||||
rope_text.offset_to_line_col(region.max());
|
||||
for line in start_line..end_line + 1 {
|
||||
let offset = if line == end_line {
|
||||
rope_text.offset_of_line_col(line, end_col)
|
||||
} else {
|
||||
rope_text.line_end_offset(line, true)
|
||||
};
|
||||
new_selection
|
||||
.add_region(SelRegion::new(offset, offset, None));
|
||||
}
|
||||
}
|
||||
cursor.set_insert(new_selection);
|
||||
}
|
||||
}
|
||||
SelectCurrentLine => {
|
||||
if let CursorMode::Insert(selection) = cursor.mode.clone() {
|
||||
let mut new_selection = Selection::new();
|
||||
for region in selection.regions() {
|
||||
let start_line = rope_text.line_of_offset(region.min());
|
||||
let start = rope_text.offset_of_line(start_line);
|
||||
let end_line = rope_text.line_of_offset(region.max());
|
||||
let end = rope_text.offset_of_line(end_line + 1);
|
||||
new_selection.add_region(SelRegion::new(start, end, None));
|
||||
}
|
||||
cursor.set_insert(new_selection);
|
||||
}
|
||||
}
|
||||
SelectAllCurrent => {
|
||||
if let CursorMode::Insert(mut selection) = cursor.mode.clone() {
|
||||
if !selection.is_empty() {
|
||||
let find = view.find();
|
||||
let config = view.config.get_untracked();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
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 = view.find();
|
||||
let config = view.config.get_untracked();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
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 = view.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);
|
||||
}
|
||||
}
|
||||
SelectAll => {
|
||||
let new_selection = Selection::region(0, rope_text.len());
|
||||
cursor.set_insert(new_selection);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn do_motion_mode(
|
||||
doc: &mut Document,
|
||||
cursor: &mut Cursor,
|
||||
motion_mode: MotionMode,
|
||||
register: &mut Register,
|
||||
) {
|
||||
if let Some(m) = &cursor.motion_mode {
|
||||
if m == &motion_mode {
|
||||
let offset = cursor.offset();
|
||||
let deltas = Editor::execute_motion_mode(
|
||||
cursor,
|
||||
doc.buffer_mut(),
|
||||
motion_mode,
|
||||
offset,
|
||||
offset,
|
||||
true,
|
||||
register,
|
||||
);
|
||||
doc.apply_deltas(&deltas);
|
||||
}
|
||||
cursor.motion_mode = None;
|
||||
} else {
|
||||
cursor.motion_mode = Some(motion_mode);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Write tests for the various functions.
|
||||
@@ -1,747 +0,0 @@
|
||||
use std::{cell::RefCell, collections::HashMap, rc::Rc, sync::Arc};
|
||||
|
||||
use floem::{
|
||||
cosmic_text::TextLayout,
|
||||
peniko::{kurbo::Point, Color},
|
||||
reactive::{ReadSignal, RwSignal, Scope},
|
||||
views::VirtualListVector,
|
||||
};
|
||||
use lapce_core::{
|
||||
buffer::{
|
||||
diff::DiffLines,
|
||||
rope_text::{RopeText, RopeTextVal},
|
||||
},
|
||||
char_buffer::CharBuffer,
|
||||
cursor::ColPosition,
|
||||
mode::Mode,
|
||||
soft_tab::{snap_to_soft_tab_line_col, SnapDirection},
|
||||
word::WordCursor,
|
||||
};
|
||||
use lapce_xi_rope::Rope;
|
||||
|
||||
use crate::{
|
||||
config::LapceConfig,
|
||||
doc::{phantom_text::PhantomTextLine, Document},
|
||||
find::{Find, FindResult},
|
||||
};
|
||||
|
||||
use super::diff::DiffInfo;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct LineExtraStyle {
|
||||
pub x: f64,
|
||||
pub width: Option<f64>,
|
||||
pub bg_color: Option<Color>,
|
||||
pub under_line: Option<Color>,
|
||||
pub wave_line: Option<Color>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TextLayoutLine {
|
||||
/// Extra styling that should be applied to the text
|
||||
/// (x0, x1 or line display end, style)
|
||||
pub extra_style: Vec<LineExtraStyle>,
|
||||
pub text: TextLayout,
|
||||
pub whitespaces: Option<Vec<(char, (f64, f64))>>,
|
||||
pub indent: f64,
|
||||
}
|
||||
|
||||
/// Keeps track of the text layouts so that we can efficiently reuse them.
|
||||
#[derive(Clone, Default)]
|
||||
pub struct TextLayoutCache {
|
||||
/// The id of the last config, which lets us know when the config changes so we can update
|
||||
/// the cache.
|
||||
config_id: u64,
|
||||
cache_rev: u64,
|
||||
/// (Font Size -> (Line Number -> Text Layout))
|
||||
/// Different font-sizes are cached separately, which is useful for features like code lens
|
||||
/// where the text becomes small but you may wish to revert quickly.
|
||||
pub layouts: HashMap<usize, HashMap<usize, Arc<TextLayoutLine>>>,
|
||||
pub max_width: f64,
|
||||
}
|
||||
|
||||
impl TextLayoutCache {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
config_id: 0,
|
||||
cache_rev: 0,
|
||||
layouts: HashMap::new(),
|
||||
max_width: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear(&mut self, cache_rev: u64) {
|
||||
self.layouts.clear();
|
||||
self.cache_rev = cache_rev;
|
||||
self.max_width = 0.0;
|
||||
}
|
||||
|
||||
pub fn check_attributes(&mut self, config_id: u64) {
|
||||
if self.config_id != config_id {
|
||||
self.clear(self.cache_rev + 1);
|
||||
self.config_id = config_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DocLine {
|
||||
pub rev: u64,
|
||||
pub style_rev: u64,
|
||||
pub line: usize,
|
||||
pub text: Arc<TextLayoutLine>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum EditorViewKind {
|
||||
Normal,
|
||||
Diff(DiffInfo),
|
||||
}
|
||||
|
||||
impl EditorViewKind {
|
||||
pub fn is_normal(&self) -> bool {
|
||||
matches!(self, EditorViewKind::Normal)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(minor): Should this go in another file? It doesn't really need to be with the drawing code for editor views
|
||||
/// Data specific to the rendering of a view.
|
||||
/// This has various helper methods that may dispatch to the held [`Document`] signal, but are
|
||||
/// untracked by default. If you need your signal to depend on the document,
|
||||
/// then you should call [`EditorViewData::track_doc`].
|
||||
/// Note: This should be cheap to clone.
|
||||
#[derive(Clone)]
|
||||
pub struct EditorViewData {
|
||||
/// Equivalent to the `EditorData::doc` that contains this view.
|
||||
pub doc: RwSignal<Document>,
|
||||
pub kind: RwSignal<EditorViewKind>,
|
||||
/// The text layouts for the document. This may be shared with other views.
|
||||
pub text_layouts: Rc<RefCell<TextLayoutCache>>,
|
||||
|
||||
pub config: ReadSignal<Arc<LapceConfig>>,
|
||||
}
|
||||
|
||||
impl VirtualListVector<DocLine> for EditorViewData {
|
||||
type ItemIterator = std::vec::IntoIter<DocLine>;
|
||||
|
||||
fn total_len(&self) -> usize {
|
||||
self.num_lines()
|
||||
}
|
||||
|
||||
fn slice(&mut self, range: std::ops::Range<usize>) -> Self::ItemIterator {
|
||||
let lines = range
|
||||
.into_iter()
|
||||
.map(|line| DocLine {
|
||||
rev: self.rev(),
|
||||
style_rev: self.style_rev(),
|
||||
line,
|
||||
text: self.get_text_layout(line, 12),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
lines.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl EditorViewData {
|
||||
pub fn new(
|
||||
doc: RwSignal<Document>,
|
||||
kind: RwSignal<EditorViewKind>,
|
||||
config: ReadSignal<Arc<LapceConfig>>,
|
||||
) -> EditorViewData {
|
||||
EditorViewData {
|
||||
doc,
|
||||
kind,
|
||||
text_layouts: Rc::new(RefCell::new(TextLayoutCache::new())),
|
||||
config,
|
||||
}
|
||||
}
|
||||
|
||||
// Note: There is no document / buffer function as that would require cloning it
|
||||
// since we can't get a reference out of the RwSignal document.
|
||||
|
||||
/// Subscribe to the doc signal, since the getter functions use `with_untracked` by default.
|
||||
pub fn track_doc(&self) {
|
||||
self.doc.track();
|
||||
}
|
||||
|
||||
/// Return the underlying `Rope` of the document.
|
||||
pub fn text(&self) -> Rope {
|
||||
self.doc.with_untracked(|doc| doc.buffer().text().clone())
|
||||
}
|
||||
|
||||
/// Return a [`RopeTextVal`] wrapper.
|
||||
/// Unfortunately, we can't implement [`RopeText`] directly on [`EditorViewData`] due to
|
||||
/// it not having a reference to the rope.
|
||||
pub fn rope_text(&self) -> RopeTextVal {
|
||||
RopeTextVal::new(self.text())
|
||||
}
|
||||
|
||||
/// Return the [`Document`]'s [`Find`] instance. Find uses signals, and so can be updated.
|
||||
pub fn find(&self) -> Find {
|
||||
self.doc.with_untracked(|doc| doc.find().clone())
|
||||
}
|
||||
|
||||
pub fn find_result(&self) -> FindResult {
|
||||
self.doc.with_untracked(|doc| doc.find_result.clone())
|
||||
}
|
||||
|
||||
pub fn update_find(&self, min_line: usize, max_line: usize) {
|
||||
self.doc
|
||||
.with_untracked(|doc| doc.update_find(min_line, max_line));
|
||||
}
|
||||
|
||||
/// The current revision of the underlying buffer. This is used to track when the buffer has
|
||||
/// changed.
|
||||
pub fn rev(&self) -> u64 {
|
||||
self.doc.with_untracked(|doc| doc.rev())
|
||||
}
|
||||
|
||||
// TODO: Should the editor view handle the style information?
|
||||
// It does not need to, since we can just get that information from the document,
|
||||
// but it might be nicer? It would let us make the document more strictly about the
|
||||
// data and the view more about the rendering.
|
||||
// but you could also consider it as the document managing the syntax it should apply
|
||||
// Though. There is the question of whether we'd want to allow different syntax highlighting in
|
||||
// different views. However, we probably wouldn't.
|
||||
|
||||
pub fn style_rev(&self) -> u64 {
|
||||
self.doc.with_untracked(|doc| doc.cache_rev())
|
||||
}
|
||||
|
||||
/// The document for the given view was swapped out.
|
||||
pub fn update_doc(&mut self, doc: RwSignal<Document>) {
|
||||
self.doc = doc;
|
||||
self.text_layouts.borrow_mut().clear(0);
|
||||
}
|
||||
|
||||
/// Duplicate as a new view which refers to the same document.
|
||||
pub fn duplicate(&self, cx: Scope) -> Self {
|
||||
// TODO: This is correct right now, as it has the views share the same text layout cache.
|
||||
// However, once we have line wrapping or other view-specific rendering changes, this should check for whether they're different.
|
||||
// This will likely require more information to be passed into duplicate,
|
||||
// like whether the wrap width will be the editor's width, and so it is unlikely to be exactly the same as the current view.
|
||||
EditorViewData {
|
||||
doc: self.doc,
|
||||
text_layouts: Rc::new(RefCell::new(TextLayoutCache::new())),
|
||||
kind: cx.create_rw_signal(self.kind.get_untracked()),
|
||||
config: self.config,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn line_phantom_text(&self, line: usize) -> PhantomTextLine {
|
||||
self.doc.with_untracked(|doc| doc.line_phantom_text(line))
|
||||
}
|
||||
|
||||
/// Get the text layout for the given line.
|
||||
/// If the text layout is not cached, it will be created and cached.
|
||||
pub fn get_text_layout(
|
||||
&self,
|
||||
line: usize,
|
||||
font_size: usize,
|
||||
) -> Arc<TextLayoutLine> {
|
||||
{
|
||||
let mut text_layouts = self.text_layouts.borrow_mut();
|
||||
self.doc.with_untracked(|doc| {
|
||||
if doc.cache_rev() != text_layouts.cache_rev {
|
||||
text_layouts.clear(doc.cache_rev());
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: Should we just move the config cache check into `check_cache`?
|
||||
let config = self.config.get_untracked();
|
||||
// Check if the text layout needs to update due to the config being changed
|
||||
self.text_layouts.borrow_mut().check_attributes(config.id);
|
||||
// If we don't have a second layer of the hashmap initialized for this specific font size,
|
||||
// do it now
|
||||
if self.text_layouts.borrow().layouts.get(&font_size).is_none() {
|
||||
let mut cache = self.text_layouts.borrow_mut();
|
||||
cache.layouts.insert(font_size, HashMap::new());
|
||||
}
|
||||
|
||||
// Get whether there's an entry for this specific font size and line
|
||||
let cache_exists = self
|
||||
.text_layouts
|
||||
.borrow()
|
||||
.layouts
|
||||
.get(&font_size)
|
||||
.unwrap()
|
||||
.get(&line)
|
||||
.is_some();
|
||||
// If there isn't an entry then we actually have to create it
|
||||
if !cache_exists {
|
||||
let text_layout = self
|
||||
.doc
|
||||
.with_untracked(|doc| doc.get_text_layout(line, font_size));
|
||||
let mut cache = self.text_layouts.borrow_mut();
|
||||
let width = text_layout.text.size().width;
|
||||
if width > cache.max_width {
|
||||
cache.max_width = width;
|
||||
}
|
||||
cache
|
||||
.layouts
|
||||
.get_mut(&font_size)
|
||||
.unwrap()
|
||||
.insert(line, text_layout);
|
||||
}
|
||||
|
||||
// Just get the entry, assuming it has been created because we initialize it above.
|
||||
self.text_layouts
|
||||
.borrow()
|
||||
.layouts
|
||||
.get(&font_size)
|
||||
.unwrap()
|
||||
.get(&line)
|
||||
.cloned()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn indent_unit(&self) -> &'static str {
|
||||
self.doc.with_untracked(|doc| doc.buffer().indent_unit())
|
||||
}
|
||||
|
||||
// ==== Position Information ====
|
||||
|
||||
/// The number of visual lines in the document.
|
||||
pub fn num_lines(&self) -> usize {
|
||||
self.doc.with_untracked(|doc| doc.buffer().num_lines())
|
||||
}
|
||||
|
||||
/// The last allowed line in the document.
|
||||
pub fn last_line(&self) -> usize {
|
||||
self.doc.with_untracked(|doc| doc.buffer().last_line())
|
||||
}
|
||||
|
||||
// ==== Line/Column Positioning ====
|
||||
|
||||
/// Convert an offset into the buffer into a line and column.
|
||||
pub fn offset_to_line_col(&self, offset: usize) -> (usize, usize) {
|
||||
self.doc
|
||||
.with_untracked(|doc| doc.buffer().offset_to_line_col(offset))
|
||||
}
|
||||
|
||||
pub fn offset_of_line(&self, offset: usize) -> usize {
|
||||
self.doc
|
||||
.with_untracked(|doc| doc.buffer().offset_of_line(offset))
|
||||
}
|
||||
|
||||
pub fn offset_of_line_col(&self, line: usize, col: usize) -> usize {
|
||||
self.doc
|
||||
.with_untracked(|doc| doc.buffer().offset_of_line_col(line, col))
|
||||
}
|
||||
|
||||
pub fn line_of_offset(&self, offset: usize) -> usize {
|
||||
self.doc
|
||||
.with_untracked(|doc| doc.buffer().line_of_offset(offset))
|
||||
}
|
||||
|
||||
/// Returns the offset into the buffer of the first non blank character on the given line.
|
||||
pub fn first_non_blank_character_on_line(&self, line: usize) -> usize {
|
||||
self.doc.with_untracked(|doc| {
|
||||
doc.buffer().first_non_blank_character_on_line(line)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn line_end_col(&self, line: usize, caret: bool) -> usize {
|
||||
self.doc
|
||||
.with_untracked(|doc| doc.buffer().line_end_col(line, caret))
|
||||
}
|
||||
|
||||
pub fn select_word(&self, offset: usize) -> (usize, usize) {
|
||||
self.doc
|
||||
.with_untracked(|doc| doc.buffer().select_word(offset))
|
||||
}
|
||||
|
||||
// ==== Points of locations ====
|
||||
|
||||
/// Returns the point into the text layout of the line at the given offset.
|
||||
/// `x` being the leading edge of the character, and `y` being the baseline.
|
||||
pub fn line_point_of_offset(&self, offset: usize, font_size: usize) -> Point {
|
||||
let (line, col) = self.offset_to_line_col(offset);
|
||||
self.line_point_of_line_col(line, col, font_size)
|
||||
}
|
||||
|
||||
/// Returns the point into the text layout of the line at the given line and column.
|
||||
/// `x` being the leading edge of the character, and `y` being the baseline.
|
||||
pub fn line_point_of_line_col(
|
||||
&self,
|
||||
line: usize,
|
||||
col: usize,
|
||||
font_size: usize,
|
||||
) -> Point {
|
||||
let text_layout = self.get_text_layout(line, font_size);
|
||||
text_layout.text.hit_position(col).point
|
||||
}
|
||||
|
||||
/// Get the (point above, point below) of a particular offset within the editor.
|
||||
pub fn points_of_offset(&self, offset: usize) -> (Point, Point) {
|
||||
let (line, col) = self.offset_to_line_col(offset);
|
||||
self.points_of_line_col(line, col)
|
||||
}
|
||||
|
||||
/// Get the (point above, point below) of a particular (line, col) within the editor.
|
||||
pub fn points_of_line_col(&self, line: usize, col: usize) -> (Point, Point) {
|
||||
let config = self.config.get_untracked();
|
||||
let (line_height, font_size) =
|
||||
(config.editor.line_height(), config.editor.font_size());
|
||||
|
||||
let y = self.visual_line(line) * line_height;
|
||||
|
||||
let line = line.min(self.last_line());
|
||||
|
||||
let phantom_text = self.line_phantom_text(line);
|
||||
let col = phantom_text.col_after(col, false);
|
||||
|
||||
let mut x_shift = 0.0;
|
||||
if font_size < config.editor.font_size() {
|
||||
let mut col = 0usize;
|
||||
self.doc.with_untracked(|doc| {
|
||||
let line_content = doc.buffer().line_content(line);
|
||||
for ch in line_content.chars() {
|
||||
if ch == ' ' || ch == '\t' {
|
||||
col += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if col > 0 {
|
||||
let normal_text_layout =
|
||||
self.get_text_layout(line, config.editor.font_size());
|
||||
let small_text_layout = self.get_text_layout(line, font_size);
|
||||
x_shift = normal_text_layout.text.hit_position(col).point.x
|
||||
- small_text_layout.text.hit_position(col).point.x;
|
||||
}
|
||||
}
|
||||
|
||||
let x = self.line_point_of_line_col(line, col, font_size).x + x_shift;
|
||||
(
|
||||
Point::new(x, y as f64),
|
||||
Point::new(x, (y + line_height) as f64),
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the offset of a particular point within the editor.
|
||||
/// The boolean indicates whether the point is inside the text or not
|
||||
/// Points outside of vertical bounds will return the last line.
|
||||
/// Points outside of horizontal bounds will return the last column on the line.
|
||||
pub fn offset_of_point(&self, mode: Mode, point: Point) -> (usize, bool) {
|
||||
let ((line, col), is_inside) = self.line_col_of_point(mode, point);
|
||||
(self.offset_of_line_col(line, col), is_inside)
|
||||
}
|
||||
|
||||
/// Get the (line, col) of a particular point within the editor.
|
||||
/// The boolean indicates whether the point is within the text bounds.
|
||||
/// Points outside of vertical bounds will return the last line.
|
||||
/// Points outside of horizontal bounds will return the last column on the line.
|
||||
pub fn line_col_of_point(
|
||||
&self,
|
||||
mode: Mode,
|
||||
point: Point,
|
||||
) -> ((usize, usize), bool) {
|
||||
let config = self.config.get_untracked();
|
||||
|
||||
let visual_line =
|
||||
(point.y / config.editor.line_height() as f64).floor() as usize;
|
||||
let line = self.actual_line(visual_line, true);
|
||||
let line = line.min(self.last_line());
|
||||
let font_size = config.editor.font_size();
|
||||
let text_layout = self.get_text_layout(line, font_size);
|
||||
let hit_point = text_layout.text.hit_point(Point::new(point.x, 0.0));
|
||||
// We have to unapply the phantom text shifting in order to get back to the column in
|
||||
// the actual buffer
|
||||
let phantom_text = self.line_phantom_text(line);
|
||||
let col = phantom_text.before_col(hit_point.index);
|
||||
// Ensure that the column doesn't end up out of bounds, so things like clicking on the far
|
||||
// right end will just go to the end of the line.
|
||||
let max_col = self.line_end_col(line, mode != Mode::Normal);
|
||||
let mut col = col.min(max_col);
|
||||
|
||||
if config.editor.atomic_soft_tabs && config.editor.tab_width > 1 {
|
||||
col = snap_to_soft_tab_line_col(
|
||||
&self.text(),
|
||||
line,
|
||||
col,
|
||||
SnapDirection::Nearest,
|
||||
config.editor.tab_width,
|
||||
);
|
||||
}
|
||||
|
||||
((line, col), hit_point.is_inside)
|
||||
}
|
||||
|
||||
pub fn line_horiz_col(
|
||||
&self,
|
||||
line: usize,
|
||||
font_size: usize,
|
||||
horiz: &ColPosition,
|
||||
caret: bool,
|
||||
) -> usize {
|
||||
match *horiz {
|
||||
ColPosition::Col(x) => {
|
||||
let text_layout = self.get_text_layout(line, font_size);
|
||||
let hit_point = text_layout.text.hit_point(Point::new(x, 0.0));
|
||||
let n = hit_point.index;
|
||||
|
||||
n.min(self.line_end_col(line, caret))
|
||||
}
|
||||
ColPosition::End => self.line_end_col(line, caret),
|
||||
ColPosition::Start => 0,
|
||||
ColPosition::FirstNonBlank => {
|
||||
self.first_non_blank_character_on_line(line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Advance to the right in the manner of the given mode.
|
||||
/// This is not the same as the [`Movement::Right`] command.
|
||||
pub fn move_right(&self, offset: usize, mode: Mode, count: usize) -> usize {
|
||||
self.doc
|
||||
.with_untracked(|doc| doc.buffer().move_right(offset, mode, count))
|
||||
}
|
||||
|
||||
/// Advance to the left in the manner of the given mode.
|
||||
/// This is not the same as the [`Movement::Left`] command.
|
||||
pub fn move_left(&self, offset: usize, mode: Mode, count: usize) -> usize {
|
||||
self.doc
|
||||
.with_untracked(|doc| doc.buffer().move_left(offset, mode, count))
|
||||
}
|
||||
|
||||
/// Find the next/previous offset of the match of the given character.
|
||||
/// This is intended for use by the [`Movement::NextUnmatched`] and
|
||||
/// [`Movement::PreviousUnmatched`] commands.
|
||||
pub fn find_unmatched(&self, offset: usize, previous: bool, ch: char) -> usize {
|
||||
// This needs the doc's syntax, but it isn't cheap to clone
|
||||
// so this has to be a method on view for now.
|
||||
self.doc.with_untracked(|doc| {
|
||||
let syntax = doc.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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Find the offset of the matching pair character.
|
||||
/// This is intended for use by the [`Movement::MatchPairs`] command.
|
||||
pub fn find_matching_pair(&self, offset: usize) -> usize {
|
||||
// This needs the doc's syntax, but it isn't cheap to clone
|
||||
// so this has to be a method on view for now.
|
||||
self.doc.with_untracked(|doc| {
|
||||
let syntax = doc.syntax();
|
||||
if syntax.layers.is_some() {
|
||||
syntax.find_matching_pair(offset).unwrap_or(offset)
|
||||
} else {
|
||||
WordCursor::new(&self.text(), offset)
|
||||
.match_pairs()
|
||||
.unwrap_or(offset)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,34 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use floem::{
|
||||
peniko::kurbo::{Point, Rect},
|
||||
reactive::{RwSignal, Scope},
|
||||
peniko::{
|
||||
kurbo::{Point, Rect},
|
||||
Color,
|
||||
},
|
||||
reactive::{create_memo, create_rw_signal, Memo, ReadSignal, RwSignal, Scope},
|
||||
views::editor::id::EditorId,
|
||||
};
|
||||
use lapce_rpc::plugin::VoltID;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
doc::{DocContent, Document},
|
||||
config::{color::LapceColor, icon::LapceIcons, LapceConfig},
|
||||
doc::{Doc, DocContent},
|
||||
editor::{
|
||||
diff::{DiffEditorData, DiffEditorInfo},
|
||||
location::EditorLocation,
|
||||
EditorData, EditorInfo,
|
||||
},
|
||||
id::{DiffEditorId, EditorId, EditorTabId, KeymapId, SettingsId, SplitId},
|
||||
main_split::MainSplitData,
|
||||
id::{
|
||||
DiffEditorId, EditorTabId, KeymapId, SettingsId, SplitId,
|
||||
ThemeColorSettingsId, VoltViewId,
|
||||
},
|
||||
main_split::{Editors, MainSplitData},
|
||||
plugin::PluginData,
|
||||
window_tab::WindowTabData,
|
||||
};
|
||||
|
||||
@@ -23,7 +37,9 @@ pub enum EditorTabChildInfo {
|
||||
Editor(EditorInfo),
|
||||
DiffEditor(DiffEditorInfo),
|
||||
Settings,
|
||||
ThemeColorSettings,
|
||||
Keymap,
|
||||
Volt(VoltID),
|
||||
}
|
||||
|
||||
impl EditorTabChildInfo {
|
||||
@@ -34,10 +50,8 @@ impl EditorTabChildInfo {
|
||||
) -> EditorTabChild {
|
||||
match &self {
|
||||
EditorTabChildInfo::Editor(editor_info) => {
|
||||
let editor_data = editor_info.to_data(data, editor_tab_id);
|
||||
EditorTabChild::Editor(
|
||||
editor_data.with_untracked(|editor_data| editor_data.editor_id),
|
||||
)
|
||||
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);
|
||||
@@ -46,7 +60,13 @@ impl EditorTabChildInfo {
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -78,6 +98,7 @@ impl EditorTabInfo {
|
||||
.map(|child| {
|
||||
(
|
||||
cx.create_rw_signal(0),
|
||||
cx.create_rw_signal(Rect::ZERO),
|
||||
child.to_data(data.clone(), editor_tab_id),
|
||||
)
|
||||
})
|
||||
@@ -100,17 +121,13 @@ impl EditorTabInfo {
|
||||
}
|
||||
|
||||
pub enum EditorTabChildSource {
|
||||
Editor {
|
||||
path: PathBuf,
|
||||
doc: RwSignal<Document>,
|
||||
},
|
||||
DiffEditor {
|
||||
left: RwSignal<Document>,
|
||||
right: RwSignal<Document>,
|
||||
},
|
||||
Editor { path: PathBuf, doc: Rc<Doc> },
|
||||
DiffEditor { left: Rc<Doc>, right: Rc<Doc> },
|
||||
NewFileEditor,
|
||||
Settings,
|
||||
ThemeColorSettings,
|
||||
Keymap,
|
||||
Volt(VoltID),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
@@ -118,7 +135,18 @@ 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<Color>,
|
||||
pub path: String,
|
||||
pub confirmed: Option<RwSignal<bool>>,
|
||||
pub is_pristine: bool,
|
||||
}
|
||||
|
||||
impl EditorTabChild {
|
||||
@@ -127,7 +155,9 @@ impl EditorTabChild {
|
||||
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(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,13 +171,9 @@ impl EditorTabChild {
|
||||
let editor_data = data
|
||||
.main_split
|
||||
.editors
|
||||
.get_untracked()
|
||||
.get(editor_id)
|
||||
.cloned()
|
||||
.editor_untracked(*editor_id)
|
||||
.unwrap();
|
||||
EditorTabChildInfo::Editor(
|
||||
editor_data.get_untracked().editor_info(data),
|
||||
)
|
||||
EditorTabChildInfo::Editor(editor_data.editor_info(data))
|
||||
}
|
||||
EditorTabChild::DiffEditor(diff_editor_id) => {
|
||||
let diff_editor_data = data
|
||||
@@ -160,7 +186,206 @@ impl EditorTabChild {
|
||||
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<im::HashMap<DiffEditorId, DiffEditorData>>,
|
||||
plugin: PluginData,
|
||||
config: ReadSignal<Arc<LapceConfig>>,
|
||||
) -> Memo<EditorTabChildViewInfo> {
|
||||
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, path, confirmed, is_pristine) = match path {
|
||||
Some((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,
|
||||
path,
|
||||
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,
|
||||
path,
|
||||
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)),
|
||||
path: "Settings".to_string(),
|
||||
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)),
|
||||
path: "Theme Colors".to_string(),
|
||||
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)),
|
||||
path: "Keyboard Shortcuts".to_string(),
|
||||
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)),
|
||||
path: display_name,
|
||||
confirmed: None,
|
||||
is_pristine: true,
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -171,7 +396,7 @@ pub struct EditorTabData {
|
||||
pub split: SplitId,
|
||||
pub editor_tab_id: EditorTabId,
|
||||
pub active: usize,
|
||||
pub children: Vec<(RwSignal<usize>, EditorTabChild)>,
|
||||
pub children: Vec<(RwSignal<usize>, RwSignal<Rect>, EditorTabChild)>,
|
||||
pub window_origin: Point,
|
||||
pub layout_rect: Rect,
|
||||
pub locations: RwSignal<im::Vector<EditorLocation>>,
|
||||
@@ -181,22 +406,21 @@ pub struct EditorTabData {
|
||||
impl EditorTabData {
|
||||
pub fn get_editor(
|
||||
&self,
|
||||
editors: &im::HashMap<EditorId, RwSignal<EditorData>>,
|
||||
editors: Editors,
|
||||
path: &Path,
|
||||
) -> Option<(usize, RwSignal<EditorData>)> {
|
||||
) -> Option<(usize, EditorData)> {
|
||||
for (i, child) in self.children.iter().enumerate() {
|
||||
if let (_, EditorTabChild::Editor(editor_id)) = child {
|
||||
if let Some(editor) = editors.get(editor_id) {
|
||||
let e = editor.get_untracked();
|
||||
let is_path = e.view.doc.with_untracked(|doc| {
|
||||
if let DocContent::File(p) = &doc.content {
|
||||
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));
|
||||
return Some((i, editor));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -206,15 +430,14 @@ impl EditorTabData {
|
||||
|
||||
pub fn get_unconfirmed_editor_tab_child(
|
||||
&self,
|
||||
editors: &im::HashMap<EditorId, RwSignal<EditorData>>,
|
||||
editors: Editors,
|
||||
diff_editors: &im::HashMap<EditorId, DiffEditorData>,
|
||||
) -> Option<(usize, EditorTabChild)> {
|
||||
for (i, (_, child)) in self.children.iter().enumerate() {
|
||||
for (i, (_, _, child)) in self.children.iter().enumerate() {
|
||||
match child {
|
||||
EditorTabChild::Editor(editor_id) => {
|
||||
if let Some(editor) = editors.get(editor_id) {
|
||||
let e = editor.get_untracked();
|
||||
let confirmed = e.confirmed.get_untracked();
|
||||
if let Some(editor) = editors.editor_untracked(*editor_id) {
|
||||
let confirmed = editor.confirmed.get_untracked();
|
||||
if !confirmed {
|
||||
return Some((i, child.clone()));
|
||||
}
|
||||
@@ -222,15 +445,8 @@ impl EditorTabData {
|
||||
}
|
||||
EditorTabChild::DiffEditor(diff_editor_id) => {
|
||||
if let Some(diff_editor) = diff_editors.get(diff_editor_id) {
|
||||
let left_confirmed =
|
||||
diff_editor.left.with_untracked(|editor| {
|
||||
editor.confirmed.get_untracked()
|
||||
});
|
||||
let right_confirmed =
|
||||
diff_editor.right.with_untracked(|editor| {
|
||||
editor.confirmed.get_untracked()
|
||||
});
|
||||
if !left_confirmed && !right_confirmed {
|
||||
let confirmed = diff_editor.confirmed.get_untracked();
|
||||
if !confirmed {
|
||||
return Some((i, child.clone()));
|
||||
}
|
||||
}
|
||||
@@ -241,24 +457,6 @@ impl EditorTabData {
|
||||
None
|
||||
}
|
||||
|
||||
pub fn get_unconfirmed_editor(
|
||||
&self,
|
||||
editors: &im::HashMap<EditorId, RwSignal<EditorData>>,
|
||||
) -> Option<(usize, RwSignal<EditorData>)> {
|
||||
for (i, child) in self.children.iter().enumerate() {
|
||||
if let (_, EditorTabChild::Editor(editor_id)) = child {
|
||||
if let Some(editor) = editors.get(editor_id) {
|
||||
let e = editor.get_untracked();
|
||||
let confirmed = e.confirmed.get_untracked();
|
||||
if !confirmed {
|
||||
return Some((i, *editor));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn tab_info(&self, data: &WindowTabData) -> EditorTabInfo {
|
||||
let info = EditorTabInfo {
|
||||
active: self.active,
|
||||
@@ -267,7 +465,7 @@ impl EditorTabData {
|
||||
children: self
|
||||
.children
|
||||
.iter()
|
||||
.map(|(_, child)| child.child_info(data))
|
||||
.map(|(_, _, child)| child.child_info(data))
|
||||
.collect(),
|
||||
};
|
||||
info
|
||||
|
||||
@@ -1,44 +1,605 @@
|
||||
use std::path::PathBuf;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
collections::HashMap,
|
||||
ffi::OsStr,
|
||||
path::{Path, PathBuf},
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
use floem::reactive::{RwSignal, Scope};
|
||||
use indexmap::IndexMap;
|
||||
use floem::{
|
||||
action::show_context_menu,
|
||||
event::EventPropagation,
|
||||
ext_event::create_ext_action,
|
||||
keyboard::Modifiers,
|
||||
menu::{Menu, MenuItem},
|
||||
reactive::{RwSignal, Scope},
|
||||
views::editor::text::SystemClipboard,
|
||||
};
|
||||
use globset::Glob;
|
||||
use lapce_core::{
|
||||
command::{EditCommand, FocusCommand},
|
||||
mode::Mode,
|
||||
register::Clipboard,
|
||||
};
|
||||
use lapce_rpc::{
|
||||
file::{Duplicating, FileNodeItem, Naming, NamingState, NewNode, Renaming},
|
||||
proxy::ProxyResponse,
|
||||
};
|
||||
use tracing::{event, Level};
|
||||
use url::Url;
|
||||
|
||||
use super::node::FileNode;
|
||||
use crate::window_tab::CommonData;
|
||||
use crate::{
|
||||
command::{CommandExecuted, CommandKind, InternalCommand, LapceCommand},
|
||||
editor::EditorData,
|
||||
keypress::{condition::Condition, KeyPressFocus},
|
||||
main_split::Editors,
|
||||
window_tab::CommonData,
|
||||
};
|
||||
|
||||
enum RenamedPath {
|
||||
NotRenaming,
|
||||
NameUnchanged,
|
||||
Renamed { current_path: Url, new_path: Url },
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct FileExplorerData {
|
||||
pub root: FileNode,
|
||||
pub common: CommonData,
|
||||
pub all_files: RwSignal<im::HashMap<PathBuf, FileNode>>,
|
||||
pub root: RwSignal<FileNodeItem>,
|
||||
pub naming: RwSignal<Naming>,
|
||||
pub naming_editor_data: EditorData,
|
||||
pub common: Rc<CommonData>,
|
||||
left_diff_path: RwSignal<Option<PathBuf>>,
|
||||
}
|
||||
|
||||
impl FileExplorerData {
|
||||
pub fn new(cx: Scope, common: CommonData) -> Self {
|
||||
let path = common.workspace.path.clone().unwrap_or_default();
|
||||
let all_files = cx.create_rw_signal(im::HashMap::new());
|
||||
let root = FileNode {
|
||||
scope: cx,
|
||||
path: path.clone(),
|
||||
is_dir: true,
|
||||
read: cx.create_rw_signal(false),
|
||||
expanded: cx.create_rw_signal(false),
|
||||
children: cx.create_rw_signal(IndexMap::new()),
|
||||
children_open_count: cx.create_rw_signal(0),
|
||||
all_files,
|
||||
line_height: common.ui_line_height,
|
||||
internal_command: common.internal_command,
|
||||
};
|
||||
all_files.update(|all_files| {
|
||||
all_files.insert(path, root.clone());
|
||||
});
|
||||
if common.workspace.path.is_some() {
|
||||
root.toggle_expand(&common.proxy);
|
||||
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<usize>,
|
||||
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
|
||||
}
|
||||
Self {
|
||||
root,
|
||||
common,
|
||||
all_files,
|
||||
}
|
||||
|
||||
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<CommonData>) -> Self {
|
||||
let path = common
|
||||
.workspace
|
||||
.path
|
||||
.clone()
|
||||
.and_then(|w| Url::from_directory_path(w).ok());
|
||||
let root = cx.create_rw_signal(FileNodeItem {
|
||||
path: path.unwrap_or_default(),
|
||||
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,
|
||||
left_diff_path: 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(&mut &path.unwrap());
|
||||
}
|
||||
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: &mut Url) {
|
||||
let 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
|
||||
})
|
||||
.unwrap()
|
||||
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: &Url) {
|
||||
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: &Url, done: impl FnOnce(bool) + 'static) {
|
||||
let root = self.root;
|
||||
let data = self.clone();
|
||||
let config = self.common.config;
|
||||
let send = {
|
||||
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(&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.to_file_path().unwrap())
|
||||
});
|
||||
}
|
||||
Err(e) => tracing::error!(
|
||||
target:"files_exclude",
|
||||
"Failed to compile glob: {}",
|
||||
e
|
||||
),
|
||||
}
|
||||
|
||||
node.read = true;
|
||||
|
||||
// Remove paths that no longer exist
|
||||
let removed_paths = node
|
||||
.children
|
||||
.keys()
|
||||
.filter(|p| !items.iter().any(|i| &&i.path == p))
|
||||
.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(&mut &path);
|
||||
});
|
||||
|
||||
done(true);
|
||||
})
|
||||
};
|
||||
|
||||
// Ask the proxy for the directory information
|
||||
self.common.proxy.read_dir(path.clone(), 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(&mut Url::from_directory_path(path.clone()).unwrap())
|
||||
.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<Url> {
|
||||
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();
|
||||
|
||||
Some(Url::from_file_path(relative_path).ok()?)
|
||||
}
|
||||
Naming::Duplicating(d) => {
|
||||
let relative_path = self.naming_editor_data.text();
|
||||
|
||||
let new_path = lapce_rpc::file::parent(&d.path)
|
||||
.and_then(|p| p.join(relative_path.to_owned()).ok())
|
||||
.unwrap();
|
||||
|
||||
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<OsStr> =
|
||||
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 = lapce_rpc::file::parent(current_path)
|
||||
.and_then(|p| p.join(new_relative_path).ok())
|
||||
.unwrap();
|
||||
|
||||
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) {
|
||||
if self.is_dir(path) {
|
||||
self.toggle_expand(path);
|
||||
} else {
|
||||
self.common
|
||||
.internal_command
|
||||
.send(InternalCommand::OpenFile {
|
||||
path: path.to_path_buf(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn double_click(&self, path: &Path) -> EventPropagation {
|
||||
if self.is_dir(path) {
|
||||
EventPropagation::Continue
|
||||
} 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();
|
||||
menu = menu.entry(MenuItem::new("Reveal in file explorer").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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,153 +1,38 @@
|
||||
use std::path::PathBuf;
|
||||
use floem::views::VirtualVector;
|
||||
use lapce_rpc::file::{FileNodeItem, FileNodeViewData, Naming};
|
||||
|
||||
use floem::{
|
||||
ext_event::create_ext_action,
|
||||
reactive::{Memo, RwSignal, Scope},
|
||||
views::VirtualListVector,
|
||||
};
|
||||
use indexmap::IndexMap;
|
||||
use lapce_rpc::proxy::{ProxyResponse, ProxyRpcHandler};
|
||||
|
||||
use crate::{command::InternalCommand, listener::Listener};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct FileNode {
|
||||
pub scope: Scope,
|
||||
pub path: PathBuf,
|
||||
pub is_dir: bool,
|
||||
pub read: RwSignal<bool>,
|
||||
pub expanded: RwSignal<bool>,
|
||||
pub children: RwSignal<IndexMap<PathBuf, FileNode>>,
|
||||
pub children_open_count: RwSignal<usize>,
|
||||
pub all_files: RwSignal<im::HashMap<PathBuf, FileNode>>,
|
||||
pub line_height: Memo<f64>,
|
||||
pub internal_command: Listener<InternalCommand>,
|
||||
pub struct FileNodeVirtualList {
|
||||
file_node_item: FileNodeItem,
|
||||
naming: Naming,
|
||||
}
|
||||
|
||||
impl VirtualListVector<(PathBuf, FileNode)> for FileNode {
|
||||
type ItemIterator = Box<dyn Iterator<Item = (PathBuf, FileNode)>>;
|
||||
|
||||
fn total_size(&self) -> Option<f64> {
|
||||
let line_height = self.line_height.get();
|
||||
let count = self.children_open_count.get() + 1;
|
||||
Some(line_height * count as f64)
|
||||
impl FileNodeVirtualList {
|
||||
pub fn new(file_node_item: FileNodeItem, naming: Naming) -> Self {
|
||||
Self {
|
||||
file_node_item,
|
||||
naming,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl VirtualVector<FileNodeViewData> for FileNodeVirtualList {
|
||||
fn total_len(&self) -> usize {
|
||||
0
|
||||
self.file_node_item.children_open_count
|
||||
}
|
||||
|
||||
fn slice(&mut self, _range: std::ops::Range<usize>) -> Self::ItemIterator {
|
||||
let children = if !self.is_dir {
|
||||
IndexMap::new()
|
||||
} else {
|
||||
let expanded = self.expanded.get();
|
||||
if expanded {
|
||||
self.children.get()
|
||||
} else {
|
||||
IndexMap::new()
|
||||
}
|
||||
};
|
||||
Box::new(children.into_iter())
|
||||
}
|
||||
}
|
||||
|
||||
impl FileNode {
|
||||
pub fn click(&self, proxy: &ProxyRpcHandler) {
|
||||
if self.is_dir {
|
||||
self.toggle_expand(proxy)
|
||||
} else {
|
||||
self.internal_command.send(InternalCommand::OpenFile {
|
||||
path: self.path.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_expand(&self, proxy: &ProxyRpcHandler) {
|
||||
if !self.is_dir {
|
||||
return;
|
||||
}
|
||||
let expanded = self.expanded.get_untracked();
|
||||
if expanded {
|
||||
self.expanded.set(false);
|
||||
self.update_open_count();
|
||||
} else {
|
||||
self.expanded.set(true);
|
||||
if self.read.get_untracked() {
|
||||
self.update_open_count();
|
||||
} else {
|
||||
self.read_dir(proxy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn update_open_count(&self) {
|
||||
self.all_files.with_untracked(|all_files| {
|
||||
for current_path in self.path.ancestors() {
|
||||
if let Some(node) = all_files.get(current_path) {
|
||||
node.update_node_open_count();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn update_node_open_count(&self) {
|
||||
if self.is_dir {
|
||||
let expanded = self.expanded.get_untracked();
|
||||
if expanded {
|
||||
let count = self.children.with_untracked(|children| {
|
||||
children
|
||||
.values()
|
||||
.map(|node| node.children_open_count.get_untracked() + 1)
|
||||
.sum::<usize>()
|
||||
});
|
||||
self.children_open_count.set(count);
|
||||
} else {
|
||||
self.children_open_count.set(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn read_dir(&self, proxy: &ProxyRpcHandler) {
|
||||
if self.read.get_untracked() {
|
||||
return;
|
||||
}
|
||||
self.read.set(true);
|
||||
let cx = self.scope;
|
||||
let file_node = self.clone();
|
||||
let send = create_ext_action(self.scope, move |result| {
|
||||
if let Ok(ProxyResponse::ReadDirResponse { items }) = result {
|
||||
let items = items
|
||||
.into_iter()
|
||||
.map(|item| {
|
||||
(
|
||||
item.path_buf.clone(),
|
||||
FileNode {
|
||||
scope: cx,
|
||||
path: item.path_buf,
|
||||
is_dir: item.is_dir,
|
||||
read: cx.create_rw_signal(false),
|
||||
expanded: cx.create_rw_signal(false),
|
||||
children: cx.create_rw_signal(IndexMap::new()),
|
||||
children_open_count: cx.create_rw_signal(0),
|
||||
all_files: file_node.all_files,
|
||||
line_height: file_node.line_height,
|
||||
internal_command: file_node.internal_command,
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect::<IndexMap<PathBuf, FileNode>>();
|
||||
file_node.all_files.update(|all_files| {
|
||||
for (_, item) in items.iter() {
|
||||
all_files.insert(item.path.clone(), item.clone());
|
||||
}
|
||||
});
|
||||
file_node.children.set(items);
|
||||
file_node.update_open_count();
|
||||
}
|
||||
});
|
||||
proxy.read_dir(self.path.clone(), move |result| {
|
||||
send(result);
|
||||
})
|
||||
fn slice(
|
||||
&mut self,
|
||||
range: std::ops::Range<usize>,
|
||||
) -> impl Iterator<Item = FileNodeViewData> {
|
||||
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_children_view_slice(&mut view_items, naming, min, max, 0, 0);
|
||||
|
||||
view_items.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,198 +1,554 @@
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
use std::{path::Path, rc::Rc, sync::Arc};
|
||||
|
||||
use floem::{
|
||||
cosmic_text::Style as FontStyle,
|
||||
event::{Event, EventListener},
|
||||
peniko::Color,
|
||||
reactive::ReadSignal,
|
||||
style::{CursorStyle, Style},
|
||||
view::View,
|
||||
reactive::{create_rw_signal, ReadSignal, RwSignal},
|
||||
style::{AlignItems, CursorStyle, Position, Style},
|
||||
views::{
|
||||
container, container_box, label, scroll, stack, svg, virtual_list,
|
||||
Decorators, VirtualListDirection, VirtualListItemSize, VirtualListVector,
|
||||
container, dyn_stack, label, scroll, stack, svg, virtual_stack, Container,
|
||||
Decorators, VirtualDirection, VirtualItemSize,
|
||||
},
|
||||
View,
|
||||
};
|
||||
use lapce_rpc::proxy::ProxyRpcHandler;
|
||||
use lapce_core::selection::Selection;
|
||||
use lapce_rpc::{
|
||||
file::{FileNodeViewData, FileNodeViewKind, Naming},
|
||||
source_control::FileDiffKind,
|
||||
};
|
||||
use lapce_xi_rope::Rope;
|
||||
|
||||
use super::node::FileNode;
|
||||
use super::{data::FileExplorerData, node::FileNodeVirtualList};
|
||||
use crate::{
|
||||
app::clickable_icon,
|
||||
command::InternalCommand,
|
||||
config::{color::LapceColor, icon::LapceIcons, LapceConfig},
|
||||
panel::{position::PanelPosition, view::panel_header},
|
||||
window_tab::WindowTabData,
|
||||
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 Color {
|
||||
r: background_r,
|
||||
g: background_g,
|
||||
b: background_b,
|
||||
..
|
||||
} = background;
|
||||
let Color {
|
||||
r: foreground_r,
|
||||
g: foreground_g,
|
||||
b: foreground_b,
|
||||
a,
|
||||
} = foreground;
|
||||
let a: u16 = 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 { r, g, b, a: 255 }
|
||||
}
|
||||
|
||||
pub fn file_explorer_panel(
|
||||
window_tab_data: Arc<WindowTabData>,
|
||||
window_tab_data: Rc<WindowTabData>,
|
||||
position: PanelPosition,
|
||||
) -> impl View {
|
||||
let config = window_tab_data.common.config;
|
||||
let root_file_node = window_tab_data.file_explorer.root.clone();
|
||||
let proxy = window_tab_data.common.proxy.clone();
|
||||
stack(|| {
|
||||
(
|
||||
stack(move || (panel_header("Open Editors".to_string(), config),))
|
||||
.style(|| Style::BASE.width_pct(100.0).flex_col().height_px(150.0)),
|
||||
stack(|| {
|
||||
(
|
||||
panel_header("File Explorer".to_string(), config),
|
||||
container(|| {
|
||||
scroll(move || {
|
||||
file_node_view(
|
||||
root_file_node.clone(),
|
||||
proxy.clone(),
|
||||
0,
|
||||
config,
|
||||
)
|
||||
})
|
||||
.scroll_bar_color(move || {
|
||||
*config.get().get_color(LapceColor::LAPCE_SCROLL_BAR)
|
||||
})
|
||||
.style(|| Style::BASE.absolute().size_pct(100.0, 100.0))
|
||||
})
|
||||
.style(|| Style::BASE.size_pct(100.0, 100.0).line_height(1.6)),
|
||||
)
|
||||
})
|
||||
.style(|| Style::BASE.width_pct(100.0).height_pct(100.0).flex_col()),
|
||||
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, Style::hide),
|
||||
)
|
||||
})
|
||||
.style(move || {
|
||||
Style::BASE
|
||||
.width_pct(100.0)
|
||||
.apply_if(!position.is_bottom(), |s| s.flex_col())
|
||||
})
|
||||
.add(
|
||||
"File Explorer",
|
||||
container(
|
||||
new_file_node_view(data, source_control).style(|s| s.absolute()),
|
||||
)
|
||||
.style(|s| s.size_full().line_height(1.6)),
|
||||
window_tab_data
|
||||
.panel
|
||||
.section_open(PanelSection::FileExplorer),
|
||||
)
|
||||
.build()
|
||||
}
|
||||
|
||||
fn file_node_view(
|
||||
file_node: FileNode,
|
||||
proxy: ProxyRpcHandler,
|
||||
level: usize,
|
||||
/// 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<usize>,
|
||||
) {
|
||||
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<Arc<LapceConfig>>,
|
||||
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 {
|
||||
virtual_list(
|
||||
VirtualListDirection::Vertical,
|
||||
VirtualListItemSize::Fn(Box::new(|(_, file_node): &(PathBuf, FileNode)| {
|
||||
file_node.total_size().unwrap_or(0.0)
|
||||
})),
|
||||
move || file_node.clone(),
|
||||
|(path, _)| path.to_owned(),
|
||||
move |(path, file_node)| {
|
||||
let proxy = proxy.clone();
|
||||
stack(move || {
|
||||
(
|
||||
let config = data.common.config;
|
||||
let ui_line_height = data.common.ui_line_height;
|
||||
|
||||
let view = match node.kind.clone() {
|
||||
FileNodeViewKind::Path(path) => container(
|
||||
label(move || {
|
||||
path.file_name()
|
||||
.map(|f| f.to_string_lossy().to_string())
|
||||
.unwrap_or_default()
|
||||
})
|
||||
.style(move |s| {
|
||||
s.flex_grow(1.0).height(ui_line_height.get()).color(
|
||||
file_node_text_color(
|
||||
config,
|
||||
node.clone(),
|
||||
source_control.clone(),
|
||||
),
|
||||
)
|
||||
}),
|
||||
),
|
||||
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())
|
||||
}
|
||||
};
|
||||
|
||||
view.style(|s| s.flex_grow(1.0).padding(0.0).margin(0.0))
|
||||
}
|
||||
|
||||
/// Input used for naming a file/directory
|
||||
fn file_node_input_view(data: FileExplorerData, err: Option<String>) -> 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)
|
||||
}
|
||||
}
|
||||
|
||||
fn new_file_node_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 secondary_click_data = data.clone();
|
||||
|
||||
scroll(
|
||||
virtual_stack(
|
||||
VirtualDirection::Vertical,
|
||||
VirtualItemSize::Fixed(Box::new(move || ui_line_height.get())),
|
||||
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 file_node = file_node.clone();
|
||||
let proxy = proxy.clone();
|
||||
let expanded = file_node.expanded;
|
||||
let is_dir = file_node.is_dir;
|
||||
stack(|| {
|
||||
(
|
||||
svg(move || {
|
||||
let config = config.get();
|
||||
let expanded = expanded.get();
|
||||
let svg_str = match expanded {
|
||||
true => LapceIcons::ITEM_OPENED,
|
||||
false => LapceIcons::ITEM_CLOSED,
|
||||
};
|
||||
config.ui_svg(svg_str)
|
||||
})
|
||||
.style(move || {
|
||||
let config = config.get();
|
||||
let size = config.ui.icon_size() as f32;
|
||||
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;
|
||||
|
||||
let color = if is_dir {
|
||||
*config
|
||||
.get_color(LapceColor::LAPCE_ICON_ACTIVE)
|
||||
} else {
|
||||
Color::TRANSPARENT
|
||||
};
|
||||
Style::BASE
|
||||
.size_px(size, size)
|
||||
.margin_left_px(10.0)
|
||||
.color(color)
|
||||
}),
|
||||
{
|
||||
let path = path.clone();
|
||||
let path_for_style = path.clone();
|
||||
svg(move || {
|
||||
let config = config.get();
|
||||
if is_dir {
|
||||
let expanded = expanded.get();
|
||||
let svg_str = match expanded {
|
||||
true => LapceIcons::DIRECTORY_OPENED,
|
||||
false => {
|
||||
LapceIcons::DIRECTORY_CLOSED
|
||||
}
|
||||
};
|
||||
config.ui_svg(svg_str)
|
||||
} else {
|
||||
config.file_svg(&path).0
|
||||
}
|
||||
})
|
||||
.style(
|
||||
move || {
|
||||
let config = config.get();
|
||||
let size = config.ui.icon_size() as f32;
|
||||
|
||||
Style::BASE
|
||||
.size_px(size, size)
|
||||
.margin_horiz_px(6.0)
|
||||
.apply_if(is_dir, |s| {
|
||||
s.color(*config.get_color(
|
||||
LapceColor::LAPCE_ICON_ACTIVE,
|
||||
))
|
||||
})
|
||||
.apply_if(!is_dir, |s| {
|
||||
s.apply_opt(
|
||||
config
|
||||
.file_svg(
|
||||
&path_for_style,
|
||||
)
|
||||
.1
|
||||
.cloned(),
|
||||
Style::color,
|
||||
)
|
||||
})
|
||||
},
|
||||
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),
|
||||
)
|
||||
},
|
||||
label(move || {
|
||||
path.file_name()
|
||||
.map(|f| f.to_string_lossy().to_string())
|
||||
.unwrap_or_default()
|
||||
}),
|
||||
)
|
||||
})
|
||||
.on_click(move |_| {
|
||||
file_node.click(&proxy);
|
||||
true
|
||||
})
|
||||
.style(move || {
|
||||
Style::BASE
|
||||
.items_center()
|
||||
.padding_right_px(10.0)
|
||||
.padding_left_px((level * 10) as f32)
|
||||
.min_width_pct(100.0)
|
||||
})
|
||||
.hover_style(move || {
|
||||
Style::BASE
|
||||
.background(
|
||||
*config.get().get_color(
|
||||
LapceColor::PANEL_HOVERED_BACKGROUND,
|
||||
),
|
||||
)
|
||||
.cursor(CursorStyle::Pointer)
|
||||
})
|
||||
.apply_if(!is_dir, |s| {
|
||||
s.apply_opt(
|
||||
kind_for_style
|
||||
.path()
|
||||
.and_then(|p| config.file_svg(p).1),
|
||||
Style::color,
|
||||
)
|
||||
})
|
||||
})
|
||||
},
|
||||
container_box(move || {
|
||||
Box::new(file_node_view(
|
||||
file_node,
|
||||
proxy.clone(),
|
||||
level + 1,
|
||||
config,
|
||||
))
|
||||
}),
|
||||
)
|
||||
})
|
||||
.style(|| Style::BASE.flex_col().min_width_pct(100.0))
|
||||
},
|
||||
file_node_text_view(data, node, source_control.clone()),
|
||||
))
|
||||
.style(move |s| {
|
||||
s.padding_right(5.0)
|
||||
.padding_left((level * 10) as f32)
|
||||
.align_items(AlignItems::Center)
|
||||
.hover(|s| {
|
||||
s.background(
|
||||
config
|
||||
.get()
|
||||
.color(LapceColor::PANEL_HOVERED_BACKGROUND),
|
||||
)
|
||||
.cursor(CursorStyle::Pointer)
|
||||
})
|
||||
});
|
||||
|
||||
// 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;
|
||||
view.on_click_stop(move |_| {
|
||||
click_data.click(&click_path);
|
||||
})
|
||||
.on_double_click(move |_| {
|
||||
double_click_data.double_click(&double_click_path)
|
||||
})
|
||||
.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
|
||||
}
|
||||
},
|
||||
)
|
||||
.style(|s| s.flex_col().align_items(AlignItems::Stretch).width_full()),
|
||||
)
|
||||
.style(|| Style::BASE.flex_col().min_width_pct(100.0))
|
||||
.style(|s| s.size_full())
|
||||
.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);
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn open_editors_view(window_tab_data: Rc<WindowTabData>) -> 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<EditorTabData>,
|
||||
child_index: RwSignal<usize>,
|
||||
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.path.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)
|
||||
.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_pct(100.0, 100.0).line_height(1.6))
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@ pub struct FindSearchString {
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Find {
|
||||
pub rev: RwSignal<u64>,
|
||||
/// If the find is shown
|
||||
pub visual: RwSignal<bool>,
|
||||
/// The currently active search string.
|
||||
@@ -78,6 +79,7 @@ pub struct Find {
|
||||
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),
|
||||
@@ -104,6 +106,18 @@ impl Find {
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
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
|
||||
}
|
||||
|
||||
@@ -201,7 +215,7 @@ impl Find {
|
||||
let end = find_cursor.pos();
|
||||
|
||||
if whole_words
|
||||
&& !self.is_matching_whole_words(text, start, end)
|
||||
&& !Self::is_matching_whole_words(text, start, end)
|
||||
{
|
||||
raw_lines =
|
||||
text.lines_raw(find_cursor.pos()..text.len());
|
||||
@@ -226,7 +240,7 @@ impl Find {
|
||||
let end = find_cursor.pos();
|
||||
|
||||
if whole_words
|
||||
&& !self.is_matching_whole_words(text, start, end)
|
||||
&& !Self::is_matching_whole_words(text, start, end)
|
||||
{
|
||||
raw_lines =
|
||||
text.lines_raw(find_cursor.pos()..offset);
|
||||
@@ -249,7 +263,7 @@ impl Find {
|
||||
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)
|
||||
&& !Self::is_matching_whole_words(text, start, end)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -274,7 +288,7 @@ impl Find {
|
||||
let end = find_cursor.pos();
|
||||
|
||||
if whole_words
|
||||
&& !self.is_matching_whole_words(text, start, end)
|
||||
&& !Self::is_matching_whole_words(text, start, end)
|
||||
{
|
||||
raw_lines =
|
||||
text.lines_raw(find_cursor.pos()..text.len());
|
||||
@@ -298,12 +312,7 @@ impl Find {
|
||||
}
|
||||
|
||||
/// Checks if the start and end of a match is matching whole words.
|
||||
fn is_matching_whole_words(
|
||||
&self,
|
||||
text: &Rope,
|
||||
start: usize,
|
||||
end: usize,
|
||||
) -> bool {
|
||||
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);
|
||||
|
||||
@@ -319,7 +328,7 @@ impl Find {
|
||||
}
|
||||
|
||||
/// Returns `true` if the search query is a multi-line regex.
|
||||
pub(crate) fn is_multiline_regex(&self) -> bool {
|
||||
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)
|
||||
@@ -329,6 +338,88 @@ impl Find {
|
||||
})
|
||||
}
|
||||
|
||||
#[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,
|
||||
@@ -380,7 +471,7 @@ impl Find {
|
||||
) {
|
||||
let end = find_cursor.pos();
|
||||
|
||||
if whole_words && !self.is_matching_whole_words(text, start, end) {
|
||||
if whole_words && !Self::is_matching_whole_words(text, start, end) {
|
||||
raw_lines = text.lines_raw(find_cursor.pos()..to);
|
||||
continue;
|
||||
}
|
||||
@@ -420,6 +511,7 @@ impl Find {
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct FindResult {
|
||||
pub find_rev: RwSignal<u64>,
|
||||
pub progress: RwSignal<FindProgress>,
|
||||
pub occurrences: RwSignal<Selection>,
|
||||
pub search_string: RwSignal<Option<FindSearchString>>,
|
||||
@@ -431,6 +523,7 @@ pub struct FindResult {
|
||||
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),
|
||||
@@ -442,6 +535,5 @@ impl FindResult {
|
||||
|
||||
pub fn reset(&self) {
|
||||
self.progress.set(FindProgress::Started);
|
||||
self.occurrences.set(Selection::new());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,25 @@
|
||||
use floem::{
|
||||
cosmic_text::{
|
||||
Attrs, AttrsList, FamilyOwned, LineHeightValue, TextLayout, Weight,
|
||||
},
|
||||
id::Id,
|
||||
cosmic_text::{Attrs, AttrsList, FamilyOwned, TextLayout, Weight},
|
||||
peniko::{
|
||||
kurbo::{Point, Rect},
|
||||
Color,
|
||||
},
|
||||
prop_extractor,
|
||||
reactive::create_effect,
|
||||
style::{ComputedStyle, Style},
|
||||
taffy::prelude::Node,
|
||||
view::{ChangeFlags, View},
|
||||
Renderer, ViewContext,
|
||||
style::{FontFamily, FontSize, LineHeight, Style, TextColor},
|
||||
taffy::prelude::NodeId,
|
||||
Renderer, View, ViewId,
|
||||
};
|
||||
|
||||
prop_extractor! {
|
||||
Extractor {
|
||||
color: TextColor,
|
||||
font_size: FontSize,
|
||||
font_family: FontFamily,
|
||||
line_height: LineHeight,
|
||||
}
|
||||
}
|
||||
|
||||
enum FocusTextState {
|
||||
Text(String),
|
||||
FocusColor(Color),
|
||||
@@ -25,65 +31,57 @@ pub fn focus_text(
|
||||
focus_indices: impl Fn() -> Vec<usize> + 'static,
|
||||
focus_color: impl Fn() -> Color + 'static,
|
||||
) -> FocusText {
|
||||
let cx = ViewContext::get_current();
|
||||
|
||||
let id = cx.new_id();
|
||||
let id = ViewId::new();
|
||||
|
||||
create_effect(move |_| {
|
||||
let new_text = text();
|
||||
id.update_state(FocusTextState::Text(new_text), false);
|
||||
id.update_state(FocusTextState::Text(new_text));
|
||||
});
|
||||
|
||||
create_effect(move |_| {
|
||||
let focus_color = focus_color();
|
||||
id.update_state(FocusTextState::FocusColor(focus_color), false);
|
||||
id.update_state(FocusTextState::FocusColor(focus_color));
|
||||
});
|
||||
|
||||
create_effect(move |_| {
|
||||
let focus_indices = focus_indices();
|
||||
id.update_state(FocusTextState::FocusIndices(focus_indices), false);
|
||||
id.update_state(FocusTextState::FocusIndices(focus_indices));
|
||||
});
|
||||
|
||||
FocusText {
|
||||
id,
|
||||
text: "".to_string(),
|
||||
text_layout: None,
|
||||
color: None,
|
||||
focus_color: Color::default(),
|
||||
focus_indices: Vec::new(),
|
||||
text_node: None,
|
||||
font_size: None,
|
||||
font_family: None,
|
||||
line_height: None,
|
||||
available_text: None,
|
||||
available_width: None,
|
||||
available_text_layout: None,
|
||||
style: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FocusText {
|
||||
id: Id,
|
||||
id: ViewId,
|
||||
text: String,
|
||||
text_layout: Option<TextLayout>,
|
||||
color: Option<Color>,
|
||||
focus_color: Color,
|
||||
focus_indices: Vec<usize>,
|
||||
font_size: Option<f32>,
|
||||
font_family: Option<String>,
|
||||
line_height: Option<LineHeightValue>,
|
||||
text_node: Option<Node>,
|
||||
text_node: Option<NodeId>,
|
||||
available_text: Option<String>,
|
||||
available_width: Option<f32>,
|
||||
available_text_layout: Option<TextLayout>,
|
||||
style: Extractor,
|
||||
}
|
||||
|
||||
impl FocusText {
|
||||
fn set_text_layout(&mut self) {
|
||||
let mut attrs = Attrs::new().color(self.color.unwrap_or_default());
|
||||
if let Some(font_size) = self.font_size {
|
||||
let mut attrs = Attrs::new().color(self.style.color().unwrap_or_default());
|
||||
if let Some(font_size) = self.style.font_size() {
|
||||
attrs = attrs.font_size(font_size);
|
||||
}
|
||||
let font_family = self.font_family.as_ref().map(|font_family| {
|
||||
let font_family = self.style.font_family().as_ref().map(|font_family| {
|
||||
let family: Vec<FamilyOwned> =
|
||||
FamilyOwned::parse_list(font_family).collect();
|
||||
family
|
||||
@@ -91,7 +89,7 @@ impl FocusText {
|
||||
if let Some(font_family) = font_family.as_ref() {
|
||||
attrs = attrs.family(font_family);
|
||||
}
|
||||
if let Some(line_height) = self.line_height {
|
||||
if let Some(line_height) = self.style.line_height() {
|
||||
attrs = attrs.line_height(line_height);
|
||||
}
|
||||
|
||||
@@ -120,11 +118,12 @@ impl FocusText {
|
||||
if let Some(new_text) = self.available_text.as_ref() {
|
||||
let new_text_len = new_text.len();
|
||||
|
||||
let mut attrs = Attrs::new().color(self.color.unwrap_or_default());
|
||||
if let Some(font_size) = self.font_size {
|
||||
let mut attrs =
|
||||
Attrs::new().color(self.style.color().unwrap_or_default());
|
||||
if let Some(font_size) = self.style.font_size() {
|
||||
attrs = attrs.font_size(font_size);
|
||||
}
|
||||
let font_family = self.font_family.as_ref().map(|font_family| {
|
||||
let font_family = self.style.font_family().as_ref().map(|font_family| {
|
||||
let family: Vec<FamilyOwned> =
|
||||
FamilyOwned::parse_list(font_family).collect();
|
||||
family
|
||||
@@ -162,31 +161,15 @@ impl FocusText {
|
||||
}
|
||||
|
||||
impl View for FocusText {
|
||||
fn id(&self) -> floem::id::Id {
|
||||
fn id(&self) -> ViewId {
|
||||
self.id
|
||||
}
|
||||
|
||||
fn child(&self, _id: floem::id::Id) -> Option<&dyn View> {
|
||||
None
|
||||
}
|
||||
|
||||
fn child_mut(&mut self, _id: floem::id::Id) -> Option<&mut dyn View> {
|
||||
None
|
||||
}
|
||||
|
||||
fn children(&self) -> Vec<&dyn View> {
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
fn children_mut(&mut self) -> Vec<&mut dyn View> {
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
fn update(
|
||||
&mut self,
|
||||
cx: &mut floem::context::UpdateCx,
|
||||
_cx: &mut floem::context::UpdateCx,
|
||||
state: Box<dyn std::any::Any>,
|
||||
) -> floem::view::ChangeFlags {
|
||||
) {
|
||||
if let Ok(state) = state.downcast() {
|
||||
match *state {
|
||||
FocusTextState::Text(text) => {
|
||||
@@ -200,68 +183,67 @@ impl View for FocusText {
|
||||
}
|
||||
}
|
||||
self.set_text_layout();
|
||||
cx.request_layout(self.id());
|
||||
ChangeFlags::LAYOUT
|
||||
} else {
|
||||
ChangeFlags::empty()
|
||||
self.id.request_layout();
|
||||
}
|
||||
}
|
||||
|
||||
fn style(&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::Node {
|
||||
cx.layout_node(self.id, true, |cx| {
|
||||
if self.font_size != cx.current_font_size()
|
||||
|| self.font_family.as_deref() != cx.current_font_family()
|
||||
|| self.line_height != cx.current_line_height()
|
||||
{
|
||||
self.font_size = cx.current_font_size();
|
||||
self.font_family = cx.current_font_family().map(|s| s.to_string());
|
||||
self.line_height = cx.current_line_height();
|
||||
self.set_text_layout();
|
||||
}
|
||||
) -> 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(cx.new_node());
|
||||
self.text_node = Some(self.id.new_taffy_node());
|
||||
}
|
||||
let text_node = self.text_node.unwrap();
|
||||
|
||||
let style = Style::BASE
|
||||
.size_px(width, height)
|
||||
.compute(&ComputedStyle::default())
|
||||
.to_taffy_style();
|
||||
cx.set_style(text_node, style);
|
||||
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::LayoutCx) -> Option<Rect> {
|
||||
fn compute_layout(
|
||||
&mut self,
|
||||
_cx: &mut floem::context::ComputeLayoutCx,
|
||||
) -> Option<Rect> {
|
||||
let text_node = self.text_node.unwrap();
|
||||
let layout = cx.layout(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.color.unwrap_or_else(|| Color::rgb8(0xf0, 0xf0, 0xea)),
|
||||
self.style
|
||||
.color()
|
||||
.unwrap_or_else(|| Color::rgb8(0xf0, 0xf0, 0xea)),
|
||||
);
|
||||
if let Some(font_size) = self.font_size {
|
||||
if let Some(font_size) = self.style.font_size() {
|
||||
attrs = attrs.font_size(font_size);
|
||||
}
|
||||
let font_family = self.font_family.as_ref().map(|font_family| {
|
||||
let family: Vec<FamilyOwned> =
|
||||
FamilyOwned::parse_list(font_family).collect();
|
||||
family
|
||||
});
|
||||
let font_family =
|
||||
self.style.font_family().as_ref().map(|font_family| {
|
||||
let family: Vec<FamilyOwned> =
|
||||
FamilyOwned::parse_list(font_family).collect();
|
||||
family
|
||||
});
|
||||
if let Some(font_family) = font_family.as_ref() {
|
||||
attrs = attrs.family(font_family);
|
||||
}
|
||||
@@ -291,27 +273,9 @@ impl View for FocusText {
|
||||
None
|
||||
}
|
||||
|
||||
fn event(
|
||||
&mut self,
|
||||
_cx: &mut floem::context::EventCx,
|
||||
_id_path: Option<&[floem::id::Id]>,
|
||||
_event: floem::event::Event,
|
||||
) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn paint(&mut self, cx: &mut floem::context::PaintCx) {
|
||||
if self.color != cx.current_color()
|
||||
|| self.font_size != cx.current_font_size()
|
||||
|| self.font_family.as_deref() != cx.current_font_family()
|
||||
{
|
||||
self.color = cx.current_color();
|
||||
self.font_size = cx.current_font_size();
|
||||
self.font_family = cx.current_font_family().map(|s| s.to_string());
|
||||
self.set_text_layout();
|
||||
}
|
||||
let text_node = self.text_node.unwrap();
|
||||
let location = cx.layout(text_node).unwrap().location;
|
||||
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);
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use std::{ops::Range, path::PathBuf};
|
||||
use std::{ops::Range, path::PathBuf, rc::Rc};
|
||||
|
||||
use floem::{
|
||||
ext_event::create_ext_action,
|
||||
keyboard::Modifiers,
|
||||
reactive::{Memo, RwSignal, Scope},
|
||||
views::VirtualListVector,
|
||||
views::VirtualVector,
|
||||
};
|
||||
use indexmap::IndexMap;
|
||||
use lapce_core::{mode::Mode, selection::Selection};
|
||||
@@ -13,7 +14,6 @@ use lapce_xi_rope::Rope;
|
||||
use crate::{
|
||||
command::{CommandExecuted, CommandKind},
|
||||
editor::EditorData,
|
||||
id::EditorId,
|
||||
keypress::{condition::Condition, KeyPressFocus},
|
||||
main_split::MainSplitData,
|
||||
window_tab::CommonData,
|
||||
@@ -43,7 +43,7 @@ pub struct GlobalSearchData {
|
||||
pub editor: EditorData,
|
||||
pub search_result: RwSignal<IndexMap<PathBuf, SearchMatchData>>,
|
||||
pub main_split: MainSplitData,
|
||||
pub common: CommonData,
|
||||
pub common: Rc<CommonData>,
|
||||
}
|
||||
|
||||
impl KeyPressFocus for GlobalSearchData {
|
||||
@@ -59,19 +59,20 @@ impl KeyPressFocus for GlobalSearchData {
|
||||
&self,
|
||||
command: &crate::command::LapceCommand,
|
||||
count: Option<usize>,
|
||||
mods: floem::glazier::Modifiers,
|
||||
mods: Modifiers,
|
||||
) -> CommandExecuted {
|
||||
match &command.kind {
|
||||
CommandKind::Workbench(_) => {}
|
||||
CommandKind::Scroll(_) => {}
|
||||
CommandKind::Focus(_) => {}
|
||||
CommandKind::Edit(_)
|
||||
| CommandKind::Move(_)
|
||||
| CommandKind::MultiSelection(_) => {
|
||||
self.editor.run_command(command, count, mods);
|
||||
return self.editor.run_command(command, count, mods);
|
||||
}
|
||||
CommandKind::MotionMode(_) => {}
|
||||
}
|
||||
CommandExecuted::Yes
|
||||
CommandExecuted::No
|
||||
}
|
||||
|
||||
fn receive_char(&self, c: &str) {
|
||||
@@ -79,16 +80,9 @@ impl KeyPressFocus for GlobalSearchData {
|
||||
}
|
||||
}
|
||||
|
||||
impl VirtualListVector<(PathBuf, SearchMatchData)> for GlobalSearchData {
|
||||
type ItemIterator = Box<dyn Iterator<Item = (PathBuf, SearchMatchData)>>;
|
||||
|
||||
impl VirtualVector<(PathBuf, SearchMatchData)> for GlobalSearchData {
|
||||
fn total_len(&self) -> usize {
|
||||
0
|
||||
}
|
||||
|
||||
fn total_size(&self) -> Option<f64> {
|
||||
let line_height = self.common.ui_line_height.get();
|
||||
let count: usize = self.search_result.with(|result| {
|
||||
self.search_result.with(|result| {
|
||||
result
|
||||
.iter()
|
||||
.map(|(_, data)| {
|
||||
@@ -99,18 +93,21 @@ impl VirtualListVector<(PathBuf, SearchMatchData)> for GlobalSearchData {
|
||||
}
|
||||
})
|
||||
.sum()
|
||||
});
|
||||
Some(line_height * count as f64)
|
||||
})
|
||||
}
|
||||
|
||||
fn slice(&mut self, _range: Range<usize>) -> Self::ItemIterator {
|
||||
Box::new(self.search_result.get().into_iter())
|
||||
fn slice(
|
||||
&mut self,
|
||||
_range: Range<usize>,
|
||||
) -> impl Iterator<Item = (PathBuf, SearchMatchData)> {
|
||||
self.search_result.get().into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl GlobalSearchData {
|
||||
pub fn new(cx: Scope, main_split: MainSplitData, common: CommonData) -> Self {
|
||||
let editor = EditorData::new_local(cx, EditorId::next(), common.clone());
|
||||
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 {
|
||||
@@ -122,12 +119,9 @@ impl GlobalSearchData {
|
||||
|
||||
{
|
||||
let global_search = global_search.clone();
|
||||
let buffer = global_search.editor.doc().buffer;
|
||||
cx.create_effect(move |_| {
|
||||
let pattern = global_search
|
||||
.editor
|
||||
.view
|
||||
.doc
|
||||
.with(|doc| doc.buffer().to_string());
|
||||
let pattern = buffer.with(|buffer| buffer.to_string());
|
||||
if pattern.is_empty() {
|
||||
global_search.search_result.update(|r| r.clear());
|
||||
return;
|
||||
@@ -158,10 +152,10 @@ impl GlobalSearchData {
|
||||
}
|
||||
|
||||
{
|
||||
let global_search_doc = global_search.editor.view.doc;
|
||||
let buffer = global_search.editor.doc().buffer;
|
||||
let main_split = global_search.main_split.clone();
|
||||
cx.create_effect(move |_| {
|
||||
let content = global_search_doc.with(|doc| doc.buffer().to_string());
|
||||
let content = buffer.with(|buffer| buffer.to_string());
|
||||
main_split.set_find_pattern(Some(content));
|
||||
});
|
||||
}
|
||||
@@ -198,12 +192,9 @@ impl GlobalSearchData {
|
||||
|
||||
pub fn set_pattern(&self, pattern: String) {
|
||||
let pattern_len = pattern.len();
|
||||
self.editor.doc().reload(Rope::from(pattern), true);
|
||||
self.editor
|
||||
.view
|
||||
.doc
|
||||
.update(|doc| doc.reload(Rope::from(pattern), true));
|
||||
self.editor
|
||||
.cursor
|
||||
.cursor()
|
||||
.update(|cursor| cursor.set_insert(Selection::region(0, pattern_len)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<bool>,
|
||||
pub offset: RwSignal<usize>,
|
||||
pub editor_id: RwSignal<EditorId>,
|
||||
pub content: RwSignal<Vec<MarkdownContent>>,
|
||||
pub layout_rect: RwSignal<Rect>,
|
||||
}
|
||||
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,11 @@
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct Id(u64);
|
||||
|
||||
impl Id {
|
||||
/// Allocate a new, unique `Id`.
|
||||
pub fn next() -> Id {
|
||||
use floem::glazier::Counter;
|
||||
static ID_COUNTER: Counter = Counter::new();
|
||||
Id(ID_COUNTER.next())
|
||||
}
|
||||
|
||||
pub fn to_raw(self) -> u64 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
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 EditorId = Id;
|
||||
pub type ThemeColorSettingsId = Id;
|
||||
pub type VoltViewId = Id;
|
||||
pub type DiffEditorId = Id;
|
||||
pub type TerminalTabId = Id;
|
||||
|
||||
@@ -0,0 +1,288 @@
|
||||
use std::{borrow::Cow, ops::Range, path::PathBuf, str::FromStr};
|
||||
|
||||
use floem::reactive::{batch, RwSignal, Scope};
|
||||
use lapce_core::{
|
||||
buffer::{
|
||||
rope_text::{RopeText, RopeTextRef},
|
||||
Buffer,
|
||||
},
|
||||
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<String>,
|
||||
/// The range (of offsets) to replace
|
||||
pub range: Option<Range<usize>>,
|
||||
pub command: Option<lsp_types::Command>,
|
||||
pub insert_text_format: Option<InsertTextFormat>,
|
||||
}
|
||||
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<usize>,
|
||||
pub items: im::Vector<InlineCompletionItem>,
|
||||
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<InlineCompletionItem>,
|
||||
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())
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::sync::Arc;
|
||||
use std::{rc::Rc, sync::Arc};
|
||||
|
||||
use floem::{
|
||||
event::{Event, EventListener},
|
||||
@@ -6,22 +6,24 @@ use floem::{
|
||||
create_effect, create_memo, create_rw_signal, Memo, ReadSignal, RwSignal,
|
||||
Scope,
|
||||
},
|
||||
style::{CursorStyle, Style},
|
||||
view::View,
|
||||
style::CursorStyle,
|
||||
views::{
|
||||
container, label, list, scroll, stack, virtual_list, Decorators,
|
||||
VirtualListDirection, VirtualListItemSize,
|
||||
container, dyn_stack, label, scroll, stack, text, virtual_stack, Decorators,
|
||||
VirtualDirection, VirtualItemSize,
|
||||
},
|
||||
View,
|
||||
};
|
||||
use lapce_core::mode::Modes;
|
||||
|
||||
use crate::{
|
||||
command::LapceCommand,
|
||||
config::{color::LapceColor, LapceConfig},
|
||||
editor::EditorData,
|
||||
id::EditorId,
|
||||
keypress::{keymap::KeyMap, KeyPress, KeyPressData},
|
||||
text_input::text_input,
|
||||
keypress::{
|
||||
keymap::{KeyMap, KeyMapPress},
|
||||
KeyPressData,
|
||||
},
|
||||
main_split::Editors,
|
||||
text_input::TextInputBuilder,
|
||||
window_tab::CommonData,
|
||||
};
|
||||
|
||||
@@ -29,13 +31,14 @@ use crate::{
|
||||
pub struct KeymapPicker {
|
||||
cmd: RwSignal<Option<LapceCommand>>,
|
||||
keymap: RwSignal<Option<KeyMap>>,
|
||||
keys: RwSignal<Vec<KeyPress>>,
|
||||
keys: RwSignal<Vec<(KeyMapPress, bool)>>,
|
||||
}
|
||||
|
||||
pub fn keymap_view(common: CommonData) -> impl View {
|
||||
pub fn keymap_view(editors: Editors, common: Rc<CommonData>) -> impl View {
|
||||
let config = common.config;
|
||||
let keypress = common.keypress;
|
||||
let ui_line_height = move || common.ui_line_height.get() * 1.2;
|
||||
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),
|
||||
@@ -44,28 +47,45 @@ pub fn keymap_view(common: CommonData) -> impl View {
|
||||
};
|
||||
|
||||
let cx = Scope::current();
|
||||
let editor = EditorData::new_local(cx, EditorId::next(), common.clone());
|
||||
let doc = editor.view.doc;
|
||||
let text_input_view = TextInputBuilder::new().build(cx, editors, common.clone());
|
||||
let doc = text_input_view.doc_signal();
|
||||
|
||||
let items = move || {
|
||||
let pattern = doc.with(|doc| doc.buffer().to_string().to_lowercase());
|
||||
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 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;
|
||||
|
||||
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
|
||||
}
|
||||
Some((cmd, Some(keymap.clone())))
|
||||
})
|
||||
.collect::<im::Vector<(LapceCommand, Option<KeyMap>)>>();
|
||||
items.extend(keypress.commands_without_keymap.iter().filter_map(|cmd| {
|
||||
@@ -90,168 +110,146 @@ pub fn keymap_view(common: CommonData) -> impl View {
|
||||
move |(i, (cmd, keymap)): (usize, (LapceCommand, Option<KeyMap>))| {
|
||||
let local_keymap = keymap.clone();
|
||||
let local_cmd = cmd.clone();
|
||||
stack(|| {
|
||||
(
|
||||
container(|| {
|
||||
label(move || {
|
||||
cmd.kind
|
||||
.desc()
|
||||
.map(|desc| desc.to_string())
|
||||
.unwrap_or_else(|| cmd.kind.str().replace('_', " "))
|
||||
})
|
||||
.style(|| {
|
||||
Style::BASE
|
||||
.text_ellipsis()
|
||||
.absolute()
|
||||
.items_center()
|
||||
.min_width_px(0.0)
|
||||
.padding_horiz_px(10.0)
|
||||
.size_pct(100.0, 100.0)
|
||||
})
|
||||
})
|
||||
.style(move || {
|
||||
Style::BASE
|
||||
.height_pct(100.0)
|
||||
.min_width_px(0.0)
|
||||
.flex_basis_px(0.0)
|
||||
.flex_grow(1.0)
|
||||
.border_right(1.0)
|
||||
.border_color(
|
||||
*config.get().get_color(LapceColor::LAPCE_BORDER),
|
||||
)
|
||||
}),
|
||||
{
|
||||
let keymap = keymap.clone();
|
||||
list(
|
||||
move || {
|
||||
keymap
|
||||
.as_ref()
|
||||
.map(|keymap| {
|
||||
keymap
|
||||
.key
|
||||
.iter()
|
||||
.map(|key| {
|
||||
key.label().trim().to_string()
|
||||
})
|
||||
.filter(|l| !l.is_empty())
|
||||
.collect::<Vec<String>>()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
},
|
||||
|k| k.clone(),
|
||||
move |key| {
|
||||
label(move || key.clone()).style(move || {
|
||||
Style::BASE
|
||||
.padding_horiz_px(5.0)
|
||||
.padding_vert_px(1.0)
|
||||
.margin_right_px(5.0)
|
||||
.border(1.0)
|
||||
.border_radius(6.0)
|
||||
.border_color(
|
||||
*config
|
||||
.get()
|
||||
.get_color(LapceColor::LAPCE_BORDER),
|
||||
)
|
||||
})
|
||||
},
|
||||
)
|
||||
.style(move || {
|
||||
Style::BASE
|
||||
.items_center()
|
||||
.padding_horiz_px(10.0)
|
||||
.min_width_px(200.0)
|
||||
.height_pct(100.0)
|
||||
.border_right(1.0)
|
||||
.border_color(
|
||||
*config
|
||||
.get()
|
||||
.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::<Vec<String>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
list(
|
||||
move || modes.clone(),
|
||||
|m| m.clone(),
|
||||
move |mode| {
|
||||
label(move || mode.clone()).style(move || {
|
||||
Style::BASE
|
||||
.padding_horiz_px(5.0)
|
||||
.padding_vert_px(1.0)
|
||||
.margin_right_px(5.0)
|
||||
.border(1.0)
|
||||
.border_radius(6.0)
|
||||
.border_color(
|
||||
*config
|
||||
.get()
|
||||
.get_color(LapceColor::LAPCE_BORDER),
|
||||
)
|
||||
})
|
||||
},
|
||||
)
|
||||
.style(move || {
|
||||
Style::BASE
|
||||
.items_center()
|
||||
.padding_horiz_px(10.0)
|
||||
.min_width_px(200.0)
|
||||
.height_pct(100.0)
|
||||
.border_right(1.0)
|
||||
.border_color(
|
||||
*config
|
||||
.get()
|
||||
.get_color(LapceColor::LAPCE_BORDER),
|
||||
)
|
||||
.apply_if(!modal.get(), |s| s.hide())
|
||||
})
|
||||
},
|
||||
container(|| {
|
||||
label(move || {
|
||||
keymap
|
||||
.as_ref()
|
||||
.and_then(|keymap| keymap.when.clone())
|
||||
.unwrap_or_default()
|
||||
})
|
||||
.style(|| {
|
||||
Style::BASE
|
||||
.text_ellipsis()
|
||||
.absolute()
|
||||
.items_center()
|
||||
.min_width_px(0.0)
|
||||
.padding_horiz_px(10.0)
|
||||
.size_pct(100.0, 100.0)
|
||||
})
|
||||
})
|
||||
.style(move || {
|
||||
Style::BASE
|
||||
.height_pct(100.0)
|
||||
.min_width_px(0.0)
|
||||
.flex_basis_px(0.0)
|
||||
.flex_grow(1.0)
|
||||
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)
|
||||
}),
|
||||
)
|
||||
})
|
||||
.on_click(move |_| {
|
||||
.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::<Vec<String>>()
|
||||
})
|
||||
.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::<Vec<String>>()
|
||||
})
|
||||
.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 {
|
||||
@@ -267,157 +265,111 @@ pub fn keymap_view(common: CommonData) -> impl View {
|
||||
picker.keys.update(|keys| {
|
||||
keys.clear();
|
||||
});
|
||||
true
|
||||
})
|
||||
.style(move || {
|
||||
.style(move |s| {
|
||||
let config = config.get();
|
||||
Style::BASE
|
||||
.items_center()
|
||||
.height_px(ui_line_height() as f32)
|
||||
s.items_center()
|
||||
.height(ui_line_height() as f32)
|
||||
.width_pct(100.0)
|
||||
.apply_if(i % 2 > 0, |s| {
|
||||
s.background(
|
||||
*config.get_color(LapceColor::EDITOR_CURRENT_LINE),
|
||||
)
|
||||
s.background(config.color(LapceColor::EDITOR_CURRENT_LINE))
|
||||
})
|
||||
.border_bottom(1.0)
|
||||
.border_color(*config.get_color(LapceColor::LAPCE_BORDER))
|
||||
.border_color(config.color(LapceColor::LAPCE_BORDER))
|
||||
})
|
||||
};
|
||||
|
||||
stack(|| {
|
||||
(
|
||||
{
|
||||
container(|| {
|
||||
text_input(editor, || false)
|
||||
.placeholder(|| "Search Key Bindings".to_string())
|
||||
.keyboard_navigatable()
|
||||
.style(move || {
|
||||
Style::BASE
|
||||
.width_pct(100.0)
|
||||
.border_radius(6.0)
|
||||
.border(1.0)
|
||||
.border_color(
|
||||
*config
|
||||
.get()
|
||||
.get_color(LapceColor::LAPCE_BORDER),
|
||||
)
|
||||
})
|
||||
})
|
||||
.style(|| Style::BASE.padding_bottom_px(10.0).width_pct(100.0))
|
||||
},
|
||||
stack(move || {
|
||||
(
|
||||
container(|| {
|
||||
label(|| "Command".to_string()).style(move || {
|
||||
Style::BASE
|
||||
.text_ellipsis()
|
||||
.padding_horiz_px(10.0)
|
||||
.min_width_px(0.0)
|
||||
})
|
||||
})
|
||||
.style(move || {
|
||||
Style::BASE
|
||||
.items_center()
|
||||
.height_pct(100.0)
|
||||
.min_width_px(0.0)
|
||||
.flex_basis_px(0.0)
|
||||
.flex_grow(1.0)
|
||||
.border_right(1.0)
|
||||
.border_color(
|
||||
*config.get().get_color(LapceColor::LAPCE_BORDER),
|
||||
)
|
||||
}),
|
||||
label(|| "Key Binding".to_string()).style(move || {
|
||||
Style::BASE
|
||||
.width_px(200.0)
|
||||
.items_center()
|
||||
.padding_horiz_px(10.0)
|
||||
.height_pct(100.0)
|
||||
.border_right(1.0)
|
||||
.border_color(
|
||||
*config.get().get_color(LapceColor::LAPCE_BORDER),
|
||||
)
|
||||
}),
|
||||
label(|| "Modes".to_string()).style(move || {
|
||||
Style::BASE
|
||||
.width_px(200.0)
|
||||
.items_center()
|
||||
.padding_horiz_px(10.0)
|
||||
.height_pct(100.0)
|
||||
.border_right(1.0)
|
||||
.border_color(
|
||||
*config.get().get_color(LapceColor::LAPCE_BORDER),
|
||||
)
|
||||
.apply_if(!modal.get(), |s| s.hide())
|
||||
}),
|
||||
container(|| {
|
||||
label(|| "When".to_string()).style(move || {
|
||||
Style::BASE
|
||||
.text_ellipsis()
|
||||
.padding_horiz_px(10.0)
|
||||
.min_width_px(0.0)
|
||||
})
|
||||
})
|
||||
.style(move || {
|
||||
Style::BASE
|
||||
.items_center()
|
||||
.height_pct(100.0)
|
||||
.min_width_px(0.0)
|
||||
.flex_basis_px(0.0)
|
||||
.flex_grow(1.0)
|
||||
}),
|
||||
)
|
||||
})
|
||||
.style(move || {
|
||||
let config = config.get();
|
||||
Style::BASE
|
||||
.font_bold()
|
||||
.height_px(ui_line_height() as f32)
|
||||
.width_pct(100.0)
|
||||
.border_top(1.0)
|
||||
.border_bottom(1.0)
|
||||
.border_color(*config.get_color(LapceColor::LAPCE_BORDER))
|
||||
.background(*config.get_color(LapceColor::EDITOR_CURRENT_LINE))
|
||||
stack((
|
||||
container(
|
||||
text_input_view
|
||||
.placeholder(|| "Search Key Bindings".to_string())
|
||||
.keyboard_navigatable()
|
||||
.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))
|
||||
}),
|
||||
container(|| {
|
||||
scroll(|| {
|
||||
virtual_list(
|
||||
VirtualListDirection::Vertical,
|
||||
VirtualListItemSize::Fixed(Box::new(ui_line_height)),
|
||||
items,
|
||||
|(i, (cmd, keymap)): &(
|
||||
usize,
|
||||
(LapceCommand, Option<KeyMap>),
|
||||
)| {
|
||||
(*i, cmd.kind.str(), keymap.clone())
|
||||
},
|
||||
view_fn,
|
||||
)
|
||||
.style(|| Style::BASE.flex_col().width_pct(100.0))
|
||||
})
|
||||
.scroll_bar_color(move || {
|
||||
*config.get().get_color(LapceColor::LAPCE_SCROLL_BAR)
|
||||
})
|
||||
.style(|| Style::BASE.absolute().size_pct(100.0, 100.0))
|
||||
})
|
||||
.style(|| {
|
||||
Style::BASE
|
||||
.width_pct(100.0)
|
||||
.flex_basis_px(0.0)
|
||||
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)
|
||||
}),
|
||||
keyboard_picker_view(picker, common.ui_line_height, config),
|
||||
))
|
||||
.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(
|
||||
VirtualDirection::Vertical,
|
||||
VirtualItemSize::Fixed(Box::new(ui_line_height)),
|
||||
items,
|
||||
|(i, (cmd, keymap)): &(
|
||||
usize,
|
||||
(LapceCommand, Option<KeyMap>),
|
||||
)| { (*i, cmd.kind.str(), keymap.clone()) },
|
||||
view_fn,
|
||||
)
|
||||
.style(|s| s.flex_col().width_pct(100.0)),
|
||||
)
|
||||
.style(|s| s.absolute().size_pct(100.0, 100.0)),
|
||||
)
|
||||
})
|
||||
.style(|| {
|
||||
Style::BASE
|
||||
.absolute()
|
||||
.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_px(20.0)
|
||||
.padding_left_px(20.0)
|
||||
.padding_right_px(20.0)
|
||||
.padding_top(20.0)
|
||||
.padding_left(20.0)
|
||||
.padding_right(20.0)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -427,175 +379,170 @@ fn keyboard_picker_view(
|
||||
config: ReadSignal<Arc<LapceConfig>>,
|
||||
) -> 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()
|
||||
})
|
||||
}),
|
||||
list(
|
||||
move || {
|
||||
picker
|
||||
.keys
|
||||
.get()
|
||||
.iter()
|
||||
.map(|key| key.label().trim().to_string())
|
||||
.filter(|l| !l.is_empty())
|
||||
.enumerate()
|
||||
.collect::<Vec<(usize, String)>>()
|
||||
},
|
||||
|(i, k)| (*i, k.clone()),
|
||||
move |(_, key)| {
|
||||
label(move || key.clone()).style(move || {
|
||||
Style::BASE
|
||||
.padding_horiz_px(5.0)
|
||||
.padding_vert_px(1.0)
|
||||
.margin_right_px(5.0)
|
||||
.border(1.0)
|
||||
.border_radius(6.0)
|
||||
.border_color(
|
||||
*config
|
||||
.get()
|
||||
.get_color(LapceColor::LAPCE_BORDER),
|
||||
)
|
||||
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('_', " "))
|
||||
})
|
||||
},
|
||||
)
|
||||
.style(move || {
|
||||
let config = config.get();
|
||||
Style::BASE
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.width_pct(100.0)
|
||||
.margin_top_px(20.0)
|
||||
.height_px(ui_line_height.get() as f32 + 16.0)
|
||||
.border(1.0)
|
||||
.border_radius(6.0)
|
||||
.border_color(*config.get_color(LapceColor::LAPCE_BORDER))
|
||||
.background(*config.get_color(LapceColor::EDITOR_BACKGROUND))
|
||||
}),
|
||||
stack(|| {
|
||||
(
|
||||
label(|| "Save".to_string())
|
||||
.style(move || {
|
||||
Style::BASE
|
||||
.width_px(100.0)
|
||||
.justify_center()
|
||||
.padding_vert_px(8.0)
|
||||
.border(1.0)
|
||||
.border_radius(6.0)
|
||||
.border_color(
|
||||
*config
|
||||
.get()
|
||||
.get_color(LapceColor::LAPCE_BORDER),
|
||||
)
|
||||
})
|
||||
.on_click(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);
|
||||
}
|
||||
true
|
||||
})
|
||||
.hover_style(move || {
|
||||
Style::BASE.cursor(CursorStyle::Pointer).background(
|
||||
*config.get().get_color(
|
||||
LapceColor::PANEL_HOVERED_BACKGROUND,
|
||||
),
|
||||
)
|
||||
})
|
||||
.active_style(move || {
|
||||
Style::BASE.background(*config.get().get_color(
|
||||
LapceColor::PANEL_HOVERED_ACTIVE_BACKGROUND,
|
||||
))
|
||||
}),
|
||||
label(|| "Cancel".to_string())
|
||||
.style(move || {
|
||||
Style::BASE
|
||||
.margin_left_px(20.0)
|
||||
.width_px(100.0)
|
||||
.justify_center()
|
||||
.padding_vert_px(8.0)
|
||||
.border(1.0)
|
||||
.border_radius(6.0)
|
||||
.border_color(
|
||||
*config
|
||||
.get()
|
||||
.get_color(LapceColor::LAPCE_BORDER),
|
||||
)
|
||||
})
|
||||
.on_click(move |_| {
|
||||
picker.keymap.set(None);
|
||||
true
|
||||
})
|
||||
.hover_style(move || {
|
||||
Style::BASE.cursor(CursorStyle::Pointer).background(
|
||||
*config.get().get_color(
|
||||
LapceColor::PANEL_HOVERED_BACKGROUND,
|
||||
),
|
||||
)
|
||||
})
|
||||
.active_style(move || {
|
||||
Style::BASE.background(*config.get().get_color(
|
||||
LapceColor::PANEL_HOVERED_ACTIVE_BACKGROUND,
|
||||
))
|
||||
}),
|
||||
)
|
||||
.unwrap_or_default()
|
||||
})
|
||||
.style(move || {
|
||||
let config = config.get();
|
||||
Style::BASE
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.width_pct(100.0)
|
||||
.margin_top_px(20.0)
|
||||
.border_color(*config.get_color(LapceColor::LAPCE_BORDER))
|
||||
}),
|
||||
}),
|
||||
dyn_stack(
|
||||
move || {
|
||||
picker
|
||||
.keys
|
||||
.get()
|
||||
.iter()
|
||||
.map(|(key, _)| key.label())
|
||||
.filter(|l| !l.is_empty())
|
||||
.enumerate()
|
||||
.collect::<Vec<(usize, String)>>()
|
||||
},
|
||||
|(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)
|
||||
.border(1.0)
|
||||
.border_radius(6.0)
|
||||
.border_color(
|
||||
config.get().color(LapceColor::LAPCE_BORDER),
|
||||
)
|
||||
})
|
||||
},
|
||||
)
|
||||
})
|
||||
.style(move || {
|
||||
.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 + 16.0)
|
||||
.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::<Vec<KeyMapPress>>(),
|
||||
);
|
||||
}
|
||||
}),
|
||||
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))
|
||||
}),
|
||||
))
|
||||
.style(move |s| {
|
||||
let config = config.get();
|
||||
Style::BASE
|
||||
.items_center()
|
||||
s.items_center()
|
||||
.flex_col()
|
||||
.padding_px(20.0)
|
||||
.width_px(400.0)
|
||||
.padding(20.0)
|
||||
.width(400.0)
|
||||
.border(1.0)
|
||||
.border_radius(6.0)
|
||||
.border_color(*config.get_color(LapceColor::LAPCE_BORDER))
|
||||
.background(*config.get_color(LapceColor::PANEL_BACKGROUND))
|
||||
})
|
||||
})
|
||||
.border_color(config.color(LapceColor::LAPCE_BORDER))
|
||||
.background(config.color(LapceColor::PANEL_BACKGROUND))
|
||||
}),
|
||||
)
|
||||
.keyboard_navigatable()
|
||||
.on_event(EventListener::KeyDown, move |event| {
|
||||
.on_event_stop(EventListener::KeyDown, move |event| {
|
||||
if let Event::KeyDown(key_event) = event {
|
||||
if let Some(keypress) = KeyPressData::keypress(key_event) {
|
||||
let 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);
|
||||
keys.push((keypress, false));
|
||||
})
|
||||
}
|
||||
}
|
||||
true
|
||||
})
|
||||
.style(move || {
|
||||
Style::BASE
|
||||
.absolute()
|
||||
.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()
|
||||
|
||||
@@ -8,30 +8,29 @@ use std::{path::PathBuf, rc::Rc, str::FromStr};
|
||||
|
||||
use anyhow::Result;
|
||||
use floem::{
|
||||
glazier::{KbKey, KeyEvent, Modifiers, PointerEvent},
|
||||
keyboard::{Key, KeyEvent, KeyEventExtModifierSupplement, Modifiers, NamedKey},
|
||||
pointer::PointerInputEvent,
|
||||
reactive::{RwSignal, Scope},
|
||||
};
|
||||
use indexmap::IndexMap;
|
||||
use itertools::Itertools;
|
||||
use lapce_core::mode::{Mode, Modes};
|
||||
use tracing::{debug, error};
|
||||
|
||||
use self::{key::Key, keymap::KeyMap, loader::KeyMapLoader};
|
||||
pub use self::press::KeyPress;
|
||||
use self::{
|
||||
key::KeyInput,
|
||||
keymap::{KeyMap, KeyMapPress},
|
||||
loader::KeyMapLoader,
|
||||
};
|
||||
use crate::{
|
||||
command::{
|
||||
lapce_internal_commands, CommandExecuted, CommandKind, LapceCommand,
|
||||
LapceWorkbenchCommand,
|
||||
},
|
||||
command::{lapce_internal_commands, CommandExecuted, CommandKind, LapceCommand},
|
||||
config::LapceConfig,
|
||||
keypress::{
|
||||
condition::{CheckCondition, Condition},
|
||||
keymap::KeymapMatch,
|
||||
},
|
||||
listener::Listener,
|
||||
};
|
||||
|
||||
pub use self::press::KeyPress;
|
||||
|
||||
const DEFAULT_KEYMAPS_COMMON: &str =
|
||||
include_str!("../../defaults/keymaps-common.toml");
|
||||
const DEFAULT_KEYMAPS_MACOS: &str =
|
||||
@@ -61,11 +60,69 @@ pub trait KeyPressFocus {
|
||||
|
||||
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<usize>,
|
||||
_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<dyn KeyPressFocus> {
|
||||
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<usize>,
|
||||
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::glazier::KeyEvent),
|
||||
Pointer(&'a floem::glazier::PointerEvent),
|
||||
Keyboard(&'a floem::keyboard::KeyEvent),
|
||||
Pointer(&'a floem::pointer::PointerInputEvent),
|
||||
}
|
||||
|
||||
impl<'a> From<&'a KeyEvent> for EventRef<'a> {
|
||||
@@ -74,8 +131,8 @@ impl<'a> From<&'a KeyEvent> for EventRef<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a PointerEvent> for EventRef<'a> {
|
||||
fn from(ev: &'a PointerEvent) -> Self {
|
||||
impl<'a> From<&'a PointerInputEvent> for EventRef<'a> {
|
||||
fn from(ev: &'a PointerInputEvent) -> Self {
|
||||
Self::Pointer(ev)
|
||||
}
|
||||
}
|
||||
@@ -84,20 +141,15 @@ impl<'a> From<&'a PointerEvent> for EventRef<'a> {
|
||||
pub struct KeyPressData {
|
||||
count: RwSignal<Option<usize>>,
|
||||
pending_keypress: RwSignal<Vec<KeyPress>>,
|
||||
workbench_cmd: Listener<LapceWorkbenchCommand>,
|
||||
pub commands: Rc<IndexMap<String, LapceCommand>>,
|
||||
pub keymaps: Rc<IndexMap<Vec<KeyPress>, Vec<KeyMap>>>,
|
||||
pub keymaps: Rc<IndexMap<Vec<KeyMapPress>, Vec<KeyMap>>>,
|
||||
pub command_keymaps: Rc<IndexMap<String, Vec<KeyMap>>>,
|
||||
pub commands_with_keymap: Rc<Vec<KeyMap>>,
|
||||
pub commands_without_keymap: Rc<Vec<LapceCommand>>,
|
||||
}
|
||||
|
||||
impl KeyPressData {
|
||||
pub fn new(
|
||||
cx: Scope,
|
||||
config: &LapceConfig,
|
||||
workbench_cmd: Listener<LapceWorkbenchCommand>,
|
||||
) -> Self {
|
||||
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 {
|
||||
@@ -108,7 +160,6 @@ impl KeyPressData {
|
||||
commands: Rc::new(lapce_internal_commands()),
|
||||
commands_with_keymap: Rc::new(Vec::new()),
|
||||
commands_without_keymap: Rc::new(Vec::new()),
|
||||
workbench_cmd,
|
||||
};
|
||||
keypress.load_commands();
|
||||
keypress
|
||||
@@ -143,7 +194,7 @@ impl KeyPressData {
|
||||
self.commands_without_keymap = Rc::new(commands_without_keymap);
|
||||
}
|
||||
|
||||
fn handle_count<T: KeyPressFocus>(
|
||||
fn handle_count<T: KeyPressFocus + ?Sized>(
|
||||
&self,
|
||||
focus: &T,
|
||||
keypress: &KeyPress,
|
||||
@@ -160,7 +211,11 @@ impl KeyPressData {
|
||||
return false;
|
||||
}
|
||||
|
||||
if let Key::Keyboard(KbKey::Character(c)) = &keypress.key {
|
||||
if let KeyInput::Keyboard {
|
||||
logical: Key::Character(c),
|
||||
..
|
||||
} = &keypress.key
|
||||
{
|
||||
if let Ok(n) = c.parse::<usize>() {
|
||||
if self.count.with_untracked(|count| count.is_some()) || n > 0 {
|
||||
self.count
|
||||
@@ -173,7 +228,7 @@ impl KeyPressData {
|
||||
false
|
||||
}
|
||||
|
||||
fn run_command<T: KeyPressFocus>(
|
||||
fn run_command<T: KeyPressFocus + ?Sized>(
|
||||
&self,
|
||||
command: &str,
|
||||
count: Option<usize>,
|
||||
@@ -181,19 +236,7 @@ impl KeyPressData {
|
||||
focus: &T,
|
||||
) -> CommandExecuted {
|
||||
if let Some(cmd) = self.commands.get(command) {
|
||||
match &cmd.kind {
|
||||
CommandKind::Workbench(cmd) => {
|
||||
self.workbench_cmd.send(cmd.clone());
|
||||
CommandExecuted::Yes
|
||||
}
|
||||
CommandKind::Move(_)
|
||||
| CommandKind::Edit(_)
|
||||
| CommandKind::Focus(_)
|
||||
| CommandKind::MotionMode(_)
|
||||
| CommandKind::MultiSelection(_) => {
|
||||
focus.run_command(cmd, count, mods)
|
||||
}
|
||||
}
|
||||
focus.run_command(cmd, count, mods)
|
||||
} else {
|
||||
CommandExecuted::No
|
||||
}
|
||||
@@ -201,28 +244,26 @@ impl KeyPressData {
|
||||
|
||||
pub fn keypress<'a>(event: impl Into<EventRef<'a>>) -> Option<KeyPress> {
|
||||
let event = event.into();
|
||||
debug!("{event:?}");
|
||||
tracing::trace!("{event:?}");
|
||||
|
||||
let keypress = match event {
|
||||
EventRef::Keyboard(ev)
|
||||
if ev.key == KbKey::Shift && ev.mods.is_empty() =>
|
||||
{
|
||||
return None;
|
||||
}
|
||||
EventRef::Keyboard(ev) => KeyPress {
|
||||
key: Key::Keyboard(ev.key.clone()),
|
||||
// We are removing Shift modifier since the character is already upper case.
|
||||
key: KeyInput::Keyboard {
|
||||
logical: ev.key.logical_key.to_owned(),
|
||||
physical: ev.key.physical_key,
|
||||
key_without_modifiers: ev.key.key_without_modifiers(),
|
||||
},
|
||||
mods: Self::get_key_modifiers(ev),
|
||||
},
|
||||
EventRef::Pointer(ev) => KeyPress {
|
||||
key: Key::Pointer(ev.button),
|
||||
key: KeyInput::Pointer(ev.button),
|
||||
mods: ev.modifiers,
|
||||
},
|
||||
};
|
||||
Some(keypress)
|
||||
}
|
||||
|
||||
pub fn key_down<'a, T: KeyPressFocus>(
|
||||
pub fn key_down<'a, T: KeyPressFocus + ?Sized>(
|
||||
&self,
|
||||
event: impl Into<EventRef<'a>>,
|
||||
focus: &T,
|
||||
@@ -233,9 +274,8 @@ impl KeyPressData {
|
||||
};
|
||||
let mods = keypress.mods;
|
||||
|
||||
let mode = focus.get_mode();
|
||||
if self.handle_count(focus, &keypress) {
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
self.pending_keypress.update(|pending_keypress| {
|
||||
@@ -251,8 +291,8 @@ impl KeyPressData {
|
||||
pending_keypress.clear();
|
||||
});
|
||||
let count = self.count.try_update(|count| count.take()).unwrap();
|
||||
self.run_command(&command, count, mods, focus);
|
||||
return true;
|
||||
return self.run_command(&command, count, mods, focus)
|
||||
== CommandExecuted::Yes;
|
||||
}
|
||||
KeymapMatch::Multiple(commands) => {
|
||||
self.pending_keypress.update(|pending_keypress| {
|
||||
@@ -267,12 +307,12 @@ impl KeyPressData {
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
KeymapMatch::Prefix => {
|
||||
// Here pending_keypress contains only a prefix of some keymap, so let's keep
|
||||
// collecting key presses.
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
KeymapMatch::None => {
|
||||
self.pending_keypress.update(|pending_keypress| {
|
||||
@@ -286,8 +326,8 @@ impl KeyPressData {
|
||||
{
|
||||
if let Some(cmd) = self.commands.get(&command) {
|
||||
if let CommandKind::Move(_) = cmd.kind {
|
||||
focus.run_command(cmd, None, mods);
|
||||
return true;
|
||||
return focus.run_command(cmd, None, mods)
|
||||
== CommandExecuted::Yes;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -295,35 +335,27 @@ impl KeyPressData {
|
||||
}
|
||||
}
|
||||
|
||||
if mode != Mode::Insert
|
||||
&& mode != Mode::Terminal
|
||||
&& self.handle_count(focus, &keypress)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
self.count.set(None);
|
||||
|
||||
let mut mods = keypress.mods;
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
mods.set(Modifiers::SHIFT, false);
|
||||
if mods.is_empty() {
|
||||
if let Key::Keyboard(KbKey::Character(c)) = &keypress.key {
|
||||
focus.receive_char(c);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
mods.set(Modifiers::SHIFT, false);
|
||||
mods.set(Modifiers::ALT, false);
|
||||
if mods.is_empty() {
|
||||
if let Key::Keyboard(KbKey::Character(c)) = &keypress.key {
|
||||
}
|
||||
#[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 true;
|
||||
} else if let Key::Named(NamedKey::Space) = logical {
|
||||
focus.receive_char(" ");
|
||||
self.count.set(None);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -333,42 +365,28 @@ impl KeyPressData {
|
||||
}
|
||||
|
||||
fn get_key_modifiers(key_event: &KeyEvent) -> Modifiers {
|
||||
// We only care about some modifiers
|
||||
let mut mods = (Modifiers::ALT
|
||||
| Modifiers::CONTROL
|
||||
| Modifiers::SHIFT
|
||||
| Modifiers::META)
|
||||
& key_event.mods;
|
||||
let mut mods = key_event.modifiers;
|
||||
|
||||
if mods == Modifiers::SHIFT {
|
||||
if let KbKey::Character(c) = &key_event.key {
|
||||
if !c.chars().all(|c| c.is_alphabetic()) {
|
||||
// We remove the shift if there's only shift pressed,
|
||||
// and the character isn't a letter
|
||||
return Modifiers::empty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match &key_event.key {
|
||||
KbKey::Shift => mods.set(Modifiers::SHIFT, false),
|
||||
KbKey::Alt => mods.set(Modifiers::ALT, false),
|
||||
KbKey::Meta => mods.set(Modifiers::META, false),
|
||||
KbKey::Control => mods.set(Modifiers::CONTROL, false),
|
||||
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<T: KeyPressFocus>(
|
||||
fn match_keymap<T: KeyPressFocus + ?Sized>(
|
||||
&self,
|
||||
keypresses: &[KeyPress],
|
||||
check: &T,
|
||||
) -> KeymapMatch {
|
||||
let keypresses: Vec<KeyPress> =
|
||||
keypresses.iter().map(KeyPress::to_lowercase).collect();
|
||||
let matches = self
|
||||
let keypresses: Vec<KeyMapPress> =
|
||||
keypresses.iter().map(|k| k.keymap_press()).collect();
|
||||
let matches: Vec<_> = self
|
||||
.keymaps
|
||||
.get(&keypresses)
|
||||
.map(|keymaps| {
|
||||
@@ -395,7 +413,7 @@ impl KeyPressData {
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_else(Vec::new);
|
||||
.unwrap_or_default();
|
||||
|
||||
if matches.is_empty() {
|
||||
KeymapMatch::None
|
||||
@@ -412,8 +430,11 @@ impl KeyPressData {
|
||||
}
|
||||
}
|
||||
|
||||
fn check_condition<T: KeyPressFocus>(condition: &str, check: &T) -> bool {
|
||||
fn check_one_condition<T: KeyPressFocus>(
|
||||
fn check_condition<T: KeyPressFocus + ?Sized>(
|
||||
condition: &str,
|
||||
check: &T,
|
||||
) -> bool {
|
||||
fn check_one_condition<T: KeyPressFocus + ?Sized>(
|
||||
condition: &str,
|
||||
check: &T,
|
||||
) -> bool {
|
||||
@@ -454,7 +475,7 @@ impl KeyPressData {
|
||||
fn get_keymaps(
|
||||
config: &LapceConfig,
|
||||
) -> Result<(
|
||||
IndexMap<Vec<KeyPress>, Vec<KeyMap>>,
|
||||
IndexMap<Vec<KeyMapPress>, Vec<KeyMap>>,
|
||||
IndexMap<String, Vec<KeyMap>>,
|
||||
)> {
|
||||
let is_modal = config.core.modal;
|
||||
@@ -462,7 +483,7 @@ impl KeyPressData {
|
||||
let mut loader = KeyMapLoader::new();
|
||||
|
||||
if let Err(err) = loader.load_from_str(DEFAULT_KEYMAPS_COMMON, is_modal) {
|
||||
error!("Failed to load common defaults: {err}");
|
||||
tracing::error!("Failed to load common defaults: {err}");
|
||||
}
|
||||
|
||||
let os_keymaps = if std::env::consts::OS == "macos" {
|
||||
@@ -472,13 +493,13 @@ impl KeyPressData {
|
||||
};
|
||||
|
||||
if let Err(err) = loader.load_from_str(os_keymaps, is_modal) {
|
||||
error!("Failed to load OS defaults: {err}");
|
||||
tracing::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) {
|
||||
error!("Failed to load from {path:?}: {err}");
|
||||
tracing::warn!("Failed to load from {path:?}: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -501,7 +522,7 @@ impl KeyPressData {
|
||||
.cloned()
|
||||
}
|
||||
|
||||
pub fn update_file(keymap: &KeyMap, keys: &[KeyPress]) -> Option<()> {
|
||||
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())
|
||||
@@ -513,16 +534,14 @@ impl KeyPressData {
|
||||
== value
|
||||
.get("key")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(KeyPress::parse)
|
||||
.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().map(|k| k.to_string()).join(" "),
|
||||
)),
|
||||
toml_edit::value(toml_edit::Value::from(keys.iter().join(" "))),
|
||||
);
|
||||
} else {
|
||||
array.remove(index);
|
||||
@@ -551,9 +570,7 @@ impl KeyPressData {
|
||||
if !keys.is_empty() {
|
||||
table.insert(
|
||||
"key",
|
||||
toml_edit::value(toml_edit::Value::from(
|
||||
keys.iter().map(|k| k.to_string()).join(" "),
|
||||
)),
|
||||
toml_edit::value(toml_edit::Value::from(keys.iter().join(" "))),
|
||||
);
|
||||
array.push(table.clone());
|
||||
}
|
||||
@@ -562,7 +579,7 @@ impl KeyPressData {
|
||||
table.insert(
|
||||
"key",
|
||||
toml_edit::value(toml_edit::Value::from(
|
||||
keymap.key.iter().map(|k| k.to_string()).join(" "),
|
||||
keymap.key.iter().join(" "),
|
||||
)),
|
||||
);
|
||||
table.insert(
|
||||
|
||||
@@ -49,6 +49,8 @@ pub enum Condition {
|
||||
PaletteFocus,
|
||||
#[strum(serialize = "completion_focus")]
|
||||
CompletionFocus,
|
||||
#[strum(serialize = "inline_completion_visible")]
|
||||
InlineCompletionVisible,
|
||||
#[strum(serialize = "modal_focus")]
|
||||
ModalFocus,
|
||||
#[strum(serialize = "in_snippet")]
|
||||
@@ -71,7 +73,7 @@ pub enum Condition {
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use floem::glazier::Modifiers;
|
||||
use floem::keyboard::Modifiers;
|
||||
use lapce_core::mode::Mode;
|
||||
|
||||
use super::Condition;
|
||||
|
||||
@@ -1,389 +1,41 @@
|
||||
use std::{
|
||||
fmt::Display,
|
||||
hash::{Hash, Hasher},
|
||||
str::FromStr,
|
||||
};
|
||||
use floem::keyboard::{Key, NamedKey, PhysicalKey};
|
||||
|
||||
use floem::glazier::KbKey;
|
||||
use super::keymap::KeyMapKey;
|
||||
|
||||
#[derive(Clone, Debug, Eq)]
|
||||
pub(crate) enum Key {
|
||||
Keyboard(floem::glazier::KbKey),
|
||||
Pointer(floem::glazier::PointerButton),
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum KeyInput {
|
||||
Keyboard {
|
||||
physical: PhysicalKey,
|
||||
logical: Key,
|
||||
key_without_modifiers: Key,
|
||||
},
|
||||
Pointer(floem::pointer::PointerButton),
|
||||
}
|
||||
|
||||
impl Key {
|
||||
fn keyboard_from_str(s: &str) -> Option<KbKey> {
|
||||
// Checks if it is a character key
|
||||
fn is_key_string(s: &str) -> bool {
|
||||
s.chars().all(|c| !c.is_control())
|
||||
&& s.chars().skip(1).all(|c| !c.is_ascii())
|
||||
}
|
||||
|
||||
// Import into scope to reduce noise
|
||||
use floem::glazier::keyboard_types::Key::*;
|
||||
Some(match s {
|
||||
s if is_key_string(s) => Character(s.to_string()),
|
||||
"unidentified" => Unidentified,
|
||||
"alt" => Alt,
|
||||
"altgraph" => AltGraph,
|
||||
"capslock" => CapsLock,
|
||||
"control" => Control,
|
||||
"fn" => Fn,
|
||||
"fnlock" => FnLock,
|
||||
"meta" => Meta,
|
||||
"numlock" => NumLock,
|
||||
"scrolllock" => ScrollLock,
|
||||
"shift" => Shift,
|
||||
"symbol" => Symbol,
|
||||
"symbollock" => SymbolLock,
|
||||
"hyper" => Hyper,
|
||||
"super" => Super,
|
||||
"enter" => Enter,
|
||||
"tab" => Tab,
|
||||
"arrowdown" => ArrowDown,
|
||||
"arrowleft" => ArrowLeft,
|
||||
"arrowright" => ArrowRight,
|
||||
"arrowup" => ArrowUp,
|
||||
"end" => End,
|
||||
"home" => Home,
|
||||
"pagedown" => PageDown,
|
||||
"pageup" => PageUp,
|
||||
"backspace" => Backspace,
|
||||
"clear" => Clear,
|
||||
"copy" => Copy,
|
||||
"crsel" => CrSel,
|
||||
"cut" => Cut,
|
||||
"delete" => Delete,
|
||||
"eraseeof" => EraseEof,
|
||||
"exsel" => ExSel,
|
||||
"insert" => Insert,
|
||||
"paste" => Paste,
|
||||
"redo" => Redo,
|
||||
"undo" => Undo,
|
||||
"accept" => Accept,
|
||||
"again" => Again,
|
||||
"attn" => Attn,
|
||||
"cancel" => Cancel,
|
||||
"contextmenu" => ContextMenu,
|
||||
"escape" => Escape,
|
||||
"execute" => Execute,
|
||||
"find" => Find,
|
||||
"help" => Help,
|
||||
"pause" => Pause,
|
||||
"play" => Play,
|
||||
"props" => Props,
|
||||
"select" => Select,
|
||||
"zoomin" => ZoomIn,
|
||||
"zoomout" => ZoomOut,
|
||||
"brightnessdown" => BrightnessDown,
|
||||
"brightnessup" => BrightnessUp,
|
||||
"eject" => Eject,
|
||||
"logoff" => LogOff,
|
||||
"power" => Power,
|
||||
"poweroff" => PowerOff,
|
||||
"printscreen" => PrintScreen,
|
||||
"hibernate" => Hibernate,
|
||||
"standby" => Standby,
|
||||
"wakeup" => WakeUp,
|
||||
"allcandidates" => AllCandidates,
|
||||
"alphanumeric" => Alphanumeric,
|
||||
"codeinput" => CodeInput,
|
||||
"compose" => Compose,
|
||||
"convert" => Convert,
|
||||
"dead" => Dead,
|
||||
"finalmode" => FinalMode,
|
||||
"groupfirst" => GroupFirst,
|
||||
"grouplast" => GroupLast,
|
||||
"groupnext" => GroupNext,
|
||||
"groupprevious" => GroupPrevious,
|
||||
"modechange" => ModeChange,
|
||||
"nextcandidate" => NextCandidate,
|
||||
"nonconvert" => NonConvert,
|
||||
"previouscandidate" => PreviousCandidate,
|
||||
"process" => Process,
|
||||
"singlecandidate" => SingleCandidate,
|
||||
"hangulmode" => HangulMode,
|
||||
"hanjamode" => HanjaMode,
|
||||
"junjamode" => JunjaMode,
|
||||
"eisu" => Eisu,
|
||||
"hankaku" => Hankaku,
|
||||
"hiragana" => Hiragana,
|
||||
"hiraganakatakana" => HiraganaKatakana,
|
||||
"kanamode" => KanaMode,
|
||||
"kanjimode" => KanjiMode,
|
||||
"katakana" => Katakana,
|
||||
"romaji" => Romaji,
|
||||
"zenkaku" => Zenkaku,
|
||||
"zenkakuhankaku" => ZenkakuHankaku,
|
||||
"f1" => F1,
|
||||
"f2" => F2,
|
||||
"f3" => F3,
|
||||
"f4" => F4,
|
||||
"f5" => F5,
|
||||
"f6" => F6,
|
||||
"f7" => F7,
|
||||
"f8" => F8,
|
||||
"f9" => F9,
|
||||
"f10" => F10,
|
||||
"f11" => F11,
|
||||
"f12" => F12,
|
||||
"soft1" => Soft1,
|
||||
"soft2" => Soft2,
|
||||
"soft3" => Soft3,
|
||||
"soft4" => Soft4,
|
||||
"channeldown" => ChannelDown,
|
||||
"channelup" => ChannelUp,
|
||||
"close" => Close,
|
||||
"mailforward" => MailForward,
|
||||
"mailreply" => MailReply,
|
||||
"mailsend" => MailSend,
|
||||
"mediaclose" => MediaClose,
|
||||
"mediafastforward" => MediaFastForward,
|
||||
"mediapause" => MediaPause,
|
||||
"mediaplay" => MediaPlay,
|
||||
"mediaplaypause" => MediaPlayPause,
|
||||
"mediarecord" => MediaRecord,
|
||||
"mediarewind" => MediaRewind,
|
||||
"mediastop" => MediaStop,
|
||||
"mediatracknext" => MediaTrackNext,
|
||||
"mediatrackprevious" => MediaTrackPrevious,
|
||||
"new" => New,
|
||||
"open" => Open,
|
||||
"print" => Print,
|
||||
"save" => Save,
|
||||
"spellcheck" => SpellCheck,
|
||||
"key11" => Key11,
|
||||
"key12" => Key12,
|
||||
"audiobalanceleft" => AudioBalanceLeft,
|
||||
"audiobalanceright" => AudioBalanceRight,
|
||||
"audiobassboostdown" => AudioBassBoostDown,
|
||||
"audiobassboosttoggle" => AudioBassBoostToggle,
|
||||
"audiobassboostup" => AudioBassBoostUp,
|
||||
"audiofaderfront" => AudioFaderFront,
|
||||
"audiofaderrear" => AudioFaderRear,
|
||||
"audiosurroundmodenext" => AudioSurroundModeNext,
|
||||
"audiotrebledown" => AudioTrebleDown,
|
||||
"audiotrebleup" => AudioTrebleUp,
|
||||
"audiovolumedown" => AudioVolumeDown,
|
||||
"audiovolumeup" => AudioVolumeUp,
|
||||
"audiovolumemute" => AudioVolumeMute,
|
||||
"microphonetoggle" => MicrophoneToggle,
|
||||
"microphonevolumedown" => MicrophoneVolumeDown,
|
||||
"microphonevolumeup" => MicrophoneVolumeUp,
|
||||
"microphonevolumemute" => MicrophoneVolumeMute,
|
||||
"speechcorrectionlist" => SpeechCorrectionList,
|
||||
"speechinputtoggle" => SpeechInputToggle,
|
||||
"launchapplication1" => LaunchApplication1,
|
||||
"launchapplication2" => LaunchApplication2,
|
||||
"launchcalendar" => LaunchCalendar,
|
||||
"launchcontacts" => LaunchContacts,
|
||||
"launchmail" => LaunchMail,
|
||||
"launchmediaplayer" => LaunchMediaPlayer,
|
||||
"launchmusicplayer" => LaunchMusicPlayer,
|
||||
"launchphone" => LaunchPhone,
|
||||
"launchscreensaver" => LaunchScreenSaver,
|
||||
"launchspreadsheet" => LaunchSpreadsheet,
|
||||
"launchwebbrowser" => LaunchWebBrowser,
|
||||
"launchwebcam" => LaunchWebCam,
|
||||
"launchwordprocessor" => LaunchWordProcessor,
|
||||
"browserback" => BrowserBack,
|
||||
"browserfavorites" => BrowserFavorites,
|
||||
"browserforward" => BrowserForward,
|
||||
"browserhome" => BrowserHome,
|
||||
"browserrefresh" => BrowserRefresh,
|
||||
"browsersearch" => BrowserSearch,
|
||||
"browserstop" => BrowserStop,
|
||||
"appswitch" => AppSwitch,
|
||||
"call" => Call,
|
||||
"camera" => Camera,
|
||||
"camerafocus" => CameraFocus,
|
||||
"endcall" => EndCall,
|
||||
"goback" => GoBack,
|
||||
"gohome" => GoHome,
|
||||
"headsethook" => HeadsetHook,
|
||||
"lastnumberredial" => LastNumberRedial,
|
||||
"notification" => Notification,
|
||||
"mannermode" => MannerMode,
|
||||
"voicedial" => VoiceDial,
|
||||
"tv" => TV,
|
||||
"tv3dmode" => TV3DMode,
|
||||
"tvantennacable" => TVAntennaCable,
|
||||
"tvaudiodescription" => TVAudioDescription,
|
||||
"tvaudiodescriptionmixdown" => TVAudioDescriptionMixDown,
|
||||
"tvaudiodescriptionmixup" => TVAudioDescriptionMixUp,
|
||||
"tvcontentsmenu" => TVContentsMenu,
|
||||
"tvdataservice" => TVDataService,
|
||||
"tvinput" => TVInput,
|
||||
"tvinputcomponent1" => TVInputComponent1,
|
||||
"tvinputcomponent2" => TVInputComponent2,
|
||||
"tvinputcomposite1" => TVInputComposite1,
|
||||
"tvinputcomposite2" => TVInputComposite2,
|
||||
"tvinputhdmi1" => TVInputHDMI1,
|
||||
"tvinputhdmi2" => TVInputHDMI2,
|
||||
"tvinputhdmi3" => TVInputHDMI3,
|
||||
"tvinputhdmi4" => TVInputHDMI4,
|
||||
"tvinputvga1" => TVInputVGA1,
|
||||
"tvmediacontext" => TVMediaContext,
|
||||
"tvnetwork" => TVNetwork,
|
||||
"tvnumberentry" => TVNumberEntry,
|
||||
"tvpower" => TVPower,
|
||||
"tvradioservice" => TVRadioService,
|
||||
"tvsatellite" => TVSatellite,
|
||||
"tvsatellitebs" => TVSatelliteBS,
|
||||
"tvsatellitecs" => TVSatelliteCS,
|
||||
"tvsatellitetoggle" => TVSatelliteToggle,
|
||||
"tvterrestrialanalog" => TVTerrestrialAnalog,
|
||||
"tvterrestrialdigital" => TVTerrestrialDigital,
|
||||
"tvtimer" => TVTimer,
|
||||
"avrinput" => AVRInput,
|
||||
"avrpower" => AVRPower,
|
||||
"colorf0red" => ColorF0Red,
|
||||
"colorf1green" => ColorF1Green,
|
||||
"colorf2yellow" => ColorF2Yellow,
|
||||
"colorf3blue" => ColorF3Blue,
|
||||
"colorf4grey" => ColorF4Grey,
|
||||
"colorf5brown" => ColorF5Brown,
|
||||
"closedcaptiontoggle" => ClosedCaptionToggle,
|
||||
"dimmer" => Dimmer,
|
||||
"displayswap" => DisplaySwap,
|
||||
"dvr" => DVR,
|
||||
"exit" => Exit,
|
||||
"favoriteclear0" => FavoriteClear0,
|
||||
"favoriteclear1" => FavoriteClear1,
|
||||
"favoriteclear2" => FavoriteClear2,
|
||||
"favoriteclear3" => FavoriteClear3,
|
||||
"favoriterecall0" => FavoriteRecall0,
|
||||
"favoriterecall1" => FavoriteRecall1,
|
||||
"favoriterecall2" => FavoriteRecall2,
|
||||
"favoriterecall3" => FavoriteRecall3,
|
||||
"favoritestore0" => FavoriteStore0,
|
||||
"favoritestore1" => FavoriteStore1,
|
||||
"favoritestore2" => FavoriteStore2,
|
||||
"favoritestore3" => FavoriteStore3,
|
||||
"guide" => Guide,
|
||||
"guidenextday" => GuideNextDay,
|
||||
"guidepreviousday" => GuidePreviousDay,
|
||||
"info" => Info,
|
||||
"instantreplay" => InstantReplay,
|
||||
"link" => Link,
|
||||
"listprogram" => ListProgram,
|
||||
"livecontent" => LiveContent,
|
||||
"lock" => Lock,
|
||||
"mediaapps" => MediaApps,
|
||||
"mediaaudiotrack" => MediaAudioTrack,
|
||||
"medialast" => MediaLast,
|
||||
"mediaskipbackward" => MediaSkipBackward,
|
||||
"mediaskipforward" => MediaSkipForward,
|
||||
"mediastepbackward" => MediaStepBackward,
|
||||
"mediastepforward" => MediaStepForward,
|
||||
"mediatopmenu" => MediaTopMenu,
|
||||
"navigatein" => NavigateIn,
|
||||
"navigatenext" => NavigateNext,
|
||||
"navigateout" => NavigateOut,
|
||||
"navigateprevious" => NavigatePrevious,
|
||||
"nextfavoritechannel" => NextFavoriteChannel,
|
||||
"nextuserprofile" => NextUserProfile,
|
||||
"ondemand" => OnDemand,
|
||||
"pairing" => Pairing,
|
||||
"pinpdown" => PinPDown,
|
||||
"pinpmove" => PinPMove,
|
||||
"pinptoggle" => PinPToggle,
|
||||
"pinpup" => PinPUp,
|
||||
"playspeeddown" => PlaySpeedDown,
|
||||
"playspeedreset" => PlaySpeedReset,
|
||||
"playspeedup" => PlaySpeedUp,
|
||||
"randomtoggle" => RandomToggle,
|
||||
"rclowbattery" => RcLowBattery,
|
||||
"recordspeednext" => RecordSpeedNext,
|
||||
"rfbypass" => RfBypass,
|
||||
"scanchannelstoggle" => ScanChannelsToggle,
|
||||
"screenmodenext" => ScreenModeNext,
|
||||
"settings" => Settings,
|
||||
"splitscreentoggle" => SplitScreenToggle,
|
||||
"stbinput" => STBInput,
|
||||
"stbpower" => STBPower,
|
||||
"subtitle" => Subtitle,
|
||||
"teletext" => Teletext,
|
||||
"videomodenext" => VideoModeNext,
|
||||
"wink" => Wink,
|
||||
"zoomtoggle" => ZoomToggle,
|
||||
|
||||
// Custom key name mappings
|
||||
"esc" => Escape,
|
||||
"space" => Character(" ".to_string()),
|
||||
"bs" => Backspace,
|
||||
"up" => ArrowUp,
|
||||
"down" => ArrowDown,
|
||||
"right" => ArrowRight,
|
||||
"left" => ArrowLeft,
|
||||
"del" => Delete,
|
||||
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
fn mouse_from_str(s: &str) -> Option<floem::glazier::PointerButton> {
|
||||
use floem::glazier::PointerButton as B;
|
||||
|
||||
Some(match s {
|
||||
"mousemiddle" => B::Auxiliary,
|
||||
"mouseforward" => B::X2,
|
||||
"mousebackward" => B::X1,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Key {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
use floem::glazier::PointerButton as B;
|
||||
|
||||
impl KeyInput {
|
||||
pub fn keymap_key(&self) -> KeyMapKey {
|
||||
match self {
|
||||
Self::Keyboard(KbKey::Meta) => match std::env::consts::OS {
|
||||
"macos" => "Cmd",
|
||||
"windows" => "Win",
|
||||
_ => "Meta",
|
||||
KeyInput::Pointer(b) => KeyMapKey::Pointer(*b),
|
||||
KeyInput::Keyboard {
|
||||
physical,
|
||||
key_without_modifiers,
|
||||
..
|
||||
} => 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),
|
||||
},
|
||||
Self::Keyboard(key) => return key.fmt(f),
|
||||
Self::Pointer(B::Auxiliary) => "MouseMiddle",
|
||||
Self::Pointer(B::X2) => "MouseForward",
|
||||
Self::Pointer(B::X1) => "MouseBackward",
|
||||
Self::Pointer(_) => "MouseUnimplemented",
|
||||
}
|
||||
.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Key {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let s = s.to_lowercase();
|
||||
|
||||
Key::keyboard_from_str(&s)
|
||||
.map(Key::Keyboard)
|
||||
.or_else(|| Key::mouse_from_str(&s).map(Key::Pointer))
|
||||
.ok_or(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for Key {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
match self {
|
||||
Self::Keyboard(key) => key.hash(state),
|
||||
// TODO: Implement `Hash` for `druid::MouseButton`
|
||||
Self::Pointer(btn) => (*btn as u8).hash(state),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Key {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(Key::Keyboard(a), Key::Keyboard(b)) => a.eq(b),
|
||||
(Key::Pointer(a), Key::Pointer(b)) => a.eq(b),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
use lapce_core::mode::Modes;
|
||||
use std::{fmt::Display, str::FromStr};
|
||||
|
||||
use super::KeyPress;
|
||||
use floem::{
|
||||
keyboard::{Key, KeyCode, Modifiers, NamedKey, PhysicalKey},
|
||||
pointer::PointerButton,
|
||||
};
|
||||
use lapce_core::mode::Modes;
|
||||
|
||||
#[derive(PartialEq, Debug)]
|
||||
pub(super) enum KeymapMatch {
|
||||
@@ -12,8 +16,680 @@ pub(super) enum KeymapMatch {
|
||||
|
||||
#[derive(PartialEq, Eq, Hash, Clone, Debug)]
|
||||
pub struct KeyMap {
|
||||
pub key: Vec<KeyPress>,
|
||||
pub key: Vec<KeyMapPress>,
|
||||
pub modes: Modes,
|
||||
pub when: Option<String>,
|
||||
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<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
match self {
|
||||
Self::Pointer(btn) => (*btn 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<Self> {
|
||||
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<Self, Self::Err> {
|
||||
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) {
|
||||
let _ = f.write_str("Ctrl+");
|
||||
}
|
||||
if self.mods.contains(Modifiers::ALT) {
|
||||
let _ = f.write_str("Alt+");
|
||||
}
|
||||
if self.mods.contains(Modifiers::ALTGR) {
|
||||
let _ = f.write_str("AltGr+");
|
||||
}
|
||||
if self.mods.contains(Modifiers::META) {
|
||||
let _ = f.write_str("Meta+");
|
||||
}
|
||||
if self.mods.contains(Modifiers::SHIFT) {
|
||||
let _ = f.write_str("Shift+");
|
||||
}
|
||||
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::Auxiliary) => f.write_str("MouseMiddle"),
|
||||
Self::Pointer(B::X2) => f.write_str("MouseForward"),
|
||||
Self::Pointer(B::X1) => f.write_str("MouseBackward"),
|
||||
Self::Pointer(_) => f.write_str("MouseUnimplemented"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,10 @@ use indexmap::IndexMap;
|
||||
use lapce_core::mode::Modes;
|
||||
use tracing::{debug, error};
|
||||
|
||||
use super::{keymap::KeyMap, press::KeyPress};
|
||||
use super::keymap::{KeyMap, KeyMapPress};
|
||||
|
||||
pub struct KeyMapLoader {
|
||||
keymaps: IndexMap<Vec<KeyPress>, Vec<KeyMap>>,
|
||||
keymaps: IndexMap<Vec<KeyMapPress>, Vec<KeyMap>>,
|
||||
command_keymaps: IndexMap<String, Vec<KeyMap>>,
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ impl KeyMapLoader {
|
||||
pub fn finalize(
|
||||
self,
|
||||
) -> (
|
||||
IndexMap<Vec<KeyPress>, Vec<KeyMap>>,
|
||||
IndexMap<Vec<KeyMapPress>, Vec<KeyMap>>,
|
||||
IndexMap<String, Vec<KeyMap>>,
|
||||
) {
|
||||
let Self {
|
||||
@@ -112,7 +112,7 @@ impl KeyMapLoader {
|
||||
}
|
||||
|
||||
Ok(Some(KeyMap {
|
||||
key: KeyPress::parse(key),
|
||||
key: KeyMapPress::parse(key),
|
||||
modes,
|
||||
when: toml_keymap
|
||||
.get("when")
|
||||
@@ -122,18 +122,29 @@ impl KeyMapLoader {
|
||||
.get("command")
|
||||
.and_then(|c| c.as_str())
|
||||
.map(|w| w.trim().to_string())
|
||||
.unwrap_or_else(|| "".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###"
|
||||
let keymaps = r#"
|
||||
[[keymaps]]
|
||||
key = "ctrl+w l l"
|
||||
command = "right"
|
||||
@@ -175,64 +186,68 @@ 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 = KeyPress::parse("ctrl+w");
|
||||
let keypress = KeyMapPress::parse("ctrl+w");
|
||||
assert_eq!(keymaps.get(&keypress).unwrap().len(), 4);
|
||||
|
||||
let keypress = KeyPress::parse("ctrl+w l");
|
||||
let keypress = KeyMapPress::parse("ctrl+w l");
|
||||
assert_eq!(keymaps.get(&keypress).unwrap().len(), 2);
|
||||
|
||||
let keypress = KeyPress::parse("ctrl+w h");
|
||||
let keypress = KeyMapPress::parse("ctrl+w h");
|
||||
assert_eq!(keymaps.get(&keypress).unwrap().len(), 1);
|
||||
|
||||
let keypress = KeyPress::parse("ctrl+w l l");
|
||||
let keypress = KeyMapPress::parse("ctrl+w l l");
|
||||
assert_eq!(keymaps.get(&keypress).unwrap().len(), 1);
|
||||
|
||||
let keypress = KeyPress::parse("end");
|
||||
let keypress = KeyMapPress::parse("end");
|
||||
assert_eq!(keymaps.get(&keypress).unwrap().len(), 1);
|
||||
|
||||
// Upper case modifiers
|
||||
let keypress = KeyPress::parse("Ctrl+w");
|
||||
let keypress = KeyMapPress::parse("Ctrl+w");
|
||||
assert_eq!(keymaps.get(&keypress).unwrap().len(), 4);
|
||||
|
||||
let keypress = KeyPress::parse("Ctrl+w l");
|
||||
let keypress = KeyMapPress::parse("Ctrl+w l");
|
||||
assert_eq!(keymaps.get(&keypress).unwrap().len(), 2);
|
||||
|
||||
let keypress = KeyPress::parse("Ctrl+w h");
|
||||
let keypress = KeyMapPress::parse("Ctrl+w h");
|
||||
assert_eq!(keymaps.get(&keypress).unwrap().len(), 1);
|
||||
|
||||
let keypress = KeyPress::parse("Ctrl+w l l");
|
||||
let keypress = KeyMapPress::parse("Ctrl+w l l");
|
||||
assert_eq!(keymaps.get(&keypress).unwrap().len(), 1);
|
||||
|
||||
let keypress = KeyPress::parse("End");
|
||||
let keypress = KeyMapPress::parse("End");
|
||||
assert_eq!(keymaps.get(&keypress).unwrap().len(), 1);
|
||||
|
||||
// No modifier
|
||||
let keypress = KeyPress::parse("shift+i");
|
||||
let keypress = KeyMapPress::parse("shift+i");
|
||||
assert_eq!(keymaps.get(&keypress).unwrap().len(), 1);
|
||||
|
||||
// Mouse keys
|
||||
let keypress = KeyPress::parse("MouseForward");
|
||||
let keypress = KeyMapPress::parse("MouseForward");
|
||||
assert_eq!(keymaps.get(&keypress).unwrap().len(), 1);
|
||||
|
||||
let keypress = KeyPress::parse("mousebackward");
|
||||
let keypress = KeyMapPress::parse("mousebackward");
|
||||
assert_eq!(keymaps.get(&keypress).unwrap().len(), 1);
|
||||
|
||||
let keypress = KeyPress::parse("Ctrl+MouseMiddle");
|
||||
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()))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -1,113 +1,18 @@
|
||||
use std::fmt::Display;
|
||||
use floem::keyboard::Modifiers;
|
||||
|
||||
use floem::glazier::{KbKey, Modifiers};
|
||||
use tracing::warn;
|
||||
use super::{key::KeyInput, keymap::KeyMapPress};
|
||||
|
||||
use super::key::Key;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct KeyPress {
|
||||
pub(super) key: Key,
|
||||
pub(super) key: KeyInput,
|
||||
pub(super) mods: Modifiers,
|
||||
}
|
||||
|
||||
impl KeyPress {
|
||||
pub fn to_lowercase(&self) -> Self {
|
||||
let key = match &self.key {
|
||||
Key::Keyboard(KbKey::Character(c)) => {
|
||||
Key::Keyboard(KbKey::Character(c.to_lowercase()))
|
||||
}
|
||||
_ => self.key.clone(),
|
||||
};
|
||||
Self {
|
||||
key,
|
||||
pub fn keymap_press(&self) -> KeyMapPress {
|
||||
KeyMapPress {
|
||||
key: self.key.keymap_key(),
|
||||
mods: self.mods,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_char(&self) -> bool {
|
||||
let mut mods = self.mods;
|
||||
mods.set(Modifiers::SHIFT, false);
|
||||
if mods.is_empty() {
|
||||
if let Key::Keyboard(KbKey::Character(_c)) = &self.key {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn label(&self) -> String {
|
||||
let mut keys = String::from("");
|
||||
if self.mods.contains(Modifiers::CONTROL) {
|
||||
keys.push_str("Ctrl+");
|
||||
}
|
||||
if self.mods.contains(Modifiers::ALT) {
|
||||
keys.push_str("Alt+");
|
||||
}
|
||||
if self.mods.contains(Modifiers::META) {
|
||||
let keyname = match std::env::consts::OS {
|
||||
"macos" => "Cmd+",
|
||||
"windows" => "Win+",
|
||||
_ => "Meta+",
|
||||
};
|
||||
keys.push_str(keyname);
|
||||
}
|
||||
if self.mods.contains(Modifiers::SHIFT) {
|
||||
keys.push_str("Shift+");
|
||||
}
|
||||
keys.push_str(&self.key.to_string());
|
||||
keys.trim().to_string()
|
||||
}
|
||||
|
||||
pub fn parse(key: &str) -> Vec<Self> {
|
||||
key.split(' ')
|
||||
.filter_map(|k| {
|
||||
let (modifiers, key) = match k.rsplit_once('+') {
|
||||
Some(pair) => pair,
|
||||
None => ("", k),
|
||||
};
|
||||
|
||||
let key = match key.parse().ok() {
|
||||
Some(key) => key,
|
||||
None => {
|
||||
// Skip past unrecognized key definitions
|
||||
warn!("Unrecognized key: {key}");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let mut mods = Modifiers::default();
|
||||
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),
|
||||
"" => (),
|
||||
other => warn!("Invalid key modifier: {}", other),
|
||||
}
|
||||
}
|
||||
|
||||
Some(KeyPress { key, mods })
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for KeyPress {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
if self.mods.contains(Modifiers::CONTROL) {
|
||||
let _ = f.write_str("Ctrl+");
|
||||
}
|
||||
if self.mods.contains(Modifiers::ALT) {
|
||||
let _ = f.write_str("Alt+");
|
||||
}
|
||||
if self.mods.contains(Modifiers::META) {
|
||||
let _ = f.write_str("Meta+");
|
||||
}
|
||||
if self.mods.contains(Modifiers::SHIFT) {
|
||||
let _ = f.write_str("Shift+");
|
||||
}
|
||||
f.write_str(&self.key.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,11 +15,15 @@ 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;
|
||||
@@ -28,6 +32,7 @@ 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;
|
||||
@@ -38,3 +43,6 @@ pub mod web_link;
|
||||
pub mod window;
|
||||
pub mod window_tab;
|
||||
pub mod workspace;
|
||||
|
||||
#[cfg(windows)]
|
||||
extern crate windows_sys as windows;
|
||||
|
||||
@@ -28,11 +28,21 @@ impl<T: Clone + 'static> Listener<T> {
|
||||
Listener { val, cx }
|
||||
}
|
||||
|
||||
/// Listen for values sent to this listener.
|
||||
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;
|
||||
|
||||
self.cx.create_effect(move |_| {
|
||||
cx.create_effect(move |_| {
|
||||
// TODO(minor): Signals could have a `take` method to avoid cloning.
|
||||
if let Some(cmd) = val.get() {
|
||||
on_val(cmd);
|
||||
@@ -50,9 +60,6 @@ impl<T: 'static> Copy for Listener<T> {}
|
||||
|
||||
impl<T: 'static> Clone for Listener<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Listener {
|
||||
cx: self.cx,
|
||||
val: self.val,
|
||||
}
|
||||
*self
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use tracing::{event, Level};
|
||||
use url::Url;
|
||||
|
||||
// Rust-analyzer returns paths in the form of "file:///<drive>:/...", which gets parsed into URL
|
||||
// as "/<drive>://" 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)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,340 @@
|
||||
use floem::cosmic_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::{color::LapceColor, LapceConfig};
|
||||
|
||||
#[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<MarkdownContent> {
|
||||
let mut res = Vec::new();
|
||||
|
||||
let mut current_text = String::new();
|
||||
let code_font_family: Vec<FamilyOwned> =
|
||||
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);
|
||||
|
||||
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,
|
||||
&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.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);
|
||||
res.push(MarkdownContent::Text(text_layout));
|
||||
attr_list = AttrsList::new(default_attrs);
|
||||
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.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 `<code>text</code>`.
|
||||
Event::Html(text) => {
|
||||
attr_list.add_span(
|
||||
pos..pos + text.len(),
|
||||
default_attrs
|
||||
.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
|
||||
}
|
||||
}
|
||||
|
||||
if builder_dirty {
|
||||
let mut text_layout = TextLayout::new();
|
||||
text_layout.set_text(¤t_text, attr_list);
|
||||
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<Attrs<'a>> {
|
||||
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 => 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<LapceLanguage> {
|
||||
// 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<LapceLanguage>,
|
||||
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.color(color),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_marked_string(
|
||||
text: MarkedString,
|
||||
config: &LapceConfig,
|
||||
) -> Vec<MarkdownContent> {
|
||||
match text {
|
||||
MarkedString::String(text) => parse_markdown(&text, 1.5, 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.5,
|
||||
config,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_plaintext(
|
||||
text: &str,
|
||||
line_height: f64,
|
||||
config: &LapceConfig,
|
||||
) -> Vec<MarkdownContent> {
|
||||
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)),
|
||||
),
|
||||
);
|
||||
vec![MarkdownContent::Text(text_layout)]
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
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,
|
||||
command::{LapceCommand, LapceWorkbenchCommand},
|
||||
debug::RunDebugMode,
|
||||
editor::location::EditorLocation,
|
||||
workspace::{LapceWorkspace, SshHost},
|
||||
@@ -14,12 +15,15 @@ use crate::{
|
||||
pub struct PaletteItem {
|
||||
pub content: PaletteItemContent,
|
||||
pub filter_text: String,
|
||||
pub score: i64,
|
||||
pub score: u32,
|
||||
pub indices: Vec<usize>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum PaletteItemContent {
|
||||
PaletteHelp {
|
||||
cmd: LapceWorkbenchCommand,
|
||||
},
|
||||
File {
|
||||
path: PathBuf,
|
||||
full_path: PathBuf,
|
||||
@@ -53,6 +57,10 @@ pub enum PaletteItemContent {
|
||||
SshHost {
|
||||
host: SshHost,
|
||||
},
|
||||
#[cfg(windows)]
|
||||
WslHost {
|
||||
host: crate::workspace::WslHost,
|
||||
},
|
||||
RunAndDebug {
|
||||
mode: RunDebugMode,
|
||||
config: RunDebugConfig,
|
||||
@@ -66,7 +74,14 @@ pub enum PaletteItemContent {
|
||||
Language {
|
||||
name: String,
|
||||
},
|
||||
LineEnding {
|
||||
kind: LineEnding,
|
||||
},
|
||||
SCMReference {
|
||||
name: String,
|
||||
},
|
||||
TerminalProfile {
|
||||
name: String,
|
||||
profile: lapce_rpc::terminal::TerminalProfile,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
use strum_macros::EnumIter;
|
||||
|
||||
use crate::command::LapceWorkbenchCommand;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, EnumIter)]
|
||||
pub enum PaletteKind {
|
||||
PaletteHelp,
|
||||
File,
|
||||
Line,
|
||||
Command,
|
||||
@@ -8,23 +13,30 @@ pub enum PaletteKind {
|
||||
DocumentSymbol,
|
||||
WorkspaceSymbol,
|
||||
SshHost,
|
||||
#[cfg(windows)]
|
||||
WslHost,
|
||||
RunAndDebug,
|
||||
ColorTheme,
|
||||
IconTheme,
|
||||
Language,
|
||||
LineEnding,
|
||||
SCMReferences,
|
||||
TerminalProfile,
|
||||
DiffFiles,
|
||||
}
|
||||
|
||||
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
|
||||
@@ -32,22 +44,63 @@ impl PaletteKind {
|
||||
| PaletteKind::ColorTheme
|
||||
| PaletteKind::IconTheme
|
||||
| PaletteKind::Language
|
||||
| PaletteKind::SCMReferences => "",
|
||||
| PaletteKind::LineEnding
|
||||
| PaletteKind::SCMReferences
|
||||
| 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<LapceWorkbenchCommand> {
|
||||
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::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,
|
||||
@@ -61,6 +114,8 @@ impl PaletteKind {
|
||||
|
||||
pub fn get_input<'a>(&self, input: &'a str) -> &'a str {
|
||||
match self {
|
||||
#[cfg(windows)]
|
||||
PaletteKind::WslHost => input,
|
||||
PaletteKind::File
|
||||
| PaletteKind::Reference
|
||||
| PaletteKind::SshHost
|
||||
@@ -68,14 +123,18 @@ impl PaletteKind {
|
||||
| PaletteKind::ColorTheme
|
||||
| PaletteKind::IconTheme
|
||||
| PaletteKind::Language
|
||||
| PaletteKind::SCMReferences => input,
|
||||
PaletteKind::Command
|
||||
| PaletteKind::LineEnding
|
||||
| PaletteKind::SCMReferences
|
||||
| PaletteKind::DiffFiles => input,
|
||||
PaletteKind::PaletteHelp
|
||||
| PaletteKind::Command
|
||||
| PaletteKind::Workspace
|
||||
| PaletteKind::DocumentSymbol
|
||||
| PaletteKind::WorkspaceSymbol
|
||||
| PaletteKind::Line
|
||||
| PaletteKind::TerminalProfile
|
||||
// | PaletteType::GlobalSearch
|
||||
=> if !input.is_empty() {&input[1..]} else {input},
|
||||
=> input.get(1..).unwrap_or(""),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
use std::sync::Arc;
|
||||
use std::{rc::Rc, sync::Arc};
|
||||
|
||||
use floem::reactive::{use_context, RwSignal, Scope};
|
||||
use floem::{
|
||||
kurbo::Size,
|
||||
reactive::{use_context, Memo, RwSignal, Scope},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::{
|
||||
@@ -34,6 +37,21 @@ pub fn default_panel_order() -> PanelOrder {
|
||||
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,
|
||||
@@ -49,6 +67,7 @@ pub struct PanelInfo {
|
||||
pub panels: PanelOrder,
|
||||
pub styles: im::HashMap<PanelPosition, PanelStyle>,
|
||||
pub size: PanelSize,
|
||||
pub sections: im::HashMap<PanelSection, bool>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -56,14 +75,18 @@ pub struct PanelData {
|
||||
pub panels: RwSignal<PanelOrder>,
|
||||
pub styles: RwSignal<im::HashMap<PanelPosition, PanelStyle>>,
|
||||
pub size: RwSignal<PanelSize>,
|
||||
pub common: CommonData,
|
||||
pub available_size: Memo<Size>,
|
||||
pub sections: RwSignal<im::HashMap<PanelSection, RwSignal<bool>>>,
|
||||
pub common: Rc<CommonData>,
|
||||
}
|
||||
|
||||
impl PanelData {
|
||||
pub fn new(
|
||||
cx: Scope,
|
||||
panels: im::HashMap<PanelPosition, im::Vector<PanelKind>>,
|
||||
common: CommonData,
|
||||
available_size: Memo<Size>,
|
||||
sections: im::HashMap<PanelSection, bool>,
|
||||
common: Rc<CommonData>,
|
||||
) -> Self {
|
||||
let panels = cx.create_rw_signal(panels);
|
||||
|
||||
@@ -125,11 +148,19 @@ impl PanelData {
|
||||
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,
|
||||
}
|
||||
}
|
||||
@@ -139,6 +170,12 @@ impl PanelData {
|
||||
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(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -383,6 +420,21 @@ impl PanelData {
|
||||
let db: Arc<LapceDb> = use_context().unwrap();
|
||||
db.save_panel_orders(self.panels.get_untracked());
|
||||
}
|
||||
|
||||
pub fn section_open(&self, section: PanelSection) -> RwSignal<bool> {
|
||||
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(
|
||||
|
||||