diff --git a/.github/labeler.yml b/.github/labeler.yml index 41f66c7c..7c0ceeb7 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -33,3 +33,6 @@ "Category: C Core": - src/**/* + +"Category: Libraries": + - lib/**/* diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2778de79..a9596150 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,45 +1,20 @@ name: CI -# All builds use lhelper only for releases, -# otherwise for normal builds dependencies are dynamically linked. - on: push: branches: - - '*' -# tags: -# - 'v[0-9]*' + - '*' + pull_request: branches: - - '*' + - '*' + + workflow_dispatch: jobs: - archive_source_code: - name: Source Code Tarball - runs-on: ubuntu-18.04 - # Only on tags/releases - if: startsWith(github.ref, 'refs/tags/') - steps: - - uses: actions/checkout@v2 - - name: Python Setup - uses: actions/setup-python@v2 - with: - python-version: 3.6 - - name: Install Dependencies - run: | - sudo apt-get install -qq ninja-build - pip3 install meson - - name: Package - shell: bash - run: bash scripts/package.sh --version ${GITHUB_REF##*/} --debug --source - - uses: actions/upload-artifact@v2 - with: - name: Source Code Tarball - path: "lite-xl-*-src.tar.gz" - build_linux: name: Linux - runs-on: ubuntu-18.04 + runs-on: ubuntu-22.04 strategy: matrix: config: @@ -49,103 +24,71 @@ jobs: CC: ${{ matrix.config.cc }} CXX: ${{ matrix.config.cxx }} steps: - - name: Set Environment Variables - if: ${{ matrix.config.cc == 'gcc' }} - run: | - echo "$HOME/.local/bin" >> "$GITHUB_PATH" - echo "INSTALL_REF=${GITHUB_REF##*/}" >> "$GITHUB_ENV" - echo "INSTALL_NAME=lite-xl-${GITHUB_REF##*/}-linux-$(uname -m)" >> "$GITHUB_ENV" - - uses: actions/checkout@v2 - - name: Python Setup - uses: actions/setup-python@v2 - with: - python-version: 3.6 - - name: Update Packages - run: sudo apt-get update - - name: Install Dependencies - if: ${{ !startsWith(github.ref, 'refs/tags/') }} - run: bash scripts/install-dependencies.sh --debug - - name: Install Release Dependencies - if: ${{ startsWith(github.ref, 'refs/tags/') }} - run: | - bash scripts/install-dependencies.sh --debug --lhelper - bash scripts/lhelper.sh --debug - - name: Build - run: | - bash --version - bash scripts/build.sh --debug --forcefallback - - name: Package - if: ${{ matrix.config.cc == 'gcc' }} - run: bash scripts/package.sh --version ${INSTALL_REF} --debug --addons --binary - - name: AppImage - if: ${{ matrix.config.cc == 'gcc' && startsWith(github.ref, 'refs/tags/') }} - run: bash scripts/appimage.sh --nobuild --version ${INSTALL_REF} - - name: Upload Artifacts - uses: actions/upload-artifact@v2 - if: ${{ matrix.config.cc == 'gcc' }} - with: - name: Linux Artifacts - path: | - ${{ env.INSTALL_NAME }}.tar.gz - LiteXL-${{ env.INSTALL_REF }}-x86_64.AppImage + - name: Set Environment Variables + if: ${{ matrix.config.cc == 'gcc' }} + run: | + echo "$HOME/.local/bin" >> "$GITHUB_PATH" + echo "INSTALL_REF=${GITHUB_REF##*/}" >> "$GITHUB_ENV" + echo "INSTALL_NAME=lite-xl-${GITHUB_REF##*/}-linux-$(uname -m)-portable" >> "$GITHUB_ENV" + - uses: actions/checkout@v2 + - name: Python Setup + uses: actions/setup-python@v2 + with: + python-version: 3.9 + - name: Update Packages + run: sudo apt-get update + - name: Install Dependencies + run: bash scripts/install-dependencies.sh --debug + - name: Build + run: | + bash --version + bash scripts/build.sh --debug --forcefallback --portable + - name: Package + if: ${{ matrix.config.cc == 'gcc' }} + run: bash scripts/package.sh --version ${INSTALL_REF} --debug --binary + - name: Upload Artifacts + uses: actions/upload-artifact@v2 + if: ${{ matrix.config.cc == 'gcc' }} + with: + name: Linux Artifacts + path: ${{ env.INSTALL_NAME }}.tar.gz build_macos: name: macOS (x86_64) - runs-on: macos-10.15 + runs-on: macos-11 env: CC: clang CXX: clang++ steps: - - name: System Information - run: | - system_profiler SPSoftwareDataType - bash --version - gcc -v - xcodebuild -version - - name: Set Environment Variables - run: | - echo "$HOME/.local/bin" >> "$GITHUB_PATH" - echo "INSTALL_REF=${GITHUB_REF##*/}" >> "$GITHUB_ENV" - echo "INSTALL_NAME=lite-xl-${GITHUB_REF##*/}-macos-$(uname -m)" >> "$GITHUB_ENV" - - uses: actions/checkout@v2 - - name: Python Setup - uses: actions/setup-python@v2 - with: - python-version: 3.9 - - name: Install Dependencies - if: ${{ !startsWith(github.ref, 'refs/tags/') }} - run: bash scripts/install-dependencies.sh --debug - - name: Install Release Dependencies - if: ${{ startsWith(github.ref, 'refs/tags/') }} - run: | - bash scripts/install-dependencies.sh --debug --lhelper - bash scripts/lhelper.sh --debug - - name: Build - run: | - bash --version - bash scripts/build.sh --bundle --debug --forcefallback - - name: Error Logs - if: failure() - run: | - mkdir ${INSTALL_NAME} - cp /usr/var/lhenv/lite-xl/logs/* ${INSTALL_NAME} - tar czvf ${INSTALL_NAME}.tar.gz ${INSTALL_NAME} -# - name: Package -# if: ${{ !startsWith(github.ref, 'refs/tags/') }} -# run: bash scripts/package.sh --version ${INSTALL_REF} --debug --addons - - name: Create DMG Image - run: bash scripts/package.sh --version ${INSTALL_REF} --debug --addons --dmg - - name: Upload DMG Image - uses: actions/upload-artifact@v2 - with: - name: macOS DMG Image - path: ${{ env.INSTALL_NAME }}.dmg - - name: Upload Error Logs - uses: actions/upload-artifact@v2 - if: failure() - with: - name: Error Logs - path: ${{ env.INSTALL_NAME }}.tar.gz + - name: System Information + run: | + system_profiler SPSoftwareDataType + bash --version + gcc -v + xcodebuild -version + - name: Set Environment Variables + run: | + echo "$HOME/.local/bin" >> "$GITHUB_PATH" + echo "INSTALL_REF=${GITHUB_REF##*/}" >> "$GITHUB_ENV" + echo "INSTALL_NAME=lite-xl-${GITHUB_REF##*/}-macos-$(uname -m)" >> "$GITHUB_ENV" + - uses: actions/checkout@v2 + - name: Python Setup + uses: actions/setup-python@v2 + with: + python-version: 3.9 + - name: Install Dependencies + run: bash scripts/install-dependencies.sh --debug + - name: Build + run: | + bash --version + bash scripts/build.sh --bundle --debug --forcefallback + - name: Create DMG Image + run: bash scripts/package.sh --version ${INSTALL_REF} --debug --dmg + - name: Upload DMG Image + uses: actions/upload-artifact@v2 + with: + name: macOS DMG Image + path: ${{ env.INSTALL_NAME }}.dmg build_windows_msys2: name: Windows @@ -160,7 +103,6 @@ jobs: - uses: actions/checkout@v2 - uses: msys2/setup-msys2@v2 with: - #msystem: MINGW64 msystem: ${{ matrix.msystem }} update: true install: >- @@ -170,83 +112,22 @@ jobs: - name: Set Environment Variables run: | echo "$HOME/.local/bin" >> "$GITHUB_PATH" - echo "INSTALL_NAME=lite-xl-${GITHUB_REF##*/}-windows-$(uname -m)" >> "$GITHUB_ENV" echo "INSTALL_REF=${GITHUB_REF##*/}" >> "$GITHUB_ENV" + if [[ "${MSYSTEM}" == "MINGW64" ]]; then + echo "INSTALL_NAME=lite-xl-${GITHUB_REF##*/}-windows-x86_64" >> "$GITHUB_ENV" + else + echo "INSTALL_NAME=lite-xl-${GITHUB_REF##*/}-windows-i686" >> "$GITHUB_ENV" + fi - name: Install Dependencies - if: ${{ !startsWith(github.ref, 'refs/tags/') }} run: bash scripts/install-dependencies.sh --debug - - name: Install Release Dependencies - if: ${{ startsWith(github.ref, 'refs/tags/') }} - run: bash scripts/install-dependencies.sh --debug --lhelper - name: Build run: | bash --version - bash scripts/build.sh --debug --forcefallback - - name: Error Logs - if: failure() - run: | - mkdir ${INSTALL_NAME} - cp /usr/var/lhenv/lite-xl/logs/* ${INSTALL_NAME} - tar czvf ${INSTALL_NAME}.tar.gz ${INSTALL_NAME} + bash scripts/build.sh -U --debug --forcefallback - name: Package - run: bash scripts/package.sh --version ${INSTALL_REF} --debug --addons --binary - - name: Build Installer - if: ${{ startsWith(github.ref, 'refs/tags/') }} - run: bash scripts/innosetup/innosetup.sh --debug + run: bash scripts/package.sh --version ${INSTALL_REF} --debug --binary - name: Upload Artifacts uses: actions/upload-artifact@v2 with: name: Windows Artifacts - path: | - LiteXL*.exe - ${{ env.INSTALL_NAME }}.zip - - name: Upload Error Logs - uses: actions/upload-artifact@v2 - if: failure() - with: - name: Error Logs - path: ${{ env.INSTALL_NAME }}.tar.gz - - deploy: - name: Deployment - runs-on: ubuntu-18.04 -# if: startsWith(github.ref, 'refs/tags/') - if: false - needs: - - archive_source_code - - build_linux - - build_macos - - build_windows_msys2 - steps: - - name: Set Environment Variables - run: echo "INSTALL_REF=${GITHUB_REF##*/}" >> "$GITHUB_ENV" - - uses: actions/download-artifact@v2 - with: - name: Linux Artifacts - - uses: actions/download-artifact@v2 - with: - name: macOS DMG Image - - uses: actions/download-artifact@v2 - with: - name: Source Code Tarball - - uses: actions/download-artifact@v2 - with: - name: Windows Artifacts - - name: Display File Information - shell: bash - run: ls -lR - # Note: not using `actions/create-release@v1` - # because it cannot update an existing release - # see https://github.com/actions/create-release/issues/29 - - uses: softprops/action-gh-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag_name: ${{ env.INSTALL_REF }} - name: Release ${{ env.INSTALL_REF }} - draft: false - prerelease: false - files: | - lite-xl-${{ env.INSTALL_REF }}-* - LiteXL*.AppImage - LiteXL*.exe + path: ${{ env.INSTALL_NAME }}.zip diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..bc2f2e67 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,183 @@ +name: Release + +on: + workflow_dispatch: + inputs: + version: + description: Release Version + default: v2.1.0 + required: true + +jobs: + release: + name: Create Release + runs-on: ubuntu-20.04 + outputs: + upload_url: ${{ steps.create_release.outputs.upload_url }} + version: ${{ steps.tag.outputs.version }} + steps: + - name: Checkout code + uses: actions/checkout@v2 + - name: Fetch Version + id: tag + run: | + if [[ "${{ github.event.inputs.version }}" != "" ]]; then + echo ::set-output name=version::${{ github.event.inputs.version }} + else + echo ::set-output name=version::${GITHUB_REF/refs\/tags\//} + fi + - name: Create Release + id: create_release + uses: softprops/action-gh-release@v1 + with: + tag_name: ${{ steps.tag.outputs.version }} + name: Lite XL ${{ steps.tag.outputs.version }} + draft: true + prerelease: false + body_path: changelog.md + generate_release_notes: true + + build_linux: + name: Linux + needs: release + runs-on: ubuntu-20.04 + env: + CC: gcc + CXX: g++ + steps: + - name: Set Environment Variables + run: | + echo "$HOME/.local/bin" >> "$GITHUB_PATH" + echo "INSTALL_REF=${{ needs.release.outputs.version }}" >> "$GITHUB_ENV" + - uses: actions/checkout@v2 + - name: Python Setup + uses: actions/setup-python@v2 + with: + python-version: 3.9 + - name: Update Packages + run: sudo apt-get update + - name: Install Dependencies + run: | + bash scripts/install-dependencies.sh --debug + sudo apt-get install -y ccache + - name: Build Portable + run: | + bash --version + bash scripts/build.sh --debug --forcefallback --portable --release + - name: Package Portables + run: | + bash scripts/package.sh --version ${INSTALL_REF} --debug --binary --release + bash scripts/package.sh --version ${INSTALL_REF} --debug --addons --binary --release + - name: Build AppImages + run: | + bash scripts/appimage.sh --debug --static --version ${INSTALL_REF} --release + bash scripts/appimage.sh --debug --nobuild --addons --version ${INSTALL_REF} + - name: Upload Files + uses: softprops/action-gh-release@v1 + with: + tag_name: ${{ needs.release.outputs.version }} + files: | + lite-xl-${{ env.INSTALL_REF }}-linux-x86_64-portable.tar.gz + lite-xl-${{ env.INSTALL_REF }}-addons-linux-x86_64-portable.tar.gz + LiteXL-${{ env.INSTALL_REF }}-x86_64.AppImage + LiteXL-${{ env.INSTALL_REF }}-addons-x86_64.AppImage + + build_macos: + name: macOS (x86_64) + needs: release + runs-on: macos-11 + env: + CC: clang + CXX: clang++ + steps: + - name: System Information + run: | + system_profiler SPSoftwareDataType + bash --version + gcc -v + xcodebuild -version + - name: Set Environment Variables + run: | + echo "$HOME/.local/bin" >> "$GITHUB_PATH" + echo "INSTALL_REF=${{ needs.release.outputs.version }}" >> "$GITHUB_ENV" + echo "INSTALL_NAME=lite-xl-${{ needs.release.outputs.version }}-macos-$(uname -m)" >> "$GITHUB_ENV" + echo "INSTALL_NAME_ADDONS=lite-xl-${{ needs.release.outputs.version }}-addons-macos-$(uname -m)" >> "$GITHUB_ENV" + - uses: actions/checkout@v2 + - name: Python Setup + uses: actions/setup-python@v2 + with: + python-version: 3.9 + - name: Install Dependencies + run: bash scripts/install-dependencies.sh --debug + - name: Build + run: | + bash --version + bash scripts/build.sh --bundle --debug --forcefallback --release + - name: Create DMG Image + run: | + bash scripts/package.sh --version ${INSTALL_REF} --debug --dmg --release + bash scripts/package.sh --version ${INSTALL_REF} --debug --addons --dmg --release + - name: Upload Files + uses: softprops/action-gh-release@v1 + with: + tag_name: ${{ needs.release.outputs.version }} + files: | + ${{ env.INSTALL_NAME }}.dmg + ${{ env.INSTALL_NAME_ADDONS }}.dmg + + build_windows_msys2: + name: Windows + needs: release + runs-on: windows-2019 + strategy: + matrix: + msystem: [MINGW32, MINGW64] + defaults: + run: + shell: msys2 {0} + steps: + - uses: actions/checkout@v2 + - uses: msys2/setup-msys2@v2 + with: + msystem: ${{ matrix.msystem }} + update: true + install: >- + base-devel + git + zip + - name: Set Environment Variables + run: | + echo "$HOME/.local/bin" >> "$GITHUB_PATH" + echo "INSTALL_REF=${{ needs.release.outputs.version }}" >> "$GITHUB_ENV" + if [[ "${MSYSTEM}" == "MINGW64" ]]; then + echo "BUILD_ARCH=x86_64" >> "$GITHUB_ENV" + echo "INSTALL_NAME=lite-xl-${{ needs.release.outputs.version }}-windows-x86_64" >> "$GITHUB_ENV" + echo "INSTALL_NAME_ADDONS=lite-xl-${{ needs.release.outputs.version }}-addons-windows-x86_64" >> "$GITHUB_ENV" + else + echo "BUILD_ARCH=i686" >> "$GITHUB_ENV" + echo "INSTALL_NAME=lite-xl-${{ needs.release.outputs.version }}-windows-i686" >> "$GITHUB_ENV" + echo "INSTALL_NAME_ADDONS=lite-xl-${{ needs.release.outputs.version }}-addons-windows-i686" >> "$GITHUB_ENV" + fi + - name: Install Dependencies + run: bash scripts/install-dependencies.sh --debug + - name: Build + run: | + bash --version + bash scripts/build.sh -U --debug --forcefallback --release + - name: Package + run: bash scripts/package.sh --version ${INSTALL_REF} --debug --binary --release + - name: Build Installer + run: bash scripts/innosetup/innosetup.sh --debug --version ${INSTALL_REF} + - name: Package With Addons + run: bash scripts/package.sh --version ${INSTALL_REF} --debug --addons --binary --release + - name: Build Installer With Addons + run: bash scripts/innosetup/innosetup.sh --debug --version ${INSTALL_REF} --addons + - name: Upload Files + uses: softprops/action-gh-release@v1 + with: + tag_name: ${{ needs.release.outputs.version }} + files: | + ${{ env.INSTALL_NAME }}.zip + ${{ env.INSTALL_NAME_ADDONS }}.zip + LiteXL-${{ env.INSTALL_REF }}-${{ env.BUILD_ARCH }}-setup.exe + LiteXL-${{ env.INSTALL_REF }}-addons-${{ env.BUILD_ARCH }}-setup.exe diff --git a/.gitignore b/.gitignore index 745daf8d..1e112b8a 100644 --- a/.gitignore +++ b/.gitignore @@ -2,9 +2,10 @@ build*/ .build*/ lhelper/ submodules/ -subprojects/lua/ -subprojects/reproc/ +subprojects/*/ /appimage* +.vscode +.cache .ccls-cache .lite-debug.log .run* @@ -25,3 +26,5 @@ release_files *.o *.snalyzerinfo + +!resources/windows/*.diff diff --git a/LICENSE b/LICENSE index 20ca7d69..da7be0e2 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2020-2021 Francesco Abbate +Copyright (c) 2020-2021 Lite XL Team Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/README.md b/README.md index b54c17e4..5d7e947b 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ The changes and differences between Lite XL and rxi/lite are listed in the ## Overview -Lite XL is derived from lite. +Lite XL is derived from [lite]. It is a lightweight text editor written mostly in Lua — it aims to provide something practical, pretty, *small* and fast easy to modify and extend, or to use without doing either. @@ -148,12 +148,13 @@ See the [licenses] file for details on licenses used by the required dependencie [screenshot-dark]: https://user-images.githubusercontent.com/433545/111063905-66943980-84b1-11eb-9040-3876f1133b20.png [lite]: https://github.com/rxi/lite [website]: https://lite-xl.com -[build]: https://lite-xl.com/en/documentation/build/ +[build]: https://lite-xl.com/en/documentation/build [Get Lite XL]: https://github.com/lite-xl/lite-xl/releases/latest [Get plugins]: https://github.com/lite-xl/lite-xl-plugins [Get color themes]: https://github.com/lite-xl/lite-xl-colors [changelog]: https://github.com/lite-xl/lite-xl/blob/master/changelog.md [Lite XL plugins repository]: https://github.com/lite-xl/lite-xl-plugins +[plugins repository]: https://github.com/rxi/lite-plugins [colors repository]: https://github.com/lite-xl/lite-xl-colors [LICENSE]: LICENSE [licenses]: licenses/licenses.md diff --git a/build-packages.sh b/build-packages.sh index 4ecda0a0..30a4ea2d 100755 --- a/build-packages.sh +++ b/build-packages.sh @@ -37,6 +37,7 @@ show_help() { echo "-D --dmg Create a DMG disk image (macOS only)." echo " Requires NPM and AppDMG." echo "-I --innosetup Create an InnoSetup installer (Windows only)." + echo "-r --release Compile in release mode." echo "-S --source Create a source code package," echo " including subprojects dependencies." echo @@ -58,6 +59,7 @@ main() { local innosetup local portable local pgo + local release for i in "$@"; do case $i in @@ -109,6 +111,10 @@ main() { portable="--portable" shift ;; + -r|--release) + release="--release" + shift + ;; -S|--source) source="--source" shift @@ -145,6 +151,7 @@ main() { $force_fallback \ $bundle \ $portable \ + $release \ $pgo source scripts/package.sh \ @@ -158,6 +165,7 @@ main() { $appimage \ $dmg \ $innosetup \ + $release \ $source } diff --git a/build.lhelper b/build.lhelper new file mode 100644 index 00000000..1fcdca3a --- /dev/null +++ b/build.lhelper @@ -0,0 +1,8 @@ +CC="${CC:-gcc}" +CXX="${CXX:-g++}" +CFLAGS= +CXXFLAGS= +LDFLAGS= +BUILD_TYPE=Release + +packages=(pcre2 freetype2 sdl2 lua) diff --git a/changelog.md b/changelog.md index c81c7dbe..dbc457a5 100644 --- a/changelog.md +++ b/changelog.md @@ -1,20 +1,405 @@ -This files document the changes done in Lite XL for each release. +# Changes Log -### 2.0.3 +## [2.1.0] - 2022-09-25 -Replace periodic rescan of project folder with a notification based system using the -[dmon library](https://github.com/septag/dmon). Improves performance especially for -large project folders since the application no longer needs to rescan. -The application also reports immediatly any change in the project directory even -when the application is unfocused. +### New Features +* Make distinction between + [line and block comments](https://github.com/lite-xl/lite-xl/pull/771), + and added all appropriate functionality to the commenting/uncommenting lines. + +* [Added in line paste mode](https://github.com/lite-xl/lite-xl/pull/713), + if you copy without a selection. + +* Many [improvements to treeview](https://github.com/lite-xl/lite-xl/pull/732), + including keyboard navigation of treeview, and ability to specify single vs. + double-click behavior. + +* Added in [soft line wrapping](https://github.com/lite-xl/lite-xl/pull/636) + as core plugin, under `linewrapping.lua`, use `F10` to activate. + +* Revamped [StatusView](https://github.com/lite-xl/lite-xl/pull/852) API with + new features that include: + + * Support for predicates, click actions, tooltips on item hover + and custom drawing of added items. + * Hide items that are too huge by rendering with clip_rect. + * Ability to drag or scroll the left or right if too many items to display. + * New status bar commands accessible from the command palette that + include: toggling status bar visibility, toggling specific item visibility, + enable/disable status messages, etc... + +* Added `renderer.font.group` interface to set up + [font fallback groups](https://github.com/lite-xl/lite-xl/pull/616) in + the font renderer, if a token doesn't have a corresponding glyph. + + **Example:** + ```lua + local emoji_font = renderer.font.load(USERDIR .. "/fonts/NotoEmoji-Regular.ttf", 15 * SCALE) + local nonicons = renderer.font.load(USERDIR .. "/fonts/nonicons.ttf", 15 * SCALE) + style.code_font = renderer.font.group({style.code_font, nonicons, emoji_font}) + ``` + +* Added in the ability to specify + [mouse clicks](https://github.com/lite-xl/lite-xl/pull/589) in the + keymap, allowing for easy binds of `ctrl+lclick`, and the like. + + **Example:** + ```lua + keymap.add { ["ctrl+shift+3lclick"] = "core:open-log" } + ``` + +* Improved ability for plugins to be loaded at a given time, by making the + convention of defining a config for the plugin using `common.merge` to merge + existing hashes together, rather than overwriting. + +* Releases will now include all language plugins and the + [settings gui](https://github.com/lite-xl/lite-xl-plugins/pull/65) plugin. + +* New [core.warn](https://github.com/lite-xl/lite-xl/pull/1005) was introduced. + +* Added [suggestions warping](https://github.com/lite-xl/lite-xl/pull/1003) + for `CommandView`. + +* Allow regexes in tokenizer to + [split tokens with group](https://github.com/lite-xl/lite-xl/pull/999). + +* Added [settings gui support](https://github.com/lite-xl/lite-xl/pull/995) + to core plugins. + +* Support for [stricter predicates](https://github.com/lite-xl/lite-xl/pull/990) + by appending a `!`, eg: `"core.docview!"`. + +* [UTF8 support in tokenizer](https://github.com/lite-xl/lite-xl/pull/945) + and new utf8 counter parts of string functions, + eg: `string.ulen`, `string.ulower`, etc... + +* Added [utf8 support](https://github.com/lite-xl/lite-xl/pull/986) on doc + lower and upper commands. + +* Allow syntax patterns to match with the + [beginning of the line](https://github.com/lite-xl/lite-xl/pull/860). + + **Example:** + ```lua + { pattern = "^my_pattern_starting_at_beginning", type="symbol" } + ``` + +* [Add View:on_file_dropped](https://github.com/lite-xl/lite-xl/pull/845). + +* Implemented new function to retrieve current process id of lite-xl + [system.get_process_id()](https://github.com/lite-xl/lite-xl/pull/833). + +* [Allow functions in keymap](https://github.com/lite-xl/lite-xl/pull/948). + +* [Add type ahead to CommandView](https://github.com/lite-xl/lite-xl/pull/963). + +* Add syntax symbols to + [auto-complete](https://github.com/lite-xl/lite-xl/pull/913). + +* Add [animation categories](https://github.com/lite-xl/lite-xl/pull/941) + to enable finer transitions control. + +* Added in a [native plugin](https://github.com/lite-xl/lite-xl/pull/527) + interface that allows for C-level interfacing with a statically-linked + lite-xl. The implementation of this may change in future. + +* Config: added new development option to prevent plugin version checking at + startup named [skip_plugins_version](https://github.com/lite-xl/lite-xl/pull/879) + +* Added a smoothing and strikethrough option to font loading + ([#1087](https://github.com/lite-xl/lite-xl/pull/1087)) + +* Allow command predicates to manage parameters, allow overwriting commands + ([#1098](https://github.com/lite-xl/lite-xl/pull/1098)) + +* Added in simple directory search to treeview. + ([#1110](https://github.com/lite-xl/lite-xl/pull/1110)) + +* Added in native modules suffixes. + ([#1111](https://github.com/lite-xl/lite-xl/pull/1111)) + +* plugin scale: added option to set default scale + ([#1115](https://github.com/lite-xl/lite-xl/pull/1115)) + +* Added in ability to have init.so as a require for cpath. + ([#1126](https://github.com/lite-xl/lite-xl/pull/1126)) + +### Performance Improvements + +* [Load space metrics only when creating font](https://github.com/lite-xl/lite-xl/pull/1032) + +* [Performance improvement](https://github.com/lite-xl/lite-xl/pull/883) + of detect indent plugin. + +* Improve performance of + [ren_draw_rect](https://github.com/lite-xl/lite-xl/pull/935). + +* Improved [tokenizer performance](https://github.com/lite-xl/lite-xl/pull/896). + +* drawwhitespace: [Cache whitespace location](https://github.com/lite-xl/lite-xl/pull/1030) + +* CommandView: improve performance by + [only drawing visible](https://github.com/lite-xl/lite-xl/pull/1047) + +### Backward Incompatible Changes +* [Upgraded Lua to 5.4](https://github.com/lite-xl/lite-xl/pull/781), which + should improve performance, and provide useful extra functionality. It should + also be more available out of the box with most modern + linux/unix-based package managers. + +* Bumped plugin mod-version number as various interfaces like: `DocView`, + `StatusView` and `CommandView` have changed which should require a revision + from plugin developers to make sure their plugins work with this new release. + +* Changed interface for key handling; now, all components should return true if + they've handled the event. + +* For plugin developers, declaring config options by directly assigning + to the plugin table (eg: `config.plugins.plugin_name.myvalue = 10`) was + deprecated in favor of using `common.merge`. + + **Example:** + ```lua + config.plugins.autowrap = common.merge({ + enabled = false, + files = { "%.md$", "%.txt$" } + }, config.plugins.autowrap) + ``` + +* `DocView:draw_text_line` and related functions been used by plugin developers + require a revision, since some of this interfaces were updated to support + line wrapping. + +* Removed `cp_replace`, and replaced this with a core plugin, + [drawwhitespace.lua](https://github.com/lite-xl/lite-xl/pull/908). + +### Deprecated Features +* For plugins the usage of the `--lite-xl` version tag was dropped + in favor of `--mod-version`. + +* Overriding `StatusView:get_items()` has been deprecated in favor of + the new dedicated interface to insert status bar items: + + **New Interface:** + ```lua + ------@return StatusView.Item + function StatusView:add_item( + { predicate, name, alignment, get_item, command, position, tooltip, separator } + ) end + ``` + + **Example:** + ```lua + core.status_view:add_item({ + predicate = nil, + name = "status:memory-usage", + alignment = StatusView.Item.RIGHT, + get_item = function() + return { + style.text, + string.format( + "%.2f MB", + (math.floor(collectgarbage("count") / 10.24) / 100) + ) + } + end, + command = nil, + position = 1, + tooltip = "lua memory usage", + separator = core.status_view.separator2 + }) + ``` + +* [CommandView:enter](https://github.com/lite-xl/lite-xl/pull/1004) now accepts + a single options table as a parameter, meaning that the old way of calling + this function will now show a deprecation message. Also `CommandView:set_text` + and `CommandView:set_hidden_suggestions` has been + [deprecated](https://github.com/lite-xl/lite-xl/pull/1014). + + **Example:** + ```lua + core.command_view:enter("Title", { + submit = function() end, + suggest = function() return end, + cancel = function() end, + validate = function() return true end, + text = "", + select_text = false, + show_suggestions = true, + typeahead = true, + wrap = true + }) + ``` + +### Other Changes +* Removed `dmon`, and implemented independent backends for dirmonitoring. Also + more cleanly split out dirmonitoring into its own class in lua, from core.init. + We should now support FreeBSD; and any other system that uses `kqueue` as + their dir monitoring library. We also have a dummy-backend, which reverts + transparently to scanning if there is some issue with applying OS-level + watches (such as system limits). + +* Removed `libagg` and the font renderer; compacted all font rendering into a + single renderer.c file which uses `libfreetype` directly. Now allows for ad-hoc + bolding, italics, and underlining of fonts. + +* Removed `reproc` and replaced this with a simple POSIX/Windows implementation + in `process.c`. This allows for greater tweakability (i.e. we can now `break` + for debugging purposes), performance (startup time of subprocesses is + noticeably shorter), and simplicity (we no longer have to link reproc, or + winsock, on windows). + +* [Split out `Node` and `EmptyView`](https://github.com/lite-xl/lite-xl/pull/715) + into their own lua files, for plugin extensibility reasons. + +* Improved fuzzy_matching to probably give you something closer to what you're + looking for. + +* Improved handling of alternate keyboard layouts. + +* Added in a default keymap for `core:restart`, `ctrl+shift+r`. + +* Improvements to the [C and C++](https://github.com/lite-xl/lite-xl/pull/875) + syntax files. + +* Improvements to [markdown](https://github.com/lite-xl/lite-xl/pull/862) + syntax file. + +* [Improvements to borderless](https://github.com/lite-xl/lite-xl/pull/994) + mode on Windows. + +* Fixed a bunch of problems relating to + [multi-cursor](https://github.com/lite-xl/lite-xl/pull/886). + +* NagView: [support vscroll](https://github.com/lite-xl/lite-xl/pull/876) when + message is too long. + +* Meson improvements which include: + * Added in meson wraps for freetype, pcre2, and SDL2 which target public, + rather than lite-xl maintained repos. + * [Seperate dirmonitor logic](https://github.com/lite-xl/lite-xl/pull/866), + add build time detection of features. + * Add [fallbacks](https://github.com/lite-xl/lite-xl/pull/798) to all + common dependencies. + * [Update SDL to 2.0.20](https://github.com/lite-xl/lite-xl/pull/884). + * install [docs/api](https://github.com/lite-xl/lite-xl/pull/979) to datadir + for lsp support. + +* Always check if the beginning of the + [text needs to be clipped](https://github.com/lite-xl/lite-xl/pull/871). + +* Added [git commit](https://github.com/lite-xl/lite-xl/pull/859) + on development builds. + +* Update [autocomplete](https://github.com/lite-xl/lite-xl/pull/832) + with changes needed for latest LSP plugin. + +* Use SDL to manage color format mapping in + [ren_draw_rect](https://github.com/lite-xl/lite-xl/pull/829). + +* Various code [clean ups](https://github.com/lite-xl/lite-xl/pull/826). + +* [Autoreload Nagview](https://github.com/lite-xl/lite-xl/pull/942). + +* [Enhancements to scrollbar](https://github.com/lite-xl/lite-xl/pull/916). + +* Set the correct working directory for the + [AppImage version](https://github.com/lite-xl/lite-xl/pull/937). + +* Core: fixes and changes to + [temp file](https://github.com/lite-xl/lite-xl/pull/906) functions. + +* [Added plugin load-time log](https://github.com/lite-xl/lite-xl/pull/966). + +* TreeView improvements for + [multi-project](https://github.com/lite-xl/lite-xl/pull/1010). + +* Open LogView on user/project + [module reload error](https://github.com/lite-xl/lite-xl/pull/1022). + +* Check if ["open" pattern is escaped](https://github.com/lite-xl/lite-xl/pull/1034) + +* Support [UTF-8 on Windows](https://github.com/lite-xl/lite-xl/pull/1041) (Lua) + +* Make system.* functions support + [UTF8 filenames on windows](https://github.com/lite-xl/lite-xl/pull/1042) + +* [Fix memory leak](https://github.com/lite-xl/lite-xl/pull/1039) and wrong + check in font_retrieve + +* Many, many, many more changes that are too numerous to list. + +* CommandView: do not change caret size with config.line_height + ([#1080](https://github.com/lite-xl/lite-xl/pull/1080)) + +## [2.0.5] - 2022-01-29 + +Revamp the project's user module so that modifications are immediately applied. + +Add a mechanism to ignore files or directory based on their project's path. +The new mechanism is backward compatible.* + +Essentially there are two mechanisms: + +- if a '/' or a '/$' appear at the end of the pattern it will match only + directories +- if a '/' appears anywhere in the pattern except at the end the pattern will + be applied to the path + +In the first case, when the pattern corresponds to a directory, a '/' will be +appended to the name of each directory before checking the pattern. + +In the second case, when the pattern corresponds to a path, the complete path of +the file or directory will be used with an initial '/' added to the path. + +Fix several problems with the directory monitoring library. +Now the application should no longer assert when some related system call fails +and we fallback to rescan when an error happens. +On linux no longer use the recursive monitoring which was a source of problem. + +Directory monitoring is now aware of symlinks and treat them appropriately. + +Fix problem when encountering special files type on linux. + +Improve directory monitoring so that the related thread actually waits without +using any CPU time when there are no events. + +Improve the suggestion when changing project folder or opening a new one. +Now the previously used directory are suggested but if the path is changed the +actual existing directories that match the pattern are suggested. +In addition always use the text entered in the command view even if a suggested +entry is highlighted. + +The NagView warning window now no longer moves the document content. + +## [2.0.4] - 2021-12-20 + +Fix some bugs related to newly introduced directory monitoring using the +dmon library. + +Fix a problem with plain text search using Lua patterns by error. + +Fix a problem with visualization of UTF-8 characters that caused garbage +characters visualization. + +Other fixes and improvements contributed by @Guldoman. + +## [2.0.3] - 2021-10-23 + +Replace periodic rescan of project folder with a notification based system +using the [dmon library](https://github.com/septag/dmon). Improves performance +especially for large project folders since the application no longer needs to +rescan. The application also reports immediately any change in the project +directory even when the application is unfocused. Improved find-replace reverse and forward search. -Fixed a bug in incremental syntax highlighting affecting documents with multiple-lines -comments or strings. +Fixed a bug in incremental syntax highlighting affecting documents with +multiple-lines comments or strings. -The application now always shows the tabs in the documents' view even when a single -document is opened. Can be changed with the option `config.always_show_tabs`. +The application now always shows the tabs in the documents' view even when +a single document is opened. Can be changed with the option +`config.always_show_tabs`. Fix problem with numeric keypad function keys not properly working. @@ -22,32 +407,36 @@ Fix problem with pixel not correctly drawn at the window's right edge. Treat correctly and open network paths on Windows. -Add some improvements for very slow network filesystems. +Add some improvements for very slow network file systems. -Fix problem with python syntax highliting, contributed by @dflock. +Fix problem with python syntax highlighting, contributed by @dflock. -### 2.0.2 +## [2.0.2] - 2021-09-10 -Fix problem project directory when starting the application from Launcher on macOS. +Fix problem project directory when starting the application from Launcher on +macOS. -Improved LogView. Entries can now be expanded and there is a context menu to copy the item's content. +Improved LogView. Entries can now be expanded and there is a context menu to +copy the item's content. -Change the behavior of `ctrl+d` to add a multi-cursor selection to the next occurrence. -The old behavior to move the selection to the next occurrence is now done using the shortcut `ctrl+f3`. +Change the behavior of `ctrl+d` to add a multi-cursor selection to the next +occurrence. The old behavior to move the selection to the next occurrence is +now done using the shortcut `ctrl+f3`. -Added a command to create a multi-cursor with all the occurrences of the current selection. -Activated with the shortcut `ctrl+shift+l`. +Added a command to create a multi-cursor with all the occurrences of the +current selection. Activated with the shortcut `ctrl+shift+l`. Fix problem when trying to close an unsaved new document. -No longer shows an error for the `-psn` argument passed to the application on macOS. +No longer shows an error for the `-psn` argument passed to the application on +macOS. Fix `treeview:open-in-system` command on Windows. Fix rename command to update name of document if opened. -Improve the find and replace dialog so that previously used expressions can be recalled -using "up" and "down" keys. +Improve the find and replace dialog so that previously used expressions can be +recalled using "up" and "down" keys. Build package script rewrite with many improvements. @@ -55,63 +444,76 @@ Use bigger fonts by default. Other minor improvements and fixes. -With many thanks to the contributors: @adamharrison, @takase1121, @Guldoman, @redtide, @Timofffee, @boppyt, @Jan200101. +With many thanks to the contributors: @adamharrison, @takase1121, @Guldoman, +@redtide, @Timofffee, @boppyt, @Jan200101. -### 2.0.1 +## [2.0.1] - 2021-08-28 Fix a few bugs and we mandate the mod-version 2 for plugins. This means that users should ensure they have up-to-date plugins for Lite XL 2.0. Here some details about the bug fixes: -- fix a bug that created a fatal error when using the command to change project folder or when closing all the active documents -- add a limit to avoid scaling fonts too much and fix a related invalid memory access for very small fonts +- fix a bug that created a fatal error when using the command to change project + folder or when closing all the active documents +- add a limit to avoid scaling fonts too much and fix a related invalid memory + access for very small fonts - fix focus problem with NagView when switching project directory - fix error that prevented the verification of plugins versions - fix error on X11 that caused a bug window event on exit -### 2.0 +## [2.0] - 2021-08-16 -The 2.0 version of lite contains *breaking changes* to lite, in terms of how plugin settings are structured; -any custom plugins may need to be adjusted accordingly (see note below about plugin namespacing). +The 2.0 version of lite contains *breaking changes* to lite, in terms of how +plugin settings are structured; any custom plugins may need to be adjusted +accordingly (see note below about plugin namespacing). Contains the following new features: -Full PCRE (regex) support for find and replace, as well as in language syntax definitions. Can be accessed -programatically via the lua `regex` module. +Full PCRE (regex) support for find and replace, as well as in language syntax +definitions. Can be accessed programatically via the lua `regex` module. -A full, finalized subprocess API, using libreproc. Subprocess can be started and interacted with using -`Process.new`. +A full, finalized subprocess API, using libreproc. Subprocess can be started +and interacted with using `Process.new`. -Support for multi-cursor editing. Cursors can be created by either ctrl+clicking on the screen, or by using -the keyboard shortcuts ctrl+shift+up/down to create an additional cursor on the previous/next line. +Support for multi-cursor editing. Cursors can be created by either ctrl+clicking +on the screen, or by using the keyboard shortcuts ctrl+shift+up/down to create +an additional cursor on the previous/next line. All build systems other than meson removed. -A more organized directory structure has been implemented; in particular a docs folder which contains C api -documentation, and a resource folder which houses all build resources. +A more organized directory structure has been implemented; in particular a docs +folder which contains C api documentation, and a resource folder which houses +all build resources. -Plugin config namespacing has been implemented. This means that instead of using `config.myplugin.a`, -to read settings, and `config.myplugin = false` to disable plugins, this has been changed to -`config.plugins.myplugin.a`, and `config.plugins.myplugin = false` repsectively. This may require changes to +Plugin config namespacing has been implemented. This means that instead of +using `config.myplugin.a`, to read settings, and `config.myplugin = false` to +disable plugins, this has been changed to `config.plugins.myplugin.a`, and +`config.plugins.myplugin = false` respectively. This may require changes to your user plugin, or to any custom plugins you have. A context menu on right click has been added. -Changes to how we deal with indentation have been implemented; in particular, hitting home no longer brings you -to the start of a line, it'll bring you to the start of indentation, which is more in line with other editors. +Changes to how we deal with indentation have been implemented; in particular, +hitting home no longer brings you to the start of a line, it'll bring you to +the start of indentation, which is more in line with other editors. -Lineguide, and scale plugins moved into the core, and removed from `lite-plugins`. This may also require you to -adjust your personal plugin folder to remove these if they're present. +Lineguide, and scale plugins moved into the core, and removed from +`lite-plugins`. This may also require you to adjust your personal plugin +folder to remove these if they're present. -In addition, there have been many other small fixes and improvements, too numerous to list here. +In addition, there have been many other small fixes and improvements, too +numerous to list here. -### 1.16.11 +## [1.16.11] - 2021-05-28 -When opening directories with too many files lite-xl now keep diplaying files and directories in the treeview. -The application remains functional and the directories can be explored without using too much memory. -In this operating mode the files of the project are not indexed so the command "Core: Find File" will act as the "Core: Open File" command. -The "Project Search: Find" will work by searching all the files present in the project directory even if they are not indexed. +When opening directories with too many files lite-xl now keep displaying files +and directories in the treeview. The application remains functional and the +directories can be explored without using too much memory. In this operating +mode the files of the project are not indexed so the command "Core: Find File" +will act as the "Core: Open File" command.The "Project Search: Find" will work +by searching all the files present in the project directory even if they are +not indexed. Implemented changing fonts per syntax group by @liquidev. @@ -131,30 +533,30 @@ Fix bug with close button not working in borderless window mode. Fix problem with normalization of filename for opened documents. -### 1.16.10 +## [1.16.10] - 2021-05-22 Improved syntax highlight system thanks to @liquidev and @adamharrison. -Thanks to the new system we provide more a accurate syntax highlighting for Lua, C and C++. -Other syntax improvements contributed by @vincens2005. +Thanks to the new system we provide more a accurate syntax highlighting for +Lua, C and C++. Other syntax improvements contributed by @vincens2005. Move to JetBrains Mono and Fira Sans fonts for code and UI respectively. -Thet are provided under the SIL Open Font License, Version 1.1. +They are provided under the SIL Open Font License, Version 1.1. See `doc/licenses.md` for license details. -Fixed bug with fonts and rencache module. -Under very specific situations the application was crashing due to invalid memory access. +Fixed bug with fonts and rencache module. Under very specific situations the +application was crashing due to invalid memory access. Add documentation for keymap binding, thanks to @Janis-Leuenberger. Added a contributors page in `doc/contributors.md`. -### 1.16.9 +## [1.16.9] - 2021-05-06 Fix a bug related to nested panes resizing. Fix problem preventing creating a new file. -### 1.16.8 +## [1.16.8] - 2021-05-06 Fix application crash when using the command `core:restart`. @@ -176,27 +578,28 @@ Both kind of tags can appear in new plugins in the form: where the old tag needs to appear at the end for compatibility. -### 1.16.7 +## [1.16.7] - 2021-05-01 Add support for retina displays on Mac OS X. Fix a few problems related to file paths. -### 1.16.6 +## [1.16.6] - 2021-04-21 -Implement a system to check the compatibility of plugins by checking a release tag. -Plugins that don't have the release tag will not be loaded. +Implement a system to check the compatibility of plugins by checking a release +tag. Plugins that don't have the release tag will not be loaded. Improve and extend the NagView with keyboard commands. -Special thanks to @takase1121 for the implementation and @liquidev for proposing and -discussing the enhancements. +Special thanks to @takase1121 for the implementation and @liquidev for proposing +and discussing the enhancements. Add support to build on Mac OS X and create an application bundle. Special thanks to @mathewmariani for his lite-macos fork, the Mac OS specific resources and his support. -Add hook function `DocView.on_text_change` so that plugin can accurately react on document changes. -Thanks to @vincens2005 for the suggestion and testing the implementation. +Add hook function `DocView.on_text_change` so that plugin can accurately react +on document changes. Thanks to @vincens2005 for the suggestion and testing the +implementation. Enable borderless window mode using the `config.borderless` variable. If enable the system window's bar will be replaced by a title bar provided @@ -214,13 +617,14 @@ commands `draw-whitespace:toggle`, `draw-whitespace:enable`, Improve the NagView to accept keyboard commands and introduce dialog commands. -Add hook function `Doc:on_text_change` called on document changes, to be used by plugins. +Add hook function `Doc:on_text_change` called on document changes, to be +used by plugins. -### 1.16.5 +## [1.16.5] - 2021-03-20 Hotfix for Github's issue https://github.com/franko/lite-xl/issues/122 -### 1.16.4 +## [1.16.4] - 2021-03-20 Add tooltips to show full file names from the tree-view. @@ -235,7 +639,7 @@ Made borders between tabs look cleaner. Fix problem with files using hard tabs. -### 1.16.2 +## [1.16.2] - 2021-03-05 Implement close button for tabs. @@ -243,12 +647,12 @@ Make the command view list of suggestion scrollable to see all the items. Improve update/resize behavior of treeview and toolbar. -### 1.16.1 +## [1.16.1] - 2021-02-25 Improve behavior of commands to move, delete and duplicate multiple lines: no longer include the last line if it does not contain any selection. -Fix graphical artefacts when rendering some fonts like FiraSans. +Fix graphical artifacts when rendering some fonts like FiraSans. Introduce the `config.transitions` boolean variable. When false the transitions will be disabled and changes will be done immediately. @@ -257,7 +661,7 @@ Very useful for remote sessions where visual transitions doesn't work well. Fix many small problems related to the new toolbar and the tooptips. Fix problem with spacing in treeview when using monospace fonts. -### 1.16 +## [1.16] - 2021-02-19 Implement a toolbar shown in the bottom part of the tree-view. The toolbar is especially meant for new users to give an easy, visual, access @@ -269,8 +673,8 @@ are actually resizable. Add config mechanism to disable a plugin by setting `config. = false`. -Improve the "detect indent" plugin to take into account the syntax and exclude comments -for much accurate results. +Improve the "detect indent" plugin to take into account the syntax and exclude +comments for much accurate results. Add command `root:close-all` to close all the documents currently opened. @@ -278,21 +682,24 @@ Show the full path filename of the active document in the window's title. Fix problem with user's module reload not always enabled. -### 1.15 +## [1.15] - 2021-01-04 **Project directories** -Extend your project by adding more directories using the command `core:add-directory`. -To remove them use the corresponding command `core:remove-directory`. +Extend your project by adding more directories using the command +`core:add-directory`. To remove them use the corresponding command +`core:remove-directory`. **Workspaces** The workspace plugin from rxi/lite-plugins is now part of Lite XL. -In addition to the functionalities of the original plugin the extended version will -also remember the window size and position and the additonal project directories. -To not interfere with the project's files the workspace file is saved in the personal -Lite's configuration folder. -On unix-like systems it will be in: `$HOME/.config/lite-xl/ws`. +In addition to the functionalities of the original plugin the extended version +will also remember the window size and position and the additional project +directories. + +To not interfere with the project's files the workspace file is saved in the +personal Lite's configuration folder. On unix-like systems it will be in: +`$HOME/.config/lite-xl/ws`. **Scrolling the Tree View** @@ -304,10 +711,11 @@ As in the unix shell `~` is now used to identify the home directory. **Files and Directories** -Add command to create a new empty directory within the project using the command -`files:create-directory`. -In addition a control-click on a project directory will prompt the user to create -a new directory inside the directory pointed. +Add command to create a new empty directory within the project using the +command `files:create-directory`. + +In addition a control-click on a project directory will prompt the user to +create a new directory inside the directory pointed. **New welcome screen** @@ -315,51 +723,56 @@ Show 'Lite XL' instead of 'lite' and the version number. **Various fixes and improvements** -A few quirks previously with some of the new features have been fixed for a better user experience. +A few quirks previously with some of the new features have been fixed for a +better user experience. -### 1.14 +## [1.14] - 2020-12-13 **Project Management** -Add a new command, Core: Change Project Folder, to change project directory by staying on the same window. -All the current opened documents will be closed. +Add a new command, Core: Change Project Folder, to change project directory by +staying on the same window. All the current opened documents will be closed. The new command is associated with the keyboard combination ctrl+shit+c. -A similar command is also added, Core: Open Project Folder, with key binding ctrl+shift+o. -It will open the chosen folder in a new window. +A similar command is also added, Core: Open Project Folder, with key binding +ctrl+shift+o. It will open the chosen folder in a new window. -In addition Lite XL will now remember the recently used projects across different sessions. -When invoked without arguments it will now open the project more recently used. -If a directory is specified it will behave like before and open the directory indicated as an argument. +In addition Lite XL will now remember the recently used projects across +different sessions. When invoked without arguments it will now open the project +more recently used. If a directory is specified it will behave like before and +open the directory indicated as an argument. **Restart command** -A Core: Restart command is added to restart the editor without leaving the current window. -Very convenient when modifying the Lua code for the editor itself. +A Core: Restart command is added to restart the editor without leaving the +current window. Very convenient when modifying the Lua code for the editor +itself. **User's setting auto-reload** -When saving the user configuration, the user's module, the changes will be automatically applied to the -current instance. +When saving the user configuration, the user's module, the changes will be +automatically applied to the current instance. **Bundle community provided colors schemes** -Included now in the release files the colors schemes from github.com/rxi/lite-colors. +Included now in the release files the colors schemes from +github.com/rxi/lite-colors. **Usability improvements** -Improve left and right scrolling of text to behave like other editors and improves text selection with mouse. +Improve left and right scrolling of text to behave like other editors and +improves text selection with mouse. **Fixes** Correct font's rendering for full hinting mode when using subpixel antialiasing. -### 1.13 +## [1.13] - 2020-12-06 **Rendering options for fonts** -When loading fonts with the function renderer.font.load some rendering options can -be optionally specified: +When loading fonts with the function renderer.font.load some rendering options +can be optionally specified: - antialiasing: grayscale or subpixel - hinting: none, slight or full @@ -368,36 +781,39 @@ See data/core/style.lua for the details about its utilisation. The default remains antialiasing subpixel and hinting slight to reproduce the behavior of previous versions. -The option grayscale with full hinting is specially interesting for crisp font rendering -without color artifacts. +The option grayscale with full hinting is specially interesting for crisp font +rendering without color artifacts. **Unix-like install directories** Use unix-like install directories for the executable and for the data directory. The executable will be placed under $prefix/bin and the data folder will be $prefix/share/lite-xl. + The folder $prefix is not hard-coded in the binary but is determined at runtime as the directory such as the executable is inside $prefix/bin. -If no such $prefix exist it will fall back to the old behavior and use the "data" -folder from the executable directory. -In addtion to the `EXEDIR` global variable an additional variable is exposed, `DATADIR`, -to point to the data directory. +If no such $prefix exist it will fall back to the old behavior and use the +"data" folder from the executable directory. -The old behavior using the "data" directory can be still selected at compile time -using the "portable" option. The released Windows package will use the "data" -directory as before. +In addtion to the `EXEDIR` global variable an additional variable is exposed, +`DATADIR`, to point to the data directory. + +The old behavior using the "data" directory can be still selected at compile +time using the "portable" option. The released Windows package will use the +"data" directory as before. **Configuration stored into the user's home directory** -Now the Lite XL user's configuration will be stored in the user's home directory under -".config/lite-xl". -The home directory is determined using the "HOME" environment variable except on Windows -wher "USERPROFILE" is used instead. +Now the Lite XL user's configuration will be stored in the user's home directory +under .config/lite-xl". + +The home directory is determined using the "HOME" environment variable except +on Windows wher "USERPROFILE" is used instead. A new global variable `USERDIR` is exposed to point to the user's directory. -### 1.11 +## [1.11] - 2020-07-05 - include changes from rxi's Lite 1.11 - fix behavior of tab to indent multiple lines @@ -405,11 +821,36 @@ A new global variable `USERDIR` is exposed to point to the user's directory. - limit project scan to a maximum number of files to limit memory usage - list recently visited files when using "Find File" command -### 1.08 +## [1.08] - 2020-06-14 - Subpixel font rendering, removed gamma correction - Avoid using CPU when the editor is idle -### 1.06 +## [1.06] - 2020-05-31 - subpixel font rendering with gamma correction + +[2.1.0]: https://github.com/lite-xl/lite-xl/releases/tag/v2.1.0 +[2.0.5]: https://github.com/lite-xl/lite-xl/releases/tag/v2.0.5 +[2.0.4]: https://github.com/lite-xl/lite-xl/releases/tag/v2.0.4 +[2.0.3]: https://github.com/lite-xl/lite-xl/releases/tag/v2.0.3 +[2.0.2]: https://github.com/lite-xl/lite-xl/releases/tag/v2.0.2 +[2.0.1]: https://github.com/lite-xl/lite-xl/releases/tag/v2.0.1 +[2.0]: https://github.com/lite-xl/lite-xl/releases/tag/v2.0.0 +[1.16.11]: https://github.com/lite-xl/lite-xl/releases/tag/v1.16.11 +[1.16.10]: https://github.com/lite-xl/lite-xl/releases/tag/v1.16.10 +[1.16.9]: https://github.com/lite-xl/lite-xl/releases/tag/v1.16.9 +[1.16.8]: https://github.com/lite-xl/lite-xl/releases/tag/v1.16.8 +[1.16.7]: https://github.com/lite-xl/lite-xl/releases/tag/v1.16.7 +[1.16.6]: https://github.com/lite-xl/lite-xl/releases/tag/v1.16.6 +[1.16.5]: https://github.com/lite-xl/lite-xl/releases/tag/v1.16.5 +[1.16.4]: https://github.com/lite-xl/lite-xl/releases/tag/v1.16.4 +[1.16.2]: https://github.com/lite-xl/lite-xl/releases/tag/v1.16.2-lite-xl +[1.16.1]: https://github.com/lite-xl/lite-xl/releases/tag/v1.16.1-lite-xl +[1.16]: https://github.com/lite-xl/lite-xl/releases/tag/v1.16.0-lite-xl +[1.15]: https://github.com/lite-xl/lite-xl/releases/tag/v1.15-lite-xl +[1.14]: https://github.com/lite-xl/lite-xl/releases/tag/v1.14-lite-xl +[1.13]: https://github.com/lite-xl/lite-xl/releases/tag/v1.13-lite-xl +[1.11]: https://github.com/lite-xl/lite-xl/releases/tag/v1.11-lite-xl +[1.08]: https://github.com/lite-xl/lite-xl/releases/tag/v1.08-subpixel +[1.06]: https://github.com/lite-xl/lite-xl/releases/tag/1.06-subpixel-rc1 diff --git a/data/colors/default.lua b/data/colors/default.lua new file mode 100644 index 00000000..8f60deee --- /dev/null +++ b/data/colors/default.lua @@ -0,0 +1,46 @@ +local style = require "core.style" +local common = require "core.common" + +style.background = { common.color "#2e2e32" } -- Docview +style.background2 = { common.color "#252529" } -- Treeview +style.background3 = { common.color "#252529" } -- Command view +style.text = { common.color "#97979c" } +style.caret = { common.color "#93DDFA" } +style.accent = { common.color "#e1e1e6" } +-- style.dim - text color for nonactive tabs, tabs divider, prefix in log and +-- search result, hotkeys for context menu and command view +style.dim = { common.color "#525257" } +style.divider = { common.color "#202024" } -- Line between nodes +style.selection = { common.color "#48484f" } +style.line_number = { common.color "#525259" } +style.line_number2 = { common.color "#83838f" } -- With cursor +style.line_highlight = { common.color "#343438" } +style.scrollbar = { common.color "#414146" } +style.scrollbar2 = { common.color "#4b4b52" } -- Hovered +style.scrollbar_track = { common.color "#252529" } +style.nagbar = { common.color "#FF0000" } +style.nagbar_text = { common.color "#FFFFFF" } +style.nagbar_dim = { common.color "rgba(0, 0, 0, 0.45)" } +style.drag_overlay = { common.color "rgba(255,255,255,0.1)" } +style.drag_overlay_tab = { common.color "#93DDFA" } +style.good = { common.color "#72b886" } +style.warn = { common.color "#FFA94D" } +style.error = { common.color "#FF3333" } +style.modified = { common.color "#1c7c9c" } + +style.syntax["normal"] = { common.color "#e1e1e6" } +style.syntax["symbol"] = { common.color "#e1e1e6" } +style.syntax["comment"] = { common.color "#676b6f" } +style.syntax["keyword"] = { common.color "#E58AC9" } -- local function end if case +style.syntax["keyword2"] = { common.color "#F77483" } -- self int float +style.syntax["number"] = { common.color "#FFA94D" } +style.syntax["literal"] = { common.color "#FFA94D" } -- true false nil +style.syntax["string"] = { common.color "#f7c95c" } +style.syntax["operator"] = { common.color "#93DDFA" } -- = + - / < > +style.syntax["function"] = { common.color "#93DDFA" } + +style.log["INFO"] = { icon = "i", color = style.text } +style.log["WARN"] = { icon = "!", color = style.warn } +style.log["ERROR"] = { icon = "!", color = style.error } + +return style diff --git a/data/core/bit.lua b/data/core/bit.lua new file mode 100644 index 00000000..e55fb9bf --- /dev/null +++ b/data/core/bit.lua @@ -0,0 +1,36 @@ +local bit = {} + +local LUA_NBITS = 32 +local ALLONES = (~(((~0) << (LUA_NBITS - 1)) << 1)) + +local function trim(x) + return (x & ALLONES) +end + +local function mask(n) + return (~((ALLONES << 1) << ((n) - 1))) +end + +local function check_args(field, width) + assert(field >= 0, "field cannot be negative") + assert(width > 0, "width must be positive") + assert(field + width < LUA_NBITS and field + width >= 0, + "trying to access non-existent bits") +end + +function bit.extract(n, field, width) + local w = width or 1 + check_args(field, w) + local m = trim(n) + return m >> field & mask(w) +end + +function bit.replace(n, v, field, width) + local w = width or 1 + check_args(field, w) + local m = trim(n) + local x = v & mask(width); + return m & ~(mask(w) << field) | (x << field) +end + +return bit diff --git a/data/core/command.lua b/data/core/command.lua index 2cf851da..f5738304 100644 --- a/data/core/command.lua +++ b/data/core/command.lua @@ -6,17 +6,48 @@ command.map = {} local always_true = function() return true end -function command.add(predicate, map) +---Used iternally by command.add, statusview, and contextmenu to generate a +---function with a condition to evaluate returning the boolean result of this +---evaluation. +--- +---If a string predicate is given it is treated as a require import that should +---return a valid object which is checked against the current active view, +---eg: "core.docview" will match any view that inherits from DocView. Appending +---a `!` at the end of the string means we want to match the given object +---from the import strcitly eg: "core.docview!" only DocView is matched. +---A function that returns a boolean can be used instead to perform a custom +---evaluation, setting to nil means always evaluates to true. +--- +---@param predicate string | table | function +---@return function +function command.generate_predicate(predicate) predicate = predicate or always_true + local strict = false if type(predicate) == "string" then + if predicate:match("!$") then + strict = true + predicate = predicate:gsub("!$", "") + end predicate = require(predicate) end if type(predicate) == "table" then local class = predicate - predicate = function() return core.active_view:is(class) end + if not strict then + predicate = function(...) return core.active_view:extends(class), core.active_view, ... end + else + predicate = function(...) return core.active_view:is(class), core.active_view, ... end + end end + return predicate +end + + +function command.add(predicate, map) + predicate = command.generate_predicate(predicate) for name, fn in pairs(map) do - assert(not command.map[name], "command already exists: " .. name) + if command.map[name] then + core.log_quiet("Replacing existing command \"%s\"", name) + end command.map[name] = { predicate = predicate, perform = fn } end end @@ -33,8 +64,12 @@ end function command.get_all_valid() local res = {} + local memoized_predicates = {} for name, cmd in pairs(command.map) do - if cmd.predicate() then + if memoized_predicates[cmd.predicate] == nil then + memoized_predicates[cmd.predicate] = cmd.predicate() + end + if memoized_predicates[cmd.predicate] then table.insert(res, name) end end @@ -47,8 +82,16 @@ end local function perform(name, ...) local cmd = command.map[name] - if cmd and cmd.predicate(...) then - cmd.perform(...) + if not cmd then return false end + local res = { cmd.predicate(...) } + if table.remove(res, 1) then + if #res > 0 then + -- send values returned from predicate + cmd.perform(table.unpack(res)) + else + -- send original parameters + cmd.perform(...) + end return true end return false @@ -64,7 +107,7 @@ end function command.add_defaults() local reg = { "core", "root", "command", "doc", "findreplace", - "files", "drawwhitespace", "dialog" + "files", "drawwhitespace", "dialog", "log", "statusbar" } for _, name in ipairs(reg) do require("core.commands." .. name) diff --git a/data/core/commands/command.lua b/data/core/commands/command.lua index 1a635a86..44e894f5 100644 --- a/data/core/commands/command.lua +++ b/data/core/commands/command.lua @@ -2,23 +2,23 @@ local core = require "core" local command = require "core.command" command.add("core.commandview", { - ["command:submit"] = function() - core.active_view:submit() + ["command:submit"] = function(active_view) + active_view:submit() end, - ["command:complete"] = function() - core.active_view:complete() + ["command:complete"] = function(active_view) + active_view:complete() end, - ["command:escape"] = function() - core.active_view:exit() + ["command:escape"] = function(active_view) + active_view:exit() end, - ["command:select-previous"] = function() - core.active_view:move_suggestion_idx(1) + ["command:select-previous"] = function(active_view) + active_view:move_suggestion_idx(1) end, - ["command:select-next"] = function() - core.active_view:move_suggestion_idx(-1) + ["command:select-next"] = function(active_view) + active_view:move_suggestion_idx(-1) end, }) diff --git a/data/core/commands/core.lua b/data/core/commands/core.lua index ad0d4b10..cdf8d421 100644 --- a/data/core/commands/core.lua +++ b/data/core/commands/core.lua @@ -10,8 +10,18 @@ local restore_title_view = false local function suggest_directory(text) text = common.home_expand(text) - return common.home_encode_list((text == "" or text == common.home_expand(common.dirname(core.project_dir))) - and core.recent_projects or common.dir_path_suggest(text)) + local basedir = common.dirname(core.project_dir) + return common.home_encode_list((basedir and text == basedir .. PATHSEP or text == "") and + core.recent_projects or common.dir_path_suggest(text)) +end + +local function check_directory_path(path) + local abs_path = system.absolute_path(path) + local info = abs_path and system.get_file_info(abs_path) + if not info or info.type ~= 'dir' then + return nil + end + return abs_path end command.add(nil, { @@ -38,36 +48,42 @@ command.add(nil, { end, ["core:reload-module"] = function() - core.command_view:enter("Reload Module", function(text, item) - local text = item and item.text or text - core.reload_module(text) - core.log("Reloaded module %q", text) - end, function(text) - local items = {} - for name in pairs(package.loaded) do - table.insert(items, name) + core.command_view:enter("Reload Module", { + submit = function(text, item) + local text = item and item.text or text + core.reload_module(text) + core.log("Reloaded module %q", text) + end, + suggest = function(text) + local items = {} + for name in pairs(package.loaded) do + table.insert(items, name) + end + return common.fuzzy_match(items, text) end - return common.fuzzy_match(items, text) - end) + }) end, ["core:find-command"] = function() local commands = command.get_all_valid() - core.command_view:enter("Do Command", function(text, item) - if item then - command.perform(item.command) + core.command_view:enter("Do Command", { + submit = function(text, item) + if item then + command.perform(item.command) + end + end, + suggest = function(text) + local res = common.fuzzy_match(commands, text) + for i, name in ipairs(res) do + res[i] = { + text = command.prettify_name(name), + info = keymap.get_binding(name), + command = name, + } + end + return res end - end, function(text) - local res = common.fuzzy_match(commands, text) - for i, name in ipairs(res) do - res[i] = { - text = command.prettify_name(name), - info = keymap.get_binding(name), - command = name, - } - end - return res - end) + }) end, ["core:find-file"] = function() @@ -81,56 +97,72 @@ command.add(nil, { table.insert(files, common.home_encode(path .. item.filename)) end end - core.command_view:enter("Open File From Project", function(text, item) - text = item and item.text or text - core.root_view:open_doc(core.open_doc(common.home_expand(text))) - end, function(text) - return common.fuzzy_match_with_recents(files, core.visited_files, text) - end) + core.command_view:enter("Open File From Project", { + submit = function(text, item) + text = item and item.text or text + core.root_view:open_doc(core.open_doc(common.home_expand(text))) + end, + suggest = function(text) + return common.fuzzy_match_with_recents(files, core.visited_files, text) + end + }) end, ["core:new-doc"] = function() core.root_view:open_doc(core.open_doc()) end, + ["core:new-named-doc"] = function() + core.command_view:enter("File name", { + submit = function(text) + core.root_view:open_doc(core.open_doc(text)) + end + }) + end, + ["core:open-file"] = function() local view = core.active_view + local text if view.doc and view.doc.abs_filename then local dirname, filename = view.doc.abs_filename:match("(.*)[/\\](.+)$") if dirname then dirname = core.normalize_to_project_dir(dirname) - local text = dirname == core.project_dir and "" or common.home_encode(dirname) .. PATHSEP - core.command_view:set_text(text) + text = dirname == core.project_dir and "" or common.home_encode(dirname) .. PATHSEP end end - core.command_view:enter("Open File", function(text) - local filename = system.absolute_path(common.home_expand(text)) - core.root_view:open_doc(core.open_doc(filename)) - end, function (text) - return common.home_encode_list(common.path_suggest(common.home_expand(text))) - end, nil, function(text) - local filename = common.home_expand(text) - local path_stat, err = system.get_file_info(filename) - if err then - if err:find("No such file", 1, true) then - -- check if the containing directory exists - local dirname = common.dirname(filename) - local dir_stat = dirname and system.get_file_info(dirname) - if not dirname or (dir_stat and dir_stat.type == 'dir') then + core.command_view:enter("Open File", { + text = text, + submit = function(text) + local filename = system.absolute_path(common.home_expand(text)) + core.root_view:open_doc(core.open_doc(filename)) + end, + suggest = function (text) + return common.home_encode_list(common.path_suggest(common.home_expand(text))) + end, + validate = function(text) + local filename = common.home_expand(text) + local path_stat, err = system.get_file_info(filename) + if err then + if err:find("No such file", 1, true) then + -- check if the containing directory exists + local dirname = common.dirname(filename) + local dir_stat = dirname and system.get_file_info(dirname) + if not dirname or (dir_stat and dir_stat.type == 'dir') then + return true + end + end + core.error("Cannot open file %s: %s", text, err) + elseif path_stat.type == 'dir' then + core.error("Cannot open %s, is a folder", text) + else return true end - end - core.error("Cannot open file %s: %s", text, err) - elseif path_stat.type == 'dir' then - core.error("Cannot open %s, is a folder", text) - else - return true - end - end) + end, + }) end, ["core:open-log"] = function() - local node = core.root_view:get_active_node() + local node = core.root_view:get_active_node_default() node:add_view(LogView()) end, @@ -141,62 +173,79 @@ command.add(nil, { end, ["core:open-project-module"] = function() - local filename = ".lite_project.lua" - if system.get_file_info(filename) then - core.root_view:open_doc(core.open_doc(filename)) - else - local doc = core.open_doc() - core.root_view:open_doc(doc) - doc:save(filename) + if not system.get_file_info(".lite_project.lua") then + core.try(core.write_init_project_module, ".lite_project.lua") end + local doc = core.open_doc(".lite_project.lua") + core.root_view:open_doc(doc) + doc:save() end, ["core:change-project-folder"] = function() local dirname = common.dirname(core.project_dir) + local text if dirname then - core.command_view:set_text(common.home_encode(dirname)) + text = common.home_encode(dirname) .. PATHSEP end - core.command_view:enter("Change Project Folder", function(text, item) - text = system.absolute_path(common.home_expand(item and item.text or text)) - if text == core.project_dir then return end - local path_stat = system.get_file_info(text) - if not path_stat or path_stat.type ~= 'dir' then - core.error("Cannot open folder %q", text) - return - end - core.confirm_close_docs(core.docs, core.open_folder_project, text) - end, suggest_directory) + core.command_view:enter("Change Project Folder", { + text = text, + submit = function(text) + local path = common.home_expand(text) + local abs_path = check_directory_path(path) + if not abs_path then + core.error("Cannot open directory %q", path) + return + end + if abs_path == core.project_dir then return end + core.confirm_close_docs(core.docs, function(dirpath) + core.open_folder_project(dirpath) + end, abs_path) + end, + suggest = suggest_directory + }) end, ["core:open-project-folder"] = function() local dirname = common.dirname(core.project_dir) + local text if dirname then - core.command_view:set_text(common.home_encode(dirname)) + text = common.home_encode(dirname) .. PATHSEP end - core.command_view:enter("Open Project", function(text, item) - text = common.home_expand(item and item.text or text) - local path_stat = system.get_file_info(text) - if not path_stat or path_stat.type ~= 'dir' then - core.error("Cannot open folder %q", text) - return - end - system.exec(string.format("%q %q", EXEFILE, text)) - end, suggest_directory) + core.command_view:enter("Open Project", { + text = text, + submit = function(text) + local path = common.home_expand(text) + local abs_path = check_directory_path(path) + if not abs_path then + core.error("Cannot open directory %q", path) + return + end + if abs_path == core.project_dir then + core.error("Directory %q is currently opened", abs_path) + return + end + system.exec(string.format("%q %q", EXEFILE, abs_path)) + end, + suggest = suggest_directory + }) end, ["core:add-directory"] = function() - core.command_view:enter("Add Directory", function(text) - text = common.home_expand(text) - local path_stat, err = system.get_file_info(text) - if not path_stat then - core.error("cannot open %q: %s", text, err) - return - elseif path_stat.type ~= 'dir' then - core.error("%q is not a directory", text) - return - end - core.add_project_directory(system.absolute_path(text)) - end, suggest_directory) + core.command_view:enter("Add Directory", { + submit = function(text) + text = common.home_expand(text) + local path_stat, err = system.get_file_info(text) + if not path_stat then + core.error("cannot open %q: %s", text, err) + return + elseif path_stat.type ~= 'dir' then + core.error("%q is not a directory", text) + return + end + core.add_project_directory(system.absolute_path(text)) + end, + suggest = suggest_directory + }) end, ["core:remove-directory"] = function() @@ -205,14 +254,17 @@ command.add(nil, { for i = n, 2, -1 do dir_list[n - i + 1] = core.project_directories[i].name end - core.command_view:enter("Remove Directory", function(text, item) - text = common.home_expand(item and item.text or text) - if not core.remove_project_directory(text) then - core.error("No directory %q to be removed", text) + core.command_view:enter("Remove Directory", { + submit = function(text, item) + text = common.home_expand(item and item.text or text) + if not core.remove_project_directory(text) then + core.error("No directory %q to be removed", text) + end + end, + suggest = function(text) + text = common.home_expand(text) + return common.home_encode_list(common.dir_list_suggest(text, dir_list)) end - end, function(text) - text = common.home_expand(text) - return common.home_encode_list(common.dir_list_suggest(text, dir_list)) - end) + }) end, }) diff --git a/data/core/commands/dialog.lua b/data/core/commands/dialog.lua index 90606abb..1f3b4e71 100644 --- a/data/core/commands/dialog.lua +++ b/data/core/commands/dialog.lua @@ -3,30 +3,25 @@ local command = require "core.command" local common = require "core.common" command.add("core.nagview", { - ["dialog:previous-entry"] = function() - local v = core.active_view + ["dialog:previous-entry"] = function(v) local hover = v.hovered_item or 1 v:change_hovered(hover == 1 and #v.options or hover - 1) end, - ["dialog:next-entry"] = function() - local v = core.active_view + ["dialog:next-entry"] = function(v) local hover = v.hovered_item or 1 v:change_hovered(hover == #v.options and 1 or hover + 1) end, - ["dialog:select-yes"] = function() - local v = core.active_view + ["dialog:select-yes"] = function(v) if v ~= core.nag_view then return end v:change_hovered(common.find_index(v.options, "default_yes")) command.perform "dialog:select" end, - ["dialog:select-no"] = function() - local v = core.active_view + ["dialog:select-no"] = function(v) if v ~= core.nag_view then return end v:change_hovered(common.find_index(v.options, "default_no")) command.perform "dialog:select" end, - ["dialog:select"] = function() - local v = core.active_view + ["dialog:select"] = function(v) if v.hovered_item then v.on_selected(v.options[v.hovered_item]) v:next() diff --git a/data/core/commands/doc.lua b/data/core/commands/doc.lua index c3063f97..365c0d19 100644 --- a/data/core/commands/doc.lua +++ b/data/core/commands/doc.lua @@ -47,19 +47,34 @@ end local function cut_or_copy(delete) local full_text = "" + local text = "" + core.cursor_clipboard = {} + core.cursor_clipboard_whole_line = {} for idx, line1, col1, line2, col2 in doc():get_selections() do if line1 ~= line2 or col1 ~= col2 then - local text = doc():get_text(line1, col1, line2, col2) + text = doc():get_text(line1, col1, line2, col2) + full_text = full_text == "" and text or (full_text .. " " .. text) + core.cursor_clipboard_whole_line[idx] = false if delete then doc():delete_to_cursor(idx, 0) end - full_text = full_text == "" and text or (full_text .. "\n" .. text) - doc().cursor_clipboard[idx] = text - else - doc().cursor_clipboard[idx] = "" + else -- Cut/copy whole line + text = doc().lines[line1] + full_text = full_text == "" and text or (full_text .. text) + core.cursor_clipboard_whole_line[idx] = true + if delete then + if line1 < #doc().lines then + doc():remove(line1, 1, line1 + 1, 1) + elseif #doc().lines == 1 then + doc():remove(line1, 1, line1, math.huge) + else + doc():remove(line1 - 1, math.huge, line1, math.huge) + end + end end + core.cursor_clipboard[idx] = text end - doc().cursor_clipboard["full"] = full_text + core.cursor_clipboard["full"] = full_text system.set_clipboard(full_text) end @@ -74,17 +89,100 @@ local function split_cursor(direction) core.blink_reset() end -local function set_cursor(x, y, snap_type) - local line, col = dv():resolve_screen_position(x, y) - doc():set_selection(line, col, line, col) +local function set_cursor(dv, x, y, snap_type) + local line, col = dv:resolve_screen_position(x, y) + dv.doc:set_selection(line, col, line, col) if snap_type == "word" or snap_type == "lines" then command.perform("doc:select-" .. snap_type) end - dv().mouse_selecting = { line, col, snap_type } + dv.mouse_selecting = { line, col, snap_type } core.blink_reset() end -local selection_commands = { +local function line_comment(comment, line1, col1, line2, col2) + local start_comment = (type(comment) == 'table' and comment[1] or comment) .. " " + local end_comment = (type(comment) == 'table' and " " .. comment[2]) + local uncomment = true + local start_offset = math.huge + for line = line1, line2 do + local text = doc().lines[line] + local s = text:find("%S") + if s then + local cs, ce = text:find(start_comment, s, true) + if cs ~= s then + uncomment = false + end + start_offset = math.min(start_offset, s) + end + end + + local end_line = col2 == #doc().lines[line2] + for line = line1, line2 do + local text = doc().lines[line] + local s = text:find("%S") + if s and uncomment then + if end_comment and text:sub(#text - #end_comment, #text - 1) == end_comment then + doc():remove(line, #text - #end_comment, line, #text) + end + local cs, ce = text:find(start_comment, s, true) + if ce then + doc():remove(line, cs, line, ce + 1) + end + elseif s then + doc():insert(line, start_offset, start_comment) + if end_comment then + doc():insert(line, #doc().lines[line], " " .. comment[2]) + end + end + end + col1 = col1 + (col1 > start_offset and #start_comment or 0) * (uncomment and -1 or 1) + col2 = col2 + (col2 > start_offset and #start_comment or 0) * (uncomment and -1 or 1) + if end_comment and end_line then + col2 = col2 + #end_comment * (uncomment and -1 or 1) + end + return line1, col1, line2, col2 +end + +local function block_comment(comment, line1, col1, line2, col2) + -- automatically skip spaces + local word_start = doc():get_text(line1, col1, line1, math.huge):find("%S") + local word_end = doc():get_text(line2, 1, line2, col2):find("%s*$") + col1 = col1 + (word_start and (word_start - 1) or 0) + col2 = word_end and word_end or col2 + + local block_start = doc():get_text(line1, col1, line1, col1 + #comment[1]) + local block_end = doc():get_text(line2, col2 - #comment[2], line2, col2) + + if block_start == comment[1] and block_end == comment[2] then + -- remove up to 1 whitespace after the comment + local start_len, stop_len = #comment[1], #comment[2] + if doc():get_text(line1, col1 + #comment[1], line1, col1 + #comment[1] + 1):find("%s$") then + start_len = start_len + 1 + end + if doc():get_text(line2, col2 - #comment[2] - 1, line2, col2):find("^%s") then + stop_len = stop_len + 1 + end + + doc():remove(line1, col1, line1, col1 + start_len) + col2 = col2 - (line1 == line2 and start_len or 0) + doc():remove(line2, col2 - stop_len, line2, col2) + + return line1, col1, line2, col2 - stop_len + else + doc():insert(line1, col1, comment[1] .. " ") + col2 = col2 + (line1 == line2 and (#comment[1] + 1) or 0) + doc():insert(line2, col2, " " .. comment[2]) + + return line1, col1, line2, col2 + #comment[2] + 1 + end +end + +local commands = { + ["doc:select-none"] = function(dv) + local line, col = dv.doc:get_selection() + dv.doc:set_selection(line, col) + end, + ["doc:cut"] = function() cut_or_copy(true) end, @@ -93,219 +191,231 @@ local selection_commands = { cut_or_copy(false) end, - ["doc:select-none"] = function() - local line, col = doc():get_selection() - doc():set_selection(line, col) - end -} - -local commands = { - ["doc:undo"] = function() - doc():undo() + ["doc:undo"] = function(dv) + dv.doc:undo() end, - ["doc:redo"] = function() - doc():redo() + ["doc:redo"] = function(dv) + dv.doc:redo() end, - ["doc:paste"] = function() + ["doc:paste"] = function(dv) local clipboard = system.get_clipboard() -- If the clipboard has changed since our last look, use that instead - if doc().cursor_clipboard["full"] ~= clipboard then - doc().cursor_clipboard = {} + local external_paste = core.cursor_clipboard["full"] ~= clipboard + if external_paste then + core.cursor_clipboard = {} + core.cursor_clipboard_whole_line = {} end - for idx, line1, col1, line2, col2 in doc():get_selections() do - local value = doc().cursor_clipboard[idx] or clipboard - doc():text_input(value:gsub("\r", ""), idx) + local value, whole_line + for idx, line1, col1, line2, col2 in dv.doc:get_selections() do + if #core.cursor_clipboard_whole_line == (#dv.doc.selections/4) then + value = core.cursor_clipboard[idx] + whole_line = core.cursor_clipboard_whole_line[idx] == true + else + value = clipboard + whole_line = not external_paste and clipboard:find("\n") ~= nil + end + if whole_line then + dv.doc:insert(line1, 1, value:gsub("\r", "")) + if col1 == 1 then + dv.doc:move_to_cursor(idx, #value) + end + else + dv.doc:text_input(value:gsub("\r", ""), idx) + end end end, - ["doc:newline"] = function() - for idx, line, col in doc():get_selections(false, true) do - local indent = doc().lines[line]:match("^[\t ]*") + ["doc:newline"] = function(dv) + for idx, line, col in dv.doc:get_selections(false, true) do + local indent = dv.doc.lines[line]:match("^[\t ]*") if col <= #indent then indent = indent:sub(#indent + 2 - col) end - doc():text_input("\n" .. indent, idx) - end - end, - - ["doc:newline-below"] = function() - for idx, line in doc():get_selections(false, true) do - local indent = doc().lines[line]:match("^[\t ]*") - doc():insert(line, math.huge, "\n" .. indent) - doc():set_selections(idx, line + 1, math.huge) - end - end, - - ["doc:newline-above"] = function() - for idx, line in doc():get_selections(false, true) do - local indent = doc().lines[line]:match("^[\t ]*") - doc():insert(line, 1, indent .. "\n") - doc():set_selections(idx, line, math.huge) - end - end, - - ["doc:delete"] = function() - for idx, line1, col1, line2, col2 in doc():get_selections() do - if line1 == line2 and col1 == col2 and doc().lines[line1]:find("^%s*$", col1) then - doc():remove(line1, col1, line1, math.huge) + -- Remove current line if it contains only whitespace + if dv.doc.lines[line]:match("^%s+$") then + dv.doc:remove(line, 1, line, math.huge) end - doc():delete_to_cursor(idx, translate.next_char) + dv.doc:text_input("\n" .. indent, idx) end end, - ["doc:backspace"] = function() - local _, indent_size = doc():get_indent_info() - for idx, line1, col1, line2, col2 in doc():get_selections() do + ["doc:newline-below"] = function(dv) + for idx, line in dv.doc:get_selections(false, true) do + local indent = dv.doc.lines[line]:match("^[\t ]*") + dv.doc:insert(line, math.huge, "\n" .. indent) + dv.doc:set_selections(idx, line + 1, math.huge) + end + end, + + ["doc:newline-above"] = function(dv) + for idx, line in dv.doc:get_selections(false, true) do + local indent = dv.doc.lines[line]:match("^[\t ]*") + dv.doc:insert(line, 1, indent .. "\n") + dv.doc:set_selections(idx, line, math.huge) + end + end, + + ["doc:delete"] = function(dv) + for idx, line1, col1, line2, col2 in dv.doc:get_selections() do + if line1 == line2 and col1 == col2 and dv.doc.lines[line1]:find("^%s*$", col1) then + dv.doc:remove(line1, col1, line1, math.huge) + end + dv.doc:delete_to_cursor(idx, translate.next_char) + end + end, + + ["doc:backspace"] = function(dv) + local _, indent_size = dv.doc:get_indent_info() + for idx, line1, col1, line2, col2 in dv.doc:get_selections() do if line1 == line2 and col1 == col2 then - local text = doc():get_text(line1, 1, line1, col1) + local text = dv.doc:get_text(line1, 1, line1, col1) if #text >= indent_size and text:find("^ *$") then - doc():delete_to_cursor(idx, 0, -indent_size) + dv.doc:delete_to_cursor(idx, 0, -indent_size) return end end - doc():delete_to_cursor(idx, translate.previous_char) + dv.doc:delete_to_cursor(idx, translate.previous_char) end end, - ["doc:select-all"] = function() - doc():set_selection(1, 1, math.huge, math.huge) + ["doc:select-all"] = function(dv) + dv.doc:set_selection(1, 1, math.huge, math.huge) + -- avoid triggering DocView:scroll_to_make_visible + dv.last_line1 = 1 + dv.last_col1 = 1 + dv.last_line2 = #dv.doc.lines + dv.last_col2 = #dv.doc.lines[#dv.doc.lines] end, - ["doc:select-lines"] = function() - for idx, line1, _, line2 in doc():get_selections(true) do + ["doc:select-lines"] = function(dv) + for idx, line1, _, line2 in dv.doc:get_selections(true) do append_line_if_last_line(line2) - doc():set_selections(idx, line1, 1, line2 + 1, 1) + dv.doc:set_selections(idx, line1, 1, line2 + 1, 1) end end, - ["doc:select-word"] = function() - for idx, line1, col1 in doc():get_selections(true) do - local line1, col1 = translate.start_of_word(doc(), line1, col1) - local line2, col2 = translate.end_of_word(doc(), line1, col1) - doc():set_selections(idx, line2, col2, line1, col1) + ["doc:select-word"] = function(dv) + for idx, line1, col1 in dv.doc:get_selections(true) do + local line1, col1 = translate.start_of_word(dv.doc, line1, col1) + local line2, col2 = translate.end_of_word(dv.doc, line1, col1) + dv.doc:set_selections(idx, line2, col2, line1, col1) end end, - ["doc:join-lines"] = function() - for idx, line1, col1, line2, col2 in doc():get_selections(true) do + ["doc:join-lines"] = function(dv) + for idx, line1, col1, line2, col2 in dv.doc:get_selections(true) do if line1 == line2 then line2 = line2 + 1 end - local text = doc():get_text(line1, 1, line2, math.huge) + local text = dv.doc:get_text(line1, 1, line2, math.huge) text = text:gsub("(.-)\n[\t ]*", function(x) return x:find("^%s*$") and x or x .. " " end) - doc():insert(line1, 1, text) - doc():remove(line1, #text + 1, line2, math.huge) + dv.doc:insert(line1, 1, text) + dv.doc:remove(line1, #text + 1, line2, math.huge) if line1 ~= line2 or col1 ~= col2 then - doc():set_selections(idx, line1, math.huge) + dv.doc:set_selections(idx, line1, math.huge) end end end, - ["doc:indent"] = function() + ["doc:indent"] = function(dv) for idx, line1, col1, line2, col2 in doc_multiline_selections(true) do - local l1, c1, l2, c2 = doc():indent_text(false, line1, col1, line2, col2) + local l1, c1, l2, c2 = dv.doc:indent_text(false, line1, col1, line2, col2) if l1 then - doc():set_selections(idx, l1, c1, l2, c2) + dv.doc:set_selections(idx, l1, c1, l2, c2) end end end, - ["doc:unindent"] = function() + ["doc:unindent"] = function(dv) for idx, line1, col1, line2, col2 in doc_multiline_selections(true) do - local l1, c1, l2, c2 = doc():indent_text(true, line1, col1, line2, col2) + local l1, c1, l2, c2 = dv.doc:indent_text(true, line1, col1, line2, col2) if l1 then - doc():set_selections(idx, l1, c1, l2, c2) + dv.doc:set_selections(idx, l1, c1, l2, c2) end end end, - ["doc:duplicate-lines"] = function() + ["doc:duplicate-lines"] = function(dv) for idx, line1, col1, line2, col2 in doc_multiline_selections(true) do append_line_if_last_line(line2) local text = doc():get_text(line1, 1, line2 + 1, 1) - doc():insert(line2 + 1, 1, text) + dv.doc:insert(line2 + 1, 1, text) local n = line2 - line1 + 1 - doc():set_selections(idx, line1 + n, col1, line2 + n, col2) + dv.doc:set_selections(idx, line1 + n, col1, line2 + n, col2) end end, - ["doc:delete-lines"] = function() + ["doc:delete-lines"] = function(dv) for idx, line1, col1, line2, col2 in doc_multiline_selections(true) do append_line_if_last_line(line2) - doc():remove(line1, 1, line2 + 1, 1) - doc():set_selections(idx, line1, col1) + dv.doc:remove(line1, 1, line2 + 1, 1) + dv.doc:set_selections(idx, line1, col1) end end, - ["doc:move-lines-up"] = function() + ["doc:move-lines-up"] = function(dv) for idx, line1, col1, line2, col2 in doc_multiline_selections(true) do append_line_if_last_line(line2) if line1 > 1 then local text = doc().lines[line1 - 1] - doc():insert(line2 + 1, 1, text) - doc():remove(line1 - 1, 1, line1, 1) - doc():set_selections(idx, line1 - 1, col1, line2 - 1, col2) + dv.doc:insert(line2 + 1, 1, text) + dv.doc:remove(line1 - 1, 1, line1, 1) + dv.doc:set_selections(idx, line1 - 1, col1, line2 - 1, col2) end end end, - ["doc:move-lines-down"] = function() + ["doc:move-lines-down"] = function(dv) for idx, line1, col1, line2, col2 in doc_multiline_selections(true) do append_line_if_last_line(line2 + 1) - if line2 < #doc().lines then - local text = doc().lines[line2 + 1] - doc():remove(line2 + 1, 1, line2 + 2, 1) - doc():insert(line1, 1, text) - doc():set_selections(idx, line1 + 1, col1, line2 + 1, col2) + if line2 < #dv.doc.lines then + local text = dv.doc.lines[line2 + 1] + dv.doc:remove(line2 + 1, 1, line2 + 2, 1) + dv.doc:insert(line1, 1, text) + dv.doc:set_selections(idx, line1 + 1, col1, line2 + 1, col2) end end end, - ["doc:toggle-line-comments"] = function() - local comment = doc().syntax.comment - if not comment then return end - local indentation = doc():get_indent_string() - local comment_text = comment .. " " - for idx, line1, _, line2 in doc_multiline_selections(true) do - local uncomment = true - local start_offset = math.huge - for line = line1, line2 do - local text = doc().lines[line] - local s = text:find("%S") - local cs, ce = text:find(comment_text, s, true) - if s and cs ~= s then - uncomment = false - start_offset = math.min(start_offset, s) - end + ["doc:toggle-block-comments"] = function(dv) + local comment = dv.doc.syntax.block_comment + if not comment then + if dv.doc.syntax.comment then + command.perform "doc:toggle-line-comments" end - for line = line1, line2 do - local text = doc().lines[line] - local s = text:find("%S") - if uncomment then - local cs, ce = text:find(comment_text, s, true) - if ce then - doc():remove(line, cs, line, ce + 1) - end - elseif s then - doc():insert(line, start_offset, comment_text) - end + return + end + + for idx, line1, col1, line2, col2 in doc_multiline_selections(true) do + -- if nothing is selected, toggle the whole line + if line1 == line2 and col1 == col2 then + col1 = 1 + col2 = #dv.doc.lines[line2] + end + dv.doc:set_selections(idx, block_comment(comment, line1, col1, line2, col2)) + end + end, + + ["doc:toggle-line-comments"] = function(dv) + local comment = dv.doc.syntax.comment or dv.doc.syntax.block_comment + if comment then + for idx, line1, col1, line2, col2 in doc_multiline_selections(true) do + dv.doc:set_selections(idx, line_comment(comment, line1, col1, line2, col2)) end end end, - ["doc:upper-case"] = function() - doc():replace(string.upper) + ["doc:upper-case"] = function(dv) + dv.doc:replace(string.uupper) end, - ["doc:lower-case"] = function() - doc():replace(string.lower) + ["doc:lower-case"] = function(dv) + dv.doc:replace(string.ulower) end, - ["doc:go-to-line"] = function() - local dv = dv() - + ["doc:go-to-line"] = function(dv) local items local function init_items() if items then return end @@ -317,165 +427,195 @@ local commands = { end end - core.command_view:enter("Go To Line", function(text, item) - local line = item and item.line or tonumber(text) - if not line then - core.error("Invalid line number or unmatched string") - return + core.command_view:enter("Go To Line", { + submit = function(text, item) + local line = item and item.line or tonumber(text) + if not line then + core.error("Invalid line number or unmatched string") + return + end + dv.doc:set_selection(line, 1 ) + dv:scroll_to_line(line, true) + end, + suggest = function(text) + if not text:find("^%d*$") then + init_items() + return common.fuzzy_match(items, text) + end end - dv.doc:set_selection(line, 1 ) - dv:scroll_to_line(line, true) - - end, function(text) - if not text:find("^%d*$") then - init_items() - return common.fuzzy_match(items, text) - end - end) + }) end, - ["doc:toggle-line-ending"] = function() - doc().crlf = not doc().crlf + ["doc:toggle-line-ending"] = function(dv) + dv.doc.crlf = not dv.doc.crlf end, - ["doc:save-as"] = function() + ["doc:save-as"] = function(dv) local last_doc = core.last_active_view and core.last_active_view.doc - if doc().filename then - core.command_view:set_text(doc().filename) + local text + if dv.doc.filename then + text = dv.doc.filename elseif last_doc and last_doc.filename then local dirname, filename = core.last_active_view.doc.abs_filename:match("(.*)[/\\](.+)$") - core.command_view:set_text(core.normalize_to_project_dir(dirname) .. PATHSEP) + text = core.normalize_to_project_dir(dirname) .. PATHSEP end - core.command_view:enter("Save As", function(filename) - save(common.home_expand(filename)) - end, function (text) - return common.home_encode_list(common.path_suggest(common.home_expand(text))) - end) + core.command_view:enter("Save As", { + text = text, + submit = function(filename) + save(common.home_expand(filename)) + end, + suggest = function (text) + return common.home_encode_list(common.path_suggest(common.home_expand(text))) + end + }) end, - ["doc:save"] = function() - if doc().filename then + ["doc:save"] = function(dv) + if dv.doc.filename then save() else command.perform("doc:save-as") end end, - ["file:rename"] = function() - local old_filename = doc().filename + ["doc:reload"] = function(dv) + dv.doc:reload() + end, + + ["file:rename"] = function(dv) + local old_filename = dv.doc.filename if not old_filename then core.error("Cannot rename unsaved doc") return end - core.command_view:set_text(old_filename) - core.command_view:enter("Rename", function(filename) - save(common.home_expand(filename)) - core.log("Renamed \"%s\" to \"%s\"", old_filename, filename) - if filename ~= old_filename then - os.remove(old_filename) + core.command_view:enter("Rename", { + text = old_filename, + submit = function(filename) + save(common.home_expand(filename)) + core.log("Renamed \"%s\" to \"%s\"", old_filename, filename) + if filename ~= old_filename then + os.remove(old_filename) + end + end, + suggest = function (text) + return common.home_encode_list(common.path_suggest(common.home_expand(text))) end - end, function (text) - return common.home_encode_list(common.path_suggest(common.home_expand(text))) - end) + }) end, - ["file:delete"] = function() - local filename = doc().abs_filename + ["file:delete"] = function(dv) + local filename = dv.doc.abs_filename if not filename then core.error("Cannot remove unsaved doc") return end - for i,docview in ipairs(core.get_views_referencing_doc(doc())) do + for i,docview in ipairs(core.get_views_referencing_doc(dv.doc)) do local node = core.root_view.root_node:get_node_for_view(docview) - node:close_view(core.root_view, docview) + node:close_view(core.root_view.root_node, docview) end os.remove(filename) core.log("Removed \"%s\"", filename) end, - - ["doc:select-to-cursor"] = function(x, y, clicks) + + ["doc:select-to-cursor"] = function(dv, x, y, clicks) local line1, col1 = select(3, doc():get_selection()) - local line2, col2 = dv():resolve_screen_position(x, y) - dv().mouse_selecting = { line1, col1, nil } - doc():set_selection(line2, col2, line1, col1) - end, - - ["doc:set-cursor"] = function(x, y) - set_cursor(x, y, "set") - end, - - ["doc:set-cursor-word"] = function(x, y) - set_cursor(x, y, "word") - end, - - ["doc:set-cursor-line"] = function(x, y, clicks) - set_cursor(x, y, "lines") - end, - - ["doc:split-cursor"] = function(x, y, clicks) - local line, col = dv():resolve_screen_position(x, y) - doc():add_selection(line, col, line, col) + local line2, col2 = dv:resolve_screen_position(x, y) + dv.mouse_selecting = { line1, col1, nil } + dv.doc:set_selection(line2, col2, line1, col1) end, - ["doc:create-cursor-previous-line"] = function() + ["doc:create-cursor-previous-line"] = function(dv) split_cursor(-1) - doc():merge_cursors() + dv.doc:merge_cursors() end, - ["doc:create-cursor-next-line"] = function() + ["doc:create-cursor-next-line"] = function(dv) split_cursor(1) - doc():merge_cursors() + dv.doc:merge_cursors() end } +command.add(function(x, y) + if x == nil or y == nil or not core.active_view:is(DocView) then return false end + local dv = core.active_view + local x1,y1,x2,y2 = dv.position.x, dv.position.y, dv.position.x + dv.size.x, dv.position.y + dv.size.y + return x >= x1 + dv:get_gutter_width() and x < x2 and y >= y1 and y < y2, dv, x, y +end, { + ["doc:set-cursor"] = function(dv, x, y) + set_cursor(dv, x, y, "set") + end, + + ["doc:set-cursor-word"] = function(dv, x, y) + set_cursor(dv, x, y, "word") + end, + + ["doc:set-cursor-line"] = function(dv, x, y, clicks) + set_cursor(dv, x, y, "lines") + end, + + ["doc:split-cursor"] = function(dv, x, y, clicks) + local line, col = dv:resolve_screen_position(x, y) + local removal_target = nil + for idx, line1, col1 in dv.doc:get_selections(true) do + if line1 == line and col1 == col and #doc().selections > 4 then + removal_target = idx + end + end + if removal_target then + dv.doc:remove_selection(removal_target) + else + dv.doc:add_selection(line, col, line, col) + end + dv.mouse_selecting = { line, col, "set" } + end +}) local translations = { - ["previous-char"] = translate.previous_char, - ["next-char"] = translate.next_char, - ["previous-word-start"] = translate.previous_word_start, - ["next-word-end"] = translate.next_word_end, - ["previous-block-start"] = translate.previous_block_start, - ["next-block-end"] = translate.next_block_end, - ["start-of-doc"] = translate.start_of_doc, - ["end-of-doc"] = translate.end_of_doc, - ["start-of-line"] = translate.start_of_line, - ["end-of-line"] = translate.end_of_line, - ["start-of-word"] = translate.start_of_word, - ["start-of-indentation"] = translate.start_of_indentation, - ["end-of-word"] = translate.end_of_word, - ["previous-line"] = DocView.translate.previous_line, - ["next-line"] = DocView.translate.next_line, - ["previous-page"] = DocView.translate.previous_page, - ["next-page"] = DocView.translate.next_page, + ["previous-char"] = translate, + ["next-char"] = translate, + ["previous-word-start"] = translate, + ["next-word-end"] = translate, + ["previous-block-start"] = translate, + ["next-block-end"] = translate, + ["start-of-doc"] = translate, + ["end-of-doc"] = translate, + ["start-of-line"] = translate, + ["end-of-line"] = translate, + ["start-of-word"] = translate, + ["start-of-indentation"] = translate, + ["end-of-word"] = translate, + ["previous-line"] = DocView.translate, + ["next-line"] = DocView.translate, + ["previous-page"] = DocView.translate, + ["next-page"] = DocView.translate, } -for name, fn in pairs(translations) do - commands["doc:move-to-" .. name] = function() doc():move_to(fn, dv()) end - commands["doc:select-to-" .. name] = function() doc():select_to(fn, dv()) end - commands["doc:delete-to-" .. name] = function() doc():delete_to(fn, dv()) end +for name, obj in pairs(translations) do + commands["doc:move-to-" .. name] = function(dv) dv.doc:move_to(obj[name:gsub("-", "_")], dv) end + commands["doc:select-to-" .. name] = function(dv) dv.doc:select_to(obj[name:gsub("-", "_")], dv) end + commands["doc:delete-to-" .. name] = function(dv) dv.doc:delete_to(obj[name:gsub("-", "_")], dv) end end -commands["doc:move-to-previous-char"] = function() - for idx, line1, col1, line2, col2 in doc():get_selections(true) do +commands["doc:move-to-previous-char"] = function(dv) + for idx, line1, col1, line2, col2 in dv.doc:get_selections(true) do if line1 ~= line2 or col1 ~= col2 then - doc():set_selections(idx, line1, col1) + dv.doc:set_selections(idx, line1, col1) + else + dv.doc:move_to_cursor(idx, translate.previous_char) end end - doc():move_to(translate.previous_char) end -commands["doc:move-to-next-char"] = function() - for idx, line1, col1, line2, col2 in doc():get_selections(true) do +commands["doc:move-to-next-char"] = function(dv) + for idx, line1, col1, line2, col2 in dv.doc:get_selections(true) do if line1 ~= line2 or col1 ~= col2 then - doc():set_selections(idx, line2, col2) + dv.doc:set_selections(idx, line2, col2) + else + dv.doc:move_to_cursor(idx, translate.next_char) end end - doc():move_to(translate.next_char) end command.add("core.docview", commands) -command.add(function() - return core.active_view:is(DocView) and core.active_view.doc:has_any_selection() -end ,selection_commands) diff --git a/data/core/commands/files.lua b/data/core/commands/files.lua index b2fdb336..a13db5df 100644 --- a/data/core/commands/files.lua +++ b/data/core/commands/files.lua @@ -4,11 +4,13 @@ local common = require "core.common" command.add(nil, { ["files:create-directory"] = function() - core.command_view:enter("New directory name", function(text) - local success, err, path = common.mkdirp(text) - if not success then - core.error("cannot create directory %q: %s", path, err) + core.command_view:enter("New directory name", { + submit = function(text) + local success, err, path = common.mkdirp(text) + if not success then + core.error("cannot create directory %q: %s", path, err) + end end - end) + }) end, }) diff --git a/data/core/commands/findreplace.lua b/data/core/commands/findreplace.lua index db6a2dd6..3966dbf4 100644 --- a/data/core/commands/findreplace.lua +++ b/data/core/commands/findreplace.lua @@ -46,9 +46,12 @@ end local function insert_unique(t, v) local n = #t for i = 1, n do - if t[i] == v then return end + if t[i] == v then + table.remove(t, i) + break + end end - t[n + 1] = v + table.insert(t, 1, v) end @@ -58,58 +61,76 @@ local function find(label, search_fn) local text = last_view.doc:get_text(table.unpack(last_sel)) found_expression = false - core.command_view:set_text(text, true) core.status_view:show_tooltip(get_find_tooltip()) - core.command_view:set_hidden_suggestions() - core.command_view:enter(label, function(text, item) - insert_unique(core.previous_find, text) - core.status_view:remove_tooltip() - if found_expression then + core.command_view:enter(label, { + text = text, + select_text = true, + show_suggestions = false, + submit = function(text, item) + insert_unique(core.previous_find, text) + core.status_view:remove_tooltip() + if found_expression then + last_fn, last_text = search_fn, text + else + core.error("Couldn't find %q", text) + last_view.doc:set_selection(table.unpack(last_sel)) + last_view:scroll_to_make_visible(table.unpack(last_sel)) + end + end, + suggest = function(text) + update_preview(last_sel, search_fn, text) last_fn, last_text = search_fn, text - else - core.error("Couldn't find %q", text) - last_view.doc:set_selection(table.unpack(last_sel)) - last_view:scroll_to_make_visible(table.unpack(last_sel)) + return core.previous_find + end, + cancel = function(explicit) + core.status_view:remove_tooltip() + if explicit then + last_view.doc:set_selection(table.unpack(last_sel)) + last_view:scroll_to_make_visible(table.unpack(last_sel)) + end end - end, function(text) - update_preview(last_sel, search_fn, text) - last_fn, last_text = search_fn, text - return core.previous_find - end, function(explicit) - core.status_view:remove_tooltip() - if explicit then - last_view.doc:set_selection(table.unpack(last_sel)) - last_view:scroll_to_make_visible(table.unpack(last_sel)) - end - end) + }) end local function replace(kind, default, fn) - core.command_view:set_text(default, true) - core.status_view:show_tooltip(get_find_tooltip()) - core.command_view:set_hidden_suggestions() - core.command_view:enter("Find To Replace " .. kind, function(old) - insert_unique(core.previous_find, old) - core.command_view:set_text(old, true) + core.command_view:enter("Find To Replace " .. kind, { + text = default, + select_text = true, + show_suggestions = false, + submit = function(old) + insert_unique(core.previous_find, old) - local s = string.format("Replace %s %q With", kind, old) - core.command_view:set_hidden_suggestions() - core.command_view:enter(s, function(new) + local s = string.format("Replace %s %q With", kind, old) + core.command_view:enter(s, { + text = old, + select_text = true, + show_suggestions = false, + submit = function(new) + core.status_view:remove_tooltip() + insert_unique(core.previous_replace, new) + local results = doc():replace(function(text) + return fn(text, old, new) + end) + local n = 0 + for _,v in pairs(results) do + n = n + v + end + core.log("Replaced %d instance(s) of %s %q with %q", n, kind, old, new) + end, + suggest = function() return core.previous_replace end, + cancel = function() + core.status_view:remove_tooltip() + end + }) + end, + suggest = function() return core.previous_find end, + cancel = function() core.status_view:remove_tooltip() - insert_unique(core.previous_replace, new) - local n = doc():replace(function(text) - return fn(text, old, new) - end) - core.log("Replaced %d instance(s) of %s %q with %q", n, kind, old, new) - end, function() return core.previous_replace end, function() - core.status_view:remove_tooltip() - end) - end, function() return core.previous_find end, function() - core.status_view:remove_tooltip() - end) + end + }) end local function has_selection() @@ -179,7 +200,7 @@ command.add(has_unique_selection, { ["find-replace:select-add-all"] = function() select_add_next(true) end }) -command.add("core.docview", { +command.add("core.docview!", { ["find-replace:find"] = function() find("Find Text", function(doc, line, col, text, case_sensitive, find_regex, find_reverse) local opt = { wrap = true, no_case = not case_sensitive, regex = find_regex, reverse = find_reverse } diff --git a/data/core/commands/log.lua b/data/core/commands/log.lua new file mode 100644 index 00000000..8a5b7f3d --- /dev/null +++ b/data/core/commands/log.lua @@ -0,0 +1,16 @@ +local core = require "core" +local command = require "core.command" + + +command.add(nil, { + ["log:open-as-doc"] = function() + local doc = core.open_doc("logs.txt") + core.root_view:open_doc(doc) + doc:insert(1, 1, core.get_log()) + doc.new_file = false + doc:clean() + end, + ["log:copy-to-clipboard"] = function() + system.set_clipboard(core.get_log()) + end +}) diff --git a/data/core/commands/root.lua b/data/core/commands/root.lua index 5bf18390..deea858e 100644 --- a/data/core/commands/root.lua +++ b/data/core/commands/root.lua @@ -7,13 +7,11 @@ local config = require "core.config" local t = { - ["root:close"] = function() - local node = core.root_view:get_active_node() + ["root:close"] = function(node) node:close_active_view(core.root_view.root_node) end, - ["root:close-or-quit"] = function() - local node = core.root_view:get_active_node() + ["root:close-or-quit"] = function(node) if node and (not node:is_empty() or not node.is_primary_node) then node:close_active_view(core.root_view.root_node) else @@ -30,25 +28,22 @@ local t = { for i, v in ipairs(core.docs) do if v ~= active_doc then table.insert(docs, v) end end core.confirm_close_docs(docs, core.root_view.close_all_docviews, core.root_view, true) end, - - ["root:switch-to-previous-tab"] = function() - local node = core.root_view:get_active_node() + + ["root:switch-to-previous-tab"] = function(node) local idx = node:get_view_idx(core.active_view) idx = idx - 1 if idx < 1 then idx = #node.views end node:set_active_view(node.views[idx]) end, - ["root:switch-to-next-tab"] = function() - local node = core.root_view:get_active_node() + ["root:switch-to-next-tab"] = function(node) local idx = node:get_view_idx(core.active_view) idx = idx + 1 if idx > #node.views then idx = 1 end node:set_active_view(node.views[idx]) end, - ["root:move-tab-left"] = function() - local node = core.root_view:get_active_node() + ["root:move-tab-left"] = function(node) local idx = node:get_view_idx(core.active_view) if idx > 1 then table.remove(node.views, idx) @@ -56,24 +51,21 @@ local t = { end end, - ["root:move-tab-right"] = function() - local node = core.root_view:get_active_node() + ["root:move-tab-right"] = function(node) local idx = node:get_view_idx(core.active_view) if idx < #node.views then table.remove(node.views, idx) table.insert(node.views, idx + 1, core.active_view) end end, - - ["root:shrink"] = function() - local node = core.root_view:get_active_node() + + ["root:shrink"] = function(node) local parent = node:get_parent_node(core.root_view.root_node) local n = (parent.a == node) and -0.1 or 0.1 parent.divider = common.clamp(parent.divider + n, 0.1, 0.9) end, - ["root:grow"] = function() - local node = core.root_view:get_active_node() + ["root:grow"] = function(node) local parent = node:get_parent_node(core.root_view.root_node) local n = (parent.a == node) and 0.1 or -0.1 parent.divider = common.clamp(parent.divider + n, 0.1, 0.9) @@ -82,8 +74,7 @@ local t = { for i = 1, 9 do - t["root:switch-to-tab-" .. i] = function() - local node = core.root_view:get_active_node() + t["root:switch-to-tab-" .. i] = function(node) local view = node.views[i] if view then node:set_active_view(view) @@ -93,8 +84,7 @@ end for _, dir in ipairs { "left", "right", "up", "down" } do - t["root:split-" .. dir] = function() - local node = core.root_view:get_active_node() + t["root:split-" .. dir] = function(node) local av = node.active_view node:split(dir) if av:is(DocView) then @@ -102,8 +92,7 @@ for _, dir in ipairs { "left", "right", "up", "down" } do end end - t["root:switch-to-" .. dir] = function() - local node = core.root_view:get_active_node() + t["root:switch-to-" .. dir] = function(node) local x, y if dir == "left" or dir == "right" then y = node.position.y + node.size.y / 2 @@ -123,7 +112,7 @@ end command.add(function() local node = core.root_view:get_active_node() local sx, sy = node:get_locked_size() - return not sx and not sy + return not sx and not sy, node end, t) command.add(nil, { diff --git a/data/core/commands/statusbar.lua b/data/core/commands/statusbar.lua new file mode 100644 index 00000000..5676ef52 --- /dev/null +++ b/data/core/commands/statusbar.lua @@ -0,0 +1,71 @@ +local core = require "core" +local command = require "core.command" +local common = require "core.common" +local style = require "core.style" +local StatusView = require "core.statusview" + +local function status_view_item_names() + local items = core.status_view:get_items_list() + local names = {} + for _, item in ipairs(items) do + table.insert(names, item.name) + end + return names +end + +local function status_view_items_data(names) + local data = {} + for _, name in ipairs(names) do + local item = core.status_view:get_item(name) + table.insert(data, { + text = command.prettify_name(item.name), + info = item.alignment == StatusView.Item.LEFT and "Left" or "Right", + name = item.name + }) + end + return data +end + +local function status_view_get_items(text) + local names = status_view_item_names() + local results = common.fuzzy_match(names, text) + results = status_view_items_data(results) + return results +end + +command.add(nil, { + ["status-bar:toggle"] = function() + core.status_view:toggle() + end, + ["status-bar:show"] = function() + core.status_view:show() + end, + ["status-bar:hide"] = function() + core.status_view:hide() + end, + ["status-bar:disable-messages"] = function() + core.status_view:display_messages(false) + end, + ["status-bar:enable-messages"] = function() + core.status_view:display_messages(true) + end, + ["status-bar:hide-item"] = function() + core.command_view:enter("Status bar item to hide", { + submit = function(text, item) + core.status_view:hide_items(item.name) + end, + suggest = status_view_get_items + }) + end, + ["status-bar:show-item"] = function() + core.command_view:enter("Status bar item to show", { + submit = function(text, item) + core.status_view:show_items(item.name) + end, + suggest = status_view_get_items + }) + end, + ["status-bar:reset-items"] = function() + core.status_view:show_items() + end, +}) diff --git a/data/core/commandview.lua b/data/core/commandview.lua index b91f1394..a77db961 100644 --- a/data/core/commandview.lua +++ b/data/core/commandview.lua @@ -6,13 +6,16 @@ local DocView = require "core.docview" local View = require "core.view" +---@class core.commandview.input : core.doc +---@field super core.doc local SingleLineDoc = Doc:extend() function SingleLineDoc:insert(line, col, text) SingleLineDoc.super.insert(self, line, col, text:gsub("\n", "")) end - +---@class core.commandview : core.docview +---@field super core.docview local CommandView = DocView:extend() CommandView.context = "application" @@ -21,11 +24,26 @@ local max_suggestions = 10 local noop = function() end +---@class core.commandview.state +---@field submit function +---@field suggest function +---@field cancel function +---@field validate function +---@field text string +---@field select_text boolean +---@field show_suggestions boolean +---@field typeahead boolean +---@field wrap boolean local default_state = { submit = noop, suggest = noop, cancel = noop, - validate = function() return true end + validate = function() return true end, + text = "", + select_text = false, + show_suggestions = true, + typeahead = true, + wrap = true, } @@ -34,8 +52,8 @@ function CommandView:new() self.suggestion_idx = 1 self.suggestions = {} self.suggestions_height = 0 - self.show_suggestions = true self.last_change_id = 0 + self.last_text = "" self.gutter_width = 0 self.gutter_text_brightness = 0 self.selection_offset = 0 @@ -46,8 +64,10 @@ function CommandView:new() end +---@deprecated function CommandView:set_hidden_suggestions() - self.show_suggestions = false + core.warn("Using deprecated function CommandView:set_hidden_suggestions") + self.state.show_suggestions = false end @@ -56,8 +76,8 @@ function CommandView:get_name() end -function CommandView:get_line_screen_position() - local x = CommandView.super.get_line_screen_position(self, 1) +function CommandView:get_line_screen_position(line, col) + local x = CommandView.super.get_line_screen_position(self, 1, col) local _, y = self:get_content_offset() local lh = self:get_line_height() return x, y + (self.size.y - lh) / 2 @@ -80,6 +100,7 @@ end function CommandView:set_text(text, select) + self.last_text = text self.doc:remove(1, 1, math.huge, math.huge) self.doc:text_input(text) if select then @@ -89,9 +110,18 @@ end function CommandView:move_suggestion_idx(dir) - if self.show_suggestions then + local function overflow_suggestion_idx(n, count) + if count == 0 then return 0 end + if self.state.wrap then + return (n - 1) % count + 1 + else + return common.clamp(n, 1, count) + end + end + + if self.state.show_suggestions then local n = self.suggestion_idx + dir - self.suggestion_idx = common.clamp(n, 1, #self.suggestions) + self.suggestion_idx = overflow_suggestion_idx(n, #self.suggestions) self:complete() self.last_change_id = self.doc:get_change_id() else @@ -102,7 +132,7 @@ function CommandView:move_suggestion_idx(dir) if n == 0 and self.save_suggestion then self:set_text(self.save_suggestion) else - self.suggestion_idx = common.clamp(n, 1, #self.suggestions) + self.suggestion_idx = overflow_suggestion_idx(n, #self.suggestions) self:complete() end else @@ -132,21 +162,53 @@ function CommandView:submit() end end - -function CommandView:enter(text, submit, suggest, cancel, validate) +---@param label string +---@varargs any +---@overload fun(label:string, options: core.commandview.state) +function CommandView:enter(label, ...) if self.state ~= default_state then return end - self.state = { - submit = submit or noop, - suggest = suggest or noop, - cancel = cancel or noop, - validate = validate or function() return true end - } + local options = select(1, ...) + + if type(options) ~= "table" then + core.warn("Using CommandView:enter in a deprecated way") + local submit, suggest, cancel, validate = ... + options = { + submit = submit, + suggest = suggest, + cancel = cancel, + validate = validate, + } + end + + -- Support deprecated CommandView:set_hidden_suggestions + -- Remove this when set_hidden_suggestions is not supported anymore + if options.show_suggestions == nil then + options.show_suggestions = self.state.show_suggestions + end + + self.state = common.merge(default_state, options) + + -- We need to keep the text entered with CommandView:set_text to + -- maintain compatibility with deprecated usage, but still allow + -- overwriting with options.text + local old_text = self:get_text() + if old_text ~= "" then + core.warn("Using deprecated function CommandView:set_text") + end + if options.text or options.select_text then + local text = options.text or old_text + self:set_text(text, self.state.select_text) + end + -- Replace with a simple + -- self:set_text(self.state.text, self.state.select_text) + -- once old usage is removed + core.set_active_view(self) self:update_suggestions() self.gutter_text_brightness = 100 - self.label = text .. ": " + self.label = label .. ": " end @@ -159,8 +221,13 @@ function CommandView:exit(submitted, inexplicit) self.doc:reset() self.suggestions = {} if not submitted then cancel(not inexplicit) end - self.show_suggestions = true self.save_suggestion = nil + self.last_text = "" +end + + +function CommandView:get_line_height() + return math.floor(self:get_font():get_height() * 1.2) end @@ -198,35 +265,45 @@ function CommandView:update() -- update suggestions if text has changed if self.last_change_id ~= self.doc:get_change_id() then self:update_suggestions() + if self.state.typeahead and self.suggestions[self.suggestion_idx] then + local current_text = self:get_text() + local suggested_text = self.suggestions[self.suggestion_idx].text or "" + if #self.last_text < #current_text and + string.find(suggested_text, current_text, 1, true) == 1 then + self:set_text(suggested_text) + self.doc:set_selection(1, #current_text + 1, 1, math.huge) + end + self.last_text = current_text + end self.last_change_id = self.doc:get_change_id() end -- update gutter text color brightness - self:move_towards("gutter_text_brightness", 0, 0.1) + self:move_towards("gutter_text_brightness", 0, 0.1, "commandview") -- update gutter width local dest = self:get_font():get_width(self.label) + style.padding.x if self.size.y <= 0 then self.gutter_width = dest else - self:move_towards("gutter_width", dest) + self:move_towards("gutter_width", dest, nil, "commandview") end -- update suggestions box height local lh = self:get_suggestion_line_height() - local dest = self.show_suggestions and math.min(#self.suggestions, max_suggestions) * lh or 0 - self:move_towards("suggestions_height", dest) + local dest = self.state.show_suggestions and math.min(#self.suggestions, max_suggestions) * lh or 0 + self:move_towards("suggestions_height", dest, nil, "commandview") -- update suggestion cursor offset local dest = math.min(self.suggestion_idx, max_suggestions) * self:get_suggestion_line_height() - self:move_towards("selection_offset", dest) + self:move_towards("selection_offset", dest, nil, "commandview") -- update size based on whether this is the active_view local dest = 0 if self == core.active_view then dest = style.font:get_height() + style.padding.y * 2 end - self:move_towards(self.size, "y", dest) + self:move_towards(self.size, "y", dest, nil, "commandview") end @@ -243,6 +320,7 @@ function CommandView:draw_line_gutter(idx, x, y) x = x + style.padding.x renderer.draw_text(self:get_font(), self.label, x, y + yoffset, color) core.pop_clip_rect() + return self:get_line_height() end @@ -262,20 +340,20 @@ local function draw_suggestions_box(self) end -- draw suggestion text - local suggestion_offset = math.max(self.suggestion_idx - max_suggestions, 0) + local offset = math.max(self.suggestion_idx - max_suggestions, 0) + local last = math.min(offset + max_suggestions, #self.suggestions) core.push_clip_rect(rx, ry, rw, rh) - local i = 1 + suggestion_offset - while i <= #self.suggestions do + local first = 1 + offset + for i=first, last do local item = self.suggestions[i] local color = (i == self.suggestion_idx) and style.accent or style.text - local y = self.position.y - (i - suggestion_offset) * lh - dh + local y = self.position.y - (i - offset) * lh - dh common.draw_text(self:get_font(), color, item.text, nil, x, y, 0, lh) if item.info then local w = self.size.x - x - style.padding.x common.draw_text(self:get_font(), style.dim, item.info, "right", x, y, w, lh) end - i = i + 1 end core.pop_clip_rect() end @@ -283,7 +361,7 @@ end function CommandView:draw() CommandView.super.draw(self) - if self.show_suggestions then + if self.state.show_suggestions then core.root_view:defer_draw(draw_suggestions_box, self) end end diff --git a/data/core/common.lua b/data/core/common.lua index 333538a8..f02cec55 100644 --- a/data/core/common.lua +++ b/data/core/common.lua @@ -16,6 +16,21 @@ function common.clamp(n, lo, hi) end +function common.merge(a, b) + a = type(a) == "table" and a or {} + local t = {} + for k, v in pairs(a) do + t[k] = v + end + if b and type(b) == "table" then + for k, v in pairs(b) do + t[k] = v + end + end + return t +end + + function common.round(n) return n >= 0 and math.floor(n + 0.5) or math.ceil(n - 0.5) end @@ -41,7 +56,7 @@ end function common.distance(x1, y1, x2, y2) - return math.sqrt(math.pow(x2-x1, 2)+math.pow(y2-y1, 2)) + return math.sqrt(((x2-x1) ^ 2)+((y2-y1) ^ 2)) end @@ -57,7 +72,7 @@ function common.color(str) r = (f() or 0) g = (f() or 0) b = (f() or 0) - a = (f() or 1) * 0xff + a = (f() or 1) * 0xff else error(string.format("bad color string '%s'", str)) end @@ -131,9 +146,29 @@ function common.fuzzy_match_with_recents(haystack, recents, needle) end -function common.path_suggest(text) +function common.path_suggest(text, root) + if root and root:sub(-1) ~= PATHSEP then + root = root .. PATHSEP + end local path, name = text:match("^(.-)([^:/\\]*)$") - local files = system.list_dir(path == "" and "." or path) or {} + local clean_dotslash = false + -- ignore root if path is absolute + local is_absolute = common.is_absolute_path(text) + if not is_absolute then + if path == "" then + path = root or "." + clean_dotslash = not root + else + path = (root or "") .. path + end + end + + -- Only in Windows allow using both styles of PATHSEP + if (PATHSEP == "\\" and not string.match(path:sub(-1), "[\\/]")) or + (PATHSEP ~= "\\" and path:sub(-1) ~= PATHSEP) then + path = path .. PATHSEP + end + local files = system.list_dir(path) or {} local res = {} for _, file in ipairs(files) do file = path .. file @@ -142,6 +177,19 @@ function common.path_suggest(text) if info.type == "dir" then file = file .. PATHSEP end + if root then + -- remove root part from file path + local s, e = file:find(root, nil, true) + if s == 1 then + file = file:sub(e + 1) + end + elseif clean_dotslash then + -- remove added dot slash + local s, e = file:find("." .. PATHSEP, nil, true) + if s == 1 then + file = file:sub(e + 1) + end + end if file:lower():find(text:lower(), nil, true) == 1 then table.insert(res, file) end @@ -213,19 +261,58 @@ function common.bench(name, fn, ...) end -function common.serialize(val) +local function serialize(val, pretty, indent_str, escape, sort, limit, level) + local space = pretty and " " or "" + local indent = pretty and string.rep(indent_str, level) or "" + local newline = pretty and "\n" or "" if type(val) == "string" then - return string.format("%q", val) + local out = string.format("%q", val) + if escape then + out = string.gsub(out, "\\\n", "\\n") + out = string.gsub(out, "\\7", "\\a") + out = string.gsub(out, "\\8", "\\b") + out = string.gsub(out, "\\9", "\\t") + out = string.gsub(out, "\\11", "\\v") + out = string.gsub(out, "\\12", "\\f") + out = string.gsub(out, "\\13", "\\r") + end + return out elseif type(val) == "table" then + -- early exit + if level >= limit then return tostring(val) end + local next_indent = pretty and (indent .. indent_str) or "" local t = {} for k, v in pairs(val) do - table.insert(t, "[" .. common.serialize(k) .. "]=" .. common.serialize(v)) + table.insert(t, + next_indent .. "[" .. + serialize(k, pretty, indent_str, escape, sort, limit, level + 1) .. + "]" .. space .. "=" .. space .. serialize(v, pretty, indent_str, escape, sort, limit, level + 1)) end - return "{" .. table.concat(t, ",") .. "}" + if #t == 0 then return "{}" end + if sort then table.sort(t) end + return "{" .. newline .. table.concat(t, "," .. newline) .. newline .. indent .. "}" end return tostring(val) end +-- Serialize `val` into a parsable string. +-- Available options +-- * pretty: enable pretty printing +-- * indent_str: indent to use (" " by default) +-- * escape: use normal escape characters instead of the ones used by string.format("%q", ...) +-- * sort: sort the keys inside tables +-- * limit: limit how deep to serialize +-- * initial_indent: the initial indentation level +function common.serialize(val, opts) + opts = opts or {} + local indent_str = opts.indent_str or " " + local initial_indent = opts.initial_indent or 0 + local indent = opts.pretty and string.rep(indent_str, initial_indent) or "" + local limit = (opts.limit or math.huge) + initial_indent + return indent .. serialize(val, opts.pretty, indent_str, + opts.escape, opts.sort, limit, initial_indent) +end + function common.basename(path) -- a path should never end by / or \ except if it is '/' (unix root) or @@ -287,11 +374,11 @@ end -- absolute path without . or .. elements. -- This function exists because on Windows the drive letter returned -- by system.absolute_path is sometimes with a lower case and sometimes --- with an upper case to we normalize to upper case. +-- with an upper case so we normalize to upper case. function common.normalize_volume(filename) if not filename then return end if PATHSEP == '\\' then - local drive, rem = filename:match('^([a-zA-Z]:\\)(.*)') + local drive, rem = filename:match('^([a-zA-Z]:\\)(.-)'..PATHSEP..'?$') if drive then return drive:upper() .. rem end @@ -340,6 +427,11 @@ function common.normalize_path(filename) end +function common.is_absolute_path(path) + return path:sub(1, 1) == PATHSEP or path:match("^(%a):\\") +end + + function common.path_belongs_to(filename, path) return string.find(filename, path .. PATHSEP, 1, true) == 1 end @@ -439,5 +531,6 @@ function common.rm(path, recursively) return true end + return common diff --git a/data/core/config.lua b/data/core/config.lua index 71e83994..efbe1f1b 100644 --- a/data/core/config.lua +++ b/data/core/config.lua @@ -1,26 +1,37 @@ local config = {} config.fps = 60 -config.max_log_items = 80 +config.max_log_items = 800 config.message_timeout = 5 config.mouse_wheel_scroll = 50 * SCALE +config.animate_drag_scroll = false config.scroll_past_end = true config.file_size_limit = 10 -config.ignore_files = "^%." +config.ignore_files = { "^%." } config.symbol_pattern = "[%a_][%w_]*" config.non_word_chars = " \t\n/\\()\"':,.;<>~!@#$%^&*|+=[]{}`?-" config.undo_merge_timeout = 0.3 config.max_undos = 10000 config.max_tabs = 8 config.always_show_tabs = true +-- Possible values: false, true, "no_selection" config.highlight_current_line = true config.line_height = 1.2 config.indent_size = 2 config.tab_type = "soft" config.line_limit = 80 -config.max_symbols = 4000 config.max_project_files = 2000 config.transitions = true +config.disabled_transitions = { + scroll = false, + commandview = false, + contextmenu = false, + logview = false, + nagbar = false, + tabs = false, + tab_drag = false, + statusbar = false, +} config.animation_rate = 1.0 config.blink_period = 0.8 config.disable_blink = false @@ -29,12 +40,20 @@ config.borderless = false config.tab_close_button = true config.max_clicks = 3 --- Disable plugin loading setting to false the config entry --- of the same name. -config.plugins = {} +-- set as true to be able to test non supported plugins +config.skip_plugins_version = false +config.plugins = {} +-- Allow you to set plugin configs even if we haven't seen the plugin before. +setmetatable(config.plugins, { + __index = function(t, k) + if rawget(t, k) == nil then rawset(t, k, {}) end + return rawget(t, k) + end +}) + +-- Disable these plugins by default. config.plugins.trimwhitespace = false -config.plugins.lineguide = false config.plugins.drawwhitespace = false return config diff --git a/data/core/contextmenu.lua b/data/core/contextmenu.lua index 9db35bb3..380fb634 100644 --- a/data/core/contextmenu.lua +++ b/data/core/contextmenu.lua @@ -5,11 +5,13 @@ local config = require "core.config" local keymap = require "core.keymap" local style = require "core.style" local Object = require "core.object" +local View = require "core.view" local border_width = 1 local divider_width = 1 local DIVIDER = {} +---@class core.contextmenu : core.object local ContextMenu = Object:extend() ContextMenu.DIVIDER = DIVIDER @@ -20,6 +22,7 @@ function ContextMenu:new() self.selected = -1 self.height = 0 self.position = { x = 0, y = 0 } + self.current_scale = SCALE end local function get_item_size(item) @@ -37,18 +40,10 @@ local function get_item_size(item) return lw, lh end -function ContextMenu:register(predicate, items) - if type(predicate) == "string" then - predicate = require(predicate) - end - if type(predicate) == "table" then - local class = predicate - predicate = function() return core.active_view:is(class) end - end - - local width, height = 0, 0 --precalculate the size of context menu - for i, item in ipairs(items) do - if item ~= DIVIDER then +local function update_items_size(items, update_binding) + local width, height = 0, 0 + for _, item in ipairs(items) do + if update_binding and item ~= DIVIDER then item.info = keymap.get_binding(item.command) end local lw, lh = get_item_size(item) @@ -57,6 +52,11 @@ function ContextMenu:register(predicate, items) end width = width + style.padding.x * 2 items.width, items.height = width, height +end + +function ContextMenu:register(predicate, items) + predicate = command.generate_predicate(predicate) + update_items_size(items, true) table.insert(self.itemset, { predicate = predicate, items = items }) end @@ -91,6 +91,7 @@ function ContextMenu:show(x, y) self.position.x, self.position.y = x, y self.show_context_menu = true + core.request_cursor("arrow") return true end return false @@ -101,6 +102,7 @@ function ContextMenu:hide() self.items = nil self.selected = -1 self.height = 0 + core.request_cursor(core.active_view.cursor) end function ContextMenu:each_item() @@ -126,9 +128,6 @@ function ContextMenu:on_mouse_moved(px, py) break end end - if self.selected >= 0 then - core.request_cursor("arrow") - end return true end @@ -140,53 +139,73 @@ function ContextMenu:on_selected(item) end end -function ContextMenu:on_mouse_pressed(button, x, y, clicks) - local selected = (self.items or {})[self.selected] - local caught = false +local function change_value(value, change) + return value + change +end - self:hide() - if button == "left" then +function ContextMenu:focus_previous() + self.selected = (self.selected == -1 or self.selected == 1) and #self.items or change_value(self.selected, -1) + if self:get_item_selected() == DIVIDER then + self.selected = change_value(self.selected, -1) + end +end + +function ContextMenu:focus_next() + self.selected = (self.selected == -1 or self.selected == #self.items) and 1 or change_value(self.selected, 1) + if self:get_item_selected() == DIVIDER then + self.selected = change_value(self.selected, 1) + end +end + +function ContextMenu:get_item_selected() + return (self.items or {})[self.selected] +end + +function ContextMenu:call_selected_item() + local selected = self:get_item_selected() + self:hide() if selected then self:on_selected(selected) - caught = true end - end +end - if button == "right" then - caught = self:show(x, y) +function ContextMenu:on_mouse_pressed(button, px, py, clicks) + local caught = false + + if self.show_context_menu then + if button == "left" then + local selected = self:get_item_selected() + if selected then + self:on_selected(selected) + end + end + self:hide() + caught = true + else + if button == "right" then + caught = self:show(px, py) + end end return caught end --- copied from core.docview -function ContextMenu:move_towards(t, k, dest, rate) - if type(t) ~= "table" then - return self:move_towards(self, t, k, dest, rate) - end - local val = t[k] - if not config.transitions or math.abs(val - dest) < 0.5 then - t[k] = dest - else - rate = rate or 0.5 - if config.fps ~= 60 or config.animation_rate ~= 1 then - local dt = 60 / config.fps - rate = 1 - common.clamp(1 - rate, 1e-8, 1 - 1e-8)^(config.animation_rate * dt) - end - t[k] = common.lerp(val, dest, rate) - end - if val ~= dest then - core.redraw = true - end -end +ContextMenu.move_towards = View.move_towards function ContextMenu:update() if self.show_context_menu then - self:move_towards("height", self.items.height) + self:move_towards("height", self.items.height, nil, "contextmenu") end end function ContextMenu:draw() if not self.show_context_menu then return end + if self.current_scale ~= SCALE then + update_items_size(self.items) + for _, set in ipairs(self.itemset) do + update_items_size(set.items) + end + self.current_scale = SCALE + end core.root_view:defer_draw(self.draw_context_menu, self) end diff --git a/data/core/dirwatch.lua b/data/core/dirwatch.lua new file mode 100644 index 00000000..5553047d --- /dev/null +++ b/data/core/dirwatch.lua @@ -0,0 +1,232 @@ +local common = require "core.common" +local config = require "core.config" +local dirwatch = {} + +function dirwatch:__index(idx) + local value = rawget(self, idx) + if value ~= nil then return value end + return dirwatch[idx] +end + +function dirwatch.new() + local t = { + scanned = {}, + watched = {}, + reverse_watched = {}, + monitor = dirmonitor.new(), + windows_watch_top = nil, + windows_watch_count = 0 + } + setmetatable(t, dirwatch) + return t +end + + +function dirwatch:scan(directory, bool) + if bool == false then return self:unwatch(directory) end + self.scanned[directory] = system.get_file_info(directory).modified +end + +-- Should be called on every directory in a subdirectory. +-- In windows, this is a no-op for anything underneath a top-level directory, +-- but code should be called anyway, so we can ensure that we have a proper +-- experience across all platforms. Should be an absolute path. +-- Can also be called on individual files, though this should be used sparingly, +-- so as not to run into system limits (like in the autoreload plugin). +function dirwatch:watch(directory, bool) + if bool == false then return self:unwatch(directory) end + local info = system.get_file_info(directory) + if not info then return end + if not self.watched[directory] and not self.scanned[directory] then + if PLATFORM == "Windows" then + if info.type ~= "dir" then return self:scan(directory) end + if not self.windows_watch_top or directory:find(self.windows_watch_top, 1, true) ~= 1 then + -- Get the highest level of directory that is common to this directory, and the original. + local target = directory + while self.windows_watch_top and self.windows_watch_top:find(target, 1, true) ~= 1 do + target = common.dirname(target) + end + if target ~= self.windows_watch_top then + local value = self.monitor:watch(target) + if value and value < 0 then + return self:scan(directory) + end + self.windows_watch_top = target + end + end + self.windows_watch_count = self.windows_watch_count + 1 + self.watched[directory] = true + else + local value = self.monitor:watch(directory) + -- If for whatever reason, we can't watch this directory, revert back to scanning. + -- Don't bother trying to find out why, for now. + if value and value < 0 then + return self:scan(directory) + end + self.watched[directory] = value + self.reverse_watched[value] = directory + end + end +end + +-- this should be an absolute path +function dirwatch:unwatch(directory) + if self.watched[directory] then + if PLATFORM ~= "Windows" then + self.monitor:unwatch(self.watched[directory]) + self.reverse_watched[directory] = nil + else + self.windows_watch_count = self.windows_watch_count - 1 + if self.windows_watch_count == 0 then + self.windows_watch_top = nil + self.monitor:unwatch(directory) + end + end + self.watched[directory] = nil + elseif self.scanned[directory] then + self.scanned[directory] = nil + end +end + +-- designed to be run inside a coroutine. +function dirwatch:check(change_callback, scan_time, wait_time) + local had_change = false + self.monitor:check(function(id) + had_change = true + if PLATFORM == "Windows" then + change_callback(common.dirname(self.windows_watch_top .. PATHSEP .. id)) + elseif self.reverse_watched[id] then + change_callback(self.reverse_watched[id]) + end + end) + local start_time = system.get_time() + for directory, old_modified in pairs(self.scanned) do + if old_modified then + local info = system.get_file_info(directory) + local new_modified = info and info.modified + if old_modified ~= new_modified then + change_callback(directory) + had_change = true + self.scanned[directory] = new_modified + end + end + if system.get_time() - start_time > (scan_time or 0.01) then + coroutine.yield(wait_time or 0.01) + start_time = system.get_time() + end + end + return had_change +end + + +-- inspect config.ignore_files patterns and prepare ready to use entries. +local function compile_ignore_files() + local ipatterns = config.ignore_files + local compiled = {} + -- config.ignore_files could be a simple string... + if type(ipatterns) ~= "table" then ipatterns = {ipatterns} end + for i, pattern in ipairs(ipatterns) do + -- we ignore malformed pattern that raise an error + if pcall(string.match, "a", pattern) then + table.insert(compiled, { + use_path = pattern:match("/[^/$]"), -- contains a slash but not at the end + -- An '/' or '/$' at the end means we want to match a directory. + match_dir = pattern:match(".+/%$?$"), -- to be used as a boolen value + pattern = pattern -- get the actual pattern + }) + end + end + return compiled +end + + +local function fileinfo_pass_filter(info, ignore_compiled) + if info.size >= config.file_size_limit * 1e6 then return false end + local basename = common.basename(info.filename) + -- replace '\' with '/' for Windows where PATHSEP = '\' + local fullname = "/" .. info.filename:gsub("\\", "/") + for _, compiled in ipairs(ignore_compiled) do + local test = compiled.use_path and fullname or basename + if compiled.match_dir then + if info.type == "dir" and string.match(test .. "/", compiled.pattern) then + return false + end + else + if string.match(test, compiled.pattern) then + return false + end + end + end + return true +end + + +local function compare_file(a, b) + return a.filename < b.filename +end + + +-- compute a file's info entry completed with "filename" to be used +-- in project scan or falsy if it shouldn't appear in the list. +local function get_project_file_info(root, file, ignore_compiled) + local info = system.get_file_info(root .. PATHSEP .. file) + -- info can be not nil but info.type may be nil if is neither a file neither + -- a directory, for example for /dev/* entries on linux. + if info and info.type then + info.filename = file + return fileinfo_pass_filter(info, ignore_compiled) and info + end +end + + +-- "root" will by an absolute path without trailing '/' +-- "path" will be a path starting without '/' and without trailing '/' +-- or the empty string. +-- It will identifies a sub-path within "root. +-- The current path location will therefore always be: root .. path. +-- When recursing "root" will always be the same, only "path" will change. +-- Returns a list of file "items". In each item the "filename" will be the +-- complete file path relative to "root" *without* the trailing '/', and without the starting '/'. +function dirwatch.get_directory_files(dir, root, path, t, entries_count, recurse_pred) + local t0 = system.get_time() + local t_elapsed = system.get_time() - t0 + local dirs, files = {}, {} + local ignore_compiled = compile_ignore_files() + + + local all = system.list_dir(root .. PATHSEP .. path) + if not all then return nil end + + for _, file in ipairs(all or {}) do + local info = get_project_file_info(root, (path ~= "" and (path .. PATHSEP) or "") .. file, ignore_compiled) + if info then + table.insert(info.type == "dir" and dirs or files, info) + entries_count = entries_count + 1 + end + end + + local recurse_complete = true + table.sort(dirs, compare_file) + for _, f in ipairs(dirs) do + table.insert(t, f) + if recurse_pred(dir, f.filename, entries_count, t_elapsed) then + local _, complete, n = dirwatch.get_directory_files(dir, root, f.filename, t, entries_count, recurse_pred) + recurse_complete = recurse_complete and complete + if n ~= nil then + entries_count = n + end + else + recurse_complete = false + end + end + + table.sort(files, compare_file) + for _, f in ipairs(files) do + table.insert(t, f) + end + + return t, recurse_complete, entries_count +end + + +return dirwatch diff --git a/data/core/doc/highlighter.lua b/data/core/doc/highlighter.lua index 9ba7b634..888c82aa 100644 --- a/data/core/doc/highlighter.lua +++ b/data/core/doc/highlighter.lua @@ -22,13 +22,21 @@ function Highlighter:new(doc) else local max = math.min(self.first_invalid_line + 40, self.max_wanted_line) + local retokenized_from for i = self.first_invalid_line, max do local state = (i > 1) and self.lines[i - 1].state local line = self.lines[i] if not (line and line.init_state == state and line.text == self.doc.lines[i]) then + retokenized_from = retokenized_from or i self.lines[i] = self:tokenize_line(i, state) + elseif retokenized_from then + self:update_notify(retokenized_from, i - retokenized_from - 1) + retokenized_from = nil end end + if retokenized_from then + self:update_notify(retokenized_from, max - retokenized_from) + end self.first_invalid_line = max + 1 core.redraw = true @@ -71,6 +79,10 @@ function Highlighter:remove_notify(line, n) common.splice(self.lines, line, n) end +function Highlighter:update_notify(line, n) + -- plugins can hook here to be notified that lines have been retokenized +end + function Highlighter:tokenize_line(idx, state) local res = {} @@ -87,6 +99,7 @@ function Highlighter:get_line(idx) local prev = self.lines[idx - 1] line = self:tokenize_line(idx, prev and prev.state) self.lines[idx] = line + self:update_notify(idx, 0) end self.max_wanted_line = math.max(self.max_wanted_line, idx) return line diff --git a/data/core/doc/init.lua b/data/core/doc/init.lua index f324b6d3..4136575d 100644 --- a/data/core/doc/init.lua +++ b/data/core/doc/init.lua @@ -5,7 +5,7 @@ local syntax = require "core.syntax" local config = require "core.config" local common = require "core.common" - +---@class core.doc : core.object local Doc = Object:extend() @@ -33,7 +33,6 @@ end function Doc:reset() self.lines = { "\n" } self.selections = { 1, 1, 1, 1 } - self.cursor_clipboard = {} self.undo_stack = { idx = 1 } self.redo_stack = { idx = 1 } self.clean_change_id = 1 @@ -55,6 +54,7 @@ end function Doc:set_filename(filename, abs_filename) self.filename = filename self.abs_filename = abs_filename + self:reset_syntax() end @@ -80,11 +80,23 @@ function Doc:load(filename) end +function Doc:reload() + if self.filename then + local sel = { self:get_selection() } + self:load(self.filename) + self:clean() + self:set_selection(table.unpack(sel)) + end +end + + function Doc:save(filename, abs_filename) if not filename then assert(self.filename, "no filename set to default to") filename = self.filename abs_filename = self.abs_filename + else + assert(self.filename or abs_filename, "calling save on unnamed doc without absolute path") end local fp = assert( io.open(filename, "wb") ) for _, line in ipairs(self.lines) do @@ -94,7 +106,6 @@ function Doc:save(filename, abs_filename) fp:close() self:set_filename(filename, abs_filename) self.new_file = false - self:reset_syntax() self:clean() end @@ -135,8 +146,8 @@ end -- curors can never swap positions; only merge or split, or change their position in cursor -- order. function Doc:get_selection(sort) - local idx, line1, col1, line2, col2 = self:get_selections(sort)({ self.selections, sort }, 0) - return line1, col1, line2, col2, sort + local idx, line1, col1, line2, col2, swap = self:get_selections(sort)({ self.selections, sort }, 0) + return line1, col1, line2, col2, swap end function Doc:get_selection_text(limit) @@ -172,9 +183,9 @@ end local function sort_positions(line1, col1, line2, col2) if line1 > line2 or line1 == line2 and col1 > col2 then - return line2, col2, line1, col1 + return line2, col2, line1, col1, true end - return line1, col1, line2, col2 + return line1, col1, line2, col2, false end function Doc:set_selections(idx, line1, col1, line2, col2, swap, rm) @@ -197,8 +208,14 @@ function Doc:add_selection(line1, col1, line2, col2, swap) self:set_selections(target, line1, col1, line2, col2, swap, 0) end + +function Doc:remove_selection(idx) + common.splice(self.selections, (idx - 1) * 4 + 1, 4) +end + + function Doc:set_selection(line1, col1, line2, col2, swap) - self.selections, self.cursor_clipboard = {}, {} + self.selections = {} self:set_selections(1, line1, col1, line2, col2, swap) end @@ -208,12 +225,10 @@ function Doc:merge_cursors(idx) if self.selections[i] == self.selections[j] and self.selections[i+1] == self.selections[j+1] then common.splice(self.selections, i, 4) - common.splice(self.cursor_clipboard, i, 1) break end end end - if #self.selections <= 4 then self.cursor_clipboard = {} end end local function selection_iterator(invariant, idx) @@ -356,7 +371,7 @@ function Doc:raw_insert(line, col, text, undo_stack, time) -- splice lines into line array common.splice(self.lines, line, 1, lines) - + -- keep cursors where they should be for idx, cline1, ccol1, cline2, ccol2 in self:get_selections(true, true) do if cline1 < line then break end @@ -388,7 +403,7 @@ function Doc:raw_remove(line1, col1, line2, col2, undo_stack, time) -- splice line into line array common.splice(self.lines, line1, line2 - line1 + 1, { before .. after }) - + -- move all cursors back if they share a line with the removed text for idx, cline1, ccol1, cline2, ccol2 in self:get_selections(true, true) do if cline1 < line2 then break end @@ -443,7 +458,7 @@ end function Doc:replace_cursor(idx, line1, col1, line2, col2, fn) local old_text = self:get_text(line1, col1, line2, col2) - local new_text, n = fn(old_text) + local new_text, res = fn(old_text) if old_text ~= new_text then self:insert(line2, col2, new_text) self:remove(line1, col1, line2, col2) @@ -452,22 +467,22 @@ function Doc:replace_cursor(idx, line1, col1, line2, col2, fn) self:set_selections(idx, line1, col1, line2, col2) end end - return n + return res end function Doc:replace(fn) - local has_selection, n = false, 0 + local has_selection, results = false, { } for idx, line1, col1, line2, col2 in self:get_selections(true) do - if line1 ~= line2 or col1 ~= col2 then - n = n + self:replace_cursor(idx, line1, col1, line2, col2, fn) + if line1 ~= line2 or col1 ~= col2 then + results[idx] = self:replace_cursor(idx, line1, col1, line2, col2, fn) has_selection = true end end if not has_selection then self:set_selection(table.unpack(self.selections)) - n = n + self:replace_cursor(1, 1, 1, #self.lines, #self.lines[#self.lines], fn) + results[1] = self:replace_cursor(1, 1, 1, #self.lines, #self.lines[#self.lines], fn) end - return n + return results end @@ -548,10 +563,12 @@ function Doc:indent_text(unindent, line1, col1, line2, col2) if unindent or has_selection or in_beginning_whitespace then local l1d, l2d = #self.lines[line1], #self.lines[line2] for line = line1, line2 do - local e, rnded = self:get_line_indent(self.lines[line], unindent) - self:remove(line, 1, line, (e or 0) + 1) - self:insert(line, 1, - unindent and rnded:sub(1, #rnded - #text) or rnded .. text) + if not has_selection or #self.lines[line] > 1 then -- don't indent empty lines in a selection + local e, rnded = self:get_line_indent(self.lines[line], unindent) + self:remove(line, 1, line, (e or 0) + 1) + self:insert(line, 1, + unindent and rnded:sub(1, #rnded - #text) or rnded .. text) + end end l1d, l2d = #self.lines[line1] - l1d, #self.lines[line2] - l2d if (unindent or in_beginning_whitespace) and not has_selection then diff --git a/data/core/docview.lua b/data/core/docview.lua index a4587670..f4270e9f 100644 --- a/data/core/docview.lua +++ b/data/core/docview.lua @@ -6,7 +6,8 @@ local keymap = require "core.keymap" local translate = require "core.doc.translate" local View = require "core.view" - +---@class core.docview : core.view +---@field super core.view local DocView = View:extend() DocView.context = "session" @@ -29,6 +30,9 @@ DocView.translate = { end, ["next_page"] = function(doc, line, col, dv) + if line == #doc.lines then + return #doc.lines, #doc.lines[line] + end local min, max = dv:get_visible_line_range() return line + (max - min), 1 end, @@ -62,19 +66,22 @@ end function DocView:try_close(do_close) if self.doc:is_dirty() and #core.get_views_referencing_doc(self.doc) == 1 then - core.command_view:enter("Unsaved Changes; Confirm Close", function(_, item) - if item.text:match("^[cC]") then - do_close() - elseif item.text:match("^[sS]") then - self.doc:save() - do_close() + core.command_view:enter("Unsaved Changes; Confirm Close", { + submit = function(_, item) + if item.text:match("^[cC]") then + do_close() + elseif item.text:match("^[sS]") then + self.doc:save() + do_close() + end + end, + suggest = function(text) + local items = {} + if not text:find("^[^cC]") then table.insert(items, "Close Without Saving") end + if not text:find("^[^sS]") then table.insert(items, "Save And Close") end + return items end - end, function(text) - local items = {} - if not text:find("^[^cC]") then table.insert(items, "Close Without Saving") end - if not text:find("^[^sS]") then table.insert(items, "Save And Close") end - return items - end) + }) else do_close() end @@ -121,14 +128,18 @@ function DocView:get_gutter_width() end -function DocView:get_line_screen_position(idx) +function DocView:get_line_screen_position(line, col) local x, y = self:get_content_offset() local lh = self:get_line_height() local gw = self:get_gutter_width() - return x + gw, y + (idx-1) * lh + style.padding.y + y = y + (line-1) * lh + style.padding.y + if col then + return x + gw + self:get_col_x_offset(line, col), y + else + return x + gw, y + end end - function DocView:get_line_text_y_offset() local lh = self:get_line_height() local th = self:get_font():get_height() @@ -198,8 +209,9 @@ end function DocView:scroll_to_line(line, ignore_if_visible, instant) local min, max = self:get_visible_line_range() if not (ignore_if_visible and line > min and line < max) then - local lh = self:get_line_height() - self.scroll.to.y = math.max(0, lh * (line - 1) - self.size.y / 2) + local x, y = self:get_line_screen_position(line) + local ox, oy = self:get_content_offset() + self.scroll.to.y = math.max(0, y - oy - self.size.y / 2) if instant then self.scroll.y = self.scroll.to.y end @@ -208,10 +220,10 @@ end function DocView:scroll_to_make_visible(line, col) - local min = self:get_line_height() * (line - 1) - local max = self:get_line_height() * (line + 2) - self.size.y - self.scroll.to.y = math.min(self.scroll.to.y, min) - self.scroll.to.y = math.max(self.scroll.to.y, max) + local ox, oy = self:get_content_offset() + local _, ly = self:get_line_screen_position(line, col) + local lh = self:get_line_height() + self.scroll.to.y = common.clamp(self.scroll.to.y, ly - oy - self.size.y + lh * 2, ly - oy - lh) local gw = self:get_gutter_width() local xoffset = self:get_col_x_offset(line, col) local xmargin = 3 * self:get_font():get_width(' ') @@ -224,11 +236,10 @@ function DocView:scroll_to_make_visible(line, col) end end - function DocView:on_mouse_moved(x, y, ...) DocView.super.on_mouse_moved(self, x, y, ...) - if self:scrollbar_overlaps_point(x, y) or self.dragging_scrollbar then + if self.hovered_scrollbar_track or self.dragging_scrollbar then self.cursor = "arrow" else self.cursor = "ibeam" @@ -271,8 +282,8 @@ function DocView:mouse_selection(doc, snap_type, line1, col1, line2, col2) end -function DocView:on_mouse_released(button) - DocView.super.on_mouse_released(self, button) +function DocView:on_mouse_released(...) + DocView.super.on_mouse_released(self, ...) self.mouse_selecting = nil end @@ -284,13 +295,15 @@ end function DocView:update() -- scroll to make caret visible and reset blink timer if it moved - local line, col = self.doc:get_selection() - if (line ~= self.last_line or col ~= self.last_col) and self.size.x > 0 then + local line1, col1, line2, col2 = self.doc:get_selection() + if (line1 ~= self.last_line1 or col1 ~= self.last_col1 or + line2 ~= self.last_line2 or col2 ~= self.last_col2) and self.size.x > 0 then if core.active_view == self then - self:scroll_to_make_visible(line, col) + self:scroll_to_make_visible(line1, col1) end core.blink_reset() - self.last_line, self.last_col = line, col + self.last_line1, self.last_col1 = line1, col1 + self.last_line2, self.last_col2 = line2, col2 end -- update blink timer @@ -313,14 +326,15 @@ function DocView:draw_line_highlight(x, y) end -function DocView:draw_line_text(idx, x, y) +function DocView:draw_line_text(line, x, y) local default_font = self:get_font() local tx, ty = x, y + self:get_line_text_y_offset() - for _, type, text in self.doc.highlighter:each_token(idx) do + for _, type, text in self.doc.highlighter:each_token(line) do local color = style.syntax[type] local font = style.syntax_fonts[type] or default_font tx = renderer.draw_text(font, text, tx, ty, color) end + return self:get_line_height() end function DocView:draw_caret(x, y) @@ -328,28 +342,37 @@ function DocView:draw_caret(x, y) renderer.draw_rect(x, y, style.caret_width, lh, style.caret) end -function DocView:draw_line_body(idx, x, y) +function DocView:draw_line_body(line, x, y) -- draw highlight if any selection ends on this line local draw_highlight = false - for lidx, line1, col1, line2, col2 in self.doc:get_selections(false) do - if line1 == idx then - draw_highlight = true - break + local hcl = config.highlight_current_line + if hcl ~= false then + for lidx, line1, col1, line2, col2 in self.doc:get_selections(false) do + if line1 == line then + if hcl == "no_selection" then + if (line1 ~= line2) or (col1 ~= col2) then + draw_highlight = false + break + end + end + draw_highlight = true + break + end end end - if draw_highlight and config.highlight_current_line and core.active_view == self then + if draw_highlight and core.active_view == self then self:draw_line_highlight(x + self.scroll.x, y) end -- draw selection if it overlaps this line + local lh = self:get_line_height() for lidx, line1, col1, line2, col2 in self.doc:get_selections(true) do - if idx >= line1 and idx <= line2 then - local text = self.doc.lines[idx] - if line1 ~= idx then col1 = 1 end - if line2 ~= idx then col2 = #text + 1 end - local x1 = x + self:get_col_x_offset(idx, col1) - local x2 = x + self:get_col_x_offset(idx, col2) - local lh = self:get_line_height() + if line >= line1 and line <= line2 then + local text = self.doc.lines[line] + if line1 ~= line then col1 = 1 end + if line2 ~= line then col2 = #text + 1 end + local x1 = x + self:get_col_x_offset(line, col1) + local x2 = x + self:get_col_x_offset(line, col2) if x1 ~= x2 then renderer.draw_rect(x1, y, x2 - x1, lh, style.selection) end @@ -357,21 +380,22 @@ function DocView:draw_line_body(idx, x, y) end -- draw line's text - self:draw_line_text(idx, x, y) + return self:draw_line_text(line, x, y) end -function DocView:draw_line_gutter(idx, x, y, width) +function DocView:draw_line_gutter(line, x, y, width) local color = style.line_number for _, line1, _, line2 in self.doc:get_selections(true) do - if idx >= line1 and idx <= line2 then + if line >= line1 and line <= line2 then color = style.line_number2 break end end - local yoffset = self:get_line_text_y_offset() x = x + style.padding.x - common.draw_text(self:get_font(), color, idx, "right", x, y + yoffset, width, self:get_line_height()) + local lh = self:get_line_height() + common.draw_text(self:get_font(), color, line, "right", x, y, width, lh) + return lh end @@ -385,8 +409,7 @@ function DocView:draw_overlay() and system.window_has_focus() then if config.disable_blink or (core.blink_timer - core.blink_start) % T < T / 2 then - local x, y = self:get_line_screen_position(line) - self:draw_caret(x + self:get_col_x_offset(line, col), y) + self:draw_caret(self:get_line_screen_position(line, col)) end end end @@ -404,8 +427,7 @@ function DocView:draw() local x, y = self:get_line_screen_position(minline) local gw, gpad = self:get_gutter_width() for i = minline, maxline do - self:draw_line_gutter(i, self.position.x, y, gpad and gw - gpad or gw) - y = y + lh + y = y + (self:draw_line_gutter(i, self.position.x, y, gpad and gw - gpad or gw) or lh) end local pos = self.position @@ -414,8 +436,7 @@ function DocView:draw() -- right side it is redundant with the Node's clip. core.push_clip_rect(pos.x + gw, pos.y, self.size.x - gw, self.size.y) for i = minline, maxline do - self:draw_line_body(i, x, y) - y = y + lh + y = y + (self:draw_line_body(i, x, y) or lh) end self:draw_overlay() core.pop_clip_rect() diff --git a/data/core/emptyview.lua b/data/core/emptyview.lua index 9d375c20..0d0a929e 100644 --- a/data/core/emptyview.lua +++ b/data/core/emptyview.lua @@ -2,14 +2,26 @@ local style = require "core.style" local keymap = require "core.keymap" local View = require "core.view" +---@class core.emptyview : core.view +---@field super core.view local EmptyView = View:extend() local function draw_text(x, y, color) local th = style.big_font:get_height() local dh = 2 * th + style.padding.y * 2 local x1, y1 = x, y + (dh - th) / 2 - x = renderer.draw_text(style.big_font, "Lite XL", x1, y1, color) - renderer.draw_text(style.font, "version " .. VERSION, x1, y1 + th, color) + local xv = x1 + local title = "Lite XL" + local version = "version " .. VERSION + local title_width = style.big_font:get_width(title) + local version_width = style.font:get_width(version) + if version_width > title_width then + version = VERSION + version_width = style.font:get_width(version) + xv = x1 - (version_width - title_width) + end + x = renderer.draw_text(style.big_font, title, x1, y1, color) + renderer.draw_text(style.font, version, xv, y1 + th, color) x = x + style.padding.x renderer.draw_rect(x, y, math.ceil(1 * SCALE), dh, color) local lines = { diff --git a/data/core/init.lua b/data/core/init.lua index 0d7c6555..b5b34c03 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -2,9 +2,10 @@ require "core.strict" require "core.regex" local common = require "core.common" local config = require "core.config" -local style = require "core.style" +local style = require "colors.default" local command local keymap +local dirwatch local RootView local StatusView local TitleView @@ -58,20 +59,38 @@ function core.set_project_dir(new_dir, change_project_fn) if change_project_fn then change_project_fn() end core.project_dir = common.normalize_volume(new_dir) core.project_directories = {} - core.add_project_directory(new_dir) - return true end - return false + return chdir_ok +end + + +local function reload_customizations() + local user_error = not core.load_user_directory() + local project_error = not core.load_project_module() + if user_error or project_error then + -- Use core.add_thread to delay opening the LogView, as opening + -- it directly here disturbs the normal save operations. + core.add_thread(function() + local LogView = require "core.logview" + local rn = core.root_view.root_node + for _,v in pairs(core.root_view.root_node:get_children()) do + if v:is(LogView) then + rn:get_node_for_view(v):set_active_view(v) + return + end + end + command.perform("core:open-log") + end) + end end function core.open_folder_project(dir_path_abs) if core.set_project_dir(dir_path_abs, core.on_quit_project) then core.root_view:close_all_docviews() + reload_customizations() update_recents_project("add", dir_path_abs) - if not core.load_project_module() then - command.perform("core:open-log") - end + core.add_project_directory(dir_path_abs) core.on_enter_project(dir_path_abs) end end @@ -88,24 +107,136 @@ local function strip_trailing_slash(filename) return filename end -local function compare_file(a, b) - return a.filename < b.filename + +function core.project_subdir_is_shown(dir, filename) + return not dir.files_limit or dir.shown_subdir[filename] end --- compute a file's info entry completed with "filename" to be used --- in project scan or falsy if it shouldn't appear in the list. -local function get_project_file_info(root, file) - local info = system.get_file_info(root .. file) - if info then - info.filename = strip_leading_path(file) - return (info.size < config.file_size_limit * 1e6 and - not common.match_pattern(common.basename(info.filename), config.ignore_files) - and info) +local function show_max_files_warning(dir) + local message = dir.slow_filesystem and + "Filesystem is too slow: project files will not be indexed." or + "Too many files in project directory: stopped reading at ".. + config.max_project_files.." files. For more information see ".. + "usage.md at https://github.com/lite-xl/lite-xl." + if core.status_view then + core.status_view:show_message("!", style.accent, message) end end +-- bisects the sorted file list to get to things in ln(n) +local function file_bisect(files, is_superior, start_idx, end_idx) + local inf, sup = start_idx or 1, end_idx or #files + while sup - inf > 8 do + local curr = math.floor((inf + sup) / 2) + if is_superior(files[curr]) then + sup = curr - 1 + else + inf = curr + end + end + while inf <= sup and not is_superior(files[inf]) do + inf = inf + 1 + end + return inf +end + + +local function file_search(files, info) + local idx = file_bisect(files, function(file) + return system.path_compare(info.filename, info.type, file.filename, file.type) + end) + if idx > 1 and files[idx-1].filename == info.filename then + return idx - 1, true + end + return idx, false +end + + +local function files_info_equal(a, b) + return (a == nil and b == nil) or (a and b and a.filename == b.filename and a.type == b.type) +end + + +local function project_subdir_bounds(dir, filename, start_index) + local found = true + if not start_index then + start_index, found = file_search(dir.files, { type = "dir", filename = filename }) + end + if found then + local end_index = file_bisect(dir.files, function(file) + return not common.path_belongs_to(file.filename, filename) + end, start_index + 1) + return start_index, end_index - start_index, dir.files[start_index] + end +end + + +-- Should be called on any directory that registers a change, or on a directory we open if we're over the file limit. +-- Uses relative paths at the project root (i.e. target = "", target = "first-level-directory", target = "first-level-directory/second-level-directory") +local function refresh_directory(topdir, target) + local directory_start_idx, directory_end_idx = 1, #topdir.files + if target and target ~= "" then + directory_start_idx, directory_end_idx = project_subdir_bounds(topdir, target) + directory_end_idx = directory_start_idx + directory_end_idx - 1 + directory_start_idx = directory_start_idx + 1 + end + + local files = dirwatch.get_directory_files(topdir, topdir.name, (target or ""), {}, 0, function() return false end) + local change = false + + -- If this file doesn't exist, we should be calling this on our parent directory, assume we'll do that. + -- Unwatch just in case. + if files == nil then + topdir.watch:unwatch(topdir.name .. PATHSEP .. (target or "")) + return true + end + + local new_idx, old_idx = 1, directory_start_idx + local new_directories = {} + -- Run through each sorted list and compare them. If we find a new entry, insert it and flag as new. If we're missing an entry + -- remove it and delete the entry from the list. + while old_idx <= directory_end_idx or new_idx <= #files do + local old_info, new_info = topdir.files[old_idx], files[new_idx] + if not files_info_equal(new_info, old_info) then + change = true + -- If we're a new file, and we exist *before* the other file in the list, then add to the list. + if not old_info or (new_info and system.path_compare(new_info.filename, new_info.type, old_info.filename, old_info.type)) then + table.insert(topdir.files, old_idx, new_info) + old_idx, new_idx = old_idx + 1, new_idx + 1 + if new_info.type == "dir" then + table.insert(new_directories, new_info) + end + directory_end_idx = directory_end_idx + 1 + else + -- If it's not there, remove the entry from the list as being out of order. + table.remove(topdir.files, old_idx) + if old_info.type == "dir" then + topdir.watch:unwatch(topdir.name .. PATHSEP .. old_info.filename) + end + directory_end_idx = directory_end_idx - 1 + end + else + -- If this file is a directory, determine in ln(n) the size of the directory, and skip every file in it. + local size = old_info and old_info.type == "dir" and select(2, project_subdir_bounds(topdir, old_info.filename, old_idx)) or 1 + old_idx, new_idx = old_idx + size, new_idx + 1 + end + end + for i, v in ipairs(new_directories) do + topdir.watch:watch(topdir.name .. PATHSEP .. v.filename) + if not topdir.files_limit or core.project_subdir_is_shown(topdir, v.filename) then + refresh_directory(topdir, v.filename) + end + end + if change then + core.redraw = true + topdir.is_dirty = true + end + return change +end + + -- Predicate function to inhibit directory recursion in get_directory_files -- based on a time limit and the number of files. local function timed_max_files_pred(dir, filename, entries_count, t_elapsed) @@ -115,253 +246,122 @@ local function timed_max_files_pred(dir, filename, entries_count, t_elapsed) end --- "root" will by an absolute path without trailing '/' --- "path" will be a path starting with '/' and without trailing '/' --- or the empty string. --- It will identifies a sub-path within "root. --- The current path location will therefore always be: root .. path. --- When recursing "root" will always be the same, only "path" will change. --- Returns a list of file "items". In eash item the "filename" will be the --- complete file path relative to "root" *without* the trailing '/'. -local function get_directory_files(dir, root, path, t, entries_count, recurse_pred, begin_hook) - if begin_hook then begin_hook() end - local t0 = system.get_time() - local all = system.list_dir(root .. path) or {} - local t_elapsed = system.get_time() - t0 - local dirs, files = {}, {} - - for _, file in ipairs(all) do - local info = get_project_file_info(root, path .. PATHSEP .. file) - if info then - table.insert(info.type == "dir" and dirs or files, info) - entries_count = entries_count + 1 - end - end - - local recurse_complete = true - table.sort(dirs, compare_file) - for _, f in ipairs(dirs) do - table.insert(t, f) - if recurse_pred(dir, f.filename, entries_count, t_elapsed) then - local _, complete, n = get_directory_files(dir, root, PATHSEP .. f.filename, t, entries_count, recurse_pred, begin_hook) - recurse_complete = recurse_complete and complete - entries_count = n - else - recurse_complete = false - end - end - - table.sort(files, compare_file) - for _, f in ipairs(files) do - table.insert(t, f) - end - - return t, recurse_complete, entries_count -end - - -function core.project_subdir_set_show(dir, filename, show) - dir.shown_subdir[filename] = show - if dir.files_limit and PLATFORM == "Linux" then - local fullpath = dir.name .. PATHSEP .. filename - local watch_fn = show and system.watch_dir_add or system.watch_dir_rm - local success = watch_fn(dir.watch_id, fullpath) - if not success then - core.log("Internal warning: error calling system.watch_dir_%s", show and "add" or "rm") - end - end -end - - -function core.project_subdir_is_shown(dir, filename) - return not dir.files_limit or dir.shown_subdir[filename] -end - - -local function show_max_files_warning(dir) - local message = dir.slow_filesystem and - "Filesystem is too slow: project files will not be indexed." or - "Too many files in project directory: stopped reading at ".. - config.max_project_files.." files. For more information see ".. - "usage.md at github.com/lite-xl/lite-xl." - core.status_view:show_message("!", style.accent, message) -end - - -local function file_search(files, info) - local filename, type = info.filename, info.type - local inf, sup = 1, #files - while sup - inf > 8 do - local curr = math.floor((inf + sup) / 2) - if system.path_compare(filename, type, files[curr].filename, files[curr].type) then - sup = curr - 1 - else - inf = curr - end - end - while inf <= sup and not system.path_compare(filename, type, files[inf].filename, files[inf].type) do - if files[inf].filename == filename then - return inf, true - end - inf = inf + 1 - end - return inf, false -end - - -local function project_scan_add_entry(dir, fileinfo) - local index, match = file_search(dir.files, fileinfo) - if not match then - table.insert(dir.files, index, fileinfo) - dir.is_dirty = true - end -end - - -local function files_info_equal(a, b) - return a.filename == b.filename and a.type == b.type -end - --- for "a" inclusive from i1 + 1 and i1 + n -local function files_list_match(a, i1, n, b) - if n ~= #b then return false end - for i = 1, n do - if not files_info_equal(a[i1 + i], b[i]) then - return false - end - end - return true -end - --- arguments like for files_list_match -local function files_list_replace(as, i1, n, bs) - local m = #bs - local i, j = 1, 1 - while i <= m or i <= n do - local a, b = as[i1 + i], bs[j] - if i > n or (j <= m and not files_info_equal(a, b) and - not system.path_compare(a.filename, a.type, b.filename, b.type)) - then - table.insert(as, i1 + i, b) - i, j, n = i + 1, j + 1, n + 1 - elseif j > m or system.path_compare(a.filename, a.type, b.filename, b.type) then - table.remove(as, i1 + i) - n = n - 1 - else - i, j = i + 1, j + 1 - end - end -end - -local function project_subdir_bounds(dir, filename) - local index, n = 0, #dir.files - for i, file in ipairs(dir.files) do - local file = dir.files[i] - if file.filename == filename then - index, n = i, #dir.files - i - for j = 1, #dir.files - i do - if not common.path_belongs_to(dir.files[i + j].filename, filename) then - n = j - 1 - break - end - end - return index, n, file - end - end -end - -local function rescan_project_subdir(dir, filename_rooted) - local new_files = get_directory_files(dir, dir.name, filename_rooted, {}, 0, core.project_subdir_is_shown, coroutine.yield) - local index, n = 0, #dir.files - if filename_rooted ~= "" then - local filename = strip_leading_path(filename_rooted) - index, n = project_subdir_bounds(dir, filename) - end - - if not files_list_match(dir.files, index, n, new_files) then - files_list_replace(dir.files, index, n, new_files) - dir.is_dirty = true - return true - end -end - - -local function add_dir_scan_thread(dir) - core.add_thread(function() - while true do - local has_changes = rescan_project_subdir(dir, "") - if has_changes then - core.redraw = true -- we run without an event, from a thread - end - coroutine.yield(5) - end - end) -end - --- Populate a project folder top directory by scanning the filesystem. -local function scan_project_folder(index) - local dir = core.project_directories[index] - if PLATFORM == "Linux" then - local fstype = system.get_fs_type(dir.name) - dir.force_rescan = (fstype == "nfs" or fstype == "fuse") - end - local t, complete, entries_count = get_directory_files(dir, dir.name, "", {}, 0, timed_max_files_pred) - if not complete then - dir.slow_filesystem = not complete and (entries_count <= config.max_project_files) - dir.files_limit = true - if not dir.force_rescan then - -- Watch non-recursively on Linux only. - -- The reason is recursively watching with dmon on linux - -- doesn't work on very large directories. - dir.watch_id = system.watch_dir(dir.name, PLATFORM ~= "Linux") - end - if core.status_view then -- May be not yet initialized. - show_max_files_warning(dir) - end - else - if not dir.force_rescan then - dir.watch_id = system.watch_dir(dir.name, true) - end - end - dir.files = t - if dir.force_rescan then - add_dir_scan_thread(dir) - else - core.dir_rescan_add_job(dir, ".") - end -end - - function core.add_project_directory(path) -- top directories has a file-like "item" but the item.filename -- will be simply the name of the directory, without its path. -- The field item.topdir will identify it as a top level directory. path = common.normalize_volume(path) - local dir = { + local topdir = { name = path, item = {filename = common.basename(path), type = "dir", topdir = true}, files_limit = false, is_dirty = true, shown_subdir = {}, + watch_thread = nil, + watch = dirwatch.new() } - table.insert(core.project_directories, dir) - scan_project_folder(#core.project_directories) + table.insert(core.project_directories, topdir) + + local fstype = PLATFORM == "Linux" and system.get_fs_type(topdir.name) or "unknown" + topdir.force_scans = (fstype == "nfs" or fstype == "fuse") + local t, complete, entries_count = dirwatch.get_directory_files(topdir, topdir.name, "", {}, 0, timed_max_files_pred) + topdir.files = t + if not complete then + topdir.slow_filesystem = not complete and (entries_count <= config.max_project_files) + topdir.files_limit = true + show_max_files_warning(topdir) + refresh_directory(topdir) + else + for i,v in ipairs(t) do + if v.type == "dir" then topdir.watch:watch(path .. PATHSEP .. v.filename) end + end + end + topdir.watch:watch(topdir.name) + -- each top level directory gets a watch thread. if the project is small, or + -- if the ablity to use directory watches hasn't been compromised in some way + -- either through error, or amount of files, then this should be incredibly + -- quick; essentially one syscall per check. Otherwise, this may take a bit of + -- time; the watch will yield in this coroutine after 0.01 second, for 0.1 seconds. + topdir.watch_thread = core.add_thread(function() + while true do + local changed = topdir.watch:check(function(target) + if target == topdir.name then return refresh_directory(topdir) end + local dirpath = target:sub(#topdir.name + 2) + local abs_dirpath = topdir.name .. PATHSEP .. dirpath + if dirpath then + -- check if the directory is in the project files list, if not exit. + local dir_index, dir_match = file_search(topdir.files, {filename = dirpath, type = "dir"}) + if not dir_match or not core.project_subdir_is_shown(topdir, topdir.files[dir_index].filename) then return end + end + return refresh_directory(topdir, dirpath) + end, 0.01, 0.01) + coroutine.yield(changed and 0.05 or 0) + end + end) + if path == core.project_dir then - core.project_files = dir.files + core.project_files = topdir.files end core.redraw = true - return dir + return topdir +end + + +-- The function below is needed to reload the project directories +-- when the project's module changes. +function core.rescan_project_directories() + local save_project_dirs = {} + local n = #core.project_directories + for i = 1, n do + local dir = core.project_directories[i] + save_project_dirs[i] = {name = dir.name, shown_subdir = dir.shown_subdir} + end + core.project_directories = {} + for i = 1, n do -- add again the directories in the project + local dir = core.add_project_directory(save_project_dirs[i].name) + if dir.files_limit then + -- We need to sort the list of shown subdirectories so that higher level + -- directories are populated first. We use the function system.path_compare + -- because it order the entries in the appropriate order. + -- TODO: we may consider storing the table shown_subdir as a sorted table + -- since the beginning. + local subdir_list = {} + for subdir in pairs(save_project_dirs[i].shown_subdir) do + table.insert(subdir_list, subdir) + end + table.sort(subdir_list, function(a, b) return system.path_compare(a, "dir", b, "dir") end) + for _, subdir in ipairs(subdir_list) do + local show = save_project_dirs[i].shown_subdir[subdir] + for j = 1, #dir.files do + if dir.files[j].filename == subdir then + -- The instructions below match when happens in TreeView:on_mouse_pressed. + -- We perform the operations only once iff the subdir is in dir.files. + -- In theory set_show below may fail and return false but is it is listed + -- there it means it succeeded before so we are optimistically assume it + -- will not fail for the sake of simplicity. + core.update_project_subdir(dir, subdir, show) + break + end + end + end + end + end +end + + +function core.project_dir_by_name(name) + for i = 1, #core.project_directories do + if core.project_directories[i].name == name then + return core.project_directories[i] + end + end end function core.update_project_subdir(dir, filename, expanded) - local index, n, file = project_subdir_bounds(dir, filename) - if index then - local new_files = expanded and get_directory_files(dir, dir.name, PATHSEP .. filename, {}, 0, core.project_subdir_is_shown) or {} - files_list_replace(dir.files, index, n, new_files) - dir.is_dirty = true - return true - end + assert(dir.files_limit, "function should be called only when directory is in files limit mode") + dir.shown_subdir[filename] = expanded + return refresh_directory(dir, filename) end @@ -377,7 +377,7 @@ local function find_files_rec(root, path) info.filename = strip_leading_path(file) if info.type == "file" then coroutine.yield(root, info) - else + elseif not common.match_pattern(common.basename(info.filename), config.ignore_files) then find_files_rec(root, PATHSEP .. info.filename) end end @@ -438,42 +438,6 @@ function core.project_files_number() end -local function project_dir_by_watch_id(watch_id) - for i = 1, #core.project_directories do - if core.project_directories[i].watch_id == watch_id then - return core.project_directories[i] - end - end -end - - -local function project_scan_remove_file(dir, filepath) - local fileinfo = { filename = filepath } - for _, filetype in ipairs {"dir", "file"} do - fileinfo.type = filetype - local index, match = file_search(dir.files, fileinfo) - if match then - table.remove(dir.files, index) - dir.is_dirty = true - return - end - end -end - - -local function project_scan_add_file(dir, filepath) - for fragment in string.gmatch(filepath, "([^/\\]+)") do - if common.match_pattern(fragment, config.ignore_files) then - return - end - end - local fileinfo = get_project_file_info(dir.name, PATHSEP .. filepath) - if fileinfo then - project_scan_add_entry(dir, fileinfo) - end -end - - -- create a directory using mkdir but may need to create the parent -- directories as well. local function create_user_directory() @@ -520,6 +484,10 @@ local style = require "core.style" -- style.font = renderer.font.load(DATADIR .. "/fonts/FiraSans-Regular.ttf", 14 * SCALE) -- style.code_font = renderer.font.load(DATADIR .. "/fonts/JetBrainsMono-Regular.ttf", 14 * SCALE) -- +-- DATADIR is the location of the installed Lite XL Lua code, default color +-- schemes and fonts. +-- USERDIR is the location of the Lite XL configuration directory. +-- -- font names used by lite: -- style.font : user interface -- style.big_font : big text in welcome screen @@ -529,11 +497,16 @@ local style = require "core.style" -- -- the function to load the font accept a 3rd optional argument like: -- --- {antialiasing="grayscale", hinting="full"} +-- {antialiasing="grayscale", hinting="full", bold=true, italic=true, underline=true, smoothing=true, strikethrough=true} -- -- possible values are: -- antialiasing: grayscale, subpixel -- hinting: none, slight, full +-- bold: true, false +-- italic: true, false +-- underline: true, false +-- smoothing: true, false +-- strikethrough: true, false ------------------------------ Plugins ---------------------------------------- @@ -549,6 +522,48 @@ local style = require "core.style" end +function core.write_init_project_module(init_filename) + local init_file = io.open(init_filename, "w") + if not init_file then error("cannot create file: \"" .. init_filename .. "\"") end + init_file:write([[ +-- Put project's module settings here. +-- This module will be loaded when opening a project, after the user module +-- configuration. +-- It will be automatically reloaded when saved. + +local config = require "core.config" + +-- you can add some patterns to ignore files within the project +-- config.ignore_files = {"^%.", } + +-- Patterns are normally applied to the file's or directory's name, without +-- its path. See below about how to apply filters on a path. +-- +-- Here some examples: +-- +-- "^%." match any file of directory whose basename begins with a dot. +-- +-- When there is an '/' or a '/$' at the end the pattern it will only match +-- directories. When using such a pattern a final '/' will be added to the name +-- of any directory entry before checking if it matches. +-- +-- "^%.git/" matches any directory named ".git" anywhere in the project. +-- +-- If a "/" appears anywhere in the pattern except if it appears at the end or +-- is immediately followed by a '$' then the pattern will be applied to the full +-- path of the file or directory. An initial "/" will be prepended to the file's +-- or directory's path to indicate the project's root. +-- +-- "^/node_modules/" will match a directory named "node_modules" at the project's root. +-- "^/build.*/" match any top level directory whose name begins with "build" +-- "^/subprojects/.+/" match any directory inside a top-level folder named "subprojects". + +-- You may activate some plugins on a pre-project base to override the user's settings. +-- config.plugins.trimwitespace = true +]]) + init_file:close() +end + function core.load_user_directory() return core.try(function() @@ -578,23 +593,53 @@ function core.remove_project_directory(path) return false end -local function reload_on_user_module_save() + +function core.configure_borderless_window() + system.set_window_bordered(not config.borderless) + core.title_view:configure_hit_test(config.borderless) + core.title_view.visible = config.borderless +end + + +local function add_config_files_hooks() -- auto-realod style when user's module is saved by overriding Doc:Save() local doc_save = Doc.save local user_filename = system.absolute_path(USERDIR .. PATHSEP .. "init.lua") function Doc:save(filename, abs_filename) + local module_filename = system.absolute_path(".lite_project.lua") doc_save(self, filename, abs_filename) - if self.abs_filename == user_filename then - core.reload_module("core.style") - core.load_user_directory() + if self.abs_filename == user_filename or self.abs_filename == module_filename then + reload_customizations() + core.rescan_project_directories() + core.configure_borderless_window() end end end +-- The function below works like system.absolute_path except it +-- doesn't fail if the file does not exist. We consider that the +-- current dir is core.project_dir so relative filename are considered +-- to be in core.project_dir. +-- Please note that .. or . in the filename are not taken into account. +-- This function should get only filenames normalized using +-- common.normalize_path function. +function core.project_absolute_path(filename) + if common.is_absolute_path(filename) then + return common.normalize_path(filename) + elseif not core.project_dir then + local cwd = system.absolute_path(".") + return cwd .. PATHSEP .. common.normalize_path(filename) + else + return core.project_dir .. PATHSEP .. filename + end +end + + function core.init() command = require "core.command" keymap = require "core.keymap" + dirwatch = require "core.dirwatch" RootView = require "core.rootview" StatusView = require "core.statusview" TitleView = require "core.titleview" @@ -624,23 +669,20 @@ function core.init() local project_dir = core.recent_projects[1] or "." local project_dir_explicit = false local files = {} - local delayed_error for i = 2, #ARGS do local arg_filename = strip_trailing_slash(ARGS[i]) local info = system.get_file_info(arg_filename) or {} - if info.type == "file" then - local file_abs = system.absolute_path(arg_filename) - if file_abs then - table.insert(files, file_abs) - project_dir = file_abs:match("^(.+)[/\\].+$") - end - elseif info.type == "dir" then + if info.type == "dir" then project_dir = arg_filename project_dir_explicit = true else -- on macOS we can get an argument like "-psn_0_52353" that we just ignore. if not ARGS[i]:match("^-psn") then - delayed_error = string.format("error: invalid file or directory %q", ARGS[i]) + local file_abs = core.project_absolute_path(arg_filename) + if file_abs then + table.insert(files, file_abs) + project_dir = file_abs:match("^(.+)[/\\].+$") + end end end end @@ -649,39 +691,30 @@ function core.init() core.clip_rect_stack = {{ 0,0,0,0 }} core.log_items = {} core.docs = {} + core.cursor_clipboard = {} + core.cursor_clipboard_whole_line = {} core.window_mode = "normal" core.threads = setmetatable({}, { __mode = "k" }) core.blink_start = system.get_time() core.blink_timer = core.blink_start - - local project_dir_abs = system.absolute_path(project_dir) - local set_project_ok = project_dir_abs and core.set_project_dir(project_dir_abs) - if set_project_ok then - if project_dir_explicit then - update_recents_project("add", project_dir_abs) - end - else - if not project_dir_explicit then - update_recents_project("remove", project_dir) - end - project_dir_abs = system.absolute_path(".") - if not core.set_project_dir(project_dir_abs) then - system.show_fatal_error("Lite XL internal error", "cannot set project directory to cwd") - os.exit(1) - end - end - core.redraw = true core.visited_files = {} core.restart_request = false core.quit_request = false + -- We load core views before plugins that may need them. + ---@type core.rootview core.root_view = RootView() + ---@type core.commandview core.command_view = CommandView() + ---@type core.statusview core.status_view = StatusView() + ---@type core.nagview core.nag_view = NagView() + ---@type core.titleview core.title_view = TitleView() + -- Some plugins (eg: console) require the nodes to be initialized to defaults local cur_node = core.root_view.root_node cur_node.is_primary_node = true cur_node:split("up", core.title_view, {y = true}) @@ -691,15 +724,44 @@ function core.init() cur_node = cur_node:split("down", core.command_view, {y = true}) cur_node = cur_node:split("down", core.status_view, {y = true}) + -- Load defaiult commands first so plugins can override them command.add_defaults() - local got_user_error = not core.load_user_directory() + + -- Load user module, plugins and project module + local got_user_error, got_project_error = not core.load_user_directory() + + local project_dir_abs = system.absolute_path(project_dir) + -- We prevent set_project_dir below to effectively add and scan the directory because the + -- project module and its ignore files is not yet loaded. + local set_project_ok = project_dir_abs and core.set_project_dir(project_dir_abs) + if set_project_ok then + got_project_error = not core.load_project_module() + if project_dir_explicit then + update_recents_project("add", project_dir_abs) + end + else + if not project_dir_explicit then + update_recents_project("remove", project_dir) + end + project_dir_abs = system.absolute_path(".") + if not core.set_project_dir(project_dir_abs, function() + got_project_error = not core.load_project_module() + end) then + system.show_fatal_error("Lite XL internal error", "cannot set project directory to cwd") + os.exit(1) + end + end + + -- Load core plugins after user ones to let the user override them local plugins_success, plugins_refuse_list = core.load_plugins() do local pdir, pname = project_dir_abs:match("(.*)[:/\\\\](.*)") core.log("Opening project %q from directory %s", pname, pdir) end - local got_project_error = not core.load_project_module() + + -- We add the project directory now because the project's module is loaded. + core.add_project_directory(project_dir_abs) -- We assume we have just a single project directory here. Now that StatusView -- is there show max files warning if needed. @@ -711,17 +773,11 @@ function core.init() core.root_view:open_doc(core.open_doc(filename)) end - if delayed_error then - core.error(delayed_error) - end - if not plugins_success or got_user_error or got_project_error then command.perform("core:open-log") end - system.set_window_bordered(not config.borderless) - core.title_view:configure_hit_test(config.borderless) - core.title_view.visible = config.borderless + core.configure_borderless_window() if #plugins_refuse_list.userdir.plugins > 0 or #plugins_refuse_list.datadir.plugins > 0 then local opt = { @@ -745,7 +801,7 @@ function core.init() end) end - reload_on_user_module_save() + add_config_files_hooks() end @@ -778,21 +834,23 @@ function core.confirm_close_docs(docs, close_fn, ...) end end -local temp_uid = (system.get_time() * 1000) % 0xffffffff -local temp_file_prefix = string.format(".lite_temp_%08x", temp_uid) +local temp_uid = math.floor(system.get_time() * 1000) % 0xffffffff +local temp_file_prefix = string.format(".lite_temp_%08x", tonumber(temp_uid)) local temp_file_counter = 0 -local function delete_temp_files() - for _, filename in ipairs(system.list_dir(EXEDIR)) do +function core.delete_temp_files(dir) + dir = type(dir) == "string" and common.normalize_path(dir) or USERDIR + for _, filename in ipairs(system.list_dir(dir) or {}) do if filename:find(temp_file_prefix, 1, true) == 1 then - os.remove(EXEDIR .. PATHSEP .. filename) + os.remove(dir .. PATHSEP .. filename) end end end -function core.temp_filename(ext) +function core.temp_filename(ext, dir) + dir = type(dir) == "string" and common.normalize_path(dir) or USERDIR temp_file_counter = temp_file_counter + 1 - return USERDIR .. PATHSEP .. temp_file_prefix + return dir .. PATHSEP .. temp_file_prefix .. string.format("%06x", temp_file_counter) .. (ext or "") end @@ -807,7 +865,7 @@ end local function quit_with_function(quit_fn, force) if force then - delete_temp_files() + core.delete_temp_files() core.on_quit_project() save_session() quit_fn() @@ -826,7 +884,7 @@ function core.restart() end -local function check_plugin_version(filename) +local function get_plugin_details(filename) local info = system.get_file_info(filename) if info ~= nil and info.type == "dir" then filename = filename .. "/init.lua" @@ -835,24 +893,28 @@ local function check_plugin_version(filename) if not info or not filename:match("%.lua$") then return false end local f = io.open(filename, "r") if not f then return false end + local priority = false local version_match = false for line in f:lines() do - local mod_version = line:match('%-%-.*%f[%a]mod%-version%s*:%s*(%d+)') - if mod_version then - version_match = (mod_version == MOD_VERSION) - break + if not version_match then + local mod_version = line:match('%-%-.*%f[%a]mod%-version%s*:%s*(%d+)') + if mod_version then + version_match = (mod_version == MOD_VERSION) + end end - -- The following pattern is used for backward compatibility only - -- Future versions will look only at the mod-version tag. - local version = line:match('%-%-%s*lite%-xl%s*(%d+%.%d+)$') - if version then - -- we consider the version tag 2.0 equivalent to mod-version:2 - version_match = (version == '2.0' and MOD_VERSION == "2") + if not priority then + priority = line:match('%-%-.*%f[%a]priority%s*:%s*(%d+)') + if priority then priority = tonumber(priority) end + end + if version_match then break end end f:close() - return true, version_match + return true, { + version_match = version_match, + priority = priority or 100 + } end @@ -866,30 +928,70 @@ function core.load_plugins() for _, root_dir in ipairs {DATADIR, USERDIR} do local plugin_dir = root_dir .. "/plugins" for _, filename in ipairs(system.list_dir(plugin_dir) or {}) do - if not files[filename] then table.insert(ordered, filename) end - files[filename] = plugin_dir -- user plugins will always replace system plugins + if not files[filename] then + table.insert( + ordered, {file = filename} + ) + end + -- user plugins will always replace system plugins + files[filename] = plugin_dir end end - table.sort(ordered) - for _, filename in ipairs(ordered) do - local plugin_dir, basename = files[filename], filename:match("(.-)%.lua$") or filename - local is_lua_file, version_match = check_plugin_version(plugin_dir .. '/' .. filename) - if is_lua_file then - if not version_match then - core.log_quiet("Version mismatch for plugin %q from %s", basename, plugin_dir) - local list = refused_list[plugin_dir:find(USERDIR, 1, true) == 1 and 'userdir' or 'datadir'].plugins - table.insert(list, filename) - end - if version_match and config.plugins[basename] ~= false then - local ok = core.try(require, "plugins." .. basename) - if ok then core.log_quiet("Loaded plugin %q from %s", basename, plugin_dir) end + for _, plugin in ipairs(ordered) do + local dir = files[plugin.file] + local name = plugin.file:match("(.-)%.lua$") or plugin.file + local is_lua_file, details = get_plugin_details(dir .. '/' .. plugin.file) + + plugin.valid = is_lua_file + plugin.name = name + plugin.dir = dir + plugin.priority = details and details.priority or 100 + plugin.version_match = details and details.version_match or false + end + + -- sort by priority or name for plugins that have same priority + table.sort(ordered, function(a, b) + if a.priority ~= b.priority then + return a.priority < b.priority + end + return a.name < b.name + end) + + local load_start = system.get_time() + for _, plugin in ipairs(ordered) do + if plugin.valid then + if not config.skip_plugins_version and not plugin.version_match then + core.log_quiet( + "Version mismatch for plugin %q from %s", + plugin.name, + plugin.dir + ) + local rlist = plugin.dir:find(USERDIR, 1, true) == 1 + and 'userdir' or 'datadir' + local list = refused_list[rlist].plugins + table.insert(list, plugin.file) + elseif config.plugins[plugin.name] ~= false then + local start = system.get_time() + local ok = core.try(require, "plugins." .. plugin.name) + if ok then + core.log_quiet( + "Loaded plugin %q from %s in %.1fms", + plugin.name, + plugin.dir, + (system.get_time() - start) * 1000 + ) + end if not ok then no_errors = false end end end end + core.log_quiet( + "Loaded all plugins in %.1fms", + (system.get_time() - load_start) * 1000 + ) return no_errors, refused_list end @@ -952,9 +1054,10 @@ function core.show_title_bar(show) end -function core.add_thread(f, weak_ref) +function core.add_thread(f, weak_ref, ...) local key = weak_ref or #core.threads + 1 - local fn = function() return core.try(f) end + local args = {...} + local fn = function() return core.try(f, table.unpack(args)) end core.threads[key] = { cr = coroutine.create(fn), wake = 0 } return key end @@ -987,22 +1090,6 @@ function core.normalize_to_project_dir(filename) end --- The function below works like system.absolute_path except it --- doesn't fail if the file does not exist. We consider that the --- current dir is core.project_dir so relative filename are considered --- to be in core.project_dir. --- Please note that .. or . in the filename are not taken into account. --- This function should get only filenames normalized using --- common.normalize_path function. -function core.project_absolute_path(filename) - if filename:match('^%a:\\') or filename:match('^%w*:') or filename:find('/', 1, true) == 1 then - return filename - else - return core.project_dir .. PATHSEP .. filename - end -end - - function core.open_doc(filename) local new_file = not filename or not system.get_file_info(filename) local abs_filename @@ -1035,15 +1122,22 @@ function core.get_views_referencing_doc(doc) end -local function log(icon, icon_color, fmt, ...) +function core.custom_log(level, show, backtrace, fmt, ...) local text = string.format(fmt, ...) - if icon then - core.status_view:show_message(icon, icon_color, text) + if show then + local s = style.log[level] + core.status_view:show_message(s.icon, s.color, text) end local info = debug.getinfo(2, "Sl") local at = string.format("%s:%d", info.short_src, info.currentline) - local item = { text = text, time = os.time(), at = at } + local item = { + level = level, + text = text, + time = os.time(), + at = at, + info = backtrace and debug.traceback(nil, 2):gsub("\t", "") + } table.insert(core.log_items, item) if #core.log_items > config.max_log_items then table.remove(core.log_items, 1) @@ -1053,17 +1147,20 @@ end function core.log(...) - return log("i", style.text, ...) + return core.custom_log("INFO", true, false, ...) end function core.log_quiet(...) - return log(nil, nil, ...) + return core.custom_log("INFO", false, false, ...) end +function core.warn(...) + return core.custom_log("WARN", true, true, ...) +end function core.error(...) - return log("!", style.accent, ...) + return core.custom_log("ERROR", true, true, ...) end @@ -1076,7 +1173,7 @@ function core.get_log(i) return table.concat(r, "\n") end local item = type(i) == "number" and core.log_items[i] or i - local text = string.format("[%s] %s at %s", os.date(nil, item.time), item.text, item.at) + local text = string.format("%s [%s] %s at %s", os.date(nil, item.time), item.level, item.text, item.at) if item.info then text = string.format("%s\n%s\n", text, item.info) end @@ -1097,85 +1194,6 @@ function core.try(fn, ...) return false, err end -local scheduled_rescan = {} - -function core.has_pending_rescan() - for _ in pairs(scheduled_rescan) do - return true - end -end - - -function core.dir_rescan_add_job(dir, filepath) - local dirpath = filepath:match("^(.+)[/\\].+$") - local dirpath_rooted = dirpath and PATHSEP .. dirpath or "" - local abs_dirpath = dir.name .. dirpath_rooted - if dirpath then - -- check if the directory is in the project files list, if not exit - local dir_index, dir_match = file_search(dir.files, {filename = dirpath, type = "dir"}) - -- Note that is dir_match is false dir_index greaten than the last valid index. - -- We use dir_index to index dir.files below only if dir_match is true. - if not dir_match or not core.project_subdir_is_shown(dir, dir.files[dir_index].filename) then return end - end - local new_time = system.get_time() + 1 - - -- evaluate new rescan request versus existing rescan - local remove_list = {} - for _, rescan in pairs(scheduled_rescan) do - if abs_dirpath == rescan.abs_path or common.path_belongs_to(abs_dirpath, rescan.abs_path) then - -- abs_dirpath is a subpath of a scan already ongoing: skip - rescan.time_limit = new_time - return - elseif common.path_belongs_to(rescan.abs_path, abs_dirpath) then - -- abs_dirpath already cover this rescan: add to the list of rescan to be removed - table.insert(remove_list, rescan.abs_path) - end - end - for _, key_path in ipairs(remove_list) do - scheduled_rescan[key_path] = nil - end - - scheduled_rescan[abs_dirpath] = {dir = dir, path = dirpath_rooted, abs_path = abs_dirpath, time_limit = new_time} - core.add_thread(function() - while true do - local rescan = scheduled_rescan[abs_dirpath] - if not rescan then return end - if system.get_time() > rescan.time_limit then - local has_changes = rescan_project_subdir(rescan.dir, rescan.path) - if has_changes then - core.redraw = true -- we run without an event, from a thread - rescan.time_limit = new_time - else - scheduled_rescan[rescan.abs_path] = nil - return - end - end - coroutine.yield(0.2) - end - end) -end - - --- no-op but can be overrided by plugins -function core.on_dirmonitor_modify(dir, filepath) -end - - -function core.on_dir_change(watch_id, action, filepath) - local dir = project_dir_by_watch_id(watch_id) - if not dir then return end - core.dir_rescan_add_job(dir, filepath) - if action == "delete" then - project_scan_remove_file(dir, filepath) - elseif action == "create" then - project_scan_add_file(dir, filepath) - core.on_dirmonitor_modify(dir, filepath); - elseif action == "modify" then - core.on_dirmonitor_modify(dir, filepath); - end -end - - function core.on_event(type, ...) local did_keymap = false if type == "textinput" then @@ -1192,6 +1210,8 @@ function core.on_event(type, ...) end elseif type == "mousereleased" then core.root_view:on_mouse_released(...) + elseif type == "mouseleft" then + core.root_view:on_mouse_left() elseif type == "mousewheel" then if not core.root_view:on_mouse_wheel(...) then did_keymap = keymap.on_mouse_wheel(...) @@ -1201,22 +1221,22 @@ function core.on_event(type, ...) elseif type == "minimized" or type == "maximized" or type == "restored" then core.window_mode = type == "restored" and "normal" or type elseif type == "filedropped" then - local filename, mx, my = ... - local info = system.get_file_info(filename) - if info and info.type == "dir" then - system.exec(string.format("%q %q", EXEFILE, filename)) - else - local ok, doc = core.try(core.open_doc, filename) - if ok then - local node = core.root_view.root_node:get_child_overlapping_point(mx, my) - node:set_active_view(node.active_view) - core.root_view:open_doc(doc) + if not core.root_view:on_file_dropped(...) then + local filename, mx, my = ... + local info = system.get_file_info(filename) + if info and info.type == "dir" then + system.exec(string.format("%q %q", EXEFILE, filename)) + else + local ok, doc = core.try(core.open_doc, filename) + if ok then + local node = core.root_view.root_node:get_child_overlapping_point(mx, my) + node:set_active_view(node.active_view) + core.root_view:open_doc(doc) + end end end elseif type == "focuslost" then core.root_view:on_focus_lost(...) - elseif type == "dirchange" then - core.on_dir_change(...) elseif type == "quit" then core.quit() end @@ -1226,12 +1246,13 @@ end local function get_title_filename(view) local doc_filename = view.get_filename and view:get_filename() or view:get_name() - return (doc_filename ~= "---") and doc_filename or "" + if doc_filename ~= "---" then return doc_filename end + return "" end function core.compose_window_title(title) - return title == "" and "Lite XL" or title .. " - Lite XL" + return (title == "" or title == nil) and "Lite XL" or title .. " - Lite XL" end @@ -1270,7 +1291,7 @@ function core.step() -- update window title local current_title = get_title_filename(core.active_view) - if current_title ~= core.window_title then + if current_title ~= nil and current_title ~= core.window_title then system.set_window_title(core.compose_window_title(current_title)) core.window_title = current_title end @@ -1322,8 +1343,8 @@ function core.run() local idle_iterations = 0 while true do core.frame_start = system.get_time() + local need_more_work = run_threads() local did_redraw = core.step() - local need_more_work = run_threads() or core.has_pending_rescan() if core.restart_request or core.quit_request then break end if not did_redraw and not need_more_work then idle_iterations = idle_iterations + 1 diff --git a/data/core/keymap.lua b/data/core/keymap.lua index bb33d9c4..1e082146 100644 --- a/data/core/keymap.lua +++ b/data/core/keymap.lua @@ -1,9 +1,25 @@ +local core = require "core" local command = require "core.command" local config = require "core.config" local keymap = {} +---@alias keymap.shortcut string +---@alias keymap.command string +---@alias keymap.modkey string +---@alias keymap.pressed boolean +---@alias keymap.map table +---@alias keymap.rmap table + +---Pressed status of mod keys. +---@type table keymap.modkeys = {} + +---List of commands assigned to a shortcut been the key of the map the shortcut. +---@type keymap.map keymap.map = {} + +---List of shortcuts assigned to a command been the key of the map the command. +---@type keymap.rmap keymap.reverse_map = {} local macos = PLATFORM == "Mac OS X" @@ -12,25 +28,88 @@ local mos = PLATFORM == "MORPHOS" -- Thanks to mathewmariani, taken from his lite-macos github repository. local modkeys_os = require("core.modkeys-" .. (macos and "macos" or os4 and "os4" or mos and "mos" or "generic")) + +---@type table local modkey_map = modkeys_os.map + +---@type keymap.modkey[] local modkeys = modkeys_os.keys -local function key_to_stroke(k) + +---Generates a stroke sequence including currently pressed mod keys. +---@param key string +---@return string +local function key_to_stroke(key) local stroke = "" for _, mk in ipairs(modkeys) do if keymap.modkeys[mk] then stroke = stroke .. mk .. "+" end end - return stroke .. k + return stroke .. key end +---Remove the given value from an array associated to a key in a table. +---@param tbl table The table containing the key +---@param k string The key containing the array +---@param v? string The value to remove from the array +local function remove_only(tbl, k, v) + if tbl[k] then + if v then + local j = 0 + for i=1, #tbl[k] do + while tbl[k][i + j] == v do + j = j + 1 + end + tbl[k][i] = tbl[k][i + j] + end + else + tbl[k] = nil + end + end +end + + +---Removes from a keymap.map the bindings that are already registered. +---@param map keymap.map +local function remove_duplicates(map) + for stroke, commands in pairs(map) do + if type(commands) == "string" or type(commands) == "function" then + commands = { commands } + end + if keymap.map[stroke] then + for _, registered_cmd in ipairs(keymap.map[stroke]) do + local j = 0 + for i=1, #commands do + while commands[i + j] == registered_cmd do + j = j + 1 + end + commands[i] = commands[i + j] + end + end + end + if #commands < 1 then + map[stroke] = nil + else + map[stroke] = commands + end + end +end + + +---Add bindings by replacing commands that were previously assigned to a shortcut. +---@param map keymap.map function keymap.add_direct(map) for stroke, commands in pairs(map) do - if type(commands) == "string" then + if type(commands) == "string" or type(commands) == "function" then commands = { commands } end + if keymap.map[stroke] then + for _, cmd in ipairs(keymap.map[stroke]) do + remove_only(keymap.reverse_map, cmd, stroke) + end + end keymap.map[stroke] = commands for _, cmd in ipairs(commands) do keymap.reverse_map[cmd] = keymap.reverse_map[cmd] or {} @@ -39,15 +118,23 @@ function keymap.add_direct(map) end end + +---Adds bindings by appending commands to already registered shortcut or by +---replacing currently assigned commands if overwrite is specified. +---@param map keymap.map +---@param overwrite? boolean function keymap.add(map, overwrite) + remove_duplicates(map) for stroke, commands in pairs(map) do if macos then stroke = stroke:gsub("%f[%a]ctrl%f[%A]", "cmd") end - if type(commands) == "string" then - commands = { commands } - end if overwrite then + if keymap.map[stroke] then + for _, cmd in ipairs(keymap.map[stroke]) do + remove_only(keymap.reverse_map, cmd, stroke) + end + end keymap.map[stroke] = commands else keymap.map[stroke] = keymap.map[stroke] or {} @@ -63,35 +150,34 @@ function keymap.add(map, overwrite) end -local function remove_only(tbl, k, v) - for key, values in pairs(tbl) do - if key == k then - if v then - for i, value in ipairs(values) do - if value == v then - table.remove(values, i) - end - end - else - tbl[key] = nil - end - break - end - end -end - - -function keymap.unbind(key, cmd) - remove_only(keymap.map, key, cmd) - remove_only(keymap.reverse_map, cmd, key) +---Unregisters the given shortcut and associated command. +---@param shortcut string +---@param cmd string +function keymap.unbind(shortcut, cmd) + remove_only(keymap.map, shortcut, cmd) + remove_only(keymap.reverse_map, cmd, shortcut) end +---Returns all the shortcuts associated to a command unpacked for easy assignment. +---@param cmd string +---@return ... function keymap.get_binding(cmd) return table.unpack(keymap.reverse_map[cmd] or {}) end +---Returns all the shortcuts associated to a command packed in a table. +---@param cmd string +---@return table | nil shortcuts +function keymap.get_bindings(cmd) + return keymap.reverse_map[cmd] +end + + +-------------------------------------------------------------------------------- +-- Events listening +-------------------------------------------------------------------------------- function keymap.on_key_pressed(k, ...) local mk = modkey_map[k] if mk then @@ -102,10 +188,19 @@ function keymap.on_key_pressed(k, ...) end else local stroke = key_to_stroke(k) - local commands, performed = keymap.map[stroke] + local commands, performed = keymap.map[stroke], false if commands then for _, cmd in ipairs(commands) do - performed = command.perform(cmd, ...) + if type(cmd) == "function" then + local ok, res = core.try(cmd, ...) + if ok then + performed = not (res == false) + else + performed = true + end + else + performed = command.perform(cmd, ...) + end if performed then break end end return performed @@ -135,6 +230,9 @@ function keymap.on_key_released(k) end +-------------------------------------------------------------------------------- +-- Register default bindings +-------------------------------------------------------------------------------- if macos then local keymap_macos = require("core.keymap-macos") keymap_macos(keymap) @@ -148,7 +246,7 @@ keymap.add_direct { ["ctrl+n"] = "core:new-doc", ["ctrl+shift+c"] = "core:change-project-folder", ["ctrl+shift+o"] = "core:open-project-folder", - ["ctrl+shift+r"] = "core:restart", + ["ctrl+alt+r"] = "core:restart", ["alt+return"] = "core:toggle-fullscreen", ["f11"] = "core:toggle-fullscreen", @@ -217,6 +315,7 @@ keymap.add_direct { ["ctrl+l"] = "doc:select-lines", ["ctrl+shift+l"] = { "find-replace:select-add-all", "doc:select-word" }, ["ctrl+/"] = "doc:toggle-line-comments", + ["ctrl+shift+/"] = "doc:toggle-block-comments", ["ctrl+up"] = "doc:move-lines-up", ["ctrl+down"] = "doc:move-lines-down", ["ctrl+shift+d"] = "doc:duplicate-lines", diff --git a/data/core/logview.lua b/data/core/logview.lua index 1ea0e43e..15aec22e 100644 --- a/data/core/logview.lua +++ b/data/core/logview.lua @@ -1,5 +1,7 @@ local core = require "core" local common = require "core.common" +local config = require "core.config" +local keymap = require "core.keymap" local style = require "core.style" local View = require "core.view" @@ -36,12 +38,15 @@ local LogView = View:extend() LogView.context = "session" + function LogView:new() LogView.super.new(self) self.last_item = core.log_items[#core.log_items] self.expanding = {} self.scrollable = true self.yoffset = 0 + + core.status_view:show_message("i", style.text, "ctrl+click to copy entry") end @@ -77,25 +82,43 @@ function LogView:each_item() end -function LogView:on_mouse_moved(px, py, ...) - LogView.super.on_mouse_moved(self, px, py, ...) - local hovered = false - for _, item, x, y, w, h in self:each_item() do - if px >= x and py >= y and px < x + w and py < y + h then - hovered = true - self.hovered_item = item - break - end +function LogView:get_scrollable_size() + local _, y_off = self:get_content_offset() + local last_y, last_h = 0, 0 + for i, item, x, y, w, h in self:each_item() do + last_y, last_h = y, h end - if not hovered then self.hovered_item = nil end + if not config.scroll_past_end then + return last_y + last_h - y_off + style.padding.y + end + return last_y + self.size.y - y_off end -function LogView:on_mouse_pressed(button, mx, my, clicks) - if LogView.super.on_mouse_pressed(self, button, mx, my, clicks) then return end - if self.hovered_item then - self:expand_item(self.hovered_item) +function LogView:on_mouse_pressed(button, px, py, clicks) + if LogView.super.on_mouse_pressed(self, button, px, py, clicks) then + return true end + + local index, selected + for i, item, x, y, w, h in self:each_item() do + if px >= x and py >= y and px < x + w and py < y + h then + index = i + selected = item + break + end + end + + if selected then + if keymap.modkeys["ctrl"] then + system.set_clipboard(core.get_log(selected)) + core.status_view:show_message("i", style.text, "copied entry #"..index.." to clipboard.") + else + self:expand_item(selected) + end + end + + return true end @@ -109,13 +132,13 @@ function LogView:update() local expanding = self.expanding[1] if expanding then - self:move_towards(expanding, "current", expanding.target) + self:move_towards(expanding, "current", expanding.target, nil, "logview") if expanding.current == expanding.target then table.remove(self.expanding, 1) end end - self:move_towards("yoffset", 0) + self:move_towards("yoffset", 0, nil, "logview") LogView.super.update(self) end @@ -131,41 +154,62 @@ local function draw_text_multiline(font, text, x, y, color) return resx, y end - +-- this is just to get a date string that's consistent +local datestr = os.date() function LogView:draw() self:draw_background(style.background) local th = style.font:get_height() local lh = th + style.padding.y -- for one line - for _, item, x, y, w in self:each_item() do - x = x + style.padding.x + local iw = math.max( + style.icon_font:get_width(style.log.ERROR.icon), + style.icon_font:get_width(style.log.INFO.icon) + ) - local time = os.date(nil, item.time) - x = common.draw_text(style.font, style.dim, time, "left", x, y, w, lh) - x = x + style.padding.x + local tw = style.font:get_width(datestr) + for _, item, x, y, w, h in self:each_item() do + if y + h >= self.position.y and y <= self.position.y + self.size.y then + core.push_clip_rect(x, y, w, h) + x = x + style.padding.x - x = common.draw_text(style.code_font, style.dim, is_expanded(item) and "-" or "+", "left", x, y, w, lh) - x = x + style.padding.x - w = w - (x - self:get_content_offset()) + x = common.draw_text( + style.icon_font, + style.log[item.level].color, + style.log[item.level].icon, + "center", + x, y, iw, lh + ) + x = x + style.padding.x - if is_expanded(item) then - y = y + common.round(style.padding.y / 2) - _, y = draw_text_multiline(style.font, item.text, x, y, style.text) + -- timestamps are always 15% of the width + local time = os.date(nil, item.time) + common.draw_text(style.font, style.dim, time, "left", x, y, tw, lh) + x = x + tw + style.padding.x - local at = "at " .. common.home_encode(item.at) - _, y = common.draw_text(style.font, style.dim, at, "left", x, y, w, lh) + w = w - (x - self:get_content_offset()) - if item.info then - _, y = draw_text_multiline(style.font, item.info, x, y, style.dim) + if is_expanded(item) then + y = y + common.round(style.padding.y / 2) + _, y = draw_text_multiline(style.font, item.text, x, y, style.text) + + local at = "at " .. common.home_encode(item.at) + _, y = common.draw_text(style.font, style.dim, at, "left", x, y, w, lh) + + if item.info then + _, y = draw_text_multiline(style.font, item.info, x, y, style.dim) + end + else + local line, has_newline = string.match(item.text, "([^\n]+)(\n?)") + if has_newline ~= "" then + line = line .. " ..." + end + _, y = common.draw_text(style.font, style.text, line, "left", x, y, w, lh) end - else - local line, has_newline = string.match(item.text, "([^\n]+)(\n?)") - if has_newline ~= "" then - line = line .. " ..." - end - _, y = common.draw_text(style.font, style.text, line, "left", x, y, w, lh) + + core.pop_clip_rect() end end + LogView.super.draw_scrollbar(self) end diff --git a/data/core/nagview.lua b/data/core/nagview.lua index 3d448cd4..9c373f58 100644 --- a/data/core/nagview.lua +++ b/data/core/nagview.lua @@ -11,13 +11,19 @@ local UNDERLINE_MARGIN = common.round(1 * SCALE) local noop = function() end +---@class core.nagview : core.view +---@field super core.view local NagView = View:extend() function NagView:new() NagView.super.new(self) self.size.y = 0 + self.show_height = 0 self.force_focus = false self.queue = {} + self.scrollable = true + self.target_height = 0 + self.on_mouse_pressed_root = nil end function NagView:get_title() @@ -46,20 +52,20 @@ function NagView:get_target_height() return self.target_height + 2 * style.padding.y end -function NagView:update() - NagView.super.update(self) - - if core.active_view == self and self.title then - self:move_towards(self.size, "y", self:get_target_height()) - self:move_towards(self, "underline_progress", 1) +function NagView:get_scrollable_size() + local w, h = system.get_window_size() + if self.visible and self:get_target_height() > h then + self.size.y = h + return self:get_target_height() else - self:move_towards(self.size, "y", 0) + self.size.y = 0 end + return 0 end -function NagView:draw_overlay() +function NagView:dim_window_content() local ox, oy = self:get_content_offset() - oy = oy + self.size.y + oy = oy + self.show_height local w, h = core.root_view.size.x, core.root_view.size.y - oy core.root_view:defer_draw(function() renderer.draw_rect(ox, oy, w, h, style.nagbar_dim) @@ -81,7 +87,7 @@ function NagView:each_option() bh = self:get_buttons_height() ox,oy = self:get_content_offset() ox = ox + self.size.x - oy = oy + self.size.y - bh - style.padding.y + oy = oy + self.show_height - bh - style.padding.y for i = #self.options, 1, -1 do opt = self.options[i] @@ -94,6 +100,8 @@ function NagView:each_option() end function NagView:on_mouse_moved(mx, my, ...) + if not self.visible then return end + core.set_active_view(self) NagView.super.on_mouse_moved(self, mx, my, ...) for i, _, x,y,w,h in self:each_option() do if mx >= x and my >= y and mx < x + w and my < y + h then @@ -103,18 +111,55 @@ function NagView:on_mouse_moved(mx, my, ...) end end +local function register_mouse_pressed(self) + if self.on_mouse_pressed_root then return end + -- RootView is loaded locally to avoid NagView and RootView being + -- mutually recursive + local RootView = require "core.rootview" + self.on_mouse_pressed_root = RootView.on_mouse_pressed + local this = self + function RootView:on_mouse_pressed(button, x, y, clicks) + if + not this:on_mouse_pressed(button, x, y, clicks) + then + return this.on_mouse_pressed_root(self, button, x, y, clicks) + else + return true + end + end + self.new_on_mouse_pressed_root = RootView.on_mouse_pressed +end + +local function unregister_mouse_pressed(self) + local RootView = require "core.rootview" + if + self.on_mouse_pressed_root + and + -- just in case prevent overwriting what something else may + -- have overwrote after us, but after testing with various + -- plugins this doesn't seems to happen, but just in case + self.new_on_mouse_pressed_root == RootView.on_mouse_pressed + then + RootView.on_mouse_pressed = self.on_mouse_pressed_root + self.on_mouse_pressed_root = nil + self.new_on_mouse_pressed_root = nil + end +end + function NagView:on_mouse_pressed(button, mx, my, clicks) - if NagView.super.on_mouse_pressed(self, button, mx, my, clicks) then return end + if not self.visible then return false end + if NagView.super.on_mouse_pressed(self, button, mx, my, clicks) then return true end for i, _, x,y,w,h in self:each_option() do if mx >= x and my >= y and mx < x + w and my < y + h then self:change_hovered(i) command.perform "dialog:select" - break end end + return true end function NagView:on_text_input(text) + if not self.visible then return end if text:lower() == "y" then command.perform "dialog:select-yes" elseif text:lower() == "n" then @@ -122,20 +167,39 @@ function NagView:on_text_input(text) end end +function NagView:update() + if not self.visible and self.show_height <= 0 then return end + NagView.super.update(self) -function NagView:draw() - if self.size.y <= 0 or not self.title then return end + if self.visible and core.active_view == self and self.title then + self:move_towards(self, "show_height", self:get_target_height(), nil, "nagbar") + self:move_towards(self, "underline_progress", 1, nil, "nagbar") + else + self:move_towards(self, "show_height", 0, nil, "nagbar") + if self.show_height <= 0 then + self.title = nil + self.message = nil + self.options = nil + self.on_selected = nil + end + end +end - self:draw_overlay() - self:draw_background(style.nagbar) +local function draw_nagview_message(self) + self:dim_window_content() + -- draw message's background local ox, oy = self:get_content_offset() + renderer.draw_rect(ox, oy, self.size.x, self.show_height, style.nagbar) + ox = ox + style.padding.x + core.push_clip_rect(ox, oy, self.size.x, self.show_height) + -- if there are other items, show it if #self.queue > 0 then local str = string.format("[%d]", #self.queue) - ox = common.draw_text(style.font, style.nagbar_text, str, "left", ox, oy, self.size.x, self.size.y) + ox = common.draw_text(style.font, style.nagbar_text, str, "left", ox, oy, self.size.x, self.show_height) ox = ox + style.padding.x end @@ -168,6 +232,17 @@ function NagView:draw() common.draw_text(opt.font, style.nagbar_text, opt.text, "center", fx,fy,fw,fh) end + + self:draw_scrollbar() + + core.pop_clip_rect() +end + +function NagView:draw() + if (not self.visible and self.show_height <= 0) or not self.title then + return + end + core.root_view:defer_draw(draw_nagview_message, self) end function NagView:get_message_height() @@ -178,23 +253,31 @@ function NagView:get_message_height() return h end - function NagView:next() local opts = table.remove(self.queue, 1) or {} - self.title = opts.title - self.message = opts.message and opts.message .. "\n" - self.options = opts.options - self.on_selected = opts.on_selected - if self.message and self.options then + if opts.title and opts.message and opts.options then + self.visible = true + self.title = opts.title + self.message = opts.message and opts.message .. "\n" + self.options = opts.options + self.on_selected = opts.on_selected + local message_height = self:get_message_height() -- self.target_height is the nagview height needed to display the message and -- the buttons, excluding the top and bottom padding space. self.target_height = math.max(message_height, self:get_buttons_height()) self:change_hovered(common.find_index(self.options, "default_yes")) + + self.force_focus = true + core.set_active_view(self) + -- We add a hook to manage all the mouse_pressed events. + register_mouse_pressed(self) + else + self.force_focus = false + core.set_active_view(core.next_active_view or core.last_active_view) + self.visible = false + unregister_mouse_pressed(self) end - self.force_focus = self.message ~= nil - core.set_active_view(self.message ~= nil and self or - core.next_active_view or core.last_active_view) end function NagView:show(title, message, options, on_select) @@ -204,7 +287,7 @@ function NagView:show(title, message, options, on_select) opts.options = assert(options, "No options") opts.on_selected = on_select or noop table.insert(self.queue, opts) - if #self.queue > 0 and not self.title then self:next() end + self:next() end return NagView diff --git a/data/core/node.lua b/data/core/node.lua index bced052d..087610a6 100644 --- a/data/core/node.lua +++ b/data/core/node.lua @@ -6,6 +6,7 @@ local Object = require "core.object" local EmptyView = require "core.emptyview" local View = require "core.view" +---@class core.node : core.object local Node = Object:extend() function Node:new(type) @@ -51,6 +52,15 @@ function Node:on_mouse_released(...) end +function Node:on_mouse_left() + if self.type == "leaf" then + self.active_view:on_mouse_left() + else + self:propagate("on_mouse_left") + end +end + + function Node:consume(node) for k, _ in pairs(self) do self[k] = nil end for k, v in pairs(node) do self[k] = v end @@ -160,8 +170,12 @@ end function Node:set_active_view(view) assert(self.type == "leaf", "Tried to set active view on non-leaf node") + local last_active_view = self.active_view self.active_view = view core.set_active_view(view) + if last_active_view and last_active_view ~= view then + last_active_view:on_mouse_left() + end end @@ -260,8 +274,8 @@ end local function close_button_location(x, w) local cw = style.icon_font:get_width("C") - local pad = style.padding.y - return x + w - pad - cw, cw, pad + local pad = style.padding.x / 2 + return x + w - cw - pad, cw, pad end @@ -468,59 +482,67 @@ function Node:update() end self:tab_hovered_update(self.hovered.x, self.hovered.y) local tab_width = self:target_tab_width() - self:move_towards("tab_shift", tab_width * (self.tab_offset - 1)) - self:move_towards("tab_width", tab_width) + self:move_towards("tab_shift", tab_width * (self.tab_offset - 1), nil, "tabs") + self:move_towards("tab_width", tab_width, nil, "tabs") else self.a:update() self.b:update() end end -function Node:draw_tab(text, is_active, is_hovered, is_close_hovered, x, y, w, h, standalone) +function Node:draw_tab_title(view, font, is_active, is_hovered, x, y, w, h) + local text = view and view:get_name() or "" + local dots_width = font:get_width("…") + local align = "center" + if font:get_width(text) > w then + align = "left" + for i = 1, #text do + local reduced_text = text:sub(1, #text - i) + if font:get_width(reduced_text) + dots_width <= w then + text = reduced_text .. "…" + break + end + end + end + local color = style.dim + if is_active then color = style.text end + if is_hovered then color = style.text end + common.draw_text(font, color, text, align, x, y, w, h) +end + +function Node:draw_tab_borders(view, is_active, is_hovered, x, y, w, h, standalone) + -- Tabs deviders local ds = style.divider_size - local dots_width = style.font:get_width("…") local color = style.dim local padding_y = style.padding.y - renderer.draw_rect(x + w, y + padding_y, ds, h - padding_y * 2, style.dim) + renderer.draw_rect(x + w, y + padding_y, ds, h - padding_y*2, style.dim) if standalone then renderer.draw_rect(x-1, y-1, w+2, h+2, style.background2) end + -- Full border if is_active then color = style.text renderer.draw_rect(x, y, w, h, style.background) renderer.draw_rect(x + w, y, ds, h, style.divider) renderer.draw_rect(x - ds, y, ds, h, style.divider) end - local cx, cw, cspace = close_button_location(x, w) + return x + ds, y, w - ds*2, h +end + +function Node:draw_tab(view, is_active, is_hovered, is_close_hovered, x, y, w, h, standalone) + x, y, w, h = self:draw_tab_borders(view, is_active, is_hovered, x, y, w, h, standalone) + -- Close button + local cx, cw, cpad = close_button_location(x, w) local show_close_button = ((is_active or is_hovered) and not standalone and config.tab_close_button) if show_close_button then local close_style = is_close_hovered and style.text or style.dim - common.draw_text(style.icon_font, close_style, "C", nil, cx, y, 0, h) + common.draw_text(style.icon_font, close_style, "C", nil, cx, y, cw, h) end - if is_hovered then - color = style.text - end - local padx = style.padding.x - -- Normally we should substract "cspace" from text_avail_width and from the - -- clipping width. It is the padding space we give to the left and right of the - -- close button. However, since we are using dots to terminate filenames, we - -- choose to ignore "cspace" accepting that the text can possibly "touch" the - -- close button. - local text_avail_width = cx - x - padx - core.push_clip_rect(x, y, cx - x, h) - x, w = x + padx, w - padx * 2 - local align = "center" - if style.font:get_width(text) > text_avail_width then - align = "left" - for i = 1, #text do - local reduced_text = text:sub(1, #text - i) - if style.font:get_width(reduced_text) + dots_width <= text_avail_width then - text = reduced_text .. "…" - break - end - end - end - common.draw_text(style.font, color, text, align, x, y, w, h) + -- Title + x = x + cpad + w = cx - x + core.push_clip_rect(x, y, w, h) + self:draw_tab_title(view, style.font, is_active, is_hovered, x, y, w, h) core.pop_clip_rect() end @@ -547,7 +569,7 @@ function Node:draw_tabs() for i = self.tab_offset, self.tab_offset + tabs_number - 1 do local view = self.views[i] local x, y, w, h = self:get_tab_rect(i) - self:draw_tab(view:get_name(), view == self.active_view, + self:draw_tab(view, view == self.active_view, i == self.hovered_tab, i == self.hovered_close, x, y, w, h) end @@ -688,7 +710,7 @@ function Node:get_split_type(mouse_x, mouse_y) local local_mouse_x = mouse_x - x local local_mouse_y = mouse_y - y - + if local_mouse_y < 0 then return "tab" else diff --git a/data/core/object.lua b/data/core/object.lua index 0941ce5d..afd13cdf 100644 --- a/data/core/object.lua +++ b/data/core/object.lua @@ -1,11 +1,12 @@ +---@class core.object +---@field super core.object local Object = {} Object.__index = Object +---Can be overrided by child objects to implement a constructor. +function Object:new() end -function Object:new() -end - - +---@return core.object function Object:extend() local cls = {} for k, v in pairs(self) do @@ -19,8 +20,17 @@ function Object:extend() return cls end - +---Check if the object is strictly of the given type. +---@param T any +---@return boolean function Object:is(T) + return getmetatable(self) == T +end + +---Check if the object inherits from the given type. +---@param T any +---@return boolean +function Object:extends(T) local mt = getmetatable(self) while mt do if mt == T then @@ -31,12 +41,14 @@ function Object:is(T) return false end - +---Metamethod to get a string representation of an object. +---@return string function Object:__tostring() return "Object" end - +---Methamethod to allow using the object call as a constructor. +---@return core.object function Object:__call(...) local obj = setmetatable({}, self) obj:new(...) diff --git a/data/core/regex.lua b/data/core/regex.lua index 637d23fd..fa85d56c 100644 --- a/data/core/regex.lua +++ b/data/core/regex.lua @@ -5,8 +5,9 @@ regex.__index = function(table, key) return regex[key]; end regex.match = function(pattern_string, string, offset, options) local pattern = type(pattern_string) == "table" and pattern_string or regex.compile(pattern_string) - local s, e = regex.cmatch(pattern, string, offset or 1, options or 0) - return s, e and e - 1 + local res = { regex.cmatch(pattern, string, offset or 1, options or 0) } + res[2] = res[2] and res[2] - 1 + return table.unpack(res) end -- Will iterate back through any UTF-8 bytes so that we don't replace bits diff --git a/data/core/rootview.lua b/data/core/rootview.lua index fb735fe3..c4eb656f 100644 --- a/data/core/rootview.lua +++ b/data/core/rootview.lua @@ -5,7 +5,10 @@ local Node = require "core.node" local View = require "core.view" local DocView = require "core.docview" - +---@class core.rootview : core.view +---@field super core.view +---@field root_node core.node +---@field mouse core.view.position local RootView = View:extend() function RootView:new() @@ -29,11 +32,15 @@ function RootView:defer_draw(fn, ...) end +---@return core.node function RootView:get_active_node() - return self.root_node:get_node_for_view(core.active_view) + local node = self.root_node:get_node_for_view(core.active_view) + if not node then node = self:get_primary_node() end + return node end +---@return core.node local function get_primary_node(node) if node.is_primary_node then return node @@ -44,8 +51,10 @@ local function get_primary_node(node) end +---@return core.node function RootView:get_active_node_default() local node = self.root_node:get_node_for_view(core.active_view) + if not node then node = self:get_primary_node() end if node.locked then local default_view = self:get_primary_node().views[1] assert(default_view, "internal error: cannot find original document node.") @@ -56,11 +65,14 @@ function RootView:get_active_node_default() end +---@return core.node function RootView:get_primary_node() return get_primary_node(self.root_node) end +---@param node core.node +---@return core.node local function select_next_primary_node(node) if node.is_primary_node then return end if node.type ~= "leaf" then @@ -74,11 +86,14 @@ local function select_next_primary_node(node) end +---@return core.node function RootView:select_next_primary_node() return select_next_primary_node(self.root_node) end +---@param doc core.doc +---@return core.docview function RootView:open_doc(doc) local node = self:get_active_node_default() for i, view in ipairs(node.views) do @@ -95,17 +110,27 @@ function RootView:open_doc(doc) end +---@param keep_active boolean function RootView:close_all_docviews(keep_active) self.root_node:close_all_docviews(keep_active) end --- Function to intercept mouse pressed events on the active view. --- Do nothing by default. +---Function to intercept mouse pressed events on the active view. +---Do nothing by default. +---@param button core.view.mousebutton +---@param x number +---@param y number +---@param clicks integer function RootView.on_view_mouse_pressed(button, x, y, clicks) end +---@param button core.view.mousebutton +---@param x number +---@param y number +---@param clicks integer +---@return boolean function RootView:on_mouse_pressed(button, x, y, clicks) local div = self.root_node:get_divider_overlapping_point(x, y) local node = self.root_node:get_child_overlapping_point(x, y) @@ -159,6 +184,9 @@ function RootView:set_show_overlay(overlay, status) end +---@param button core.view.mousebutton +---@param x number +---@param y number function RootView:on_mouse_released(button, x, y, ...) if self.dragged_divider then self.dragged_divider = nil @@ -217,6 +245,10 @@ local function resize_child_node(node, axis, value, delta) end +---@param x number +---@param y number +---@param dx number +---@param dy number function RootView:on_mouse_moved(x, y, dx, dy) if core.active_view == core.nag_view then core.request_cursor("arrow") @@ -253,8 +285,13 @@ function RootView:on_mouse_moved(x, y, dx, dy) self.root_node:on_mouse_moved(x, y, dx, dy) + local last_overlapping_node = self.overlapping_node self.overlapping_node = self.root_node:get_child_overlapping_point(x, y) - + + if last_overlapping_node and last_overlapping_node ~= self.overlapping_node then + last_overlapping_node:on_mouse_left() + end + local div = self.root_node:get_divider_overlapping_point(x, y) local tab_index = self.overlapping_node and self.overlapping_node:get_tab_overlapping_point(x, y) if self.overlapping_node and self.overlapping_node:get_scroll_button_index(x, y) then @@ -269,6 +306,23 @@ function RootView:on_mouse_moved(x, y, dx, dy) end +function RootView:on_mouse_left() + if self.overlapping_node then + self.overlapping_node:on_mouse_left() + end +end + + +---@param filename string +---@param x number +---@param y number +---@return boolean +function RootView:on_file_dropped(filename, x, y) + local node = self.root_node:get_child_overlapping_point(x, y) + return node and node.active_view:on_file_dropped(filename, x, y) +end + + function RootView:on_mouse_wheel(...) local x, y = self.mouse.x, self.mouse.y local node = self.root_node:get_child_overlapping_point(x, y) @@ -288,12 +342,12 @@ end function RootView:interpolate_drag_overlay(overlay) - self:move_towards(overlay, "x", overlay.to.x) - self:move_towards(overlay, "y", overlay.to.y) - self:move_towards(overlay, "w", overlay.to.w) - self:move_towards(overlay, "h", overlay.to.h) + self:move_towards(overlay, "x", overlay.to.x, nil, "tab_drag") + self:move_towards(overlay, "y", overlay.to.y, nil, "tab_drag") + self:move_towards(overlay, "w", overlay.to.w, nil, "tab_drag") + self:move_towards(overlay, "h", overlay.to.h, nil, "tab_drag") - self:move_towards(overlay, "opacity", overlay.visible and 100 or 0) + self:move_towards(overlay, "opacity", overlay.visible and 100 or 0, nil, "tab_drag") overlay.color[4] = overlay.base_color[4] * overlay.opacity / 100 end @@ -381,8 +435,8 @@ function RootView:draw_grabbed_tab() local _,_, w, h = dn.node:get_tab_rect(dn.idx) local x = self.mouse.x - w / 2 local y = self.mouse.y - h / 2 - local text = dn.node.views[dn.idx] and dn.node.views[dn.idx]:get_name() or "" - self.root_node:draw_tab(text, true, true, false, x, y, w, h, true) + local view = dn.node.views[dn.idx] + self.root_node:draw_tab(view, true, true, false, x, y, w, h, true) end diff --git a/data/core/start.lua b/data/core/start.lua index 63e77536..e01ac912 100644 --- a/data/core/start.lua +++ b/data/core/start.lua @@ -1,6 +1,6 @@ -- this file is used by lite-xl to setup the Lua environment when starting -VERSION = "2.0.3r3" -MOD_VERSION = "2" +VERSION = "2.0.5r1" +MOD_VERSION = "3" SCALE = tonumber(os.getenv("LITE_SCALE") or os.getenv("GDK_SCALE") or os.getenv("QT_SCALE_FACTOR")) or SCALE PATHSEP = package.config:sub(1, 1) @@ -12,16 +12,26 @@ else local prefix = EXEDIR:match("^(.+)[/\\]bin$") DATADIR = prefix and (prefix .. '/share/lite-xl') or (EXEDIR .. '/data') end -USERDIR = (os.getenv("XDG_CONFIG_HOME") and os.getenv("XDG_CONFIG_HOME") .. "/lite-xl") - or (HOME and (HOME .. '/.config/lite-xl') or (EXEDIR .. '/user')) +USERDIR = (system.get_file_info(EXEDIR .. '/user') and (EXEDIR .. '/user')) + or ((os.getenv("XDG_CONFIG_HOME") and os.getenv("XDG_CONFIG_HOME") .. "/lite-xl")) + or (HOME and (HOME .. '/.config/lite-xl')) -package.path = DATADIR .. '/?.lua;' .. package.path +package.path = DATADIR .. '/?.lua;' package.path = DATADIR .. '/?/init.lua;' .. package.path package.path = USERDIR .. '/?.lua;' .. package.path package.path = USERDIR .. '/?/init.lua;' .. package.path -local dynamic_suffix = PLATFORM == "Mac OS X" and 'lib' or (PLATFORM == "Windows" and 'dll' or 'so') -package.cpath = DATADIR .. '/?.' .. dynamic_suffix .. ";" .. USERDIR .. '/?.' .. dynamic_suffix +local suffix = PLATFORM == "Mac OS X" and 'lib' or (PLATFORM == "Windows" and 'dll' or 'so') +package.cpath = + USERDIR .. '/?.' .. ARCH .. "." .. suffix .. ";" .. + USERDIR .. '/?/init.' .. ARCH .. "." .. suffix .. ";" .. + USERDIR .. '/?.' .. suffix .. ";" .. + USERDIR .. '/?/init.' .. suffix .. ";" .. + DATADIR .. '/?.' .. ARCH .. "." .. suffix .. ";" .. + DATADIR .. '/?/init.' .. ARCH .. "." .. suffix .. ";" .. + DATADIR .. '/?.' .. suffix .. ";" .. + DATADIR .. '/?/init.' .. suffix .. ";" + package.native_plugins = {} package.searchers = { package.searchers[1], package.searchers[2], function(modname) local path = package.searchpath(modname, package.cpath) @@ -32,3 +42,15 @@ end } table.pack = table.pack or pack or function(...) return {...} end table.unpack = table.unpack or unpack +bit32 = bit32 or require "core.bit" + +require "core.utf8string" + +-- Because AppImages change the working directory before running the executable, +-- we need to change it back to the original one. +-- https://github.com/AppImage/AppImageKit/issues/172 +-- https://github.com/AppImage/AppImageKit/pull/191 +local appimage_owd = os.getenv("OWD") +if os.getenv("APPIMAGE") and appimage_owd then + system.chdir(appimage_owd) +end diff --git a/data/core/statusview.lua b/data/core/statusview.lua index 330fcc76..48ce24cf 100644 --- a/data/core/statusview.lua +++ b/data/core/statusview.lua @@ -4,37 +4,515 @@ local command = require "core.command" local config = require "core.config" local style = require "core.style" local DocView = require "core.docview" +local CommandView = require "core.commandview" local LogView = require "core.logview" local View = require "core.view" local Object = require "core.object" +---@alias core.statusview.styledtext table + +---A status bar implementation for lite, check core.status_view. +---@class core.statusview : core.view +---@field public super core.view +---@field private items core.statusview.item[] +---@field private active_items core.statusview.item[] +---@field private hovered_item core.statusview.item +---@field private message_timeout number +---@field private message core.statusview.styledtext +---@field private tooltip_mode boolean +---@field private tooltip core.statusview.styledtext +---@field private left_width number +---@field private right_width number +---@field private r_left_width number +---@field private r_right_width number +---@field private left_xoffset number +---@field private right_xoffset number +---@field private dragged_panel '"left"' | '"right"' +---@field private hovered_panel '"left"' | '"right"' +---@field private hide_messages boolean local StatusView = View:extend() +---Space separator +---@type string StatusView.separator = " " + +---Pipe separator +---@type string StatusView.separator2 = " | " +---@alias core.statusview.item.separator +---|>'core.statusview.separator' # Space separator +---| 'core.statusview.separator2' # Pipe separator +---@alias core.statusview.item.predicate fun():boolean +---@alias core.statusview.item.onclick fun(button: string, x: number, y: number) +---@alias core.statusview.item.get_item fun():core.statusview.styledtext,core.statusview.styledtext +---@alias core.statusview.item.ondraw fun(x, y, h, hovered: boolean, calc_only?: boolean):number + +---@class core.statusview.item : core.object +---@field name string +---@field predicate core.statusview.item.predicate +---@field alignment core.statusview.item.alignment +---@field tooltip string | nil +---@field command string | nil @Command to perform when the item is clicked. +---@field on_click core.statusview.item.onclick | nil @Function called when item is clicked and no command is set. +---@field on_draw core.statusview.item.ondraw | nil @Custom drawing that when passed calc true should return the needed width for drawing and when false should draw. +---@field background_color renderer.color | nil +---@field background_color_hover renderer.color | nil +---@field visible boolean +---@field separator core.statusview.item.separator +---@field private active boolean +---@field private x number +---@field private w number +---@field private cached_item core.statusview.styledtext +local StatusViewItem = Object:extend() + + +---Available StatusViewItem options. +---@class core.statusview.item.options : table +---@field predicate string | table | core.statusview.item.predicate +---@field name string +---@field alignment core.statusview.item.alignment +---@field get_item core.statusview.item.get_item +---@field command? string | core.statusview.item.onclick +---@field position? integer +---@field tooltip? string +---@field separator? core.statusview.item.separator +local StatusViewItemOptions = { + ---A condition to evaluate if the item should be displayed. If a string + ---is given it is treated as a require import that should return a valid object + ---which is checked against the current active view, the sames applies if a + ---table is given. A function that returns a boolean can be used instead to + ---perform a custom evaluation, setting to nil means always evaluates to true. + predicate = nil, + ---A unique name to identify the item on the status bar. + name = nil, + alignment = nil, + ---A function that should return a core.statusview.styledtext element, + ---returning empty table is allowed. + get_item = nil, + ---The name of a valid registered command or a callback function to execute + ---when the item is clicked. + command = nil, + ---The position in which to insert the given item on the internal table, + ---a value of -1 inserts the item at the end which is the default. A value + ---of 1 will insert the item at the beggining. + position = nil, + ---Displayed when mouse hovers the item + tooltip = nil, + separator = nil, +} + +StatusViewItem.options = StatusViewItemOptions + +---Flag to tell the item should me aligned on left side of status bar. +---@type number +StatusViewItem.LEFT = 1 + +---Flag to tell the item should me aligned on right side of status bar. +---@type number +StatusViewItem.RIGHT = 2 + +---@alias core.statusview.item.alignment +---|>'core.statusview.item.LEFT' +---| 'core.statusview.item.RIGHT' + +---Constructor +---@param options core.statusview.item.options +function StatusViewItem:new(options) + self:set_predicate(options.predicate) + self.name = options.name + self.alignment = options.alignment or StatusView.Item.LEFT + self.command = type(options.command) == "string" and options.command or nil + self.tooltip = options.tooltip or "" + self.on_click = type(options.command) == "function" and options.command or nil + self.on_draw = nil + self.background_color = nil + self.background_color_hover = nil + self.visible = options.visible == nil and true or options.visible + self.active = false + self.x = 0 + self.w = 0 + self.separator = options.separator or StatusView.separator + self.get_item = options.get_item +end + +---Called by the status bar each time that the item needs to be rendered, +---if on_draw() is set this function is obviated. +---@return core.statusview.styledtext +function StatusViewItem:get_item() return {} end + +---Do not show the item on the status bar. +function StatusViewItem:hide() self.visible = false end + +---Show the item on the status bar. +function StatusViewItem:show() self.visible = true end + +---A condition to evaluate if the item should be displayed. If a string +---is given it is treated as a require import that should return a valid object +---which is checked against the current active view, the sames applies if a +---table is given. A function that returns a boolean can be used instead to +---perform a custom evaluation, setting to nil means always evaluates to true. +---@param predicate string | table | core.statusview.item.predicate +function StatusViewItem:set_predicate(predicate) + self.predicate = command.generate_predicate(predicate) +end + +---@type core.statusview.item +StatusView.Item = StatusViewItem + + +---Predicated used on the default docview widgets. +---@return boolean +local function predicate_docview() + return core.active_view:is(DocView) + and not core.active_view:is(CommandView) +end + + +---Constructor function StatusView:new() StatusView.super.new(self) self.message_timeout = 0 self.message = {} self.tooltip_mode = false self.tooltip = {} + self.items = {} + self.active_items = {} + self.hovered_item = {} + self.pointer = {x = 0, y = 0} + self.left_width = 0 + self.right_width = 0 + self.r_left_width = 0 + self.r_right_width = 0 + self.left_xoffset = 0 + self.right_xoffset = 0 + self.dragged_panel = "" + self.hovered_panel = "" + self.hide_messages = false + self.visible = true + + self:register_docview_items() + self:register_command_items() +end + +---The predefined status bar items displayed when a document view is active. +function StatusView:register_docview_items() + if self:get_item("doc:file") then return end + + self:add_item({ + predicate = predicate_docview, + name = "doc:file", + alignment = StatusView.Item.LEFT, + get_item = function() + local dv = core.active_view + return { + dv.doc:is_dirty() and style.accent or style.text, style.icon_font, "f", + style.dim, style.font, self.separator2, style.text, + dv.doc.filename and style.text or style.dim, dv.doc:get_name() + } + end + }) + + self:add_item({ + predicate = predicate_docview, + name = "doc:position", + alignment = StatusView.Item.LEFT, + get_item = function() + local dv = core.active_view + local line, col = dv.doc:get_selection() + local _, indent_size = dv.doc:get_indent_info() + -- Calculating tabs when the doc is using the "hard" indent type. + local ntabs = 0 + local last_idx = 0 + while last_idx < col do + local s, e = string.find(dv.doc.lines[line], "\t", last_idx, true) + if s and s < col then + ntabs = ntabs + 1 + last_idx = e + 1 + else + break + end + end + col = col + ntabs * (indent_size - 1) + return { + style.text, line, ":", + col > config.line_limit and style.accent or style.text, col, + style.text, + self.separator, + string.format("%.f%%", line / #dv.doc.lines * 100) + } + end, + command = "doc:go-to-line", + tooltip = "line : column" + }) + + self:add_item({ + predicate = predicate_docview, + name = "doc:indentation", + alignment = StatusView.Item.RIGHT, + get_item = function() + local dv = core.active_view + local indent_type, indent_size, indent_confirmed = dv.doc:get_indent_info() + local indent_label = (indent_type == "hard") and "tabs: " or "spaces: " + return { + style.text, indent_label, indent_size, + indent_confirmed and "" or "*" + } + end, + command = function(button, x, y) + if button == "left" then + command.perform "indent:set-file-indent-size" + elseif button == "right" then + command.perform "indent:set-file-indent-type" + end + end, + separator = self.separator2 + }) + + self:add_item({ + predicate = predicate_docview, + name = "doc:lines", + alignment = StatusView.Item.RIGHT, + get_item = function() + local dv = core.active_view + return { + style.text, + style.icon_font, "g", + style.font, style.dim, self.separator2, + style.text, #dv.doc.lines, " lines", + } + end, + separator = self.separator2 + }) + + self:add_item({ + predicate = predicate_docview, + name = "doc:line-ending", + alignment = StatusView.Item.RIGHT, + get_item = function() + local dv = core.active_view + return { + style.text, dv.doc.crlf and "CRLF" or "LF" + } + end, + command = "doc:toggle-line-ending" + }) end -function StatusView:on_mouse_pressed() - core.set_active_view(core.last_active_view) - if system.get_time() < self.message_timeout - and not core.active_view:is(LogView) then - command.perform "core:open-log" +---The predefined status bar items displayed when a command view is active. +function StatusView:register_command_items() + if self:get_item("command:files") then return end + + self:add_item({ + predicate = "core.commandview", + name = "command:files", + alignment = StatusView.Item.RIGHT, + get_item = function() + return { + style.icon_font, "g", + style.font, style.dim, self.separator2, + style.text, #core.docs, style.text, " / ", + #core.project_files, " files" + } + end + }) +end + + +---Set a position to the best match according to total available items. +---@param self core.statusview +---@param position integer +---@param alignment core.statusview.item.alignment +---@return integer position +local function normalize_position(self, position, alignment) + local offset = 0 + local items_count = 0 + local left = self:get_items_list(1) + local right = self:get_items_list(2) + if alignment == 2 then + items_count = #right + offset = #left + else + items_count = #left end - return true + if position == 0 then + position = offset + 1 + elseif position < 0 then + position = offset + items_count + (position + 2) + else + position = offset + position + end + if position < 1 then + position = offset + 1 + elseif position > #left + #right then + position = offset + items_count + 1 + end + return position end +---Adds an item to be rendered in the status bar. +---@param options core.statusview.item.options +---@return core.statusview.item +function StatusView:add_item(options) + assert(self:get_item(options.name) == nil, "status item already exists: " .. options.name) + ---@type core.statusview.item + local item = StatusView.Item(options) + table.insert(self.items, normalize_position(self, options.position or -1, options.alignment), item) + return item +end + + +---Get an item object associated to a name or nil if not found. +---@param name string +---@return core.statusview.item | nil +function StatusView:get_item(name) + for _, item in ipairs(self.items) do + if item.name == name then return item end + end + return nil +end + + +---Get a list of items. +---@param alignment? core.statusview.item.alignment +---@return core.statusview.item[] +function StatusView:get_items_list(alignment) + if alignment then + local items = {} + for _, item in ipairs(self.items) do + if item.alignment == alignment then + table.insert(items, item) + end + end + return items + end + return self.items +end + + +---Move an item to a different position. +---@param name string +---@param position integer Can be negative value to position in reverse order +---@param alignment? core.statusview.item.alignment +---@return boolean moved +function StatusView:move_item(name, position, alignment) + assert(name, "no name provided") + assert(position, "no position provided") + local item = nil + for pos, it in ipairs(self.items) do + if it.name == name then + item = table.remove(self.items, pos) + break + end + end + if item then + if alignment then + item.alignment = alignment + end + position = normalize_position(self, position, item.alignment) + table.insert(self.items, position, item) + return true + end + return false +end + + +---Remove an item from the status view. +---@param name string +---@return core.statusview.item removed_item +function StatusView:remove_item(name) + local item = nil + for pos, it in ipairs(self.items) do + if it.name == name then + item = table.remove(self.items, pos) + break + end + end + return item +end + + +---Order the items by name +---@param names table +function StatusView:order_items(names) + local removed_items = {} + for _, name in ipairs(names) do + local item = self:remove_item(name) + if item then table.insert(removed_items, item) end + end + + for i, item in ipairs(removed_items) do + table.insert(self.items, i, item) + end +end + + +---Hide the status bar +function StatusView:hide() + self.visible = false +end + + +---Show the status bar +function StatusView:show() + self.visible = true +end + + +---Toggle the visibility of the status bar +function StatusView:toggle() + self.visible = not self.visible +end + + +---Hides the given items from the status view or all if no names given. +---@param names table | string | nil +function StatusView:hide_items(names) + if type(names) == "string" then + names = {names} + end + if not names then + for _, item in ipairs(self.items) do + item:hide() + end + return + end + for _, name in ipairs(names) do + local item = self:get_item(name) + if item then item:hide() end + end +end + + +---Shows the given items from the status view or all if no names given. +---@param names table | string | nil +function StatusView:show_items(names) + if type(names) == "string" then + names = {names} + end + if not names then + for _, item in ipairs(self.items) do + item:show() + end + return + end + for _, name in ipairs(names) do + local item = self:get_item(name) + if item then item:show() end + end +end + + +---Shows a message for a predefined amount of time. +---@param icon string +---@param icon_color renderer.color +---@param text string function StatusView:show_message(icon, icon_color, text) + if not self.visible or self.hide_messages then return end self.message = { icon_color, style.icon_font, icon, style.dim, style.font, StatusView.separator2, style.text, text @@ -43,30 +521,34 @@ function StatusView:show_message(icon, icon_color, text) end +---Enable or disable system wide messages on the status bar. +---@param enable boolean +function StatusView:display_messages(enable) + self.hide_messages = not enable +end + + +---Activates tooltip mode displaying only the given +---text until core.statusview:remove_tooltip() is called. +---@param text string | core.statusview.styledtext function StatusView:show_tooltip(text) - self.tooltip = { text } + self.tooltip = type(text) == "table" and text or { text } self.tooltip_mode = true end +---Deactivates tooltip mode. function StatusView:remove_tooltip() self.tooltip_mode = false end -function StatusView:update() - self.size.y = style.font:get_height() + style.padding.y * 2 - - if system.get_time() < self.message_timeout then - self.scroll.to.y = self.size.y - else - self.scroll.to.y = 0 - end - - StatusView.super.update(self) -end - - +---Helper function to draw the styled text. +---@param self core.statusview +---@param items core.statusview.styledtext +---@param x number +---@param y number +---@param draw_fn fun(font,color,text,align, x,y,w,h):number local function draw_items(self, items, x, y, draw_fn) local font = style.font local color = style.text @@ -85,13 +567,24 @@ local function draw_items(self, items, x, y, draw_fn) end +---Helper function to calculate the width of text by using it as part of +---the helper function draw_items(). +---@param font renderer.font +---@param text string +---@param x number local function text_width(font, _, text, _, x) return x + font:get_width(text) end -function StatusView:draw_items(items, right_align, yoffset) +---Draws a table of styled text on the status bar starting on the left or right. +---@param items core.statusview.styledtext +---@param right_align? boolean +---@param xoffset? number +---@param yoffset? number +function StatusView:draw_items(items, right_align, xoffset, yoffset) local x, y = self:get_content_offset() + x = x + (xoffset or 0) y = y + (yoffset or 0) if right_align then local w = draw_items(self, items, 0, 0, text_width) @@ -104,62 +597,581 @@ function StatusView:draw_items(items, right_align, yoffset) end -function StatusView:get_items() - if getmetatable(core.active_view) == DocView then - local dv = core.active_view - local line, col = dv.doc:get_selection() - local dirty = dv.doc:is_dirty() - local indent_type, indent_size, indent_confirmed = dv.doc:get_indent_info() - local indent_label = (indent_type == "hard") and "tabs: " or "spaces: " - local indent_size_str = tostring(indent_size) .. (indent_confirmed and "" or "*") or "unknown" +---Draw the tooltip of a given status bar item. +---@param item core.statusview.item +function StatusView:draw_item_tooltip(item) + core.root_view:defer_draw(function() + local text = item.tooltip + local w = style.font:get_width(text) + local h = style.font:get_height() + local x = self.pointer.x - (w / 2) - (style.padding.x * 2) - return { - dirty and style.accent or style.text, style.icon_font, "f", - style.dim, style.font, self.separator2, style.text, - dv.doc.filename and style.text or style.dim, dv.doc:get_name(), - style.text, - self.separator, - "line: ", line, - self.separator, - col > config.line_limit and style.accent or style.text, "col: ", col, - style.text, - self.separator, - string.format("%d%%", line / #dv.doc.lines * 100), - }, { - style.text, indent_label, indent_size, - style.dim, self.separator2, style.text, - style.icon_font, "g", - style.font, style.dim, self.separator2, style.text, - #dv.doc.lines, " lines", - self.separator, - dv.doc.crlf and "CRLF" or "LF" - } + if x < 0 then x = 0 end + if x + w + (style.padding.x * 2) > self.size.x then + x = self.size.x - w - (style.padding.x * 2) + end + + renderer.draw_rect( + x + style.padding.x, + self.position.y - h - (style.padding.y * 2), + w + (style.padding.x * 2), + h + (style.padding.y * 2), + style.background3 + ) + + renderer.draw_text( + style.font, + text, + x + (style.padding.x * 2), + self.position.y - h - style.padding.y, + style.text + ) + end) +end + + +---Older method of retrieving the status bar items and which is now +---deprecated in favour of core.status_view:add_item(). +---@deprecated +---@param nowarn boolean +---@return table left +---@return table right +function StatusView:get_items(nowarn) + if not nowarn and not self.get_items_warn then + core.warn( + "Overriding StatusView:get_items() is deprecated, " + .. "use core.status_view:add_item() instead." + ) + self.get_items_warn = true + end + return {"{:dummy:}"}, {"{:dummy:}"} +end + + +---Helper function to copy a styled text table into another. +---@param t1 core.statusview.styledtext +---@param t2 core.statusview.styledtext +local function table_add(t1, t2) + for _, value in ipairs(t2) do + table.insert(t1, value) + end +end + + +---Helper function to merge deprecated items to a temp items table. +---@param destination table +---@param items core.statusview.styledtext +---@param alignment core.statusview.item.alignment +local function merge_deprecated_items(destination, items, alignment) + local start = true + local items_start, items_end = {}, {} + for i, value in ipairs(items) do + if value ~= "{:dummy:}" then + if start then + table.insert(items_start, i, value) + else + table.insert(items_end, value) + end + else + start = false + end end - return {}, { - style.icon_font, "g", - style.font, style.dim, self.separator2, - #core.docs, style.text, " / ", - #core.project_files, " files" + local position = alignment == StatusView.Item.LEFT and "left" or "right" + + local item_start = StatusView.Item({ + name = "deprecated:"..position.."-start", + alignment = alignment, + get_item = items_start + }) + + local item_end = StatusView.Item({ + name = "deprecated:"..position.."-end", + alignment = alignment, + get_item = items_end + }) + + table.insert(destination, 1, item_start) + table.insert(destination, item_end) +end + + +---Append a space item into the given items list. +---@param self core.statusview +---@param destination core.statusview.item[] +---@param separator string +---@param alignment core.statusview.item.alignment +---@return core.statusview.item +local function add_spacing(self, destination, separator, alignment, x) + ---@type core.statusview.item + local space = StatusView.Item({name = "space", alignment = alignment}) + space.cached_item = separator == self.separator and { + style.text, separator + } or { + style.dim, separator } + space.x = x + space.w = draw_items(self, space.cached_item, 0, 0, text_width) + + table.insert(destination, space) + + return space +end + + +---Remove starting and ending separators. +---@param self core.statusview +---@param styled_text core.statusview.styledtext +local function remove_spacing(self, styled_text) + if + not Object.is(styled_text[1], renderer.font) + and + type(styled_text[1]) == "table" + and + ( + styled_text[2] == self.separator + or + styled_text[2] == self.separator2 + ) + then + table.remove(styled_text, 1) + table.remove(styled_text, 1) + end + + if + not Object.is(styled_text[#styled_text-1], renderer.font) + and + type(styled_text[#styled_text-1]) == "table" + and + ( + styled_text[#styled_text] == self.separator + or + styled_text[#styled_text] == self.separator2 + ) + then + table.remove(styled_text, #styled_text) + table.remove(styled_text, #styled_text) + end +end + + +---Set the active items that will be displayed on the left or right side +---of the status bar checking their predicates and performing positioning +---calculations for proper functioning of tooltips and clicks. +function StatusView:update_active_items() + local x = self:get_content_offset() + + local rx = x + self.size.x + local lx = x + local rw, lw = 0, 0 + + self.active_items = {} + + ---@type core.statusview.item[] + local combined_items = {} + table_add(combined_items, self.items) + + -- load deprecated items for compatibility + local dleft, dright = self:get_items(true) + merge_deprecated_items(combined_items, dleft, StatusView.Item.LEFT) + merge_deprecated_items(combined_items, dright, StatusView.Item.RIGHT) + + local lfirst, rfirst = true, true + + -- calculate left and right width + for _, item in ipairs(combined_items) do + item.cached_item = {} + if item.visible and item:predicate() then + local styled_text = type(item.get_item) == "function" + and item.get_item(self) or item.get_item + + if #styled_text > 0 then + remove_spacing(self, styled_text) + end + + if #styled_text > 0 or item.on_draw then + item.active = true + local hovered = self.hovered_item == item + if item.alignment == StatusView.Item.LEFT then + if not lfirst then + local space = add_spacing( + self, self.active_items, item.separator, item.alignment, lx + ) + lw = lw + space.w + lx = lx + space.w + else + lfirst = false + end + item.w = item.on_draw and + item.on_draw(lx, self.position.y, self.size.y, hovered, true) + or + draw_items(self, styled_text, 0, 0, text_width) + item.x = lx + lw = lw + item.w + lx = lx + item.w + else + if not rfirst then + local space = add_spacing( + self, self.active_items, item.separator, item.alignment, rx + ) + rw = rw + space.w + rx = rx + space.w + else + rfirst = false + end + item.w = item.on_draw and + item.on_draw(rx, self.position.y, self.size.y, hovered, true) + or + draw_items(self, styled_text, 0, 0, text_width) + item.x = rx + rw = rw + item.w + rx = rx + item.w + end + item.cached_item = styled_text + table.insert(self.active_items, item) + else + item.active = false + end + else + item.active = false + end + end + + self.r_left_width, self.r_right_width = lw, rw + + -- try to calc best size for left and right + if lw + rw + (style.padding.x * 4) > self.size.x then + if lw + (style.padding.x * 2) < self.size.x / 2 then + rw = self.size.x - lw - (style.padding.x * 3) + if rw > self.r_right_width then + lw = lw + (rw - self.r_right_width) + rw = self.r_right_width + end + elseif rw + (style.padding.x * 2) < self.size.x / 2 then + lw = self.size.x - rw - (style.padding.x * 3) + else + lw = self.size.x / 2 - (style.padding.x + style.padding.x / 2) + rw = self.size.x / 2 - (style.padding.x + style.padding.x / 2) + end + -- reposition left and right offsets when window is resized + if rw >= self.r_right_width then + self.right_xoffset = 0 + elseif rw > self.right_xoffset + self.r_right_width then + self.right_xoffset = rw - self.r_right_width + end + if lw >= self.r_left_width then + self.left_xoffset = 0 + elseif lw > self.left_xoffset + self.r_left_width then + self.left_xoffset = lw - self.r_left_width + end + else + self.left_xoffset = 0 + self.right_xoffset = 0 + end + + self.left_width, self.right_width = lw, rw + + for _, item in ipairs(self.active_items) do + if item.alignment == StatusView.Item.RIGHT then + -- re-calculate x position now that we have the total width + item.x = item.x - rw - (style.padding.x * 2) + end + end +end + + +---Drag the given panel if possible. +---@param panel '"left"' | '"right"' +---@param dx number +function StatusView:drag_panel(panel, dx) + if panel == "left" and self.r_left_width > self.left_width then + local nonvisible_w = self.r_left_width - self.left_width + local new_offset = self.left_xoffset + dx + if new_offset >= 0 - nonvisible_w and new_offset <= 0 then + self.left_xoffset = new_offset + elseif dx < 0 then + self.left_xoffset = 0 - nonvisible_w + else + self.left_xoffset = 0 + end + elseif panel == "right" and self.r_right_width > self.right_width then + local nonvisible_w = self.r_right_width - self.right_width + local new_offset = self.right_xoffset + dx + if new_offset >= 0 - nonvisible_w and new_offset <= 0 then + self.right_xoffset = new_offset + elseif dx < 0 then + self.right_xoffset = 0 - nonvisible_w + else + self.right_xoffset = 0 + end + end +end + + +---Return the currently hovered panel or empty string if none. +---@param x number +---@param y number +---@return string +function StatusView:get_hovered_panel(x, y) + if y >= self.position.y and x <= self.left_width + style.padding.x then + return "left" + else + return "right" + end + return "" +end + + +---@param item core.statusview.item +---@return number x +---@return number w +function StatusView:get_item_visible_area(item) + local item_ox = item.alignment == StatusView.Item.LEFT and + self.left_xoffset or self.right_xoffset + + local item_x = item_ox + item.x + style.padding.x + local item_w = item.w + + if item.alignment == StatusView.Item.LEFT then + if self.left_width - item_x > 0 and self.left_width - item_x < item.w then + item_w = (self.left_width + style.padding.x) - item_x + elseif self.left_width - item_x < 0 then + item_x = 0 + item_w = 0 + end + else + local rx = self.size.x - self.right_width - style.padding.x + if item_x < rx then + if item_x + item.w > rx then + item_x = rx + item_w = (item_x + item.w) - rx + else + item_x = 0 + item_w = 0 + end + end + end + + return item_x, item_w +end + + + +function StatusView:on_mouse_pressed(button, x, y, clicks) + if not self.visible then return end + core.set_active_view(core.last_active_view) + if + system.get_time() < self.message_timeout + and + not core.active_view:is(LogView) + then + command.perform "core:open-log" + else + if y >= self.position.y and button == "left" and clicks == 1 then + self.position.dx = x + if + self.r_left_width > self.left_width + or + self.r_right_width > self.right_width + then + self.dragged_panel = self:get_hovered_panel(x, y) + self.cursor = "hand" + end + end + end + return true +end + + +function StatusView:on_mouse_moved(x, y, dx, dy) + if not self.visible then return end + StatusView.super.on_mouse_moved(self, x, y, dx, dy) + + self.hovered_panel = self:get_hovered_panel(x, y) + + if self.dragged_panel ~= "" then + self:drag_panel(self.dragged_panel, dx) + return + end + + if y < self.position.y or system.get_time() <= self.message_timeout then + self.cursor = "arrow" + self.hovered_item = {} + return + end + + for _, item in ipairs(self.items) do + if + item.visible and item.active + and + (item.command or item.on_click or item.tooltip ~= "") + then + local item_x, item_w = self:get_item_visible_area(item) + + if x > item_x and (item_x + item_w) > x then + self.pointer.x = x + self.pointer.y = y + if self.hovered_item ~= item then + self.hovered_item = item + end + if item.command or item.on_click then + self.cursor = "hand" + end + return + end + end + end + self.cursor = "arrow" + self.hovered_item = {} +end + + +function StatusView:on_mouse_released(button, x, y) + if not self.visible then return end + StatusView.super.on_mouse_released(self, button, x, y) + + if self.dragged_panel ~= "" then + self.dragged_panel = "" + self.cursor = "arrow" + if self.position.dx ~= x then + return + end + end + + if y < self.position.y or not self.hovered_item.active then return end + + local item = self.hovered_item + local item_x, item_w = self:get_item_visible_area(item) + + if x > item_x and (item_x + item_w) > x then + if item.command then + command.perform(item.command) + elseif item.on_click then + item.on_click(button, x, y) + end + end +end + + +function StatusView:on_mouse_wheel(y) + if not self.visible then return end + self:drag_panel(self.hovered_panel, y * self.left_width / 10) +end + + +function StatusView:update() + if not self.visible and self.size.y <= 0 then + return + elseif not self.visible and self.size.y > 0 then + self:move_towards(self.size, "y", 0, nil, "statusbar") + return + end + + local height = style.font:get_height() + style.padding.y * 2; + + if self.size.y + 1 < height then + self:move_towards(self.size, "y", height, nil, "statusbar") + else + self.size.y = height + end + + if system.get_time() < self.message_timeout then + self.scroll.to.y = self.size.y + else + self.scroll.to.y = 0 + end + + StatusView.super.update(self) + + self:update_active_items() +end + + +---Retrieve the hover status and proper background color if any. +---@param self core.statusview +---@param item core.statusview.item +---@return boolean is_hovered +---@return renderer.color | nil color +local function get_item_bg_color(self, item) + local hovered = self.hovered_item == item + + local item_bg = hovered + and item.background_color_hover or item.background_color + + return hovered, item_bg end function StatusView:draw() + if not self.visible and self.size.y <= 0 then return end + self:draw_background(style.background2) - if self.message then - self:draw_items(self.message, false, self.size.y) - end - - if self.tooltip_mode then - self:draw_items(self.tooltip) + if self.message and system.get_time() <= self.message_timeout then + self:draw_items(self.message, false, 0, self.size.y) else - local left, right = self:get_items() - self:draw_items(left) - self:draw_items(right, true) + if self.tooltip_mode then + self:draw_items(self.tooltip) + end + if #self.active_items > 0 then + --- draw left pane + core.push_clip_rect( + 0, self.position.y, + self.left_width + style.padding.x, self.size.y + ) + for _, item in ipairs(self.active_items) do + local item_x = self.left_xoffset + item.x + style.padding.x + local hovered, item_bg = get_item_bg_color(self, item) + if item.alignment == StatusView.Item.LEFT and not self.tooltip_mode then + if type(item_bg) == "table" then + renderer.draw_rect( + item_x, self.position.y, + item.w, self.size.y, item_bg + ) + end + if item.on_draw then + core.push_clip_rect(item_x, self.position.y, item.w, self.size.y) + item.on_draw(item_x, self.position.y, self.size.y, hovered) + core.pop_clip_rect() + else + self:draw_items(item.cached_item, false, item_x - style.padding.x) + end + end + end + core.pop_clip_rect() + + --- draw right pane + core.push_clip_rect( + self.size.x - (self.right_width + style.padding.x), self.position.y, + self.right_width + style.padding.x, self.size.y + ) + for _, item in ipairs(self.active_items) do + local item_x = self.right_xoffset + item.x + style.padding.x + local hovered, item_bg = get_item_bg_color(self, item) + if item.alignment == StatusView.Item.RIGHT then + if type(item_bg) == "table" then + renderer.draw_rect( + item_x, self.position.y, + item.w, self.size.y, item_bg + ) + end + if item.on_draw then + core.push_clip_rect(item_x, self.position.y, item.w, self.size.y) + item.on_draw(item_x, self.position.y, self.size.y, hovered) + core.pop_clip_rect() + else + self:draw_items(item.cached_item, false, item_x - style.padding.x) + end + end + end + core.pop_clip_rect() + + -- draw tooltip + if self.hovered_item.tooltip ~= "" and self.hovered_item.active then + self:draw_item_tooltip(self.hovered_item) + end + end end end - return StatusView diff --git a/data/core/style.lua b/data/core/style.lua index 2c70d3dc..dbe92994 100644 --- a/data/core/style.lua +++ b/data/core/style.lua @@ -4,6 +4,7 @@ local style = {} style.padding = { x = common.round(14 * SCALE), y = common.round(7 * SCALE) } style.divider_size = common.round(1 * SCALE) style.scrollbar_size = common.round(4 * SCALE) +style.expanded_scrollbar_size = common.round(12 * SCALE) style.caret_width = common.round(2 * SCALE) style.tab_width = common.round(170 * SCALE) @@ -27,43 +28,7 @@ style.icon_font = renderer.font.load(DATADIR .. "/fonts/icons.ttf", 16 * SCALE, style.icon_big_font = style.icon_font:copy(23 * SCALE) style.code_font = renderer.font.load(DATADIR .. "/fonts/JetBrainsMono-Regular.ttf", 15 * SCALE) -style.background = { common.color "#2e2e32" } -- Docview -style.background2 = { common.color "#252529" } -- Treeview -style.background3 = { common.color "#252529" } -- Command view -style.text = { common.color "#97979c" } -style.caret = { common.color "#93DDFA" } -style.accent = { common.color "#e1e1e6" } --- style.dim - text color for nonactive tabs, tabs divider, prefix in log and --- search result, hotkeys for context menu and command view -style.dim = { common.color "#525257" } -style.divider = { common.color "#202024" } -- Line between nodes -style.selection = { common.color "#48484f" } -style.line_number = { common.color "#525259" } -style.line_number2 = { common.color "#83838f" } -- With cursor -style.line_highlight = { common.color "#343438" } -style.scrollbar = { common.color "#414146" } -style.scrollbar2 = { common.color "#4b4b52" } -- Hovered -style.nagbar = { common.color "#FF0000" } -style.nagbar_text = { common.color "#FFFFFF" } -style.nagbar_dim = { common.color "rgba(0, 0, 0, 0.45)" } -style.drag_overlay = { common.color "rgba(255,255,255,0.1)" } -style.drag_overlay_tab = { common.color "#93DDFA" } -style.good = { common.color "#72b886" } -style.warn = { common.color "#FFA94D" } -style.error = { common.color "#FF3333" } -style.modified = { common.color "#1c7c9c" } - style.syntax = {} -style.syntax["normal"] = { common.color "#e1e1e6" } -style.syntax["symbol"] = { common.color "#e1e1e6" } -style.syntax["comment"] = { common.color "#676b6f" } -style.syntax["keyword"] = { common.color "#E58AC9" } -- local function end if case -style.syntax["keyword2"] = { common.color "#F77483" } -- self int float -style.syntax["number"] = { common.color "#FFA94D" } -style.syntax["literal"] = { common.color "#FFA94D" } -- true false nil -style.syntax["string"] = { common.color "#f7c95c" } -style.syntax["operator"] = { common.color "#93DDFA" } -- = + - / < > -style.syntax["function"] = { common.color "#93DDFA" } -- This can be used to override fonts per syntax group. -- The syntax highlighter will take existing values from this table and @@ -72,5 +37,7 @@ style.syntax["function"] = { common.color "#93DDFA" } style.syntax_fonts = {} -- style.syntax_fonts["comment"] = renderer.font.load(path_to_font, size_of_font, rendering_options) +style.log = {} + return style diff --git a/data/core/syntax.lua b/data/core/syntax.lua index adecd0cd..89208bce 100644 --- a/data/core/syntax.lua +++ b/data/core/syntax.lua @@ -7,6 +7,24 @@ local plain_text_syntax = { name = "Plain Text", patterns = {}, symbols = {} } function syntax.add(t) + if type(t.space_handling) ~= "boolean" then t.space_handling = true end + + if t.patterns then + -- the rule %s+ gives us a performance gain for the tokenizer in lines with + -- long amounts of consecutive spaces, can be disabled by plugins where it + -- causes conflicts by declaring the table property: space_handling = false + if t.space_handling then + table.insert(t.patterns, { pattern = "%s+", type = "normal" }) + end + + -- this rule gives us additional performance gain by matching every word + -- that was not matched by the syntax patterns as a single token, preventing + -- the tokenizer from iterating over each character individually which is a + -- lot slower since iteration occurs in lua instead of C and adding to that + -- it will also try to match every pattern to a single char (same as spaces) + table.insert(t.patterns, { pattern = "%w+%f[%s]", type = "normal" }) + end + table.insert(syntax.items, t) end diff --git a/data/core/titleview.lua b/data/core/titleview.lua index 9090861c..f9d7a961 100644 --- a/data/core/titleview.lua +++ b/data/core/titleview.lua @@ -17,6 +17,8 @@ local title_commands = { {symbol = "X", action = function() core.quit() end}, } +---@class core.titleview : core.view +---@field super core.view local TitleView = View:extend() local function title_view_height() diff --git a/data/core/tokenizer.lua b/data/core/tokenizer.lua index 5fd8c69f..89364f28 100644 --- a/data/core/tokenizer.lua +++ b/data/core/tokenizer.lua @@ -1,12 +1,15 @@ +local core = require "core" local syntax = require "core.syntax" local common = require "core.common" local tokenizer = {} +local bad_patterns = {} local function push_token(t, type, text) + type = type or "normal" local prev_type = t[#t-1] local prev_text = t[#t] - if prev_type and (prev_type == type or prev_text:find("^%s*$")) then + if prev_type and (prev_type == type or prev_text:ufind("^%s*$")) then t[#t-1] = type t[#t] = prev_text .. text else @@ -38,12 +41,12 @@ local function push_tokens(t, syn, pattern, full_text, find_results) local fin = find_results[i + 1] - 1 local type = pattern.type[i - 2] -- ↑ (i - 2) to convert from [3; n] to [1; n] - local text = full_text:sub(start, fin) + local text = full_text:usub(start, fin) push_token(t, syn.symbols[text] or type, text) end else local start, fin = find_results[1], find_results[2] - local text = full_text:sub(start, fin) + local text = full_text:usub(start, fin) push_token(t, syn.symbols[text] or pattern.type, text) end end @@ -52,12 +55,12 @@ end -- State is a 32-bit number that is four separate bytes, illustrating how many -- differnet delimiters we have open, and which subsyntaxes we have active. -- At most, there are 3 subsyntaxes active at the same time. Beyond that, --- does not support further highlighting. +-- does not support further highlighting. -- You can think of it as a maximum 4 integer (0-255) stack. It always has -- 1 integer in it. Calling `push_subsyntax` increases the stack depth. Calling -- `pop_subsyntax` decreases it. The integers represent the index of a pattern --- that we're following in the syntax. The top of the stack can be any valid +-- that we're following in the syntax. The top of the stack can be any valid -- pattern index, any integer lower in the stack must represent a pattern that -- specifies a subsyntax. @@ -92,6 +95,19 @@ local function retrieve_syntax_state(incoming_syntax, state) return current_syntax, subsyntax_info, current_pattern_idx, current_level end +local function report_bad_pattern(log_fn, syntax, pattern_idx, msg, ...) + if not bad_patterns[syntax] then + bad_patterns[syntax] = { } + end + if bad_patterns[syntax][pattern_idx] then return end + bad_patterns[syntax][pattern_idx] = true + log_fn("Malformed pattern #%d in %s language plugin. " .. msg, + pattern_idx, syntax.name or "unnamed", ...) +end + +---@param incoming_syntax table +---@param text string +---@param state integer function tokenizer.tokenize(incoming_syntax, text, state) local res = {} local i = 1 @@ -102,22 +118,22 @@ function tokenizer.tokenize(incoming_syntax, text, state) state = state or 0 -- incoming_syntax : the parent syntax of the file. - -- state : a 32-bit number representing syntax state (see above) - + -- state : a 32-bit number representing syntax state (see above) + -- current_syntax : the syntax we're currently in. -- subsyntax_info : info about the delimiters of this subsyntax. -- current_pattern_idx: the index of the pattern we're on for this syntax. -- current_level : how many subsyntaxes deep we are. local current_syntax, subsyntax_info, current_pattern_idx, current_level = retrieve_syntax_state(incoming_syntax, state) - + -- Should be used to set the state variable. Don't modify it directly. local function set_subsyntax_pattern_idx(pattern_idx) current_pattern_idx = pattern_idx state = bit32.replace(state, pattern_idx, current_level*8, 8) end - - + + local function push_subsyntax(entering_syntax, pattern_idx) set_subsyntax_pattern_idx(pattern_idx) current_level = current_level + 1 @@ -126,45 +142,90 @@ function tokenizer.tokenize(incoming_syntax, text, state) entering_syntax.syntax or syntax.get(entering_syntax.syntax) current_pattern_idx = 0 end - + local function pop_subsyntax() set_subsyntax_pattern_idx(0) current_level = current_level - 1 set_subsyntax_pattern_idx(0) - current_syntax, subsyntax_info, current_pattern_idx, current_level = + current_syntax, subsyntax_info, current_pattern_idx, current_level = retrieve_syntax_state(incoming_syntax, state) end - + local function find_text(text, p, offset, at_start, close) - local target, res = p.pattern or p.regex, { 1, offset - 1 }, p.regex - local code = type(target) == "table" and target[close and 2 or 1] or target + local target, res = p.pattern or p.regex, { 1, offset - 1 } + local p_idx = close and 2 or 1 + local code = type(target) == "table" and target[p_idx] or target + + if p.whole_line == nil then p.whole_line = { } end + if p.whole_line[p_idx] == nil then + -- Match patterns that start with '^' + p.whole_line[p_idx] = code:umatch("^%^") and true or false + if p.whole_line[p_idx] then + -- Remove '^' from the beginning of the pattern + if type(target) == "table" then + target[p_idx] = code:usub(2) + else + p.pattern = p.pattern and code:usub(2) + p.regex = p.regex and code:usub(2) + end + end + end + if p.regex and type(p.regex) ~= "table" then p._regex = p._regex or regex.compile(p.regex) code = p._regex - end + end + repeat local next = res[2] + 1 - -- go to the start of the next utf-8 character - while text:byte(next) and common.is_utf8_cont(text, next) do - next = next + 1 + -- If the pattern contained '^', allow matching only the whole line + if p.whole_line[p_idx] and next > 1 then + return end - res = p.pattern and { text:find(at_start and "^" .. code or code, next) } - or { regex.match(code, text, next, at_start and regex.ANCHORED or 0) } - if res[1] and close and target[3] then - local count = 0 - for i = res[1] - 1, 1, -1 do - if text:byte(i) ~= target[3]:byte() then break end - count = count + 1 + res = p.pattern and { text:ufind((at_start or p.whole_line[p_idx]) and "^" .. code or code, next) } + or { regex.match(code, text, text:ucharpos(next), (at_start or p.whole_line[p_idx]) and regex.ANCHORED or 0) } + if p.regex and #res > 0 then -- set correct utf8 len for regex result + local char_pos_1 = string.ulen(text:sub(1, res[1])) + local char_pos_2 = char_pos_1 + string.ulen(text:sub(res[1], res[2])) - 1 + -- `regex.match` returns group results as a series of `begin, end` + -- we only want `begin`s + if #res >= 3 then + res[3] = char_pos_1 + string.ulen(text:sub(res[1], res[3])) - 1 end + for i=1,(#res-3) do + local curr = i + 3 + local from = i * 2 + 3 + if from < #res then + res[curr] = char_pos_1 + string.ulen(text:sub(res[1], res[from])) - 1 + else + res[curr] = nil + end + end + res[1] = char_pos_1 + res[2] = char_pos_2 + end + if res[1] and target[3] then -- Check to see if the escaped character is there, -- and if it is not itself escaped. - if count % 2 == 0 then break end + local count = 0 + for i = res[1] - 1, 1, -1 do + if text:ubyte(i) ~= target[3]:ubyte() then break end + count = count + 1 + end + if count % 2 == 0 then + -- The match is not escaped, so confirm it + break + elseif not close then + -- The *open* match is escaped, so avoid it + return + end end until not res[1] or not close or not target[3] return table.unpack(res) end - - while i <= #text do + + local text_len = text:ulen() + while i <= text_len do -- continue trying to match the end pattern of a pair if we have a state set if current_pattern_idx > 0 then local p = current_syntax.patterns[current_pattern_idx] @@ -176,12 +237,12 @@ function tokenizer.tokenize(incoming_syntax, text, state) -- precedence over ending the delimiter in the subsyntax. if subsyntax_info then local ss, se = find_text(text, subsyntax_info, i, false, true) - -- If we find that we end the subsyntax before the + -- If we find that we end the subsyntax before the -- delimiter, push the token, and signal we shouldn't -- treat the bit after as a token to be normally parsed -- (as it's the syntax delimiter). if ss and (s == nil or ss < s) then - push_token(res, p.type, text:sub(i, ss - 1)) + push_token(res, p.type, text:usub(i, ss - 1)) i = ss cont = false end @@ -190,11 +251,11 @@ function tokenizer.tokenize(incoming_syntax, text, state) -- continue on as normal. if cont then if s then - push_token(res, p.type, text:sub(i, e)) + push_token(res, p.type, text:usub(i, e)) set_subsyntax_pattern_idx(0) i = e + 1 else - push_token(res, p.type, text:sub(i)) + push_token(res, p.type, text:usub(i)) break end end @@ -205,7 +266,7 @@ function tokenizer.tokenize(incoming_syntax, text, state) if subsyntax_info then local s, e = find_text(text, subsyntax_info, i, true, true) if s then - push_token(res, subsyntax_info.type, text:sub(i, e)) + push_token(res, subsyntax_info.type, text:usub(i, e)) -- On finding unescaped delimiter, pop it. pop_subsyntax() i = e + 1 @@ -217,6 +278,19 @@ function tokenizer.tokenize(incoming_syntax, text, state) for n, p in ipairs(current_syntax.patterns) do local find_results = { find_text(text, p, i, true, false) } if find_results[1] then + local type_is_table = type(p.type) == "table" + local n_types = type_is_table and #p.type or 1 + if #find_results == 2 and type_is_table then + report_bad_pattern(core.warn, current_syntax, n, + "Token type is a table, but a string was expected.") + p.type = p.type[1] + elseif #find_results - 1 > n_types then + report_bad_pattern(core.error, current_syntax, n, + "Not enough token types: got %d needed %d.", n_types, #find_results - 1) + elseif #find_results - 1 < n_types then + report_bad_pattern(core.warn, current_syntax, n, + "Too many token types: got %d needed %d.", n_types, #find_results - 1) + end -- matched pattern; make and add tokens push_tokens(res, current_syntax, p, text, find_results) -- update state if this was a start|end pattern pair @@ -224,7 +298,7 @@ function tokenizer.tokenize(incoming_syntax, text, state) -- If we have a subsyntax, push that onto the subsyntax stack. if p.syntax then push_subsyntax(p, n) - else + else set_subsyntax_pattern_idx(n) end end @@ -237,13 +311,8 @@ function tokenizer.tokenize(incoming_syntax, text, state) -- consume character if we didn't match if not matched then - local n = 0 - -- reach the next character - while text:byte(i + n + 1) and common.is_utf8_cont(text, i + n + 1) do - n = n + 1 - end - push_token(res, "normal", text:sub(i, i + n)) - i = i + n + 1 + push_token(res, "normal", text:usub(i, i)) + i = i + 1 end end diff --git a/data/core/utf8string.lua b/data/core/utf8string.lua new file mode 100644 index 00000000..a22a0ef6 --- /dev/null +++ b/data/core/utf8string.lua @@ -0,0 +1,32 @@ +-------------------------------------------------------------------------------- +-- inject utf8 functions to strings +-------------------------------------------------------------------------------- + +local utf8 = require "utf8extra" + +string.ubyte = utf8.byte +string.uchar = utf8.char +string.ufind = utf8.find +string.ugmatch = utf8.gmatch +string.ugsub = utf8.gsub +string.ulen = utf8.len +string.ulower = utf8.lower +string.umatch = utf8.match +string.ureverse = utf8.reverse +string.usub = utf8.sub +string.uupper = utf8.upper + +string.uescape = utf8.escape +string.ucharpos = utf8.charpos +string.unext = utf8.next +string.uinsert = utf8.insert +string.uremove = utf8.remove +string.uwidth = utf8.width +string.uwidthindex = utf8.widthindex +string.utitle = utf8.title +string.ufold = utf8.fold +string.uncasecmp = utf8.ncasecmp + +string.uoffset = utf8.offset +string.ucodepoint = utf8.codepoint +string.ucodes = utf8.codes diff --git a/data/core/view.lua b/data/core/view.lua index 4b787d46..04d01230 100644 --- a/data/core/view.lua +++ b/data/core/view.lua @@ -4,7 +4,51 @@ local style = require "core.style" local common = require "core.common" local Object = require "core.object" +---@class core.view.position +---@field x number +---@field y number +---@class core.view.scroll +---@field x number +---@field y number +---@field to core.view.position + +---@class core.view.thumbtrack +---@field thumb number +---@field track number + +---@class core.view.thumbtrackwidth +---@field thumb number +---@field track number +---@field to core.view.thumbtrack + +---@class core.view.scrollbar +---@field x core.view.thumbtrack +---@field y core.view.thumbtrack +---@field w core.view.thumbtrackwidth +---@field h core.view.thumbtrack + +---@class core.view.increment +---@field value number +---@field to number + +---@alias core.view.cursor "'arrow'" | "'ibeam'" | "'sizeh'" | "'sizev'" | "'hand'" + +---@alias core.view.mousebutton "'left'" | "'right'" + +---@alias core.view.context "'application'" | "'session'" + +---Base view. +---@class core.view : core.object +---@field context core.view.context +---@field super core.object +---@field position core.view.position +---@field size core.view.position +---@field scroll core.view.scroll +---@field cursor core.view.cursor +---@field scrollable boolean +---@field scrollbar core.view.scrollbar +---@field scrollbar_alpha core.view.increment local View = Object:extend() -- context can be "application" or "session". The instance of objects @@ -18,14 +62,22 @@ function View:new() self.scroll = { x = 0, y = 0, to = { x = 0, y = 0 } } self.cursor = "arrow" self.scrollable = false + self.scrollbar = { + x = { thumb = 0, track = 0 }, + y = { thumb = 0, track = 0 }, + w = { thumb = 0, track = 0, to = { thumb = 0, track = 0 } }, + h = { thumb = 0, track = 0 }, + } + self.scrollbar_alpha = { value = 0, to = 0 } end -function View:move_towards(t, k, dest, rate) +function View:move_towards(t, k, dest, rate, name) if type(t) ~= "table" then - return self:move_towards(self, t, k, dest, rate) + return self:move_towards(self, t, k, dest, rate, name) end local val = t[k] - if not config.transitions or math.abs(val - dest) < 0.5 then + local diff = math.abs(val - dest) + if not config.transitions or diff < 0.5 or config.disabled_transitions[name] then t[k] = dest else rate = rate or 0.5 @@ -35,7 +87,7 @@ function View:move_towards(t, k, dest, rate) end t[k] = common.lerp(val, dest, rate) end - if val ~= dest then + if diff > 1e-8 then core.redraw = true end end @@ -46,62 +98,146 @@ function View:try_close(do_close) end +---@return string function View:get_name() return "---" end +---@return number function View:get_scrollable_size() return math.huge end +---@return number x +---@return number y +---@return number width +---@return number height +function View:get_scrollbar_track_rect() + local sz = self:get_scrollable_size() + if sz <= self.size.y or sz == math.huge then + return 0, 0, 0, 0 + end + local width = style.scrollbar_size + if self.hovered_scrollbar_track or self.dragging_scrollbar then + width = style.expanded_scrollbar_size + end + return + self.position.x + self.size.x - width, + self.position.y, + width, + self.size.y +end + + +---@return number x +---@return number y +---@return number width +---@return number height function View:get_scrollbar_rect() local sz = self:get_scrollable_size() if sz <= self.size.y or sz == math.huge then return 0, 0, 0, 0 end local h = math.max(20, self.size.y * self.size.y / sz) + local width = style.scrollbar_size + if self.hovered_scrollbar_track or self.dragging_scrollbar then + width = style.expanded_scrollbar_size + end return - self.position.x + self.size.x - style.scrollbar_size, + self.position.x + self.size.x - width, self.position.y + self.scroll.y * (self.size.y - h) / (sz - self.size.y), - style.scrollbar_size, + width, h end +---@param x number +---@param y number +---@return boolean function View:scrollbar_overlaps_point(x, y) local sx, sy, sw, sh = self:get_scrollbar_rect() - return x >= sx - sw * 3 and x < sx + sw and y >= sy and y < sy + sh + return x >= sx - style.scrollbar_size * 3 and x < sx + sw and y > sy and y <= sy + sh +end + +---@param x number +---@param y number +---@return boolean +function View:scrollbar_track_overlaps_point(x, y) + local sx, sy, sw, sh = self:get_scrollbar_track_rect() + return x >= sx - style.scrollbar_size * 3 and x < sx + sw and y > sy and y <= sy + sh end +---@param button core.view.mousebutton +---@param x number +---@param y number +---@param clicks integer +---return boolean function View:on_mouse_pressed(button, x, y, clicks) - if self:scrollbar_overlaps_point(x, y) then - self.dragging_scrollbar = true + if self:scrollbar_track_overlaps_point(x, y) then + if self:scrollbar_overlaps_point(x, y) then + self.dragging_scrollbar = true + else + local _, _, _, sh = self:get_scrollbar_rect() + local ly = (y - self.position.y) - sh / 2 + local pct = common.clamp(ly / self.size.y, 0, 100) + self.scroll.to.y = self:get_scrollable_size() * pct + end return true end end +---@param button core.view.mousebutton +---@param x number +---@param y number function View:on_mouse_released(button, x, y) self.dragging_scrollbar = false end +---@param x number +---@param y number +---@param dx number +---@param dy number function View:on_mouse_moved(x, y, dx, dy) if self.dragging_scrollbar then local delta = self:get_scrollable_size() / self.size.y * dy self.scroll.to.y = self.scroll.to.y + delta + if not config.animate_drag_scroll then + self:clamp_scroll_position() + self.scroll.y = self.scroll.to.y + end end self.hovered_scrollbar = self:scrollbar_overlaps_point(x, y) + self.hovered_scrollbar_track = self.hovered_scrollbar or self:scrollbar_track_overlaps_point(x, y) end +function View:on_mouse_left() + self.hovered_scrollbar = false + self.hovered_scrollbar_track = false +end + + +---@param filename string +---@param x number +---@param y number +---@return boolean +function View:on_file_dropped(filename, x, y) + return false +end + + +---@param text string function View:on_text_input(text) -- no-op end +---@param y number +---@return boolean function View:on_mouse_wheel(y) end @@ -113,6 +249,8 @@ function View:get_content_bounds() end +---@return number x +---@return number y function View:get_content_offset() local x = common.round(self.position.x - self.scroll.x) local y = common.round(self.position.y - self.scroll.y) @@ -126,13 +264,37 @@ function View:clamp_scroll_position() end -function View:update() - self:clamp_scroll_position() - self:move_towards(self.scroll, "x", self.scroll.to.x, 0.3) - self:move_towards(self.scroll, "y", self.scroll.to.y, 0.3) +function View:update_scrollbar() + local x, y, w, h = self:get_scrollbar_rect() + self.scrollbar.w.to.thumb = w + self:move_towards(self.scrollbar.w, "thumb", self.scrollbar.w.to.thumb, 0.3, "scroll") + self.scrollbar.x.thumb = x + w - self.scrollbar.w.thumb + self.scrollbar.y.thumb = y + self.scrollbar.h.thumb = h + + local x, y, w, h = self:get_scrollbar_track_rect() + self.scrollbar.w.to.track = w + self:move_towards(self.scrollbar.w, "track", self.scrollbar.w.to.track, 0.3, "scroll") + self.scrollbar.x.track = x + w - self.scrollbar.w.track + self.scrollbar.y.track = y + self.scrollbar.h.track = h + + -- we use 100 for a smoother transition + self.scrollbar_alpha.to = (self.hovered_scrollbar_track or self.dragging_scrollbar) and 100 or 0 + self:move_towards(self.scrollbar_alpha, "value", self.scrollbar_alpha.to, 0.3, "scroll") end +function View:update() + self:clamp_scroll_position() + self:move_towards(self.scroll, "x", self.scroll.to.x, 0.3, "scroll") + self:move_towards(self.scroll, "y", self.scroll.to.y, 0.3, "scroll") + + self:update_scrollbar() +end + + +---@param color renderer.color function View:draw_background(color) local x, y = self.position.x, self.position.y local w, h = self.size.x, self.size.y @@ -140,11 +302,29 @@ function View:draw_background(color) end -function View:draw_scrollbar() - local x, y, w, h = self:get_scrollbar_rect() +function View:draw_scrollbar_track() + if not (self.hovered_scrollbar_track or self.dragging_scrollbar) + and self.scrollbar_alpha.value == 0 then + return + end + local color = { table.unpack(style.scrollbar_track) } + color[4] = color[4] * self.scrollbar_alpha.value / 100 + renderer.draw_rect(self.scrollbar.x.track, self.scrollbar.y.track, + self.scrollbar.w.track, self.scrollbar.h.track, color) +end + + +function View:draw_scrollbar_thumb() local highlight = self.hovered_scrollbar or self.dragging_scrollbar local color = highlight and style.scrollbar2 or style.scrollbar - renderer.draw_rect(x, y, w, h, color) + renderer.draw_rect(self.scrollbar.x.thumb, self.scrollbar.y.thumb, + self.scrollbar.w.thumb, self.scrollbar.h.thumb, color) +end + + +function View:draw_scrollbar() + self:draw_scrollbar_track() + self:draw_scrollbar_thumb() end diff --git a/data/plugins/autocomplete.lua b/data/plugins/autocomplete.lua index fde9487e..d638d4ff 100644 --- a/data/plugins/autocomplete.lua +++ b/data/plugins/autocomplete.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" local common = require "core.common" local config = require "core.config" @@ -10,14 +10,66 @@ local RootView = require "core.rootview" local DocView = require "core.docview" local Doc = require "core.doc" -config.plugins.autocomplete = { - -- Amount of characters that need to be written for autocomplete - min_len = 3, - -- The max amount of visible items - max_height = 6, - -- The max amount of scrollable items - max_suggestions = 100, -} +config.plugins.autocomplete = common.merge({ + -- Amount of characters that need to be written for autocomplete + min_len = 3, + -- The max amount of visible items + max_height = 6, + -- The max amount of scrollable items + max_suggestions = 100, + -- Maximum amount of symbols to cache per document + max_symbols = 4000, + -- Font size of the description box + desc_font_size = 12, + -- The config specification used by gui generators + config_spec = { + name = "Autocomplete", + { + label = "Minimum Length", + description = "Amount of characters that need to be written for autocomplete to popup.", + path = "min_len", + type = "number", + default = 3, + min = 1, + max = 5 + }, + { + label = "Maximum Height", + description = "The maximum amount of visible items.", + path = "max_height", + type = "number", + default = 6, + min = 1, + max = 20 + }, + { + label = "Maximum Suggestions", + description = "The maximum amount of scrollable items.", + path = "max_suggestions", + type = "number", + default = 100, + min = 10, + max = 10000 + }, + { + label = "Maximum Symbols", + description = "Maximum amount of symbols to cache per document.", + path = "max_symbols", + type = "number", + default = 4000, + min = 1000, + max = 10000 + }, + { + label = "Description Font Size", + description = "Font size of the description box.", + path = "desc_font_size", + type = "number", + default = 12, + min = 8 + } + } +}, config.plugins.autocomplete) local autocomplete = {} @@ -33,7 +85,7 @@ local triggered_manually = false local mt = { __tostring = function(t) return t.text end } -function autocomplete.add(t, triggered_manually) +function autocomplete.add(t, manually_triggered) local items = {} for text, info in pairs(t.items) do if type(info) == "table" then @@ -43,9 +95,10 @@ function autocomplete.add(t, triggered_manually) { text = text, info = info.info, - desc = info.desc, -- Description shown on item selected - cb = info.cb, -- A callback called once when item is selected - data = info.data -- Optional data that can be used on cb + desc = info.desc, -- Description shown on item selected + onhover = info.onhover, -- A callback called once when item is hovered + onselect = info.onselect, -- A callback called when item is selected + data = info.data -- Optional data that can be used on cb }, mt ) @@ -56,7 +109,7 @@ function autocomplete.add(t, triggered_manually) end end - if not triggered_manually then + if not manually_triggered then autocomplete.map[t.name] = { files = t.files or ".*", items = items } else autocomplete.map_manually[t.name] = { files = t.files or ".*", items = items } @@ -66,26 +119,43 @@ end -- -- Thread that scans open document symbols and cache them -- -local max_symbols = config.max_symbols +local max_symbols = config.plugins.autocomplete.max_symbols core.add_thread(function() local cache = setmetatable({}, { __mode = "k" }) + local function get_syntax_symbols(symbols, doc) + if doc.syntax then + for sym in pairs(doc.syntax.symbols) do + symbols[sym] = true + end + end + end + local function get_symbols(doc) - if doc.disable_symbols then return {} end - local i = 1 local s = {} + get_syntax_symbols(s, doc) + if doc.disable_symbols then return s end + local i = 1 local symbols_count = 0 - while i < #doc.lines do + while i <= #doc.lines do for sym in doc.lines[i]:gmatch(config.symbol_pattern) do if not s[sym] then symbols_count = symbols_count + 1 if symbols_count > max_symbols then s = nil doc.disable_symbols = true + local filename_message + if doc.filename then + filename_message = "document " .. doc.filename + else + filename_message = "unnamed document" + end core.status_view:show_message("!", style.accent, - "Too many symbols in document "..doc.filename.. - ": stopping auto-complete for this document according to config.max_symbols.") + "Too many symbols in "..filename_message.. + ": stopping auto-complete for this document according to ".. + "config.plugins.autocomplete.max_symbols." + ) collectgarbage('collect') return {} end @@ -132,6 +202,7 @@ core.add_thread(function() for _, doc in ipairs(core.docs) do if not cache_is_valid(doc) then valid = false + break end end end @@ -159,16 +230,6 @@ local function reset_suggestions() end end -local function in_table(value, table_array) - for i, element in pairs(table_array) do - if element == value then - return true - end - end - - return false -end - local function update_suggestions() local doc = core.active_view.doc local filename = doc and doc.filename or "" @@ -199,6 +260,7 @@ local function update_suggestions() j = j + 1 end end + suggestions_idx = 1 end local function get_partial_symbol() @@ -209,7 +271,7 @@ local function get_partial_symbol() end local function get_active_view() - if getmetatable(core.active_view) == DocView then + if core.active_view:is(DocView) then return core.active_view end end @@ -220,8 +282,7 @@ local function get_suggestions_rect(av) end local line, col = av.doc:get_selection() - local x, y = av:get_line_screen_position(line) - x = x + av:get_col_x_offset(line, col - #partial) + local x, y = av:get_line_screen_position(line, col - #partial) y = y + av:get_line_height() + style.padding.y local font = av:get_font() local th = font:get_height() @@ -249,6 +310,11 @@ local function get_suggestions_rect(av) max_width = 150 end + -- if portion not visiable to right, reposition to DocView right margin + if (x - av.position.x) + max_width > av.size.x then + x = (av.size.x + av.position.x) - max_width - (style.padding.x * 2) + end + return x - style.padding.x, y - style.padding.y, @@ -256,20 +322,99 @@ local function get_suggestions_rect(av) max_items * (th + style.padding.y) + style.padding.y end +local function wrap_line(line, max_chars) + if #line > max_chars then + local lines = {} + local line_len = #line + local new_line = "" + local prev_char = "" + local position = 0 + local indent = line:match("^%s+") + for char in line:gmatch(".") do + position = position + 1 + if #new_line < max_chars then + new_line = new_line .. char + prev_char = char + if position >= line_len then + table.insert(lines, new_line) + end + else + if + not prev_char:match("%s") + and + not string.sub(line, position+1, 1):match("%s") + and + position < line_len + then + new_line = new_line .. "-" + end + table.insert(lines, new_line) + if indent then + new_line = indent .. char + else + new_line = char + end + end + end + return lines + end + return line +end + +local previous_scale = SCALE +local desc_font = style.code_font:copy( + config.plugins.autocomplete.desc_font_size * SCALE +) local function draw_description_box(text, av, sx, sy, sw, sh) + if previous_scale ~= SCALE then + desc_font = style.code_font:copy( + config.plugins.autocomplete.desc_font_size * SCALE + ) + previous_scale = SCALE + end + + local font = desc_font + local lh = font:get_height() + local y = sy + style.padding.y + local x = sx + sw + style.padding.x / 4 local width = 0 + local char_width = font:get_width(" ") + local draw_left = false; + + local max_chars = 0 + if sx - av.position.x < av.size.x - (sx - av.position.x) - sw then + max_chars = (((av.size.x+av.position.x) - x) / char_width) - 5 + else + draw_left = true; + max_chars = ( + (sx - av.position.x - (style.padding.x / 4) - style.scrollbar_size) + / char_width + ) - 5 + end local lines = {} for line in string.gmatch(text.."\n", "(.-)\n") do - width = math.max(width, style.font:get_width(line)) - table.insert(lines, line) + local wrapper_lines = wrap_line(line, max_chars) + if type(wrapper_lines) == "table" then + for _, wrapped_line in pairs(wrapper_lines) do + width = math.max(width, font:get_width(wrapped_line)) + table.insert(lines, wrapped_line) + end + else + width = math.max(width, font:get_width(line)) + table.insert(lines, line) + end end - local height = #lines * style.font:get_height() + if draw_left then + x = sx - (style.padding.x / 4) - width - (style.padding.x * 2) + end + + local height = #lines * font:get_height() -- draw background rect renderer.draw_rect( - sx + sw + style.padding.x / 4, + x, sy, width + style.padding.x * 2, height + style.padding.y * 2, @@ -277,13 +422,10 @@ local function draw_description_box(text, av, sx, sy, sw, sh) ) -- draw text - local lh = style.font:get_height() - local y = sy + style.padding.y - local x = sx + sw + style.padding.x / 4 - for _, line in pairs(lines) do common.draw_text( - style.font, style.text, line, "left", x + style.padding.x, y, width, lh + font, style.text, line, "left", + x + style.padding.x, y, width, lh ) y = y + lh end @@ -320,10 +462,9 @@ local function draw_suggestions_box(av) end y = y + lh if suggestions_idx == i then - if s.cb then - s.cb(suggestions_idx, s) - s.cb = nil - s.data = nil + if s.onhover then + s.onhover(suggestions_idx, s) + s.onhover = nil end if s.desc and #s.desc > 0 then draw_description_box(s.desc, av, rx, ry, rw, rh) @@ -480,17 +621,26 @@ end -- Commands -- local function predicate() - return get_active_view() and #suggestions > 0 + local active_docview = get_active_view() + return active_docview and #suggestions > 0, active_docview end command.add(predicate, { - ["autocomplete:complete"] = function() - local doc = core.active_view.doc + ["autocomplete:complete"] = function(dv) + local doc = dv.doc local line, col = doc:get_selection() - local text = suggestions[suggestions_idx].text - doc:insert(line, col, text) - doc:remove(line, col, line, col - #partial) - doc:set_selection(line, col + #text - #partial) + local item = suggestions[suggestions_idx] + local text = item.text + local inserted = false + if item.onselect then + inserted = item.onselect(suggestions_idx, item) + end + if not inserted then + local current_partial = get_partial_symbol() + doc:insert(line, col, text) + doc:remove(line, col, line, col - #current_partial) + doc:set_selection(line, col + #text - #current_partial) + end reset_suggestions() end, diff --git a/data/plugins/autoreload.lua b/data/plugins/autoreload.lua index 9978092e..cde1c085 100644 --- a/data/plugins/autoreload.lua +++ b/data/plugins/autoreload.lua @@ -1,44 +1,110 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" local config = require "core.config" +local style = require "core.style" local Doc = require "core.doc" +local Node = require "core.node" +local common = require "core.common" +local dirwatch = require "core.dirwatch" +config.plugins.autoreload = common.merge({ + always_show_nagview = false, + config_spec = { + name = "Autoreload", + { + label = "Always Show Nagview", + description = "Alerts you if an opened file changes externally even if you haven't modified it.", + path = "always_show_nagview", + type = "toggle", + default = false + } + } +}, config.plugins.autoreload) + +local watch = dirwatch.new() local times = setmetatable({}, { __mode = "k" }) +local visible = setmetatable({}, { __mode = "k" }) + +local function get_project_doc_watch(doc) + for i, v in ipairs(core.project_directories) do + if doc.abs_filename:find(v.name, 1, true) == 1 then return v.watch end + end + return watch +end local function update_time(doc) - local info = system.get_file_info(doc.filename) - times[doc] = info.modified + times[doc] = system.get_file_info(doc.filename).modified end local function reload_doc(doc) - local fp = io.open(doc.filename, "r") - local text = fp:read("*a") - fp:close() - - local sel = { doc:get_selection() } - doc:remove(1, 1, math.huge, math.huge) - doc:insert(1, 1, text:gsub("\r", ""):gsub("\n$", "")) - doc:set_selection(table.unpack(sel)) - + doc:reload() update_time(doc) - doc:clean() + core.redraw = true core.log_quiet("Auto-reloaded doc \"%s\"", doc.filename) end -local on_modify = core.on_dirmonitor_modify - -core.on_dirmonitor_modify = function(dir, filepath) - local abs_filename = dir.name .. PATHSEP .. filepath - for _, doc in ipairs(core.docs) do - local info = system.get_file_info(doc.filename or "") - if doc.abs_filename == abs_filename and info and times[doc] ~= info.modified then - reload_doc(doc) - break - end +local function check_prompt_reload(doc) + if doc and doc.deferred_reload then + core.nag_view:show("File Changed", doc.filename .. " has changed. Reload this file?", { + { font = style.font, text = "Yes", default_yes = true }, + { font = style.font, text = "No" , default_no = true } + }, function(item) + if item.text == "Yes" then reload_doc(doc) end + doc.deferred_reload = false + end) end - on_modify(dir, filepath) end +local function doc_changes_visiblity(doc, visibility) + if doc and visible[doc] ~= visibility and doc.abs_filename then + visible[doc] = visibility + if visibility then check_prompt_reload(doc) end + get_project_doc_watch(doc):watch(doc.abs_filename, visibility) + end +end + +local on_check = dirwatch.check +function dirwatch:check(change_callback, ...) + on_check(self, function(dir) + for _, doc in ipairs(core.docs) do + if doc.abs_filename and (dir == common.dirname(doc.abs_filename) or dir == doc.abs_filename) then + local info = system.get_file_info(doc.filename or "") + if info and times[doc] ~= info.modified then + if not doc:is_dirty() and not config.plugins.autoreload.always_show_nagview then + reload_doc(doc) + else + doc.deferred_reload = true + if doc == core.active_view.doc then check_prompt_reload(doc) end + end + end + end + end + change_callback(dir) + end, ...) +end + +local core_set_active_view = core.set_active_view +function core.set_active_view(view) + core_set_active_view(view) + doc_changes_visiblity(view.doc, true) +end + +local node_set_active_view = Node.set_active_view +function Node:set_active_view(view) + if self.active_view then doc_changes_visiblity(self.active_view.doc, false) end + node_set_active_view(self, view) + doc_changes_visiblity(self.active_view.doc, true) +end + +core.add_thread(function() + while true do + -- because we already hook this function above; we only + -- need to check the file. + watch:check(function() end) + coroutine.yield(0.05) + end +end) + -- patch `Doc.save|load` to store modified time local load = Doc.load local save = Doc.save @@ -51,6 +117,8 @@ end Doc.save = function(self, ...) local res = save(self, ...) + -- if starting with an unsaved document with a filename. + if not times[self] then get_project_doc_watch(self):watch(self.abs_filename, true) end update_time(self) return res end diff --git a/data/plugins/contextmenu.lua b/data/plugins/contextmenu.lua index 4b34dfd5..db6d28db 100644 --- a/data/plugins/contextmenu.lua +++ b/data/plugins/contextmenu.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" local command = require "core.command" local keymap = require "core.keymap" @@ -32,9 +32,9 @@ function RootView:draw(...) menu:draw() end -command.add(nil, { - ["context:show"] = function() - menu:show(core.active_view.position.x, core.active_view.position.y) +command.add("core.docview!", { + ["context:show"] = function(dv) + menu:show(dv.position.x, dv.position.y) end }) @@ -42,23 +42,24 @@ keymap.add { ["menu"] = "context:show" } -local function copy_log() - local item = core.active_view.hovered_item - if item then - system.set_clipboard(core.get_log(item)) - end -end - -local function open_as_doc() - local doc = core.open_doc("logs.txt") - core.root_view:open_doc(doc) - doc:insert(1, 1, core.get_log()) -end - -menu:register("core.logview", { - { text = "Copy entry", command = copy_log }, - { text = "Open as file", command = open_as_doc } +command.add(function() return menu.show_context_menu == true end, { + ["context:focus-previous"] = function() + menu:focus_previous() + end, + ["context:focus-next"] = function() + menu:focus_next() + end, + ["context:hide"] = function() + menu:hide() + end, + ["context:on-selected"] = function() + menu:call_selected_item() + end, }) +keymap.add { ["return"] = "context:on-selected" } +keymap.add { ["up"] = "context:focus-previous" } +keymap.add { ["down"] = "context:focus-next" } +keymap.add { ["escape"] = "context:hide" } if require("plugins.scale") then menu:register("core.docview", { diff --git a/data/plugins/detectindent.lua b/data/plugins/detectindent.lua index 9ac29882..3ab0ee45 100644 --- a/data/plugins/detectindent.lua +++ b/data/plugins/detectindent.lua @@ -1,95 +1,256 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" local command = require "core.command" local common = require "core.common" local config = require "core.config" +local core_syntax = require "core.syntax" local DocView = require "core.docview" local Doc = require "core.doc" -local tokenizer = require "core.tokenizer" local cache = setmetatable({}, { __mode = "k" }) +local comments_cache = {} +local auto_detect_max_lines = 150 -local function add_to_stat(stat, val) - for i = 1, #stat do - if val == stat[i][1] then - stat[i][2] = stat[i][2] + 1 - return - end +local function indent_occurrences_more_than_once(stat, idx) + if stat[idx-1] and stat[idx-1] == stat[idx] then + return true + elseif stat[idx+1] and stat[idx+1] == stat[idx] then + return true end - stat[#stat + 1] = {val, 1} + return false end local function optimal_indent_from_stat(stat) if #stat == 0 then return nil, 0 end - local bins = {} - for k = 1, #stat do - local indent = stat[k][1] + table.sort(stat, function(a, b) return a > b end) + local best_indent = 0 + local best_score = 0 + local count = #stat + for x=1, count do + local indent = stat[x] local score = 0 - local mult_prev, lines_prev - for i = k, #stat do - if stat[i][1] % indent == 0 then - local mult = stat[i][1] / indent - if not mult_prev or (mult_prev + 1 == mult and lines_prev / stat[i][2] > 0.1) then - -- we add the number of lines to the score only if the previous - -- multiple of "indent" was populated with enough lines. - score = score + stat[i][2] - end - mult_prev, lines_prev = mult, stat[i][2] + for y=1, count do + if y ~= x and stat[y] % indent == 0 then + score = score + 1 + elseif + indent > stat[y] + and + indent_occurrences_more_than_once(stat, y) + then + score = 0 + break end end - bins[#bins + 1] = {indent, score} - end - table.sort(bins, function(a, b) return a[2] > b[2] end) - return bins[1][1], bins[1][2] -end - - --- return nil if it is a comment or blank line or the initial part of the --- line otherwise. --- we don't need to have the whole line to detect indentation. -local function get_first_line_part(tokens) - local i, n = 1, #tokens - while i + 1 <= n do - local ttype, ttext = tokens[i], tokens[i + 1] - if ttype ~= "comment" and ttext:gsub("%s+", "") ~= "" then - return ttext + if score > best_score then + best_indent = indent + best_score = score + end + if score > 0 then + break end - i = i + 2 end + return best_score > 0 and best_indent or nil, best_score end + +local function escape_comment_tokens(token) + local special_chars = "*-%[].()+?^$" + local escaped = "" + for x=1, token:len() do + local found = false + for y=1, special_chars:len() do + if token:sub(x, x) == special_chars:sub(y, y) then + escaped = escaped .. "%" .. token:sub(x, x) + found = true + break + end + end + if not found then + escaped = escaped .. token:sub(x, x) + end + end + return escaped +end + + +local function get_comment_patterns(syntax) + if comments_cache[syntax] then + if #comments_cache[syntax] > 0 then + return comments_cache[syntax] + else + return nil + end + end + local comments = {} + for idx=1, #syntax.patterns do + local pattern = syntax.patterns[idx] + local startp = "" + if + type(pattern.type) == "string" + and + (pattern.type == "comment" or pattern.type == "string") + then + local not_is_string = pattern.type ~= "string" + if pattern.pattern then + startp = type(pattern.pattern) == "table" + and pattern.pattern[1] or pattern.pattern + if not_is_string and startp:sub(1, 1) ~= "^" then + startp = "^%s*" .. startp + elseif not_is_string then + startp = "^%s*" .. startp:sub(2, startp:len()) + end + if type(pattern.pattern) == "table" then + table.insert(comments, {"p", startp, pattern.pattern[2]}) + elseif not_is_string then + table.insert(comments, {"p", startp}) + end + elseif pattern.regex then + startp = type(pattern.regex) == "table" + and pattern.regex[1] or pattern.regex + if not_is_string and startp:sub(1, 1) ~= "^" then + startp = "^\\s*" .. startp + elseif not_is_string then + startp = "^\\s*" .. startp:sub(2, startp:len()) + end + if type(pattern.regex) == "table" then + table.insert(comments, { + "r", regex.compile(startp), regex.compile(pattern.regex[2]) + }) + elseif not_is_string then + table.insert(comments, {"r", regex.compile(startp)}) + end + end + elseif pattern.syntax then + local subsyntax = type(pattern.syntax) == 'table' and pattern.syntax + or core_syntax.get("file"..pattern.syntax, "") + local sub_comments = get_comment_patterns(subsyntax) + if sub_comments then + for s=1, #sub_comments do + table.insert(comments, sub_comments[s]) + end + end + end + end + if #comments == 0 then + local single_line_comment = syntax.comment + and escape_comment_tokens(syntax.comment) or nil + local block_comment = nil + if syntax.block_comment then + block_comment = { + escape_comment_tokens(syntax.block_comment[1]), + escape_comment_tokens(syntax.block_comment[2]) + } + end + if single_line_comment then + table.insert(comments, {"p", "^%s*" .. single_line_comment}) + end + if block_comment then + table.insert(comments, {"p", "^%s*" .. block_comment[1], block_comment[2]}) + end + end + comments_cache[syntax] = comments + if #comments > 0 then + return comments + end + return nil +end + + local function get_non_empty_lines(syntax, lines) return coroutine.wrap(function() - local tokens, state + local comments = get_comment_patterns(syntax) + local i = 0 + local end_regex = nil + local end_pattern = nil + local inside_comment = false for _, line in ipairs(lines) do - tokens, state = tokenizer.tokenize(syntax, line, state) - local line_start = get_first_line_part(tokens) - if line_start then - i = i + 1 - coroutine.yield(i, line_start) + if line:gsub("^%s+", "") ~= "" then + local is_comment = false + if comments then + if not inside_comment then + for c=1, #comments do + local comment = comments[c] + if comment[1] == "p" then + if comment[3] then + local start, ending = line:find(comment[2]) + if start then + if not line:find(comment[3], ending+1) then + is_comment = true + inside_comment = true + end_pattern = comment[3] + end + break + end + elseif line:find(comment[2]) then + is_comment = true + break + end + else + if comment[3] then + local start, ending = regex.match( + comment[2], line, 1, regex.ANCHORED + ) + if start then + if not regex.match( + comment[3], line, ending+1, regex.ANCHORED + ) + then + is_comment = true + inside_comment = true + end_regex = comment[3] + end + break + end + elseif regex.match(comment[2], line, 1, regex.ANCHORED) then + is_comment = true + break + end + end + end + elseif end_pattern and line:find(end_pattern) then + is_comment = true + inside_comment = false + end_pattern = nil + elseif end_regex and regex.match(end_regex, line) then + is_comment = true + inside_comment = false + end_regex = nil + end + end + if + not is_comment + and + not inside_comment + then + i = i + 1 + coroutine.yield(i, line) + end end end end) end -local auto_detect_max_lines = 100 - local function detect_indent_stat(doc) local stat = {} local tab_count = 0 + local runs = 1 + local max_lines = auto_detect_max_lines for i, text in get_non_empty_lines(doc.syntax, doc.lines) do - local str = text:match("^ %s+%S") - if str then add_to_stat(stat, #str - 1) end - local str = text:match("^\t+") - if str then tab_count = tab_count + 1 end + local spaces = text:match("^ +") + if spaces then table.insert(stat, spaces:len()) end + local tabs = text:match("^\t+") + if tabs then tab_count = tab_count + 1 end + -- if nothing found for first lines try at least 4 more times + if i == max_lines and runs < 5 and #stat == 0 and tab_count == 0 then + max_lines = max_lines + auto_detect_max_lines + runs = runs + 1 -- Stop parsing when files is very long. Not needed for euristic determination. - if i > auto_detect_max_lines then break end + elseif i > max_lines then break end end - table.sort(stat, function(a, b) return a[1] < b[1] end) local indent, score = optimal_indent_from_stat(stat) if tab_count > score then return "hard", config.indent_size, tab_count @@ -101,7 +262,7 @@ end local function update_cache(doc) local type, size, score = detect_indent_stat(doc) - local score_threshold = 4 + local score_threshold = 2 if score < score_threshold then -- use default values type = config.tab_type @@ -130,55 +291,54 @@ end local function set_indent_type(doc, type) local _, indent_size = doc:get_indent_info() - cache[doc] = {type = type, - size = indent_size, - confirmed = true} + cache[doc] = { + type = type, + size = indent_size, + confirmed = true + } doc.indent_info = cache[doc] end -local function set_indent_type_command() - core.command_view:enter( - "Specify indent style for this file", - function(value) -- submit - local doc = core.active_view.doc +local function set_indent_type_command(dv) + core.command_view:enter("Specify indent style for this file", { + submit = function(value) + local doc = dv.doc value = value:lower() set_indent_type(doc, value == "tabs" and "hard" or "soft") end, - function(text) -- suggest + suggest = function(text) return common.fuzzy_match({"tabs", "spaces"}, text) end, - nil, -- cancel - function(text) -- validate + validate = function(text) local t = text:lower() return t == "tabs" or t == "spaces" end - ) + }) end local function set_indent_size(doc, size) local indent_type = doc:get_indent_info() - cache[doc] = {type = indent_type, - size = size, - confirmed = true} + cache[doc] = { + type = indent_type, + size = size, + confirmed = true + } doc.indent_info = cache[doc] end -local function set_indent_size_command() - core.command_view:enter( - "Specify indent size for current file", - function(value) -- submit - local value = math.floor(tonumber(value)) - local doc = core.active_view.doc +local function set_indent_size_command(dv) + core.command_view:enter("Specify indent size for current file", { + submit = function(value) + value = math.floor(tonumber(value)) + local doc = dv.doc set_indent_size(doc, value) end, - nil, -- suggest - nil, -- cancel - function(value) -- validate - local value = tonumber(value) + validate = function(value) + value = tonumber(value) return value ~= nil and value >= 1 end - ) + }) end @@ -187,20 +347,24 @@ command.add("core.docview", { ["indent:set-file-indent-size"] = set_indent_size_command }) - -command.add(function() +command.add( + function() return core.active_view:is(DocView) - and cache[core.active_view.doc] - and cache[core.active_view.doc].type == "soft" + and cache[core.active_view.doc] + and cache[core.active_view.doc].type == "soft" end, { - ["indent:switch-file-to-tabs-indentation"] = function() set_indent_type(core.active_view.doc, "hard") end + ["indent:switch-file-to-tabs-indentation"] = function() + set_indent_type(core.active_view.doc, "hard") + end }) - -command.add(function() +command.add( + function() return core.active_view:is(DocView) - and cache[core.active_view.doc] - and cache[core.active_view.doc].type == "hard" + and cache[core.active_view.doc] + and cache[core.active_view.doc].type == "hard" end, { - ["indent:switch-file-to-spaces-indentation"] = function() set_indent_type(core.active_view.doc, "soft") end + ["indent:switch-file-to-spaces-indentation"] = function() + set_indent_type(core.active_view.doc, "soft") + end }) diff --git a/data/plugins/drawwhitespace.lua b/data/plugins/drawwhitespace.lua index 0004c7ea..a0b8ad60 100644 --- a/data/plugins/drawwhitespace.lua +++ b/data/plugins/drawwhitespace.lua @@ -1,36 +1,304 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local style = require "core.style" local DocView = require "core.docview" local common = require "core.common" +local config = require "core.config" +local Highlighter = require "core.doc.highlighter" + +config.plugins.drawwhitespace = common.merge({ + enabled = true, + show_leading = true, + show_trailing = true, + show_middle = true, + + show_middle_min = 1, + + color = style.syntax.whitespace or style.syntax.comment, + leading_color = nil, + middle_color = nil, + trailing_color = nil, + + substitutions = { + { + char = " ", + sub = "·", + -- You can put any of the previous options here too. + -- For example: + -- show_middle_min = 2, + -- show_leading = false, + }, + { + char = "\t", + sub = "»", + }, + }, + + config_spec = { + name = "Draw Whitespace", + { + label = "Enabled", + description = "Disable or enable the drawing of white spaces.", + path = "enabled", + type = "toggle", + default = true + }, + { + label = "Show Leading", + description = "Draw whitespaces starting at the beginning of a line.", + path = "show_leading", + type = "toggle", + default = true, + }, + { + label = "Show Middle", + description = "Draw whitespaces on the middle of a line.", + path = "show_middle", + type = "toggle", + default = true, + }, + { + label = "Show Trailing", + description = "Draw whitespaces on the end of a line.", + path = "show_trailing", + type = "toggle", + default = true, + }, + { + label = "Show Trailing as Error", + description = "Uses an error square to spot them easily, requires 'Show Trailing' enabled.", + path = "show_trailing_error", + type = "toggle", + default = false, + on_apply = function(enabled) + local found = nil + local substitutions = config.plugins.drawwhitespace.substitutions + for i, sub in ipairs(substitutions) do + if sub.trailing_error then + found = i + end + end + if found == nil and enabled then + table.insert(substitutions, { + char = " ", + sub = "█", + show_leading = false, + show_middle = false, + show_trailing = true, + trailing_color = style.error, + trailing_error = true + }) + elseif found ~= nil and not enabled then + table.remove(substitutions, found) + end + end + } + } +}, config.plugins.drawwhitespace) + + +local ws_cache +local cached_settings +local function reset_cache() + ws_cache = setmetatable({}, { __mode = "k" }) + local settings = config.plugins.drawwhitespace + cached_settings = { + show_leading = settings.show_leading, + show_trailing = settings.show_trailing, + show_middle = settings.show_middle, + show_middle_min = settings.show_middle_min, + color = settings.color, + leading_color = settings.leading_color, + middle_color = settings.middle_color, + trailing_color = settings.trailing_color, + substitutions = settings.substitutions, + } +end +reset_cache() + +local function reset_cache_if_needed() + local settings = config.plugins.drawwhitespace + if + not ws_cache or + cached_settings.show_leading ~= settings.show_leading + or cached_settings.show_trailing ~= settings.show_trailing + or cached_settings.show_middle ~= settings.show_middle + or cached_settings.show_middle_min ~= settings.show_middle_min + or cached_settings.color ~= settings.color + or cached_settings.leading_color ~= settings.leading_color + or cached_settings.middle_color ~= settings.middle_color + or cached_settings.trailing_color ~= settings.trailing_color + -- we assume that the entire table changes + or cached_settings.substitutions ~= settings.substitutions + then + reset_cache() + end +end + +-- Move cache to make space for new lines +local prev_insert_notify = Highlighter.insert_notify +function Highlighter:insert_notify(line, n, ...) + prev_insert_notify(self, line, n, ...) + if not ws_cache[self] then + ws_cache[self] = {} + end + local to = math.min(line + n, #self.doc.lines) + for i=#self.doc.lines+n,to,-1 do + ws_cache[self][i] = ws_cache[self][i - n] + end + for i=line,to do + ws_cache[self][i] = nil + end +end + +-- Close the cache gap created by removed lines +local prev_remove_notify = Highlighter.remove_notify +function Highlighter:remove_notify(line, n, ...) + prev_remove_notify(self, line, n, ...) + if not ws_cache[self] then + ws_cache[self] = {} + end + local to = math.max(line + n, #self.doc.lines) + for i=line,to do + ws_cache[self][i] = ws_cache[self][i + n] + end +end + +-- Remove changed lines from the cache +local prev_update_notify = Highlighter.update_notify +function Highlighter:update_notify(line, n, ...) + prev_update_notify(self, line, n, ...) + if not ws_cache[self] then + ws_cache[self] = {} + end + for i=line,line+n do + ws_cache[self][i] = nil + end +end + + +local function get_option(substitution, option) + if substitution[option] == nil then + return config.plugins.drawwhitespace[option] + end + return substitution[option] +end local draw_line_text = DocView.draw_line_text - function DocView:draw_line_text(idx, x, y) + if + not config.plugins.drawwhitespace.enabled + or + getmetatable(self) ~= DocView + then + return draw_line_text(self, idx, x, y) + end + local font = (self:get_font() or style.syntax_fonts["whitespace"] or style.syntax_fonts["comment"]) - local color = style.syntax.whitespace or style.syntax.comment - local ty = y + self:get_line_text_y_offset() - local tx - local text, offset, s, e = self.doc.lines[idx], 1 + local font_size = font:get_size() + local _, indent_size = self.doc:get_indent_info() + + reset_cache_if_needed() + if + not ws_cache[self.doc.highlighter] + or ws_cache[self.doc.highlighter].font ~= font + or ws_cache[self.doc.highlighter].font_size ~= font_size + or ws_cache[self.doc.highlighter].indent_size ~= indent_size + then + ws_cache[self.doc.highlighter] = + setmetatable( + { font = font, font_size = font_size, indent_size = indent_size }, + { __mode = "k" } + ) + end + + if not ws_cache[self.doc.highlighter][idx] then -- need to cache line + local cache = {} + + local tx + local text = self.doc.lines[idx] + + for _, substitution in pairs(config.plugins.drawwhitespace.substitutions) do + local char = substitution.char + local sub = substitution.sub + local offset = 1 + + local show_leading = get_option(substitution, "show_leading") + local show_middle = get_option(substitution, "show_middle") + local show_trailing = get_option(substitution, "show_trailing") + + local show_middle_min = get_option(substitution, "show_middle_min") + + local base_color = get_option(substitution, "color") + local leading_color = get_option(substitution, "leading_color") or base_color + local middle_color = get_option(substitution, "middle_color") or base_color + local trailing_color = get_option(substitution, "trailing_color") or base_color + + local pattern = char.."+" + while true do + local s, e = text:find(pattern, offset) + if not s then break end + + tx = self:get_col_x_offset(idx, s) + + local color = base_color + local draw = false + + if e == #text - 1 then + draw = show_trailing + color = trailing_color + elseif s == 1 then + draw = show_leading + color = leading_color + else + draw = show_middle and (e - s + 1 >= show_middle_min) + color = middle_color + end + + if draw then + local last_cache_idx = #cache + -- We need to draw tabs one at a time because they might have a + -- different size than the substituting character. + -- This also applies to any other char if we use non-monospace fonts + -- but we ignore this case for now. + if char == "\t" then + for i = s,e do + tx = self:get_col_x_offset(idx, i) + cache[last_cache_idx + 1] = sub + cache[last_cache_idx + 2] = tx + cache[last_cache_idx + 3] = font:get_width(sub) + cache[last_cache_idx + 4] = color + last_cache_idx = last_cache_idx + 4 + end + else + cache[last_cache_idx + 1] = string.rep(sub, e - s + 1) + cache[last_cache_idx + 2] = tx + cache[last_cache_idx + 3] = font:get_width(cache[last_cache_idx + 1]) + cache[last_cache_idx + 4] = color + end + end + offset = e + 1 + end + end + ws_cache[self.doc.highlighter][idx] = cache + end + + -- draw from cache local x1, _, x2, _ = self:get_content_bounds() - local _offset = self:get_x_offset_col(idx, x1) - offset = _offset - while true do - s, e = text:find(" +", offset) - if not s then break end - tx = self:get_col_x_offset(idx, s) + x - renderer.draw_text(font, string.rep("·", e - s + 1), tx, ty, color) - if tx > x + x2 then break end - offset = e + 1 + x1 = x1 + x + x2 = x2 + x + local ty = y + self:get_line_text_y_offset() + local cache = ws_cache[self.doc.highlighter][idx] + for i=1,#cache,4 do + local sub = cache[i] + local tx = cache[i + 1] + x + local tw = cache[i + 2] + local color = cache[i + 3] + if tx + tw >= x1 then + tx = renderer.draw_text(font, sub, tx, ty, color) + end + if tx > x2 then break end end - offset = _offset - while true do - s, e = text:find("\t", offset) - if not s then break end - tx = self:get_col_x_offset(idx, s) + x - renderer.draw_text(font, "»", tx, ty, color) - if tx > x + x2 then break end - offset = e + 1 - end - draw_line_text(self, idx, x, y) + + return draw_line_text(self, idx, x, y) end diff --git a/data/plugins/language_c.lua b/data/plugins/language_c.lua index 88cc7c5a..c15466be 100644 --- a/data/plugins/language_c.lua +++ b/data/plugins/language_c.lua @@ -1,12 +1,13 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { name = "C", - files = { "%.c$", "%.h$", "%.inl$" }, + files = { "%.c$" }, comment = "//", + block_comment = { "/*", "*/" }, patterns = { - { pattern = "//.-\n", type = "comment" }, + { pattern = "//.*", type = "comment" }, { pattern = { "/%*", "%*/" }, type = "comment" }, { pattern = { '"', '"', '\\' }, type = "string" }, { pattern = { "'", "'", '\\' }, type = "string" }, @@ -14,12 +15,64 @@ syntax.add { { pattern = "%d+[%d%.eE]*f?", type = "number" }, { pattern = "%.?%d+f?", type = "number" }, { pattern = "[%+%-=/%*%^%%<>!~|&]", type = "operator" }, + { pattern = "##", type = "operator" }, { pattern = "struct%s()[%a_][%w_]*", type = {"keyword", "keyword2"} }, { pattern = "union%s()[%a_][%w_]*", type = {"keyword", "keyword2"} }, + -- static declarations + { pattern = "static()%s+()inline", + type = { "keyword", "normal", "keyword" } + }, + { pattern = "static()%s+()const", + type = { "keyword", "normal", "keyword" } + }, + { pattern = "static()%s+()[%a_][%w_]*", + type = { "keyword", "normal", "literal" } + }, + -- match function type declarations + { pattern = "[%a_][%w_]*()%*+()%s+()[%a_][%w_]*%f[%(]", + type = { "literal", "operator", "normal", "function" } + }, + { pattern = "[%a_][%w_]*()%s+()%*+()[%a_][%w_]*%f[%(]", + type = { "literal", "normal", "operator", "function" } + }, + { pattern = "[%a_][%w_]*()%s+()[%a_][%w_]*%f[%(]", + type = { "literal", "normal", "function" } + }, + -- match variable type declarations + { pattern = "[%a_][%w_]*()%*+()%s+()[%a_][%w_]*", + type = { "literal", "operator", "normal", "normal" } + }, + { pattern = "[%a_][%w_]*()%s+()%*+()[%a_][%w_]*", + type = { "literal", "normal", "operator", "normal" } + }, + { pattern = "[%a_][%w_]*()%s+()[%a_][%w_]*()%s*()[;,%[%)]", + type = { "literal", "normal", "normal", "normal", "normal" } + }, + { pattern = "[%a_][%w_]*()%s+()[%a_][%w_]*()%s*()=", + type = { "literal", "normal", "normal", "normal", "operator" } + }, + { pattern = "[%a_][%w_]*()&()%s+()[%a_][%w_]*", + type = { "literal", "operator", "normal", "normal" } + }, + { pattern = "[%a_][%w_]*()%s+()&()[%a_][%w_]*", + type = { "literal", "normal", "operator", "normal" } + }, + -- Uppercase constants of at least 2 chars in len + { pattern = "_?%u[%u_][%u%d_]*%f[%s%+%*%-%.%)%]}%?%^%%=/<>~|&;:,!]", + type = "number" + }, + -- Magic constants + { pattern = "__[%u%l]+__", type = "number" }, + -- all other functions { pattern = "[%a_][%w_]*%f[(]", type = "function" }, + -- Macros + { pattern = "^%s*#%s*define%s+()[%a_][%a%d_]*", + type = { "keyword", "symbol" } + }, + { pattern = "#%s*include%s()<.->", type = {"keyword", "string"} }, + { pattern = "%f[#]#%s*[%a_][%w_]*", type = "keyword" }, + -- Everything else to make the tokenizer work properly { pattern = "[%a_][%w_]*", type = "symbol" }, - { pattern = "#include%s()<.->", type = {"keyword", "string"} }, - { pattern = "#[%a_][%w_]*", type = "keyword" }, }, symbols = { ["if"] = "keyword", @@ -44,6 +97,8 @@ syntax.add { ["case"] = "keyword", ["default"] = "keyword", ["auto"] = "keyword", + ["struct"] = "keyword", + ["union"] = "keyword", ["void"] = "keyword2", ["int"] = "keyword2", ["short"] = "keyword2", @@ -60,6 +115,7 @@ syntax.add { ["#if"] = "keyword", ["#ifdef"] = "keyword", ["#ifndef"] = "keyword", + ["#elif"] = "keyword", ["#else"] = "keyword", ["#elseif"] = "keyword", ["#endif"] = "keyword", diff --git a/data/plugins/language_cpp.lua b/data/plugins/language_cpp.lua index a945fd1f..a26ce868 100644 --- a/data/plugins/language_cpp.lua +++ b/data/plugins/language_cpp.lua @@ -1,6 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 -pcall(require, "plugins.language_c") - +-- mod-version:3 local syntax = require "core.syntax" syntax.add { @@ -10,28 +8,101 @@ syntax.add { "%.c++$", "%.hh$", "%.H$", "%.hxx$", "%.hpp$", "%.h++$" }, comment = "//", + block_comment = { "/*", "*/" }, patterns = { - { pattern = "//.-\n", type = "comment" }, - { pattern = { "/%*", "%*/" }, type = "comment" }, - { pattern = { '"', '"', '\\' }, type = "string" }, - { pattern = { "'", "'", '\\' }, type = "string" }, - { pattern = "0x%x+", type = "number" }, - { pattern = "%d+[%d%.eE]*f?", type = "number" }, - { pattern = "%.?%d+f?", type = "number" }, - { pattern = "[%+%-=/%*%^%%<>!~|&]", type = "operator" }, + { pattern = "//.*", type = "comment" }, + { pattern = { "/%*", "%*/" }, type = "comment" }, + { pattern = { '"', '"', '\\' }, type = "string" }, + { pattern = { "'", "'", '\\' }, type = "string" }, + { pattern = "0x%x+", type = "number" }, + { pattern = "%d+[%d%.'eE]*f?", type = "number" }, + { pattern = "%.?%d+f?", type = "number" }, + { pattern = "[%+%-=/%*%^%%<>!~|:&]", type = "operator" }, + { pattern = "##", type = "operator" }, { pattern = "struct%s()[%a_][%w_]*", type = {"keyword", "keyword2"} }, { pattern = "class%s()[%a_][%w_]*", type = {"keyword", "keyword2"} }, { pattern = "union%s()[%a_][%w_]*", type = {"keyword", "keyword2"} }, { pattern = "namespace%s()[%a_][%w_]*", type = {"keyword", "keyword2"} }, - { pattern = "[%a_][%w_]*::", type = "symbol" }, - { pattern = "::", type = "symbol" }, - { pattern = "[%a_][%w_]*", type = "symbol" }, - { pattern = "#include%s()<.->", type = {"keyword", "string"} }, - { pattern = "#[%a_][%w_]*", type = "keyword" }, + -- static declarations + { pattern = "static()%s+()inline", + type = { "keyword", "normal", "keyword" } + }, + { pattern = "static()%s+()const", + type = { "keyword", "normal", "keyword" } + }, + { pattern = "static()%s+()[%a_][%w_]*", + type = { "keyword", "normal", "literal" } + }, + -- match method type declarations + { pattern = "[%a_][%w_]*()%s*()%**()%s*()[%a_][%w_]*()%s*()::", + type = { + "literal", "normal", "operator", "normal", + "literal", "normal", "operator" + } + }, + -- match function type declarations + { pattern = "[%a_][%w_]*()%*+()%s+()[%a_][%w_]*%f[%(]", + type = { "literal", "operator", "normal", "function" } + }, + { pattern = "[%a_][%w_]*()%s+()%*+()[%a_][%w_]*%f[%(]", + type = { "literal", "normal", "operator", "function" } + }, + { pattern = "[%a_][%w_]*()%s+()[%a_][%w_]*%f[%(]", + type = { "literal", "normal", "function" } + }, + -- match variable type declarations + { pattern = "[%a_][%w_]*()%*+()%s+()[%a_][%w_]*", + type = { "literal", "operator", "normal", "normal" } + }, + { pattern = "[%a_][%w_]*()%s+()%*+()[%a_][%w_]*", + type = { "literal", "normal", "operator", "normal" } + }, + { pattern = "[%a_][%w_]*()%s+()[%a_][%w_]*()%s*()[;,%[%)]", + type = { "literal", "normal", "normal", "normal", "normal" } + }, + { pattern = "[%a_][%w_]*()%s+()[%a_][%w_]*()%s*()=", + type = { "literal", "normal", "normal", "normal", "operator" } + }, + { pattern = "[%a_][%w_]*()&()%s+()[%a_][%w_]*", + type = { "literal", "operator", "normal", "normal" } + }, + { pattern = "[%a_][%w_]*()%s+()&()[%a_][%w_]*", + type = { "literal", "normal", "operator", "normal" } + }, + -- Match scope operator element access + { pattern = "[%a_][%w_]*()%s*()::", + type = { "literal", "normal", "operator" } + }, + -- Uppercase constants of at least 2 chars in len + { pattern = "_?%u[%u_][%u%d_]*%f[%s%+%*%-%.%)%]}%?%^%%=/<>~|&;:,!]", + type = "number" + }, + -- Magic constants + { pattern = "__[%u%l]+__", type = "number" }, + -- all other functions + { pattern = "[%a_][%w_]*%f[(]", type = "function" }, + -- Macros + { pattern = "^%s*#%s*define%s+()[%a_][%a%d_]*", + type = { "keyword", "symbol" } + }, + { pattern = "#%s*include%s+()<.->", + type = { "keyword", "string" } + }, + { pattern = "%f[#]#%s*[%a_][%w_]*", type = "keyword" }, + -- Everything else to make the tokenizer work properly + { pattern = "[%a_][%w_]*", type = "symbol" }, }, symbols = { ["alignof"] = "keyword", ["alignas"] = "keyword", + ["and"] = "keyword", + ["and_eq"] = "keyword", + ["not"] = "keyword", + ["not_eq"] = "keyword", + ["or"] = "keyword", + ["or_eq"] = "keyword", + ["xor"] = "keyword", + ["xor_eq"] = "keyword", ["private"] = "keyword", ["protected"] = "keyword", ["public"] = "keyword", @@ -39,9 +110,12 @@ syntax.add { ["nullptr"] = "keyword", ["operator"] = "keyword", ["asm"] = "keyword", + ["bitand"] = "keyword", + ["bitor"] = "keyword", ["catch"] = "keyword", ["throw"] = "keyword", ["try"] = "keyword", + ["class"] = "keyword", ["compl"] = "keyword", ["explicit"] = "keyword", ["export"] = "keyword", @@ -51,8 +125,8 @@ syntax.add { ["constinit"] = "keyword", ["const_cast"] = "keyword", ["dynamic_cast"] = "keyword", - ["reinterpret_cast"] = "keyword", - ["static_cast"] = "keyword", + ["reinterpret_cast"] = "keyword", + ["static_cast"] = "keyword", ["static_assert"] = "keyword", ["template"] = "keyword", ["this"] = "keyword", @@ -63,7 +137,6 @@ syntax.add { ["co_yield"] = "keyword", ["decltype"] = "keyword", ["delete"] = "keyword", - ["export"] = "keyword", ["friend"] = "keyword", ["typeid"] = "keyword", ["typename"] = "keyword", @@ -71,6 +144,7 @@ syntax.add { ["override"] = "keyword", ["virtual"] = "keyword", ["using"] = "keyword", + ["namespace"] = "keyword", ["new"] = "keyword", ["noexcept"] = "keyword", ["if"] = "keyword", @@ -84,6 +158,8 @@ syntax.add { ["continue"] = "keyword", ["return"] = "keyword", ["goto"] = "keyword", + ["struct"] = "keyword", + ["union"] = "keyword", ["typedef"] = "keyword", ["enum"] = "keyword", ["extern"] = "keyword", @@ -95,7 +171,6 @@ syntax.add { ["case"] = "keyword", ["default"] = "keyword", ["auto"] = "keyword", - ["const"] = "keyword", ["void"] = "keyword2", ["int"] = "keyword2", ["short"] = "keyword2", @@ -105,12 +180,18 @@ syntax.add { ["char"] = "keyword2", ["unsigned"] = "keyword2", ["bool"] = "keyword2", - ["true"] = "keyword2", - ["false"] = "keyword2", + ["true"] = "literal", + ["false"] = "literal", + ["NULL"] = "literal", + ["wchar_t"] = "keyword2", + ["char8_t"] = "keyword2", + ["char16_t"] = "keyword2", + ["char32_t"] = "keyword2", ["#include"] = "keyword", ["#if"] = "keyword", ["#ifdef"] = "keyword", ["#ifndef"] = "keyword", + ["#elif"] = "keyword", ["#else"] = "keyword", ["#elseif"] = "keyword", ["#endif"] = "keyword", @@ -118,6 +199,5 @@ syntax.add { ["#warning"] = "keyword", ["#error"] = "keyword", ["#pragma"] = "keyword", - }, + }, } - diff --git a/data/plugins/language_css.lua b/data/plugins/language_css.lua index 395e375c..9e78cde5 100644 --- a/data/plugins/language_css.lua +++ b/data/plugins/language_css.lua @@ -1,12 +1,13 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { name = "CSS", files = { "%.css$" }, + block_comment = { "/*", "*/" }, patterns = { { pattern = "\\.", type = "normal" }, - { pattern = "//.-\n", type = "comment" }, + { pattern = "//.*", type = "comment" }, { pattern = { "/%*", "%*/" }, type = "comment" }, { pattern = { '"', '"', '\\' }, type = "string" }, { pattern = { "'", "'", '\\' }, type = "string" }, diff --git a/data/plugins/language_html.lua b/data/plugins/language_html.lua index 1f4515bc..6da1b45f 100644 --- a/data/plugins/language_html.lua +++ b/data/plugins/language_html.lua @@ -1,31 +1,23 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { name = "HTML", files = { "%.html?$" }, + block_comment = { "" }, patterns = { - { - pattern = { - "<%s*[sS][cC][rR][iI][pP][tT]%s+[tT][yY][pP][eE]%s*=%s*" .. - "['\"]%a+/[jJ][aA][vV][aA][sS][cC][rR][iI][pP][tT]['\"]%s*>", - "<%s*/[sS][cC][rR][iI][pP][tT]>" - }, - syntax = ".js", - type = "function" - }, - { - pattern = { - "<%s*[sS][cC][rR][iI][pP][tT]%s*>", - "<%s*/%s*[sS][cC][rR][iI][pP][tT]>" + { + pattern = { + "<%s*[sS][cC][rR][iI][pP][tT]%f[%s>].->", + "<%s*/%s*[sS][cC][rR][iI][pP][tT]%s*>" }, syntax = ".js", type = "function" }, - { - pattern = { - "<%s*[sS][tT][yY][lL][eE][^>]*>", - "<%s*/%s*[sS][tT][yY][lL][eE]%s*>" + { + pattern = { + "<%s*[sS][tT][yY][lL][eE]%f[%s>].->", + "<%s*/%s*[sS][tT][yY][lL][eE]%s*>" }, syntax = ".css", type = "function" diff --git a/data/plugins/language_js.lua b/data/plugins/language_js.lua index 94ab8499..f79fece6 100644 --- a/data/plugins/language_js.lua +++ b/data/plugins/language_js.lua @@ -1,12 +1,13 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { name = "JavaScript", - files = { "%.js$", "%.json$", "%.cson$" }, + files = { "%.js$", "%.json$", "%.cson$", "%.mjs$", "%.cjs$" }, comment = "//", + block_comment = { "/*", "*/" }, patterns = { - { pattern = "//.-\n", type = "comment" }, + { pattern = "//.*", type = "comment" }, { pattern = { "/%*", "%*/" }, type = "comment" }, { pattern = { '/[^= ]', '/', '\\' },type = "string" }, { pattern = { '"', '"', '\\' }, type = "string" }, diff --git a/data/plugins/language_lua.lua b/data/plugins/language_lua.lua index 5c770d43..55cc8adc 100644 --- a/data/plugins/language_lua.lua +++ b/data/plugins/language_lua.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { @@ -6,12 +6,13 @@ syntax.add { files = "%.lua$", headers = "^#!.*[ /]lua", comment = "--", + block_comment = { "--[[", "]]" }, patterns = { { pattern = { '"', '"', '\\' }, type = "string" }, { pattern = { "'", "'", '\\' }, type = "string" }, { pattern = { "%[%[", "%]%]" }, type = "string" }, { pattern = { "%-%-%[%[", "%]%]"}, type = "comment" }, - { pattern = "%-%-.-\n", type = "comment" }, + { pattern = "%-%-.*", type = "comment" }, { pattern = "0x%x+%.%x*[pP][-+]?%d+", type = "number" }, { pattern = "0x%x+%.%x*", type = "number" }, { pattern = "0x%.%x+[pP][-+]?%d+", type = "number" }, diff --git a/data/plugins/language_md.lua b/data/plugins/language_md.lua index e7c870ec..d622a04d 100644 --- a/data/plugins/language_md.lua +++ b/data/plugins/language_md.lua @@ -1,56 +1,234 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" +local style = require "core.style" +local core = require "core" +local initial_color = style.syntax["keyword2"] +-- Add 3 type of font styles for use on markdown files +for _, attr in pairs({"bold", "italic", "bold_italic"}) do + local attributes = {} + if attr ~= "bold_italic" then + attributes[attr] = true + else + attributes["bold"] = true + attributes["italic"] = true + end + style.syntax_fonts["markdown_"..attr] = style.code_font:copy( + style.code_font:get_size(), + attributes + ) + -- also add a color for it + style.syntax["markdown_"..attr] = style.syntax["keyword2"] +end + +local in_squares_match = "^%[%]" +local in_parenthesis_match = "^%(%)" syntax.add { name = "Markdown", files = { "%.md$", "%.markdown$" }, + block_comment = { "" }, + space_handling = false, -- turn off this feature to handle it our selfs patterns = { - { pattern = "\\.", type = "normal" }, - { pattern = { "" }, type = "comment" }, + ---- Place patterns that require spaces at start to optimize matching speed + ---- and apply the %s+ optimization immediately afterwards + -- bullets + { pattern = "^%s*%*%s", type = "number" }, + { pattern = "^%s*%-%s", type = "number" }, + { pattern = "^%s*%+%s", type = "number" }, + -- numbered bullet + { pattern = "^%s*[0-9]+[%.%)]%s", type = "number" }, + -- blockquote + { pattern = "^%s*>+%s", type = "string" }, + -- alternative bold italic formats + { pattern = { "%s___", "___%f[%s]" }, type = "markdown_bold_italic" }, + { pattern = { "%s__", "__%f[%s]" }, type = "markdown_bold" }, + { pattern = { "%s_[%S]", "_%f[%s]" }, type = "markdown_italic" }, + -- reference links + { + pattern = "^%s*%[%^()["..in_squares_match.."]+()%]: ", + type = { "function", "number", "function" } + }, + { + pattern = "^%s*%[%^?()["..in_squares_match.."]+()%]:%s+.*", + type = { "function", "number", "function" } + }, + -- optimization + { pattern = "%s+", type = "normal" }, + + ---- HTML rules imported and adapted from language_html + ---- to not conflict with markdown rules + -- Inline JS and CSS + { + pattern = { + "<%s*[sS][cC][rR][iI][pP][tT]%s+[tT][yY][pP][eE]%s*=%s*" .. + "['\"]%a+/[jJ][aA][vV][aA][sS][cC][rR][iI][pP][tT]['\"]%s*>", + "<%s*/[sS][cC][rR][iI][pP][tT]>" + }, + syntax = ".js", + type = "function" + }, + { + pattern = { + "<%s*[sS][cC][rR][iI][pP][tT]%s*>", + "<%s*/%s*[sS][cC][rR][iI][pP][tT]>" + }, + syntax = ".js", + type = "function" + }, + { + pattern = { + "<%s*[sS][tT][yY][lL][eE][^>]*>", + "<%s*/%s*[sS][tT][yY][lL][eE]%s*>" + }, + syntax = ".css", + type = "function" + }, + -- Comments + { pattern = { "" }, type = "comment" }, + -- Tags + { pattern = "%f[^<]![%a_][%w_]*", type = "keyword2" }, + { pattern = "%f[^<][%a_][%w_]*", type = "function" }, + { pattern = "%f[^<]/[%a_][%w_]*", type = "function" }, + -- Attributes + { + pattern = "[a-z%-]+%s*()=%s*()\".-\"", + type = { "keyword", "operator", "string" } + }, + { + pattern = "[a-z%-]+%s*()=%s*()'.-'", + type = { "keyword", "operator", "string" } + }, + { + pattern = "[a-z%-]+%s*()=%s*()%-?%d[%d%.]*", + type = { "keyword", "operator", "number" } + }, + -- Entities + { pattern = "&#?[a-zA-Z0-9]+;", type = "keyword2" }, + + ---- Markdown rules + -- math + { pattern = { "%$%$", "%$%$", "\\" }, type = "string", syntax = ".tex"}, + { regex = { "\\$", [[\$|(?=\\*\n)]], "\\" }, type = "string", syntax = ".tex"}, + -- code blocks { pattern = { "```c++", "```" }, type = "string", syntax = ".cpp" }, + { pattern = { "```cpp", "```" }, type = "string", syntax = ".cpp" }, { pattern = { "```python", "```" }, type = "string", syntax = ".py" }, { pattern = { "```ruby", "```" }, type = "string", syntax = ".rb" }, { pattern = { "```perl", "```" }, type = "string", syntax = ".pl" }, { pattern = { "```php", "```" }, type = "string", syntax = ".php" }, { pattern = { "```javascript", "```" }, type = "string", syntax = ".js" }, + { pattern = { "```json", "```" }, type = "string", syntax = ".js" }, { pattern = { "```html", "```" }, type = "string", syntax = ".html" }, + { pattern = { "```ini", "```" }, type = "string", syntax = ".ini" }, { pattern = { "```xml", "```" }, type = "string", syntax = ".xml" }, { pattern = { "```css", "```" }, type = "string", syntax = ".css" }, { pattern = { "```lua", "```" }, type = "string", syntax = ".lua" }, { pattern = { "```bash", "```" }, type = "string", syntax = ".sh" }, + { pattern = { "```sh", "```" }, type = "string", syntax = ".sh" }, { pattern = { "```java", "```" }, type = "string", syntax = ".java" }, { pattern = { "```c#", "```" }, type = "string", syntax = ".cs" }, { pattern = { "```cmake", "```" }, type = "string", syntax = ".cmake" }, { pattern = { "```d", "```" }, type = "string", syntax = ".d" }, { pattern = { "```glsl", "```" }, type = "string", syntax = ".glsl" }, { pattern = { "```c", "```" }, type = "string", syntax = ".c" }, - { pattern = { "```julia", "```" }, type = "string", syntax = ".jl" }, - { pattern = { "```rust", "```" }, type = "string", syntax = ".rs" }, - { pattern = { "```dart", "```" }, type = "string", syntax = ".dart" }, + { pattern = { "```julia", "```" }, type = "string", syntax = ".jl" }, + { pattern = { "```rust", "```" }, type = "string", syntax = ".rs" }, + { pattern = { "```dart", "```" }, type = "string", syntax = ".dart" }, { pattern = { "```v", "```" }, type = "string", syntax = ".v" }, - { pattern = { "```toml", "```" }, type = "string", syntax = ".toml" }, - { pattern = { "```yaml", "```" }, type = "string", syntax = ".yaml" }, - { pattern = { "```php", "```" }, type = "string", syntax = ".php" }, - { pattern = { "```nim", "```" }, type = "string", syntax = ".nim" }, - { pattern = { "```typescript", "```" }, type = "string", syntax = ".ts" }, - { pattern = { "```rescript", "```" }, type = "string", syntax = ".res" }, - { pattern = { "```moon", "```" }, type = "string", syntax = ".moon" }, - { pattern = { "```go", "```" }, type = "string", syntax = ".go" }, - { pattern = { "```lobster", "```" }, type = "string", syntax = ".lobster" }, - { pattern = { "```liquid", "```" }, type = "string", syntax = ".liquid" }, - { pattern = { "```", "```" }, type = "string" }, - { pattern = { "``", "``", "\\" }, type = "string" }, - { pattern = { "`", "`", "\\" }, type = "string" }, - { pattern = { "~~", "~~", "\\" }, type = "keyword2" }, - { pattern = "%-%-%-+", type = "comment" }, - { pattern = "%*%s+", type = "operator" }, - { pattern = { "%*", "[%*\n]", "\\" }, type = "operator" }, - { pattern = { "%_", "[%_\n]", "\\" }, type = "keyword2" }, - { pattern = "#.-\n", type = "keyword" }, - { pattern = "!?%[.-%]%(.-%)", type = "function" }, + { pattern = { "```toml", "```" }, type = "string", syntax = ".toml" }, + { pattern = { "```yaml", "```" }, type = "string", syntax = ".yaml" }, + { pattern = { "```nim", "```" }, type = "string", syntax = ".nim" }, + { pattern = { "```typescript", "```" }, type = "string", syntax = ".ts" }, + { pattern = { "```rescript", "```" }, type = "string", syntax = ".res" }, + { pattern = { "```moon", "```" }, type = "string", syntax = ".moon" }, + { pattern = { "```go", "```" }, type = "string", syntax = ".go" }, + { pattern = { "```lobster", "```" }, type = "string", syntax = ".lobster" }, + { pattern = { "```liquid", "```" }, type = "string", syntax = ".liquid" }, + { pattern = { "```", "```" }, type = "string" }, + { pattern = { "``", "``" }, type = "string" }, + { pattern = { "%f[\\`]%`[%S]", "`" }, type = "string" }, + -- strike + { pattern = { "~~", "~~" }, type = "keyword2" }, + -- highlight + { pattern = { "==", "==" }, type = "literal" }, + -- lines + { pattern = "^%-%-%-+$" , type = "comment" }, + { pattern = "^%*%*%*+$", type = "comment" }, + { pattern = "^___+$", type = "comment" }, + -- bold and italic + { pattern = { "%*%*%*%S", "%*%*%*" }, type = "markdown_bold_italic" }, + { pattern = { "%*%*%S", "%*%*" }, type = "markdown_bold" }, + -- handle edge case where asterisk can be at end of line and not close + { + pattern = { "%f[\\%*]%*[%S]", "%*%f[^%*]" }, + type = "markdown_italic" + }, + -- alternative bold italic formats + { pattern = "^___[%s%p%w]+___%s" , type = "markdown_bold_italic" }, + { pattern = "^__[%s%p%w]+__%s" , type = "markdown_bold" }, + { pattern = "^_[%s%p%w]+_%s" , type = "markdown_italic" }, + -- heading with custom id + { + pattern = "^#+%s[%w%s%p]+(){()#[%w%-]+()}", + type = { "keyword", "function", "string", "function" } + }, + -- headings + { pattern = "^#+%s.+$", type = "keyword" }, + -- superscript and subscript + { + pattern = "%^()%d+()%^", + type = { "function", "number", "function" } + }, + { + pattern = "%~()%d+()%~", + type = { "function", "number", "function" } + }, + -- definitions + { pattern = "^:%s.+", type = "function" }, + -- emoji + { pattern = ":[a-zA-Z0-9_%-]+:", type = "literal" }, + -- images and link + { + pattern = "!?%[!?%[()["..in_squares_match.."]+()%]%(()["..in_parenthesis_match.."]+()%)%]%(()["..in_parenthesis_match.."]+()%)", + type = { "function", "string", "function", "number", "function", "number", "function" } + }, + { + pattern = "!?%[!?%[?()["..in_squares_match.."]+()%]?%]%(()["..in_parenthesis_match.."]+()%)", + type = { "function", "string", "function", "number", "function" } + }, + -- reference links + { + pattern = "%[()["..in_squares_match.."]+()%] *()%[()["..in_squares_match.."]+()%]", + type = { "function", "string", "function", "function", "number", "function" } + }, + { + pattern = "!?%[%^?()["..in_squares_match.."]+()%]", + type = { "function", "number", "function" } + }, + -- url's and email + { + pattern = "<[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+%.[a-zA-Z0-9-.]+>", + type = "function" + }, + { pattern = "", type = "function" }, { pattern = "https?://%S+", type = "function" }, + -- optimize consecutive dashes used in tables + { pattern = "%-+", type = "normal" }, }, symbols = { }, } + +-- Adjust the color on theme changes +core.add_thread(function() + while true do + if initial_color ~= style.syntax["keyword2"] then + for _, attr in pairs({"bold", "italic", "bold_italic"}) do + style.syntax["markdown_"..attr] = style.syntax["keyword2"] + end + initial_color = style.syntax["keyword2"] + end + coroutine.yield(1) + end +end) diff --git a/data/plugins/language_python.lua b/data/plugins/language_python.lua index 8bc6fbd4..743e990a 100644 --- a/data/plugins/language_python.lua +++ b/data/plugins/language_python.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { @@ -6,9 +6,14 @@ syntax.add { files = { "%.py$", "%.pyw$", "%.rpy$" }, headers = "^#!.*[ /]python", comment = "#", + block_comment = { '"""', '"""' }, patterns = { - { pattern = { "#", "\n" }, type = "comment" }, + { pattern = "#.*", type = "comment" }, + { pattern = { '^%s*"""', '"""' }, type = "comment" }, + { pattern = '[uUrR]%f["]', type = "keyword" }, + { pattern = "class%s+()[%a_][%w_]*", type = {"keyword", "keyword2"} }, { pattern = { '[ruU]?"""', '"""'; '\\' }, type = "string" }, + { pattern = { "[ruU]?'''", "'''", '\\' }, type = "string" }, { pattern = { '[ruU]?"', '"', '\\' }, type = "string" }, { pattern = { "[ruU]?'", "'", '\\' }, type = "string" }, { pattern = "0x[%da-fA-F]+", type = "number" }, @@ -28,6 +33,8 @@ syntax.add { ["lambda"] = "keyword", ["try"] = "keyword", ["def"] = "keyword", + ["async"] = "keyword", + ["await"] = "keyword", ["from"] = "keyword", ["nonlocal"] = "keyword", ["while"] = "keyword", @@ -40,6 +47,8 @@ syntax.add { ["if"] = "keyword", ["or"] = "keyword", ["else"] = "keyword", + ["match"] = "keyword", + ["case"] = "keyword", ["import"] = "keyword", ["pass"] = "keyword", ["break"] = "keyword", diff --git a/data/plugins/language_xml.lua b/data/plugins/language_xml.lua index c858d3cf..125c260e 100644 --- a/data/plugins/language_xml.lua +++ b/data/plugins/language_xml.lua @@ -1,10 +1,11 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { name = "XML", files = { "%.xml$" }, headers = "<%?xml", + block_comment = { "" }, patterns = { { pattern = { "" }, type = "comment" }, { pattern = { '%f[^>][^<]', '%f[<]' }, type = "normal" }, diff --git a/data/plugins/lineguide.lua b/data/plugins/lineguide.lua index 96745659..efe606dd 100644 --- a/data/plugins/lineguide.lua +++ b/data/plugins/lineguide.lua @@ -1,21 +1,115 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 +local common = require "core.common" +local command = require "core.command" local config = require "core.config" local style = require "core.style" local DocView = require "core.docview" local CommandView = require "core.commandview" -local draw_overlay = DocView.draw_overlay +config.plugins.lineguide = common.merge({ + enabled = false, + width = 2, + rulers = { + -- 80, + -- 100, + -- 120, + config.line_limit + }, + -- The config specification used by gui generators + config_spec = { + name = "Line Guide", + { + label = "Enabled", + description = "Disable or enable drawing of the line guide.", + path = "enabled", + type = "toggle", + default = true + }, + { + label = "Width", + description = "Width in pixels of the line guide.", + path = "width", + type = "number", + default = 2, + min = 1 + }, + { + label = "Ruler Positions", + description = "The different column numbers for the line guides to draw.", + path = "rulers", + type = "list_strings", + default = { tostring(config.line_limit) or "80" }, + get_value = function(rulers) + if type(rulers) == "table" then + local new_rulers = {} + for _, ruler in ipairs(rulers) do + table.insert(new_rulers, tostring(ruler)) + end + return new_rulers + else + return { tostring(config.line_limit) } + end + end, + set_value = function(rulers) + local new_rulers = {} + for _, ruler in ipairs(rulers) do + local number = tonumber(ruler) + if number then + table.insert(new_rulers, number) + end + end + if #new_rulers == 0 then + table.insert(new_rulers, config.line_limit) + end + return new_rulers + end + } + } +}, config.plugins.lineguide) -function DocView:draw_overlay(...) - if not self:is(CommandView) then - local offset = self:get_font():get_width("n") * config.line_limit - local x = self:get_line_screen_position(1) + offset - local y = self.position.y - local w = math.ceil(SCALE * 1) - local h = self.size.y - - local color = style.guide or style.selection - renderer.draw_rect(x, y, w, h, color) +local function get_ruler(v) + local result = nil + if type(v) == 'number' then + result = { columns = v } + elseif type(v) == 'table' then + result = v end - draw_overlay(self, ...) + return result end + +local draw_overlay = DocView.draw_overlay +function DocView:draw_overlay(...) + draw_overlay(self, ...) + + if + type(config.plugins.lineguide) == "table" + and + config.plugins.lineguide.enabled + and + not self:is(CommandView) + then + local line_x = self:get_line_screen_position(1) + local character_width = self:get_font():get_width("n") + local ruler_width = config.plugins.lineguide.width + local ruler_color = style.guide or style.selection + + for k,v in ipairs(config.plugins.lineguide.rulers) do + local ruler = get_ruler(v) + + if ruler then + local x = line_x + (character_width * ruler.columns) + local y = self.position.y + local w = ruler_width + local h = self.size.y + + renderer.draw_rect(x, y, w, h, ruler.color or ruler_color) + end + end + end +end + +command.add(nil, { + ["lineguide:toggle"] = function() + config.plugins.lineguide.enabled = not config.plugins.lineguide.enabled + end +}) diff --git a/data/plugins/linewrapping.lua b/data/plugins/linewrapping.lua new file mode 100644 index 00000000..66b303ee --- /dev/null +++ b/data/plugins/linewrapping.lua @@ -0,0 +1,581 @@ +-- mod-version:3 --priority:10 +local core = require "core" +local common = require "core.common" +local DocView = require "core.docview" +local Doc = require "core.doc" +local style = require "core.style" +local config = require "core.config" +local command = require "core.command" +local keymap = require "core.keymap" +local translate = require "core.doc.translate" + + +config.plugins.linewrapping = common.merge({ + -- The type of wrapping to perform. Can be "letter" or "word". + mode = "letter", + -- If nil, uses the DocView's size, otherwise, uses this exact width. Can be a function. + width_override = nil, + -- Whether or not to draw a guide + guide = true, + -- Whether or not we should indent ourselves like the first line of a wrapped block. + indent = true, + -- Whether or not to enable wrapping by default when opening files. + enable_by_default = false, + -- Requires tokenization + require_tokenization = false, + -- The config specification used by gui generators + config_spec = { + name = "Line Wrapping", + { + label = "Mode", + description = "The type of wrapping to perform.", + path = "mode", + type = "selection", + default = "letter", + values = { + {"Letters", "letter"}, + {"Words", "word"} + } + }, + { + label = "Guide", + description = "Whether or not to draw a guide.", + path = "guide", + type = "toggle", + default = true + }, + { + label = "Indent", + description = "Whether or not to follow the indentation of wrapped line.", + path = "indent", + type = "toggle", + default = true + }, + { + label = "Enable by Default", + description = "Whether or not to enable wrapping by default when opening files.", + path = "enable_by_default", + type = "toggle", + default = false + }, + { + label = "Require Tokenization", + description = "Use tokenization when applying wrapping.", + path = "require_tokenization", + type = "toggle", + default = false + } + } +}, config.plugins.linewrapping) + +local LineWrapping = {} + +-- Optimzation function. The tokenizer is relatively slow (at present), and +-- so if we don't need to run it, should be run sparingly. +local function spew_tokens(doc, line) if line < math.huge then return math.huge, "normal", doc.lines[line] end end +local function get_tokens(doc, line) + if config.plugins.linewrapping.require_tokenization then + return doc.highlighter:each_token(line) + end + return spew_tokens, doc, line +end + +-- Computes the breaks for a given line, width and mode. Returns a list of columns +-- at which the line should be broken. +function LineWrapping.compute_line_breaks(doc, default_font, line, width, mode) + local xoffset, last_i, i, last_space, last_width, begin_width = 0, 1, 1, nil, 0, 0 + local splits = { 1 } + for idx, type, text in get_tokens(doc, line) do + local font = style.syntax_fonts[type] or default_font + if idx == 1 or idx == math.huge and config.plugins.linewrapping.indent then + local _, indent_end = text:find("^%s+") + if indent_end then begin_width = font:get_width(text:sub(1, indent_end)) end + end + local w = font:get_width(text) + if xoffset + w > width then + for char in common.utf8_chars(text) do + w = font:get_width(char) + xoffset = xoffset + w + if xoffset > width then + if mode == "word" and last_space then + table.insert(splits, last_space + 1) + xoffset = w + begin_width + (xoffset - last_width) + else + table.insert(splits, i) + xoffset = w + begin_width + end + last_space = nil + elseif char == ' ' then + last_space = i + last_width = xoffset + end + i = i + #char + end + else + xoffset = xoffset + w + i = i + #text + end + end + return splits, begin_width +end + +-- breaks are held in a single table that contains n*2 elements, where n is the amount of line breaks. +-- each element represents line and column of the break. line_offset will check from the specified line +-- if the first line has not changed breaks, it will stop there. +function LineWrapping.reconstruct_breaks(docview, default_font, width, line_offset) + if width ~= math.huge then + local doc = docview.doc + -- two elements per wrapped line; first maps to original line number, second to column number. + docview.wrapped_lines = { } + -- one element per actual line; maps to the first index of in wrapped_lines for this line + docview.wrapped_line_to_idx = { } + -- one element per actual line; gives the indent width for the acutal line + docview.wrapped_line_offsets = { } + docview.wrapped_settings = { ["width"] = width, ["font"] = default_font } + for i = line_offset or 1, #doc.lines do + local breaks, offset = LineWrapping.compute_line_breaks(doc, default_font, i, width, config.plugins.linewrapping.mode) + table.insert(docview.wrapped_line_offsets, offset) + for k, col in ipairs(breaks) do + table.insert(docview.wrapped_lines, i) + table.insert(docview.wrapped_lines, col) + end + end + -- list of indices for wrapped_lines, that are based on original line number + -- holds the index to the first in the wrapped_lines list. + local last_wrap = nil + for i = 1, #docview.wrapped_lines, 2 do + if not last_wrap or last_wrap ~= docview.wrapped_lines[i] then + table.insert(docview.wrapped_line_to_idx, (i + 1) / 2) + last_wrap = docview.wrapped_lines[i] + end + end + else + docview.wrapped_lines = nil + docview.wrapped_line_to_idx = nil + docview.wrapped_line_offsets = nil + docview.wrapped_settings = nil + end +end + +-- When we have an insertion or deletion, we have four sections of text. +-- 1. The unaffected section, located prior to the cursor. This is completely ignored. +-- 2. The beginning of the affected line prior to the insertion or deletion. Begins on column 1 of the selection. +-- 3. The removed/pasted lines. +-- 4. Every line after the modification, begins one line after the selection in the initial document. +function LineWrapping.update_breaks(docview, old_line1, old_line2, net_lines) + -- Step 1: Determine the index for the line for #2. + local old_idx1 = docview.wrapped_line_to_idx[old_line1] or 1 + -- Step 2: Determine the index of the line for #4. + local old_idx2 = (docview.wrapped_line_to_idx[old_line2 + 1] or ((#docview.wrapped_lines / 2) + 1)) - 1 + -- Step 3: Remove all old breaks for the old lines from the table, and all old widths from wrapped_line_offsets. + local offset = (old_idx1 - 1) * 2 + 1 + for i = old_idx1, old_idx2 do + table.remove(docview.wrapped_lines, offset) + table.remove(docview.wrapped_lines, offset) + end + for i = old_line1, old_line2 do + table.remove(docview.wrapped_line_offsets, old_line1) + end + -- Step 4: Shift the line number of wrapped_lines past #4 by the amount of inserted/deleted lines. + if net_lines ~= 0 then + for i = offset, #docview.wrapped_lines, 2 do + docview.wrapped_lines[i] = docview.wrapped_lines[i] + net_lines + end + end + -- Step 5: Compute the breaks and offsets for the lines for #2 and #3. Insert them into the table. + local new_line1 = old_line1 + local new_line2 = old_line2 + net_lines + for line = new_line1, new_line2 do + local breaks, begin_width = LineWrapping.compute_line_breaks(docview.doc, docview.wrapped_settings.font, line, docview.wrapped_settings.width, config.plugins.linewrapping.mode) + table.insert(docview.wrapped_line_offsets, line, begin_width) + for i,b in ipairs(breaks) do + table.insert(docview.wrapped_lines, offset, b) + table.insert(docview.wrapped_lines, offset, line) + offset = offset + 2 + end + end + -- Step 6: Recompute the wrapped_line_to_idx cache from #2. + local line = old_line1 + offset = (old_idx1 - 1) * 2 + 1 + while offset < #docview.wrapped_lines do + if docview.wrapped_lines[offset + 1] == 1 then + docview.wrapped_line_to_idx[line] = ((offset - 1) / 2) + 1 + line = line + 1 + end + offset = offset + 2 + end + while line <= #docview.wrapped_line_to_idx do + table.remove(docview.wrapped_line_to_idx) + end +end + +-- Draws a guide if applicable to show where wrapping is occurring. +function LineWrapping.draw_guide(docview) + if config.plugins.linewrapping.guide and docview.wrapped_settings.width ~= math.huge then + local x, y = docview:get_content_offset() + local gw = docview:get_gutter_width() + renderer.draw_rect(x + gw + docview.wrapped_settings.width, y, 1, core.root_view.size.y, style.selection) + end +end + +function LineWrapping.update_docview_breaks(docview) + local x,y,w,h = docview:get_scrollbar_rect() + local width = (type(config.plugins.linewrapping.width_override) == "function" and config.plugins.linewrapping.width_override(docview)) + or config.plugins.linewrapping.width_override or (docview.size.x - docview:get_gutter_width() - w) + if (not docview.wrapped_settings or docview.wrapped_settings.width == nil or width ~= docview.wrapped_settings.width) then + docview.scroll.to.x = 0 + LineWrapping.reconstruct_breaks(docview, docview:get_font(), width) + end +end + +local function get_idx_line_col(docview, idx) + local doc = docview.doc + if not docview.wrapped_settings then + if idx > #doc.lines then return #doc.lines, #doc.lines[#doc.lines] + 1 end + return idx, 1 + end + if idx < 1 then return 1, 1 end + local offset = (idx - 1) * 2 + 1 + if offset > #docview.wrapped_lines then return #doc.lines, #doc.lines[#doc.lines] + 1 end + return docview.wrapped_lines[offset], docview.wrapped_lines[offset + 1] +end + +local function get_idx_line_length(docview, idx) + local doc = docview.doc + if not docview.wrapped_settings then + if idx > #doc.lines then return #doc.lines[#doc.lines] + 1 end + return #doc.lines[idx] + end + local offset = (idx - 1) * 2 + 1 + local start = docview.wrapped_lines[offset + 1] + if docview.wrapped_lines[offset + 2] and docview.wrapped_lines[offset + 2] == docview.wrapped_lines[offset] then + return docview.wrapped_lines[offset + 3] - docview.wrapped_lines[offset + 1] + else + return #doc.lines[docview.wrapped_lines[offset]] - docview.wrapped_lines[offset + 1] + 1 + end +end + +local function get_total_wrapped_lines(docview) + if not docview.wrapped_settings then return docview.doc and #docview.doc.lines end + return #docview.wrapped_lines / 2 +end + +-- If line end, gives the end of an index line, rather than the first character of the next line. +local function get_line_idx_col_count(docview, line, col, line_end, ndoc) + local doc = docview.doc + if not docview.wrapped_settings then return common.clamp(line, 1, #doc.lines), col, 1, 1 end + if line > #doc.lines then return get_line_idx_col_count(docview, #doc.lines, #doc.lines[#doc.lines] + 1) end + line = math.max(line, 1) + local idx = docview.wrapped_line_to_idx[line] or 1 + local ncol, scol = 1, 1 + if col then + local i = idx + 1 + while line == docview.wrapped_lines[(i - 1) * 2 + 1] and col >= docview.wrapped_lines[(i - 1) * 2 + 2] do + local nscol = docview.wrapped_lines[(i - 1) * 2 + 2] + if line_end and col == nscol then + break + end + scol = nscol + i = i + 1 + idx = idx + 1 + end + ncol = (col - scol) + 1 + end + local count = (docview.wrapped_line_to_idx[line + 1] or (get_total_wrapped_lines(docview) + 1)) - (docview.wrapped_line_to_idx[line] or get_total_wrapped_lines(docview)) + return idx, ncol, count, scol +end + +local function get_line_col_from_index_and_x(docview, idx, x) + local doc = docview.doc + local line, col = get_idx_line_col(docview, idx) + if idx < 1 then return 1, 1 end + local xoffset, last_i, i = (col ~= 1 and docview.wrapped_line_offsets[line] or 0), col, 1 + if x < xoffset then return line, col end + local default_font = docview:get_font() + for _, type, text in doc.highlighter:each_token(line) do + local font, w = style.syntax_fonts[type] or default_font, 0 + for char in common.utf8_chars(text) do + if i >= col then + if xoffset >= x then + return line, (xoffset - x > (w / 2) and last_i or i) + end + w = font:get_width(char) + xoffset = xoffset + w + end + last_i = i + i = i + #char + end + end + return line, #doc.lines[line] +end + + +local open_files = {} + +local old_doc_insert = Doc.raw_insert +function Doc:raw_insert(line, col, text, undo_stack, time) + local old_lines = #self.lines + old_doc_insert(self, line, col, text, undo_stack, time) + if open_files[self] then + for i,docview in ipairs(open_files[self]) do + if docview.wrapped_settings then + local lines = #self.lines - old_lines + LineWrapping.update_breaks(docview, line, line, lines) + end + end + end +end + +local old_doc_remove = Doc.raw_remove +function Doc:raw_remove(line1, col1, line2, col2, undo_stack, time) + local old_lines = #self.lines + old_doc_remove(self, line1, col1, line2, col2, undo_stack, time) + if open_files[self] then + for i,docview in ipairs(open_files[self]) do + if docview.wrapped_settings then + local lines = #self.lines - old_lines + LineWrapping.update_breaks(docview, line1, line2, lines) + end + end + end +end + +local old_doc_update = DocView.update +function DocView:update() + old_doc_update(self) + if self.wrapped_settings and self.size.x > 0 then + LineWrapping.update_docview_breaks(self) + end +end + +function DocView:get_scrollable_size() + if not config.scroll_past_end then + return self:get_line_height() * get_total_wrapped_lines(self) + style.padding.y * 2 + end + return self:get_line_height() * (get_total_wrapped_lines(self) - 1) + self.size.y +end + +local old_new = DocView.new +function DocView:new(doc) + old_new(self, doc) + if not open_files[doc] then open_files[doc] = {} end + table.insert(open_files[doc], self) + if config.plugins.linewrapping.enable_by_default then + LineWrapping.update_docview_breaks(self) + end +end + +local old_scroll_to_make_visible = DocView.scroll_to_make_visible +function DocView:scroll_to_make_visible(line, col) + old_scroll_to_make_visible(self, line, col) + if self.wrapped_settings then self.scroll.to.x = 0 end +end + +local old_get_visible_line_range = DocView.get_visible_line_range +function DocView:get_visible_line_range() + if not self.wrapped_settings then return old_get_visible_line_range(self) end + local x, y, x2, y2 = self:get_content_bounds() + local lh = self:get_line_height() + local minline = get_idx_line_col(self, math.max(1, math.floor(y / lh))) + local maxline = get_idx_line_col(self, math.min(get_total_wrapped_lines(self), math.floor(y2 / lh) + 1)) + return minline, maxline +end + +local old_get_x_offset_col = DocView.get_x_offset_col +function DocView:get_x_offset_col(line, x) + if not self.wrapped_settings then return old_get_x_offset_col(self, line, x) end + local idx = get_line_idx_col_count(self, line) + return get_line_col_from_index_and_x(self, idx, x) +end + +-- If line end is true, returns the end of the previous line, in a multi-line break. +local old_get_col_x_offset = DocView.get_col_x_offset +function DocView:get_col_x_offset(line, col, line_end) + if not self.wrapped_settings then return old_get_col_x_offset(self, line, col) end + local idx, ncol, count, scol = get_line_idx_col_count(self, line, col, line_end) + local xoffset, i = (scol ~= 1 and self.wrapped_line_offsets[line] or 0), 1 + local default_font = self:get_font() + for _, type, text in self.doc.highlighter:each_token(line) do + if i + #text >= scol then + if i < scol then + text = text:sub(scol - i + 1) + i = scol + end + local font = style.syntax_fonts[type] or default_font + for char in common.utf8_chars(text) do + if i >= col then + return xoffset + end + xoffset = xoffset + font:get_width(char) + i = i + #char + end + else + i = i + #text + end + end + return xoffset +end + +local old_get_line_screen_position = DocView.get_line_screen_position +function DocView:get_line_screen_position(line, col) + if not self.wrapped_settings then return old_get_line_screen_position(self, line, col) end + local idx, ncol, count = get_line_idx_col_count(self, line, col) + local x, y = self:get_content_offset() + local lh = self:get_line_height() + local gw = self:get_gutter_width() + return x + gw + (col and self:get_col_x_offset(line, col) or 0), y + (idx-1) * lh + style.padding.y +end + +local old_resolve_screen_position = DocView.resolve_screen_position +function DocView:resolve_screen_position(x, y) + if not self.wrapped_settings then return old_resolve_screen_position(self, x, y) end + local ox, oy = self:get_line_screen_position(1) + local idx = common.clamp(math.floor((y - oy) / self:get_line_height()) + 1, 1, get_total_wrapped_lines(self)) + return get_line_col_from_index_and_x(self, idx, x - ox) +end + +local old_draw_line_text = DocView.draw_line_text +function DocView:draw_line_text(line, x, y) + if not self.wrapped_settings then return old_draw_line_text(self, line, x, y) end + local default_font = self:get_font() + local tx, ty, begin_width = x, y + self:get_line_text_y_offset(), self.wrapped_line_offsets[line] + local lh = self:get_line_height() + local idx, _, count = get_line_idx_col_count(self, line) + local total_offset = 1 + for _, type, text in self.doc.highlighter:each_token(line) do + local color = style.syntax[type] + local font = style.syntax_fonts[type] or default_font + local token_offset = 1 + -- Split tokens if we're at the end of the document. + while text ~= nil and token_offset <= #text do + local next_line, next_line_start_col = get_idx_line_col(self, idx + 1) + if next_line ~= line then + next_line_start_col = #self.doc.lines[line] + end + local max_length = next_line_start_col - total_offset + local rendered_text = text:sub(token_offset, token_offset + max_length - 1) + tx = renderer.draw_text(font, rendered_text, tx, ty, color) + total_offset = total_offset + #rendered_text + if total_offset ~= next_line_start_col or max_length == 0 then break end + token_offset = token_offset + #rendered_text + idx = idx + 1 + tx, ty = x + begin_width, ty + lh + end + end + return lh * count +end + +local old_draw_line_body = DocView.draw_line_body +function DocView:draw_line_body(line, x, y) + if not self.wrapped_settings then return old_draw_line_body(self, line, x, y) end + local lh = self:get_line_height() + local idx0 = get_line_idx_col_count(self, line) + for lidx, line1, col1, line2, col2 in self.doc:get_selections(true) do + if line >= line1 and line <= line2 then + if line1 ~= line then col1 = 1 end + if line2 ~= line then col2 = #self.doc.lines[line] + 1 end + if col1 ~= col2 then + local idx1, ncol1 = get_line_idx_col_count(self, line, col1) + local idx2, ncol2 = get_line_idx_col_count(self, line, col2) + for i = idx1, idx2 do + local x1, x2 = x + (idx1 == i and self:get_col_x_offset(line1, col1) or 0) + if idx2 == i then + x2 = x + self:get_col_x_offset(line, col2) + else + x2 = x + self:get_col_x_offset(line, get_idx_line_length(self, i, line) + 1, true) + end + renderer.draw_rect(x1, y + (i - idx0) * lh, x2 - x1, lh, style.selection) + end + end + end + end + local draw_highlight = nil + for lidx, line1, col1, line2, col2 in self.doc:get_selections(true) do + -- draw line highlight if caret is on this line + if draw_highlight ~= false and config.highlight_current_line + and line1 == line and core.active_view == self then + draw_highlight = (line1 == line2 and col1 == col2) + end + end + if draw_highlight then + local _, _, count = get_line_idx_col_count(self, line) + for i=1,count do + self:draw_line_highlight(x + self.scroll.x, y + lh * (i - 1)) + end + end + -- draw line's text + return self:draw_line_text(line, x, y) +end + +local old_draw = DocView.draw +function DocView:draw() + old_draw(self) + if self.wrapped_settings then + LineWrapping.draw_guide(self) + end +end + +local old_draw_line_gutter = DocView.draw_line_gutter +function DocView:draw_line_gutter(line, x, y, width) + local lh = self:get_line_height() + local _, _, count = get_line_idx_col_count(self, line) + return (old_draw_line_gutter(self, line, x, y, width) or lh) * count +end + +local old_translate_end_of_line = translate.end_of_line +function translate.end_of_line(doc, line, col) + if not core.active_view or core.active_view.doc ~= doc or not core.active_view.wrapped_settings then old_translate_end_of_line(doc, line, col) end + local idx, ncol = get_line_idx_col_count(core.active_view, line, col) + local nline, ncol2 = get_idx_line_col(core.active_view, idx + 1) + if nline ~= line then return line, math.huge end + return line, ncol2 - 1 +end + +local old_translate_start_of_line = translate.start_of_line +function translate.start_of_line(doc, line, col) + if not core.active_view or core.active_view.doc ~= doc or not core.active_view.wrapped_settings then old_translate_start_of_line(doc, line, col) end + local idx, ncol = get_line_idx_col_count(core.active_view, line, col) + local nline, ncol2 = get_idx_line_col(core.active_view, idx - 1) + if nline ~= line then return line, 1 end + return line, ncol2 + 1 +end + +local old_previous_line = DocView.translate.previous_line +function DocView.translate.previous_line(doc, line, col, dv) + if not dv.wrapped_settings then return old_previous_line(doc, line, col, dv) end + local idx, ncol = get_line_idx_col_count(dv, line, col) + return get_line_col_from_index_and_x(dv, idx - 1, dv:get_col_x_offset(line, col)) +end + +local old_next_line = DocView.translate.next_line +function DocView.translate.next_line(doc, line, col, dv) + if not dv.wrapped_settings then return old_next_line(doc, line, col, dv) end + local idx, ncol = get_line_idx_col_count(dv, line, col) + return get_line_col_from_index_and_x(dv, idx + 1, dv:get_col_x_offset(line, col)) +end + +command.add(nil, { + ["line-wrapping:enable"] = function() + if core.active_view and core.active_view.doc then + LineWrapping.update_docview_breaks(core.active_view) + end + end, + ["line-wrapping:disable"] = function() + if core.active_view and core.active_view.doc then + LineWrapping.reconstruct_breaks(core.active_view, core.active_view:get_font(), math.huge) + end + end, + ["line-wrapping:toggle"] = function() + if core.active_view and core.active_view.doc and core.active_view.wrapped_settings then + command.perform("line-wrapping:disable") + else + command.perform("line-wrapping:enable") + end + end +}) + +keymap.add { + ["f10"] = "line-wrapping:toggle", +} + +return LineWrapping diff --git a/data/plugins/macro.lua b/data/plugins/macro.lua index 2678363a..9f3b8482 100644 --- a/data/plugins/macro.lua +++ b/data/plugins/macro.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" local command = require "core.command" local keymap = require "core.keymap" diff --git a/data/plugins/projectsearch.lua b/data/plugins/projectsearch.lua index d0d75d7f..9a0b6a93 100644 --- a/data/plugins/projectsearch.lua +++ b/data/plugins/projectsearch.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" local common = require "core.common" local keymap = require "core.keymap" @@ -11,11 +11,11 @@ local ResultsView = View:extend() ResultsView.context = "session" -function ResultsView:new(text, fn) +function ResultsView:new(path, text, fn) ResultsView.super.new(self) self.scrollable = true self.brightness = 0 - self:begin_search(text, fn) + self:begin_search(path, text, fn) end @@ -45,8 +45,8 @@ local function find_all_matches_in_file(t, filename, fn) end -function ResultsView:begin_search(text, fn) - self.search_args = { text, fn } +function ResultsView:begin_search(path, text, fn) + self.search_args = { path, text, fn } self.results = {} self.last_file_idx = 1 self.query = text @@ -56,9 +56,9 @@ function ResultsView:begin_search(text, fn) core.add_thread(function() local i = 1 for dir_name, file in core.get_project_files() do - if file.type == "file" then - local path = (dir_name == core.project_dir and "" or (dir_name .. PATHSEP)) - find_all_matches_in_file(self.results, path .. file.filename, fn) + if file.type == "file" and (not path or (dir_name .. "/" .. file.filename):find(path, 1, true) == 1) then + local truncated_path = (dir_name == core.project_dir and "" or (dir_name .. PATHSEP)) + find_all_matches_in_file(self.results, truncated_path .. file.filename, fn) end self.last_file_idx = i i = i + 1 @@ -176,7 +176,7 @@ function ResultsView:draw() local text if self.searching then if files_number then - text = string.format("Searching %d%% (%d of %d files, %d matches) for %q...", + text = string.format("Searching %.f%% (%d of %d files, %d matches) for %q...", per * 100, self.last_file_idx, files_number, #self.results, self.query) else @@ -219,41 +219,72 @@ function ResultsView:draw() end -local function begin_search(text, fn) +local function begin_search(path, text, fn) if text == "" then core.error("Expected non-empty string") return end - local rv = ResultsView(text, fn) + local rv = ResultsView(path, text, fn) core.root_view:get_active_node_default():add_view(rv) end +local function get_selected_text() + local view = core.active_view + local doc = (view and view.doc) and view.doc or nil + if doc then + return doc:get_text(table.unpack({ doc:get_selection() })) + end +end + + +local function normalize_path(path) + if not path then return nil end + path = common.normalize_path(path) + for i, project_dir in ipairs(core.project_directories) do + if common.path_belongs_to(path, project_dir.name) then + return project_dir.item.filename .. PATHSEP .. common.relative_path(project_dir.name, path) + end + end + return path +end + + command.add(nil, { - ["project-search:find"] = function() - core.command_view:enter("Find Text In Project", function(text) - text = text:lower() - begin_search(text, function(line_text) - return line_text:lower():find(text, nil, true) - end) - end) + ["project-search:find"] = function(path) + core.command_view:enter("Find Text In " .. (normalize_path(path) or "Project"), { + text = get_selected_text(), + select_text = true, + submit = function(text) + text = text:lower() + begin_search(path, text, function(line_text) + return line_text:lower():find(text, nil, true) + end) + end + }) end, - ["project-search:find-regex"] = function() - core.command_view:enter("Find Regex In Project", function(text) - local re = regex.compile(text, "i") - begin_search(text, function(line_text) - return regex.cmatch(re, line_text) - end) - end) + ["project-search:find-regex"] = function(path) + core.command_view:enter("Find Regex In " .. (normalize_path(path) or "Project"), { + submit = function(text) + local re = regex.compile(text, "i") + begin_search(path, text, function(line_text) + return regex.cmatch(re, line_text) + end) + end + }) end, - ["project-search:fuzzy-find"] = function() - core.command_view:enter("Fuzzy Find Text In Project", function(text) - begin_search(text, function(line_text) - return common.fuzzy_match(line_text, text) and 1 - end) - end) + ["project-search:fuzzy-find"] = function(path) + core.command_view:enter("Fuzzy Find Text In " .. (normalize_path(path) or "Project"), { + text = get_selected_text(), + select_text = true, + submit = function(text) + begin_search(path, text, function(line_text) + return common.fuzzy_match(line_text, text) and 1 + end) + end + }) end, }) @@ -278,22 +309,22 @@ command.add(ResultsView, { ["project-search:refresh"] = function() core.active_view:refresh() end, - + ["project-search:move-to-previous-page"] = function() local view = core.active_view view.scroll.to.y = view.scroll.to.y - view.size.y end, - + ["project-search:move-to-next-page"] = function() local view = core.active_view view.scroll.to.y = view.scroll.to.y + view.size.y end, - + ["project-search:move-to-start-of-doc"] = function() local view = core.active_view view.scroll.to.y = 0 end, - + ["project-search:move-to-end-of-doc"] = function() local view = core.active_view view.scroll.to.y = view:get_scrollable_size() diff --git a/data/plugins/quote.lua b/data/plugins/quote.lua index c714cbf8..60f0cf1e 100644 --- a/data/plugins/quote.lua +++ b/data/plugins/quote.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" local command = require "core.command" local keymap = require "core.keymap" @@ -19,8 +19,8 @@ end command.add("core.docview", { - ["quote:quote"] = function() - core.active_view.doc:replace(function(text) + ["quote:quote"] = function(dv) + dv.doc:replace(function(text) return '"' .. text:gsub("[\0-\31\\\"]", replace) .. '"' end) end, diff --git a/data/plugins/reflow.lua b/data/plugins/reflow.lua index cbaa31ef..e70b06f6 100644 --- a/data/plugins/reflow.lua +++ b/data/plugins/reflow.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" local config = require "core.config" local command = require "core.command" @@ -25,8 +25,8 @@ end command.add("core.docview", { - ["reflow:reflow"] = function() - local doc = core.active_view.doc + ["reflow:reflow"] = function(dv) + local doc = dv.doc doc:replace(function(text) local prefix_set = "[^%w\n%[%](){}`'\"]*" diff --git a/data/plugins/scale.lua b/data/plugins/scale.lua index 616ee40b..0c9c8657 100644 --- a/data/plugins/scale.lua +++ b/data/plugins/scale.lua @@ -1,17 +1,20 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" local common = require "core.common" local command = require "core.command" local config = require "core.config" local keymap = require "core.keymap" local style = require "core.style" -local RootView = require "core.rootview" local CommandView = require "core.commandview" -config.plugins.scale = { +config.plugins.scale = common.merge({ + -- The method used to apply the scaling: "code", "ui" mode = "code", + -- Default scale applied at startup. + default_scale = "autodetect", + -- Allow using CTRL + MouseWheel for changing the scale. use_mousewheel = true -} +}, config.plugins.scale) local scale_steps = 0.05 @@ -44,18 +47,14 @@ local function set_scale(scale) style.tab_width = style.tab_width * s for _, name in ipairs {"font", "big_font", "icon_font", "icon_big_font", "code_font"} do - style[name] = renderer.font.copy(style[name], s * style[name]:get_size()) + style[name]:set_size(s * style[name]:get_size()) end else - style.code_font = renderer.font.copy(style.code_font, s * style.code_font:get_size()) + style.code_font:set_size(s * style.code_font:get_size()) end - for _, font in pairs(style.syntax_fonts) do - renderer.font.set_size(font, s * font:get_size()) - end - - for _, font in pairs(style.syntax_fonts) do - renderer.font.set_size(font, s * font:get_size()) + for name, font in pairs(style.syntax_fonts) do + style.syntax_fonts[name]:set_size(s * font:get_size()) end -- restore scroll positions @@ -83,6 +82,75 @@ local function dec_scale() set_scale(current_scale - scale_steps) end +if default_scale ~= config.plugins.scale.default_scale then + if type(config.plugins.scale.default_scale) == "number" then + set_scale(config.plugins.scale.default_scale) + end +end + +-- The config specification used by gui generators +config.plugins.scale.config_spec = { + name = "Scale", + { + label = "Mode", + description = "The method used to apply the scaling.", + path = "mode", + type = "selection", + default = "code", + values = { + {"Everything", "ui"}, + {"Code Only", "code"} + } + }, + { + label = "Default Scale", + description = "The scaling factor applied to lite-xl.", + path = "default_scale", + type = "selection", + default = "autodetect", + values = { + {"Autodetect", "autodetect"}, + {"80%", 0.80}, + {"90%", 0.90}, + {"100%", 1.00}, + {"110%", 1.10}, + {"120%", 1.20}, + {"125%", 1.25}, + {"130%", 1.30}, + {"140%", 1.40}, + {"150%", 1.50}, + {"175%", 1.75}, + {"200%", 2.00}, + {"250%", 2.50}, + {"300%", 3.00} + }, + on_apply = function(value) + if type(value) == "string" then value = default_scale end + if value ~= current_scale then + set_scale(value) + end + end + }, + { + label = "Use MouseWheel", + description = "Allow using CTRL + MouseWheel for changing the scale.", + path = "use_mousewheel", + type = "toggle", + default = true, + on_apply = function(enabled) + if enabled then + keymap.add { + ["ctrl+wheelup"] = "scale:increase", + ["ctrl+wheeldown"] = "scale:decrease" + } + else + keymap.unbind("ctrl+wheelup", "scale:increase") + keymap.unbind("ctrl+wheeldown", "scale:decrease") + end + end + } +} + command.add(nil, { ["scale:reset" ] = function() res_scale() end, @@ -93,11 +161,16 @@ command.add(nil, { keymap.add { ["ctrl+0"] = "scale:reset", ["ctrl+-"] = "scale:decrease", - ["ctrl+="] = "scale:increase", - ["ctrl+wheelup"] = "scale:increase", - ["ctrl+wheeldown"] = "scale:decrease" + ["ctrl+="] = "scale:increase" } +if config.plugins.scale.use_mousewheel then + keymap.add { + ["ctrl+wheelup"] = "scale:increase", + ["ctrl+wheeldown"] = "scale:decrease" + } +end + return { ["set"] = set_scale, ["get"] = get_scale, diff --git a/data/plugins/tabularize.lua b/data/plugins/tabularize.lua index 4cdae6ea..5185fbf6 100644 --- a/data/plugins/tabularize.lua +++ b/data/plugins/tabularize.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" local command = require "core.command" local translate = require "core.doc.translate" @@ -41,21 +41,23 @@ end command.add("core.docview", { - ["tabularize:tabularize"] = function() - core.command_view:enter("Tabularize On Delimiter", function(delim) - if delim == "" then delim = " " end + ["tabularize:tabularize"] = function(dv) + core.command_view:enter("Tabularize On Delimiter", { + submit = function(delim) + if delim == "" then delim = " " end - local doc = core.active_view.doc - local line1, col1, line2, col2, swap = doc:get_selection(true) - line1, col1 = doc:position_offset(line1, col1, translate.start_of_line) - line2, col2 = doc:position_offset(line2, col2, translate.end_of_line) - doc:set_selection(line1, col1, line2, col2, swap) + local doc = dv.doc + local line1, col1, line2, col2, swap = doc:get_selection(true) + line1, col1 = doc:position_offset(line1, col1, translate.start_of_line) + line2, col2 = doc:position_offset(line2, col2, translate.end_of_line) + doc:set_selection(line1, col1, line2, col2, swap) - doc:replace(function(text) - local lines = gmatch_to_array(text, "[^\n]*\n?") - tabularize_lines(lines, delim) - return table.concat(lines) - end) - end) + doc:replace(function(text) + local lines = gmatch_to_array(text, "[^\n]*\n?") + tabularize_lines(lines, delim) + return table.concat(lines) + end) + end + }) end, }) diff --git a/data/plugins/toolbarview.lua b/data/plugins/toolbarview.lua index bfd71138..f6c3275a 100644 --- a/data/plugins/toolbarview.lua +++ b/data/plugins/toolbarview.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" local common = require "core.common" local command = require "core.command" @@ -7,31 +7,26 @@ local View = require "core.view" local ToolbarView = View:extend() -local toolbar_commands = { - {symbol = "f", command = "core:new-doc"}, - {symbol = "D", command = "core:open-file"}, - {symbol = "S", command = "doc:save"}, - {symbol = "L", command = "core:find-file"}, - {symbol = "B", command = "core:find-command"}, - {symbol = "P", command = "core:open-user-module"}, -} - - -local function toolbar_height() - return style.icon_big_font:get_height() + style.padding.y * 2 -end - function ToolbarView:new() ToolbarView.super.new(self) self.visible = true self.init_size = true self.tooltip = false + self.toolbar_font = style.icon_big_font + self.toolbar_commands = { + {symbol = "f", command = "core:new-doc"}, + {symbol = "D", command = "core:open-file"}, + {symbol = "S", command = "doc:save"}, + {symbol = "L", command = "core:find-file"}, + {symbol = "B", command = "core:find-command"}, + {symbol = "P", command = "core:open-user-module"}, + } end function ToolbarView:update() - local dest_size = self.visible and toolbar_height() or 0 + local dest_size = self.visible and (self.toolbar_font:get_height() + style.padding.y * 2) or 0 if self.init_size then self.size.y = dest_size self.init_size = nil @@ -46,19 +41,24 @@ function ToolbarView:toggle_visible() self.visible = not self.visible end +function ToolbarView:get_icon_width() + local max_width = 0 + for i,v in ipairs(self.toolbar_commands) do max_width = math.max(max_width, self.toolbar_font:get_width(v.symbol)) end + return max_width +end function ToolbarView:each_item() - local icon_h, icon_w = style.icon_big_font:get_height(), style.icon_big_font:get_width("D") + local icon_h, icon_w = self.toolbar_font:get_height(), self:get_icon_width() local toolbar_spacing = icon_w / 2 local ox, oy = self:get_content_offset() local index = 0 local iter = function() index = index + 1 - if index <= #toolbar_commands then + if index <= #self.toolbar_commands then local dx = style.padding.x + (icon_w + toolbar_spacing) * (index - 1) local dy = style.padding.y if dx + icon_w > self.size.x then return end - return toolbar_commands[index], ox + dx, oy + dy, icon_w, icon_h + return self.toolbar_commands[index], ox + dx, oy + dy, icon_w, icon_h end end return iter @@ -66,9 +66,9 @@ end function ToolbarView:get_min_width() - local icon_w = style.icon_big_font:get_width("D") + local icon_w = self:get_icon_width() local space = icon_w / 2 - return 2 * style.padding.x + (icon_w + space) * #toolbar_commands - space + return 2 * style.padding.x + (icon_w + space) * #self.toolbar_commands - space end @@ -76,19 +76,20 @@ function ToolbarView:draw() self:draw_background(style.background2) for item, x, y, w, h in self:each_item() do - local color = item == self.hovered_item and style.text or style.dim - common.draw_text(style.icon_big_font, color, item.symbol, nil, x, y, 0, h) + local color = item == self.hovered_item and command.is_valid(item.command) and style.text or style.dim + common.draw_text(self.toolbar_font, color, item.symbol, nil, x, y, 0, h) end end function ToolbarView:on_mouse_pressed(button, x, y, clicks) local caught = ToolbarView.super.on_mouse_pressed(self, button, x, y, clicks) - if caught then return end + if caught then return caught end core.set_active_view(core.last_active_view) - if self.hovered_item then + if self.hovered_item and command.is_valid(self.hovered_item.command) then command.perform(self.hovered_item.command) end + return true end diff --git a/data/plugins/treeview.lua b/data/plugins/treeview.lua index f7c7f5ba..4b95e240 100644 --- a/data/plugins/treeview.lua +++ b/data/plugins/treeview.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" local common = require "core.common" local command = require "core.command" @@ -8,9 +8,13 @@ local style = require "core.style" local View = require "core.view" local ContextMenu = require "core.contextmenu" local RootView = require "core.rootview" +local CommandView = require "core.commandview" +config.plugins.treeview = common.merge({ + -- Default treeview width + size = 200 * SCALE +}, config.plugins.treeview) -local default_treeview_size = 200 * SCALE local tooltip_offset = style.font:get_height() local tooltip_border = 1 local tooltip_delay = 0.5 @@ -39,19 +43,26 @@ function TreeView:new() self.scrollable = true self.visible = true self.init_size = true - self.target_size = default_treeview_size + self.target_size = config.plugins.treeview.size self.cache = {} self.tooltip = { x = 0, y = 0, begin = 0, alpha = 0 } + self.cursor_pos = { x = 0, y = 0 } self.item_icon_width = 0 self.item_text_spacing = 0 - local on_dirmonitor_modify = core.on_dirmonitor_modify - function core.on_dirmonitor_modify(dir, filepath) - if self.cache[dir.name] then - self.cache[dir.name][filepath] = nil - end - on_dirmonitor_modify(dir, filepath) + self:add_core_hooks() +end + + +function TreeView:add_core_hooks() + -- When a file or directory is deleted we delete the corresponding cache entry + -- because if the entry is recreated we may use wrong information from cache. + local on_delete = core.on_dirmonitor_delete + core.on_dirmonitor_delete = function(dir, filepath) + local cache = self.cache[dir.name] + if cache then cache[filepath] = nil end + on_delete(dir, filepath) end end @@ -90,7 +101,7 @@ function TreeView:get_cached(dir, item, dirname) end t.name = basename t.type = item.type - t.dir = dir -- points to top level "dir" item + t.dir_name = dir.name -- points to top level "dir" item dir_cache[cache_name] = t end return t @@ -98,7 +109,7 @@ end function TreeView:get_name() - return "Project" + return nil end @@ -142,34 +153,51 @@ function TreeView:each_item() count_lines = count_lines + 1 y = y + h local i = 1 - while i <= #dir.files and dir_cached.expanded do - local item = dir.files[i] - local cached = self:get_cached(dir, item, dir.name) + if dir.files then -- if consumed max sys file descriptors this can be nil + while i <= #dir.files and dir_cached.expanded do + local item = dir.files[i] + local cached = self:get_cached(dir, item, dir.name) - coroutine.yield(cached, ox, y, w, h) - count_lines = count_lines + 1 - y = y + h - i = i + 1 + coroutine.yield(cached, ox, y, w, h) + count_lines = count_lines + 1 + y = y + h + i = i + 1 - if not cached.expanded then - if cached.skip then - i = cached.skip - else - local depth = cached.depth - while i <= #dir.files do - if get_depth(dir.files[i].filename) <= depth then break end - i = i + 1 + if not cached.expanded then + if cached.skip then + i = cached.skip + else + local depth = cached.depth + while i <= #dir.files do + if get_depth(dir.files[i].filename) <= depth then break end + i = i + 1 + end + cached.skip = i end - cached.skip = i end - end - end -- while files + end -- while files + end end -- for directories self.count_lines = count_lines end) end +function TreeView:set_selection(selection, selection_y) + self.selected_item = selection + if selection and selection_y + and (selection_y <= 0 or selection_y >= self.size.y) then + + local lh = self:get_item_height() + if selection_y >= self.size.y - lh then + selection_y = selection_y - self.size.y + lh + end + local _, y = self:get_content_offset() + self.scroll.to.y = selection and (selection_y - y) + end +end + + function TreeView:get_text_bounding_box(item, x, y, w, h) local icon_width = style.icon_font:get_width("D") local xoffset = item.depth * style.padding.x + style.padding.x + icon_width @@ -180,8 +208,14 @@ end function TreeView:on_mouse_moved(px, py, ...) + if not self.visible then return end TreeView.super.on_mouse_moved(self, px, py, ...) - if self.dragging_scrollbar then return end + self.cursor_pos.x = px + self.cursor_pos.y = py + if self.dragging_scrollbar then + self.hovered_item = nil + return + end local item_changed, tooltip_changed for item, x,y,w,h in self:each_item() do @@ -203,50 +237,6 @@ function TreeView:on_mouse_moved(px, py, ...) end -local function create_directory_in(item) - local path = item.abs_filename - core.command_view:enter("Create directory in " .. path, function(text) - local dirname = path .. PATHSEP .. text - local success, err = system.mkdir(dirname) - if not success then - core.error("cannot create directory %q: %s", dirname, err) - end - item.expanded = true - end) -end - - -function TreeView:on_mouse_pressed(button, x, y, clicks) - local caught = TreeView.super.on_mouse_pressed(self, button, x, y, clicks) - if caught or button ~= "left" then - return true - end - local hovered_item = self.hovered_item - if not hovered_item then - return false - elseif hovered_item.type == "dir" then - if keymap.modkeys["ctrl"] and button == "left" then - create_directory_in(hovered_item) - else - hovered_item.expanded = not hovered_item.expanded - if hovered_item.dir.files_limit then - core.update_project_subdir(hovered_item.dir, hovered_item.filename, hovered_item.expanded) - core.project_subdir_set_show(hovered_item.dir, hovered_item.filename, hovered_item.expanded) - end - end - else - core.try(function() - if core.last_active_view and core.active_view == self then - core.set_active_view(core.last_active_view) - end - local doc_filename = core.normalize_to_project_dir(hovered_item.abs_filename) - core.root_view:open_doc(core.open_doc(doc_filename)) - end) - end - return true -end - - function TreeView:update() -- update width local dest = self.visible and self.target_size or 0 @@ -254,12 +244,14 @@ function TreeView:update() self.size.x = dest self.init_size = false else - self:move_towards(self.size, "x", dest) + self:move_towards(self.size, "x", dest, nil, "treeview") end + if not self.visible then return end + local duration = system.get_time() - self.tooltip.begin if self.hovered_item and self.tooltip.x and duration > tooltip_delay then - self:move_towards(self.tooltip, "alpha", tooltip_alpha, tooltip_alpha_rate) + self:move_towards(self.tooltip, "alpha", tooltip_alpha, tooltip_alpha_rate, "treeview") else self.tooltip.alpha = 0 end @@ -267,6 +259,13 @@ function TreeView:update() self.item_icon_width = style.icon_font:get_width("D") self.item_text_spacing = style.icon_font:get_width("f") / 2 + -- this will make sure hovered_item is updated + -- we don't want events when the thing is scrolling fast + local dy = math.abs(self.scroll.to.y - self.scroll.y) + if self.scroll.to.y ~= 0 and dy < self:get_item_height() then + self:on_mouse_moved(self.cursor_pos.x, self.cursor_pos.y, 0, 0) + end + TreeView.super.update(self) end @@ -350,6 +349,10 @@ end function TreeView:draw_item_background(item, active, hovered, x, y, w, h) if hovered then + local hover_color = { table.unpack(style.line_highlight) } + hover_color[4] = 160 + renderer.draw_rect(x, y, w, h, hover_color) + elseif active then renderer.draw_rect(x, y, w, h, style.line_highlight) end end @@ -366,6 +369,7 @@ end function TreeView:draw() + if not self.visible then return end self:draw_background(style.background2) local _y, _h = self.position.y, self.size.y @@ -375,23 +379,86 @@ function TreeView:draw() for item, x,y,w,h in self:each_item() do if y + h >= _y and y < _y + _h then self:draw_item(item, - item.abs_filename == active_filename, + item == self.selected_item, item == self.hovered_item, x, y, w, h) end end self:draw_scrollbar() - if self.hovered_item and self.tooltip.alpha > 0 then + if self.hovered_item and self.tooltip.x and self.tooltip.alpha > 0 then core.root_view:defer_draw(self.draw_tooltip, self) end end +function TreeView:get_parent(item) + local parent_path = common.dirname(item.abs_filename) + if not parent_path then return end + for it, _, y in self:each_item() do + if it.abs_filename == parent_path then + return it, y + end + end +end + + +function TreeView:get_item(item, where) + local last_item, last_x, last_y, last_w, last_h + local stop = false + + for it, x, y, w, h in self:each_item() do + if not item and where >= 0 then + return it, x, y, w, h + end + if item == it then + if where < 0 and last_item then + break + elseif where == 0 or (where < 0 and not last_item) then + return it, x, y, w, h + end + stop = true + elseif stop then + item = it + return it, x, y, w, h + end + last_item, last_x, last_y, last_w, last_h = it, x, y, w, h + end + return last_item, last_x, last_y, last_w, last_h +end + +function TreeView:get_next(item) + return self:get_item(item, 1) +end + +function TreeView:get_previous(item) + return self:get_item(item, -1) +end + + +function TreeView:toggle_expand(toggle) + local item = self.selected_item + + if not item then return end + + if item.type == "dir" then + if type(toggle) == "boolean" then + item.expanded = toggle + else + item.expanded = not item.expanded + end + local hovered_dir = core.project_dir_by_name(item.dir_name) + if hovered_dir and hovered_dir.files_limit then + core.update_project_subdir(hovered_dir, item.depth == 0 and "" or item.filename, item.expanded) + end + end +end + + -- init local view = TreeView() local node = core.root_view:get_active_node() -local treeview_node = node:split("left", view, {x = true}, true) +view.node = node:split("left", view, {x = true}, true) -- The toolbarview plugin is special because it is plugged inside -- a treeview pane which is itelf provided in a plugin. @@ -400,12 +467,12 @@ local treeview_node = node:split("left", view, {x = true}, true) -- plugin module that plug itself in the active node but it is plugged here -- in the treeview node. local toolbar_view = nil -local toolbar_plugin, ToolbarView = core.try(require, "plugins.toolbarview") +local toolbar_plugin, ToolbarView = pcall(require, "plugins.toolbarview") if config.plugins.toolbarview ~= false and toolbar_plugin then toolbar_view = ToolbarView() - treeview_node:split("down", toolbar_view, {y = true}) + view.node:split("down", toolbar_view, {y = true}) local min_toolbar_width = toolbar_view:get_min_width() - view:set_target_size("x", math.max(default_treeview_size, min_toolbar_width)) + view:set_target_size("x", math.max(config.plugins.treeview.size, min_toolbar_width)) command.add(nil, { ["toolbar:toggle"] = function() toolbar_view:toggle_visible() @@ -446,7 +513,22 @@ function RootView:draw(...) menu:draw() end +local on_quit_project = core.on_quit_project +function core.on_quit_project() + view.cache = {} + on_quit_project() +end + local function is_project_folder(path) + for _,dir in pairs(core.project_directories) do + if dir.name == path then + return true + end + end + return false +end + +local function is_primary_project_folder(path) return core.project_dir == path end @@ -476,65 +558,143 @@ menu:register( } ) +menu:register( + function() + return view.hovered_item + and not is_primary_project_folder(view.hovered_item.abs_filename) + and is_project_folder(view.hovered_item.abs_filename) + end, + { + { text = "Remove directory", command = "treeview:remove-project-directory" }, + } +) + +local previous_view = nil + -- Register the TreeView commands and keymap command.add(nil, { ["treeview:toggle"] = function() view.visible = not view.visible - end}) + end, - -command.add(function() return view.hovered_item ~= nil end, { - ["treeview:rename"] = function() - local old_filename = view.hovered_item.filename - local old_abs_filename = view.hovered_item.abs_filename - core.command_view:set_text(old_filename) - core.command_view:enter("Rename", function(filename) - filename = core.normalize_to_project_dir(filename) - local abs_filename = core.project_absolute_path(filename) - local res, err = os.rename(old_abs_filename, abs_filename) - if res then -- successfully renamed - for _, doc in ipairs(core.docs) do - if doc.abs_filename and old_abs_filename == doc.abs_filename then - doc:set_filename(filename, abs_filename) -- make doc point to the new filename - doc:reset_syntax() - break -- only first needed - end - end - core.log("Renamed \"%s\" to \"%s\"", old_filename, filename) + ["treeview:toggle-focus"] = function() + if not core.active_view:is(TreeView) then + if core.active_view:is(CommandView) then + previous_view = core.last_active_view else - core.error("Error while renaming \"%s\" to \"%s\": %s", old_abs_filename, abs_filename, err) + previous_view = core.active_view + end + if not previous_view then + previous_view = core.root_view:get_primary_node().active_view + end + core.set_active_view(view) + if not view.selected_item then + for it, _, y in view:each_item() do + view:set_selection(it, y) + break + end end - end, common.path_suggest) - end, - ["treeview:new-file"] = function() - if not is_project_folder(view.hovered_item.abs_filename) then - core.command_view:set_text(view.hovered_item.filename .. "/") + else + core.set_active_view( + previous_view or core.root_view:get_primary_node().active_view + ) end - core.command_view:enter("Filename", function(filename) - local doc_filename = core.project_dir .. PATHSEP .. filename - local file = io.open(doc_filename, "a+") - file:write("") - file:close() - core.root_view:open_doc(core.open_doc(doc_filename)) - core.log("Created %s", doc_filename) - end, common.path_suggest) + end +}) + +command.add(TreeView, { + ["treeview:next"] = function() + local item, _, item_y = view:get_next(view.selected_item) + view:set_selection(item, item_y) end, - ["treeview:new-folder"] = function() - if not is_project_folder(view.hovered_item.abs_filename) then - core.command_view:set_text(view.hovered_item.filename .. "/") + ["treeview:previous"] = function() + local item, _, item_y = view:get_previous(view.selected_item) + view:set_selection(item, item_y) + end, + + ["treeview:open"] = function() + local item = view.selected_item + if not item then return end + if item.type == "dir" then + view:toggle_expand() + else + core.try(function() + if core.last_active_view and core.active_view == view then + core.set_active_view(core.last_active_view) + end + local doc_filename = core.normalize_to_project_dir(item.abs_filename) + core.root_view:open_doc(core.open_doc(doc_filename)) + end) end - core.command_view:enter("Folder Name", function(filename) - local dir_path = core.project_dir .. PATHSEP .. filename - common.mkdirp(dir_path) - core.log("Created %s", dir_path) - end, common.path_suggest) end, - ["treeview:delete"] = function() - local filename = view.hovered_item.abs_filename - local relfilename = view.hovered_item.filename + ["treeview:deselect"] = function() + view.selected_item = nil + end, + + ["treeview:select"] = function() + view:set_selection(view.hovered_item) + end, + + ["treeview:select-and-open"] = function() + if view.hovered_item then + view:set_selection(view.hovered_item) + command.perform "treeview:open" + end + end, + + ["treeview:collapse"] = function() + if view.selected_item then + if view.selected_item.type == "dir" and view.selected_item.expanded then + view:toggle_expand(false) + else + local parent_item, y = view:get_parent(view.selected_item) + if parent_item then + view:set_selection(parent_item, y) + end + end + end + end, + + ["treeview:expand"] = function() + local item = view.selected_item + if not item or item.type ~= "dir" then return end + + if item.expanded then + local next_item, _, next_y = view:get_next(item) + if next_item.depth > item.depth then + view:set_selection(next_item, next_y) + end + else + view:toggle_expand(true) + end + end, +}) + + +local function treeitem() return view.hovered_item or view.selected_item end + + +command.add( + function() + local item = treeitem() + return item ~= nil + and ( + core.active_view == view or core.active_view == menu + or (view.toolbar and core.active_view == view.toolbar) + -- sometimes the context menu is shown on top of statusbar + or core.active_view == core.status_view + ), item + end, { + ["treeview:delete"] = function(item) + local filename = item.abs_filename + local relfilename = item.filename + if item.dir_name ~= core.project_dir then + -- add secondary project dirs names to the file path to show + relfilename = common.basename(item.dir_name) .. PATHSEP .. relfilename + end local file_info = system.get_file_info(filename) local file_type = file_info.type == "dir" and "Directory" or "File" -- Ask before deleting @@ -568,22 +728,175 @@ command.add(function() return view.hovered_item ~= nil end, { end end ) + end +}) + + +command.add(function() + if not (core.root_view.overlapping_node and core.root_view.overlapping_node.active_view) then return end + if core.root_view.overlapping_node.active_view ~= view then return end + local item = treeitem() + return item ~= nil, item + end, { + ["treeview:rename"] = function(item) + local old_filename = item.filename + local old_abs_filename = item.abs_filename + core.command_view:enter("Rename", { + text = old_filename, + submit = function(filename) + local abs_filename = filename + if not common.is_absolute_path(filename) then + abs_filename = item.dir_name .. PATHSEP .. filename + end + local res, err = os.rename(old_abs_filename, abs_filename) + if res then -- successfully renamed + for _, doc in ipairs(core.docs) do + if doc.abs_filename and old_abs_filename == doc.abs_filename then + doc:set_filename(filename, abs_filename) -- make doc point to the new filename + doc:reset_syntax() + break -- only first needed + end + end + core.log("Renamed \"%s\" to \"%s\"", old_filename, filename) + else + core.error("Error while renaming \"%s\" to \"%s\": %s", old_abs_filename, abs_filename, err) + end + end, + suggest = function(text) + return common.path_suggest(text, item.dir_name) + end + }) end, - ["treeview:open-in-system"] = function() - local hovered_item = view.hovered_item - - if PLATFORM == "Windows" then - system.exec(string.format("start \"\" %q", hovered_item.abs_filename)) - elseif string.find(PLATFORM, "Mac") then - system.exec(string.format("open %q", hovered_item.abs_filename)) - elseif PLATFORM == "Linux" then - system.exec(string.format("xdg-open %q", hovered_item.abs_filename)) + ["treeview:new-file"] = function(item) + local text + if not is_project_folder(item.abs_filename) then + text = item.filename .. PATHSEP end + core.command_view:enter("Filename", { + text = text, + submit = function(filename) + local doc_filename = item.dir_name .. PATHSEP .. filename + core.log(doc_filename) + local file = io.open(doc_filename, "a+") + file:write("") + file:close() + core.root_view:open_doc(core.open_doc(doc_filename)) + core.log("Created %s", doc_filename) + end, + suggest = function(text) + return common.path_suggest(text, item.dir_name) + end + }) + end, + + ["treeview:new-folder"] = function(item) + local text + if not is_project_folder(item.abs_filename) then + text = item.filename .. PATHSEP + end + core.command_view:enter("Folder Name", { + text = text, + submit = function(filename) + local dir_path = item.dir_name .. PATHSEP .. filename + common.mkdirp(dir_path) + core.log("Created %s", dir_path) + end, + suggest = function(text) + return common.path_suggest(text, item.dir_name) + end + }) + end, + + ["treeview:open-in-system"] = function(item) + if PLATFORM == "Windows" then + system.exec(string.format("start \"\" %q", item.abs_filename)) + elseif string.find(PLATFORM, "Mac") then + system.exec(string.format("open %q", item.abs_filename)) + elseif PLATFORM == "Linux" or string.find(PLATFORM, "BSD") then + system.exec(string.format("xdg-open %q", item.abs_filename)) + end + end +}) + +local projectsearch = pcall(require, "plugins.projectsearch") +if projectsearch then + menu:register(function() + return view.hovered_item and view.hovered_item.type == "dir" + end, { + { text = "Find in directory", command = "treeview:search-in-directory" } + }) + command.add(function() + return view.hovered_item and view.hovered_item.type == "dir" + end, { + ["treeview:search-in-directory"] = function(item) + command.perform("project-search:find", view.hovered_item.abs_filename) + end + }) +end + +command.add(function() + local item = treeitem() + return item + and not is_primary_project_folder(item.abs_filename) + and is_project_folder(item.abs_filename), item + end, { + ["treeview:remove-project-directory"] = function(item) + core.remove_project_directory(item.dir_name) end, }) -keymap.add { ["ctrl+\\"] = "treeview:toggle" } + +keymap.add { + ["ctrl+\\"] = "treeview:toggle", + ["up"] = "treeview:previous", + ["down"] = "treeview:next", + ["left"] = "treeview:collapse", + ["right"] = "treeview:expand", + ["return"] = "treeview:open", + ["escape"] = "treeview:deselect", + ["delete"] = "treeview:delete", + ["ctrl+return"] = "treeview:new-folder", + ["lclick"] = "treeview:select-and-open", + ["mclick"] = "treeview:select", + ["ctrl+lclick"] = "treeview:new-folder" +} + +-- The config specification used by gui generators +config.plugins.treeview.config_spec = { + name = "Treeview", + { + label = "Size", + description = "Default treeview width.", + path = "size", + type = "number", + default = toolbar_view and math.ceil(toolbar_view:get_min_width() / SCALE) + or 200 * SCALE, + min = toolbar_view and toolbar_view:get_min_width() / SCALE + or 200 * SCALE, + get_value = function(value) + return value / SCALE + end, + set_value = function(value) + return value * SCALE + end, + on_apply = function(value) + view:set_target_size("x", math.max( + value, toolbar_view and toolbar_view:get_min_width() or 200 * SCALE + )) + end + }, + { + label = "Hide on Startup", + description = "Show or hide the treeview on startup.", + path = "visible", + type = "toggle", + default = false, + on_apply = function(value) + view.visible = not value + end + } +} -- Return the treeview with toolbar and contextmenu to allow -- user or plugin modifications diff --git a/data/plugins/trimwhitespace.lua b/data/plugins/trimwhitespace.lua index 79886c67..d6057da8 100644 --- a/data/plugins/trimwhitespace.lua +++ b/data/plugins/trimwhitespace.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" local command = require "core.command" local Doc = require "core.doc" @@ -24,8 +24,8 @@ end command.add("core.docview", { - ["trim-whitespace:trim-trailing-whitespace"] = function() - trim_trailing_whitespace(core.active_view.doc) + ["trim-whitespace:trim-trailing-whitespace"] = function(dv) + trim_trailing_whitespace(dv.doc) end, }) diff --git a/data/plugins/workspace.lua b/data/plugins/workspace.lua index 1edfbe1e..788a753a 100644 --- a/data/plugins/workspace.lua +++ b/data/plugins/workspace.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" local common = require "core.common" local DocView = require "core.docview" @@ -117,7 +117,7 @@ local function load_view(t) -- cannot be read. if dv and dv.doc then dv.doc:set_selection(table.unpack(t.selection)) - dv.last_line, dv.last_col = dv.doc:get_selection() + dv.last_line1, dv.last_col1, dv.last_line2, dv.last_col2 = dv.doc:get_selection() dv.scroll.x, dv.scroll.to.x = t.scroll.x, t.scroll.x dv.scroll.y, dv.scroll.to.y = t.scroll.y, t.scroll.y end diff --git a/docs/api/globals.lua b/docs/api/globals.lua index 98fe61b1..b32ff9af 100644 --- a/docs/api/globals.lua +++ b/docs/api/globals.lua @@ -4,6 +4,11 @@ ---@type table ARGS = {} +---The current platform tuple used for native modules loading, +---for example: "x86_64-linux", "x86_64-darwin", "x86_64-windows", etc... +---@type string +ARCH = "Architecture-OperatingSystem" + ---The current operating system. ---@type string | "'Windows'" | "'Mac OS X'" | "'Linux'" | "'iOS'" | "'Android'" PLATFORM = "Operating System" diff --git a/docs/api/process.lua b/docs/api/process.lua index 4d146bc4..c384a346 100644 --- a/docs/api/process.lua +++ b/docs/api/process.lua @@ -124,7 +124,7 @@ process.options = {} ---@return process | nil ---@return string errmsg ---@return process.errortype | integer errcode -function process:start(command_and_params, options) end +function process.start(command_and_params, options) end --- ---Translates an error code into a useful text message diff --git a/docs/api/renderer.lua b/docs/api/renderer.lua index 7a9b636d..fe42ed65 100644 --- a/docs/api/renderer.lua +++ b/docs/api/renderer.lua @@ -6,7 +6,9 @@ renderer = {} --- ----Represents a color used by the rendering functions. +---Array of bytes that represents a color used by the rendering functions. +---Note: indexes for rgba are numerical 1 = r, 2 = g, 3 = b, 4 = a but for +---documentation purposes the letters r, g, b, a were used. ---@class renderer.color ---@field public r number Red ---@field public g number Green @@ -17,11 +19,13 @@ renderer.color = {} --- ---Represent options that affect a font's rendering. ---@class renderer.fontoptions ----@field public antialiasing "'grayscale'" | "'subpixel'" +---@field public antialiasing "'none'" | "'grayscale'" | "'subpixel'" ---@field public hinting "'slight'" | "'none'" | '"full"' -- @field public bold boolean -- @field public italic boolean -- @field public underline boolean +-- @field public smoothing boolean +-- @field public strikethrough boolean renderer.fontoptions = {} --- @@ -33,18 +37,29 @@ renderer.font = {} --- ---@param path string ---@param size number ----@param options renderer.fontoptions +---@param options? renderer.fontoptions --- ---@return renderer.font function renderer.font.load(path, size, options) end +--- +---Combines an array of fonts into a single one for broader charset support, +---the order of the list determines the fonts precedence when retrieving +---a symbol from it. +--- +---@param fonts renderer.font[] +--- +---@return renderer.font +function renderer.font.group(fonts) end + --- ---Clones a font object into a new one. --- ---@param size? number Optional new size for cloned font. +---@param options? renderer.fontoptions --- ---@return renderer.font -function renderer.font:copy(size) end +function renderer.font:copy(size, options) end --- ---Set the amount of characters that represent a tab. @@ -81,23 +96,11 @@ function renderer.font:get_size() end function renderer.font:set_size(size) end --- ----Assistive functionality to replace characters in a ----rendered text with other characters. ----@class renderer.replacements -renderer.replacements = {} - +---Get the current path of the font as a string if a single font or as an +---array of strings if a group font. --- ----Create a new character replacements object. ---- ----@return renderer.replacements -function renderer.replacements.new() end - ---- ----Add to internal map a character to character replacement. ---- ----@param original_char string Should be a single character like '\t' ----@param replacement_char string Should be a single character like '»' -function renderer.replacements:add(original_char, replacement_char) end +---@return string | table +function renderer.font:get_path() end --- ---Toggles drawing debugging rectangles on the currently rendered sections @@ -141,29 +144,13 @@ function renderer.set_clip_rect(x, y, width, height) end function renderer.draw_rect(x, y, width, height, color) end --- ----Draw text. +---Draw text and return the x coordinate where the text finished drawing. --- ---@param font renderer.font ---@param text string ---@param x number ---@param y number ---@param color renderer.color ----@param replace renderer.replacements ----@param color_replace renderer.color --- ----@return number x_subpixel -function renderer.draw_text(font, text, x, y, color, replace, color_replace) end - ---- ----Draw text at subpixel level. ---- ----@param font renderer.font ----@param text string ----@param x number ----@param y number ----@param color renderer.color ----@param replace renderer.replacements ----@param color_replace renderer.color ---- ----@return number x_subpixel -function renderer.draw_text_subpixel(font, text, x, y, color, replace, color_replace) end +---@return number x +function renderer.draw_text(font, text, x, y, color) end diff --git a/docs/api/string.lua b/docs/api/string.lua new file mode 100644 index 00000000..0872b462 --- /dev/null +++ b/docs/api/string.lua @@ -0,0 +1,165 @@ +---@meta + +---UTF-8 equivalent of string.byte +---@param s string +---@param i? integer +---@param j? integer +---@return integer +---@return ... +function string.ubyte(s, i, j) end + +---UTF-8 equivalent of string.char +---@param byte integer +---@param ... integer +---@return string +---@return ... +function string.uchar(byte, ...) end + +---UTF-8 equivalent of string.find +---@param s string +---@param pattern string +---@param init? integer +---@param plain? boolean +---@return integer start +---@return integer end +---@return ... captured +function string.ufind(s, pattern, init, plain) end + +---UTF-8 equivalent of string.gmatch +---@param s string +---@param pattern string +---@param init? integer +---@return fun():string, ... +function string.ugmatch(s, pattern, init) end + +---UTF-8 equivalent of string.gsub +---@param s string +---@param pattern string +---@param repl string|table|function +---@param n integer +---@return string +---@return integer count +function string.ugsub(s, pattern, repl, n) end + +---UTF-8 equivalent of string.len +---@param s string +---@return integer +function string.ulen(s) end + +---UTF-8 equivalent of string.lower +---@param s string +---@return string +function string.ulower(s) end + +---UTF-8 equivalent of string.match +---@param s string +---@param pattern string +---@param init? integer +---@return string | number captured +function string.umatch(s, pattern, init) end + +---UTF-8 equivalent of string.reverse +---@param s string +---@return string +function string.ureverse(s) end + +---UTF-8 equivalent of string.sub +---@param s string +---@param i integer +---@param j? integer +---@return string +function string.usub(s, i, j) end + +---UTF-8 equivalent of string.upper +---@param s string +---@return string +function string.uupper(s) end + +---Equivalent to utf8.escape() +---@param s string +---@return string utf8_string +function string.uescape(s) end + + +---Equivalent to utf8.charpos() +---@param s string +---@param charpos? integer +---@param index? integer +---@return integer charpos +---@return integer codepoint +function string.ucharpos(s, charpos, index) end + +---Equivalent to utf8.next() +---@param s string +---@param charpos? integer +---@param index? integer +---@return integer charpos +---@return integer codepoint +function string.unext(s, charpos, index) end + +---Equivalent to utf8.insert() +---@param s string +---@param idx? integer +---@param substring string +---return string new_string +function string.uinsert(s, idx, substring) end + +---Equivalent to utf8.remove() +---@param s string +---@param start? integer +---@param stop? integer +---return string new_string +function string.uremove(s, start, stop) end + +---Equivalent to utf8.width() +---@param s string +---@param ambi_is_double? boolean +---@param default_width? integer +---@return integer width +function string.uwidth(s, ambi_is_double, default_width) end + +---Equivalent to utf8.widthindex() +---@param s string +---@param location integer +---@param ambi_is_double? boolean +---@param default_width? integer +---@return integer idx +---@return integer offset +---@return integer width +function string.uwidthindex(s, location, ambi_is_double, default_width) end + +---Equivalent to utf8.title() +---@param s string +---return string new_string +function string.utitle(s) end + +---Equivalent to utf8.fold() +---@param s string +---return string new_string +function string.ufold(s) end + +---Equivalent to utf8.ncasecmp() +---@param a string +---@param b string +---@return integer result +function string.uncasecmp(a, b) end + +---Equivalent to utf8.offset() +---@param s string +---@param n integer +---@param i? integer +---@return integer position_in_bytes +function string.uoffset(s, n, i) end + +---Equivalent to utf8.codepoint() +---@param s string +---@param i? integer +---@param j? integer +---@return integer code +---@return ... +function string.ucodepoint(s, i, j) end + +---Equivalent to utf8.codes() +---@param s string +---@return fun():integer, integer +function string.ucodes(s) end diff --git a/docs/api/system.lua b/docs/api/system.lua index a655099b..e5eff27f 100644 --- a/docs/api/system.lua +++ b/docs/api/system.lua @@ -192,6 +192,12 @@ function system.get_clipboard() end ---@param text string function system.set_clipboard(text) end +--- +---Get the process id of lite-xl it self. +--- +---@return integer +function system.get_process_id() end + --- ---Get amount of iterations since the application was launched ---also known as SDL_GetPerformanceCounter() / SDL_GetPerformanceFrequency() diff --git a/docs/api/utf8extra.lua b/docs/api/utf8extra.lua new file mode 100644 index 00000000..1ff4dcb6 --- /dev/null +++ b/docs/api/utf8extra.lua @@ -0,0 +1,191 @@ +---@meta + +---Additional utf8 support not provided by lua. +---@class utf8extra +utf8extra = {} + +---UTF-8 equivalent of string.byte +---@param s string +---@param i? integer +---@param j? integer +---@return integer +---@return ... +function utf8extra.byte(s, i, j) end + +---UTF-8 equivalent of string.char +---@param byte integer +---@param ... integer +---@return string +---@return ... +function utf8extra.char(byte, ...) end + +---UTF-8 equivalent of string.find +---@param s string +---@param pattern string +---@param init? integer +---@param plain? boolean +---@return integer start +---@return integer end +---@return ... captured +function utf8extra.find(s, pattern, init, plain) end + +---UTF-8 equivalent of string.gmatch +---@param s string +---@param pattern string +---@param init? integer +---@return fun():string, ... +function utf8extra.gmatch(s, pattern, init) end + +---UTF-8 equivalent of string.gsub +---@param s string +---@param pattern string +---@param repl string|table|function +---@param n integer +---@return string +---@return integer count +function utf8extra.gsub(s, pattern, repl, n) end + +---UTF-8 equivalent of string.len +---@param s string +---@return integer +function utf8extra.len(s) end + +---UTF-8 equivalent of string.lower +---@param s string +---@return string +function utf8extra.lower(s) end + +---UTF-8 equivalent of string.match +---@param s string +---@param pattern string +---@param init? integer +---@return string | number captured +function utf8extra.match(s, pattern, init) end + +---UTF-8 equivalent of string.reverse +---@param s string +---@return string +function utf8extra.reverse(s) end + +---UTF-8 equivalent of string.sub +---@param s string +---@param i integer +---@param j? integer +---@return string +function utf8extra.sub(s, i, j) end + +---UTF-8 equivalent of string.upper +---@param s string +---@return string +function utf8extra.upper(s) end + +---Escape a str to UTF-8 format string. It support several escape format: +---* %ddd - which ddd is a decimal number at any length: change Unicode code point to UTF-8 format. +---* %{ddd} - same as %nnn but has bracket around. +---* %uddd - same as %ddd, u stands Unicode +---* %u{ddd} - same as %{ddd} +---* %xhhh - hexadigit version of %ddd +---* %x{hhh} same as %xhhh. +---* %? - '?' stands for any other character: escape this character. +---Example: +---```lua +---local u = utf8.escape +---print(u"%123%u123%{123}%u{123}%xABC%x{ABC}") +---print(u"%%123%?%d%%u") +---``` +---@param s string +---@return string utf8_string +function utf8extra.escape(s) end + +---Convert UTF-8 position to byte offset. if only index is given, return byte +---offset of this UTF-8 char index. if both charpos and index is given, a new +---charpos will be calculated, by add/subtract UTF-8 char index to current +---charpos. in all cases, it returns a new char position, and code point +---(a number) at this position. +---@param s string +---@param charpos? integer +---@param index? integer +---@return integer charpos +---@return integer codepoint +function utf8extra.charpos(s, charpos, index) end + +---Iterate though the UTF-8 string s. If only s is given, it can used as a iterator: +---```lua +--- for pos, code in utf8.next, "utf8-string" do +--- -- ... +--- end +---```` +---If only charpos is given, return the next byte offset of in string. if +---charpos and index is given, a new charpos will be calculated, by add/subtract +---UTF-8 char offset to current charpos. in all case, it return a new char +---position (in bytes), and code point (a number) at this position. +---@param s string +---@param charpos? integer +---@param index? integer +---@return integer charpos +---@return integer codepoint +function utf8extra.next(s, charpos, index) end + +---Insert a substring to s. If idx is given, insert substring before char at +---this index, otherwise substring will concat to s. idx can be negative. +---@param s string +---@param idx? integer +---@param substring string +---return string new_string +function utf8extra.insert(s, idx, substring) end + +---Delete a substring in s. If neither start nor stop is given, delete the last +---UTF-8 char in s, otherwise delete char from start to end of s. if stop is +---given, delete char from start to stop (include start and stop). start and +---stop can be negative. +---@param s string +---@param start? integer +---@param stop? integer +---return string new_string +function utf8extra.remove(s, start, stop) end + +---Calculate the width of UTF-8 string s. if ambi_is_double is given, the +---ambiguous width character's width is 2, otherwise it's 1. fullwidth/doublewidth +---character's width is 2, and other character's width is 1. if default_width is +---given, it will be the width of unprintable character, used display a +---non-character mark for these characters. if s is a code point, return the +---width of this code point. +---@param s string +---@param ambi_is_double? boolean +---@param default_width? integer +---@return integer width +function utf8extra.width(s, ambi_is_double, default_width) end + +---Return the character index at given location in string s. this is a reverse +---operation of utf8.width(). this function returns a index of location, and a +---offset in UTF-8 encoding. e.g. if cursor is at the second column (middle) +---of the wide char, offset will be 2. the width of character at idx is +---returned, also. +---@param s string +---@param location integer +---@param ambi_is_double? boolean +---@param default_width? integer +---@return integer idx +---@return integer offset +---@return integer width +function utf8extra.widthindex(s, location, ambi_is_double, default_width) end + +---Convert UTF-8 string s to title-case, used to compare by ignore case. if s +---is a number, it's treat as a code point and return a convert code point +---(number). utf8.lower/utf8.pper has the same extension. +---@param s string +---return string new_string +function utf8extra.title(s) end + +---Convert UTF-8 string s to folded case, used to compare by ignore case. if s +---is a number, it's treat as a code point and return a convert code point +---(number). utf8.lower/utf8.pper has the same extension. +---@param s string +---return string new_string +function utf8extra.fold(s) end + +---Compare a and b without case, -1 means a < b, 0 means a == b and 1 means a > b. +---@param a string +---@param b string +---@return integer result +function utf8extra.ncasecmp(a, b) end diff --git a/lib/dmon/dmon.h b/lib/dmon/dmon.h deleted file mode 100644 index 3c8be357..00000000 --- a/lib/dmon/dmon.h +++ /dev/null @@ -1,1599 +0,0 @@ -#ifndef __DMON_H__ -#define __DMON_H__ - -// -// Copyright 2021 Sepehr Taghdisian (septag@github). All rights reserved. -// License: https://github.com/septag/dmon#license-bsd-2-clause -// -// Portable directory monitoring library -// watches directories for file or directory changes. -// -// Usage: -// define DMON_IMPL and include this file to use it: -// #define DMON_IMPL -// #include "dmon.h" -// -// dmon_init(): -// Call this once at the start of your program. -// This will start a low-priority monitoring thread -// dmon_deinit(): -// Call this when your work with dmon is finished, usually on program terminate -// This will free resources and stop the monitoring thread -// dmon_watch: -// Watch for directories -// You can watch multiple directories by calling this function multiple times -// rootdir: root directory to monitor -// watch_cb: callback function to receive events. -// NOTE that this function is called from another thread, so you should -// beware of data races in your application when accessing data within this -// callback -// flags: watch flags, see dmon_watch_flags_t -// user_data: user pointer that is passed to callback function -// Returns the Id of the watched directory after successful call, or returns Id=0 if error -// dmon_unwatch: -// Remove the directory from watch list -// -// see test.c for the basic example -// -// Configuration: -// You can customize some low-level functionality like malloc and logging by overriding macros: -// -// DMON_MALLOC, DMON_FREE, DMON_REALLOC: -// define these macros to override memory allocations -// default is 'malloc', 'free' and 'realloc' -// DMON_ASSERT: -// define this to provide your own assert -// default is 'assert' -// DMON_LOG_ERROR: -// define this to provide your own logging mechanism -// default implementation logs to stdout and breaks the program -// DMON_LOG_DEBUG -// define this to provide your own extra debug logging mechanism -// default implementation logs to stdout in DEBUG and does nothing in other builds -// DMON_API_DECL, DMON_API_IMPL -// define these to provide your own API declerations. (for example: static) -// default is nothing (which is extern in C language ) -// DMON_MAX_PATH -// Maximum size of path characters -// default is 260 characters -// DMON_MAX_WATCHES -// Maximum number of watch directories -// default is 64 -// -// TODO: -// - DMON_WATCHFLAGS_FOLLOW_SYMLINKS does not resolve files -// - implement DMON_WATCHFLAGS_OUTOFSCOPE_LINKS -// - implement DMON_WATCHFLAGS_IGNORE_DIRECTORIES -// -// History: -// 1.0.0 First version. working Win32/Linux backends -// 1.1.0 MacOS backend -// 1.1.1 Minor fixes, eliminate gcc/clang warnings with -Wall -// 1.1.2 Eliminate some win32 dead code -// 1.1.3 Fixed select not resetting causing high cpu usage on linux -// 1.2.1 inotify (linux) fixes and improvements, added extra functionality header for linux -// to manually add/remove directories manually to the watch handle, in case of large file sets -// - -#include -#include - -#ifndef DMON_API_DECL -# define DMON_API_DECL -#endif - -#ifndef DMON_API_IMPL -# define DMON_API_IMPL -#endif - -typedef struct { uint32_t id; } dmon_watch_id; - -// Pass these flags to `dmon_watch` -typedef enum dmon_watch_flags_t { - DMON_WATCHFLAGS_RECURSIVE = 0x1, // monitor all child directories - DMON_WATCHFLAGS_FOLLOW_SYMLINKS = 0x2, // resolve symlinks (linux only) - DMON_WATCHFLAGS_OUTOFSCOPE_LINKS = 0x4, // TODO: not implemented yet - DMON_WATCHFLAGS_IGNORE_DIRECTORIES = 0x8 // TODO: not implemented yet -} dmon_watch_flags; - -// Action is what operation performed on the file. this value is provided by watch callback -typedef enum dmon_action_t { - DMON_ACTION_CREATE = 1, - DMON_ACTION_DELETE, - DMON_ACTION_MODIFY, - DMON_ACTION_MOVE -} dmon_action; - -#ifdef __cplusplus -extern "C" { -#endif - -DMON_API_DECL void dmon_init(void); -DMON_API_DECL void dmon_deinit(void); - -DMON_API_DECL dmon_watch_id dmon_watch(const char* rootdir, - void (*watch_cb)(dmon_watch_id watch_id, dmon_action action, - const char* rootdir, const char* filepath, - const char* oldfilepath, void* user), - uint32_t flags, void* user_data); -DMON_API_DECL void dmon_unwatch(dmon_watch_id id); - -#ifdef __cplusplus -} -#endif - -#ifdef DMON_IMPL - -#define DMON_OS_WINDOWS 0 -#define DMON_OS_MACOS 0 -#define DMON_OS_LINUX 0 - -#if defined(_WIN32) || defined(_WIN64) -# undef DMON_OS_WINDOWS -# define DMON_OS_WINDOWS 1 -#elif defined(__linux__) -# undef DMON_OS_LINUX -# define DMON_OS_LINUX 1 -#elif defined(__ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__) -# undef DMON_OS_MACOS -# define DMON_OS_MACOS __ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__ -#elif defined(__amigaos4__) -# undef DMON_OS_AMIGAOS4 -# define DMON_OS_AMIGAOS4 1 -#elif defined(__morphos__) -# undef DMON_OS_MORPHOS -# define DMON_OS_MORPHOS 1 -#else -# define DMON_OS 0 -# error "unsupported platform" -#endif - -#if DMON_OS_WINDOWS -# ifndef WIN32_LEAN_AND_MEAN -# define WIN32_LEAN_AND_MEAN -# endif -# ifndef NOMINMAX -# define NOMINMAX -# endif -# include -# include -# ifdef _MSC_VER -# pragma intrinsic(_InterlockedExchange) -# endif -#elif DMON_OS_LINUX -# ifndef __USE_MISC -# define __USE_MISC -# endif -# include -# include -# include -# include -# include -# include -# include -# include -# include -# include -# include -#elif DMON_OS_MACOS -# include -# include -# include -# include -# include -#elif DMON_OS_AMIGAOS4 || DMON_OS_MORPHOS -#endif - -#ifndef DMON_MALLOC -# include -# define DMON_MALLOC(size) malloc(size) -# define DMON_FREE(ptr) free(ptr) -# define DMON_REALLOC(ptr, size) realloc(ptr, size) -#endif - -#ifndef DMON_ASSERT -# include -# define DMON_ASSERT(e) assert(e) -#endif - -#ifndef DMON_LOG_ERROR -# include -# define DMON_LOG_ERROR(s) do { puts(s); DMON_ASSERT(0); } while(0) -#endif - -#ifndef DMON_LOG_DEBUG -# ifndef NDEBUG -# include -# define DMON_LOG_DEBUG(s) do { puts(s); } while(0) -# else -# define DMON_LOG_DEBUG(s) -# endif -#endif - -#ifndef DMON_MAX_WATCHES -# define DMON_MAX_WATCHES 64 -#endif - -#ifndef DMON_MAX_PATH -# define DMON_MAX_PATH 260 -#endif - -#define _DMON_UNUSED(x) (void)(x) - -#ifndef _DMON_PRIVATE -# if defined(__GNUC__) || defined(__clang__) -# define _DMON_PRIVATE __attribute__((unused)) static -# else -# define _DMON_PRIVATE static -# endif -#endif - -#include - -#ifndef _DMON_LOG_ERRORF -# define _DMON_LOG_ERRORF(str, ...) do { char msg[512]; snprintf(msg, sizeof(msg), str, __VA_ARGS__); DMON_LOG_ERROR(msg); } while(0); -#endif - -#ifndef _DMON_LOG_DEBUGF -# define _DMON_LOG_DEBUGF(str, ...) do { char msg[512]; snprintf(msg, sizeof(msg), str, __VA_ARGS__); DMON_LOG_DEBUG(msg); } while(0); -#endif - -#ifndef dmon__min -# define dmon__min(a, b) ((a) < (b) ? (a) : (b)) -#endif - -#ifndef dmon__max -# define dmon__max(a, b) ((a) > (b) ? (a) : (b)) -#endif - -#ifndef dmon__swap -# define dmon__swap(a, b, _type) \ - do { \ - _type tmp = a; \ - a = b; \ - b = tmp; \ - } while (0) -#endif - -#ifndef dmon__make_id -# ifdef __cplusplus -# define dmon__make_id(id) {id} -# else -# define dmon__make_id(id) (dmon_watch_id) {id} -# endif -#endif // dmon__make_id - -_DMON_PRIVATE bool dmon__isrange(char ch, char from, char to) -{ - return (uint8_t)(ch - from) <= (uint8_t)(to - from); -} - -_DMON_PRIVATE bool dmon__isupperchar(char ch) -{ - return dmon__isrange(ch, 'A', 'Z'); -} - -_DMON_PRIVATE char dmon__tolowerchar(char ch) -{ - return ch + (dmon__isupperchar(ch) ? 0x20 : 0); -} - -_DMON_PRIVATE char* dmon__tolower(char* dst, int dst_sz, const char* str) -{ - int offset = 0; - int dst_max = dst_sz - 1; - while (*str && offset < dst_max) { - dst[offset++] = dmon__tolowerchar(*str); - ++str; - } - dst[offset] = '\0'; - return dst; -} - -_DMON_PRIVATE char* dmon__strcpy(char* dst, int dst_sz, const char* src) -{ - DMON_ASSERT(dst); - DMON_ASSERT(src); - - const int32_t len = (int32_t)strlen(src); - const int32_t _max = dst_sz - 1; - const int32_t num = (len < _max ? len : _max); - memcpy(dst, src, num); - dst[num] = '\0'; - - return dst; -} - -_DMON_PRIVATE char* dmon__unixpath(char* dst, int size, const char* path) -{ - size_t len = strlen(path); - len = dmon__min(len, (size_t)size - 1); - - for (size_t i = 0; i < len; i++) { - if (path[i] != '\\') - dst[i] = path[i]; - else - dst[i] = '/'; - } - dst[len] = '\0'; - return dst; -} - -#if DMON_OS_LINUX || DMON_OS_MACOS -_DMON_PRIVATE char* dmon__strcat(char* dst, int dst_sz, const char* src) -{ - int len = (int)strlen(dst); - return dmon__strcpy(dst + len, dst_sz - len, src); -} -#endif // DMON_OS_LINUX || DMON_OS_MACOS - -// stretchy buffer: https://github.com/nothings/stb/blob/master/stretchy_buffer.h -#define stb_sb_free(a) ((a) ? DMON_FREE(stb__sbraw(a)),0 : 0) -#define stb_sb_push(a,v) (stb__sbmaybegrow(a,1), (a)[stb__sbn(a)++] = (v)) -#define stb_sb_pop(a) (stb__sbn(a)--) -#define stb_sb_count(a) ((a) ? stb__sbn(a) : 0) -#define stb_sb_add(a,n) (stb__sbmaybegrow(a,n), stb__sbn(a)+=(n), &(a)[stb__sbn(a)-(n)]) -#define stb_sb_last(a) ((a)[stb__sbn(a)-1]) -#define stb_sb_reset(a) ((a) ? (stb__sbn(a) = 0) : 0) - -#define stb__sbraw(a) ((int *) (a) - 2) -#define stb__sbm(a) stb__sbraw(a)[0] -#define stb__sbn(a) stb__sbraw(a)[1] - -#define stb__sbneedgrow(a,n) ((a)==0 || stb__sbn(a)+(n) >= stb__sbm(a)) -#define stb__sbmaybegrow(a,n) (stb__sbneedgrow(a,(n)) ? stb__sbgrow(a,n) : 0) -#define stb__sbgrow(a,n) (*((void **)&(a)) = stb__sbgrowf((a), (n), sizeof(*(a)))) - -static void * stb__sbgrowf(void *arr, int increment, int itemsize) -{ - int dbl_cur = arr ? 2*stb__sbm(arr) : 0; - int min_needed = stb_sb_count(arr) + increment; - int m = dbl_cur > min_needed ? dbl_cur : min_needed; - int *p = (int *) DMON_REALLOC(arr ? stb__sbraw(arr) : 0, itemsize * m + sizeof(int)*2); - if (p) { - if (!arr) - p[1] = 0; - p[0] = m; - return p+2; - } else { - return (void *) (2*sizeof(int)); // try to force a NULL pointer exception later - } -} - -// watcher callback (same as dmon.h's decleration) -typedef void (dmon__watch_cb)(dmon_watch_id, dmon_action, const char*, const char*, const char*, void*); - -#if DMON_OS_WINDOWS -// IOCP (windows) -#ifdef UNICODE -# define _DMON_WINAPI_STR(name, size) wchar_t _##name[size]; MultiByteToWideChar(CP_UTF8, 0, name, -1, _##name, size) -#else -# define _DMON_WINAPI_STR(name, size) const char* _##name = name -#endif - -typedef struct dmon__win32_event { - char filepath[DMON_MAX_PATH]; - DWORD action; - dmon_watch_id watch_id; - bool skip; -} dmon__win32_event; - -typedef struct dmon__watch_state { - dmon_watch_id id; - OVERLAPPED overlapped; - HANDLE dir_handle; - uint8_t buffer[64512]; // http://msdn.microsoft.com/en-us/library/windows/desktop/aa365465(v=vs.85).aspx - DWORD notify_filter; - dmon__watch_cb* watch_cb; - uint32_t watch_flags; - void* user_data; - char rootdir[DMON_MAX_PATH]; - char old_filepath[DMON_MAX_PATH]; -} dmon__watch_state; - -typedef struct dmon__state { - int num_watches; - dmon__watch_state watches[DMON_MAX_WATCHES]; - HANDLE thread_handle; - CRITICAL_SECTION mutex; - volatile LONG modify_watches; - dmon__win32_event* events; - bool quit; -} dmon__state; - -static bool _dmon_init; -static dmon__state _dmon; - -_DMON_PRIVATE bool dmon__refresh_watch(dmon__watch_state* watch) -{ - return ReadDirectoryChangesW(watch->dir_handle, watch->buffer, sizeof(watch->buffer), - (watch->watch_flags & DMON_WATCHFLAGS_RECURSIVE) ? TRUE : FALSE, - watch->notify_filter, NULL, &watch->overlapped, NULL) != 0; -} - -_DMON_PRIVATE void dmon__unwatch(dmon__watch_state* watch) -{ - CancelIo(watch->dir_handle); - CloseHandle(watch->overlapped.hEvent); - CloseHandle(watch->dir_handle); - memset(watch, 0x0, sizeof(dmon__watch_state)); -} - -_DMON_PRIVATE void dmon__win32_process_events(void) -{ - for (int i = 0, c = stb_sb_count(_dmon.events); i < c; i++) { - dmon__win32_event* ev = &_dmon.events[i]; - if (ev->skip) { - continue; - } - - if (ev->action == FILE_ACTION_MODIFIED || ev->action == FILE_ACTION_ADDED) { - // remove duplicate modifies on a single file - for (int j = i + 1; j < c; j++) { - dmon__win32_event* check_ev = &_dmon.events[j]; - if (check_ev->action == FILE_ACTION_MODIFIED && - strcmp(ev->filepath, check_ev->filepath) == 0) { - check_ev->skip = true; - } - } - } - } - - // trigger user callbacks - for (int i = 0, c = stb_sb_count(_dmon.events); i < c; i++) { - dmon__win32_event* ev = &_dmon.events[i]; - if (ev->skip) { - continue; - } - dmon__watch_state* watch = &_dmon.watches[ev->watch_id.id - 1]; - - if(watch == NULL || watch->watch_cb == NULL) { - continue; - } - - switch (ev->action) { - case FILE_ACTION_ADDED: - watch->watch_cb(ev->watch_id, DMON_ACTION_CREATE, watch->rootdir, ev->filepath, NULL, - watch->user_data); - break; - case FILE_ACTION_MODIFIED: - watch->watch_cb(ev->watch_id, DMON_ACTION_MODIFY, watch->rootdir, ev->filepath, NULL, - watch->user_data); - break; - case FILE_ACTION_RENAMED_OLD_NAME: { - // find the first occurance of the NEW_NAME - // this is somewhat API flaw that we have no reference for relating old and new files - for (int j = i + 1; j < c; j++) { - dmon__win32_event* check_ev = &_dmon.events[j]; - if (check_ev->action == FILE_ACTION_RENAMED_NEW_NAME) { - watch->watch_cb(check_ev->watch_id, DMON_ACTION_MOVE, watch->rootdir, - check_ev->filepath, ev->filepath, watch->user_data); - break; - } - } - } break; - case FILE_ACTION_REMOVED: - watch->watch_cb(ev->watch_id, DMON_ACTION_DELETE, watch->rootdir, ev->filepath, NULL, - watch->user_data); - break; - } - } - stb_sb_reset(_dmon.events); -} - -_DMON_PRIVATE DWORD WINAPI dmon__thread(LPVOID arg) -{ - _DMON_UNUSED(arg); - HANDLE wait_handles[DMON_MAX_WATCHES]; - - SYSTEMTIME starttm; - GetSystemTime(&starttm); - uint64_t msecs_elapsed = 0; - - while (!_dmon.quit) { - if (_dmon.modify_watches || !TryEnterCriticalSection(&_dmon.mutex)) { - Sleep(10); - continue; - } - - if (_dmon.num_watches == 0) { - Sleep(10); - LeaveCriticalSection(&_dmon.mutex); - continue; - } - - for (int i = 0; i < _dmon.num_watches; i++) { - dmon__watch_state* watch = &_dmon.watches[i]; - wait_handles[i] = watch->overlapped.hEvent; - } - - DWORD wait_result = WaitForMultipleObjects(_dmon.num_watches, wait_handles, FALSE, 10); - DMON_ASSERT(wait_result != WAIT_FAILED); - if (wait_result != WAIT_TIMEOUT) { - dmon__watch_state* watch = &_dmon.watches[wait_result - WAIT_OBJECT_0]; - DMON_ASSERT(HasOverlappedIoCompleted(&watch->overlapped)); - - DWORD bytes; - if (GetOverlappedResult(watch->dir_handle, &watch->overlapped, &bytes, FALSE)) { - char filepath[DMON_MAX_PATH]; - PFILE_NOTIFY_INFORMATION notify; - size_t offset = 0; - - if (bytes == 0) { - dmon__refresh_watch(watch); - LeaveCriticalSection(&_dmon.mutex); - continue; - } - - do { - notify = (PFILE_NOTIFY_INFORMATION)&watch->buffer[offset]; - - int count = WideCharToMultiByte(CP_UTF8, 0, notify->FileName, - notify->FileNameLength / sizeof(WCHAR), - filepath, DMON_MAX_PATH - 1, NULL, NULL); - filepath[count] = TEXT('\0'); - dmon__unixpath(filepath, sizeof(filepath), filepath); - - // TODO: ignore directories if flag is set - - if (stb_sb_count(_dmon.events) == 0) { - msecs_elapsed = 0; - } - dmon__win32_event wev = { { 0 }, notify->Action, watch->id, false }; - dmon__strcpy(wev.filepath, sizeof(wev.filepath), filepath); - stb_sb_push(_dmon.events, wev); - - offset += notify->NextEntryOffset; - } while (notify->NextEntryOffset > 0); - - if (!_dmon.quit) { - dmon__refresh_watch(watch); - } - } - } // if (WaitForMultipleObjects) - - SYSTEMTIME tm; - GetSystemTime(&tm); - LONG dt = - (tm.wSecond - starttm.wSecond) * 1000 + (tm.wMilliseconds - starttm.wMilliseconds); - starttm = tm; - msecs_elapsed += dt; - if (msecs_elapsed > 100 && stb_sb_count(_dmon.events) > 0) { - dmon__win32_process_events(); - msecs_elapsed = 0; - } - - LeaveCriticalSection(&_dmon.mutex); - } - return 0; -} - - -DMON_API_IMPL void dmon_init(void) -{ - DMON_ASSERT(!_dmon_init); - InitializeCriticalSection(&_dmon.mutex); - - _dmon.thread_handle = - CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)dmon__thread, NULL, 0, NULL); - DMON_ASSERT(_dmon.thread_handle); - _dmon_init = true; -} - - -DMON_API_IMPL void dmon_deinit(void) -{ - DMON_ASSERT(_dmon_init); - _dmon.quit = true; - if (_dmon.thread_handle != INVALID_HANDLE_VALUE) { - WaitForSingleObject(_dmon.thread_handle, INFINITE); - CloseHandle(_dmon.thread_handle); - } - - for (int i = 0; i < _dmon.num_watches; i++) { - dmon__unwatch(&_dmon.watches[i]); - } - - DeleteCriticalSection(&_dmon.mutex); - stb_sb_free(_dmon.events); - _dmon_init = false; -} - -DMON_API_IMPL dmon_watch_id dmon_watch(const char* rootdir, - void (*watch_cb)(dmon_watch_id watch_id, dmon_action action, - const char* dirname, const char* filename, - const char* oldname, void* user), - uint32_t flags, void* user_data) -{ - DMON_ASSERT(watch_cb); - DMON_ASSERT(rootdir && rootdir[0]); - - _InterlockedExchange(&_dmon.modify_watches, 1); - EnterCriticalSection(&_dmon.mutex); - - DMON_ASSERT(_dmon.num_watches < DMON_MAX_WATCHES); - - uint32_t id = ++_dmon.num_watches; - dmon__watch_state* watch = &_dmon.watches[id - 1]; - watch->id = dmon__make_id(id); - watch->watch_flags = flags; - watch->watch_cb = watch_cb; - watch->user_data = user_data; - - dmon__strcpy(watch->rootdir, sizeof(watch->rootdir) - 1, rootdir); - dmon__unixpath(watch->rootdir, sizeof(watch->rootdir), rootdir); - size_t rootdir_len = strlen(watch->rootdir); - if (watch->rootdir[rootdir_len - 1] != '/') { - watch->rootdir[rootdir_len] = '/'; - watch->rootdir[rootdir_len + 1] = '\0'; - } - - _DMON_WINAPI_STR(rootdir, DMON_MAX_PATH); - watch->dir_handle = - CreateFile(_rootdir, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, - NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED, NULL); - if (watch->dir_handle != INVALID_HANDLE_VALUE) { - watch->notify_filter = FILE_NOTIFY_CHANGE_CREATION | FILE_NOTIFY_CHANGE_LAST_WRITE | - FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME | - FILE_NOTIFY_CHANGE_SIZE; - watch->overlapped.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL); - DMON_ASSERT(watch->overlapped.hEvent != INVALID_HANDLE_VALUE); - - if (!dmon__refresh_watch(watch)) { - dmon__unwatch(watch); - DMON_LOG_ERROR("ReadDirectoryChanges failed"); - LeaveCriticalSection(&_dmon.mutex); - _InterlockedExchange(&_dmon.modify_watches, 0); - return dmon__make_id(0); - } - } else { - _DMON_LOG_ERRORF("Could not open: %s", rootdir); - LeaveCriticalSection(&_dmon.mutex); - _InterlockedExchange(&_dmon.modify_watches, 0); - return dmon__make_id(0); - } - - LeaveCriticalSection(&_dmon.mutex); - _InterlockedExchange(&_dmon.modify_watches, 0); - return dmon__make_id(id); -} - -DMON_API_IMPL void dmon_unwatch(dmon_watch_id id) -{ - DMON_ASSERT(id.id > 0); - - _InterlockedExchange(&_dmon.modify_watches, 1); - EnterCriticalSection(&_dmon.mutex); - - int index = id.id - 1; - DMON_ASSERT(index < _dmon.num_watches); - - dmon__unwatch(&_dmon.watches[index]); - if (index != _dmon.num_watches - 1) { - dmon__swap(_dmon.watches[index], _dmon.watches[_dmon.num_watches - 1], dmon__watch_state); - } - --_dmon.num_watches; - - LeaveCriticalSection(&_dmon.mutex); - _InterlockedExchange(&_dmon.modify_watches, 0); -} - -#elif DMON_OS_LINUX -// inotify linux backend -#define _DMON_TEMP_BUFFSIZE ((sizeof(struct inotify_event) + PATH_MAX) * 1024) - -typedef struct dmon__watch_subdir { - char rootdir[DMON_MAX_PATH]; -} dmon__watch_subdir; - -typedef struct dmon__inotify_event { - char filepath[DMON_MAX_PATH]; - uint32_t mask; - uint32_t cookie; - dmon_watch_id watch_id; - bool skip; -} dmon__inotify_event; - -typedef struct dmon__watch_state { - dmon_watch_id id; - int fd; - uint32_t watch_flags; - dmon__watch_cb* watch_cb; - void* user_data; - char rootdir[DMON_MAX_PATH]; - dmon__watch_subdir* subdirs; - int* wds; -} dmon__watch_state; - -typedef struct dmon__state { - dmon__watch_state watches[DMON_MAX_WATCHES]; - dmon__inotify_event* events; - int num_watches; - pthread_t thread_handle; - pthread_mutex_t mutex; - bool quit; -} dmon__state; - -static bool _dmon_init; -static dmon__state _dmon; - -_DMON_PRIVATE void dmon__watch_recursive(const char* dirname, int fd, uint32_t mask, - bool followlinks, dmon__watch_state* watch) -{ - struct dirent* entry; - DIR* dir = opendir(dirname); - DMON_ASSERT(dir); - - char watchdir[DMON_MAX_PATH]; - - while ((entry = readdir(dir)) != NULL) { - bool entry_valid = false; - if (entry->d_type == DT_DIR) { - if (strcmp(entry->d_name, "..") != 0 && strcmp(entry->d_name, ".") != 0) { - dmon__strcpy(watchdir, sizeof(watchdir), dirname); - dmon__strcat(watchdir, sizeof(watchdir), entry->d_name); - entry_valid = true; - } - } else if (followlinks && entry->d_type == DT_LNK) { - char linkpath[PATH_MAX]; - dmon__strcpy(watchdir, sizeof(watchdir), dirname); - dmon__strcat(watchdir, sizeof(watchdir), entry->d_name); - char* r = realpath(watchdir, linkpath); - _DMON_UNUSED(r); - DMON_ASSERT(r); - dmon__strcpy(watchdir, sizeof(watchdir), linkpath); - entry_valid = true; - } - - // add sub-directory to watch dirs - if (entry_valid) { - int watchdir_len = (int)strlen(watchdir); - if (watchdir[watchdir_len - 1] != '/') { - watchdir[watchdir_len] = '/'; - watchdir[watchdir_len + 1] = '\0'; - } - int wd = inotify_add_watch(fd, watchdir, mask); - _DMON_UNUSED(wd); - DMON_ASSERT(wd != -1); - - dmon__watch_subdir subdir; - dmon__strcpy(subdir.rootdir, sizeof(subdir.rootdir), watchdir); - if (strstr(subdir.rootdir, watch->rootdir) == subdir.rootdir) { - dmon__strcpy(subdir.rootdir, sizeof(subdir.rootdir), watchdir + strlen(watch->rootdir)); - } - - stb_sb_push(watch->subdirs, subdir); - stb_sb_push(watch->wds, wd); - - // recurse - dmon__watch_recursive(watchdir, fd, mask, followlinks, watch); - } - } - closedir(dir); -} - -_DMON_PRIVATE const char* dmon__find_subdir(const dmon__watch_state* watch, int wd) -{ - const int* wds = watch->wds; - for (int i = 0, c = stb_sb_count(wds); i < c; i++) { - if (wd == wds[i]) { - return watch->subdirs[i].rootdir; - } - } - - return NULL; -} - -_DMON_PRIVATE void dmon__gather_recursive(dmon__watch_state* watch, const char* dirname) -{ - struct dirent* entry; - DIR* dir = opendir(dirname); - DMON_ASSERT(dir); - - char newdir[DMON_MAX_PATH]; - while ((entry = readdir(dir)) != NULL) { - bool entry_valid = false; - bool is_dir = false; - if (strcmp(entry->d_name, "..") != 0 && strcmp(entry->d_name, ".") != 0) { - dmon__strcpy(newdir, sizeof(newdir), dirname); - dmon__strcat(newdir, sizeof(newdir), entry->d_name); - is_dir = (entry->d_type == DT_DIR); - entry_valid = true; - } - - // add sub-directory to watch dirs - if (entry_valid) { - dmon__watch_subdir subdir; - dmon__strcpy(subdir.rootdir, sizeof(subdir.rootdir), newdir); - if (strstr(subdir.rootdir, watch->rootdir) == subdir.rootdir) { - dmon__strcpy(subdir.rootdir, sizeof(subdir.rootdir), newdir + strlen(watch->rootdir)); - } - - dmon__inotify_event dev = { { 0 }, IN_CREATE|(is_dir ? IN_ISDIR : 0), 0, watch->id, false }; - dmon__strcpy(dev.filepath, sizeof(dev.filepath), subdir.rootdir); - stb_sb_push(_dmon.events, dev); - } - } - closedir(dir); -} - -_DMON_PRIVATE void dmon__inotify_process_events(void) -{ - for (int i = 0, c = stb_sb_count(_dmon.events); i < c; i++) { - dmon__inotify_event* ev = &_dmon.events[i]; - if (ev->skip) { - continue; - } - - // remove redundant modify events on a single file - if (ev->mask & IN_MODIFY) { - for (int j = i + 1; j < c; j++) { - dmon__inotify_event* check_ev = &_dmon.events[j]; - if ((check_ev->mask & IN_MODIFY) && strcmp(ev->filepath, check_ev->filepath) == 0) { - ev->skip = true; - break; - } else if ((ev->mask & IN_ISDIR) && (check_ev->mask & (IN_ISDIR|IN_MODIFY))) { - // in some cases, particularly when created files under sub directories - // there can be two modify events for a single subdir one with trailing slash and one without - // remove traling slash from both cases and test - int l1 = (int)strlen(ev->filepath); - int l2 = (int)strlen(check_ev->filepath); - if (ev->filepath[l1-1] == '/') ev->filepath[l1-1] = '\0'; - if (check_ev->filepath[l2-1] == '/') check_ev->filepath[l2-1] = '\0'; - if (strcmp(ev->filepath, check_ev->filepath) == 0) { - ev->skip = true; - break; - } - } - } - } else if (ev->mask & IN_CREATE) { - bool loop_break = false; - for (int j = i + 1; j < c && !loop_break; j++) { - dmon__inotify_event* check_ev = &_dmon.events[j]; - if ((check_ev->mask & IN_MOVED_FROM) && strcmp(ev->filepath, check_ev->filepath) == 0) { - // there is a case where some programs (like gedit): - // when we save, it creates a temp file, and moves it to the file being modified - // search for these cases and remove all of them - for (int k = j + 1; k < c; k++) { - dmon__inotify_event* third_ev = &_dmon.events[k]; - if (third_ev->mask & IN_MOVED_TO && check_ev->cookie == third_ev->cookie) { - third_ev->mask = IN_MODIFY; // change to modified - ev->skip = check_ev->skip = true; - loop_break = true; - break; - } - } - } else if ((check_ev->mask & IN_MODIFY) && strcmp(ev->filepath, check_ev->filepath) == 0) { - // Another case is that file is copied. CREATE and MODIFY happens sequentially - // so we ignore MODIFY event - check_ev->skip = true; - } - } - } else if (ev->mask & IN_MOVED_FROM) { - bool move_valid = false; - for (int j = i + 1; j < c; j++) { - dmon__inotify_event* check_ev = &_dmon.events[j]; - if (check_ev->mask & IN_MOVED_TO && ev->cookie == check_ev->cookie) { - move_valid = true; - break; - } - } - - // in some environments like nautilus file explorer: - // when a file is deleted, it is moved to recycle bin - // so if the destination of the move is not valid, it's probably DELETE - if (!move_valid) { - ev->mask = IN_DELETE; - } - } else if (ev->mask & IN_MOVED_TO) { - bool move_valid = false; - for (int j = 0; j < i; j++) { - dmon__inotify_event* check_ev = &_dmon.events[j]; - if (check_ev->mask & IN_MOVED_FROM && ev->cookie == check_ev->cookie) { - move_valid = true; - break; - } - } - - // in some environments like nautilus file explorer: - // when a file is deleted, it is moved to recycle bin, on undo it is moved back it - // so if the destination of the move is not valid, it's probably CREATE - if (!move_valid) { - ev->mask = IN_CREATE; - } - } else if (ev->mask & IN_DELETE) { - for (int j = i + 1; j < c; j++) { - dmon__inotify_event* check_ev = &_dmon.events[j]; - // if the file is DELETED and then MODIFIED after, just ignore the modify event - if ((check_ev->mask & IN_MODIFY) && strcmp(ev->filepath, check_ev->filepath) == 0) { - check_ev->skip = true; - break; - } - } - } - } - - // trigger user callbacks - for (int i = 0; i < stb_sb_count(_dmon.events); i++) { - dmon__inotify_event* ev = &_dmon.events[i]; - if (ev->skip) { - continue; - } - dmon__watch_state* watch = &_dmon.watches[ev->watch_id.id - 1]; - - if(watch == NULL || watch->watch_cb == NULL) { - continue; - } - - if (ev->mask & IN_CREATE) { - if (ev->mask & IN_ISDIR) { - if (watch->watch_flags & DMON_WATCHFLAGS_RECURSIVE) { - char watchdir[DMON_MAX_PATH]; - dmon__strcpy(watchdir, sizeof(watchdir), watch->rootdir); - dmon__strcat(watchdir, sizeof(watchdir), ev->filepath); - dmon__strcat(watchdir, sizeof(watchdir), "/"); - uint32_t mask = IN_MOVED_TO | IN_CREATE | IN_MOVED_FROM | IN_DELETE | IN_MODIFY; - int wd = inotify_add_watch(watch->fd, watchdir, mask); - // Removing the assertion below because it was giving errors for some reason - // when building a new package. - // _DMON_UNUSED(wd); - // DMON_ASSERT(wd != -1); - if (wd == -1) continue; - - dmon__watch_subdir subdir; - dmon__strcpy(subdir.rootdir, sizeof(subdir.rootdir), watchdir); - if (strstr(subdir.rootdir, watch->rootdir) == subdir.rootdir) { - dmon__strcpy(subdir.rootdir, sizeof(subdir.rootdir), watchdir + strlen(watch->rootdir)); - } - - stb_sb_push(watch->subdirs, subdir); - stb_sb_push(watch->wds, wd); - - // some directories may be already created, for instance, with the command: mkdir -p - // so we will enumerate them manually and add them to the events - dmon__gather_recursive(watch, watchdir); - ev = &_dmon.events[i]; // gotta refresh the pointer because it may be relocated - } - } - watch->watch_cb(ev->watch_id, DMON_ACTION_CREATE, watch->rootdir, ev->filepath, NULL, watch->user_data); - } - else if (ev->mask & IN_MODIFY) { - watch->watch_cb(ev->watch_id, DMON_ACTION_MODIFY, watch->rootdir, ev->filepath, NULL, watch->user_data); - } - else if (ev->mask & IN_MOVED_FROM) { - for (int j = i + 1; j < stb_sb_count(_dmon.events); j++) { - dmon__inotify_event* check_ev = &_dmon.events[j]; - if (check_ev->mask & IN_MOVED_TO && ev->cookie == check_ev->cookie) { - watch->watch_cb(check_ev->watch_id, DMON_ACTION_MOVE, watch->rootdir, - check_ev->filepath, ev->filepath, watch->user_data); - break; - } - } - } - else if (ev->mask & IN_DELETE) { - watch->watch_cb(ev->watch_id, DMON_ACTION_DELETE, watch->rootdir, ev->filepath, NULL, watch->user_data); - } - } - - stb_sb_reset(_dmon.events); -} - -static void* dmon__thread(void* arg) -{ - _DMON_UNUSED(arg); - - static uint8_t buff[_DMON_TEMP_BUFFSIZE]; - struct timespec req = { (time_t)10 / 1000, (long)(10 * 1000000) }; - struct timespec rem = { 0, 0 }; - struct timeval timeout; - uint64_t usecs_elapsed = 0; - - struct timeval starttm; - gettimeofday(&starttm, 0); - - while (!_dmon.quit) { - nanosleep(&req, &rem); - if (_dmon.num_watches == 0 || pthread_mutex_trylock(&_dmon.mutex) != 0) { - continue; - } - - // Create read FD set - fd_set rfds; - FD_ZERO(&rfds); - for (int i = 0; i < _dmon.num_watches; i++) { - dmon__watch_state* watch = &_dmon.watches[i]; - FD_SET(watch->fd, &rfds); - } - - timeout.tv_sec = 0; - timeout.tv_usec = 100000; - if (select(FD_SETSIZE, &rfds, NULL, NULL, &timeout)) { - for (int i = 0; i < _dmon.num_watches; i++) { - dmon__watch_state* watch = &_dmon.watches[i]; - if (FD_ISSET(watch->fd, &rfds)) { - ssize_t offset = 0; - ssize_t len = read(watch->fd, buff, _DMON_TEMP_BUFFSIZE); - if (len <= 0) { - continue; - } - - while (offset < len) { - struct inotify_event* iev = (struct inotify_event*)&buff[offset]; - - const char *subdir = dmon__find_subdir(watch, iev->wd); - if (subdir) { - char filepath[DMON_MAX_PATH]; - dmon__strcpy(filepath, sizeof(filepath), subdir); - dmon__strcat(filepath, sizeof(filepath), iev->name); - - // TODO: ignore directories if flag is set - - if (stb_sb_count(_dmon.events) == 0) { - usecs_elapsed = 0; - } - dmon__inotify_event dev = { { 0 }, iev->mask, iev->cookie, watch->id, false }; - dmon__strcpy(dev.filepath, sizeof(dev.filepath), filepath); - stb_sb_push(_dmon.events, dev); - } - - offset += sizeof(struct inotify_event) + iev->len; - } - } - } - } - - struct timeval tm; - gettimeofday(&tm, 0); - long dt = (tm.tv_sec - starttm.tv_sec) * 1000000 + tm.tv_usec - starttm.tv_usec; - starttm = tm; - usecs_elapsed += dt; - if (usecs_elapsed > 100000 && stb_sb_count(_dmon.events) > 0) { - dmon__inotify_process_events(); - usecs_elapsed = 0; - } - - pthread_mutex_unlock(&_dmon.mutex); - } - return 0x0; -} - -_DMON_PRIVATE void dmon__unwatch(dmon__watch_state* watch) -{ - close(watch->fd); - stb_sb_free(watch->subdirs); - stb_sb_free(watch->wds); - memset(watch, 0x0, sizeof(dmon__watch_state)); -} - -DMON_API_IMPL void dmon_init(void) -{ - DMON_ASSERT(!_dmon_init); - pthread_mutex_init(&_dmon.mutex, NULL); - - int r = pthread_create(&_dmon.thread_handle, NULL, dmon__thread, NULL); - _DMON_UNUSED(r); - DMON_ASSERT(r == 0 && "pthread_create failed"); - _dmon_init = true; -} - -DMON_API_IMPL void dmon_deinit(void) -{ - DMON_ASSERT(_dmon_init); - _dmon.quit = true; - pthread_join(_dmon.thread_handle, NULL); - - for (int i = 0; i < _dmon.num_watches; i++) { - dmon__unwatch(&_dmon.watches[i]); - } - - pthread_mutex_destroy(&_dmon.mutex); - stb_sb_free(_dmon.events); - _dmon_init = false; -} - -DMON_API_IMPL dmon_watch_id dmon_watch(const char* rootdir, - void (*watch_cb)(dmon_watch_id watch_id, dmon_action action, - const char* dirname, const char* filename, - const char* oldname, void* user), - uint32_t flags, void* user_data) -{ - DMON_ASSERT(watch_cb); - DMON_ASSERT(rootdir && rootdir[0]); - - pthread_mutex_lock(&_dmon.mutex); - - DMON_ASSERT(_dmon.num_watches < DMON_MAX_WATCHES); - - uint32_t id = ++_dmon.num_watches; - dmon__watch_state* watch = &_dmon.watches[id - 1]; - watch->id = dmon__make_id(id); - watch->watch_flags = flags; - watch->watch_cb = watch_cb; - watch->user_data = user_data; - - struct stat root_st; - if (stat(rootdir, &root_st) != 0 || !S_ISDIR(root_st.st_mode) || - (root_st.st_mode & S_IRUSR) != S_IRUSR) { - _DMON_LOG_ERRORF("Could not open/read directory: %s", rootdir); - pthread_mutex_unlock(&_dmon.mutex); - return dmon__make_id(0); - } - - - if (S_ISLNK(root_st.st_mode)) { - if (flags & DMON_WATCHFLAGS_FOLLOW_SYMLINKS) { - char linkpath[PATH_MAX]; - char* r = realpath(rootdir, linkpath); - _DMON_UNUSED(r); - DMON_ASSERT(r); - - dmon__strcpy(watch->rootdir, sizeof(watch->rootdir) - 1, linkpath); - } else { - _DMON_LOG_ERRORF("symlinks are unsupported: %s. use DMON_WATCHFLAGS_FOLLOW_SYMLINKS", - rootdir); - pthread_mutex_unlock(&_dmon.mutex); - return dmon__make_id(0); - } - } else { - dmon__strcpy(watch->rootdir, sizeof(watch->rootdir) - 1, rootdir); - } - - // add trailing slash - int rootdir_len = (int)strlen(watch->rootdir); - if (watch->rootdir[rootdir_len - 1] != '/') { - watch->rootdir[rootdir_len] = '/'; - watch->rootdir[rootdir_len + 1] = '\0'; - } - - watch->fd = inotify_init(); - if (watch->fd < -1) { - DMON_LOG_ERROR("could not create inotify instance"); - pthread_mutex_unlock(&_dmon.mutex); - return dmon__make_id(0); - } - - uint32_t inotify_mask = IN_MOVED_TO | IN_CREATE | IN_MOVED_FROM | IN_DELETE | IN_MODIFY; - int wd = inotify_add_watch(watch->fd, watch->rootdir, inotify_mask); - if (wd < 0) { - _DMON_LOG_ERRORF("Error watching directory '%s'. (inotify_add_watch:err=%d)", watch->rootdir, errno); - pthread_mutex_unlock(&_dmon.mutex); - return dmon__make_id(0); - } - dmon__watch_subdir subdir; - dmon__strcpy(subdir.rootdir, sizeof(subdir.rootdir), ""); // root dir is just a dummy entry - stb_sb_push(watch->subdirs, subdir); - stb_sb_push(watch->wds, wd); - - // recursive mode: enumarate all child directories and add them to watch - if (flags & DMON_WATCHFLAGS_RECURSIVE) { - dmon__watch_recursive(watch->rootdir, watch->fd, inotify_mask, - (flags & DMON_WATCHFLAGS_FOLLOW_SYMLINKS) ? true : false, watch); - } - - - pthread_mutex_unlock(&_dmon.mutex); - return dmon__make_id(id); -} - -DMON_API_IMPL void dmon_unwatch(dmon_watch_id id) -{ - DMON_ASSERT(id.id > 0); - - pthread_mutex_lock(&_dmon.mutex); - - int index = id.id - 1; - DMON_ASSERT(index < _dmon.num_watches); - - dmon__unwatch(&_dmon.watches[index]); - if (index != _dmon.num_watches - 1) { - dmon__swap(_dmon.watches[index], _dmon.watches[_dmon.num_watches - 1], dmon__watch_state); - } - --_dmon.num_watches; - - pthread_mutex_unlock(&_dmon.mutex); -} -#elif DMON_OS_MACOS -// FSEvents MacOS backend -typedef struct dmon__fsevent_event { - char filepath[DMON_MAX_PATH]; - uint64_t event_id; - long event_flags; - dmon_watch_id watch_id; - bool skip; - bool move_valid; -} dmon__fsevent_event; - -typedef struct dmon__watch_state { - dmon_watch_id id; - uint32_t watch_flags; - FSEventStreamRef fsev_stream_ref; - dmon__watch_cb* watch_cb; - void* user_data; - char rootdir[DMON_MAX_PATH]; - char rootdir_unmod[DMON_MAX_PATH]; - bool init; -} dmon__watch_state; - -typedef struct dmon__state { - dmon__watch_state watches[DMON_MAX_WATCHES]; - dmon__fsevent_event* events; - int num_watches; - volatile int modify_watches; - pthread_t thread_handle; - dispatch_semaphore_t thread_sem; - pthread_mutex_t mutex; - CFRunLoopRef cf_loop_ref; - CFAllocatorRef cf_alloc_ref; - bool quit; -} dmon__state; - -union dmon__cast_userdata { - void* ptr; - uint32_t id; -}; - -static bool _dmon_init; -static dmon__state _dmon; - -_DMON_PRIVATE void* dmon__cf_malloc(CFIndex size, CFOptionFlags hints, void* info) -{ - _DMON_UNUSED(hints); - _DMON_UNUSED(info); - return DMON_MALLOC(size); -} - -_DMON_PRIVATE void dmon__cf_free(void* ptr, void* info) -{ - _DMON_UNUSED(info); - DMON_FREE(ptr); -} - -_DMON_PRIVATE void* dmon__cf_realloc(void* ptr, CFIndex newsize, CFOptionFlags hints, void* info) -{ - _DMON_UNUSED(hints); - _DMON_UNUSED(info); - return DMON_REALLOC(ptr, (size_t)newsize); -} - -_DMON_PRIVATE void dmon__fsevent_process_events(void) -{ - for (int i = 0, c = stb_sb_count(_dmon.events); i < c; i++) { - dmon__fsevent_event* ev = &_dmon.events[i]; - if (ev->skip) { - continue; - } - - // remove redundant modify events on a single file - if (ev->event_flags & kFSEventStreamEventFlagItemModified) { - for (int j = i + 1; j < c; j++) { - dmon__fsevent_event* check_ev = &_dmon.events[j]; - if ((check_ev->event_flags & kFSEventStreamEventFlagItemModified) && - strcmp(ev->filepath, check_ev->filepath) == 0) { - ev->skip = true; - break; - } - } - } else if ((ev->event_flags & kFSEventStreamEventFlagItemRenamed) && !ev->move_valid) { - for (int j = i + 1; j < c; j++) { - dmon__fsevent_event* check_ev = &_dmon.events[j]; - if ((check_ev->event_flags & kFSEventStreamEventFlagItemRenamed) && - check_ev->event_id == (ev->event_id + 1)) { - ev->move_valid = check_ev->move_valid = true; - break; - } - } - - // in some environments like finder file explorer: - // when a file is deleted, it is moved to recycle bin - // so if the destination of the move is not valid, it's probably DELETE or CREATE - // decide CREATE if file exists - if (!ev->move_valid) { - ev->event_flags &= ~kFSEventStreamEventFlagItemRenamed; - - char abs_filepath[DMON_MAX_PATH]; - dmon__watch_state* watch = &_dmon.watches[ev->watch_id.id-1]; - dmon__strcpy(abs_filepath, sizeof(abs_filepath), watch->rootdir); - dmon__strcat(abs_filepath, sizeof(abs_filepath), ev->filepath); - - struct stat root_st; - if (stat(abs_filepath, &root_st) != 0) { - ev->event_flags |= kFSEventStreamEventFlagItemRemoved; - } else { - ev->event_flags |= kFSEventStreamEventFlagItemCreated; - } - } - } - } - - // trigger user callbacks - for (int i = 0, c = stb_sb_count(_dmon.events); i < c; i++) { - dmon__fsevent_event* ev = &_dmon.events[i]; - if (ev->skip) { - continue; - } - dmon__watch_state* watch = &_dmon.watches[ev->watch_id.id - 1]; - - if(watch == NULL || watch->watch_cb == NULL) { - continue; - } - - if (ev->event_flags & kFSEventStreamEventFlagItemCreated) { - watch->watch_cb(ev->watch_id, DMON_ACTION_CREATE, watch->rootdir_unmod, ev->filepath, NULL, - watch->user_data); - } else if (ev->event_flags & kFSEventStreamEventFlagItemModified) { - watch->watch_cb(ev->watch_id, DMON_ACTION_MODIFY, watch->rootdir_unmod, ev->filepath, NULL, - watch->user_data); - } else if (ev->event_flags & kFSEventStreamEventFlagItemRenamed) { - for (int j = i + 1; j < c; j++) { - dmon__fsevent_event* check_ev = &_dmon.events[j]; - if (check_ev->event_flags & kFSEventStreamEventFlagItemRenamed) { - watch->watch_cb(check_ev->watch_id, DMON_ACTION_MOVE, watch->rootdir_unmod, - check_ev->filepath, ev->filepath, watch->user_data); - break; - } - } - } else if (ev->event_flags & kFSEventStreamEventFlagItemRemoved) { - watch->watch_cb(ev->watch_id, DMON_ACTION_DELETE, watch->rootdir_unmod, ev->filepath, NULL, - watch->user_data); - } - } - - stb_sb_reset(_dmon.events); -} - -static void* dmon__thread(void* arg) -{ - _DMON_UNUSED(arg); - - struct timespec req = { (time_t)10 / 1000, (long)(10 * 1000000) }; - struct timespec rem = { 0, 0 }; - - _dmon.cf_loop_ref = CFRunLoopGetCurrent(); - dispatch_semaphore_signal(_dmon.thread_sem); - - while (!_dmon.quit) { - if (_dmon.modify_watches || pthread_mutex_trylock(&_dmon.mutex) != 0) { - nanosleep(&req, &rem); - continue; - } - - if (_dmon.num_watches == 0) { - nanosleep(&req, &rem); - pthread_mutex_unlock(&_dmon.mutex); - continue; - } - - for (int i = 0; i < _dmon.num_watches; i++) { - dmon__watch_state* watch = &_dmon.watches[i]; - if (!watch->init) { - DMON_ASSERT(watch->fsev_stream_ref); - FSEventStreamScheduleWithRunLoop(watch->fsev_stream_ref, _dmon.cf_loop_ref, - kCFRunLoopDefaultMode); - FSEventStreamStart(watch->fsev_stream_ref); - - watch->init = true; - } - } - - CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.5, kCFRunLoopRunTimedOut); - dmon__fsevent_process_events(); - - pthread_mutex_unlock(&_dmon.mutex); - } - - CFRunLoopStop(_dmon.cf_loop_ref); - _dmon.cf_loop_ref = NULL; - return 0x0; -} - -_DMON_PRIVATE void dmon__unwatch(dmon__watch_state* watch) -{ - if (watch->fsev_stream_ref) { - FSEventStreamStop(watch->fsev_stream_ref); - FSEventStreamInvalidate(watch->fsev_stream_ref); - FSEventStreamRelease(watch->fsev_stream_ref); - watch->fsev_stream_ref = NULL; - } - - memset(watch, 0x0, sizeof(dmon__watch_state)); -} - -DMON_API_IMPL void dmon_init(void) -{ - DMON_ASSERT(!_dmon_init); - pthread_mutex_init(&_dmon.mutex, NULL); - - CFAllocatorContext cf_alloc_ctx = { 0 }; - cf_alloc_ctx.allocate = dmon__cf_malloc; - cf_alloc_ctx.deallocate = dmon__cf_free; - cf_alloc_ctx.reallocate = dmon__cf_realloc; - _dmon.cf_alloc_ref = CFAllocatorCreate(NULL, &cf_alloc_ctx); - - _dmon.thread_sem = dispatch_semaphore_create(0); - DMON_ASSERT(_dmon.thread_sem); - - int r = pthread_create(&_dmon.thread_handle, NULL, dmon__thread, NULL); - _DMON_UNUSED(r); - DMON_ASSERT(r == 0 && "pthread_create failed"); - - // wait for thread to initialize loop object - dispatch_semaphore_wait(_dmon.thread_sem, DISPATCH_TIME_FOREVER); - - _dmon_init = true; -} - -DMON_API_IMPL void dmon_deinit(void) -{ - DMON_ASSERT(_dmon_init); - _dmon.quit = true; - pthread_join(_dmon.thread_handle, NULL); - - dispatch_release(_dmon.thread_sem); - - for (int i = 0; i < _dmon.num_watches; i++) { - dmon__unwatch(&_dmon.watches[i]); - } - - pthread_mutex_destroy(&_dmon.mutex); - stb_sb_free(_dmon.events); - if (_dmon.cf_alloc_ref) { - CFRelease(_dmon.cf_alloc_ref); - } - - _dmon_init = false; -} - -_DMON_PRIVATE void dmon__fsevent_callback(ConstFSEventStreamRef stream_ref, void* user_data, - size_t num_events, void* event_paths, - const FSEventStreamEventFlags event_flags[], - const FSEventStreamEventId event_ids[]) -{ - _DMON_UNUSED(stream_ref); - - union dmon__cast_userdata _userdata; - _userdata.ptr = user_data; - dmon_watch_id watch_id = dmon__make_id(_userdata.id); - DMON_ASSERT(watch_id.id > 0); - dmon__watch_state* watch = &_dmon.watches[watch_id.id - 1]; - char abs_filepath[DMON_MAX_PATH]; - char abs_filepath_lower[DMON_MAX_PATH]; - - for (size_t i = 0; i < num_events; i++) { - const char* filepath = ((const char**)event_paths)[i]; - long flags = (long)event_flags[i]; - uint64_t event_id = (uint64_t)event_ids[i]; - dmon__fsevent_event ev; - memset(&ev, 0x0, sizeof(ev)); - - dmon__strcpy(abs_filepath, sizeof(abs_filepath), filepath); - dmon__unixpath(abs_filepath, sizeof(abs_filepath), abs_filepath); - - // normalize path, so it would be the same on both MacOS file-system types (case/nocase) - dmon__tolower(abs_filepath_lower, sizeof(abs_filepath), abs_filepath); - DMON_ASSERT(strstr(abs_filepath_lower, watch->rootdir) == abs_filepath_lower); - - // strip the root dir from the begining - dmon__strcpy(ev.filepath, sizeof(ev.filepath), abs_filepath + strlen(watch->rootdir)); - - ev.event_flags = flags; - ev.event_id = event_id; - ev.watch_id = watch_id; - stb_sb_push(_dmon.events, ev); - } -} - -DMON_API_IMPL dmon_watch_id dmon_watch(const char* rootdir, - void (*watch_cb)(dmon_watch_id watch_id, dmon_action action, - const char* dirname, const char* filename, - const char* oldname, void* user), - uint32_t flags, void* user_data) -{ - DMON_ASSERT(watch_cb); - DMON_ASSERT(rootdir && rootdir[0]); - - __sync_lock_test_and_set(&_dmon.modify_watches, 1); - pthread_mutex_lock(&_dmon.mutex); - - DMON_ASSERT(_dmon.num_watches < DMON_MAX_WATCHES); - - uint32_t id = ++_dmon.num_watches; - dmon__watch_state* watch = &_dmon.watches[id - 1]; - watch->id = dmon__make_id(id); - watch->watch_flags = flags; - watch->watch_cb = watch_cb; - watch->user_data = user_data; - - struct stat root_st; - if (stat(rootdir, &root_st) != 0 || !S_ISDIR(root_st.st_mode) || - (root_st.st_mode & S_IRUSR) != S_IRUSR) { - _DMON_LOG_ERRORF("Could not open/read directory: %s", rootdir); - pthread_mutex_unlock(&_dmon.mutex); - __sync_lock_test_and_set(&_dmon.modify_watches, 0); - return dmon__make_id(0); - } - - if (S_ISLNK(root_st.st_mode)) { - if (flags & DMON_WATCHFLAGS_FOLLOW_SYMLINKS) { - char linkpath[PATH_MAX]; - char* r = realpath(rootdir, linkpath); - _DMON_UNUSED(r); - DMON_ASSERT(r); - - dmon__strcpy(watch->rootdir, sizeof(watch->rootdir) - 1, linkpath); - } else { - _DMON_LOG_ERRORF("symlinks are unsupported: %s. use DMON_WATCHFLAGS_FOLLOW_SYMLINKS", rootdir); - pthread_mutex_unlock(&_dmon.mutex); - __sync_lock_test_and_set(&_dmon.modify_watches, 0); - return dmon__make_id(0); - } - } else { - char rootdir_abspath[DMON_MAX_PATH]; - if (realpath(rootdir, rootdir_abspath) != NULL) { - dmon__strcpy(watch->rootdir, sizeof(watch->rootdir) - 1, rootdir_abspath); - } else { - dmon__strcpy(watch->rootdir, sizeof(watch->rootdir) - 1, rootdir); - } - } - - dmon__unixpath(watch->rootdir, sizeof(watch->rootdir), watch->rootdir); - - // add trailing slash - int rootdir_len = (int)strlen(watch->rootdir); - if (watch->rootdir[rootdir_len - 1] != '/') { - watch->rootdir[rootdir_len] = '/'; - watch->rootdir[rootdir_len + 1] = '\0'; - } - - dmon__strcpy(watch->rootdir_unmod, sizeof(watch->rootdir_unmod), watch->rootdir); - dmon__tolower(watch->rootdir, sizeof(watch->rootdir), watch->rootdir); - - // create FS objects - CFStringRef cf_dir = CFStringCreateWithCString(NULL, watch->rootdir_unmod, kCFStringEncodingUTF8); - CFArrayRef cf_dirarr = CFArrayCreate(NULL, (const void**)&cf_dir, 1, NULL); - - FSEventStreamContext ctx; - union dmon__cast_userdata userdata; - userdata.id = id; - ctx.version = 0; - ctx.info = userdata.ptr; - ctx.retain = NULL; - ctx.release = NULL; - ctx.copyDescription = NULL; - watch->fsev_stream_ref = FSEventStreamCreate(_dmon.cf_alloc_ref, dmon__fsevent_callback, &ctx, - cf_dirarr, kFSEventStreamEventIdSinceNow, 0.25, - kFSEventStreamCreateFlagFileEvents); - - - CFRelease(cf_dirarr); - CFRelease(cf_dir); - - pthread_mutex_unlock(&_dmon.mutex); - __sync_lock_test_and_set(&_dmon.modify_watches, 0); - return dmon__make_id(id); -} - -DMON_API_IMPL void dmon_unwatch(dmon_watch_id id) -{ - DMON_ASSERT(id.id > 0); - - __sync_lock_test_and_set(&_dmon.modify_watches, 1); - pthread_mutex_lock(&_dmon.mutex); - - int index = id.id - 1; - DMON_ASSERT(index < _dmon.num_watches); - - dmon__unwatch(&_dmon.watches[index]); - if (index != _dmon.num_watches - 1) { - dmon__swap(_dmon.watches[index], _dmon.watches[_dmon.num_watches - 1], dmon__watch_state); - } - --_dmon.num_watches; - - pthread_mutex_unlock(&_dmon.mutex); - __sync_lock_test_and_set(&_dmon.modify_watches, 0); -} - -#endif - -#endif // DMON_IMPL -#endif // __DMON_H__ - diff --git a/lib/dmon/dmon_extra.h b/lib/dmon/dmon_extra.h deleted file mode 100644 index 4b321034..00000000 --- a/lib/dmon/dmon_extra.h +++ /dev/null @@ -1,162 +0,0 @@ -#ifndef __DMON_EXTRA_H__ -#define __DMON_EXTRA_H__ - -// -// Copyright 2021 Sepehr Taghdisian (septag@github). All rights reserved. -// License: https://github.com/septag/dmon#license-bsd-2-clause -// -// Extra header functionality for dmon.h for the backend based on inotify -// -// Add/Remove directory functions: -// dmon_watch_add: Adds a sub-directory to already valid watch_id. sub-directories are assumed to be relative to watch root_dir -// dmon_watch_add: Removes a sub-directory from already valid watch_id. sub-directories are assumed to be relative to watch root_dir -// Reason: The inotify backend does not work well when watching in recursive mode a root directory of a large tree, it may take -// quite a while until all inotify watches are established, and events will not be received in this time. Also, since one -// inotify watch will be established per subdirectory, it is possible that the maximum amount of inotify watches per user -// will be reached. The default maximum is 8192. -// When using inotify backend users may turn off the DMON_WATCHFLAGS_RECURSIVE flag and add/remove selectively the -// sub-directories to be watched based on application-specific logic about which sub-directory actually needs to be watched. -// The function dmon_watch_add and dmon_watch_rm are used to this purpose. -// - -#ifndef __DMON_H__ -#error "Include 'dmon.h' before including this file" -#endif - -#ifdef __cplusplus -extern "C" { -#endif - -DMON_API_DECL bool dmon_watch_add(dmon_watch_id id, const char* subdir); -DMON_API_DECL bool dmon_watch_rm(dmon_watch_id id, const char* watchdir); - -#ifdef __cplusplus -} -#endif - -#ifdef DMON_IMPL -#if DMON_OS_LINUX -DMON_API_IMPL bool dmon_watch_add(dmon_watch_id id, const char* watchdir) -{ - DMON_ASSERT(id.id > 0 && id.id <= DMON_MAX_WATCHES); - - bool skip_lock = pthread_self() == _dmon.thread_handle; - - if (!skip_lock) - pthread_mutex_lock(&_dmon.mutex); - - dmon__watch_state* watch = &_dmon.watches[id.id - 1]; - - // check if the directory exists - // if watchdir contains absolute/root-included path, try to strip the rootdir from it - // else, we assume that watchdir is correct, so save it as it is - struct stat st; - dmon__watch_subdir subdir; - if (stat(watchdir, &st) == 0 && (st.st_mode & S_IFDIR)) { - dmon__strcpy(subdir.rootdir, sizeof(subdir.rootdir), watchdir); - if (strstr(subdir.rootdir, watch->rootdir) == subdir.rootdir) { - dmon__strcpy(subdir.rootdir, sizeof(subdir.rootdir), watchdir + strlen(watch->rootdir)); - } - } else { - char fullpath[DMON_MAX_PATH]; - dmon__strcpy(fullpath, sizeof(fullpath), watch->rootdir); - dmon__strcat(fullpath, sizeof(fullpath), watchdir); - if (stat(fullpath, &st) != 0 || (st.st_mode & S_IFDIR) == 0) { - _DMON_LOG_ERRORF("Watch directory '%s' is not valid", watchdir); - if (!skip_lock) - pthread_mutex_unlock(&_dmon.mutex); - return false; - } - dmon__strcpy(subdir.rootdir, sizeof(subdir.rootdir), watchdir); - } - - int dirlen = (int)strlen(subdir.rootdir); - if (subdir.rootdir[dirlen - 1] != '/') { - subdir.rootdir[dirlen] = '/'; - subdir.rootdir[dirlen + 1] = '\0'; - } - - // check that the directory is not already added - for (int i = 0, c = stb_sb_count(watch->subdirs); i < c; i++) { - if (strcmp(subdir.rootdir, watch->subdirs[i].rootdir) == 0) { - _DMON_LOG_ERRORF("Error watching directory '%s', because it is already added.", watchdir); - if (!skip_lock) - pthread_mutex_unlock(&_dmon.mutex); - return false; - } - } - - const uint32_t inotify_mask = IN_MOVED_TO | IN_CREATE | IN_MOVED_FROM | IN_DELETE | IN_MODIFY; - char fullpath[DMON_MAX_PATH]; - dmon__strcpy(fullpath, sizeof(fullpath), watch->rootdir); - dmon__strcat(fullpath, sizeof(fullpath), subdir.rootdir); - int wd = inotify_add_watch(watch->fd, fullpath, inotify_mask); - if (wd == -1) { - _DMON_LOG_ERRORF("Error watching directory '%s'. (inotify_add_watch:err=%d)", watchdir, errno); - if (!skip_lock) - pthread_mutex_unlock(&_dmon.mutex); - return false; - } - - stb_sb_push(watch->subdirs, subdir); - stb_sb_push(watch->wds, wd); - - if (!skip_lock) - pthread_mutex_unlock(&_dmon.mutex); - - return true; -} - -DMON_API_IMPL bool dmon_watch_rm(dmon_watch_id id, const char* watchdir) -{ - DMON_ASSERT(id.id > 0 && id.id <= DMON_MAX_WATCHES); - - bool skip_lock = pthread_self() == _dmon.thread_handle; - - if (!skip_lock) - pthread_mutex_lock(&_dmon.mutex); - - dmon__watch_state* watch = &_dmon.watches[id.id - 1]; - - char subdir[DMON_MAX_PATH]; - dmon__strcpy(subdir, sizeof(subdir), watchdir); - if (strstr(subdir, watch->rootdir) == subdir) { - dmon__strcpy(subdir, sizeof(subdir), watchdir + strlen(watch->rootdir)); - } - - int dirlen = (int)strlen(subdir); - if (subdir[dirlen - 1] != '/') { - subdir[dirlen] = '/'; - subdir[dirlen + 1] = '\0'; - } - - int i, c = stb_sb_count(watch->subdirs); - for (i = 0; i < c; i++) { - if (strcmp(watch->subdirs[i].rootdir, subdir) == 0) { - break; - } - } - if (i >= c) { - _DMON_LOG_ERRORF("Watch directory '%s' is not valid", watchdir); - if (!skip_lock) - pthread_mutex_unlock(&_dmon.mutex); - return false; - } - inotify_rm_watch(watch->fd, watch->wds[i]); - - /* Remove entry from subdirs and wds by swapping position with the last entry */ - watch->subdirs[i] = stb_sb_last(watch->subdirs); - stb_sb_pop(watch->subdirs); - - watch->wds[i] = stb_sb_last(watch->wds); - stb_sb_pop(watch->wds); - - if (!skip_lock) - pthread_mutex_unlock(&_dmon.mutex); - return true; -} -#endif // DMON_OS_LINUX -#endif // DMON_IMPL - -#endif // __DMON_EXTRA_H__ - diff --git a/lib/dmon/meson.build b/lib/dmon/meson.build deleted file mode 100644 index 83edd1c9..00000000 --- a/lib/dmon/meson.build +++ /dev/null @@ -1 +0,0 @@ -lite_includes += include_directories('.') diff --git a/licenses/licenses.md b/licenses/licenses.md index 928d88d9..8005c4a7 100644 --- a/licenses/licenses.md +++ b/licenses/licenses.md @@ -22,33 +22,6 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -## septag/dmon - -Copyright 2019 Sepehr Taghdisian. All rights reserved. - -https://github.com/septag/dmon - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - - 1. Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - - 2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY COPYRIGHT HOLDER "AS IS" AND ANY EXPRESS OR -IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO -EVENT SHALL COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, -INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, -BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE -OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF -ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - ## Fira Sans Digitized data copyright (c) 2012-2015, The Mozilla Foundation and Telefonica S.A. diff --git a/meson.build b/meson.build index 87544c7f..9454fceb 100644 --- a/meson.build +++ b/meson.build @@ -1,18 +1,41 @@ project('lite-xl', ['c'], - version : '2.0.3', + version : '2.1.0', license : 'MIT', - meson_version : '>= 0.54', - default_options : ['c_std=gnu11'] + meson_version : '>= 0.47', + default_options : [ + 'c_std=gnu11', + 'wrap_mode=nofallback' + ] ) +#=============================================================================== +# Project version including git commit if possible +#=============================================================================== +version = meson.project_version() + +if get_option('buildtype') != 'release' + git_command = find_program('git', required : false) + + if git_command.found() + git_commit = run_command( + [git_command, 'rev-parse', 'HEAD'], + check : false + ).stdout().strip() + + if git_commit != '' + version += ' (git-' + git_commit.substring(0, 8) + ')' + endif + endif +endif + #=============================================================================== # Configuration #=============================================================================== conf_data = configuration_data() conf_data.set('PROJECT_BUILD_DIR', meson.current_build_dir()) conf_data.set('PROJECT_SOURCE_DIR', meson.current_source_dir()) -conf_data.set('PROJECT_VERSION', meson.project_version()) +conf_data.set('PROJECT_VERSION', version) #=============================================================================== # Compiler Settings @@ -24,7 +47,7 @@ endif cc = meson.get_compiler('c') lite_includes = [] -lite_cargs = [] +lite_cargs = ['-DSDL_MAIN_HANDLED', '-DPCRE2_STATIC'] # On macos we need to use the SDL renderer to support retina displays if get_option('renderer') or host_machine.system() == 'darwin' lite_cargs += '-DLITE_USE_SDL_RENDERER' @@ -46,14 +69,84 @@ endif if not get_option('source-only') libm = cc.find_library('m', required : false) libdl = cc.find_library('dl', required : false) - threads_dep = dependency('threads') - lua_dep = dependency('lua5.2', fallback: ['lua', 'lua_dep'], - default_options: ['shared=false', 'use_readline=false', 'app=false'] + + default_fallback_options = ['warning_level=0', 'werror=false'] + + # Lua has no official .pc file + # so distros come up with their own names + lua_names = [ + 'lua5.4', # Debian + 'lua-5.4', # FreeBSD + 'lua', # Fedora + ] + + foreach lua : lua_names + last_lua = (lua == lua_names[-1] or get_option('wrap_mode') == 'forcefallback') + lua_dep = dependency(lua, fallback: last_lua ? ['lua', 'lua_dep'] : [], required : last_lua, + version: '>= 5.4', + default_options: default_fallback_options + ['default_library=static', 'line_editing=false', 'interpreter=false'] + ) + if lua_dep.found() + break + endif + endforeach + + pcre2_dep = dependency('libpcre2-8', fallback: ['pcre2', 'libpcre2_8'], + default_options: default_fallback_options + ['default_library=static', 'grep=false', 'test=false'] ) - pcre2_dep = dependency('libpcre2-8') - freetype_dep = dependency('freetype2') - sdl_dep = dependency('sdl2', method: 'config-tool') - lite_deps = [lua_dep, sdl_dep, freetype_dep, pcre2_dep, libm, libdl, threads_dep] + + freetype_dep = dependency('freetype2', fallback: ['freetype2', 'freetype_dep'], + default_options: default_fallback_options + ['default_library=static', 'zlib=disabled', 'bzip2=disabled', 'png=disabled', 'harfbuzz=disabled', 'brotli=disabled'] + ) + + + sdl_options = ['default_library=static'] + + # we explicitly need these + sdl_options += 'use_loadso=enabled' + sdl_options += 'prefer_dlopen=true' + sdl_options += 'use_video=enabled' + sdl_options += 'use_atomic=enabled' + sdl_options += 'use_threads=enabled' + sdl_options += 'use_timers=enabled' + # investigate if this is truly needed + # Do not remove before https://github.com/libsdl-org/SDL/issues/5413 is released + sdl_options += 'use_events=enabled' + + if host_machine.system() == 'darwin' or host_machine.system() == 'windows' + sdl_options += 'use_video_x11=disabled' + sdl_options += 'use_video_wayland=disabled' + else + sdl_options += 'use_render=enabled' + sdl_options += 'use_video_x11=auto' + sdl_options += 'use_video_wayland=auto' + endif + + # we leave this up to what the host system has except on windows + if host_machine.system() != 'windows' + sdl_options += 'use_video_opengl=auto' + sdl_options += 'use_video_openglesv2=auto' + else + sdl_options += 'use_video_opengl=disabled' + sdl_options += 'use_video_openglesv2=disabled' + endif + + # we don't need these + sdl_options += 'test=false' + sdl_options += 'use_sensor=disabled' + sdl_options += 'use_haptic=disabled' + sdl_options += 'use_audio=disabled' + sdl_options += 'use_cpuinfo=disabled' + sdl_options += 'use_joystick=disabled' + sdl_options += 'use_video_vulkan=disabled' + sdl_options += 'use_video_offscreen=disabled' + sdl_options += 'use_power=disabled' + + sdl_dep = dependency('sdl2', fallback: ['sdl2', 'sdl2_dep'], + default_options: default_fallback_options + sdl_options + ) + + lite_deps = [lua_dep, sdl_dep, freetype_dep, pcre2_dep, libm, libdl] endif #=============================================================================== # Install Configuration @@ -94,21 +187,20 @@ endif install_data('licenses/licenses.md', install_dir : lite_docdir) -install_subdir('data' / 'core' , install_dir : lite_datadir, exclude_files : 'start.lua') +install_subdir('docs/api' , install_dir : lite_datadir, strip_directory: true) +install_subdir('data/core' , install_dir : lite_datadir, exclude_files : 'start.lua') foreach data_module : ['fonts', 'plugins', 'colors'] - install_subdir('data' / data_module , install_dir : lite_datadir) + install_subdir(join_paths('data', data_module), install_dir : lite_datadir) endforeach configure_file( input : 'data/core/start.lua', output : 'start.lua', configuration : conf_data, - install : true, - install_dir : lite_datadir / 'core', + install_dir : join_paths(lite_datadir, 'core'), ) if not get_option('source-only') - subdir('lib/dmon') subdir('src') subdir('scripts') endif diff --git a/meson_options.txt b/meson_options.txt index 1cf3e22f..7850416e 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -2,3 +2,4 @@ option('bundle', type : 'boolean', value : false, description: 'Build a macOS bu option('source-only', type : 'boolean', value : false, description: 'Configure source files only, doesn\'t checks for dependencies') option('portable', type : 'boolean', value : false, description: 'Portable install') option('renderer', type : 'boolean', value : false, description: 'Use SDL renderer') +option('dirmonitor_backend', type : 'combo', value : '', choices : ['', 'inotify', 'kqueue', 'win32', 'dummy'], description: 'define what dirmonitor backend to use') \ No newline at end of file diff --git a/resources/linux/org.lite_xl.lite_xl.appdata.xml b/resources/linux/org.lite_xl.lite_xl.appdata.xml index c5895178..1932af90 100644 --- a/resources/linux/org.lite_xl.lite_xl.appdata.xml +++ b/resources/linux/org.lite_xl.lite_xl.appdata.xml @@ -6,6 +6,7 @@ Lite XL A lightweight text editor written in Lua + org.lite_xl.lite_xl.desktop

@@ -17,11 +18,11 @@ The editor window - https://lite-xl.github.io/assets/img/screenshots/editor.png + https://lite-xl.com/assets/img/editor.png - https://lite-xl.github.io + https://lite-xl.com lite-xl diff --git a/resources/macos/Info.plist.in b/resources/macos/Info.plist.in index 4d715f2f..2cb6208e 100644 --- a/resources/macos/Info.plist.in +++ b/resources/macos/Info.plist.in @@ -8,6 +8,8 @@ lite-xl CFBundleIconFile icon.icns + CFBundleIdentifier + com.lite-xl CFBundleName Lite XL CFBundlePackageType diff --git a/resources/notes-dmon-integration.md b/resources/notes-dmon-integration.md deleted file mode 100644 index 5179df40..00000000 --- a/resources/notes-dmon-integration.md +++ /dev/null @@ -1,54 +0,0 @@ - -`core.set_project_dir`: - Reset project directories and set its directory. - It chdir into the directory, empty the `core.project_directories` and add - the given directory. - `core.add_project_directory`: - Add a new top-level directory to the project. - Also called from modules and commands outside core.init. - local function `scan_project_folder`: - Scan all files for a given top-level project directory. - Can emit a warning about file limit. - Called only from within core.init module. - -`core.scan_project_subdir`: (before was named `core.scan_project_folder`) - scan a single folder, without recursion. Used when too many files. - -Local function `scan_project_folder`: - Populate the project folder top directory. Done only once when the directory - is added to the project. - -`core.add_project_directory`: - Add a new top-level folder to the project. - -`core.set_project_dir`: - Set the initial project directory. - -`core.dir_rescan_add_job`: - Add a job to rescan after an elapsed time a project's subdirectory to fix for any - changes. - -Local function `rescan_project_subdir`: - Rescan a project's subdirectory, compare to the current version and patch the list if - a difference is found. - - -`core.project_scan_thread`: - Should disappear now that we use dmon. - - -`core.project_scan_topdir`: - New function to scan a top level project folder. - - -`config.project_scan_rate`: -`core.project_scan_thread_id`: -`core.reschedule_project_scan`: -`core.project_files_limit`: - A eliminer. - -`core.get_project_files`: - To be fixed. Use `find_project_files_co` for a single directory - -In TreeView remove usage of self.last to detect new scan that changed the files list. - diff --git a/resources/windows/001-lua-unicode.diff b/resources/windows/001-lua-unicode.diff new file mode 100644 index 00000000..8306d354 --- /dev/null +++ b/resources/windows/001-lua-unicode.diff @@ -0,0 +1,195 @@ +diff -ruN lua-5.4.3/meson.build newlua/meson.build +--- lua-5.4.3/meson.build 2022-05-29 21:04:17.850449500 +0800 ++++ newlua/meson.build 2022-06-10 19:23:55.685139800 +0800 +@@ -82,6 +82,7 @@ + 'src/lutf8lib.c', + 'src/lvm.c', + 'src/lzio.c', ++ 'src/utf8_wrappers.c', + dependencies: lua_lib_deps, + override_options: project_options, + implicit_include_directories: false, +Binary files lua-5.4.3/src/lua54.dll and newlua/src/lua54.dll differ +diff -ruN lua-5.4.3/src/luaconf.h newlua/src/luaconf.h +--- lua-5.4.3/src/luaconf.h 2021-03-15 21:32:52.000000000 +0800 ++++ newlua/src/luaconf.h 2022-06-10 19:15:03.014745300 +0800 +@@ -786,5 +786,15 @@ + + + ++#if defined(lua_c) || defined(luac_c) || (defined(LUA_LIB) && \ ++ (defined(lauxlib_c) || defined(liolib_c) || \ ++ defined(loadlib_c) || defined(loslib_c))) ++#include "utf8_wrappers.h" ++#endif ++ ++ ++ ++ ++ + #endif + +diff -ruN lua-5.4.3/src/Makefile newlua/src/Makefile +--- lua-5.4.3/src/Makefile 2021-02-10 02:47:17.000000000 +0800 ++++ newlua/src/Makefile 2022-06-10 19:22:45.267931400 +0800 +@@ -33,7 +33,7 @@ + PLATS= guess aix bsd c89 freebsd generic linux linux-readline macosx mingw posix solaris + + LUA_A= liblua.a +-CORE_O= lapi.o lcode.o lctype.o ldebug.o ldo.o ldump.o lfunc.o lgc.o llex.o lmem.o lobject.o lopcodes.o lparser.o lstate.o lstring.o ltable.o ltm.o lundump.o lvm.o lzio.o ++CORE_O= lapi.o lcode.o lctype.o ldebug.o ldo.o ldump.o lfunc.o lgc.o llex.o lmem.o lobject.o lopcodes.o lparser.o lstate.o lstring.o ltable.o ltm.o lundump.o lvm.o lzio.o utf8_wrappers.o + LIB_O= lauxlib.o lbaselib.o lcorolib.o ldblib.o liolib.o lmathlib.o loadlib.o loslib.o lstrlib.o ltablib.o lutf8lib.o linit.o + BASE_O= $(CORE_O) $(LIB_O) $(MYOBJS) + + +diff -ruN lua-5.4.3/src/utf8_wrappers.c newlua/src/utf8_wrappers.c +--- lua-5.4.3/src/utf8_wrappers.c 1970-01-01 07:30:00.000000000 +0730 ++++ newlua/src/utf8_wrappers.c 2022-06-10 19:13:11.904613300 +0800 +@@ -0,0 +1,101 @@ ++/** ++ * Wrappers to provide Unicode (UTF-8) support on Windows. ++ * ++ * Copyright (c) 2018 Peter Wu ++ * SPDX-License-Identifier: (GPL-2.0-or-later OR MIT) ++ */ ++ ++#ifdef _WIN32 ++#include /* for MultiByteToWideChar */ ++#include /* for _wrename */ ++#include ++#include ++#include ++ ++// Set a high limit in case long paths are enabled. ++#define MAX_PATH_SIZE 4096 ++#define MAX_MODE_SIZE 128 ++// cmd.exe argument length is reportedly limited to 8192. ++#define MAX_CMD_SIZE 8192 ++ ++FILE *fopen_utf8(const char *pathname, const char *mode) { ++ wchar_t pathname_w[MAX_PATH_SIZE]; ++ wchar_t mode_w[MAX_MODE_SIZE]; ++ if (!MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, pathname, -1, pathname_w, MAX_PATH_SIZE) || ++ !MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, mode, -1, mode_w, MAX_MODE_SIZE)) { ++ errno = EINVAL; ++ return NULL; ++ } ++ return _wfopen(pathname_w, mode_w); ++} ++ ++FILE *freopen_utf8(const char *pathname, const char *mode, FILE *stream) { ++ wchar_t pathname_w[MAX_PATH_SIZE]; ++ wchar_t mode_w[MAX_MODE_SIZE]; ++ if (!MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, pathname, -1, pathname_w, MAX_PATH_SIZE) || ++ !MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, mode, -1, mode_w, MAX_MODE_SIZE)) { ++ // Close stream as documented for the error case. ++ fclose(stream); ++ errno = EINVAL; ++ return NULL; ++ } ++ return _wfreopen(pathname_w, mode_w, stream); ++} ++ ++int remove_utf8(const char *pathname) { ++ wchar_t pathname_w[MAX_PATH_SIZE]; ++ if (!MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, pathname, -1, pathname_w, MAX_PATH_SIZE)) { ++ errno = EINVAL; ++ return -1; ++ } ++ return _wremove(pathname_w); ++} ++ ++int rename_utf8(const char *oldpath, const char *newpath) { ++ wchar_t oldpath_w[MAX_PATH_SIZE]; ++ wchar_t newpath_w[MAX_PATH_SIZE]; ++ if (!MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, oldpath, -1, oldpath_w, MAX_PATH_SIZE) || ++ !MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, newpath, -1, newpath_w, MAX_PATH_SIZE)) { ++ errno = EINVAL; ++ return -1; ++ } ++ return _wrename(oldpath_w, newpath_w); ++} ++ ++FILE *popen_utf8(const char *command, const char *mode) { ++ wchar_t command_w[MAX_CMD_SIZE]; ++ wchar_t mode_w[MAX_MODE_SIZE]; ++ if (!MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, command, -1, command_w, MAX_CMD_SIZE) || ++ !MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, mode, -1, mode_w, MAX_MODE_SIZE)) { ++ errno = EINVAL; ++ return NULL; ++ } ++ return _wpopen(command_w, mode_w); ++} ++ ++int system_utf8(const char *command) { ++ wchar_t command_w[MAX_CMD_SIZE]; ++ if (!MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, command, -1, command_w, MAX_CMD_SIZE)) { ++ errno = EINVAL; ++ return -1; ++ } ++ return _wsystem(command_w); ++} ++ ++DWORD GetModuleFileNameA_utf8(HMODULE hModule, LPSTR lpFilename, DWORD nSize) { ++ wchar_t filename_w[MAX_PATH + 1]; ++ if (!GetModuleFileNameW(hModule, filename_w, MAX_PATH + 1)) { ++ return 0; ++ } ++ return WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, filename_w, -1, lpFilename, nSize, NULL, NULL); ++} ++ ++HMODULE LoadLibraryExA_utf8(LPCSTR lpLibFileName, HANDLE hFile, DWORD dwFlags) { ++ wchar_t pathname_w[MAX_PATH_SIZE]; ++ if (!MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, lpLibFileName, -1, pathname_w, MAX_PATH_SIZE)) { ++ SetLastError(ERROR_INVALID_NAME); ++ return NULL; ++ } ++ return LoadLibraryExW(pathname_w, hFile, dwFlags); ++} ++#endif +diff -ruN lua-5.4.3/src/utf8_wrappers.h newlua/src/utf8_wrappers.h +--- lua-5.4.3/src/utf8_wrappers.h 1970-01-01 07:30:00.000000000 +0730 ++++ newlua/src/utf8_wrappers.h 2022-06-10 19:22:53.554879400 +0800 +@@ -0,0 +1,42 @@ ++/** ++ * Wrappers to provide Unicode (UTF-8) support on Windows. ++ * ++ * Copyright (c) 2018 Peter Wu ++ * SPDX-License-Identifier: (GPL-2.0-or-later OR MIT) ++ */ ++ ++#ifdef _WIN32 ++ ++#if defined(loadlib_c) || defined(lauxlib_c) || defined(liolib_c) || defined(luac_c) ++#include /* for loadlib_c */ ++FILE *fopen_utf8(const char *pathname, const char *mode); ++#define fopen fopen_utf8 ++#endif ++ ++#ifdef lauxlib_c ++FILE *freopen_utf8(const char *pathname, const char *mode, FILE *stream); ++#define freopen freopen_utf8 ++#endif ++ ++#ifdef liolib_c ++FILE *popen_utf8(const char *command, const char *mode); ++#define _popen popen_utf8 ++#endif ++ ++#ifdef loslib_c ++int remove_utf8(const char *pathname); ++int rename_utf8(const char *oldpath, const char *newpath); ++int system_utf8(const char *command); ++#define remove remove_utf8 ++#define rename rename_utf8 ++#define system system_utf8 ++#endif ++ ++#ifdef loadlib_c ++#include ++DWORD GetModuleFileNameA_utf8(HMODULE hModule, LPSTR lpFilename, DWORD nSize); ++HMODULE LoadLibraryExA_utf8(LPCSTR lpLibFileName, HANDLE hFile, DWORD dwFlags); ++#define GetModuleFileNameA GetModuleFileNameA_utf8 ++#define LoadLibraryExA LoadLibraryExA_utf8 ++#endif ++#endif diff --git a/scripts/appimage.sh b/scripts/appimage.sh index 8844fafe..03de616c 100644 --- a/scripts/appimage.sh +++ b/scripts/appimage.sh @@ -1,5 +1,5 @@ #!/bin/env bash -set -ex +set -e if [ ! -e "src/api/api.h" ]; then echo "Please run this script from the root directory of Lite XL." @@ -8,6 +8,13 @@ fi source scripts/common.sh +ARCH="$(uname -m)" +BUILD_DIR="$(get_default_build_dir)" +RUN_BUILD=true +STATIC_BUILD=false +ADDONS=false +BUILD_TYPE="debug" + show_help(){ echo echo "Usage: $0 " @@ -16,22 +23,21 @@ show_help(){ echo echo "-h --help Show this help and exits." echo "-b --builddir DIRNAME Sets the name of the build dir (no path)." - echo " Default: 'build'." + echo " Default: '${BUILD_DIR}'." + echo " --debug Debug this script." echo "-n --nobuild Skips the build step, use existing files." - echo "-s --static Specify if building using static libraries" - echo " by using lhelper tool." + echo "-s --static Specify if building using static libraries." echo "-v --version VERSION Specify a version, non whitespace separated string." + echo "-a --addons Install 3rd party addons." + echo "-r --release Compile in release mode." echo } -ARCH="$(uname -m)" -BUILD_DIR="$(get_default_build_dir)" -RUN_BUILD=true -STATIC_BUILD=false +initial_arg_count=$# for i in "$@"; do case $i in - -h|--belp) + -h|--help) show_help exit 0 ;; @@ -40,10 +46,22 @@ for i in "$@"; do shift shift ;; + -a|--addons) + ADDONS=true + shift + ;; + --debug) + set -x + shift + ;; -n|--nobuild) RUN_BUILD=false shift ;; + -r|--release) + BUILD_TYPE="release" + shift + ;; -s|--static) STATIC_BUILD=true shift @@ -59,25 +77,19 @@ for i in "$@"; do esac done -# TODO: Versioning using git -#if [[ -z $VERSION && -d .git ]]; then -# VERSION=$(git describe --tags --long | sed 's/^v//; s/\([^-]*-g\)/r\1/; s/-/./g') -#fi - -if [[ -n $1 ]]; then +# show help if no valid argument was found +if [ $initial_arg_count -eq $# ]; then show_help exit 1 fi setup_appimagetool() { - if ! which appimagetool > /dev/null ; then - if [ ! -e appimagetool ]; then - if ! wget -O appimagetool "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-${ARCH}.AppImage" ; then - echo "Could not download the appimagetool for the arch '${ARCH}'." - exit 1 - else - chmod 0755 appimagetool - fi + if [ ! -e appimagetool ]; then + if ! wget -O appimagetool "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-${ARCH}.AppImage" ; then + echo "Could not download the appimagetool for the arch '${ARCH}'." + exit 1 + else + chmod 0755 appimagetool fi fi } @@ -104,7 +116,14 @@ build_litexl() { echo "Build lite-xl..." sleep 1 - meson setup --buildtype=release --prefix /usr ${BUILD_DIR} + if [[ $STATIC_BUILD == false ]]; then + meson setup --buildtype=$BUILD_TYPE --prefix=/usr ${BUILD_DIR} + else + meson setup --wrap-mode=forcefallback \ + --buildtype=$BUILD_TYPE \ + --prefix=/usr \ + ${BUILD_DIR} + fi meson compile -C ${BUILD_DIR} } @@ -121,6 +140,11 @@ generate_appimage() { cp resources/icons/lite-xl.svg LiteXL.AppDir/ cp resources/linux/org.lite_xl.lite_xl.desktop LiteXL.AppDir/ + if [[ $ADDONS == true ]]; then + addons_download "${BUILD_DIR}" + addons_install "${BUILD_DIR}" "LiteXL.AppDir/usr/share/lite-xl" + fi + if [[ $STATIC_BUILD == false ]]; then echo "Copying libraries..." @@ -153,6 +177,10 @@ generate_appimage() { version="-$VERSION" fi + if [[ $ADDONS == true ]]; then + version="${version}-addons" + fi + ./appimagetool LiteXL.AppDir LiteXL${version}-${ARCH}.AppImage } diff --git a/scripts/build.sh b/scripts/build.sh index 75212468..9f8e6567 100644 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -22,19 +22,25 @@ show_help() { echo "-B --bundle Create an App bundle (macOS only)" echo "-P --portable Create a portable binary package." echo "-O --pgo Use profile guided optimizations (pgo)." + echo "-U --windows-lua-utf Use the UTF8 patch for Lua." echo " macOS: disabled when used with --bundle," echo " Windows: Implicit being the only option." + echo "-r --release Compile in release mode." echo } main() { local platform="$(get_platform_name)" local build_dir="$(get_default_build_dir)" + local build_type="debug" local prefix=/ local force_fallback local bundle local portable local pgo + local patch_lua + + local lua_subproject_path for i in "$@"; do case $i in @@ -76,6 +82,14 @@ main() { pgo="-Db_pgo=generate" shift ;; + -U|--windows-lua-utf) + patch_lua="true" + shift + ;; + -r|--release) + build_type="release" + shift + ;; *) # unknown option ;; @@ -95,7 +109,7 @@ main() { rm -rf "${build_dir}" CFLAGS=$CFLAGS LDFLAGS=$LDFLAGS meson setup \ - --buildtype=release \ + --buildtype=$build_type \ --prefix "$prefix" \ $force_fallback \ $bundle \ @@ -103,6 +117,11 @@ main() { $pgo \ "${build_dir}" + lua_subproject_path=$(echo subprojects/lua-*/) + if [[ $patch_lua == "true" ]] && [[ ! -z $force_fallback ]] && [[ -d $lua_subproject_path ]]; then + patch -d $lua_subproject_path -p1 --forward < resources/windows/001-lua-unicode.diff + fi + meson compile -C "${build_dir}" if [ ! -z ${pgo+x} ]; then diff --git a/scripts/common.sh b/scripts/common.sh index 2b49d362..f598e45c 100644 --- a/scripts/common.sh +++ b/scripts/common.sh @@ -2,6 +2,64 @@ set -e +addons_download() { + local build_dir="$1" + + if [[ -d "${build_dir}/third/data/colors" ]]; then + echo "Warning: found previous addons installation, skipping." + echo " addons path: ${build_dir}/third/data/colors" + return 0 + fi + + # Download third party color themes + curl --insecure \ + -L "https://github.com/lite-xl/lite-xl-colors/archive/master.zip" \ + -o "${build_dir}/lite-xl-colors.zip" + + mkdir -p "${build_dir}/third/data/colors" + unzip "${build_dir}/lite-xl-colors.zip" -d "${build_dir}" + mv "${build_dir}/lite-xl-colors-master/colors" "${build_dir}/third/data" + rm -rf "${build_dir}/lite-xl-colors-master" + + # Download widgets library + curl --insecure \ + -L "https://github.com/lite-xl/lite-xl-widgets/archive/master.zip" \ + -o "${build_dir}/lite-xl-widgets.zip" + + unzip "${build_dir}/lite-xl-widgets.zip" -d "${build_dir}" + mv "${build_dir}/lite-xl-widgets-master" "${build_dir}/third/data/widget" + + # Downlaod thirdparty plugins + curl --insecure \ + -L "https://github.com/lite-xl/lite-xl-plugins/archive/2.1.zip" \ + -o "${build_dir}/lite-xl-plugins.zip" + + unzip "${build_dir}/lite-xl-plugins.zip" -d "${build_dir}" + mv "${build_dir}/lite-xl-plugins-2.1/plugins" "${build_dir}/third/data" + rm -rf "${build_dir}/lite-xl-plugins-2.1" +} + +# Addons installation: some distributions forbid external downloads +# so make it as optional module. +addons_install() { + local build_dir="$1" + local data_dir="$2" + + for module_name in colors widget; do + cp -r "${build_dir}/third/data/$module_name" "${data_dir}" + done + + mkdir -p "${data_dir}/plugins" + + for plugin_name in settings open_ext; do + cp -r "${build_dir}/third/data/plugins/${plugin_name}.lua" \ + "${data_dir}/plugins/" + done + + cp "${build_dir}/third/data/plugins/"language_* \ + "${data_dir}/plugins/" +} + get_platform_name() { if [[ "$OSTYPE" == "msys" ]]; then echo "windows" @@ -14,9 +72,23 @@ get_platform_name() { fi } +get_platform_arch() { + platform=$(get_platform_name) + arch=$(uname -m) + if [[ $MSYSTEM != "" ]]; then + if [[ $MSYSTEM == "MINGW64" ]]; then + arch=x86_64 + else + arch=i686 + fi + fi + echo "$arch" +} + get_default_build_dir() { platform=$(get_platform_name) - echo "build-$platform-$(uname -m)" + arch=$(get_platform_arch) + echo "build-$platform-$arch" } if [[ $(get_platform_name) == "UNSUPPORTED-OS" ]]; then diff --git a/scripts/innosetup/innosetup.sh b/scripts/innosetup/innosetup.sh index 4384d13c..a37a2836 100644 --- a/scripts/innosetup/innosetup.sh +++ b/scripts/innosetup/innosetup.sh @@ -15,15 +15,29 @@ show_help() { echo echo "-b --builddir DIRNAME Sets the name of the build directory (not path)." echo " Default: '$(get_default_build_dir)'." + echo "-v --version VERSION Sets the version on the package name." + echo "-a --addons Tell the script we are packaging an install with addons." echo " --debug Debug this script." echo } main() { local build_dir=$(get_default_build_dir) + local addons=false local arch + local arch_file + local version + local output - if [[ $MSYSTEM == "MINGW64" ]]; then arch=x64; else arch=Win32; fi + if [[ $MSYSTEM == "MINGW64" ]]; then + arch=x64 + arch_file=x86_64 + else + arch=i686; + arch_file=i686 + fi + + initial_arg_count=$# for i in "$@"; do case $i in @@ -31,11 +45,20 @@ main() { show_help exit 0 ;; + -a|--addons) + addons=true + shift + ;; -b|--builddir) build_dir="$2" shift shift ;; + -v|--version) + if [[ -n $2 ]]; then version="-$2"; fi + shift + shift + ;; --debug) set -x shift @@ -46,19 +69,19 @@ main() { esac done - if [[ -n $1 ]]; then + # show help if no valid argument was found + if [ $initial_arg_count -eq $# ]; then show_help exit 1 fi - # Copy MinGW libraries dependencies. - # MSYS2 ldd command seems to be only 64bit, so use ntldd - # see https://github.com/msys2/MINGW-packages/issues/4164 - local mingwLibsDir="${build_dir}/mingwLibs$arch" - mkdir -p "$mingwLibsDir" - ntldd -R "${build_dir}/src/lite-xl.exe" | grep mingw | awk '{print $3}' | sed 's#\\#/#g' | xargs -I '{}' cp -v '{}' $mingwLibsDir + if [[ $addons == true ]]; then + version="${version}-addons" + fi - "/c/Program Files (x86)/Inno Setup 6/ISCC.exe" -dARCH=$arch "${build_dir}/scripts/innosetup.iss" + output="LiteXL${version}-${arch_file}-setup" + + "/c/Program Files (x86)/Inno Setup 6/ISCC.exe" -dARCH=$arch //F"${output}" "${build_dir}/scripts/innosetup.iss" pushd "${build_dir}/scripts"; mv LiteXL*.exe "./../../"; popd } diff --git a/scripts/install-dependencies.sh b/scripts/install-dependencies.sh index 2f9519b1..d817097d 100644 --- a/scripts/install-dependencies.sh +++ b/scripts/install-dependencies.sh @@ -48,7 +48,7 @@ main() { if [[ $lhelper == true ]]; then sudo apt-get install -qq ninja-build else - sudo apt-get install -qq ninja-build libsdl2-dev libfreetype6 + sudo apt-get install -qq libfuse2 ninja-build wayland-protocols libsdl2-dev libfreetype6 fi pip3 install meson elif [[ "$OSTYPE" == "darwin"* ]]; then @@ -63,10 +63,10 @@ main() { elif [[ "$OSTYPE" == "msys" ]]; then if [[ $lhelper == true ]]; then pacman --noconfirm -S \ - ${MINGW_PACKAGE_PREFIX}-{gcc,meson,ninja,ntldd,pkg-config} unzip + ${MINGW_PACKAGE_PREFIX}-{gcc,meson,ninja,ntldd,pkg-config,mesa} unzip else pacman --noconfirm -S \ - ${MINGW_PACKAGE_PREFIX}-{gcc,meson,ninja,ntldd,pkg-config,freetype,pcre2,SDL2} unzip + ${MINGW_PACKAGE_PREFIX}-{gcc,meson,ninja,ntldd,pkg-config,mesa,freetype,pcre2,SDL2} unzip fi fi } diff --git a/scripts/lhelper.sh b/scripts/lhelper.sh index af6ae158..8d7ccded 100644 --- a/scripts/lhelper.sh +++ b/scripts/lhelper.sh @@ -51,25 +51,23 @@ main() { pushd lhelper; bash install "${lhelper_prefix}"; popd if [[ "$OSTYPE" == "darwin"* ]]; then - CC=clang CXX=clang++ lhelper create lite-xl -n + CC=clang CXX=clang++ lhelper create build else - lhelper create lite-xl -n + lhelper create lite-xl build fi fi # Not using $(lhelper activate lite-xl) to support CI - source "$(lhelper env-source lite-xl)" - - lhelper install freetype2 - lhelper install sdl2 2.0.14-wait-event-timeout-1 - lhelper install pcre2 + source "$(lhelper env-source build)" # Help MSYS2 to find the SDL2 include and lib directories to avoid errors # during build and linking when using lhelper. - if [[ "$OSTYPE" == "msys" ]]; then - CFLAGS=-I${LHELPER_ENV_PREFIX}/include/SDL2 - LDFLAGS=-L${LHELPER_ENV_PREFIX}/lib - fi + # Francesco: not sure why this is needed. I have never observed the problem when + # building on window. + # if [[ "$OSTYPE" == "msys" ]]; then + # CFLAGS=-I${LHELPER_ENV_PREFIX}/include/SDL2 + # LDFLAGS=-L${LHELPER_ENV_PREFIX}/lib + # fi } -main +main "$@" diff --git a/scripts/package.sh b/scripts/package.sh index c90e5b7c..15dbd47f 100644 --- a/scripts/package.sh +++ b/scripts/package.sh @@ -20,44 +20,19 @@ show_help() { echo "-h --help Show this help and exit." echo "-p --prefix PREFIX Install directory prefix. Default: '/'." echo "-v --version VERSION Sets the version on the package name." - echo " --addons Install 3rd party addons (currently Lite XL colors)." + echo "-a --addons Install 3rd party addons." echo " --debug Debug this script." echo "-A --appimage Create an AppImage (Linux only)." echo "-B --binary Create a normal / portable package or macOS bundle," echo " depending on how the build was configured. (Default.)" echo "-D --dmg Create a DMG disk image with AppDMG (macOS only)." echo "-I --innosetup Create a InnoSetup package (Windows only)." + echo "-r --release Strip debugging symbols." echo "-S --source Create a source code package," echo " including subprojects dependencies." echo } -# Addons installation: some distributions forbid external downloads -# so make it as optional module. -install_addons() { - local build_dir="$1" - local data_dir="$2" - - if [[ -d "${build_dir}/third/data/colors" ]]; then - echo "Warning: found previous colors addons installation, skipping." - return 0 - fi - - # Copy third party color themes - curl --insecure \ - -L "https://github.com/lite-xl/lite-xl-colors/archive/master.zip" \ - -o "${build_dir}/lite-xl-colors.zip" - - mkdir -p "${build_dir}/third/data/colors" - unzip "${build_dir}/lite-xl-colors.zip" -d "${build_dir}" - mv "${build_dir}/lite-xl-colors-master/colors" "${build_dir}/third/data" - rm -rf "${build_dir}/lite-xl-colors-master" - - for module_name in colors; do - cp -r "${build_dir}/third/data/$module_name" "${data_dir}" - done -} - source_package() { local build_dir=build-src local package_name=$1 @@ -85,7 +60,7 @@ source_package() { } main() { - local arch="$(uname -m)" + local arch="$(get_platform_arch)" local platform="$(get_platform_name)" local build_dir="$(get_default_build_dir)" local dest_dir=lite-xl @@ -96,8 +71,12 @@ main() { local binary=false local dmg=false local innosetup=false + local release=false local source=false + # store the current flags to easily pass them to appimage script + local flags="$@" + for i in "$@"; do case $i in -b|--builddir) @@ -152,11 +131,15 @@ main() { fi shift ;; + -r|--release) + release=true + shift + ;; -S|--source) source=true shift ;; - --addons) + -a|--addons) addons=true shift ;; @@ -170,6 +153,10 @@ main() { esac done + if [[ $addons == true ]]; then + version="$version-addons" + fi + if [[ -n $1 ]]; then show_help; exit 1; fi # The source package doesn't require a previous build, @@ -190,6 +177,7 @@ main() { local data_dir="$(pwd)/${dest_dir}/data" local exe_file="$(pwd)/${dest_dir}/lite-xl" + local package_name=lite-xl$version-$platform-$arch local bundle=false local portable=false @@ -202,6 +190,14 @@ main() { if [[ $platform == "windows" ]]; then exe_file="${exe_file}.exe" stripcmd="strip --strip-all" + # Copy MinGW libraries dependencies. + # MSYS2 ldd command seems to be only 64bit, so use ntldd + # see https://github.com/msys2/MINGW-packages/issues/4164 + ntldd -R "${exe_file}" \ + | grep mingw \ + | awk '{print $3}' \ + | sed 's#\\#/#g' \ + | xargs -I '{}' cp -v '{}' "$(pwd)/${dest_dir}/" else # Windows archive is always portable package_name+="-portable" @@ -216,18 +212,21 @@ main() { rm -rf "Lite XL.app"; mv "${dest_dir}" "Lite XL.app" dest_dir="Lite XL.app" exe_file="$(pwd)/${dest_dir}/Contents/MacOS/lite-xl" + data_dir="$(pwd)/${dest_dir}/Contents/Resources" fi fi if [[ $bundle == false && $portable == false ]]; then - echo "Creating a compressed archive..." data_dir="$(pwd)/${dest_dir}/$prefix/share/lite-xl" exe_file="$(pwd)/${dest_dir}/$prefix/bin/lite-xl" fi mkdir -p "${data_dir}" - if [[ $addons == true ]]; then install_addons "${build_dir}" "${data_dir}"; fi + if [[ $addons == true ]]; then + addons_download "${build_dir}" + addons_install "${build_dir}" "${data_dir}" + fi # TODO: use --skip-subprojects when 0.58.0 will be available on supported # distributions to avoid subprojects' include and lib directories to be copied. @@ -238,8 +237,11 @@ main() { find . -type d -empty -delete popd - $stripcmd "${exe_file}" + if [[ $release == true ]]; then + $stripcmd "${exe_file}" + fi + echo "Creating a compressed archive ${package_name}" if [[ $binary == true ]]; then rm -f "${package_name}".tar.gz rm -f "${package_name}".zip @@ -251,9 +253,15 @@ main() { fi fi - if [[ $appimage == true ]]; then source scripts/appimage.sh; fi - if [[ $bundle == true && $dmg == true ]]; then source scripts/appdmg.sh "${package_name}"; fi - if [[ $innosetup == true ]]; then source scripts/innosetup/innosetup.sh -b "${build_dir}"; fi + if [[ $appimage == true ]]; then + source scripts/appimage.sh $flags --static + fi + if [[ $bundle == true && $dmg == true ]]; then + source scripts/appdmg.sh "${package_name}" + fi + if [[ $innosetup == true ]]; then + source scripts/innosetup/innosetup.sh $flags + fi } main "$@" diff --git a/src/api/api.c b/src/api/api.c index 4ef8e5d0..acf6eec2 100644 --- a/src/api/api.c +++ b/src/api/api.c @@ -1,19 +1,23 @@ #include "api.h" - int luaopen_system(lua_State *L); int luaopen_renderer(lua_State *L); int luaopen_regex(lua_State *L); -// int luaopen_process(lua_State *L); +int luaopen_process(lua_State *L); +int luaopen_dirmonitor(lua_State* L); +int luaopen_utf8extra(lua_State* L); static const luaL_Reg libs[] = { - { "system", luaopen_system }, - { "renderer", luaopen_renderer }, - { "regex", luaopen_regex }, - // { "process", luaopen_process }, + { "system", luaopen_system }, + { "renderer", luaopen_renderer }, + { "regex", luaopen_regex }, + { "process", luaopen_process }, + { "dirmonitor", luaopen_dirmonitor }, + { "utf8extra", luaopen_utf8extra }, { NULL, NULL } }; + void api_load_libs(lua_State *L) { for (int i = 0; libs[i].name; i++) luaL_requiref(L, libs[i].name, libs[i].func, 1); diff --git a/src/api/api.h b/src/api/api.h index e7bc57ea..c11fbdb3 100644 --- a/src/api/api.h +++ b/src/api/api.h @@ -7,6 +7,7 @@ #define API_TYPE_FONT "Font" #define API_TYPE_PROCESS "Process" +#define API_TYPE_DIRMONITOR "Dirmonitor" #define API_CONSTANT_DEFINE(L, idx, key, n) (lua_pushnumber(L, n), lua_setfield(L, idx - 1, key)) diff --git a/src/api/dirmonitor.c b/src/api/dirmonitor.c new file mode 100644 index 00000000..73dfb348 --- /dev/null +++ b/src/api/dirmonitor.c @@ -0,0 +1,130 @@ +#include "api.h" +#include +#include +#include +#include +#include +#include + +static unsigned int DIR_EVENT_TYPE = 0; + +struct dirmonitor { + SDL_Thread* thread; + SDL_mutex* mutex; + char buffer[64512]; + volatile int length; + struct dirmonitor_internal* internal; +}; + + +struct dirmonitor_internal* init_dirmonitor(); +void deinit_dirmonitor(struct dirmonitor_internal*); +int get_changes_dirmonitor(struct dirmonitor_internal*, char*, int); +int translate_changes_dirmonitor(struct dirmonitor_internal*, char*, int, int (*)(int, const char*, void*), void*); +int add_dirmonitor(struct dirmonitor_internal*, const char*); +void remove_dirmonitor(struct dirmonitor_internal*, int); + + +static int f_check_dir_callback(int watch_id, const char* path, void* L) { + lua_pushvalue(L, -1); + if (path) + lua_pushlstring(L, path, watch_id); + else + lua_pushnumber(L, watch_id); + lua_call(L, 1, 1); + int result = lua_toboolean(L, -1); + lua_pop(L, 1); + return !result; +} + + +static int dirmonitor_check_thread(void* data) { + struct dirmonitor* monitor = data; + while (monitor->length >= 0) { + if (monitor->length == 0) { + int result = get_changes_dirmonitor(monitor->internal, monitor->buffer, sizeof(monitor->buffer)); + SDL_LockMutex(monitor->mutex); + if (monitor->length == 0) + monitor->length = result; + SDL_UnlockMutex(monitor->mutex); + } + SDL_Delay(1); + SDL_Event event = { .type = DIR_EVENT_TYPE }; + SDL_PushEvent(&event); + } + return 0; +} + + +static int f_dirmonitor_new(lua_State* L) { + if (DIR_EVENT_TYPE == 0) + DIR_EVENT_TYPE = SDL_RegisterEvents(1); + struct dirmonitor* monitor = lua_newuserdata(L, sizeof(struct dirmonitor)); + luaL_setmetatable(L, API_TYPE_DIRMONITOR); + memset(monitor, 0, sizeof(struct dirmonitor)); + monitor->internal = init_dirmonitor(); + return 1; +} + + +static int f_dirmonitor_gc(lua_State* L) { + struct dirmonitor* monitor = luaL_checkudata(L, 1, API_TYPE_DIRMONITOR); + SDL_LockMutex(monitor->mutex); + monitor->length = -1; + deinit_dirmonitor(monitor->internal); + SDL_UnlockMutex(monitor->mutex); + SDL_WaitThread(monitor->thread, NULL); + free(monitor->internal); + SDL_DestroyMutex(monitor->mutex); + return 0; +} + + +static int f_dirmonitor_watch(lua_State *L) { + struct dirmonitor* monitor = luaL_checkudata(L, 1, API_TYPE_DIRMONITOR); + lua_pushnumber(L, add_dirmonitor(monitor->internal, luaL_checkstring(L, 2))); + if (!monitor->thread) + monitor->thread = SDL_CreateThread(dirmonitor_check_thread, "dirmonitor_check_thread", monitor); + return 1; +} + + +static int f_dirmonitor_unwatch(lua_State *L) { + remove_dirmonitor(((struct dirmonitor*)luaL_checkudata(L, 1, API_TYPE_DIRMONITOR))->internal, lua_tonumber(L, 2)); + return 0; +} + + +static int f_dirmonitor_check(lua_State* L) { + struct dirmonitor* monitor = luaL_checkudata(L, 1, API_TYPE_DIRMONITOR); + SDL_LockMutex(monitor->mutex); + if (monitor->length < 0) + lua_pushnil(L); + else if (monitor->length > 0) { + if (translate_changes_dirmonitor(monitor->internal, monitor->buffer, monitor->length, f_check_dir_callback, L) == 0) + monitor->length = 0; + lua_pushboolean(L, 1); + } else + lua_pushboolean(L, 0); + SDL_UnlockMutex(monitor->mutex); + return 1; +} + + +static const luaL_Reg dirmonitor_lib[] = { + { "new", f_dirmonitor_new }, + { "__gc", f_dirmonitor_gc }, + { "watch", f_dirmonitor_watch }, + { "unwatch", f_dirmonitor_unwatch }, + { "check", f_dirmonitor_check }, + {NULL, NULL} +}; + + +int luaopen_dirmonitor(lua_State* L) { + luaL_newmetatable(L, API_TYPE_DIRMONITOR); + luaL_setfuncs(L, dirmonitor_lib, 0); + lua_pushvalue(L, -1); + lua_setfield(L, -2, "__index"); + return 1; +} diff --git a/src/api/dirmonitor/dummy.c b/src/api/dirmonitor/dummy.c new file mode 100644 index 00000000..62b1e624 --- /dev/null +++ b/src/api/dirmonitor/dummy.c @@ -0,0 +1,8 @@ +#include + +struct dirmonitor_internal* init_dirmonitor() { return NULL; } +void deinit_dirmonitor(struct dirmonitor_internal* monitor) { } +int get_changes_dirmonitor(struct dirmonitor_internal* monitor, char* buffer, size_t len) { return -1; } +int translate_changes_dirmonitor(struct dirmonitor_internal* monitor, char* buffer, int size, int (*callback)(int, const char*, void*), void* data) { return -1; } +int add_dirmonitor(struct dirmonitor_internal* monitor, const char* path) { return -1; } +void remove_dirmonitor(struct dirmonitor_internal* monitor, int fd) { } \ No newline at end of file diff --git a/src/api/dirmonitor/inotify.c b/src/api/dirmonitor/inotify.c new file mode 100644 index 00000000..697e1815 --- /dev/null +++ b/src/api/dirmonitor/inotify.c @@ -0,0 +1,53 @@ +#include +#include +#include +#include +#include + + +struct dirmonitor_internal { + int fd; + // a pipe is used to wake the thread in case of exit + int sig[2]; +}; + + +struct dirmonitor_internal* init_dirmonitor() { + struct dirmonitor_internal* monitor = calloc(sizeof(struct dirmonitor_internal), 1); + monitor->fd = inotify_init(); + pipe(monitor->sig); + fcntl(monitor->sig[0], F_SETFD, FD_CLOEXEC); + fcntl(monitor->sig[1], F_SETFD, FD_CLOEXEC); + return monitor; +} + + +void deinit_dirmonitor(struct dirmonitor_internal* monitor) { + close(monitor->fd); + close(monitor->sig[0]); + close(monitor->sig[1]); +} + + +int get_changes_dirmonitor(struct dirmonitor_internal* monitor, char* buffer, int length) { + struct pollfd fds[2] = { { .fd = monitor->fd, .events = POLLIN | POLLERR, .revents = 0 }, { .fd = monitor->sig[0], .events = POLLIN | POLLERR, .revents = 0 } }; + poll(fds, 2, -1); + return read(monitor->fd, buffer, length); +} + + +int translate_changes_dirmonitor(struct dirmonitor_internal* monitor, char* buffer, int length, int (*change_callback)(int, const char*, void*), void* data) { + for (struct inotify_event* info = (struct inotify_event*)buffer; (char*)info < buffer + length; info = (struct inotify_event*)((char*)info + sizeof(struct inotify_event))) + change_callback(info->wd, NULL, data); + return 0; +} + + +int add_dirmonitor(struct dirmonitor_internal* monitor, const char* path) { + return inotify_add_watch(monitor->fd, path, IN_CREATE | IN_DELETE | IN_MOVED_FROM | IN_MODIFY | IN_MOVED_TO); +} + + +void remove_dirmonitor(struct dirmonitor_internal* monitor, int fd) { + inotify_rm_watch(monitor->fd, fd); +} diff --git a/src/api/dirmonitor/kqueue.c b/src/api/dirmonitor/kqueue.c new file mode 100644 index 00000000..7c6e89d8 --- /dev/null +++ b/src/api/dirmonitor/kqueue.c @@ -0,0 +1,55 @@ +#include +#include +#include +#include +#include +#include + +struct dirmonitor_internal { + int fd; +}; + + +struct dirmonitor_internal* init_dirmonitor() { + struct dirmonitor_internal* monitor = calloc(sizeof(struct dirmonitor_internal), 1); + monitor->fd = kqueue(); + return monitor; +} + + +void deinit_dirmonitor(struct dirmonitor_internal* monitor) { + close(monitor->fd); +} + + +int get_changes_dirmonitor(struct dirmonitor_internal* monitor, char* buffer, int buffer_size) { + int nev = kevent(monitor->fd, NULL, 0, (struct kevent*)buffer, buffer_size / sizeof(kevent), NULL); + if (nev == -1) + return -1; + if (nev <= 0) + return 0; + return nev * sizeof(struct kevent); +} + + +int translate_changes_dirmonitor(struct dirmonitor_internal* monitor, char* buffer, int buffer_size, int (*change_callback)(int, const char*, void*), void* data) { + for (struct kevent* info = (struct kevent*)buffer; (char*)info < buffer + buffer_size; info = (struct kevent*)(((char*)info) + sizeof(kevent))) + change_callback(info->ident, NULL, data); + return 0; +} + + +int add_dirmonitor(struct dirmonitor_internal* monitor, const char* path) { + int fd = open(path, O_RDONLY); + struct kevent change; + + EV_SET(&change, fd, EVFILT_VNODE, EV_ADD | EV_CLEAR, NOTE_DELETE | NOTE_EXTEND | NOTE_WRITE | NOTE_ATTRIB | NOTE_LINK | NOTE_RENAME, 0, (void*)path); + kevent(monitor->fd, &change, 1, NULL, 0, NULL); + + return fd; +} + + +void remove_dirmonitor(struct dirmonitor_internal* monitor, int fd) { + close(fd); +} diff --git a/src/api/dirmonitor/win32.c b/src/api/dirmonitor/win32.c new file mode 100644 index 00000000..5483584f --- /dev/null +++ b/src/api/dirmonitor/win32.c @@ -0,0 +1,62 @@ +#include + + +struct dirmonitor_internal { + HANDLE handle; +}; + + +int get_changes_dirmonitor(struct dirmonitor_internal* monitor, char* buffer, int buffer_size) { + HANDLE handle = monitor->handle; + if (handle && handle != INVALID_HANDLE_VALUE) { + DWORD bytes_transferred; + if (ReadDirectoryChangesW(handle, buffer, buffer_size, TRUE, FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME, &bytes_transferred, NULL, NULL) == 0) + return 0; + return bytes_transferred; + } + return 0; +} + + +struct dirmonitor* init_dirmonitor() { + return calloc(sizeof(struct dirmonitor_internal), 1); +} + + +static void close_monitor_handle(struct dirmonitor_internal* monitor) { + if (monitor->handle && monitor->handle != INVALID_HANDLE_VALUE) { + HANDLE handle = monitor->handle; + monitor->handle = NULL; + CancelIoEx(handle, NULL); + CloseHandle(handle); + } +} + + +void deinit_dirmonitor(struct dirmonitor_internal* monitor) { + close_monitor_handle(monitor); +} + + +int translate_changes_dirmonitor(struct dirmonitor_internal* monitor, char* buffer, int buffer_size, int (*change_callback)(int, const char*, void*), void* data) { + for (FILE_NOTIFY_INFORMATION* info = (FILE_NOTIFY_INFORMATION*)buffer; (char*)info < buffer + buffer_size; info = (FILE_NOTIFY_INFORMATION*)(((char*)info) + info->NextEntryOffset)) { + char transform_buffer[PATH_MAX*4]; + int count = WideCharToMultiByte(CP_UTF8, 0, (WCHAR*)info->FileName, info->FileNameLength, transform_buffer, PATH_MAX*4 - 1, NULL, NULL); + change_callback(count, transform_buffer, data); + if (!info->NextEntryOffset) + break; + } + return 0; +} + + +int add_dirmonitor(struct dirmonitor_internal* monitor, const char* path) { + close_monitor_handle(monitor); + monitor->handle = CreateFileA(path, FILE_LIST_DIRECTORY, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL); + return !monitor->handle || monitor->handle == INVALID_HANDLE_VALUE ? -1 : 1; +} + + +void remove_dirmonitor(struct dirmonitor_internal* monitor, int fd) { + close_monitor_handle(monitor); +} diff --git a/src/api/process.c b/src/api/process.c index b961ca51..d713a84b 100644 --- a/src/api/process.c +++ b/src/api/process.c @@ -2,8 +2,10 @@ #include #include +#include #include #include +#include #if _WIN32 // https://stackoverflow.com/questions/60645/overlapped-i-o-on-anonymous-pipe @@ -20,19 +22,23 @@ #define READ_BUF_SIZE 2048 +#if _WIN32 +typedef HANDLE process_handle; +#else +typedef int process_handle; +#endif + typedef struct { bool running; int returncode, deadline; long pid; #if _WIN32 PROCESS_INFORMATION process_information; - HANDLE child_pipes[3][2]; OVERLAPPED overlapped[2]; bool reading[2]; char buffer[2][READ_BUF_SIZE]; - #else - int child_pipes[3][2]; #endif + process_handle child_pipes[3][2]; } process_t; typedef enum { @@ -59,17 +65,18 @@ typedef enum { #ifdef _WIN32 static volatile long PipeSerialNumber; - static void close_fd(HANDLE handle) { CloseHandle(handle); } + static void close_fd(HANDLE* handle) { if (*handle) CloseHandle(*handle); *handle = NULL; } #else - static void close_fd(int fd) { close(fd); } + static void close_fd(int* fd) { if (*fd) close(*fd); *fd = 0; } #endif static bool poll_process(process_t* proc, int timeout) { if (!proc->running) return false; - unsigned int ticks = SDL_GetTicks(); + uint32_t ticks = SDL_GetTicks(); if (timeout == WAIT_DEADLINE) timeout = proc->deadline; + do { #ifdef _WIN32 DWORD exit_code = -1; @@ -89,14 +96,9 @@ static bool poll_process(process_t* proc, int timeout) { #endif if (timeout) SDL_Delay(5); - } while (timeout == WAIT_INFINITE || SDL_GetTicks() - ticks < timeout); - if (!proc->running) { - close_fd(proc->child_pipes[STDIN_FD ][1]); - close_fd(proc->child_pipes[STDOUT_FD][0]); - close_fd(proc->child_pipes[STDERR_FD][0]); - return false; - } - return true; + } while (timeout == WAIT_INFINITE || (int)SDL_GetTicks() - ticks < timeout); + + return proc->running; } static bool signal_process(process_t* proc, signal_e sig) { @@ -109,9 +111,9 @@ static bool signal_process(process_t* proc, signal_e sig) { } #else switch (sig) { - case SIGNAL_TERM: terminate = kill(proc->pid, SIGTERM) == 1; break; - case SIGNAL_KILL: terminate = kill(proc->pid, SIGKILL) == 1; break; - case SIGNAL_INTERRUPT: kill(proc->pid, SIGINT); break; + case SIGNAL_TERM: terminate = kill(-proc->pid, SIGTERM) == 1; break; + case SIGNAL_KILL: terminate = kill(-proc->pid, SIGKILL) == 1; break; + case SIGNAL_INTERRUPT: kill(-proc->pid, SIGINT); break; } #endif if (terminate) @@ -120,24 +122,29 @@ static bool signal_process(process_t* proc, signal_e sig) { } static int process_start(lua_State* L) { + int retval = 1; size_t env_len = 0, key_len, val_len; - const char *cmd[256], *env[256] = { NULL }, *cwd = NULL; + const char *cmd[256] = { NULL }, *env_names[256] = { NULL }, *env_values[256] = { NULL }, *cwd = NULL; bool detach = false; int deadline = 10, new_fds[3] = { STDIN_FD, STDOUT_FD, STDERR_FD }; luaL_checktype(L, 1, LUA_TTABLE); #if LUA_VERSION_NUM > 501 lua_len(L, 1); #else - lua_pushnumber(L, (int)lua_objlen(L, 1)); + lua_pushinteger(L, (int)lua_objlen(L, 1)); #endif size_t cmd_len = luaL_checknumber(L, -1); lua_pop(L, 1); size_t arg_len = lua_gettop(L); for (size_t i = 1; i <= cmd_len; ++i) { - lua_pushnumber(L, i); + lua_pushinteger(L, i); lua_rawget(L, 1); cmd[i-1] = luaL_checkstring(L, -1); } - cmd[cmd_len] = NULL; + + // this should never trip + // but if it does we are in deep trouble + assert(cmd[0]); + if (arg_len > 1) { lua_getfield(L, 2, "env"); if (!lua_isnil(L, -1)) { @@ -145,9 +152,12 @@ static int process_start(lua_State* L) { while (lua_next(L, -2) != 0) { const char* key = luaL_checklstring(L, -2, &key_len); const char* val = luaL_checklstring(L, -1, &val_len); - env[env_len] = malloc(key_len+val_len+2); - snprintf((char*)env[env_len++], key_len+val_len+2, "%s=%s", key, val); + env_names[env_len] = malloc(key_len+1); + strcpy((char*)env_names[env_len], key); + env_values[env_len] = malloc(val_len+1); + strcpy((char*)env_values[env_len], val); lua_pop(L, 1); + ++env_len; } } else lua_pop(L, 1); @@ -158,11 +168,16 @@ static int process_start(lua_State* L) { lua_getfield(L, 2, "stdout"); new_fds[STDOUT_FD] = luaL_optnumber(L, -1, STDOUT_FD); lua_getfield(L, 2, "stderr"); new_fds[STDERR_FD] = luaL_optnumber(L, -1, STDERR_FD); for (int stream = STDIN_FD; stream <= STDERR_FD; ++stream) { - if (new_fds[stream] > STDERR_FD || new_fds[stream] < REDIRECT_PARENT) - return luaL_error(L, "redirect to handles, FILE* and paths are not supported"); + if (new_fds[stream] > STDERR_FD || new_fds[stream] < REDIRECT_PARENT) { + for (size_t i = 0; i < env_len; ++i) { + free((char*)env_names[i]); + free((char*)env_values[i]); + } + retval = luaL_error(L, "redirect to handles, FILE* and paths are not supported"); + goto cleanup; + } } } - env[env_len] = NULL; process_t* self = lua_newuserdata(L, sizeof(process_t)); memset(self, 0, sizeof(process_t)); @@ -189,16 +204,21 @@ static int process_start(lua_State* L) { sprintf(pipeNameBuffer, "\\\\.\\Pipe\\RemoteExeAnon.%08lx.%08lx", GetCurrentProcessId(), InterlockedIncrement(&PipeSerialNumber)); self->child_pipes[i][0] = CreateNamedPipeA(pipeNameBuffer, PIPE_ACCESS_INBOUND | FILE_FLAG_OVERLAPPED, PIPE_TYPE_BYTE | PIPE_WAIT, 1, READ_BUF_SIZE, READ_BUF_SIZE, 0, NULL); - if (self->child_pipes[i][0] == INVALID_HANDLE_VALUE) - return luaL_error(L, "Error creating read pipe: %d.", GetLastError()); + if (self->child_pipes[i][0] == INVALID_HANDLE_VALUE) { + retval = luaL_error(L, "Error creating read pipe: %d.", GetLastError()); + goto cleanup; + } self->child_pipes[i][1] = CreateFileA(pipeNameBuffer, GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (self->child_pipes[i][1] == INVALID_HANDLE_VALUE) { CloseHandle(self->child_pipes[i][0]); - return luaL_error(L, "Error creating write pipe: %d.", GetLastError()); + retval = luaL_error(L, "Error creating write pipe: %d.", GetLastError()); + goto cleanup; } if (!SetHandleInformation(self->child_pipes[i][i == STDIN_FD ? 1 : 0], HANDLE_FLAG_INHERIT, 0) || - !SetHandleInformation(self->child_pipes[i][i == STDIN_FD ? 0 : 1], HANDLE_FLAG_INHERIT, 1)) - return luaL_error(L, "Error inheriting pipes: %d.", GetLastError()); + !SetHandleInformation(self->child_pipes[i][i == STDIN_FD ? 0 : 1], HANDLE_FLAG_INHERIT, 1)) { + retval = luaL_error(L, "Error inheriting pipes: %d.", GetLastError()); + goto cleanup; + } } } break; } @@ -217,44 +237,48 @@ static int process_start(lua_State* L) { siStartInfo.hStdInput = self->child_pipes[STDIN_FD][0]; siStartInfo.hStdOutput = self->child_pipes[STDOUT_FD][1]; siStartInfo.hStdError = self->child_pipes[STDERR_FD][1]; - char commandLine[32767] = { 0 }, environmentBlock[32767]; - int offset = 0; + char commandLine[32767] = { 0 }, environmentBlock[32767], wideEnvironmentBlock[32767*2]; strcpy(commandLine, cmd[0]); + int offset = 0; for (size_t i = 1; i < cmd_len; ++i) { size_t len = strlen(cmd[i]); - if (offset + len + 1 >= sizeof(commandLine)) + offset += len + 1; + if (offset >= sizeof(commandLine)) break; strcat(commandLine, " "); strcat(commandLine, cmd[i]); } + offset = 0; for (size_t i = 0; i < env_len; ++i) { - size_t len = strlen(env[i]); - if (offset + len >= sizeof(environmentBlock)) + if (offset + strlen(env_values[i]) + strlen(env_names[i]) + 1 >= sizeof(environmentBlock)) break; - memcpy(&environmentBlock[offset], env[i], len); - offset += len; + offset += snprintf(&environmentBlock[offset], sizeof(environmentBlock) - offset, "%s=%s", env_names[i], env_values[i]); environmentBlock[offset++] = 0; } environmentBlock[offset++] = 0; - if (!CreateProcess(NULL, commandLine, NULL, NULL, true, (detach ? DETACHED_PROCESS : CREATE_NO_WINDOW) | CREATE_UNICODE_ENVIRONMENT, env_len > 0 ? environmentBlock : NULL, cwd, &siStartInfo, &self->process_information)) - return luaL_error(L, "Error creating a process: %d.", GetLastError()); + if (env_len > 0) + MultiByteToWideChar(CP_UTF8, MB_PRECOMPOSED, environmentBlock, offset, (LPWSTR)wideEnvironmentBlock, sizeof(wideEnvironmentBlock)); + if (!CreateProcess(NULL, commandLine, NULL, NULL, true, (detach ? DETACHED_PROCESS : CREATE_NO_WINDOW) | CREATE_UNICODE_ENVIRONMENT, env_len > 0 ? wideEnvironmentBlock : NULL, cwd, &siStartInfo, &self->process_information)) { + retval = luaL_error(L, "Error creating a process: %d.", GetLastError()); + goto cleanup; + } self->pid = (long)self->process_information.dwProcessId; if (detach) CloseHandle(self->process_information.hProcess); CloseHandle(self->process_information.hThread); #else for (int i = 0; i < 3; ++i) { // Make only the parents fd's non-blocking. Children should block. - if (pipe(self->child_pipes[i]) || fcntl(self->child_pipes[i][i == STDIN_FD ? 1 : 0], F_SETFL, O_NONBLOCK) == -1) - return luaL_error(L, "Error creating pipes: %s", strerror(errno)); + if (pipe(self->child_pipes[i]) || fcntl(self->child_pipes[i][i == STDIN_FD ? 1 : 0], F_SETFL, O_NONBLOCK) == -1) { + retval = luaL_error(L, "Error creating pipes: %s", strerror(errno)); + goto cleanup; + } } self->pid = (long)fork(); if (self->pid < 0) { - for (int i = 0; i < 3; ++i) { - close(self->child_pipes[i][0]); - close(self->child_pipes[i][1]); - } - return luaL_error(L, "Error running fork: %s.", strerror(errno)); + retval = luaL_error(L, "Error running fork: %s.", strerror(errno)); + goto cleanup; } else if (!self->pid) { + setpgid(0,0); for (int stream = 0; stream < 3; ++stream) { if (new_fds[stream] == REDIRECT_DISCARD) { // Close the stream if we don't want it. close(self->child_pipes[stream][stream == STDIN_FD ? 0 : 1]); @@ -263,19 +287,28 @@ static int process_start(lua_State* L) { dup2(self->child_pipes[new_fds[stream]][new_fds[stream] == STDIN_FD ? 0 : 1], stream); close(self->child_pipes[stream][stream == STDIN_FD ? 1 : 0]); } - if ((!detach || setsid() != -1) && (!cwd || chdir(cwd) != -1)) - execvp((const char*)cmd[0], (char* const*)cmd); + size_t set; + for (set = 0; set < env_len && setenv(env_names[set], env_values[set], 1) == 0; ++set); + if (set == env_len && (!detach || setsid() != -1) && (!cwd || chdir(cwd) != -1)) + execvp(cmd[0], (char** const)cmd); const char* msg = strerror(errno); - int result = write(STDERR_FD, msg, strlen(msg)+1); - exit(result == strlen(msg)+1 ? -1 : -2); + size_t result = write(STDERR_FD, msg, strlen(msg)+1); + _exit(result == strlen(msg)+1 ? -1 : -2); } #endif - for (size_t i = 0; i < env_len; ++i) - free((char*)env[i]); - for (int stream = 0; stream < 3; ++stream) - close_fd(self->child_pipes[stream][stream == STDIN_FD ? 0 : 1]); + cleanup: + for (size_t i = 0; i < env_len; ++i) { + free((char*)env_names[i]); + free((char*)env_values[i]); + } + for (int stream = 0; stream < 3; ++stream) { + process_handle* pipe = &self->child_pipes[stream][stream == STDIN_FD ? 0 : 1]; + if (*pipe) { + close_fd(pipe); + } + } self->running = true; - return 1; + return retval; } static int g_read(lua_State* L, int stream, unsigned long read_size) { @@ -306,7 +339,7 @@ static int g_read(lua_State* L, int stream, unsigned long read_size) { #else luaL_Buffer b; luaL_buffinit(L, &b); - uint8_t* buffer = (uint8_t*)luaL_prepbuffer(&b); + uint8_t* buffer = (uint8_t*)luaL_prepbuffsize(&b, READ_BUF_SIZE); length = read(self->child_pipes[stream][0], buffer, read_size > READ_BUF_SIZE ? READ_BUF_SIZE : read_size); if (length == 0 && !poll_process(self, WAIT_NONE)) return 0; @@ -330,8 +363,9 @@ static int f_write(lua_State* L) { #if _WIN32 DWORD dwWritten; if (!WriteFile(self->child_pipes[STDIN_FD][1], data, data_size, &dwWritten, NULL)) { + int lastError = GetLastError(); signal_process(self, SIGNAL_TERM); - return luaL_error(L, "error writing to process: %d", GetLastError()); + return luaL_error(L, "error writing to process: %d", lastError); } length = dwWritten; #else @@ -339,18 +373,19 @@ static int f_write(lua_State* L) { if (length < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) length = 0; else if (length < 0) { + const char* lastError = strerror(errno); signal_process(self, SIGNAL_TERM); - return luaL_error(L, "error writing to process: %s", strerror(errno)); + return luaL_error(L, "error writing to process: %s", lastError); } #endif - lua_pushnumber(L, length); + lua_pushinteger(L, length); return 1; } static int f_close_stream(lua_State* L) { process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS); int stream = luaL_checknumber(L, 2); - close_fd(self->child_pipes[stream][stream == STDIN_FD ? 1 : 0]); + close_fd(&self->child_pipes[stream][stream == STDIN_FD ? 1 : 0]); lua_pushboolean(L, 1); return 1; } @@ -375,7 +410,7 @@ static int f_tostring(lua_State* L) { static int f_pid(lua_State* L) { process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS); - lua_pushnumber(L, self->pid); + lua_pushinteger(L, self->pid); return 1; } @@ -383,7 +418,7 @@ static int f_returncode(lua_State *L) { process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS); if (self->running) return 0; - lua_pushnumber(L, self->returncode); + lua_pushinteger(L, self->returncode); return 1; } @@ -404,7 +439,7 @@ static int f_wait(lua_State* L) { int timeout = luaL_optnumber(L, 2, 0); if (poll_process(self, timeout)) return 0; - lua_pushnumber(L, self->returncode); + lua_pushinteger(L, self->returncode); return 1; } @@ -417,11 +452,19 @@ static int self_signal(lua_State* L, signal_e sig) { static int f_terminate(lua_State* L) { return self_signal(L, SIGNAL_TERM); } static int f_kill(lua_State* L) { return self_signal(L, SIGNAL_KILL); } static int f_interrupt(lua_State* L) { return self_signal(L, SIGNAL_INTERRUPT); } -static int f_gc(lua_State* L) { return self_signal(L, SIGNAL_TERM); } +static int f_gc(lua_State* L) { + process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS); + signal_process(self, SIGNAL_TERM); + close_fd(&self->child_pipes[STDIN_FD ][1]); + close_fd(&self->child_pipes[STDOUT_FD][0]); + close_fd(&self->child_pipes[STDERR_FD][0]); + poll_process(self, 10); + return 0; +} static int f_running(lua_State* L) { - process_t* self = (process_t*)luaL_checkudata(L, 1, API_TYPE_PROCESS); - lua_pushboolean(L, self->running); + process_t* self = (process_t*)luaL_checkudata(L, 1, API_TYPE_PROCESS); + lua_pushboolean(L, poll_process(self, WAIT_NONE)); return 1; } diff --git a/src/api/regex.c b/src/api/regex.c index 9f6bd3ee..d23eaf71 100644 --- a/src/api/regex.c +++ b/src/api/regex.c @@ -60,12 +60,14 @@ static int f_pcre_match(lua_State *L) { const char* str = luaL_checklstring(L, 2, &len); if (lua_gettop(L) > 2) offset = luaL_checknumber(L, 3); + offset -= 1; + len -= offset; if (lua_gettop(L) > 3) opts = luaL_checknumber(L, 4); lua_rawgeti(L, 1, 1); pcre2_code* re = (pcre2_code*)lua_touserdata(L, -1); pcre2_match_data* md = pcre2_match_data_create_from_pattern(re, NULL); - int rc = pcre2_match(re, (PCRE2_SPTR)str, len, offset - 1, opts, md, NULL); + int rc = pcre2_match(re, (PCRE2_SPTR)&str[offset], len, 0, opts, md, NULL); if (rc < 0) { pcre2_match_data_free(md); if (rc != PCRE2_ERROR_NOMATCH) { @@ -86,7 +88,7 @@ static int f_pcre_match(lua_State *L) { return 0; } for (int i = 0; i < rc*2; i++) - lua_pushnumber(L, ovector[i]+1); + lua_pushinteger(L, ovector[i]+offset+1); pcre2_match_data_free(md); return rc*2; } @@ -104,17 +106,17 @@ int luaopen_regex(lua_State *L) { lua_setfield(L, -2, "__name"); lua_pushvalue(L, -1); lua_setfield(L, LUA_REGISTRYINDEX, "regex"); - lua_pushnumber(L, PCRE2_ANCHORED); + lua_pushinteger(L, PCRE2_ANCHORED); lua_setfield(L, -2, "ANCHORED"); - lua_pushnumber(L, PCRE2_ANCHORED) ; + lua_pushinteger(L, PCRE2_ANCHORED) ; lua_setfield(L, -2, "ENDANCHORED"); - lua_pushnumber(L, PCRE2_NOTBOL); + lua_pushinteger(L, PCRE2_NOTBOL); lua_setfield(L, -2, "NOTBOL"); - lua_pushnumber(L, PCRE2_NOTEOL); + lua_pushinteger(L, PCRE2_NOTEOL); lua_setfield(L, -2, "NOTEOL"); - lua_pushnumber(L, PCRE2_NOTEMPTY); + lua_pushinteger(L, PCRE2_NOTEMPTY); lua_setfield(L, -2, "NOTEMPTY"); - lua_pushnumber(L, PCRE2_NOTEMPTY_ATSTART); + lua_pushinteger(L, PCRE2_NOTEMPTY_ATSTART); lua_setfield(L, -2, "NOTEMPTY_ATSTART"); return 1; } diff --git a/src/api/renderer.c b/src/api/renderer.c index fa8316c5..d9ab83f6 100644 --- a/src/api/renderer.c +++ b/src/api/renderer.c @@ -1,56 +1,92 @@ +#include #include "api.h" #include "renderer.h" #include "rencache.h" -static int f_font_load(lua_State *L) { - const char *filename = luaL_checkstring(L, 1); - float size = luaL_checknumber(L, 2); - unsigned int font_hinting = FONT_HINTING_SLIGHT, font_style = 0; - ERenFontAntialiasing font_antialiasing = FONT_ANTIALIASING_SUBPIXEL; +static int font_get_options( + lua_State *L, + ERenFontAntialiasing *antialiasing, + ERenFontHinting *hinting, + int *style +) { if (lua_gettop(L) > 2 && lua_istable(L, 3)) { lua_getfield(L, 3, "antialiasing"); if (lua_isstring(L, -1)) { - const char *antialiasing = lua_tostring(L, -1); - if (antialiasing) { - if (strcmp(antialiasing, "none") == 0) { - font_antialiasing = FONT_ANTIALIASING_NONE; - } else if (strcmp(antialiasing, "grayscale") == 0) { - font_antialiasing = FONT_ANTIALIASING_GRAYSCALE; - } else if (strcmp(antialiasing, "subpixel") == 0) { - font_antialiasing = FONT_ANTIALIASING_SUBPIXEL; + const char *antialiasing_str = lua_tostring(L, -1); + if (antialiasing_str) { + if (strcmp(antialiasing_str, "none") == 0) { + *antialiasing = FONT_ANTIALIASING_NONE; + } else if (strcmp(antialiasing_str, "grayscale") == 0) { + *antialiasing = FONT_ANTIALIASING_GRAYSCALE; + } else if (strcmp(antialiasing_str, "subpixel") == 0) { + *antialiasing = FONT_ANTIALIASING_SUBPIXEL; } else { - return luaL_error(L, "error in renderer.font.load, unknown antialiasing option: \"%s\"", antialiasing); + return luaL_error( + L, + "error in font options, unknown antialiasing option: \"%s\"", + antialiasing_str + ); } } } lua_getfield(L, 3, "hinting"); if (lua_isstring(L, -1)) { - const char *hinting = lua_tostring(L, -1); - if (hinting) { - if (strcmp(hinting, "slight") == 0) { - font_hinting = FONT_HINTING_SLIGHT; - } else if (strcmp(hinting, "none") == 0) { - font_hinting = FONT_HINTING_NONE; - } else if (strcmp(hinting, "full") == 0) { - font_hinting = FONT_HINTING_FULL; + const char *hinting_str = lua_tostring(L, -1); + if (hinting_str) { + if (strcmp(hinting_str, "slight") == 0) { + *hinting = FONT_HINTING_SLIGHT; + } else if (strcmp(hinting_str, "none") == 0) { + *hinting = FONT_HINTING_NONE; + } else if (strcmp(hinting_str, "full") == 0) { + *hinting = FONT_HINTING_FULL; } else { - return luaL_error(L, "error in renderer.font.load, unknown hinting option: \"%s\"", hinting); + return luaL_error( + L, + "error in font options, unknown hinting option: \"%s\"", + hinting + ); } } } + int style_local = 0; lua_getfield(L, 3, "italic"); if (lua_toboolean(L, -1)) - font_style |= FONT_STYLE_ITALIC; + style_local |= FONT_STYLE_ITALIC; lua_getfield(L, 3, "bold"); if (lua_toboolean(L, -1)) - font_style |= FONT_STYLE_BOLD; + style_local |= FONT_STYLE_BOLD; lua_getfield(L, 3, "underline"); if (lua_toboolean(L, -1)) - font_style |= FONT_STYLE_UNDERLINE; + style_local |= FONT_STYLE_UNDERLINE; + lua_getfield(L, 3, "smoothing"); + if (lua_toboolean(L, -1)) + style_local |= FONT_STYLE_SMOOTH; + lua_getfield(L, 3, "strikethrough"); + if (lua_toboolean(L, -1)) + style_local |= FONT_STYLE_STRIKETHROUGH; + lua_pop(L, 5); + + if (style_local != 0) + *style = style_local; } + + return 0; +} + +static int f_font_load(lua_State *L) { + const char *filename = luaL_checkstring(L, 1); + float size = luaL_checknumber(L, 2); + int style = 0; + ERenFontHinting hinting = FONT_HINTING_SLIGHT; + ERenFontAntialiasing antialiasing = FONT_ANTIALIASING_SUBPIXEL; + + int ret_code = font_get_options(L, &antialiasing, &hinting, &style); + if (ret_code > 0) + return ret_code; + RenFont** font = lua_newuserdata(L, sizeof(RenFont*)); - *font = ren_font_load(filename, size, font_antialiasing, font_hinting, font_style); + *font = ren_font_load(filename, size, antialiasing, hinting, style); if (!*font) return luaL_error(L, "failed to load font"); luaL_setmetatable(L, API_TYPE_FONT); @@ -59,16 +95,16 @@ static int f_font_load(lua_State *L) { static bool font_retrieve(lua_State* L, RenFont** fonts, int idx) { memset(fonts, 0, sizeof(RenFont*)*FONT_FALLBACK_MAX); - if (lua_type(L, 1) != LUA_TTABLE) { + if (lua_type(L, idx) != LUA_TTABLE) { fonts[0] = *(RenFont**)luaL_checkudata(L, idx, API_TYPE_FONT); return false; } - int i = 0; - do { + int len = luaL_len(L, idx); len = len > FONT_FALLBACK_MAX ? FONT_FALLBACK_MAX : len; + for (int i = 0; i < len; i++) { lua_rawgeti(L, idx, i+1); - fonts[i] = !lua_isnil(L, -1) ? *(RenFont**)luaL_checkudata(L, -1, API_TYPE_FONT) : NULL; + fonts[i] = *(RenFont**) luaL_checkudata(L, -1, API_TYPE_FONT); lua_pop(L, 1); - } while (fonts[i] && i++ < FONT_FALLBACK_MAX); + } return true; } @@ -76,13 +112,21 @@ static int f_font_copy(lua_State *L) { RenFont* fonts[FONT_FALLBACK_MAX]; bool table = font_retrieve(L, fonts, 1); float size = lua_gettop(L) >= 2 ? luaL_checknumber(L, 2) : ren_font_group_get_height(fonts); + int style = -1; + ERenFontHinting hinting = -1; + ERenFontAntialiasing antialiasing = -1; + + int ret_code = font_get_options(L, &antialiasing, &hinting, &style); + if (ret_code > 0) + return ret_code; + if (table) { lua_newtable(L); luaL_setmetatable(L, API_TYPE_FONT); } for (int i = 0; i < FONT_FALLBACK_MAX && fonts[i]; ++i) { RenFont** font = lua_newuserdata(L, sizeof(RenFont*)); - *font = ren_font_copy(fonts[i], size); + *font = ren_font_copy(fonts[i], size, antialiasing, hinting, style); if (!*font) return luaL_error(L, "failed to copy font"); luaL_setmetatable(L, API_TYPE_FONT); @@ -92,12 +136,28 @@ static int f_font_copy(lua_State *L) { return 1; } -static int f_font_group(lua_State* L) { +static int f_font_group(lua_State* L) { luaL_checktype(L, 1, LUA_TTABLE); luaL_setmetatable(L, API_TYPE_FONT); return 1; } +static int f_font_get_path(lua_State *L) { + RenFont* fonts[FONT_FALLBACK_MAX]; + bool table = font_retrieve(L, fonts, 1); + + if (table) { + lua_newtable(L); + } + for (int i = 0; i < FONT_FALLBACK_MAX && fonts[i]; ++i) { + const char* path = ren_font_get_path(fonts[i]); + lua_pushstring(L, path); + if (table) + lua_rawseti(L, -2, i+1); + } + return 1; +} + static int f_font_set_tab_size(lua_State *L) { RenFont* fonts[FONT_FALLBACK_MAX]; font_retrieve(L, fonts, 1); int n = luaL_checknumber(L, 2); @@ -106,8 +166,10 @@ static int f_font_set_tab_size(lua_State *L) { } static int f_font_gc(lua_State *L) { + if (lua_istable(L, 1)) return 0; // do not run if its FontGroup RenFont** self = luaL_checkudata(L, 1, API_TYPE_FONT); ren_font_free(*self); + return 0; } @@ -130,6 +192,13 @@ static int f_font_get_size(lua_State *L) { return 1; } +static int f_font_set_size(lua_State *L) { + RenFont* fonts[FONT_FALLBACK_MAX]; font_retrieve(L, fonts, 1); + float size = luaL_checknumber(L, 2); + ren_font_group_set_size(fonts, size); + return 0; +} + static RenColor checkcolor(lua_State *L, int idx, int def) { RenColor color; if (lua_isnoneornil(L, idx)) { @@ -164,14 +233,14 @@ static int f_get_size(lua_State *L) { } -static int f_begin_frame(lua_State *L) { - rencache_begin_frame(L); +static int f_begin_frame(UNUSED lua_State *L) { + rencache_begin_frame(); return 0; } -static int f_end_frame(lua_State *L) { - rencache_end_frame(L); +static int f_end_frame(UNUSED lua_State *L) { + rencache_end_frame(); return 0; } @@ -212,7 +281,7 @@ static int f_draw_text(lua_State *L) { float x = luaL_checknumber(L, 3); int y = luaL_checknumber(L, 4); RenColor color = checkcolor(L, 5, 255); - x = rencache_draw_text(L, fonts, text, x, y, color); + x = rencache_draw_text(fonts, text, x, y, color); lua_pushnumber(L, x); return 1; } @@ -237,6 +306,8 @@ static const luaL_Reg fontLib[] = { { "get_width", f_font_get_width }, { "get_height", f_font_get_height }, { "get_size", f_font_get_size }, + { "set_size", f_font_set_size }, + { "get_path", f_font_get_path }, { NULL, NULL } }; diff --git a/src/api/system.c b/src/api/system.c index c5343b7d..3d5364d9 100644 --- a/src/api/system.c +++ b/src/api/system.c @@ -1,24 +1,30 @@ #include +#include #include #include -#include -#include #include +#include #include #include "api.h" -// #include "dirmonitor.h" #include "rencache.h" #ifdef _WIN32 #include #include #include -#elif __linux__ + #include "../utfconv.h" +#else + +#include +#include + +#ifdef __linux__ #include #elif __amigaos4__ #include "platform/amigaos4.h" #elif __morphos__ #include "platform/morphos.h" #endif +#endif extern SDL_Window *window; @@ -49,7 +55,7 @@ struct HitTestInfo { }; typedef struct HitTestInfo HitTestInfo; -static HitTestInfo window_hit_info[1] = {{0, 0}}; +static HitTestInfo window_hit_info[1] = {{0, 0, 0}}; #define RESIZE_FROM_TOP 0 #define RESIZE_FROM_RIGHT 0 @@ -103,7 +109,7 @@ static const char *numpad[] = { "end", "down", "pagedown", "left", "", "right", static const char *get_key_name(const SDL_Event *e, char *buf) { SDL_Scancode scancode = e->key.keysym.scancode; - + #if !defined(__amigaos4__) && !defined(__morphos__) /* Is the scancode from the keypad and the number-lock off? ** We assume that SDL_SCANCODE_KP_1 up to SDL_SCANCODE_KP_9 and SDL_SCANCODE_KP_0 @@ -113,24 +119,54 @@ static const char *get_key_name(const SDL_Event *e, char *buf) { return numpad[scancode - SDL_SCANCODE_KP_1]; } else { #endif - + /* We need to correctly handle non-standard layouts such as dvorak. Therefore, if a Latin letter(code<128) is pressed in the current layout, then we transmit it as it is. But we also need to support shortcuts in - other languages, so for non-Latin characters we pass the scancode that - matches the letter in the QWERTY layout. */ - if (e->key.keysym.sym < 128) + other languages, so for non-Latin characters(code>128) we pass the + scancode based name that matches the letter in the QWERTY layout. + + In SDL, the codes of all special buttons such as control, shift, arrows + and others, are masked with SDLK_SCANCODE_MASK, which moves them outside + the unicode range (>0x10FFFF). Users can remap these buttons, so we need + to return the correct name, not scancode based. */ + if ((e->key.keysym.sym < 128) || (e->key.keysym.sym & SDLK_SCANCODE_MASK)) strcpy(buf, SDL_GetKeyName(e->key.keysym.sym)); else strcpy(buf, SDL_GetScancodeName(scancode)); str_tolower(buf); return buf; - + #if !defined(__amigaos4__) && !defined(__MORPHOS__) } #endif } +#ifdef _WIN32 +static char *win32_error(DWORD rc) { + LPSTR message; + FormatMessage( + FORMAT_MESSAGE_ALLOCATE_BUFFER | + FORMAT_MESSAGE_FROM_SYSTEM | + FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, + rc, + MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + (LPTSTR) &message, + 0, + NULL + ); + + return message; +} + +static void push_win32_error(lua_State *L, DWORD rc) { + LPSTR message = win32_error(rc); + lua_pushstring(L, message); + LocalFree(message); +} +#endif + static int f_poll_event(lua_State *L) { char buf[16]; int mx, my, wx, wy; @@ -151,8 +187,8 @@ top: ren_resize_window(); lua_pushstring(L, "resized"); /* The size below will be in points. */ - lua_pushnumber(L, e.window.data1); - lua_pushnumber(L, e.window.data2); + lua_pushinteger(L, e.window.data1); + lua_pushinteger(L, e.window.data2); return 3; } else if (e.window.event == SDL_WINDOWEVENT_EXPOSED) { rencache_invalidate(); @@ -167,6 +203,9 @@ top: } else if (e.window.event == SDL_WINDOWEVENT_RESTORED) { lua_pushstring(L, "restored"); return 1; + } else if (e.window.event == SDL_WINDOWEVENT_LEAVE) { + lua_pushstring(L, "mouseleft"); + return 1; } if (e.window.event == SDL_WINDOWEVENT_FOCUS_LOST) { lua_pushstring(L, "focuslost"); @@ -185,8 +224,8 @@ top: SDL_GetWindowPosition(window, &wx, &wy); lua_pushstring(L, "filedropped"); lua_pushstring(L, e.drop.file); - lua_pushnumber(L, mx - wx); - lua_pushnumber(L, my - wy); + lua_pushinteger(L, mx - wx); + lua_pushinteger(L, my - wy); SDL_free(e.drop.file); return 4; @@ -226,17 +265,17 @@ top: if (e.button.button == 1) { SDL_CaptureMouse(1); } lua_pushstring(L, "mousepressed"); lua_pushstring(L, button_name(e.button.button)); - lua_pushnumber(L, e.button.x); - lua_pushnumber(L, e.button.y); - lua_pushnumber(L, e.button.clicks); + lua_pushinteger(L, e.button.x); + lua_pushinteger(L, e.button.y); + lua_pushinteger(L, e.button.clicks); return 5; case SDL_MOUSEBUTTONUP: if (e.button.button == 1) { SDL_CaptureMouse(0); } lua_pushstring(L, "mousereleased"); lua_pushstring(L, button_name(e.button.button)); - lua_pushnumber(L, e.button.x); - lua_pushnumber(L, e.button.y); + lua_pushinteger(L, e.button.x); + lua_pushinteger(L, e.button.y); return 4; case SDL_MOUSEMOTION: @@ -249,37 +288,17 @@ top: e.motion.yrel += event_plus.motion.yrel; } lua_pushstring(L, "mousemoved"); - lua_pushnumber(L, e.motion.x); - lua_pushnumber(L, e.motion.y); - lua_pushnumber(L, e.motion.xrel); - lua_pushnumber(L, e.motion.yrel); + lua_pushinteger(L, e.motion.x); + lua_pushinteger(L, e.motion.y); + lua_pushinteger(L, e.motion.xrel); + lua_pushinteger(L, e.motion.yrel); return 5; case SDL_MOUSEWHEEL: lua_pushstring(L, "mousewheel"); - lua_pushnumber(L, e.wheel.y); + lua_pushinteger(L, e.wheel.y); return 2; - case SDL_USEREVENT: - lua_pushstring(L, "dirchange"); - lua_pushnumber(L, e.user.code >> 16); - // switch (e.user.code & 0xffff) { - // case DMON_ACTION_DELETE: - // lua_pushstring(L, "delete"); - // break; - // case DMON_ACTION_CREATE: - // lua_pushstring(L, "create"); - // break; - // case DMON_ACTION_MODIFY: - // lua_pushstring(L, "modify"); - // break; - // default: - // return luaL_error(L, "unknown dmon event action: %d", e.user.code & 0xffff); - // } - lua_pushstring(L, e.user.data1); - free(e.user.data1); - return 4; - default: goto top; } @@ -346,10 +365,10 @@ static int f_set_window_mode(lua_State *L) { int n = luaL_checkoption(L, 1, "normal", window_opts); SDL_SetWindowFullscreen(window, n == WIN_FULLSCREEN ? SDL_WINDOW_FULLSCREEN_DESKTOP : 0); - if (n == WIN_NORMAL) - { + if (n == WIN_NORMAL) + { ren_resize_window(); - SDL_RestoreWindow(window); + SDL_RestoreWindow(window); } if (n == WIN_MAXIMIZED) { SDL_MaximizeWindow(window); } if (n == WIN_MINIMIZED) { SDL_MinimizeWindow(window); } @@ -381,10 +400,10 @@ static int f_get_window_size(lua_State *L) { int x, y, w, h; SDL_GetWindowSize(window, &w, &h); SDL_GetWindowPosition(window, &x, &y); - lua_pushnumber(L, w); - lua_pushnumber(L, h); - lua_pushnumber(L, x); - lua_pushnumber(L, y); + lua_pushinteger(L, w); + lua_pushinteger(L, h); + lua_pushinteger(L, x); + lua_pushinteger(L, y); return 4; } @@ -452,29 +471,14 @@ static int f_rmdir(lua_State *L) { const char *path = luaL_checkstring(L, 1); #ifdef _WIN32 - int deleted = RemoveDirectoryA(path); - if(deleted > 0) { + LPWSTR wpath = utfconv_utf8towc(path); + int deleted = RemoveDirectoryW(wpath); + free(wpath); + if (deleted > 0) { lua_pushboolean(L, 1); } else { - DWORD error_code = GetLastError(); - LPVOID message; - - FormatMessage( - FORMAT_MESSAGE_ALLOCATE_BUFFER | - FORMAT_MESSAGE_FROM_SYSTEM | - FORMAT_MESSAGE_IGNORE_INSERTS, - NULL, - error_code, - MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), - (LPTSTR) &message, - 0, - NULL - ); - lua_pushboolean(L, 0); - lua_pushlstring(L, (LPCTSTR)message, lstrlen((LPCTSTR)message)); - LocalFree(message); - + push_win32_error(L, GetLastError()); return 2; } #else @@ -495,8 +499,15 @@ static int f_rmdir(lua_State *L) { static int f_chdir(lua_State *L) { const char *path = luaL_checkstring(L, 1); +#ifdef _WIN32 + LPWSTR wpath = utfconv_utf8towc(path); + if (wpath == NULL) { return luaL_error(L, UTFCONV_ERROR_INVALID_CONVERSION ); } + int err = _wchdir(wpath); + free(wpath); +#else int err = chdir(path); - if (err) { luaL_error(L, "chdir() failed"); } +#endif + if (err) { luaL_error(L, "chdir() failed: %s", strerror(errno)); } return 0; } @@ -504,6 +515,57 @@ static int f_chdir(lua_State *L) { static int f_list_dir(lua_State *L) { const char *path = luaL_checkstring(L, 1); +#ifdef _WIN32 + lua_settop(L, 1); + if (strchr("\\/", path[strlen(path) - 2]) != NULL) + lua_pushstring(L, "*"); + else + lua_pushstring(L, "/*"); + + lua_concat(L, 2); + path = lua_tostring(L, -1); + + LPWSTR wpath = utfconv_utf8towc(path); + if (wpath == NULL) { + lua_pushnil(L); + lua_pushstring(L, UTFCONV_ERROR_INVALID_CONVERSION); + return 2; + } + + WIN32_FIND_DATAW fd; + HANDLE find_handle = FindFirstFileExW(wpath, FindExInfoBasic, &fd, FindExSearchNameMatch, NULL, 0); + free(wpath); + if (find_handle == INVALID_HANDLE_VALUE) { + lua_pushnil(L); + push_win32_error(L, GetLastError()); + return 2; + } + + char mbpath[MAX_PATH * 4]; // utf-8 spans 4 bytes at most + int len, i = 1; + lua_newtable(L); + + do + { + if (wcscmp(fd.cFileName, L".") == 0) { continue; } + if (wcscmp(fd.cFileName, L"..") == 0) { continue; } + + len = WideCharToMultiByte(CP_UTF8, 0, fd.cFileName, -1, mbpath, MAX_PATH * 4, NULL, NULL); + if (len == 0) { break; } + lua_pushlstring(L, mbpath, len - 1); // len includes \0 + lua_rawseti(L, -2, i++); + } while (FindNextFileW(find_handle, &fd)); + + if (GetLastError() != ERROR_NO_MORE_FILES) { + lua_pushnil(L); + push_win32_error(L, GetLastError()); + FindClose(find_handle); + return 2; + } + + FindClose(find_handle); + return 1; +#else DIR *dir = opendir(path); if (!dir) { lua_pushnil(L); @@ -524,12 +586,12 @@ static int f_list_dir(lua_State *L) { closedir(dir); return 1; +#endif } #ifdef _WIN32 - #include - #define realpath(x, y) _fullpath(y, x, MAX_PATH) + #define realpath(x, y) _wfullpath(y, x, MAX_PATH) #endif #if defined(__amigaos4__) || defined(__morphos__) @@ -538,7 +600,19 @@ static int f_list_dir(lua_State *L) { static int f_absolute_path(lua_State *L) { const char *path = luaL_checkstring(L, 1); +#ifdef _WIN32 + LPWSTR wpath = utfconv_utf8towc(path); + if (!wpath) { return 0; } + + LPWSTR wfullpath = realpath(wpath, NULL); + free(wpath); + if (!wfullpath) { return 0; } + + char *res = utfconv_wctoutf8(wfullpath); + free(wfullpath); +#else char *res = realpath(path, NULL); +#endif if (!res) { return 0; } lua_pushstring(L, res); free(res); @@ -549,8 +623,20 @@ static int f_absolute_path(lua_State *L) { static int f_get_file_info(lua_State *L) { const char *path = luaL_checkstring(L, 1); +#ifdef _WIN32 + struct _stat s; + LPWSTR wpath = utfconv_utf8towc(path); + if (wpath == NULL) { + lua_pushnil(L); + lua_pushstring(L, UTFCONV_ERROR_INVALID_CONVERSION); + return 2; + } + int err = _wstat(wpath, &s); + free(wpath); +#else struct stat s; int err = stat(path, &s); +#endif if (err < 0) { lua_pushnil(L); lua_pushstring(L, strerror(errno)); @@ -558,10 +644,10 @@ static int f_get_file_info(lua_State *L) { } lua_newtable(L); - lua_pushnumber(L, s.st_mtime); + lua_pushinteger(L, s.st_mtime); lua_setfield(L, -2, "modified"); - lua_pushnumber(L, s.st_size); + lua_pushinteger(L, s.st_size); lua_setfield(L, -2, "size"); if (S_ISREG(s.st_mode)) { @@ -573,6 +659,14 @@ static int f_get_file_info(lua_State *L) { } lua_setfield(L, -2, "type"); +#if __linux__ + if (S_ISDIR(s.st_mode)) { + if (lstat(path, &s) == 0) { + lua_pushboolean(L, S_ISLNK(s.st_mode)); + lua_setfield(L, -2, "symlink"); + } + } +#endif return 1; } @@ -597,30 +691,41 @@ static struct f_type_names fs_names[] = { { 0x0, NULL }, }; +#endif + static int f_get_fs_type(lua_State *L) { - const char *path = luaL_checkstring(L, 1); - struct statfs buf; - int status = statfs(path, &buf); - if (status != 0) { - return luaL_error(L, "error calling statfs on %s", path); - } - for (int i = 0; fs_names[i].magic; i++) { - if (fs_names[i].magic == buf.f_type) { - lua_pushstring(L, fs_names[i].name); - return 1; + #if __linux__ + const char *path = luaL_checkstring(L, 1); + struct statfs buf; + int status = statfs(path, &buf); + if (status != 0) { + return luaL_error(L, "error calling statfs on %s", path); } - } + for (int i = 0; fs_names[i].magic; i++) { + if (fs_names[i].magic == buf.f_type) { + lua_pushstring(L, fs_names[i].name); + return 1; + } + } + #endif lua_pushstring(L, "unknown"); return 1; } -#endif static int f_mkdir(lua_State *L) { const char *path = luaL_checkstring(L, 1); #ifdef _WIN32 - int err = _mkdir(path); + LPWSTR wpath = utfconv_utf8towc(path); + if (wpath == NULL) { + lua_pushboolean(L, 0); + lua_pushstring(L, UTFCONV_ERROR_INVALID_CONVERSION); + return 2; + } + + int err = _wmkdir(wpath); + free(wpath); #else int err = mkdir(path, S_IRUSR|S_IWUSR|S_IXUSR|S_IRGRP|S_IXGRP|S_IROTH|S_IXOTH); #endif @@ -651,6 +756,16 @@ static int f_set_clipboard(lua_State *L) { } +static int f_get_process_id(lua_State *L) { +#ifdef _WIN32 + lua_pushinteger(L, GetCurrentProcessId()); +#else + lua_pushinteger(L, getpid()); +#endif + return 1; +} + + static int f_get_time(lua_State *L) { double n = SDL_GetPerformanceCounter() / (double) SDL_GetPerformanceFrequency(); lua_pushnumber(L, n); @@ -707,7 +822,7 @@ static int f_fuzzy_match(lua_State *L) { strTarget += increment; } if (ptnTarget >= ptn && *ptnTarget) { return 0; } - lua_pushnumber(L, score - (int)strLen * 10); + lua_pushinteger(L, score - (int)strLen * 10); return 1; } @@ -718,13 +833,15 @@ static int f_set_window_opacity(lua_State *L) { return 1; } +typedef void (*fptr)(void); + typedef struct lua_function_node { const char *symbol; - void *address; + fptr address; } lua_function_node; -#define P(FUNC) { "lua_" #FUNC, (void*)(lua_##FUNC) } -#define U(FUNC) { "luaL_" #FUNC, (void*)(luaL_##FUNC) } +#define P(FUNC) { "lua_" #FUNC, (fptr)(lua_##FUNC) } +#define U(FUNC) { "luaL_" #FUNC, (fptr)(luaL_##FUNC) } static void* api_require(const char* symbol) { static lua_function_node nodes[] = { P(atpanic), P(checkstack), @@ -732,13 +849,13 @@ static void* api_require(const char* symbol) { P(error), P(gc), P(getallocf), P(getfield), P(gethook), P(gethookcount), P(gethookmask), P(getinfo), P(getlocal), P(getmetatable), P(getstack), P(gettable), P(gettop), P(getupvalue), - P(insert), P(isnumber), P(isstring), P(isuserdata), - P(load), P(newstate), P(newthread), P(newuserdata), P(next), + P(isnumber), P(isstring), P(isuserdata), + P(load), P(newstate), P(newthread), P(next), P(pushboolean), P(pushcclosure), P(pushfstring), P(pushinteger), P(pushlightuserdata), P(pushlstring), P(pushnil), P(pushnumber), P(pushstring), P(pushthread), P(pushvalue), P(pushvfstring), P(rawequal), P(rawget), P(rawgeti), - P(rawset), P(rawseti), P(remove), P(replace), P(resume), + P(rawset), P(rawseti), P(resume), P(setallocf), P(setfield), P(sethook), P(setlocal), P(setmetatable), P(settable), P(settop), P(setupvalue), P(status), P(tocfunction), P(tointegerx), P(tolstring), P(toboolean), @@ -749,22 +866,22 @@ static void* api_require(const char* symbol) { U(newmetatable), U(setmetatable), U(testudata), U(checkudata), U(where), U(error), U(fileresult), U(execresult), U(ref), U(unref), U(loadstring), U(newstate), U(setfuncs), U(buffinit), U(addlstring), U(addstring), - U(addvalue), U(pushresult), + U(addvalue), U(pushresult), {"api_load_libs", (void*)(api_load_libs)}, #if LUA_VERSION_NUM >= 502 - P(absindex), P(arith), P(callk), P(compare), P(getctx), P(getglobal), P(getuservalue), - P(len), P(pcallk), P(pushunsigned), P(rawgetp), P(rawlen), P(rawsetp), P(setglobal), - P(iscfunction), P(setuservalue), P(tounsignedx), P(yieldk), - U(checkversion_), U(tolstring), U(checkunsigned), U(len), U(getsubtable), U(prepbuffsize), + P(absindex), P(arith), P(callk), P(compare), P(getglobal), + P(len), P(pcallk), P(rawgetp), P(rawlen), P(rawsetp), P(setglobal), + P(iscfunction), P(yieldk), + U(checkversion_), U(tolstring), U(len), U(getsubtable), U(prepbuffsize), U(pushresultsize), U(buffinitsize), U(checklstring), U(checkoption), U(gsub), U(loadbufferx), - U(loadfilex), U(optinteger), U(optlstring), U(optunsigned), U(requiref), U(traceback) + U(loadfilex), U(optinteger), U(optlstring), U(requiref), U(traceback) #else P(objlen) #endif }; - for (int i = 0; i < sizeof(nodes) / sizeof(lua_function_node); ++i) { + for (size_t i = 0; i < sizeof(nodes) / sizeof(lua_function_node); ++i) { if (strcmp(nodes[i].symbol, symbol) == 0) - return nodes[i].address; + return *(void**)(&nodes[i].address); } return NULL; } @@ -777,7 +894,7 @@ static int f_load_native_plugin(lua_State *L) { const char *path = luaL_checkstring(L, 2); void *library = SDL_LoadObject(path); if (!library) - return luaL_error(L, "Unable to load %s: %s", name, SDL_GetError()); + return (lua_pushstring(L, SDL_GetError()), lua_error(L)); lua_getglobal(L, "package"); lua_getfield(L, -1, "native_plugins"); @@ -788,10 +905,12 @@ static int f_load_native_plugin(lua_State *L) { const char *basename = strrchr(name, '.'); basename = !basename ? name : basename + 1; snprintf(entrypoint_name, sizeof(entrypoint_name), "luaopen_lite_xl_%s", basename); - int (*ext_entrypoint) (lua_State *L, void*) = SDL_LoadFunction(library, entrypoint_name); + int (*ext_entrypoint) (lua_State *L, void* (*)(const char*)); + *(void**)(&ext_entrypoint) = SDL_LoadFunction(library, entrypoint_name); if (!ext_entrypoint) { snprintf(entrypoint_name, sizeof(entrypoint_name), "luaopen_%s", basename); - int (*entrypoint)(lua_State *L) = SDL_LoadFunction(library, entrypoint_name); + int (*entrypoint)(lua_State *L); + *(void**)(&entrypoint) = SDL_LoadFunction(library, entrypoint_name); if (!entrypoint) return luaL_error(L, "Unable to load %s: Can't find %s(lua_State *L, void *XL)", name, entrypoint_name); result = entrypoint(L); @@ -805,35 +924,6 @@ static int f_load_native_plugin(lua_State *L) { return result; } -static int f_watch_dir(lua_State *L) { - const char *path = luaL_checkstring(L, 1); - const int recursive = lua_toboolean(L, 2); - // uint32_t dmon_flags = (recursive ? DMON_WATCHFLAGS_RECURSIVE : 0); - // dmon_watch_id watch_id = dmon_watch(path, dirmonitor_watch_callback, dmon_flags, NULL); - // if (watch_id.id == 0) { luaL_error(L, "directory monitoring watch failed"); } - // lua_pushnumber(L, watch_id.id); - lua_pushnumber(L, 0); - return 1; -} - -#if __linux__ -static int f_watch_dir_add(lua_State *L) { - // dmon_watch_id watch_id; - // watch_id.id = luaL_checkinteger(L, 1); - // const char *subdir = luaL_checkstring(L, 2); - // lua_pushboolean(L, dmon_watch_add(watch_id, subdir)); - return 1; -} - -static int f_watch_dir_rm(lua_State *L) { - // dmon_watch_id watch_id; - // watch_id.id = luaL_checkinteger(L, 1); - // const char *subdir = luaL_checkstring(L, 2); - // lua_pushboolean(L, dmon_watch_rm(watch_id, subdir)); - return 1; -} -#endif - #ifdef _WIN32 #define PATHSEP '\\' #else @@ -913,19 +1003,15 @@ static const luaL_Reg lib[] = { { "get_file_info", f_get_file_info }, { "get_clipboard", f_get_clipboard }, { "set_clipboard", f_set_clipboard }, + { "get_process_id", f_get_process_id }, { "get_time", f_get_time }, { "sleep", f_sleep }, { "exec", f_exec }, { "fuzzy_match", f_fuzzy_match }, { "set_window_opacity", f_set_window_opacity }, { "load_native_plugin", f_load_native_plugin }, - { "watch_dir", f_watch_dir }, { "path_compare", f_path_compare }, -#if __linux__ - { "watch_dir_add", f_watch_dir_add }, - { "watch_dir_rm", f_watch_dir_rm }, { "get_fs_type", f_get_fs_type }, -#endif { NULL, NULL } }; diff --git a/src/api/utf8.c b/src/api/utf8.c new file mode 100644 index 00000000..6f0d6c17 --- /dev/null +++ b/src/api/utf8.c @@ -0,0 +1,1305 @@ +/* + * Integration of https://github.com/starwing/luautf8 + * + * Copyright (c) 2018 Xavier Wang + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include +#include +#include + + +#include +#include + +#include "../unidata.h" + +/* UTF-8 string operations */ + +#define UTF8_BUFFSZ 8 +#define UTF8_MAX 0x7FFFFFFFu +#define UTF8_MAXCP 0x10FFFFu +#define iscont(p) ((*(p) & 0xC0) == 0x80) +#define CAST(tp,expr) ((tp)(expr)) + +#ifndef LUA_QL +# define LUA_QL(x) "'" x "'" +#endif + +static int utf8_invalid (utfint ch) +{ return (ch > UTF8_MAXCP || (0xD800u <= ch && ch <= 0xDFFFu)); } + +static size_t utf8_encode (char *buff, utfint x) { + int n = 1; /* number of bytes put in buffer (backwards) */ + lua_assert(x <= UTF8_MAX); + if (x < 0x80) /* ascii? */ + buff[UTF8_BUFFSZ - 1] = x & 0x7F; + else { /* need continuation bytes */ + utfint mfb = 0x3f; /* maximum that fits in first byte */ + do { /* add continuation bytes */ + buff[UTF8_BUFFSZ - (n++)] = 0x80 | (x & 0x3f); + x >>= 6; /* remove added bits */ + mfb >>= 1; /* now there is one less bit available in first byte */ + } while (x > mfb); /* still needs continuation byte? */ + buff[UTF8_BUFFSZ - n] = ((~mfb << 1) | x) & 0xFF; /* add first byte */ + } + return n; +} + +static const char *utf8_decode (const char *s, utfint *val, int strict) { + static const utfint limits[] = + {~0u, 0x80u, 0x800u, 0x10000u, 0x200000u, 0x4000000u}; + unsigned int c = (unsigned char)s[0]; + utfint res = 0; /* final result */ + if (c < 0x80) /* ascii? */ + res = c; + else { + int count = 0; /* to count number of continuation bytes */ + for (; c & 0x40; c <<= 1) { /* while it needs continuation bytes... */ + unsigned int cc = (unsigned char)s[++count]; /* read next byte */ + if ((cc & 0xC0) != 0x80) /* not a continuation byte? */ + return NULL; /* invalid byte sequence */ + res = (res << 6) | (cc & 0x3F); /* add lower 6 bits from cont. byte */ + } + res |= ((utfint)(c & 0x7F) << (count * 5)); /* add first byte */ + if (count > 5 || res > UTF8_MAX || res < limits[count]) + return NULL; /* invalid byte sequence */ + s += count; /* skip continuation bytes read */ + } + if (strict) { + /* check for invalid code points; too large or surrogates */ + if (res > UTF8_MAXCP || (0xD800u <= res && res <= 0xDFFFu)) + return NULL; + } + if (val) *val = res; + return s + 1; /* +1 to include first byte */ +} + +static const char *utf8_prev (const char *s, const char *e) { + while (s < e && iscont(e - 1)) --e; + return s < e ? e - 1 : s; +} + +static const char *utf8_next (const char *s, const char *e) { + while (s < e && iscont(s + 1)) ++s; + return s < e ? s + 1 : e; +} + +static size_t utf8_length (const char *s, const char *e) { + size_t i; + for (i = 0; s < e; ++i) + s = utf8_next(s, e); + return i; +} + +static const char *utf8_offset (const char *s, const char *e, lua_Integer offset, lua_Integer idx) { + const char *p = s + offset - 1; + if (idx >= 0) { + while (p < e && idx > 0) + p = utf8_next(p, e), --idx; + return idx == 0 ? p : NULL; + } else { + while (s < p && idx < 0) + p = utf8_prev(s, p), ++idx; + return idx == 0 ? p : NULL; + } +} + +static const char *utf8_relat (const char *s, const char *e, int idx) { + return idx >= 0 ? + utf8_offset(s, e, 1, idx - 1) : + utf8_offset(s, e, e-s+1, idx); +} + +static int utf8_range(const char *s, const char *e, lua_Integer *i, lua_Integer *j) { + const char *ps = utf8_relat(s, e, CAST(int, *i)); + const char *pe = utf8_relat(s, e, CAST(int, *j)); + *i = (ps ? ps : (*i > 0 ? e : s)) - s; + *j = (pe ? utf8_next(pe, e) : (*j > 0 ? e : s)) - s; + return *i < *j; +} + + +/* Unicode character categories */ + +#define table_size(t) (sizeof(t)/sizeof((t)[0])) + +#define utf8_categories(X) \ + X('a', alpha) \ + X('c', cntrl) \ + X('d', digit) \ + X('l', lower) \ + X('p', punct) \ + X('s', space) \ + X('t', compose) \ + X('u', upper) \ + X('x', xdigit) + +#define utf8_converters(X) \ + X(lower) \ + X(upper) \ + X(title) \ + X(fold) + +static int find_in_range (range_table *t, size_t size, utfint ch) { + size_t begin, end; + + begin = 0; + end = size; + + while (begin < end) { + size_t mid = (begin + end) / 2; + if (t[mid].last < ch) + begin = mid + 1; + else if (t[mid].first > ch) + end = mid; + else + return (ch - t[mid].first) % t[mid].step == 0; + } + + return 0; +} + +static int convert_char (conv_table *t, size_t size, utfint ch) { + size_t begin, end; + + begin = 0; + end = size; + + while (begin < end) { + size_t mid = (begin + end) / 2; + if (t[mid].last < ch) + begin = mid + 1; + else if (t[mid].first > ch) + end = mid; + else if ((ch - t[mid].first) % t[mid].step == 0) + return ch + t[mid].offset; + else + return ch; + } + + return ch; +} + +#define define_category(cls, name) static int utf8_is##name (utfint ch)\ +{ return find_in_range(name##_table, table_size(name##_table), ch); } +#define define_converter(name) static utfint utf8_to##name (utfint ch) \ +{ return convert_char(to##name##_table, table_size(to##name##_table), ch); } +utf8_categories(define_category) +utf8_converters(define_converter) +#undef define_category +#undef define_converter + +static int utf8_isgraph (utfint ch) { + if (find_in_range(space_table, table_size(space_table), ch)) + return 0; + if (find_in_range(graph_table, table_size(graph_table), ch)) + return 1; + if (find_in_range(compose_table, table_size(compose_table), ch)) + return 1; + return 0; +} + +static int utf8_isalnum (utfint ch) { + if (find_in_range(alpha_table, table_size(alpha_table), ch)) + return 1; + if (find_in_range(alnum_extend_table, table_size(alnum_extend_table), ch)) + return 1; + return 0; +} + +static int utf8_width (utfint ch, int ambi_is_single) { + if (find_in_range(doublewidth_table, table_size(doublewidth_table), ch)) + return 2; + if (find_in_range(ambiwidth_table, table_size(ambiwidth_table), ch)) + return ambi_is_single ? 1 : 2; + if (find_in_range(compose_table, table_size(compose_table), ch)) + return 0; + if (find_in_range(unprintable_table, table_size(unprintable_table), ch)) + return 0; + return 1; +} + + +/* string module compatible interface */ + +static int typeerror (lua_State *L, int idx, const char *tname) +{ return luaL_error(L, "%s expected, got %s", tname, luaL_typename(L, idx)); } + +static const char *check_utf8 (lua_State *L, int idx, const char **end) { + size_t len; + const char *s = luaL_checklstring(L, idx, &len); + if (end) *end = s+len; + return s; +} + +static const char *to_utf8 (lua_State *L, int idx, const char **end) { + size_t len; + const char *s = lua_tolstring(L, idx, &len); + if (end) *end = s+len; + return s; +} + +static const char *utf8_safe_decode (lua_State *L, const char *p, utfint *pval) { + p = utf8_decode(p, pval, 0); + if (p == NULL) luaL_error(L, "invalid UTF-8 code"); + return p; +} + +static void add_utf8char (luaL_Buffer *b, utfint ch) { + char buff[UTF8_BUFFSZ]; + size_t n = utf8_encode(buff, ch); + luaL_addlstring(b, buff+UTF8_BUFFSZ-n, n); +} + +static lua_Integer byte_relat (lua_Integer pos, size_t len) { + if (pos >= 0) return pos; + else if (0u - (size_t)pos > len) return 0; + else return (lua_Integer)len + pos + 1; +} + +static int Lutf8_len (lua_State *L) { + size_t len, n; + const char *s = luaL_checklstring(L, 1, &len), *p, *e; + lua_Integer posi = byte_relat(luaL_optinteger(L, 2, 1), len); + lua_Integer pose = byte_relat(luaL_optinteger(L, 3, -1), len); + int lax = lua_toboolean(L, 4); + luaL_argcheck(L, 1 <= posi && --posi <= (lua_Integer)len, 2, + "initial position out of string"); + luaL_argcheck(L, --pose < (lua_Integer)len, 3, + "final position out of string"); + for (n = 0, p=s+posi, e=s+pose+1; p < e; ++n) { + if (lax) + p = utf8_next(p, e); + else { + utfint ch; + const char *np = utf8_decode(p, &ch, !lax); + if (np == NULL || utf8_invalid(ch)) { + lua_pushnil(L); + lua_pushinteger(L, p - s + 1); + return 2; + } + p = np; + } + } + lua_pushinteger(L, n); + return 1; +} + +static int Lutf8_sub (lua_State *L) { + const char *e, *s = check_utf8(L, 1, &e); + lua_Integer posi = luaL_checkinteger(L, 2); + lua_Integer pose = luaL_optinteger(L, 3, -1); + if (utf8_range(s, e, &posi, &pose)) + lua_pushlstring(L, s+posi, pose-posi); + else + lua_pushliteral(L, ""); + return 1; +} + +static int Lutf8_reverse (lua_State *L) { + luaL_Buffer b; + const char *prev, *pprev, *ends, *e, *s = check_utf8(L, 1, &e); + (void) ends; + int lax = lua_toboolean(L, 2); + luaL_buffinit(L, &b); + if (lax) { + for (prev = e; s < prev; e = prev) { + prev = utf8_prev(s, prev); + luaL_addlstring(&b, prev, e-prev); + } + } else { + for (prev = e; s < prev; prev = pprev) { + utfint code = 0; + ends = utf8_safe_decode(L, pprev = utf8_prev(s, prev), &code); + assert(ends == prev); + if (utf8_invalid(code)) + return luaL_error(L, "invalid UTF-8 code"); + if (!utf8_iscompose(code)) { + luaL_addlstring(&b, pprev, e-pprev); + e = pprev; + } + } + } + luaL_pushresult(&b); + return 1; +} + +static int Lutf8_byte (lua_State *L) { + size_t n = 0; + const char *e, *s = check_utf8(L, 1, &e); + lua_Integer posi = luaL_optinteger(L, 2, 1); + lua_Integer pose = luaL_optinteger(L, 3, posi); + if (utf8_range(s, e, &posi, &pose)) { + for (e = s + pose, s = s + posi; s < e; ++n) { + utfint ch = 0; + s = utf8_safe_decode(L, s, &ch); + lua_pushinteger(L, ch); + } + } + return CAST(int, n); +} + +static int Lutf8_codepoint (lua_State *L) { + const char *e, *s = check_utf8(L, 1, &e); + size_t len = e-s; + lua_Integer posi = byte_relat(luaL_optinteger(L, 2, 1), len); + lua_Integer pose = byte_relat(luaL_optinteger(L, 3, posi), len); + int lax = lua_toboolean(L, 4); + int n; + const char *se; + luaL_argcheck(L, posi >= 1, 2, "out of range"); + luaL_argcheck(L, pose <= (lua_Integer)len, 3, "out of range"); + if (posi > pose) return 0; /* empty interval; return no values */ + if (pose - posi >= INT_MAX) /* (lua_Integer -> int) overflow? */ + return luaL_error(L, "string slice too long"); + n = (int)(pose - posi + 1); + luaL_checkstack(L, n, "string slice too long"); + n = 0; /* count the number of returns */ + se = s + pose; /* string end */ + for (n = 0, s += posi - 1; s < se;) { + utfint code = 0; + s = utf8_safe_decode(L, s, &code); + if (!lax && utf8_invalid(code)) + return luaL_error(L, "invalid UTF-8 code"); + lua_pushinteger(L, code); + n++; + } + return n; +} + +static int Lutf8_char (lua_State *L) { + int i, n = lua_gettop(L); /* number of arguments */ + luaL_Buffer b; + luaL_buffinit(L, &b); + for (i = 1; i <= n; ++i) { + lua_Integer code = luaL_checkinteger(L, i); + luaL_argcheck(L, code <= UTF8_MAXCP, i, "value out of range"); + add_utf8char(&b, CAST(utfint, code)); + } + luaL_pushresult(&b); + return 1; +} + +#define bind_converter(name) \ +static int Lutf8_##name (lua_State *L) { \ + int t = lua_type(L, 1); \ + if (t == LUA_TNUMBER) \ + lua_pushinteger(L, utf8_to##name(CAST(utfint, lua_tointeger(L, 1)))); \ + else if (t == LUA_TSTRING) { \ + luaL_Buffer b; \ + const char *e, *s = to_utf8(L, 1, &e); \ + luaL_buffinit(L, &b); \ + while (s < e) { \ + utfint ch = 0; \ + s = utf8_safe_decode(L, s, &ch); \ + add_utf8char(&b, utf8_to##name(ch)); \ + } \ + luaL_pushresult(&b); \ + } \ + else return typeerror(L, 1, "number/string"); \ + return 1; \ +} +utf8_converters(bind_converter) +#undef bind_converter + + +/* unicode extra interface */ + +static const char *parse_escape (lua_State *L, const char *s, const char *e, int hex, utfint *pch) { + utfint code = 0; + int in_bracket = 0; + if (*s == '{') ++s, in_bracket = 1; + for (; s < e; ++s) { + utfint ch = (unsigned char)*s; + if (ch >= '0' && ch <= '9') ch = ch - '0'; + else if (hex && ch >= 'A' && ch <= 'F') ch = 10 + (ch - 'A'); + else if (hex && ch >= 'a' && ch <= 'f') ch = 10 + (ch - 'a'); + else if (!in_bracket) break; + else if (ch == '}') { ++s; break; } + else luaL_error(L, "invalid escape '%c'", ch); + code *= hex ? 16 : 10; + code += ch; + } + *pch = code; + return s; +} + +static int Lutf8_escape (lua_State *L) { + const char *e, *s = check_utf8(L, 1, &e); + luaL_Buffer b; + luaL_buffinit(L, &b); + while (s < e) { + utfint ch = 0; + s = utf8_safe_decode(L, s, &ch); + if (ch == '%') { + int hex = 0; + switch (*s) { + case '0': case '1': case '2': case '3': + case '4': case '5': case '6': case '7': + case '8': case '9': case '{': + break; + case 'x': case 'X': hex = 1; /* fall through */ + case 'u': case 'U': if (s+1 < e) { ++s; break; } + /* fall through */ + default: + s = utf8_safe_decode(L, s, &ch); + goto next; + } + s = parse_escape(L, s, e, hex, &ch); + } +next: + add_utf8char(&b, ch); + } + luaL_pushresult(&b); + return 1; +} + +static int Lutf8_insert (lua_State *L) { + const char *e, *s = check_utf8(L, 1, &e); + size_t sublen; + const char *subs; + luaL_Buffer b; + int nargs = 2; + const char *first = e; + if (lua_type(L, 2) == LUA_TNUMBER) { + int idx = (int)lua_tointeger(L, 2); + if (idx != 0) first = utf8_relat(s, e, idx); + luaL_argcheck(L, first, 2, "invalid index"); + ++nargs; + } + subs = luaL_checklstring(L, nargs, &sublen); + luaL_buffinit(L, &b); + luaL_addlstring(&b, s, first-s); + luaL_addlstring(&b, subs, sublen); + luaL_addlstring(&b, first, e-first); + luaL_pushresult(&b); + return 1; +} + +static int Lutf8_remove (lua_State *L) { + const char *e, *s = check_utf8(L, 1, &e); + lua_Integer posi = luaL_optinteger(L, 2, -1); + lua_Integer pose = luaL_optinteger(L, 3, -1); + if (!utf8_range(s, e, &posi, &pose)) + lua_settop(L, 1); + else { + luaL_Buffer b; + luaL_buffinit(L, &b); + luaL_addlstring(&b, s, posi); + luaL_addlstring(&b, s+pose, e-s-pose); + luaL_pushresult(&b); + } + return 1; +} + +static int push_offset (lua_State *L, const char *s, const char *e, lua_Integer offset, lua_Integer idx) { + utfint ch = 0; + const char *p; + if (idx != 0) + p = utf8_offset(s, e, offset, idx); + else if (p = s+offset-1, iscont(p)) + p = utf8_prev(s, p); + if (p == NULL || p == e) return 0; + utf8_decode(p, &ch, 0); + lua_pushinteger(L, p-s+1); + lua_pushinteger(L, ch); + return 2; +} + +static int Lutf8_charpos (lua_State *L) { + const char *e, *s = check_utf8(L, 1, &e); + lua_Integer offset = 1; + if (lua_isnoneornil(L, 3)) { + lua_Integer idx = luaL_optinteger(L, 2, 0); + if (idx > 0) --idx; + else if (idx < 0) offset = e-s+1; + return push_offset(L, s, e, offset, idx); + } + offset = byte_relat(luaL_optinteger(L, 2, 1), e-s); + if (offset < 1) offset = 1; + return push_offset(L, s, e, offset, luaL_checkinteger(L, 3)); +} + +static int Lutf8_offset (lua_State *L) { + size_t len; + const char *s = luaL_checklstring(L, 1, &len); + lua_Integer n = luaL_checkinteger(L, 2); + lua_Integer posi = (n >= 0) ? 1 : len + 1; + posi = byte_relat(luaL_optinteger(L, 3, posi), len); + luaL_argcheck(L, 1 <= posi && --posi <= (lua_Integer)len, 3, + "position out of range"); + if (n == 0) { + /* find beginning of current byte sequence */ + while (posi > 0 && iscont(s + posi)) posi--; + } else { + if (iscont(s + posi)) + return luaL_error(L, "initial position is a continuation byte"); + if (n < 0) { + while (n < 0 && posi > 0) { /* move back */ + do { /* find beginning of previous character */ + posi--; + } while (posi > 0 && iscont(s + posi)); + n++; + } + } else { + n--; /* do not move for 1st character */ + while (n > 0 && posi < (lua_Integer)len) { + do { /* find beginning of next character */ + posi++; + } while (iscont(s + posi)); /* (cannot pass final '\0') */ + n--; + } + } + } + if (n == 0) /* did it find given character? */ + lua_pushinteger(L, posi + 1); + else /* no such character */ + lua_pushnil(L); + return 1; +} + +static int Lutf8_next (lua_State *L) { + const char *e, *s = check_utf8(L, 1, &e); + lua_Integer offset = byte_relat(luaL_optinteger(L, 2, 1), e-s); + lua_Integer idx = luaL_optinteger(L, 3, !lua_isnoneornil(L, 2)); + return push_offset(L, s, e, offset, idx); +} + +static int iter_aux (lua_State *L, int strict) { + const char *e, *s = check_utf8(L, 1, &e); + int n = CAST(int, lua_tointeger(L, 2)); + const char *p = n <= 0 ? s : utf8_next(s+n-1, e); + if (p < e) { + utfint code = 0; + utf8_safe_decode(L, p, &code); + if (strict && utf8_invalid(code)) + return luaL_error(L, "invalid UTF-8 code"); + lua_pushinteger(L, p-s+1); + lua_pushinteger(L, code); + return 2; + } + return 0; /* no more codepoints */ +} + +static int iter_auxstrict (lua_State *L) { return iter_aux(L, 1); } +static int iter_auxlax (lua_State *L) { return iter_aux(L, 0); } + +static int Lutf8_codes (lua_State *L) { + int lax = lua_toboolean(L, 2); + luaL_checkstring(L, 1); + lua_pushcfunction(L, lax ? iter_auxlax : iter_auxstrict); + lua_pushvalue(L, 1); + lua_pushinteger(L, 0); + return 3; +} + +static int Lutf8_width (lua_State *L) { + int t = lua_type(L, 1); + int ambi_is_single = !lua_toboolean(L, 2); + int default_width = CAST(int, luaL_optinteger(L, 3, 0)); + if (t == LUA_TNUMBER) { + size_t chwidth = utf8_width(CAST(utfint, lua_tointeger(L, 1)), ambi_is_single); + if (chwidth == 0) chwidth = default_width; + lua_pushinteger(L, (lua_Integer)chwidth); + } else if (t != LUA_TSTRING) + return typeerror(L, 1, "number/string"); + else { + const char *e, *s = to_utf8(L, 1, &e); + int width = 0; + while (s < e) { + utfint ch = 0; + int chwidth; + s = utf8_safe_decode(L, s, &ch); + chwidth = utf8_width(ch, ambi_is_single); + width += chwidth == 0 ? default_width : chwidth; + } + lua_pushinteger(L, (lua_Integer)width); + } + return 1; +} + +static int Lutf8_widthindex (lua_State *L) { + const char *e, *s = check_utf8(L, 1, &e); + int width = CAST(int, luaL_checkinteger(L, 2)); + int ambi_is_single = !lua_toboolean(L, 3); + int default_width = CAST(int, luaL_optinteger(L, 4, 0)); + size_t idx = 1; + while (s < e) { + utfint ch = 0; + size_t chwidth; + s = utf8_safe_decode(L, s, &ch); + chwidth = utf8_width(ch, ambi_is_single); + if (chwidth == 0) chwidth = default_width; + width -= CAST(int, chwidth); + if (width <= 0) { + lua_pushinteger(L, idx); + lua_pushinteger(L, width + chwidth); + lua_pushinteger(L, chwidth); + return 3; + } + ++idx; + } + lua_pushinteger(L, (lua_Integer)idx); + return 1; +} + +static int Lutf8_ncasecmp (lua_State *L) { + const char *e1, *s1 = check_utf8(L, 1, &e1); + const char *e2, *s2 = check_utf8(L, 2, &e2); + while (s1 < e1 || s2 < e2) { + utfint ch1 = 0, ch2 = 0; + if (s1 == e1) + ch2 = 1; + else if (s2 == e2) + ch1 = 1; + else { + s1 = utf8_safe_decode(L, s1, &ch1); + s2 = utf8_safe_decode(L, s2, &ch2); + ch1 = utf8_tofold(ch1); + ch2 = utf8_tofold(ch2); + } + if (ch1 != ch2) { + lua_pushinteger(L, ch1 > ch2 ? 1 : -1); + return 1; + } + } + lua_pushinteger(L, 0); + return 1; +} + + +/* utf8 pattern matching implement */ + +#ifndef LUA_MAXCAPTURES +# define LUA_MAXCAPTURES 32 +#endif /* LUA_MAXCAPTURES */ + +#define CAP_UNFINISHED (-1) +#define CAP_POSITION (-2) + + +typedef struct MatchState { + int matchdepth; /* control for recursive depth (to avoid C stack overflow) */ + const char *src_init; /* init of source string */ + const char *src_end; /* end ('\0') of source string */ + const char *p_end; /* end ('\0') of pattern */ + lua_State *L; + int level; /* total number of captures (finished or unfinished) */ + struct { + const char *init; + ptrdiff_t len; + } capture[LUA_MAXCAPTURES]; +} MatchState; + +/* recursive function */ +static const char *match (MatchState *ms, const char *s, const char *p); + +/* maximum recursion depth for 'match' */ +#if !defined(MAXCCALLS) +#define MAXCCALLS 200 +#endif + +#define L_ESC '%' +#define SPECIALS "^$*+?.([%-" + +static int check_capture (MatchState *ms, int l) { + l -= '1'; + if (l < 0 || l >= ms->level || ms->capture[l].len == CAP_UNFINISHED) + return luaL_error(ms->L, "invalid capture index %%%d", l + 1); + return l; +} + +static int capture_to_close (MatchState *ms) { + int level = ms->level; + while (--level >= 0) + if (ms->capture[level].len == CAP_UNFINISHED) return level; + return luaL_error(ms->L, "invalid pattern capture"); +} + +static const char *classend (MatchState *ms, const char *p) { + utfint ch = 0; + p = utf8_safe_decode(ms->L, p, &ch); + switch (ch) { + case L_ESC: { + if (p == ms->p_end) + luaL_error(ms->L, "malformed pattern (ends with " LUA_QL("%%") ")"); + return utf8_next(p, ms->p_end); + } + case '[': { + if (*p == '^') p++; + do { /* look for a `]' */ + if (p == ms->p_end) + luaL_error(ms->L, "malformed pattern (missing " LUA_QL("]") ")"); + if (*(p++) == L_ESC && p < ms->p_end) + p++; /* skip escapes (e.g. `%]') */ + } while (*p != ']'); + return p+1; + } + default: { + return p; + } + } +} + +static int match_class (utfint c, utfint cl) { + int res; + switch (utf8_tolower(cl)) { +#define X(cls, name) case cls: res = utf8_is##name(c); break; + utf8_categories(X) +#undef X + case 'g' : res = utf8_isgraph(c); break; + case 'w' : res = utf8_isalnum(c); break; + case 'z' : res = (c == 0); break; /* deprecated option */ + default: return (cl == c); + } + return (utf8_islower(cl) ? res : !res); +} + +static int matchbracketclass (MatchState *ms, utfint c, const char *p, const char *ec) { + int sig = 1; + assert(*p == '['); + if (*++p == '^') { + sig = 0; + p++; /* skip the `^' */ + } + while (p < ec) { + utfint ch = 0; + p = utf8_safe_decode(ms->L, p, &ch); + if (ch == L_ESC) { + p = utf8_safe_decode(ms->L, p, &ch); + if (match_class(c, ch)) + return sig; + } else { + utfint next = 0; + const char *np = utf8_safe_decode(ms->L, p, &next); + if (next == '-' && np < ec) { + p = utf8_safe_decode(ms->L, np, &next); + if (ch <= c && c <= next) + return sig; + } + else if (ch == c) return sig; + } + } + return !sig; +} + +static int singlematch (MatchState *ms, const char *s, const char *p, const char *ep) { + if (s >= ms->src_end) + return 0; + else { + utfint ch=0, pch=0; + utf8_safe_decode(ms->L, s, &ch); + p = utf8_safe_decode(ms->L, p, &pch); + switch (pch) { + case '.': return 1; /* matches any char */ + case L_ESC: utf8_safe_decode(ms->L, p, &pch); + return match_class(ch, pch); + case '[': return matchbracketclass(ms, ch, p-1, ep-1); + default: return pch == ch; + } + } +} + +static const char *matchbalance (MatchState *ms, const char *s, const char **p) { + utfint ch=0, begin=0, end=0; + *p = utf8_safe_decode(ms->L, *p, &begin); + if (*p >= ms->p_end) + luaL_error(ms->L, "malformed pattern " + "(missing arguments to " LUA_QL("%%b") ")"); + *p = utf8_safe_decode(ms->L, *p, &end); + s = utf8_safe_decode(ms->L, s, &ch); + if (ch != begin) return NULL; + else { + int cont = 1; + while (s < ms->src_end) { + s = utf8_safe_decode(ms->L, s, &ch); + if (ch == end) { + if (--cont == 0) return s; + } + else if (ch == begin) cont++; + } + } + return NULL; /* string ends out of balance */ +} + +static const char *max_expand (MatchState *ms, const char *s, const char *p, const char *ep) { + const char *m = s; /* matched end of single match p */ + while (singlematch(ms, m, p, ep)) + m = utf8_next(m, ms->src_end); + /* keeps trying to match with the maximum repetitions */ + while (s <= m) { + const char *res = match(ms, m, ep+1); + if (res) return res; + /* else didn't match; reduce 1 repetition to try again */ + if (s == m) break; + m = utf8_prev(s, m); + } + return NULL; +} + +static const char *min_expand (MatchState *ms, const char *s, const char *p, const char *ep) { + for (;;) { + const char *res = match(ms, s, ep+1); + if (res != NULL) + return res; + else if (singlematch(ms, s, p, ep)) + s = utf8_next(s, ms->src_end); /* try with one more repetition */ + else return NULL; + } +} + +static const char *start_capture (MatchState *ms, const char *s, const char *p, int what) { + const char *res; + int level = ms->level; + if (level >= LUA_MAXCAPTURES) luaL_error(ms->L, "too many captures"); + ms->capture[level].init = s; + ms->capture[level].len = what; + ms->level = level+1; + if ((res=match(ms, s, p)) == NULL) /* match failed? */ + ms->level--; /* undo capture */ + return res; +} + +static const char *end_capture (MatchState *ms, const char *s, const char *p) { + int l = capture_to_close(ms); + const char *res; + ms->capture[l].len = s - ms->capture[l].init; /* close capture */ + if ((res = match(ms, s, p)) == NULL) /* match failed? */ + ms->capture[l].len = CAP_UNFINISHED; /* undo capture */ + return res; +} + +static const char *match_capture (MatchState *ms, const char *s, int l) { + size_t len; + l = check_capture(ms, l); + len = ms->capture[l].len; + if ((size_t)(ms->src_end-s) >= len && + memcmp(ms->capture[l].init, s, len) == 0) + return s+len; + else return NULL; +} + +static const char *match (MatchState *ms, const char *s, const char *p) { + if (ms->matchdepth-- == 0) + luaL_error(ms->L, "pattern too complex"); + init: /* using goto's to optimize tail recursion */ + if (p != ms->p_end) { /* end of pattern? */ + utfint ch = 0; + utf8_safe_decode(ms->L, p, &ch); + switch (ch) { + case '(': { /* start capture */ + if (*(p + 1) == ')') /* position capture? */ + s = start_capture(ms, s, p + 2, CAP_POSITION); + else + s = start_capture(ms, s, p + 1, CAP_UNFINISHED); + break; + } + case ')': { /* end capture */ + s = end_capture(ms, s, p + 1); + break; + } + case '$': { + if ((p + 1) != ms->p_end) /* is the `$' the last char in pattern? */ + goto dflt; /* no; go to default */ + s = (s == ms->src_end) ? s : NULL; /* check end of string */ + break; + } + case L_ESC: { /* escaped sequence not in the format class[*+?-]? */ + const char *prev_p = p; + p = utf8_safe_decode(ms->L, p+1, &ch); + switch (ch) { + case 'b': { /* balanced string? */ + s = matchbalance(ms, s, &p); + if (s != NULL) + goto init; /* return match(ms, s, p + 4); */ + /* else fail (s == NULL) */ + break; + } + case 'f': { /* frontier? */ + const char *ep; utfint previous = 0, current = 0; + if (*p != '[') + luaL_error(ms->L, "missing " LUA_QL("[") " after " + LUA_QL("%%f") " in pattern"); + ep = classend(ms, p); /* points to what is next */ + if (s != ms->src_init) + utf8_decode(utf8_prev(ms->src_init, s), &previous, 0); + if (s != ms->src_end) + utf8_decode(s, ¤t, 0); + if (!matchbracketclass(ms, previous, p, ep - 1) && + matchbracketclass(ms, current, p, ep - 1)) { + p = ep; goto init; /* return match(ms, s, ep); */ + } + s = NULL; /* match failed */ + break; + } + case '0': case '1': case '2': case '3': + case '4': case '5': case '6': case '7': + case '8': case '9': { /* capture results (%0-%9)? */ + s = match_capture(ms, s, ch); + if (s != NULL) goto init; /* return match(ms, s, p + 2) */ + break; + } + default: p = prev_p; goto dflt; + } + break; + } + default: dflt: { /* pattern class plus optional suffix */ + const char *ep = classend(ms, p); /* points to optional suffix */ + /* does not match at least once? */ + if (!singlematch(ms, s, p, ep)) { + if (*ep == '*' || *ep == '?' || *ep == '-') { /* accept empty? */ + p = ep + 1; goto init; /* return match(ms, s, ep + 1); */ + } else /* '+' or no suffix */ + s = NULL; /* fail */ + } else { /* matched once */ + const char *next_s = utf8_next(s, ms->src_end); + switch (*ep) { /* handle optional suffix */ + case '?': { /* optional */ + const char *res; + const char *next_ep = utf8_next(ep, ms->p_end); + if ((res = match(ms, next_s, next_ep)) != NULL) + s = res; + else { + p = next_ep; goto init; /* else return match(ms, s, ep + 1); */ + } + break; + } + case '+': /* 1 or more repetitions */ + s = next_s; /* 1 match already done */ + /* fall through */ + case '*': /* 0 or more repetitions */ + s = max_expand(ms, s, p, ep); + break; + case '-': /* 0 or more repetitions (minimum) */ + s = min_expand(ms, s, p, ep); + break; + default: /* no suffix */ + s = next_s; p = ep; goto init; /* return match(ms, s + 1, ep); */ + } + } + break; + } + } + } + ms->matchdepth++; + return s; +} + +static const char *lmemfind (const char *s1, size_t l1, const char *s2, size_t l2) { + if (l2 == 0) return s1; /* empty strings are everywhere */ + else if (l2 > l1) return NULL; /* avoids a negative `l1' */ + else { + const char *init; /* to search for a `*s2' inside `s1' */ + l2--; /* 1st char will be checked by `memchr' */ + l1 = l1-l2; /* `s2' cannot be found after that */ + while (l1 > 0 && (init = (const char *)memchr(s1, *s2, l1)) != NULL) { + init++; /* 1st char is already checked */ + if (memcmp(init, s2+1, l2) == 0) + return init-1; + else { /* correct `l1' and `s1' to try again */ + l1 -= init-s1; + s1 = init; + } + } + return NULL; /* not found */ + } +} + +static int get_index (const char *p, const char *s, const char *e) { + int idx; + for (idx = 0; s < e && s < p; ++idx) + s = utf8_next(s, e); + return s == p ? idx : idx - 1; +} + +static void push_onecapture (MatchState *ms, int i, const char *s, const char *e) { + if (i >= ms->level) { + if (i == 0) /* ms->level == 0, too */ + lua_pushlstring(ms->L, s, e - s); /* add whole match */ + else + luaL_error(ms->L, "invalid capture index"); + } else { + ptrdiff_t l = ms->capture[i].len; + if (l == CAP_UNFINISHED) luaL_error(ms->L, "unfinished capture"); + if (l == CAP_POSITION) { + int idx = get_index(ms->capture[i].init, ms->src_init, ms->src_end); + lua_pushinteger(ms->L, idx+1); + } else + lua_pushlstring(ms->L, ms->capture[i].init, l); + } +} + +static int push_captures (MatchState *ms, const char *s, const char *e) { + int i; + int nlevels = (ms->level == 0 && s) ? 1 : ms->level; + luaL_checkstack(ms->L, nlevels, "too many captures"); + for (i = 0; i < nlevels; i++) + push_onecapture(ms, i, s, e); + return nlevels; /* number of strings pushed */ +} + +/* check whether pattern has no special characters */ +static int nospecials (const char *p, const char * ep) { + while (p < ep) { + if (strpbrk(p, SPECIALS)) + return 0; /* pattern has a special character */ + p += strlen(p) + 1; /* may have more after \0 */ + } + return 1; /* no special chars found */ +} + + +/* utf8 pattern matching interface */ + +static int find_aux (lua_State *L, int find) { + const char *es, *s = check_utf8(L, 1, &es); + const char *ep, *p = check_utf8(L, 2, &ep); + lua_Integer idx = luaL_optinteger(L, 3, 1); + const char *init; + if (!idx) idx = 1; + init = utf8_relat(s, es, CAST(int, idx)); + if (init == NULL) { + if (idx > 0) { + lua_pushnil(L); /* cannot find anything */ + return 1; + } + init = s; + } + /* explicit request or no special characters? */ + if (find && (lua_toboolean(L, 4) || nospecials(p, ep))) { + /* do a plain search */ + const char *s2 = lmemfind(init, es-init, p, ep-p); + if (s2) { + const char *e2 = s2 + (ep - p); + if (iscont(e2)) e2 = utf8_next(e2, es); + lua_pushinteger(L, idx = get_index(s2, s, es) + 1); + lua_pushinteger(L, idx + get_index(e2, s2, es) - 1); + return 2; + } + } else { + MatchState ms; + int anchor = (*p == '^'); + if (anchor) p++; /* skip anchor character */ + if (idx < 0) idx += utf8_length(s, es)+1; /* TODO not very good */ + ms.L = L; + ms.matchdepth = MAXCCALLS; + ms.src_init = s; + ms.src_end = es; + ms.p_end = ep; + do { + const char *res; + ms.level = 0; + assert(ms.matchdepth == MAXCCALLS); + if ((res=match(&ms, init, p)) != NULL) { + if (find) { + lua_pushinteger(L, idx); /* start */ + lua_pushinteger(L, idx + utf8_length(init, res) - 1); /* end */ + return push_captures(&ms, NULL, 0) + 2; + } else + return push_captures(&ms, init, res); + } + if (init == es) break; + idx += 1; + init = utf8_next(init, es); + } while (init <= es && !anchor); + } + lua_pushnil(L); /* not found */ + return 1; +} + +static int Lutf8_find (lua_State *L) { return find_aux(L, 1); } +static int Lutf8_match (lua_State *L) { return find_aux(L, 0); } + +static int gmatch_aux (lua_State *L) { + MatchState ms; + const char *es, *s = check_utf8(L, lua_upvalueindex(1), &es); + const char *ep, *p = check_utf8(L, lua_upvalueindex(2), &ep); + const char *src; + ms.L = L; + ms.matchdepth = MAXCCALLS; + ms.src_init = s; + ms.src_end = es; + ms.p_end = ep; + for (src = s + (size_t)lua_tointeger(L, lua_upvalueindex(3)); + src <= ms.src_end; + src = utf8_next(src, ms.src_end)) { + const char *e; + ms.level = 0; + assert(ms.matchdepth == MAXCCALLS); + if ((e = match(&ms, src, p)) != NULL) { + lua_Integer newstart = e-s; + if (e == src) newstart++; /* empty match? go at least one position */ + lua_pushinteger(L, newstart); + lua_replace(L, lua_upvalueindex(3)); + return push_captures(&ms, src, e); + } + if (src == ms.src_end) break; + } + return 0; /* not found */ +} + +static int Lutf8_gmatch (lua_State *L) { + luaL_checkstring(L, 1); + luaL_checkstring(L, 2); + lua_settop(L, 2); + lua_pushinteger(L, 0); + lua_pushcclosure(L, gmatch_aux, 3); + return 1; +} + +static void add_s (MatchState *ms, luaL_Buffer *b, const char *s, const char *e) { + const char *new_end, *news = to_utf8(ms->L, 3, &new_end); + while (news < new_end) { + utfint ch = 0; + news = utf8_safe_decode(ms->L, news, &ch); + if (ch != L_ESC) + add_utf8char(b, ch); + else { + news = utf8_safe_decode(ms->L, news, &ch); /* skip ESC */ + if (!utf8_isdigit(ch)) { + if (ch != L_ESC) + luaL_error(ms->L, "invalid use of " LUA_QL("%c") + " in replacement string", L_ESC); + add_utf8char(b, ch); + } else if (ch == '0') + luaL_addlstring(b, s, e-s); + else { + push_onecapture(ms, ch-'1', s, e); + luaL_addvalue(b); /* add capture to accumulated result */ + } + } + } +} + +static void add_value (MatchState *ms, luaL_Buffer *b, const char *s, const char *e, int tr) { + lua_State *L = ms->L; + switch (tr) { + case LUA_TFUNCTION: { + int n; + lua_pushvalue(L, 3); + n = push_captures(ms, s, e); + lua_call(L, n, 1); + break; + } + case LUA_TTABLE: { + push_onecapture(ms, 0, s, e); + lua_gettable(L, 3); + break; + } + default: { /* LUA_TNUMBER or LUA_TSTRING */ + add_s(ms, b, s, e); + return; + } + } + if (!lua_toboolean(L, -1)) { /* nil or false? */ + lua_pop(L, 1); + lua_pushlstring(L, s, e - s); /* keep original text */ + } else if (!lua_isstring(L, -1)) + luaL_error(L, "invalid replacement value (a %s)", luaL_typename(L, -1)); + luaL_addvalue(b); /* add result to accumulator */ +} + +static int Lutf8_gsub (lua_State *L) { + const char *es, *s = check_utf8(L, 1, &es); + const char *ep, *p = check_utf8(L, 2, &ep); + int tr = lua_type(L, 3); + lua_Integer max_s = luaL_optinteger(L, 4, (es-s)+1); + int anchor = (*p == '^'); + lua_Integer n = 0; + MatchState ms; + luaL_Buffer b; + luaL_argcheck(L, tr == LUA_TNUMBER || tr == LUA_TSTRING || + tr == LUA_TFUNCTION || tr == LUA_TTABLE, 3, + "string/function/table expected"); + luaL_buffinit(L, &b); + if (anchor) p++; /* skip anchor character */ + ms.L = L; + ms.matchdepth = MAXCCALLS; + ms.src_init = s; + ms.src_end = es; + ms.p_end = ep; + while (n < max_s) { + const char *e; + ms.level = 0; + assert(ms.matchdepth == MAXCCALLS); + e = match(&ms, s, p); + if (e) { + n++; + add_value(&ms, &b, s, e, tr); + } + if (e && e > s) /* non empty match? */ + s = e; /* skip it */ + else if (s < es) { + utfint ch = 0; + s = utf8_safe_decode(L, s, &ch); + add_utf8char(&b, ch); + } else break; + if (anchor) break; + } + luaL_addlstring(&b, s, es-s); + luaL_pushresult(&b); + lua_pushinteger(L, n); /* number of substitutions */ + return 2; +} + + +/* lua module import interface */ + +#if LUA_VERSION_NUM >= 502 +static const char UTF8PATT[] = "[\0-\x7F\xC2-\xF4][\x80-\xBF]*"; +#else +static const char UTF8PATT[] = "[%z\1-\x7F\xC2-\xF4][\x80-\xBF]*"; +#endif + +int luaopen_utf8extra (lua_State *L) { + luaL_Reg libs[] = { +#define ENTRY(name) { #name, Lutf8_##name } + ENTRY(offset), + ENTRY(codes), + ENTRY(codepoint), + + ENTRY(len), + ENTRY(sub), + ENTRY(reverse), + ENTRY(lower), + ENTRY(upper), + ENTRY(title), + ENTRY(fold), + ENTRY(byte), + ENTRY(char), + ENTRY(escape), + ENTRY(insert), + ENTRY(remove), + ENTRY(charpos), + ENTRY(next), + ENTRY(width), + ENTRY(widthindex), + ENTRY(ncasecmp), + ENTRY(find), + ENTRY(gmatch), + ENTRY(gsub), + ENTRY(match), +#undef ENTRY + { NULL, NULL } + }; + + luaL_newlib(L, libs); + + lua_pushlstring(L, UTF8PATT, sizeof(UTF8PATT)-1); + lua_setfield(L, -2, "charpattern"); + + return 1; +} diff --git a/src/dirmonitor.c b/src/dirmonitor.c deleted file mode 100644 index fe3cc41c..00000000 --- a/src/dirmonitor.c +++ /dev/null @@ -1,61 +0,0 @@ -#include -#include - -#include - -#ifndef __amigaos4__ -#define DMON_IMPL -#include "dmon.h" -#include "dmon_extra.h" -#endif - -#include "dirmonitor.h" - -static void send_sdl_event(dmon_watch_id watch_id, dmon_action action, const char *filepath) { - SDL_Event ev; - const int size = strlen(filepath) + 1; - /* The string allocated below should be deallocated as soon as the event is - treated in the SDL main loop. */ - char *new_filepath = malloc(size); - if (!new_filepath) return; - memcpy(new_filepath, filepath, size); -#ifdef _WIN32 - for (int i = 0; i < size; i++) { - if (new_filepath[i] == '/') { - new_filepath[i] = '\\'; - } - } -#endif - SDL_zero(ev); - ev.type = SDL_USEREVENT; - ev.user.code = ((watch_id.id & 0xffff) << 16) | (action & 0xffff); - ev.user.data1 = new_filepath; - SDL_PushEvent(&ev); -} - -void dirmonitor_init() { - //dmon_init(); - /* In theory we should register our user event but since we - have just one type of user event this is not really needed. */ - /* sdl_dmon_event_type = SDL_RegisterEvents(1); */ -} - -void dirmonitor_deinit() { - //dmon_deinit(); -} - -void dirmonitor_watch_callback(dmon_watch_id watch_id, dmon_action action, const char *rootdir, - const char *filepath, const char *oldfilepath, void *user) -{ - (void) rootdir; - (void) user; - switch (action) { - case DMON_ACTION_MOVE: - //send_sdl_event(watch_id, DMON_ACTION_DELETE, oldfilepath); - //send_sdl_event(watch_id, DMON_ACTION_CREATE, filepath); - break; - //default: - //send_sdl_event(watch_id, action, filepath); - } -} - diff --git a/src/dirmonitor.h b/src/dirmonitor.h deleted file mode 100644 index 074a9ae8..00000000 --- a/src/dirmonitor.h +++ /dev/null @@ -1,15 +0,0 @@ -#ifndef DIRMONITOR_H -#define DIRMONITOR_H - -#include - -#include "dmon.h" -#include "dmon_extra.h" - -void dirmonitor_init(); -void dirmonitor_deinit(); -void dirmonitor_watch_callback(dmon_watch_id watch_id, dmon_action action, const char *rootdir, - const char *filepath, const char *oldfilepath, void *user); - -#endif - diff --git a/src/dirmonitor.o b/src/dirmonitor.o new file mode 100644 index 00000000..42f5e61e Binary files /dev/null and b/src/dirmonitor.o differ diff --git a/src/main.c b/src/main.c index fea71c91..405b9dd8 100644 --- a/src/main.c +++ b/src/main.c @@ -12,7 +12,7 @@ #ifdef _WIN32 #include -#elif __linux__ +#elif __linux__ || __FreeBSD__ #include #include #elif __APPLE__ @@ -27,8 +27,6 @@ UBYTE VString[] = VERSTAG; #endif -#include "dirmonitor.h" - SDL_Window *window; @@ -99,6 +97,28 @@ void set_macos_bundle_resources(lua_State *L); #endif #endif +#ifndef LITE_ARCH_TUPLE + #if __x86_64__ || _WIN64 || __MINGW64__ + #define ARCH_PROCESSOR "x86_64" + #elif __aarch64__ + #define ARCH_PROCESSOR "aarch64" + #elif __arm__ + #define ARCH_PROCESSOR "arm" + #else + #define ARCH_PROCESSOR "x86" + #endif + #if _WIN32 + #define ARCH_PLATFORM "windows" + #elif __linux__ + #define ARCH_PLATFORM "linux" + #elif __APPLE__ + #define ARCH_PLATFORM "darwin" + #else + #error "Please define -DLITE_ARCH_TUPLE." + #endif + #define LITE_ARCH_TUPLE ARCH_PROCESSOR "-" ARCH_PLATFORM +#endif + int main(int argc, char **argv) { #ifdef _WIN32 HINSTANCE lib = LoadLibrary("user32.dll"); @@ -110,7 +130,10 @@ int main(int argc, char **argv) { signal(SIGPIPE, SIG_IGN); #endif - SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS); + if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS) != 0) { + fprintf(stderr, "Error initializing sdl: %s", SDL_GetError()); + exit(1); + } SDL_EnableScreenSaver(); SDL_EventState(SDL_DROPFILE, SDL_ENABLE); atexit(SDL_Quit); @@ -122,17 +145,34 @@ int main(int argc, char **argv) { SDL_SetHint(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1"); #endif +#if SDL_VERSION_ATLEAST(2, 0, 8) + /* This hint tells SDL to respect borderless window as a normal window. + ** For example, the window will sit right on top of the taskbar instead + ** of obscuring it. */ + SDL_SetHint("SDL_BORDERLESS_WINDOWED_STYLE", "1"); +#endif +#if SDL_VERSION_ATLEAST(2, 0, 12) + /* This hint tells SDL to allow the user to resize a borderless windoow. + ** It also enables aero-snap on Windows apparently. */ + SDL_SetHint("SDL_BORDERLESS_RESIZABLE_STYLE", "1"); +#endif +#if SDL_VERSION_ATLEAST(2, 0, 9) + SDL_SetHint("SDL_MOUSE_DOUBLE_CLICK_RADIUS", "4"); +#endif + SDL_DisplayMode dm; SDL_GetCurrentDisplayMode(0, &dm); - // dirmonitor_init(); - window = SDL_CreateWindow( "", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, dm.w * 0.8, dm.h * 0.8, SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_HIDDEN); SDL_SetWindowDisplayMode(window, &dm); - + init_window_icon(); + if (!window) { + fprintf(stderr, "Error creating lite-xl window: %s", SDL_GetError()); + exit(1); + } ren_init(window); lua_State *L; @@ -152,6 +192,9 @@ init_lua: lua_pushstring(L, SDL_GetPlatform()); lua_setglobal(L, "PLATFORM"); + lua_pushstring(L, LITE_ARCH_TUPLE); + lua_setglobal(L, "ARCH"); + lua_pushnumber(L, get_scale()); lua_setglobal(L, "SCALE"); @@ -214,7 +257,6 @@ init_lua: lua_close(L); ren_free_window_resources(); - // dirmonitor_deinit(); return EXIT_SUCCESS; } diff --git a/src/meson.build b/src/meson.build index 1eaf87fd..fa4a1390 100644 --- a/src/meson.build +++ b/src/meson.build @@ -4,13 +4,46 @@ lite_sources = [ 'api/regex.c', 'api/system.c', 'api/process.c', - 'dirmonitor.c', + 'api/utf8.c', 'renderer.c', 'renwindow.c', 'rencache.c', 'main.c', ] +# dirmonitor backend +if get_option('dirmonitor_backend') == '' + if cc.has_function('inotify_init', prefix : '#include') + dirmonitor_backend = 'inotify' + elif cc.has_function('kqueue', prefix : '#include') + dirmonitor_backend = 'kqueue' + elif dependency('libkqueue', required : false).found() + dirmonitor_backend = 'kqueue' + elif host_machine.system() == 'windows' + dirmonitor_backend = 'win32' + else + dirmonitor_backend = 'dummy' + warning('no suitable backend found, defaulting to dummy backend') + endif +else + dirmonitor_backend = get_option('dirmonitor_backend') +endif + +message('dirmonitor_backend: @0@'.format(dirmonitor_backend)) + +if dirmonitor_backend == 'kqueue' + libkqueue_dep = dependency('libkqueue', required : false) + if libkqueue_dep.found() + lite_deps += libkqueue_dep + endif +endif + +lite_sources += [ + 'api/dirmonitor.c', + 'api/dirmonitor/' + dirmonitor_backend + '.c', +] + + lite_rc = [] if host_machine.system() == 'windows' windows = import('windows') diff --git a/src/rencache.c b/src/rencache.c index 5d464226..c847ce34 100644 --- a/src/rencache.c +++ b/src/rencache.c @@ -3,6 +3,7 @@ #include #include #include +#include #include #include "rencache.h" @@ -28,7 +29,7 @@ typedef struct { RenColor color; RenFont *fonts[FONT_FALLBACK_MAX]; float text_x; - char text[0]; + char text[]; } Command; static unsigned cells_buf1[CELLS_X * CELLS_Y]; @@ -95,7 +96,7 @@ static Command* push_command(int type, int size) { return NULL; } command_buf_idx = n; - memset(cmd, 0, COMMAND_BARE_SIZE); + memset(cmd, 0, size); cmd->type = type; cmd->size = size; return cmd; @@ -134,7 +135,7 @@ void rencache_draw_rect(RenRect rect, RenColor color) { } } -float rencache_draw_text(lua_State *L, RenFont **fonts, const char *text, float x, int y, RenColor color) +float rencache_draw_text(RenFont **fonts, const char *text, float x, int y, RenColor color) { float width = ren_font_group_get_width(fonts, text); RenRect rect = { x, y, (int)width, ren_font_group_get_height(fonts) }; @@ -159,7 +160,7 @@ void rencache_invalidate(void) { } -void rencache_begin_frame(lua_State *L) { +void rencache_begin_frame() { /* reset all cells if the screen width/height has changed */ int w, h; ren_get_size(&w, &h); @@ -200,7 +201,7 @@ static void push_rect(RenRect r, int *count) { } -void rencache_end_frame(lua_State *L) { +void rencache_end_frame() { /* update cells from commands */ Command *cmd = NULL; RenRect cr = screen_rect; diff --git a/src/rencache.h b/src/rencache.h index 5f817403..251bc030 100644 --- a/src/rencache.h +++ b/src/rencache.h @@ -8,11 +8,10 @@ void rencache_show_debug(bool enable); void rencache_set_clip_rect(RenRect rect); void rencache_draw_rect(RenRect rect, RenColor color); -float rencache_draw_text(lua_State *L, RenFont **font, -const char *text, float x, int y, RenColor color); +float rencache_draw_text(RenFont **font, const char *text, float x, int y, RenColor color); void rencache_invalidate(void); -void rencache_begin_frame(lua_State *L); -void rencache_end_frame(lua_State *L); +void rencache_begin_frame(); +void rencache_end_frame(); #endif diff --git a/src/renderer.c b/src/renderer.c index 190d64f5..30592852 100644 --- a/src/renderer.c +++ b/src/renderer.c @@ -18,6 +18,9 @@ static RenWindow window_renderer = {0}; static FT_Library library; +// draw_rect_surface is used as a 1x1 surface to simplify ren_draw_rect with blending +static SDL_Surface *draw_rect_surface; + static void* check_alloc(void *ptr) { if (!ptr) { fprintf(stderr, "Fatal error: memory allocation failed\n"); @@ -36,38 +39,40 @@ typedef struct { typedef struct { SDL_Surface* surface; - GlyphMetric metrics[MAX_GLYPHSET]; + GlyphMetric metrics[MAX_GLYPHSET]; } GlyphSet; typedef struct RenFont { FT_Face face; GlyphSet* sets[SUBPIXEL_BITMAPS_CACHED][MAX_LOADABLE_GLYPHSETS]; float size, space_advance, tab_advance; - short max_height; + unsigned short max_height, baseline, height; ERenFontAntialiasing antialiasing; ERenFontHinting hinting; unsigned char style; - char path[0]; + unsigned short underline_thickness; + char path[1]; } RenFont; static const char* utf8_to_codepoint(const char *p, unsigned *dst) { + const unsigned char *up = (unsigned char*)p; unsigned res, n; switch (*p & 0xf0) { - case 0xf0 : res = *p & 0x07; n = 3; break; - case 0xe0 : res = *p & 0x0f; n = 2; break; + case 0xf0 : res = *up & 0x07; n = 3; break; + case 0xe0 : res = *up & 0x0f; n = 2; break; case 0xd0 : - case 0xc0 : res = *p & 0x1f; n = 1; break; - default : res = *p; n = 0; break; + case 0xc0 : res = *up & 0x1f; n = 1; break; + default : res = *up; n = 0; break; } while (n--) { - res = (res << 6) | (*(++p) & 0x3f); + res = (res << 6) | (*(++up) & 0x3f); } *dst = res; - return p + 1; + return (const char*)up + 1; } static int font_set_load_options(RenFont* font) { - int load_target = font->antialiasing == FONT_ANTIALIASING_NONE ? FT_LOAD_TARGET_MONO + int load_target = font->antialiasing == FONT_ANTIALIASING_NONE ? FT_LOAD_TARGET_MONO : (font->hinting == FONT_HINTING_SLIGHT ? FT_LOAD_TARGET_LIGHT : FT_LOAD_TARGET_NORMAL); int hinting = font->hinting == FONT_HINTING_NONE ? FT_LOAD_NO_HINTING : FT_LOAD_FORCE_AUTOHINT; return load_target | hinting; @@ -96,6 +101,8 @@ static int font_set_render_options(RenFont* font) { static int font_set_style(FT_Outline* outline, int x_translation, unsigned char style) { FT_Outline_Translate(outline, x_translation, 0 ); + if (style & FONT_STYLE_SMOOTH) + FT_Outline_Embolden(outline, 1 << 5); if (style & FONT_STYLE_BOLD) FT_Outline_EmboldenXY(outline, 1 << 5, 0); if (style & FONT_STYLE_ITALIC) { @@ -114,8 +121,10 @@ static void font_load_glyphset(RenFont* font, int idx) { font->sets[j][idx] = set; for (int i = 0; i < MAX_GLYPHSET; ++i) { int glyph_index = FT_Get_Char_Index(font->face, i + idx * MAX_GLYPHSET); - if (!glyph_index || FT_Load_Glyph(font->face, glyph_index, load_option | FT_LOAD_BITMAP_METRICS_ONLY) || font_set_style(&font->face->glyph->outline, j * (64 / SUBPIXEL_BITMAPS_CACHED), font->style) || FT_Render_Glyph(font->face->glyph, render_option)) + if (!glyph_index || FT_Load_Glyph(font->face, glyph_index, load_option | FT_LOAD_BITMAP_METRICS_ONLY) + || font_set_style(&font->face->glyph->outline, j * (64 / SUBPIXEL_BITMAPS_CACHED), font->style) || FT_Render_Glyph(font->face->glyph, render_option)) { continue; + } FT_GlyphSlot slot = font->face->glyph; int glyph_width = slot->bitmap.width / byte_width; if (font->antialiasing == FONT_ANTIALIASING_NONE) @@ -123,11 +132,18 @@ static void font_load_glyphset(RenFont* font, int idx) { set->metrics[i] = (GlyphMetric){ pen_x, pen_x + glyph_width, 0, slot->bitmap.rows, true, slot->bitmap_left, slot->bitmap_top, (slot->advance.x + slot->lsb_delta - slot->rsb_delta) / 64.0f}; pen_x += glyph_width; font->max_height = slot->bitmap.rows > font->max_height ? slot->bitmap.rows : font->max_height; + // In order to fix issues with monospacing; we need the unhinted xadvance; as FreeType doesn't correctly report the hinted advance for spaces on monospace fonts (like RobotoMono). See #843. + if (!glyph_index || FT_Load_Glyph(font->face, glyph_index, (load_option | FT_LOAD_BITMAP_METRICS_ONLY | FT_LOAD_NO_HINTING) & ~FT_LOAD_FORCE_AUTOHINT) + || font_set_style(&font->face->glyph->outline, j * (64 / SUBPIXEL_BITMAPS_CACHED), font->style) || FT_Render_Glyph(font->face->glyph, render_option)) { + continue; + } + slot = font->face->glyph; + set->metrics[i].xadvance = slot->advance.x / 64.0f; } if (pen_x == 0) continue; set->surface = check_alloc(SDL_CreateRGBSurface(0, pen_x, font->max_height, font->antialiasing == FONT_ANTIALIASING_SUBPIXEL ? 24 : 8, 0, 0, 0, 0)); - unsigned char* pixels = set->surface->pixels; + uint8_t* pixels = set->surface->pixels; for (int i = 0; i < MAX_GLYPHSET; ++i) { int glyph_index = FT_Get_Char_Index(font->face, i + idx * MAX_GLYPHSET); if (!glyph_index || FT_Load_Glyph(font->face, glyph_index, load_option)) @@ -135,12 +151,12 @@ static void font_load_glyphset(RenFont* font, int idx) { FT_GlyphSlot slot = font->face->glyph; font_set_style(&slot->outline, (64 / bitmaps_cached) * j, font->style); if (FT_Render_Glyph(slot, render_option)) - continue; - for (int line = 0; line < slot->bitmap.rows; ++line) { + continue; + for (unsigned int line = 0; line < slot->bitmap.rows; ++line) { int target_offset = set->surface->pitch * line + set->metrics[i].x0 * byte_width; int source_offset = line * slot->bitmap.pitch; if (font->antialiasing == FONT_ANTIALIASING_NONE) { - for (int column = 0; column < slot->bitmap.width; ++column) { + for (unsigned int column = 0; column < slot->bitmap.width; ++column) { int current_source_offset = source_offset + (column / 8); int source_pixel = slot->bitmap.buffer[current_source_offset]; pixels[++target_offset] = ((source_pixel >> (7 - (column % 8))) & 0x1) << 7; @@ -160,6 +176,9 @@ static GlyphSet* font_get_glyphset(RenFont* font, unsigned int codepoint, int su } static RenFont* font_group_get_glyph(GlyphSet** set, GlyphMetric** metric, RenFont** fonts, unsigned int codepoint, int bitmap_index) { + if (!metric) { + return NULL; + } if (bitmap_index < 0) bitmap_index += SUBPIXEL_BITMAPS_CACHED; for (int i = 0; i < FONT_FALLBACK_MAX && fonts[i]; ++i) { @@ -168,11 +187,24 @@ static RenFont* font_group_get_glyph(GlyphSet** set, GlyphMetric** metric, RenFo if ((*metric)->loaded || codepoint < 0xFF) return fonts[i]; } - if (!(*metric)->loaded && codepoint > 0xFF && codepoint != 0x25A1) + if (*metric && !(*metric)->loaded && codepoint > 0xFF && codepoint != 0x25A1) return font_group_get_glyph(set, metric, fonts, 0x25A1, bitmap_index); return fonts[0]; } +static void font_clear_glyph_cache(RenFont* font) { + for (int i = 0; i < SUBPIXEL_BITMAPS_CACHED; ++i) { + for (int j = 0; j < MAX_GLYPHSET; ++j) { + if (font->sets[i][j]) { + if (font->sets[i][j]->surface) + SDL_FreeSurface(font->sets[i][j]->surface); + free(font->sets[i][j]); + font->sets[i][j] = NULL; + } + } + } +} + RenFont* ren_font_load(const char* path, float size, ERenFontAntialiasing antialiasing, ERenFontHinting hinting, unsigned char style) { FT_Face face; if (FT_New_Face( library, path, 0, &face)) @@ -185,51 +217,80 @@ RenFont* ren_font_load(const char* path, float size, ERenFontAntialiasing antial strcpy(font->path, path); font->face = face; font->size = size; + font->height = (short)((face->height / (float)face->units_per_EM) * font->size); + font->baseline = (short)((face->ascender / (float)face->units_per_EM) * font->size); font->antialiasing = antialiasing; font->hinting = hinting; font->style = style; - font->space_advance = (int)font_get_glyphset(font, ' ', 0)->metrics[' '].xadvance; + + if(FT_IS_SCALABLE(face)) + font->underline_thickness = (unsigned short)((face->underline_thickness / (float)face->units_per_EM) * font->size); + if(!font->underline_thickness) font->underline_thickness = ceil((double) font->height / 14.0); + + if (FT_Load_Char(face, ' ', font_set_load_options(font))) + goto failure; + font->space_advance = face->glyph->advance.x / 64.0f; font->tab_advance = font->space_advance * 2; return font; - failure: + failure: FT_Done_Face(face); return NULL; } -RenFont* ren_font_copy(RenFont* font, float size) { - return ren_font_load(font->path, size, font->antialiasing, font->hinting, font->style); +RenFont* ren_font_copy(RenFont* font, float size, ERenFontAntialiasing antialiasing, ERenFontHinting hinting, int style) { + antialiasing = antialiasing == -1 ? font->antialiasing : antialiasing; + hinting = hinting == -1 ? font->hinting : hinting; + style = style == -1 ? font->style : style; + + return ren_font_load(font->path, size, antialiasing, hinting, style); +} + +const char* ren_font_get_path(RenFont *font) { + return font->path; } void ren_font_free(RenFont* font) { - for (int i = 0; i < SUBPIXEL_BITMAPS_CACHED; ++i) { - for (int j = 0; j < MAX_GLYPHSET; ++j) { - if (font->sets[i][j]) { - if (font->sets[i][j]->surface) - SDL_FreeSurface(font->sets[i][j]->surface); - free(font->sets[i][j]); - } - } - } + font_clear_glyph_cache(font); FT_Done_Face(font->face); free(font); } void ren_font_group_set_tab_size(RenFont **fonts, int n) { for (int j = 0; j < FONT_FALLBACK_MAX && fonts[j]; ++j) { - for (int i = 0; i < (fonts[j]->antialiasing == FONT_ANTIALIASING_SUBPIXEL ? SUBPIXEL_BITMAPS_CACHED : 1); ++i) + for (int i = 0; i < (fonts[j]->antialiasing == FONT_ANTIALIASING_SUBPIXEL ? SUBPIXEL_BITMAPS_CACHED : 1); ++i) font_get_glyphset(fonts[j], '\t', i)->metrics['\t'].xadvance = fonts[j]->space_advance * n; } } int ren_font_group_get_tab_size(RenFont **fonts) { - return font_get_glyphset(fonts[0], '\t', 0)->metrics['\t'].xadvance / fonts[0]->space_advance; + float advance = font_get_glyphset(fonts[0], '\t', 0)->metrics['\t'].xadvance; + if (fonts[0]->space_advance) { + advance /= fonts[0]->space_advance; + } + return advance; } float ren_font_group_get_size(RenFont **fonts) { return fonts[0]->size; } + +void ren_font_group_set_size(RenFont **fonts, float size) { + const int surface_scale = renwin_surface_scale(&window_renderer); + for (int i = 0; i < FONT_FALLBACK_MAX && fonts[i]; ++i) { + font_clear_glyph_cache(fonts[i]); + FT_Face face = fonts[i]->face; + FT_Set_Pixel_Sizes(face, 0, (int)(size*surface_scale)); + fonts[i]->size = size; + fonts[i]->height = (short)((face->height / (float)face->units_per_EM) * size); + fonts[i]->baseline = (short)((face->ascender / (float)face->units_per_EM) * size); + FT_Load_Char(face, ' ', font_set_load_options(fonts[i])); + fonts[i]->space_advance = face->glyph->advance.x / 64.0f; + fonts[i]->tab_advance = fonts[i]->space_advance * 2; + } +} + int ren_font_group_get_height(RenFont **fonts) { - return fonts[0]->size + 3; + return fonts[0]->height; } float ren_font_group_get_width(RenFont **fonts, const char *text) { @@ -239,8 +300,10 @@ float ren_font_group_get_width(RenFont **fonts, const char *text) { while (text < end) { unsigned int codepoint; text = utf8_to_codepoint(text, &codepoint); - font_group_get_glyph(&set, &metric, fonts, codepoint, 0); - width += metric->xadvance ? metric->xadvance : fonts[0]->space_advance; + RenFont* font = font_group_get_glyph(&set, &metric, fonts, codepoint, 0); + if (!metric) + break; + width += (!font || metric->xadvance) ? metric->xadvance : fonts[0]->space_advance; } const int surface_scale = renwin_surface_scale(&window_renderer); return width / surface_scale; @@ -255,51 +318,85 @@ float ren_draw_text(RenFont **fonts, const char *text, float x, int y, RenColor y *= surface_scale; int bytes_per_pixel = surface->format->BytesPerPixel; const char* end = text + strlen(text); - unsigned char* destination_pixels = surface->pixels; + uint8_t* destination_pixels = surface->pixels; int clip_end_x = clip.x + clip.width, clip_end_y = clip.y + clip.height; - + + RenFont* last; + float last_pen_x = x; + bool underline = fonts[0]->style & FONT_STYLE_UNDERLINE; + bool strikethrough = fonts[0]->style & FONT_STYLE_STRIKETHROUGH; + while (text < end) { unsigned int codepoint, r, g, b; text = utf8_to_codepoint(text, &codepoint); - GlyphSet* set = NULL; GlyphMetric* metric = NULL; + GlyphSet* set = NULL; GlyphMetric* metric = NULL; RenFont* font = font_group_get_glyph(&set, &metric, fonts, codepoint, (int)(fmod(pen_x, 1.0) * SUBPIXEL_BITMAPS_CACHED)); + if (!metric) + break; int start_x = floor(pen_x) + metric->bitmap_left; int end_x = (metric->x1 - metric->x0) + start_x; int glyph_end = metric->x1, glyph_start = metric->x0; if (!metric->loaded && codepoint > 0xFF) ren_draw_rect((RenRect){ start_x + 1, y, font->space_advance - 1, ren_font_group_get_height(fonts) }, color); if (set->surface && color.a > 0 && end_x >= clip.x && start_x < clip_end_x) { - unsigned char* source_pixels = set->surface->pixels; + uint8_t* source_pixels = set->surface->pixels; for (int line = metric->y0; line < metric->y1; ++line) { - int target_y = line + y - metric->y0 - metric->bitmap_top + font->size * surface_scale; + int target_y = line + y - metric->bitmap_top + font->baseline * surface_scale; if (target_y < clip.y) continue; if (target_y >= clip_end_y) break; if (start_x + (glyph_end - glyph_start) >= clip_end_x) glyph_end = glyph_start + (clip_end_x - start_x); - else if (start_x < clip.x) { + if (start_x < clip.x) { int offset = clip.x - start_x; start_x += offset; glyph_start += offset; } - unsigned int* destination_pixel = (unsigned int*)&destination_pixels[surface->pitch * target_y + start_x * bytes_per_pixel]; - unsigned char* source_pixel = &source_pixels[line * set->surface->pitch + glyph_start * (font->antialiasing == FONT_ANTIALIASING_SUBPIXEL ? 3 : 1)]; + uint32_t* destination_pixel = (uint32_t*)&(destination_pixels[surface->pitch * target_y + start_x * bytes_per_pixel]); + uint8_t* source_pixel = &source_pixels[line * set->surface->pitch + glyph_start * (font->antialiasing == FONT_ANTIALIASING_SUBPIXEL ? 3 : 1)]; for (int x = glyph_start; x < glyph_end; ++x) { - unsigned int destination_color = *destination_pixel; + uint32_t destination_color = *destination_pixel; + // the standard way of doing this would be SDL_GetRGBA, but that introduces a performance regression. needs to be investigated SDL_Color dst = { (destination_color & surface->format->Rmask) >> surface->format->Rshift, (destination_color & surface->format->Gmask) >> surface->format->Gshift, (destination_color & surface->format->Bmask) >> surface->format->Bshift, (destination_color & surface->format->Amask) >> surface->format->Ashift }; - SDL_Color src = { *(font->antialiasing == FONT_ANTIALIASING_SUBPIXEL ? source_pixel++ : source_pixel), *(font->antialiasing == FONT_ANTIALIASING_SUBPIXEL ? source_pixel++ : source_pixel), *source_pixel++ }; + SDL_Color src; + + if (font->antialiasing == FONT_ANTIALIASING_SUBPIXEL) { + src.r = *(source_pixel++); + src.g = *(source_pixel++); + } + else { + src.r = *(source_pixel); + src.g = *(source_pixel); + } + + src.b = *(source_pixel++); + src.a = 0xFF; + r = (color.r * src.r * color.a + dst.r * (65025 - src.r * color.a) + 32767) / 65025; g = (color.g * src.g * color.a + dst.g * (65025 - src.g * color.a) + 32767) / 65025; b = (color.b * src.b * color.a + dst.b * (65025 - src.b * color.a) + 32767) / 65025; + // the standard way of doing this would be SDL_GetRGBA, but that introduces a performance regression. needs to be investigated *destination_pixel++ = dst.a << surface->format->Ashift | r << surface->format->Rshift | g << surface->format->Gshift | b << surface->format->Bshift; } } } - pen_x += metric->xadvance ? metric->xadvance : font->space_advance; + + float adv = metric->xadvance ? metric->xadvance : font->space_advance; + + if(!last) last = font; + else if(font != last || text == end) { + float local_pen_x = text == end ? pen_x + adv : pen_x; + if (underline) + ren_draw_rect((RenRect){last_pen_x, y / surface_scale + last->height - 1, (local_pen_x - last_pen_x) / surface_scale, last->underline_thickness * surface_scale}, color); + if (strikethrough) + ren_draw_rect((RenRect){last_pen_x, y / surface_scale + last->height / 2, (local_pen_x - last_pen_x) / surface_scale, last->underline_thickness * surface_scale}, color); + last = font; + last_pen_x = pen_x; + } + + pen_x += adv; } - if (fonts[0]->style & FONT_STYLE_UNDERLINE) - ren_draw_rect((RenRect){ x, y / surface_scale + ren_font_group_get_height(fonts) - 1, (pen_x - x) / surface_scale, 1 }, color); return pen_x / surface_scale; } @@ -333,7 +430,7 @@ void ren_draw_rect(RenRect rect, RenColor color) { SDL_Surface *surface = renwin_get_surface(&window_renderer); uint32_t *d = surface->pixels; - + #if defined(__amigaos4__) || defined(__morphos__) d += x1 + y1 * surface->pitch/sizeof(uint32_t); int dr = surface->pitch/sizeof(uint32_t) - (x2 - x1); @@ -362,6 +459,7 @@ void ren_draw_rect(RenRect rect, RenColor color) { /*************** Window Management ****************/ void ren_free_window_resources() { renwin_free(&window_renderer); + SDL_FreeSurface(draw_rect_surface); } void ren_init(SDL_Window *win) { @@ -374,6 +472,8 @@ void ren_init(SDL_Window *win) { window_renderer.window = win; renwin_init_surface(&window_renderer); renwin_clip_to_surface(&window_renderer); + draw_rect_surface = SDL_CreateRGBSurface(0, 1, 1, 32, + 0xFF000000, 0x00FF0000, 0x0000FF00, 0x000000FF); } @@ -404,4 +504,3 @@ void ren_get_size(int *x, int *y) { *x = surface->w / scale; *y = surface->h / scale; } - diff --git a/src/renderer.h b/src/renderer.h index a97706ff..3e631ce4 100644 --- a/src/renderer.h +++ b/src/renderer.h @@ -5,20 +5,28 @@ #include #include +#ifdef __GNUC__ +#define UNUSED __attribute__((__unused__)) +#else +#define UNUSED +#endif + #define FONT_FALLBACK_MAX 4 typedef struct RenFont RenFont; typedef enum { FONT_HINTING_NONE, FONT_HINTING_SLIGHT, FONT_HINTING_FULL } ERenFontHinting; typedef enum { FONT_ANTIALIASING_NONE, FONT_ANTIALIASING_GRAYSCALE, FONT_ANTIALIASING_SUBPIXEL } ERenFontAntialiasing; -typedef enum { FONT_STYLE_BOLD = 1, FONT_STYLE_ITALIC = 2, FONT_STYLE_UNDERLINE = 4 } ERenFontStyle; +typedef enum { FONT_STYLE_BOLD = 1, FONT_STYLE_ITALIC = 2, FONT_STYLE_UNDERLINE = 4, FONT_STYLE_SMOOTH = 8, FONT_STYLE_STRIKETHROUGH = 16 } ERenFontStyle; typedef struct { uint8_t b, g, r, a; } RenColor; typedef struct { int x, y, width, height; } RenRect; RenFont* ren_font_load(const char *filename, float size, ERenFontAntialiasing antialiasing, ERenFontHinting hinting, unsigned char style); -RenFont* ren_font_copy(RenFont* font, float size); +RenFont* ren_font_copy(RenFont* font, float size, ERenFontAntialiasing antialiasing, ERenFontHinting hinting, int style); +const char* ren_font_get_path(RenFont *font); void ren_font_free(RenFont *font); int ren_font_group_get_tab_size(RenFont **font); int ren_font_group_get_height(RenFont **font); float ren_font_group_get_size(RenFont **font); +void ren_font_group_set_size(RenFont **font, float size); void ren_font_group_set_tab_size(RenFont **font, int n); float ren_font_group_get_width(RenFont **font, const char *text); float ren_draw_text(RenFont **font, const char *text, float x, int y, RenColor color); diff --git a/src/renwindow.c b/src/renwindow.c index dde9fc2f..c381e772 100644 --- a/src/renwindow.c +++ b/src/renwindow.c @@ -1,4 +1,5 @@ #include +#include #include "renwindow.h" #ifdef LITE_USE_SDL_RENDERER @@ -39,7 +40,7 @@ static void setup_renderer(RenWindow *ren, int w, int h) { #endif -void renwin_init_surface(RenWindow *ren) { +void renwin_init_surface(UNUSED RenWindow *ren) { #ifdef LITE_USE_SDL_RENDERER if (ren->surface) { SDL_FreeSurface(ren->surface); @@ -47,11 +48,15 @@ void renwin_init_surface(RenWindow *ren) { int w, h; SDL_GL_GetDrawableSize(ren->window, &w, &h); ren->surface = SDL_CreateRGBSurfaceWithFormat(0, w, h, 32, SDL_PIXELFORMAT_BGRA32); + if (!ren->surface) { + fprintf(stderr, "Error creating surface: %s", SDL_GetError()); + exit(1); + } setup_renderer(ren, w, h); #endif } -int renwin_surface_scale(RenWindow *ren) { +int renwin_surface_scale(UNUSED RenWindow *ren) { #ifdef LITE_USE_SDL_RENDERER return ren->surface_scale; #else @@ -80,11 +85,16 @@ SDL_Surface *renwin_get_surface(RenWindow *ren) { #ifdef LITE_USE_SDL_RENDERER return ren->surface; #else - return SDL_GetWindowSurface(ren->window); + SDL_Surface *surface = SDL_GetWindowSurface(ren->window); + if (!surface) { + fprintf(stderr, "Error getting window surface: %s", SDL_GetError()); + exit(1); + } + return surface; #endif } -void renwin_resize_surface(RenWindow *ren) { +void renwin_resize_surface(UNUSED RenWindow *ren) { #ifdef LITE_USE_SDL_RENDERER int new_w, new_h; SDL_GL_GetDrawableSize(ren->window, &new_w, &new_h); diff --git a/src/unidata.h b/src/unidata.h new file mode 100644 index 00000000..b615dff4 --- /dev/null +++ b/src/unidata.h @@ -0,0 +1,3753 @@ +/* + * unidata.h - generated by parseucd.lua + */ +#ifndef unidata_h +#define unidata_h + +#ifndef utfint +# define utfint utfint +typedef unsigned int utfint; +#endif + +typedef struct range_table { + utfint first; + utfint last; + int step; +} range_table; + +typedef struct conv_table { + utfint first; + utfint last; + int step; + int offset; +} conv_table; + +static struct range_table alpha_table[] = { + { 0x41, 0x5A, 1 }, + { 0x61, 0x7A, 1 }, + { 0xAA, 0xB5, 11 }, + { 0xBA, 0xC0, 6 }, + { 0xC1, 0xD6, 1 }, + { 0xD8, 0xF6, 1 }, + { 0xF8, 0x2C1, 1 }, + { 0x2C6, 0x2D1, 1 }, + { 0x2E0, 0x2E4, 1 }, + { 0x2EC, 0x2EE, 2 }, + { 0x345, 0x370, 43 }, + { 0x371, 0x374, 1 }, + { 0x376, 0x377, 1 }, + { 0x37A, 0x37D, 1 }, + { 0x37F, 0x386, 7 }, + { 0x388, 0x38A, 1 }, + { 0x38C, 0x38E, 2 }, + { 0x38F, 0x3A1, 1 }, + { 0x3A3, 0x3F5, 1 }, + { 0x3F7, 0x481, 1 }, + { 0x48A, 0x52F, 1 }, + { 0x531, 0x556, 1 }, + { 0x559, 0x560, 7 }, + { 0x561, 0x588, 1 }, + { 0x5B0, 0x5BD, 1 }, + { 0x5BF, 0x5C1, 2 }, + { 0x5C2, 0x5C4, 2 }, + { 0x5C5, 0x5C7, 2 }, + { 0x5D0, 0x5EA, 1 }, + { 0x5EF, 0x5F2, 1 }, + { 0x610, 0x61A, 1 }, + { 0x620, 0x657, 1 }, + { 0x659, 0x65F, 1 }, + { 0x66E, 0x6D3, 1 }, + { 0x6D5, 0x6DC, 1 }, + { 0x6E1, 0x6E8, 1 }, + { 0x6ED, 0x6EF, 1 }, + { 0x6FA, 0x6FC, 1 }, + { 0x6FF, 0x710, 17 }, + { 0x711, 0x73F, 1 }, + { 0x74D, 0x7B1, 1 }, + { 0x7CA, 0x7EA, 1 }, + { 0x7F4, 0x7F5, 1 }, + { 0x7FA, 0x800, 6 }, + { 0x801, 0x817, 1 }, + { 0x81A, 0x82C, 1 }, + { 0x840, 0x858, 1 }, + { 0x860, 0x86A, 1 }, + { 0x870, 0x887, 1 }, + { 0x889, 0x88E, 1 }, + { 0x8A0, 0x8C9, 1 }, + { 0x8D4, 0x8DF, 1 }, + { 0x8E3, 0x8E9, 1 }, + { 0x8F0, 0x93B, 1 }, + { 0x93D, 0x94C, 1 }, + { 0x94E, 0x950, 1 }, + { 0x955, 0x963, 1 }, + { 0x971, 0x983, 1 }, + { 0x985, 0x98C, 1 }, + { 0x98F, 0x990, 1 }, + { 0x993, 0x9A8, 1 }, + { 0x9AA, 0x9B0, 1 }, + { 0x9B2, 0x9B6, 4 }, + { 0x9B7, 0x9B9, 1 }, + { 0x9BD, 0x9C4, 1 }, + { 0x9C7, 0x9C8, 1 }, + { 0x9CB, 0x9CC, 1 }, + { 0x9CE, 0x9D7, 9 }, + { 0x9DC, 0x9DD, 1 }, + { 0x9DF, 0x9E3, 1 }, + { 0x9F0, 0x9F1, 1 }, + { 0x9FC, 0xA01, 5 }, + { 0xA02, 0xA03, 1 }, + { 0xA05, 0xA0A, 1 }, + { 0xA0F, 0xA10, 1 }, + { 0xA13, 0xA28, 1 }, + { 0xA2A, 0xA30, 1 }, + { 0xA32, 0xA33, 1 }, + { 0xA35, 0xA36, 1 }, + { 0xA38, 0xA39, 1 }, + { 0xA3E, 0xA42, 1 }, + { 0xA47, 0xA48, 1 }, + { 0xA4B, 0xA4C, 1 }, + { 0xA51, 0xA59, 8 }, + { 0xA5A, 0xA5C, 1 }, + { 0xA5E, 0xA70, 18 }, + { 0xA71, 0xA75, 1 }, + { 0xA81, 0xA83, 1 }, + { 0xA85, 0xA8D, 1 }, + { 0xA8F, 0xA91, 1 }, + { 0xA93, 0xAA8, 1 }, + { 0xAAA, 0xAB0, 1 }, + { 0xAB2, 0xAB3, 1 }, + { 0xAB5, 0xAB9, 1 }, + { 0xABD, 0xAC5, 1 }, + { 0xAC7, 0xAC9, 1 }, + { 0xACB, 0xACC, 1 }, + { 0xAD0, 0xAE0, 16 }, + { 0xAE1, 0xAE3, 1 }, + { 0xAF9, 0xAFC, 1 }, + { 0xB01, 0xB03, 1 }, + { 0xB05, 0xB0C, 1 }, + { 0xB0F, 0xB10, 1 }, + { 0xB13, 0xB28, 1 }, + { 0xB2A, 0xB30, 1 }, + { 0xB32, 0xB33, 1 }, + { 0xB35, 0xB39, 1 }, + { 0xB3D, 0xB44, 1 }, + { 0xB47, 0xB48, 1 }, + { 0xB4B, 0xB4C, 1 }, + { 0xB56, 0xB57, 1 }, + { 0xB5C, 0xB5D, 1 }, + { 0xB5F, 0xB63, 1 }, + { 0xB71, 0xB82, 17 }, + { 0xB83, 0xB85, 2 }, + { 0xB86, 0xB8A, 1 }, + { 0xB8E, 0xB90, 1 }, + { 0xB92, 0xB95, 1 }, + { 0xB99, 0xB9A, 1 }, + { 0xB9C, 0xB9E, 2 }, + { 0xB9F, 0xBA3, 4 }, + { 0xBA4, 0xBA8, 4 }, + { 0xBA9, 0xBAA, 1 }, + { 0xBAE, 0xBB9, 1 }, + { 0xBBE, 0xBC2, 1 }, + { 0xBC6, 0xBC8, 1 }, + { 0xBCA, 0xBCC, 1 }, + { 0xBD0, 0xBD7, 7 }, + { 0xC00, 0xC0C, 1 }, + { 0xC0E, 0xC10, 1 }, + { 0xC12, 0xC28, 1 }, + { 0xC2A, 0xC39, 1 }, + { 0xC3D, 0xC44, 1 }, + { 0xC46, 0xC48, 1 }, + { 0xC4A, 0xC4C, 1 }, + { 0xC55, 0xC56, 1 }, + { 0xC58, 0xC5A, 1 }, + { 0xC5D, 0xC60, 3 }, + { 0xC61, 0xC63, 1 }, + { 0xC80, 0xC83, 1 }, + { 0xC85, 0xC8C, 1 }, + { 0xC8E, 0xC90, 1 }, + { 0xC92, 0xCA8, 1 }, + { 0xCAA, 0xCB3, 1 }, + { 0xCB5, 0xCB9, 1 }, + { 0xCBD, 0xCC4, 1 }, + { 0xCC6, 0xCC8, 1 }, + { 0xCCA, 0xCCC, 1 }, + { 0xCD5, 0xCD6, 1 }, + { 0xCDD, 0xCDE, 1 }, + { 0xCE0, 0xCE3, 1 }, + { 0xCF1, 0xCF3, 1 }, + { 0xD00, 0xD0C, 1 }, + { 0xD0E, 0xD10, 1 }, + { 0xD12, 0xD3A, 1 }, + { 0xD3D, 0xD44, 1 }, + { 0xD46, 0xD48, 1 }, + { 0xD4A, 0xD4C, 1 }, + { 0xD4E, 0xD54, 6 }, + { 0xD55, 0xD57, 1 }, + { 0xD5F, 0xD63, 1 }, + { 0xD7A, 0xD7F, 1 }, + { 0xD81, 0xD83, 1 }, + { 0xD85, 0xD96, 1 }, + { 0xD9A, 0xDB1, 1 }, + { 0xDB3, 0xDBB, 1 }, + { 0xDBD, 0xDC0, 3 }, + { 0xDC1, 0xDC6, 1 }, + { 0xDCF, 0xDD4, 1 }, + { 0xDD6, 0xDD8, 2 }, + { 0xDD9, 0xDDF, 1 }, + { 0xDF2, 0xDF3, 1 }, + { 0xE01, 0xE3A, 1 }, + { 0xE40, 0xE46, 1 }, + { 0xE4D, 0xE81, 52 }, + { 0xE82, 0xE86, 2 }, + { 0xE87, 0xE8A, 1 }, + { 0xE8C, 0xEA3, 1 }, + { 0xEA5, 0xEA7, 2 }, + { 0xEA8, 0xEB9, 1 }, + { 0xEBB, 0xEBD, 1 }, + { 0xEC0, 0xEC4, 1 }, + { 0xEC6, 0xECD, 7 }, + { 0xEDC, 0xEDF, 1 }, + { 0xF00, 0xF40, 64 }, + { 0xF41, 0xF47, 1 }, + { 0xF49, 0xF6C, 1 }, + { 0xF71, 0xF83, 1 }, + { 0xF88, 0xF97, 1 }, + { 0xF99, 0xFBC, 1 }, + { 0x1000, 0x1036, 1 }, + { 0x1038, 0x103B, 3 }, + { 0x103C, 0x103F, 1 }, + { 0x1050, 0x108F, 1 }, + { 0x109A, 0x109D, 1 }, + { 0x10A0, 0x10C5, 1 }, + { 0x10C7, 0x10CD, 6 }, + { 0x10D0, 0x10FA, 1 }, + { 0x10FC, 0x1248, 1 }, + { 0x124A, 0x124D, 1 }, + { 0x1250, 0x1256, 1 }, + { 0x1258, 0x125A, 2 }, + { 0x125B, 0x125D, 1 }, + { 0x1260, 0x1288, 1 }, + { 0x128A, 0x128D, 1 }, + { 0x1290, 0x12B0, 1 }, + { 0x12B2, 0x12B5, 1 }, + { 0x12B8, 0x12BE, 1 }, + { 0x12C0, 0x12C2, 2 }, + { 0x12C3, 0x12C5, 1 }, + { 0x12C8, 0x12D6, 1 }, + { 0x12D8, 0x1310, 1 }, + { 0x1312, 0x1315, 1 }, + { 0x1318, 0x135A, 1 }, + { 0x1380, 0x138F, 1 }, + { 0x13A0, 0x13F5, 1 }, + { 0x13F8, 0x13FD, 1 }, + { 0x1401, 0x166C, 1 }, + { 0x166F, 0x167F, 1 }, + { 0x1681, 0x169A, 1 }, + { 0x16A0, 0x16EA, 1 }, + { 0x16EE, 0x16F8, 1 }, + { 0x1700, 0x1713, 1 }, + { 0x171F, 0x1733, 1 }, + { 0x1740, 0x1753, 1 }, + { 0x1760, 0x176C, 1 }, + { 0x176E, 0x1770, 1 }, + { 0x1772, 0x1773, 1 }, + { 0x1780, 0x17B3, 1 }, + { 0x17B6, 0x17C8, 1 }, + { 0x17D7, 0x17DC, 5 }, + { 0x1820, 0x1878, 1 }, + { 0x1880, 0x18AA, 1 }, + { 0x18B0, 0x18F5, 1 }, + { 0x1900, 0x191E, 1 }, + { 0x1920, 0x192B, 1 }, + { 0x1930, 0x1938, 1 }, + { 0x1950, 0x196D, 1 }, + { 0x1970, 0x1974, 1 }, + { 0x1980, 0x19AB, 1 }, + { 0x19B0, 0x19C9, 1 }, + { 0x1A00, 0x1A1B, 1 }, + { 0x1A20, 0x1A5E, 1 }, + { 0x1A61, 0x1A74, 1 }, + { 0x1AA7, 0x1ABF, 24 }, + { 0x1AC0, 0x1ACC, 12 }, + { 0x1ACD, 0x1ACE, 1 }, + { 0x1B00, 0x1B33, 1 }, + { 0x1B35, 0x1B43, 1 }, + { 0x1B45, 0x1B4C, 1 }, + { 0x1B80, 0x1BA9, 1 }, + { 0x1BAC, 0x1BAF, 1 }, + { 0x1BBA, 0x1BE5, 1 }, + { 0x1BE7, 0x1BF1, 1 }, + { 0x1C00, 0x1C36, 1 }, + { 0x1C4D, 0x1C4F, 1 }, + { 0x1C5A, 0x1C7D, 1 }, + { 0x1C80, 0x1C88, 1 }, + { 0x1C90, 0x1CBA, 1 }, + { 0x1CBD, 0x1CBF, 1 }, + { 0x1CE9, 0x1CEC, 1 }, + { 0x1CEE, 0x1CF3, 1 }, + { 0x1CF5, 0x1CF6, 1 }, + { 0x1CFA, 0x1D00, 6 }, + { 0x1D01, 0x1DBF, 1 }, + { 0x1DE7, 0x1DF4, 1 }, + { 0x1E00, 0x1F15, 1 }, + { 0x1F18, 0x1F1D, 1 }, + { 0x1F20, 0x1F45, 1 }, + { 0x1F48, 0x1F4D, 1 }, + { 0x1F50, 0x1F57, 1 }, + { 0x1F59, 0x1F5F, 2 }, + { 0x1F60, 0x1F7D, 1 }, + { 0x1F80, 0x1FB4, 1 }, + { 0x1FB6, 0x1FBC, 1 }, + { 0x1FBE, 0x1FC2, 4 }, + { 0x1FC3, 0x1FC4, 1 }, + { 0x1FC6, 0x1FCC, 1 }, + { 0x1FD0, 0x1FD3, 1 }, + { 0x1FD6, 0x1FDB, 1 }, + { 0x1FE0, 0x1FEC, 1 }, + { 0x1FF2, 0x1FF4, 1 }, + { 0x1FF6, 0x1FFC, 1 }, + { 0x2071, 0x207F, 14 }, + { 0x2090, 0x209C, 1 }, + { 0x2102, 0x2107, 5 }, + { 0x210A, 0x2113, 1 }, + { 0x2115, 0x2119, 4 }, + { 0x211A, 0x211D, 1 }, + { 0x2124, 0x212A, 2 }, + { 0x212B, 0x212D, 1 }, + { 0x212F, 0x2139, 1 }, + { 0x213C, 0x213F, 1 }, + { 0x2145, 0x2149, 1 }, + { 0x214E, 0x2160, 18 }, + { 0x2161, 0x2188, 1 }, + { 0x24B6, 0x24E9, 1 }, + { 0x2C00, 0x2CE4, 1 }, + { 0x2CEB, 0x2CEE, 1 }, + { 0x2CF2, 0x2CF3, 1 }, + { 0x2D00, 0x2D25, 1 }, + { 0x2D27, 0x2D2D, 6 }, + { 0x2D30, 0x2D67, 1 }, + { 0x2D6F, 0x2D80, 17 }, + { 0x2D81, 0x2D96, 1 }, + { 0x2DA0, 0x2DA6, 1 }, + { 0x2DA8, 0x2DAE, 1 }, + { 0x2DB0, 0x2DB6, 1 }, + { 0x2DB8, 0x2DBE, 1 }, + { 0x2DC0, 0x2DC6, 1 }, + { 0x2DC8, 0x2DCE, 1 }, + { 0x2DD0, 0x2DD6, 1 }, + { 0x2DD8, 0x2DDE, 1 }, + { 0x2DE0, 0x2DFF, 1 }, + { 0x2E2F, 0x3005, 470 }, + { 0x3006, 0x3007, 1 }, + { 0x3021, 0x3029, 1 }, + { 0x3031, 0x3035, 1 }, + { 0x3038, 0x303C, 1 }, + { 0x3041, 0x3096, 1 }, + { 0x309D, 0x309F, 1 }, + { 0x30A1, 0x30FA, 1 }, + { 0x30FC, 0x30FF, 1 }, + { 0x3105, 0x312F, 1 }, + { 0x3131, 0x318E, 1 }, + { 0x31A0, 0x31BF, 1 }, + { 0x31F0, 0x31FF, 1 }, + { 0x3400, 0x4DBF, 1 }, + { 0x4E00, 0xA48C, 1 }, + { 0xA4D0, 0xA4FD, 1 }, + { 0xA500, 0xA60C, 1 }, + { 0xA610, 0xA61F, 1 }, + { 0xA62A, 0xA62B, 1 }, + { 0xA640, 0xA66E, 1 }, + { 0xA674, 0xA67B, 1 }, + { 0xA67F, 0xA6EF, 1 }, + { 0xA717, 0xA71F, 1 }, + { 0xA722, 0xA788, 1 }, + { 0xA78B, 0xA7CA, 1 }, + { 0xA7D0, 0xA7D1, 1 }, + { 0xA7D3, 0xA7D5, 2 }, + { 0xA7D6, 0xA7D9, 1 }, + { 0xA7F2, 0xA805, 1 }, + { 0xA807, 0xA827, 1 }, + { 0xA840, 0xA873, 1 }, + { 0xA880, 0xA8C3, 1 }, + { 0xA8C5, 0xA8F2, 45 }, + { 0xA8F3, 0xA8F7, 1 }, + { 0xA8FB, 0xA8FD, 2 }, + { 0xA8FE, 0xA8FF, 1 }, + { 0xA90A, 0xA92A, 1 }, + { 0xA930, 0xA952, 1 }, + { 0xA960, 0xA97C, 1 }, + { 0xA980, 0xA9B2, 1 }, + { 0xA9B4, 0xA9BF, 1 }, + { 0xA9CF, 0xA9E0, 17 }, + { 0xA9E1, 0xA9EF, 1 }, + { 0xA9FA, 0xA9FE, 1 }, + { 0xAA00, 0xAA36, 1 }, + { 0xAA40, 0xAA4D, 1 }, + { 0xAA60, 0xAA76, 1 }, + { 0xAA7A, 0xAABE, 1 }, + { 0xAAC0, 0xAAC2, 2 }, + { 0xAADB, 0xAADD, 1 }, + { 0xAAE0, 0xAAEF, 1 }, + { 0xAAF2, 0xAAF5, 1 }, + { 0xAB01, 0xAB06, 1 }, + { 0xAB09, 0xAB0E, 1 }, + { 0xAB11, 0xAB16, 1 }, + { 0xAB20, 0xAB26, 1 }, + { 0xAB28, 0xAB2E, 1 }, + { 0xAB30, 0xAB5A, 1 }, + { 0xAB5C, 0xAB69, 1 }, + { 0xAB70, 0xABEA, 1 }, + { 0xAC00, 0xD7A3, 1 }, + { 0xD7B0, 0xD7C6, 1 }, + { 0xD7CB, 0xD7FB, 1 }, + { 0xF900, 0xFA6D, 1 }, + { 0xFA70, 0xFAD9, 1 }, + { 0xFB00, 0xFB06, 1 }, + { 0xFB13, 0xFB17, 1 }, + { 0xFB1D, 0xFB28, 1 }, + { 0xFB2A, 0xFB36, 1 }, + { 0xFB38, 0xFB3C, 1 }, + { 0xFB3E, 0xFB40, 2 }, + { 0xFB41, 0xFB43, 2 }, + { 0xFB44, 0xFB46, 2 }, + { 0xFB47, 0xFBB1, 1 }, + { 0xFBD3, 0xFD3D, 1 }, + { 0xFD50, 0xFD8F, 1 }, + { 0xFD92, 0xFDC7, 1 }, + { 0xFDF0, 0xFDFB, 1 }, + { 0xFE70, 0xFE74, 1 }, + { 0xFE76, 0xFEFC, 1 }, + { 0xFF21, 0xFF3A, 1 }, + { 0xFF41, 0xFF5A, 1 }, + { 0xFF66, 0xFFBE, 1 }, + { 0xFFC2, 0xFFC7, 1 }, + { 0xFFCA, 0xFFCF, 1 }, + { 0xFFD2, 0xFFD7, 1 }, + { 0xFFDA, 0xFFDC, 1 }, + { 0x10000, 0x1000B, 1 }, + { 0x1000D, 0x10026, 1 }, + { 0x10028, 0x1003A, 1 }, + { 0x1003C, 0x1003D, 1 }, + { 0x1003F, 0x1004D, 1 }, + { 0x10050, 0x1005D, 1 }, + { 0x10080, 0x100FA, 1 }, + { 0x10140, 0x10174, 1 }, + { 0x10280, 0x1029C, 1 }, + { 0x102A0, 0x102D0, 1 }, + { 0x10300, 0x1031F, 1 }, + { 0x1032D, 0x1034A, 1 }, + { 0x10350, 0x1037A, 1 }, + { 0x10380, 0x1039D, 1 }, + { 0x103A0, 0x103C3, 1 }, + { 0x103C8, 0x103CF, 1 }, + { 0x103D1, 0x103D5, 1 }, + { 0x10400, 0x1049D, 1 }, + { 0x104B0, 0x104D3, 1 }, + { 0x104D8, 0x104FB, 1 }, + { 0x10500, 0x10527, 1 }, + { 0x10530, 0x10563, 1 }, + { 0x10570, 0x1057A, 1 }, + { 0x1057C, 0x1058A, 1 }, + { 0x1058C, 0x10592, 1 }, + { 0x10594, 0x10595, 1 }, + { 0x10597, 0x105A1, 1 }, + { 0x105A3, 0x105B1, 1 }, + { 0x105B3, 0x105B9, 1 }, + { 0x105BB, 0x105BC, 1 }, + { 0x10600, 0x10736, 1 }, + { 0x10740, 0x10755, 1 }, + { 0x10760, 0x10767, 1 }, + { 0x10780, 0x10785, 1 }, + { 0x10787, 0x107B0, 1 }, + { 0x107B2, 0x107BA, 1 }, + { 0x10800, 0x10805, 1 }, + { 0x10808, 0x1080A, 2 }, + { 0x1080B, 0x10835, 1 }, + { 0x10837, 0x10838, 1 }, + { 0x1083C, 0x1083F, 3 }, + { 0x10840, 0x10855, 1 }, + { 0x10860, 0x10876, 1 }, + { 0x10880, 0x1089E, 1 }, + { 0x108E0, 0x108F2, 1 }, + { 0x108F4, 0x108F5, 1 }, + { 0x10900, 0x10915, 1 }, + { 0x10920, 0x10939, 1 }, + { 0x10980, 0x109B7, 1 }, + { 0x109BE, 0x109BF, 1 }, + { 0x10A00, 0x10A03, 1 }, + { 0x10A05, 0x10A06, 1 }, + { 0x10A0C, 0x10A13, 1 }, + { 0x10A15, 0x10A17, 1 }, + { 0x10A19, 0x10A35, 1 }, + { 0x10A60, 0x10A7C, 1 }, + { 0x10A80, 0x10A9C, 1 }, + { 0x10AC0, 0x10AC7, 1 }, + { 0x10AC9, 0x10AE4, 1 }, + { 0x10B00, 0x10B35, 1 }, + { 0x10B40, 0x10B55, 1 }, + { 0x10B60, 0x10B72, 1 }, + { 0x10B80, 0x10B91, 1 }, + { 0x10C00, 0x10C48, 1 }, + { 0x10C80, 0x10CB2, 1 }, + { 0x10CC0, 0x10CF2, 1 }, + { 0x10D00, 0x10D27, 1 }, + { 0x10E80, 0x10EA9, 1 }, + { 0x10EAB, 0x10EAC, 1 }, + { 0x10EB0, 0x10EB1, 1 }, + { 0x10F00, 0x10F1C, 1 }, + { 0x10F27, 0x10F30, 9 }, + { 0x10F31, 0x10F45, 1 }, + { 0x10F70, 0x10F81, 1 }, + { 0x10FB0, 0x10FC4, 1 }, + { 0x10FE0, 0x10FF6, 1 }, + { 0x11000, 0x11045, 1 }, + { 0x11071, 0x11075, 1 }, + { 0x11080, 0x110B8, 1 }, + { 0x110C2, 0x110D0, 14 }, + { 0x110D1, 0x110E8, 1 }, + { 0x11100, 0x11132, 1 }, + { 0x11144, 0x11147, 1 }, + { 0x11150, 0x11172, 1 }, + { 0x11176, 0x11180, 10 }, + { 0x11181, 0x111BF, 1 }, + { 0x111C1, 0x111C4, 1 }, + { 0x111CE, 0x111CF, 1 }, + { 0x111DA, 0x111DC, 2 }, + { 0x11200, 0x11211, 1 }, + { 0x11213, 0x11234, 1 }, + { 0x11237, 0x1123E, 7 }, + { 0x1123F, 0x11241, 1 }, + { 0x11280, 0x11286, 1 }, + { 0x11288, 0x1128A, 2 }, + { 0x1128B, 0x1128D, 1 }, + { 0x1128F, 0x1129D, 1 }, + { 0x1129F, 0x112A8, 1 }, + { 0x112B0, 0x112E8, 1 }, + { 0x11300, 0x11303, 1 }, + { 0x11305, 0x1130C, 1 }, + { 0x1130F, 0x11310, 1 }, + { 0x11313, 0x11328, 1 }, + { 0x1132A, 0x11330, 1 }, + { 0x11332, 0x11333, 1 }, + { 0x11335, 0x11339, 1 }, + { 0x1133D, 0x11344, 1 }, + { 0x11347, 0x11348, 1 }, + { 0x1134B, 0x1134C, 1 }, + { 0x11350, 0x11357, 7 }, + { 0x1135D, 0x11363, 1 }, + { 0x11400, 0x11441, 1 }, + { 0x11443, 0x11445, 1 }, + { 0x11447, 0x1144A, 1 }, + { 0x1145F, 0x11461, 1 }, + { 0x11480, 0x114C1, 1 }, + { 0x114C4, 0x114C5, 1 }, + { 0x114C7, 0x11580, 185 }, + { 0x11581, 0x115B5, 1 }, + { 0x115B8, 0x115BE, 1 }, + { 0x115D8, 0x115DD, 1 }, + { 0x11600, 0x1163E, 1 }, + { 0x11640, 0x11644, 4 }, + { 0x11680, 0x116B5, 1 }, + { 0x116B8, 0x11700, 72 }, + { 0x11701, 0x1171A, 1 }, + { 0x1171D, 0x1172A, 1 }, + { 0x11740, 0x11746, 1 }, + { 0x11800, 0x11838, 1 }, + { 0x118A0, 0x118DF, 1 }, + { 0x118FF, 0x11906, 1 }, + { 0x11909, 0x1190C, 3 }, + { 0x1190D, 0x11913, 1 }, + { 0x11915, 0x11916, 1 }, + { 0x11918, 0x11935, 1 }, + { 0x11937, 0x11938, 1 }, + { 0x1193B, 0x1193C, 1 }, + { 0x1193F, 0x11942, 1 }, + { 0x119A0, 0x119A7, 1 }, + { 0x119AA, 0x119D7, 1 }, + { 0x119DA, 0x119DF, 1 }, + { 0x119E1, 0x119E3, 2 }, + { 0x119E4, 0x11A00, 28 }, + { 0x11A01, 0x11A32, 1 }, + { 0x11A35, 0x11A3E, 1 }, + { 0x11A50, 0x11A97, 1 }, + { 0x11A9D, 0x11AB0, 19 }, + { 0x11AB1, 0x11AF8, 1 }, + { 0x11C00, 0x11C08, 1 }, + { 0x11C0A, 0x11C36, 1 }, + { 0x11C38, 0x11C3E, 1 }, + { 0x11C40, 0x11C72, 50 }, + { 0x11C73, 0x11C8F, 1 }, + { 0x11C92, 0x11CA7, 1 }, + { 0x11CA9, 0x11CB6, 1 }, + { 0x11D00, 0x11D06, 1 }, + { 0x11D08, 0x11D09, 1 }, + { 0x11D0B, 0x11D36, 1 }, + { 0x11D3A, 0x11D3C, 2 }, + { 0x11D3D, 0x11D3F, 2 }, + { 0x11D40, 0x11D41, 1 }, + { 0x11D43, 0x11D46, 3 }, + { 0x11D47, 0x11D60, 25 }, + { 0x11D61, 0x11D65, 1 }, + { 0x11D67, 0x11D68, 1 }, + { 0x11D6A, 0x11D8E, 1 }, + { 0x11D90, 0x11D91, 1 }, + { 0x11D93, 0x11D96, 1 }, + { 0x11D98, 0x11EE0, 328 }, + { 0x11EE1, 0x11EF6, 1 }, + { 0x11F00, 0x11F10, 1 }, + { 0x11F12, 0x11F3A, 1 }, + { 0x11F3E, 0x11F40, 1 }, + { 0x11FB0, 0x12000, 80 }, + { 0x12001, 0x12399, 1 }, + { 0x12400, 0x1246E, 1 }, + { 0x12480, 0x12543, 1 }, + { 0x12F90, 0x12FF0, 1 }, + { 0x13000, 0x1342F, 1 }, + { 0x13441, 0x13446, 1 }, + { 0x14400, 0x14646, 1 }, + { 0x16800, 0x16A38, 1 }, + { 0x16A40, 0x16A5E, 1 }, + { 0x16A70, 0x16ABE, 1 }, + { 0x16AD0, 0x16AED, 1 }, + { 0x16B00, 0x16B2F, 1 }, + { 0x16B40, 0x16B43, 1 }, + { 0x16B63, 0x16B77, 1 }, + { 0x16B7D, 0x16B8F, 1 }, + { 0x16E40, 0x16E7F, 1 }, + { 0x16F00, 0x16F4A, 1 }, + { 0x16F4F, 0x16F87, 1 }, + { 0x16F8F, 0x16F9F, 1 }, + { 0x16FE0, 0x16FE1, 1 }, + { 0x16FE3, 0x16FF0, 13 }, + { 0x16FF1, 0x17000, 15 }, + { 0x17001, 0x187F7, 1 }, + { 0x18800, 0x18CD5, 1 }, + { 0x18D00, 0x18D08, 1 }, + { 0x1AFF0, 0x1AFF3, 1 }, + { 0x1AFF5, 0x1AFFB, 1 }, + { 0x1AFFD, 0x1AFFE, 1 }, + { 0x1B000, 0x1B122, 1 }, + { 0x1B132, 0x1B150, 30 }, + { 0x1B151, 0x1B152, 1 }, + { 0x1B155, 0x1B164, 15 }, + { 0x1B165, 0x1B167, 1 }, + { 0x1B170, 0x1B2FB, 1 }, + { 0x1BC00, 0x1BC6A, 1 }, + { 0x1BC70, 0x1BC7C, 1 }, + { 0x1BC80, 0x1BC88, 1 }, + { 0x1BC90, 0x1BC99, 1 }, + { 0x1BC9E, 0x1D400, 5986 }, + { 0x1D401, 0x1D454, 1 }, + { 0x1D456, 0x1D49C, 1 }, + { 0x1D49E, 0x1D49F, 1 }, + { 0x1D4A2, 0x1D4A5, 3 }, + { 0x1D4A6, 0x1D4A9, 3 }, + { 0x1D4AA, 0x1D4AC, 1 }, + { 0x1D4AE, 0x1D4B9, 1 }, + { 0x1D4BB, 0x1D4BD, 2 }, + { 0x1D4BE, 0x1D4C3, 1 }, + { 0x1D4C5, 0x1D505, 1 }, + { 0x1D507, 0x1D50A, 1 }, + { 0x1D50D, 0x1D514, 1 }, + { 0x1D516, 0x1D51C, 1 }, + { 0x1D51E, 0x1D539, 1 }, + { 0x1D53B, 0x1D53E, 1 }, + { 0x1D540, 0x1D544, 1 }, + { 0x1D546, 0x1D54A, 4 }, + { 0x1D54B, 0x1D550, 1 }, + { 0x1D552, 0x1D6A5, 1 }, + { 0x1D6A8, 0x1D6C0, 1 }, + { 0x1D6C2, 0x1D6DA, 1 }, + { 0x1D6DC, 0x1D6FA, 1 }, + { 0x1D6FC, 0x1D714, 1 }, + { 0x1D716, 0x1D734, 1 }, + { 0x1D736, 0x1D74E, 1 }, + { 0x1D750, 0x1D76E, 1 }, + { 0x1D770, 0x1D788, 1 }, + { 0x1D78A, 0x1D7A8, 1 }, + { 0x1D7AA, 0x1D7C2, 1 }, + { 0x1D7C4, 0x1D7CB, 1 }, + { 0x1DF00, 0x1DF1E, 1 }, + { 0x1DF25, 0x1DF2A, 1 }, + { 0x1E000, 0x1E006, 1 }, + { 0x1E008, 0x1E018, 1 }, + { 0x1E01B, 0x1E021, 1 }, + { 0x1E023, 0x1E024, 1 }, + { 0x1E026, 0x1E02A, 1 }, + { 0x1E030, 0x1E06D, 1 }, + { 0x1E08F, 0x1E100, 113 }, + { 0x1E101, 0x1E12C, 1 }, + { 0x1E137, 0x1E13D, 1 }, + { 0x1E14E, 0x1E290, 322 }, + { 0x1E291, 0x1E2AD, 1 }, + { 0x1E2C0, 0x1E2EB, 1 }, + { 0x1E4D0, 0x1E4EB, 1 }, + { 0x1E7E0, 0x1E7E6, 1 }, + { 0x1E7E8, 0x1E7EB, 1 }, + { 0x1E7ED, 0x1E7EE, 1 }, + { 0x1E7F0, 0x1E7FE, 1 }, + { 0x1E800, 0x1E8C4, 1 }, + { 0x1E900, 0x1E943, 1 }, + { 0x1E947, 0x1E94B, 4 }, + { 0x1EE00, 0x1EE03, 1 }, + { 0x1EE05, 0x1EE1F, 1 }, + { 0x1EE21, 0x1EE22, 1 }, + { 0x1EE24, 0x1EE27, 3 }, + { 0x1EE29, 0x1EE32, 1 }, + { 0x1EE34, 0x1EE37, 1 }, + { 0x1EE39, 0x1EE3B, 2 }, + { 0x1EE42, 0x1EE47, 5 }, + { 0x1EE49, 0x1EE4D, 2 }, + { 0x1EE4E, 0x1EE4F, 1 }, + { 0x1EE51, 0x1EE52, 1 }, + { 0x1EE54, 0x1EE57, 3 }, + { 0x1EE59, 0x1EE61, 2 }, + { 0x1EE62, 0x1EE64, 2 }, + { 0x1EE67, 0x1EE6A, 1 }, + { 0x1EE6C, 0x1EE72, 1 }, + { 0x1EE74, 0x1EE77, 1 }, + { 0x1EE79, 0x1EE7C, 1 }, + { 0x1EE7E, 0x1EE80, 2 }, + { 0x1EE81, 0x1EE89, 1 }, + { 0x1EE8B, 0x1EE9B, 1 }, + { 0x1EEA1, 0x1EEA3, 1 }, + { 0x1EEA5, 0x1EEA9, 1 }, + { 0x1EEAB, 0x1EEBB, 1 }, + { 0x1F130, 0x1F149, 1 }, + { 0x1F150, 0x1F169, 1 }, + { 0x1F170, 0x1F189, 1 }, + { 0x20000, 0x2A6DF, 1 }, + { 0x2A700, 0x2B739, 1 }, + { 0x2B740, 0x2B81D, 1 }, + { 0x2B820, 0x2CEA1, 1 }, + { 0x2CEB0, 0x2EBE0, 1 }, + { 0x2F800, 0x2FA1D, 1 }, + { 0x30000, 0x3134A, 1 }, + { 0x31350, 0x323AF, 1 }, +}; + +static struct range_table lower_table[] = { + { 0x61, 0x7A, 1 }, + { 0xAA, 0xB5, 11 }, + { 0xBA, 0xDF, 37 }, + { 0xE0, 0xF6, 1 }, + { 0xF8, 0xFF, 1 }, + { 0x101, 0x137, 2 }, + { 0x138, 0x148, 2 }, + { 0x149, 0x177, 2 }, + { 0x17A, 0x17E, 2 }, + { 0x17F, 0x180, 1 }, + { 0x183, 0x185, 2 }, + { 0x188, 0x18C, 4 }, + { 0x18D, 0x192, 5 }, + { 0x195, 0x199, 4 }, + { 0x19A, 0x19B, 1 }, + { 0x19E, 0x1A1, 3 }, + { 0x1A3, 0x1A5, 2 }, + { 0x1A8, 0x1AA, 2 }, + { 0x1AB, 0x1AD, 2 }, + { 0x1B0, 0x1B4, 4 }, + { 0x1B6, 0x1B9, 3 }, + { 0x1BA, 0x1BD, 3 }, + { 0x1BE, 0x1BF, 1 }, + { 0x1C6, 0x1CC, 3 }, + { 0x1CE, 0x1DC, 2 }, + { 0x1DD, 0x1EF, 2 }, + { 0x1F0, 0x1F3, 3 }, + { 0x1F5, 0x1F9, 4 }, + { 0x1FB, 0x233, 2 }, + { 0x234, 0x239, 1 }, + { 0x23C, 0x23F, 3 }, + { 0x240, 0x242, 2 }, + { 0x247, 0x24F, 2 }, + { 0x250, 0x293, 1 }, + { 0x295, 0x2B8, 1 }, + { 0x2C0, 0x2C1, 1 }, + { 0x2E0, 0x2E4, 1 }, + { 0x345, 0x371, 44 }, + { 0x373, 0x377, 4 }, + { 0x37A, 0x37D, 1 }, + { 0x390, 0x3AC, 28 }, + { 0x3AD, 0x3CE, 1 }, + { 0x3D0, 0x3D1, 1 }, + { 0x3D5, 0x3D7, 1 }, + { 0x3D9, 0x3EF, 2 }, + { 0x3F0, 0x3F3, 1 }, + { 0x3F5, 0x3FB, 3 }, + { 0x3FC, 0x430, 52 }, + { 0x431, 0x45F, 1 }, + { 0x461, 0x481, 2 }, + { 0x48B, 0x4BF, 2 }, + { 0x4C2, 0x4CE, 2 }, + { 0x4CF, 0x52F, 2 }, + { 0x560, 0x588, 1 }, + { 0x10D0, 0x10FA, 1 }, + { 0x10FC, 0x10FF, 1 }, + { 0x13F8, 0x13FD, 1 }, + { 0x1C80, 0x1C88, 1 }, + { 0x1D00, 0x1DBF, 1 }, + { 0x1E01, 0x1E95, 2 }, + { 0x1E96, 0x1E9D, 1 }, + { 0x1E9F, 0x1EFF, 2 }, + { 0x1F00, 0x1F07, 1 }, + { 0x1F10, 0x1F15, 1 }, + { 0x1F20, 0x1F27, 1 }, + { 0x1F30, 0x1F37, 1 }, + { 0x1F40, 0x1F45, 1 }, + { 0x1F50, 0x1F57, 1 }, + { 0x1F60, 0x1F67, 1 }, + { 0x1F70, 0x1F7D, 1 }, + { 0x1F80, 0x1F87, 1 }, + { 0x1F90, 0x1F97, 1 }, + { 0x1FA0, 0x1FA7, 1 }, + { 0x1FB0, 0x1FB4, 1 }, + { 0x1FB6, 0x1FB7, 1 }, + { 0x1FBE, 0x1FC2, 4 }, + { 0x1FC3, 0x1FC4, 1 }, + { 0x1FC6, 0x1FC7, 1 }, + { 0x1FD0, 0x1FD3, 1 }, + { 0x1FD6, 0x1FD7, 1 }, + { 0x1FE0, 0x1FE7, 1 }, + { 0x1FF2, 0x1FF4, 1 }, + { 0x1FF6, 0x1FF7, 1 }, + { 0x2071, 0x207F, 14 }, + { 0x2090, 0x209C, 1 }, + { 0x210A, 0x210E, 4 }, + { 0x210F, 0x2113, 4 }, + { 0x212F, 0x2139, 5 }, + { 0x213C, 0x213D, 1 }, + { 0x2146, 0x2149, 1 }, + { 0x214E, 0x2170, 34 }, + { 0x2171, 0x217F, 1 }, + { 0x2184, 0x24D0, 844 }, + { 0x24D1, 0x24E9, 1 }, + { 0x2C30, 0x2C5F, 1 }, + { 0x2C61, 0x2C65, 4 }, + { 0x2C66, 0x2C6C, 2 }, + { 0x2C71, 0x2C73, 2 }, + { 0x2C74, 0x2C76, 2 }, + { 0x2C77, 0x2C7D, 1 }, + { 0x2C81, 0x2CE3, 2 }, + { 0x2CE4, 0x2CEC, 8 }, + { 0x2CEE, 0x2CF3, 5 }, + { 0x2D00, 0x2D25, 1 }, + { 0x2D27, 0x2D2D, 6 }, + { 0xA641, 0xA66D, 2 }, + { 0xA681, 0xA69B, 2 }, + { 0xA69C, 0xA69D, 1 }, + { 0xA723, 0xA72F, 2 }, + { 0xA730, 0xA731, 1 }, + { 0xA733, 0xA76F, 2 }, + { 0xA770, 0xA778, 1 }, + { 0xA77A, 0xA77C, 2 }, + { 0xA77F, 0xA787, 2 }, + { 0xA78C, 0xA78E, 2 }, + { 0xA791, 0xA793, 2 }, + { 0xA794, 0xA795, 1 }, + { 0xA797, 0xA7A9, 2 }, + { 0xA7AF, 0xA7B5, 6 }, + { 0xA7B7, 0xA7C3, 2 }, + { 0xA7C8, 0xA7CA, 2 }, + { 0xA7D1, 0xA7D9, 2 }, + { 0xA7F2, 0xA7F4, 1 }, + { 0xA7F6, 0xA7F8, 2 }, + { 0xA7F9, 0xA7FA, 1 }, + { 0xAB30, 0xAB5A, 1 }, + { 0xAB5C, 0xAB69, 1 }, + { 0xAB70, 0xABBF, 1 }, + { 0xFB00, 0xFB06, 1 }, + { 0xFB13, 0xFB17, 1 }, + { 0xFF41, 0xFF5A, 1 }, + { 0x10428, 0x1044F, 1 }, + { 0x104D8, 0x104FB, 1 }, + { 0x10597, 0x105A1, 1 }, + { 0x105A3, 0x105B1, 1 }, + { 0x105B3, 0x105B9, 1 }, + { 0x105BB, 0x105BC, 1 }, + { 0x10780, 0x10783, 3 }, + { 0x10784, 0x10785, 1 }, + { 0x10787, 0x107B0, 1 }, + { 0x107B2, 0x107BA, 1 }, + { 0x10CC0, 0x10CF2, 1 }, + { 0x118C0, 0x118DF, 1 }, + { 0x16E60, 0x16E7F, 1 }, + { 0x1D41A, 0x1D433, 1 }, + { 0x1D44E, 0x1D454, 1 }, + { 0x1D456, 0x1D467, 1 }, + { 0x1D482, 0x1D49B, 1 }, + { 0x1D4B6, 0x1D4B9, 1 }, + { 0x1D4BB, 0x1D4BD, 2 }, + { 0x1D4BE, 0x1D4C3, 1 }, + { 0x1D4C5, 0x1D4CF, 1 }, + { 0x1D4EA, 0x1D503, 1 }, + { 0x1D51E, 0x1D537, 1 }, + { 0x1D552, 0x1D56B, 1 }, + { 0x1D586, 0x1D59F, 1 }, + { 0x1D5BA, 0x1D5D3, 1 }, + { 0x1D5EE, 0x1D607, 1 }, + { 0x1D622, 0x1D63B, 1 }, + { 0x1D656, 0x1D66F, 1 }, + { 0x1D68A, 0x1D6A5, 1 }, + { 0x1D6C2, 0x1D6DA, 1 }, + { 0x1D6DC, 0x1D6E1, 1 }, + { 0x1D6FC, 0x1D714, 1 }, + { 0x1D716, 0x1D71B, 1 }, + { 0x1D736, 0x1D74E, 1 }, + { 0x1D750, 0x1D755, 1 }, + { 0x1D770, 0x1D788, 1 }, + { 0x1D78A, 0x1D78F, 1 }, + { 0x1D7AA, 0x1D7C2, 1 }, + { 0x1D7C4, 0x1D7C9, 1 }, + { 0x1D7CB, 0x1DF00, 1845 }, + { 0x1DF01, 0x1DF09, 1 }, + { 0x1DF0B, 0x1DF1E, 1 }, + { 0x1DF25, 0x1DF2A, 1 }, + { 0x1E030, 0x1E06D, 1 }, + { 0x1E922, 0x1E943, 1 }, +}; + +static struct range_table upper_table[] = { + { 0x41, 0x5A, 1 }, + { 0xC0, 0xD6, 1 }, + { 0xD8, 0xDE, 1 }, + { 0x100, 0x136, 2 }, + { 0x139, 0x147, 2 }, + { 0x14A, 0x178, 2 }, + { 0x179, 0x17D, 2 }, + { 0x181, 0x182, 1 }, + { 0x184, 0x186, 2 }, + { 0x187, 0x189, 2 }, + { 0x18A, 0x18B, 1 }, + { 0x18E, 0x191, 1 }, + { 0x193, 0x194, 1 }, + { 0x196, 0x198, 1 }, + { 0x19C, 0x19D, 1 }, + { 0x19F, 0x1A0, 1 }, + { 0x1A2, 0x1A6, 2 }, + { 0x1A7, 0x1A9, 2 }, + { 0x1AC, 0x1AE, 2 }, + { 0x1AF, 0x1B1, 2 }, + { 0x1B2, 0x1B3, 1 }, + { 0x1B5, 0x1B7, 2 }, + { 0x1B8, 0x1BC, 4 }, + { 0x1C4, 0x1CD, 3 }, + { 0x1CF, 0x1DB, 2 }, + { 0x1DE, 0x1EE, 2 }, + { 0x1F1, 0x1F4, 3 }, + { 0x1F6, 0x1F8, 1 }, + { 0x1FA, 0x232, 2 }, + { 0x23A, 0x23B, 1 }, + { 0x23D, 0x23E, 1 }, + { 0x241, 0x243, 2 }, + { 0x244, 0x246, 1 }, + { 0x248, 0x24E, 2 }, + { 0x370, 0x372, 2 }, + { 0x376, 0x37F, 9 }, + { 0x386, 0x388, 2 }, + { 0x389, 0x38A, 1 }, + { 0x38C, 0x38E, 2 }, + { 0x38F, 0x391, 2 }, + { 0x392, 0x3A1, 1 }, + { 0x3A3, 0x3AB, 1 }, + { 0x3CF, 0x3D2, 3 }, + { 0x3D3, 0x3D4, 1 }, + { 0x3D8, 0x3EE, 2 }, + { 0x3F4, 0x3F7, 3 }, + { 0x3F9, 0x3FA, 1 }, + { 0x3FD, 0x42F, 1 }, + { 0x460, 0x480, 2 }, + { 0x48A, 0x4C0, 2 }, + { 0x4C1, 0x4CD, 2 }, + { 0x4D0, 0x52E, 2 }, + { 0x531, 0x556, 1 }, + { 0x10A0, 0x10C5, 1 }, + { 0x10C7, 0x10CD, 6 }, + { 0x13A0, 0x13F5, 1 }, + { 0x1C90, 0x1CBA, 1 }, + { 0x1CBD, 0x1CBF, 1 }, + { 0x1E00, 0x1E94, 2 }, + { 0x1E9E, 0x1EFE, 2 }, + { 0x1F08, 0x1F0F, 1 }, + { 0x1F18, 0x1F1D, 1 }, + { 0x1F28, 0x1F2F, 1 }, + { 0x1F38, 0x1F3F, 1 }, + { 0x1F48, 0x1F4D, 1 }, + { 0x1F59, 0x1F5F, 2 }, + { 0x1F68, 0x1F6F, 1 }, + { 0x1FB8, 0x1FBB, 1 }, + { 0x1FC8, 0x1FCB, 1 }, + { 0x1FD8, 0x1FDB, 1 }, + { 0x1FE8, 0x1FEC, 1 }, + { 0x1FF8, 0x1FFB, 1 }, + { 0x2102, 0x2107, 5 }, + { 0x210B, 0x210D, 1 }, + { 0x2110, 0x2112, 1 }, + { 0x2115, 0x2119, 4 }, + { 0x211A, 0x211D, 1 }, + { 0x2124, 0x212A, 2 }, + { 0x212B, 0x212D, 1 }, + { 0x2130, 0x2133, 1 }, + { 0x213E, 0x213F, 1 }, + { 0x2145, 0x2160, 27 }, + { 0x2161, 0x216F, 1 }, + { 0x2183, 0x24B6, 819 }, + { 0x24B7, 0x24CF, 1 }, + { 0x2C00, 0x2C2F, 1 }, + { 0x2C60, 0x2C62, 2 }, + { 0x2C63, 0x2C64, 1 }, + { 0x2C67, 0x2C6D, 2 }, + { 0x2C6E, 0x2C70, 1 }, + { 0x2C72, 0x2C75, 3 }, + { 0x2C7E, 0x2C80, 1 }, + { 0x2C82, 0x2CE2, 2 }, + { 0x2CEB, 0x2CED, 2 }, + { 0x2CF2, 0xA640, 31054 }, + { 0xA642, 0xA66C, 2 }, + { 0xA680, 0xA69A, 2 }, + { 0xA722, 0xA72E, 2 }, + { 0xA732, 0xA76E, 2 }, + { 0xA779, 0xA77D, 2 }, + { 0xA77E, 0xA786, 2 }, + { 0xA78B, 0xA78D, 2 }, + { 0xA790, 0xA792, 2 }, + { 0xA796, 0xA7AA, 2 }, + { 0xA7AB, 0xA7AE, 1 }, + { 0xA7B0, 0xA7B4, 1 }, + { 0xA7B6, 0xA7C4, 2 }, + { 0xA7C5, 0xA7C7, 1 }, + { 0xA7C9, 0xA7D0, 7 }, + { 0xA7D6, 0xA7D8, 2 }, + { 0xA7F5, 0xFF21, 22316 }, + { 0xFF22, 0xFF3A, 1 }, + { 0x10400, 0x10427, 1 }, + { 0x104B0, 0x104D3, 1 }, + { 0x10570, 0x1057A, 1 }, + { 0x1057C, 0x1058A, 1 }, + { 0x1058C, 0x10592, 1 }, + { 0x10594, 0x10595, 1 }, + { 0x10C80, 0x10CB2, 1 }, + { 0x118A0, 0x118BF, 1 }, + { 0x16E40, 0x16E5F, 1 }, + { 0x1D400, 0x1D419, 1 }, + { 0x1D434, 0x1D44D, 1 }, + { 0x1D468, 0x1D481, 1 }, + { 0x1D49C, 0x1D49E, 2 }, + { 0x1D49F, 0x1D4A5, 3 }, + { 0x1D4A6, 0x1D4A9, 3 }, + { 0x1D4AA, 0x1D4AC, 1 }, + { 0x1D4AE, 0x1D4B5, 1 }, + { 0x1D4D0, 0x1D4E9, 1 }, + { 0x1D504, 0x1D505, 1 }, + { 0x1D507, 0x1D50A, 1 }, + { 0x1D50D, 0x1D514, 1 }, + { 0x1D516, 0x1D51C, 1 }, + { 0x1D538, 0x1D539, 1 }, + { 0x1D53B, 0x1D53E, 1 }, + { 0x1D540, 0x1D544, 1 }, + { 0x1D546, 0x1D54A, 4 }, + { 0x1D54B, 0x1D550, 1 }, + { 0x1D56C, 0x1D585, 1 }, + { 0x1D5A0, 0x1D5B9, 1 }, + { 0x1D5D4, 0x1D5ED, 1 }, + { 0x1D608, 0x1D621, 1 }, + { 0x1D63C, 0x1D655, 1 }, + { 0x1D670, 0x1D689, 1 }, + { 0x1D6A8, 0x1D6C0, 1 }, + { 0x1D6E2, 0x1D6FA, 1 }, + { 0x1D71C, 0x1D734, 1 }, + { 0x1D756, 0x1D76E, 1 }, + { 0x1D790, 0x1D7A8, 1 }, + { 0x1D7CA, 0x1E900, 4406 }, + { 0x1E901, 0x1E921, 1 }, + { 0x1F130, 0x1F149, 1 }, + { 0x1F150, 0x1F169, 1 }, + { 0x1F170, 0x1F189, 1 }, +}; + +static struct range_table xdigit_table[] = { + { 0x30, 0x39, 1 }, + { 0x41, 0x46, 1 }, + { 0x61, 0x66, 1 }, + { 0xFF10, 0xFF19, 1 }, + { 0xFF21, 0xFF26, 1 }, + { 0xFF41, 0xFF46, 1 }, +}; + +static struct range_table space_table[] = { + { 0x9, 0xD, 1 }, + { 0x20, 0x85, 101 }, + { 0xA0, 0x1680, 5600 }, + { 0x2000, 0x200A, 1 }, + { 0x2028, 0x2029, 1 }, + { 0x202F, 0x205F, 48 }, + { 0x3000, 0x3000, 1 }, +}; + +static struct range_table unprintable_table[] = { + { 0xAD, 0x34F, 674 }, + { 0x61C, 0x115F, 2883 }, + { 0x1160, 0x17B4, 1620 }, + { 0x17B5, 0x180B, 86 }, + { 0x180C, 0x180F, 1 }, + { 0x200B, 0x200F, 1 }, + { 0x202A, 0x202E, 1 }, + { 0x2060, 0x206F, 1 }, + { 0x3164, 0xFE00, 52380 }, + { 0xFE01, 0xFE0F, 1 }, + { 0xFEFF, 0xFFA0, 161 }, + { 0xFFF0, 0xFFF8, 1 }, + { 0x1BCA0, 0x1BCA3, 1 }, + { 0x1D173, 0x1D17A, 1 }, + { 0xE0000, 0xE0FFF, 1 }, +}; + +static struct range_table graph_table[] = { + { 0x20, 0x7E, 1 }, + { 0xA0, 0xAC, 1 }, + { 0xAE, 0x2FF, 1 }, + { 0x370, 0x377, 1 }, + { 0x37A, 0x37F, 1 }, + { 0x384, 0x38A, 1 }, + { 0x38C, 0x38E, 2 }, + { 0x38F, 0x3A1, 1 }, + { 0x3A3, 0x482, 1 }, + { 0x48A, 0x52F, 1 }, + { 0x531, 0x556, 1 }, + { 0x559, 0x58A, 1 }, + { 0x58D, 0x58F, 1 }, + { 0x5BE, 0x5C0, 2 }, + { 0x5C3, 0x5C6, 3 }, + { 0x5D0, 0x5EA, 1 }, + { 0x5EF, 0x5F4, 1 }, + { 0x606, 0x60F, 1 }, + { 0x61B, 0x61D, 2 }, + { 0x61E, 0x64A, 1 }, + { 0x660, 0x66F, 1 }, + { 0x671, 0x6D5, 1 }, + { 0x6DE, 0x6E5, 7 }, + { 0x6E6, 0x6E9, 3 }, + { 0x6EE, 0x70D, 1 }, + { 0x710, 0x712, 2 }, + { 0x713, 0x72F, 1 }, + { 0x74D, 0x7A5, 1 }, + { 0x7B1, 0x7C0, 15 }, + { 0x7C1, 0x7EA, 1 }, + { 0x7F4, 0x7FA, 1 }, + { 0x7FE, 0x815, 1 }, + { 0x81A, 0x824, 10 }, + { 0x828, 0x830, 8 }, + { 0x831, 0x83E, 1 }, + { 0x840, 0x858, 1 }, + { 0x85E, 0x860, 2 }, + { 0x861, 0x86A, 1 }, + { 0x870, 0x88E, 1 }, + { 0x8A0, 0x8C9, 1 }, + { 0x903, 0x939, 1 }, + { 0x93B, 0x93D, 2 }, + { 0x93E, 0x940, 1 }, + { 0x949, 0x94C, 1 }, + { 0x94E, 0x950, 1 }, + { 0x958, 0x961, 1 }, + { 0x964, 0x980, 1 }, + { 0x982, 0x983, 1 }, + { 0x985, 0x98C, 1 }, + { 0x98F, 0x990, 1 }, + { 0x993, 0x9A8, 1 }, + { 0x9AA, 0x9B0, 1 }, + { 0x9B2, 0x9B6, 4 }, + { 0x9B7, 0x9B9, 1 }, + { 0x9BD, 0x9BF, 2 }, + { 0x9C0, 0x9C7, 7 }, + { 0x9C8, 0x9CB, 3 }, + { 0x9CC, 0x9CE, 2 }, + { 0x9DC, 0x9DD, 1 }, + { 0x9DF, 0x9E1, 1 }, + { 0x9E6, 0x9FD, 1 }, + { 0xA03, 0xA05, 2 }, + { 0xA06, 0xA0A, 1 }, + { 0xA0F, 0xA10, 1 }, + { 0xA13, 0xA28, 1 }, + { 0xA2A, 0xA30, 1 }, + { 0xA32, 0xA33, 1 }, + { 0xA35, 0xA36, 1 }, + { 0xA38, 0xA39, 1 }, + { 0xA3E, 0xA40, 1 }, + { 0xA59, 0xA5C, 1 }, + { 0xA5E, 0xA66, 8 }, + { 0xA67, 0xA6F, 1 }, + { 0xA72, 0xA74, 1 }, + { 0xA76, 0xA83, 13 }, + { 0xA85, 0xA8D, 1 }, + { 0xA8F, 0xA91, 1 }, + { 0xA93, 0xAA8, 1 }, + { 0xAAA, 0xAB0, 1 }, + { 0xAB2, 0xAB3, 1 }, + { 0xAB5, 0xAB9, 1 }, + { 0xABD, 0xAC0, 1 }, + { 0xAC9, 0xACB, 2 }, + { 0xACC, 0xAD0, 4 }, + { 0xAE0, 0xAE1, 1 }, + { 0xAE6, 0xAF1, 1 }, + { 0xAF9, 0xB02, 9 }, + { 0xB03, 0xB05, 2 }, + { 0xB06, 0xB0C, 1 }, + { 0xB0F, 0xB10, 1 }, + { 0xB13, 0xB28, 1 }, + { 0xB2A, 0xB30, 1 }, + { 0xB32, 0xB33, 1 }, + { 0xB35, 0xB39, 1 }, + { 0xB3D, 0xB40, 3 }, + { 0xB47, 0xB48, 1 }, + { 0xB4B, 0xB4C, 1 }, + { 0xB5C, 0xB5D, 1 }, + { 0xB5F, 0xB61, 1 }, + { 0xB66, 0xB77, 1 }, + { 0xB83, 0xB85, 2 }, + { 0xB86, 0xB8A, 1 }, + { 0xB8E, 0xB90, 1 }, + { 0xB92, 0xB95, 1 }, + { 0xB99, 0xB9A, 1 }, + { 0xB9C, 0xB9E, 2 }, + { 0xB9F, 0xBA3, 4 }, + { 0xBA4, 0xBA8, 4 }, + { 0xBA9, 0xBAA, 1 }, + { 0xBAE, 0xBB9, 1 }, + { 0xBBF, 0xBC1, 2 }, + { 0xBC2, 0xBC6, 4 }, + { 0xBC7, 0xBC8, 1 }, + { 0xBCA, 0xBCC, 1 }, + { 0xBD0, 0xBE6, 22 }, + { 0xBE7, 0xBFA, 1 }, + { 0xC01, 0xC03, 1 }, + { 0xC05, 0xC0C, 1 }, + { 0xC0E, 0xC10, 1 }, + { 0xC12, 0xC28, 1 }, + { 0xC2A, 0xC39, 1 }, + { 0xC3D, 0xC41, 4 }, + { 0xC42, 0xC44, 1 }, + { 0xC58, 0xC5A, 1 }, + { 0xC5D, 0xC60, 3 }, + { 0xC61, 0xC66, 5 }, + { 0xC67, 0xC6F, 1 }, + { 0xC77, 0xC80, 1 }, + { 0xC82, 0xC8C, 1 }, + { 0xC8E, 0xC90, 1 }, + { 0xC92, 0xCA8, 1 }, + { 0xCAA, 0xCB3, 1 }, + { 0xCB5, 0xCB9, 1 }, + { 0xCBD, 0xCBE, 1 }, + { 0xCC0, 0xCC1, 1 }, + { 0xCC3, 0xCC4, 1 }, + { 0xCC7, 0xCC8, 1 }, + { 0xCCA, 0xCCB, 1 }, + { 0xCDD, 0xCDE, 1 }, + { 0xCE0, 0xCE1, 1 }, + { 0xCE6, 0xCEF, 1 }, + { 0xCF1, 0xCF3, 1 }, + { 0xD02, 0xD0C, 1 }, + { 0xD0E, 0xD10, 1 }, + { 0xD12, 0xD3A, 1 }, + { 0xD3D, 0xD3F, 2 }, + { 0xD40, 0xD46, 6 }, + { 0xD47, 0xD48, 1 }, + { 0xD4A, 0xD4C, 1 }, + { 0xD4E, 0xD4F, 1 }, + { 0xD54, 0xD56, 1 }, + { 0xD58, 0xD61, 1 }, + { 0xD66, 0xD7F, 1 }, + { 0xD82, 0xD83, 1 }, + { 0xD85, 0xD96, 1 }, + { 0xD9A, 0xDB1, 1 }, + { 0xDB3, 0xDBB, 1 }, + { 0xDBD, 0xDC0, 3 }, + { 0xDC1, 0xDC6, 1 }, + { 0xDD0, 0xDD1, 1 }, + { 0xDD8, 0xDDE, 1 }, + { 0xDE6, 0xDEF, 1 }, + { 0xDF2, 0xDF4, 1 }, + { 0xE01, 0xE30, 1 }, + { 0xE32, 0xE33, 1 }, + { 0xE3F, 0xE46, 1 }, + { 0xE4F, 0xE5B, 1 }, + { 0xE81, 0xE82, 1 }, + { 0xE84, 0xE86, 2 }, + { 0xE87, 0xE8A, 1 }, + { 0xE8C, 0xEA3, 1 }, + { 0xEA5, 0xEA7, 2 }, + { 0xEA8, 0xEB0, 1 }, + { 0xEB2, 0xEB3, 1 }, + { 0xEBD, 0xEC0, 3 }, + { 0xEC1, 0xEC4, 1 }, + { 0xEC6, 0xED0, 10 }, + { 0xED1, 0xED9, 1 }, + { 0xEDC, 0xEDF, 1 }, + { 0xF00, 0xF17, 1 }, + { 0xF1A, 0xF34, 1 }, + { 0xF36, 0xF3A, 2 }, + { 0xF3B, 0xF47, 1 }, + { 0xF49, 0xF6C, 1 }, + { 0xF7F, 0xF85, 6 }, + { 0xF88, 0xF8C, 1 }, + { 0xFBE, 0xFC5, 1 }, + { 0xFC7, 0xFCC, 1 }, + { 0xFCE, 0xFDA, 1 }, + { 0x1000, 0x102C, 1 }, + { 0x1031, 0x1038, 7 }, + { 0x103B, 0x103C, 1 }, + { 0x103F, 0x1057, 1 }, + { 0x105A, 0x105D, 1 }, + { 0x1061, 0x1070, 1 }, + { 0x1075, 0x1081, 1 }, + { 0x1083, 0x1084, 1 }, + { 0x1087, 0x108C, 1 }, + { 0x108E, 0x109C, 1 }, + { 0x109E, 0x10C5, 1 }, + { 0x10C7, 0x10CD, 6 }, + { 0x10D0, 0x1248, 1 }, + { 0x124A, 0x124D, 1 }, + { 0x1250, 0x1256, 1 }, + { 0x1258, 0x125A, 2 }, + { 0x125B, 0x125D, 1 }, + { 0x1260, 0x1288, 1 }, + { 0x128A, 0x128D, 1 }, + { 0x1290, 0x12B0, 1 }, + { 0x12B2, 0x12B5, 1 }, + { 0x12B8, 0x12BE, 1 }, + { 0x12C0, 0x12C2, 2 }, + { 0x12C3, 0x12C5, 1 }, + { 0x12C8, 0x12D6, 1 }, + { 0x12D8, 0x1310, 1 }, + { 0x1312, 0x1315, 1 }, + { 0x1318, 0x135A, 1 }, + { 0x1360, 0x137C, 1 }, + { 0x1380, 0x1399, 1 }, + { 0x13A0, 0x13F5, 1 }, + { 0x13F8, 0x13FD, 1 }, + { 0x1400, 0x169C, 1 }, + { 0x16A0, 0x16F8, 1 }, + { 0x1700, 0x1711, 1 }, + { 0x1715, 0x171F, 10 }, + { 0x1720, 0x1731, 1 }, + { 0x1734, 0x1736, 1 }, + { 0x1740, 0x1751, 1 }, + { 0x1760, 0x176C, 1 }, + { 0x176E, 0x1770, 1 }, + { 0x1780, 0x17B3, 1 }, + { 0x17B6, 0x17BE, 8 }, + { 0x17BF, 0x17C5, 1 }, + { 0x17C7, 0x17C8, 1 }, + { 0x17D4, 0x17DC, 1 }, + { 0x17E0, 0x17E9, 1 }, + { 0x17F0, 0x17F9, 1 }, + { 0x1800, 0x180A, 1 }, + { 0x1810, 0x1819, 1 }, + { 0x1820, 0x1878, 1 }, + { 0x1880, 0x1884, 1 }, + { 0x1887, 0x18A8, 1 }, + { 0x18AA, 0x18B0, 6 }, + { 0x18B1, 0x18F5, 1 }, + { 0x1900, 0x191E, 1 }, + { 0x1923, 0x1926, 1 }, + { 0x1929, 0x192B, 1 }, + { 0x1930, 0x1931, 1 }, + { 0x1933, 0x1938, 1 }, + { 0x1940, 0x1944, 4 }, + { 0x1945, 0x196D, 1 }, + { 0x1970, 0x1974, 1 }, + { 0x1980, 0x19AB, 1 }, + { 0x19B0, 0x19C9, 1 }, + { 0x19D0, 0x19DA, 1 }, + { 0x19DE, 0x1A16, 1 }, + { 0x1A19, 0x1A1A, 1 }, + { 0x1A1E, 0x1A55, 1 }, + { 0x1A57, 0x1A61, 10 }, + { 0x1A63, 0x1A64, 1 }, + { 0x1A6D, 0x1A72, 1 }, + { 0x1A80, 0x1A89, 1 }, + { 0x1A90, 0x1A99, 1 }, + { 0x1AA0, 0x1AAD, 1 }, + { 0x1B04, 0x1B33, 1 }, + { 0x1B3B, 0x1B3D, 2 }, + { 0x1B3E, 0x1B41, 1 }, + { 0x1B43, 0x1B4C, 1 }, + { 0x1B50, 0x1B6A, 1 }, + { 0x1B74, 0x1B7E, 1 }, + { 0x1B82, 0x1BA1, 1 }, + { 0x1BA6, 0x1BA7, 1 }, + { 0x1BAA, 0x1BAE, 4 }, + { 0x1BAF, 0x1BE5, 1 }, + { 0x1BE7, 0x1BEA, 3 }, + { 0x1BEB, 0x1BEC, 1 }, + { 0x1BEE, 0x1BF2, 4 }, + { 0x1BF3, 0x1BFC, 9 }, + { 0x1BFD, 0x1C2B, 1 }, + { 0x1C34, 0x1C35, 1 }, + { 0x1C3B, 0x1C49, 1 }, + { 0x1C4D, 0x1C88, 1 }, + { 0x1C90, 0x1CBA, 1 }, + { 0x1CBD, 0x1CC7, 1 }, + { 0x1CD3, 0x1CE1, 14 }, + { 0x1CE9, 0x1CEC, 1 }, + { 0x1CEE, 0x1CF3, 1 }, + { 0x1CF5, 0x1CF7, 1 }, + { 0x1CFA, 0x1D00, 6 }, + { 0x1D01, 0x1DBF, 1 }, + { 0x1E00, 0x1F15, 1 }, + { 0x1F18, 0x1F1D, 1 }, + { 0x1F20, 0x1F45, 1 }, + { 0x1F48, 0x1F4D, 1 }, + { 0x1F50, 0x1F57, 1 }, + { 0x1F59, 0x1F5F, 2 }, + { 0x1F60, 0x1F7D, 1 }, + { 0x1F80, 0x1FB4, 1 }, + { 0x1FB6, 0x1FC4, 1 }, + { 0x1FC6, 0x1FD3, 1 }, + { 0x1FD6, 0x1FDB, 1 }, + { 0x1FDD, 0x1FEF, 1 }, + { 0x1FF2, 0x1FF4, 1 }, + { 0x1FF6, 0x1FFE, 1 }, + { 0x2000, 0x200A, 1 }, + { 0x2010, 0x2027, 1 }, + { 0x202F, 0x205F, 1 }, + { 0x2070, 0x2071, 1 }, + { 0x2074, 0x208E, 1 }, + { 0x2090, 0x209C, 1 }, + { 0x20A0, 0x20C0, 1 }, + { 0x2100, 0x218B, 1 }, + { 0x2190, 0x2426, 1 }, + { 0x2440, 0x244A, 1 }, + { 0x2460, 0x2B73, 1 }, + { 0x2B76, 0x2B95, 1 }, + { 0x2B97, 0x2CEE, 1 }, + { 0x2CF2, 0x2CF3, 1 }, + { 0x2CF9, 0x2D25, 1 }, + { 0x2D27, 0x2D2D, 6 }, + { 0x2D30, 0x2D67, 1 }, + { 0x2D6F, 0x2D70, 1 }, + { 0x2D80, 0x2D96, 1 }, + { 0x2DA0, 0x2DA6, 1 }, + { 0x2DA8, 0x2DAE, 1 }, + { 0x2DB0, 0x2DB6, 1 }, + { 0x2DB8, 0x2DBE, 1 }, + { 0x2DC0, 0x2DC6, 1 }, + { 0x2DC8, 0x2DCE, 1 }, + { 0x2DD0, 0x2DD6, 1 }, + { 0x2DD8, 0x2DDE, 1 }, + { 0x2E00, 0x2E5D, 1 }, + { 0x2E80, 0x2E99, 1 }, + { 0x2E9B, 0x2EF3, 1 }, + { 0x2F00, 0x2FD5, 1 }, + { 0x2FF0, 0x2FFB, 1 }, + { 0x3000, 0x3029, 1 }, + { 0x3030, 0x303F, 1 }, + { 0x3041, 0x3096, 1 }, + { 0x309B, 0x30FF, 1 }, + { 0x3105, 0x312F, 1 }, + { 0x3131, 0x318E, 1 }, + { 0x3190, 0x31E3, 1 }, + { 0x31F0, 0x321E, 1 }, + { 0x3220, 0xA48C, 1 }, + { 0xA490, 0xA4C6, 1 }, + { 0xA4D0, 0xA62B, 1 }, + { 0xA640, 0xA66E, 1 }, + { 0xA673, 0xA67E, 11 }, + { 0xA67F, 0xA69D, 1 }, + { 0xA6A0, 0xA6EF, 1 }, + { 0xA6F2, 0xA6F7, 1 }, + { 0xA700, 0xA7CA, 1 }, + { 0xA7D0, 0xA7D1, 1 }, + { 0xA7D3, 0xA7D5, 2 }, + { 0xA7D6, 0xA7D9, 1 }, + { 0xA7F2, 0xA801, 1 }, + { 0xA803, 0xA805, 1 }, + { 0xA807, 0xA80A, 1 }, + { 0xA80C, 0xA824, 1 }, + { 0xA827, 0xA82B, 1 }, + { 0xA830, 0xA839, 1 }, + { 0xA840, 0xA877, 1 }, + { 0xA880, 0xA8C3, 1 }, + { 0xA8CE, 0xA8D9, 1 }, + { 0xA8F2, 0xA8FE, 1 }, + { 0xA900, 0xA925, 1 }, + { 0xA92E, 0xA946, 1 }, + { 0xA952, 0xA953, 1 }, + { 0xA95F, 0xA97C, 1 }, + { 0xA983, 0xA9B2, 1 }, + { 0xA9B4, 0xA9B5, 1 }, + { 0xA9BA, 0xA9BB, 1 }, + { 0xA9BE, 0xA9CD, 1 }, + { 0xA9CF, 0xA9D9, 1 }, + { 0xA9DE, 0xA9E4, 1 }, + { 0xA9E6, 0xA9FE, 1 }, + { 0xAA00, 0xAA28, 1 }, + { 0xAA2F, 0xAA30, 1 }, + { 0xAA33, 0xAA34, 1 }, + { 0xAA40, 0xAA42, 1 }, + { 0xAA44, 0xAA4B, 1 }, + { 0xAA4D, 0xAA50, 3 }, + { 0xAA51, 0xAA59, 1 }, + { 0xAA5C, 0xAA7B, 1 }, + { 0xAA7D, 0xAAAF, 1 }, + { 0xAAB1, 0xAAB5, 4 }, + { 0xAAB6, 0xAAB9, 3 }, + { 0xAABA, 0xAABD, 1 }, + { 0xAAC0, 0xAAC2, 2 }, + { 0xAADB, 0xAAEB, 1 }, + { 0xAAEE, 0xAAF5, 1 }, + { 0xAB01, 0xAB06, 1 }, + { 0xAB09, 0xAB0E, 1 }, + { 0xAB11, 0xAB16, 1 }, + { 0xAB20, 0xAB26, 1 }, + { 0xAB28, 0xAB2E, 1 }, + { 0xAB30, 0xAB6B, 1 }, + { 0xAB70, 0xABE4, 1 }, + { 0xABE6, 0xABE7, 1 }, + { 0xABE9, 0xABEC, 1 }, + { 0xABF0, 0xABF9, 1 }, + { 0xAC00, 0xD7A3, 1 }, + { 0xD7B0, 0xD7C6, 1 }, + { 0xD7CB, 0xD7FB, 1 }, + { 0xF900, 0xFA6D, 1 }, + { 0xFA70, 0xFAD9, 1 }, + { 0xFB00, 0xFB06, 1 }, + { 0xFB13, 0xFB17, 1 }, + { 0xFB1D, 0xFB1F, 2 }, + { 0xFB20, 0xFB36, 1 }, + { 0xFB38, 0xFB3C, 1 }, + { 0xFB3E, 0xFB40, 2 }, + { 0xFB41, 0xFB43, 2 }, + { 0xFB44, 0xFB46, 2 }, + { 0xFB47, 0xFBC2, 1 }, + { 0xFBD3, 0xFD8F, 1 }, + { 0xFD92, 0xFDC7, 1 }, + { 0xFDCF, 0xFDF0, 33 }, + { 0xFDF1, 0xFDFF, 1 }, + { 0xFE10, 0xFE19, 1 }, + { 0xFE30, 0xFE52, 1 }, + { 0xFE54, 0xFE66, 1 }, + { 0xFE68, 0xFE6B, 1 }, + { 0xFE70, 0xFE74, 1 }, + { 0xFE76, 0xFEFC, 1 }, + { 0xFF01, 0xFF9D, 1 }, + { 0xFFA0, 0xFFBE, 1 }, + { 0xFFC2, 0xFFC7, 1 }, + { 0xFFCA, 0xFFCF, 1 }, + { 0xFFD2, 0xFFD7, 1 }, + { 0xFFDA, 0xFFDC, 1 }, + { 0xFFE0, 0xFFE6, 1 }, + { 0xFFE8, 0xFFEE, 1 }, + { 0xFFFC, 0xFFFD, 1 }, + { 0x10000, 0x1000B, 1 }, + { 0x1000D, 0x10026, 1 }, + { 0x10028, 0x1003A, 1 }, + { 0x1003C, 0x1003D, 1 }, + { 0x1003F, 0x1004D, 1 }, + { 0x10050, 0x1005D, 1 }, + { 0x10080, 0x100FA, 1 }, + { 0x10100, 0x10102, 1 }, + { 0x10107, 0x10133, 1 }, + { 0x10137, 0x1018E, 1 }, + { 0x10190, 0x1019C, 1 }, + { 0x101A0, 0x101D0, 48 }, + { 0x101D1, 0x101FC, 1 }, + { 0x10280, 0x1029C, 1 }, + { 0x102A0, 0x102D0, 1 }, + { 0x102E1, 0x102FB, 1 }, + { 0x10300, 0x10323, 1 }, + { 0x1032D, 0x1034A, 1 }, + { 0x10350, 0x10375, 1 }, + { 0x10380, 0x1039D, 1 }, + { 0x1039F, 0x103C3, 1 }, + { 0x103C8, 0x103D5, 1 }, + { 0x10400, 0x1049D, 1 }, + { 0x104A0, 0x104A9, 1 }, + { 0x104B0, 0x104D3, 1 }, + { 0x104D8, 0x104FB, 1 }, + { 0x10500, 0x10527, 1 }, + { 0x10530, 0x10563, 1 }, + { 0x1056F, 0x1057A, 1 }, + { 0x1057C, 0x1058A, 1 }, + { 0x1058C, 0x10592, 1 }, + { 0x10594, 0x10595, 1 }, + { 0x10597, 0x105A1, 1 }, + { 0x105A3, 0x105B1, 1 }, + { 0x105B3, 0x105B9, 1 }, + { 0x105BB, 0x105BC, 1 }, + { 0x10600, 0x10736, 1 }, + { 0x10740, 0x10755, 1 }, + { 0x10760, 0x10767, 1 }, + { 0x10780, 0x10785, 1 }, + { 0x10787, 0x107B0, 1 }, + { 0x107B2, 0x107BA, 1 }, + { 0x10800, 0x10805, 1 }, + { 0x10808, 0x1080A, 2 }, + { 0x1080B, 0x10835, 1 }, + { 0x10837, 0x10838, 1 }, + { 0x1083C, 0x1083F, 3 }, + { 0x10840, 0x10855, 1 }, + { 0x10857, 0x1089E, 1 }, + { 0x108A7, 0x108AF, 1 }, + { 0x108E0, 0x108F2, 1 }, + { 0x108F4, 0x108F5, 1 }, + { 0x108FB, 0x1091B, 1 }, + { 0x1091F, 0x10939, 1 }, + { 0x1093F, 0x10980, 65 }, + { 0x10981, 0x109B7, 1 }, + { 0x109BC, 0x109CF, 1 }, + { 0x109D2, 0x10A00, 1 }, + { 0x10A10, 0x10A13, 1 }, + { 0x10A15, 0x10A17, 1 }, + { 0x10A19, 0x10A35, 1 }, + { 0x10A40, 0x10A48, 1 }, + { 0x10A50, 0x10A58, 1 }, + { 0x10A60, 0x10A9F, 1 }, + { 0x10AC0, 0x10AE4, 1 }, + { 0x10AEB, 0x10AF6, 1 }, + { 0x10B00, 0x10B35, 1 }, + { 0x10B39, 0x10B55, 1 }, + { 0x10B58, 0x10B72, 1 }, + { 0x10B78, 0x10B91, 1 }, + { 0x10B99, 0x10B9C, 1 }, + { 0x10BA9, 0x10BAF, 1 }, + { 0x10C00, 0x10C48, 1 }, + { 0x10C80, 0x10CB2, 1 }, + { 0x10CC0, 0x10CF2, 1 }, + { 0x10CFA, 0x10D23, 1 }, + { 0x10D30, 0x10D39, 1 }, + { 0x10E60, 0x10E7E, 1 }, + { 0x10E80, 0x10EA9, 1 }, + { 0x10EAD, 0x10EB0, 3 }, + { 0x10EB1, 0x10F00, 79 }, + { 0x10F01, 0x10F27, 1 }, + { 0x10F30, 0x10F45, 1 }, + { 0x10F51, 0x10F59, 1 }, + { 0x10F70, 0x10F81, 1 }, + { 0x10F86, 0x10F89, 1 }, + { 0x10FB0, 0x10FCB, 1 }, + { 0x10FE0, 0x10FF6, 1 }, + { 0x11000, 0x11002, 2 }, + { 0x11003, 0x11037, 1 }, + { 0x11047, 0x1104D, 1 }, + { 0x11052, 0x1106F, 1 }, + { 0x11071, 0x11072, 1 }, + { 0x11075, 0x11082, 13 }, + { 0x11083, 0x110B2, 1 }, + { 0x110B7, 0x110B8, 1 }, + { 0x110BB, 0x110BC, 1 }, + { 0x110BE, 0x110C1, 1 }, + { 0x110D0, 0x110E8, 1 }, + { 0x110F0, 0x110F9, 1 }, + { 0x11103, 0x11126, 1 }, + { 0x1112C, 0x11136, 10 }, + { 0x11137, 0x11147, 1 }, + { 0x11150, 0x11172, 1 }, + { 0x11174, 0x11176, 1 }, + { 0x11182, 0x111B5, 1 }, + { 0x111BF, 0x111C8, 1 }, + { 0x111CD, 0x111CE, 1 }, + { 0x111D0, 0x111DF, 1 }, + { 0x111E1, 0x111F4, 1 }, + { 0x11200, 0x11211, 1 }, + { 0x11213, 0x1122E, 1 }, + { 0x11232, 0x11233, 1 }, + { 0x11235, 0x11238, 3 }, + { 0x11239, 0x1123D, 1 }, + { 0x1123F, 0x11240, 1 }, + { 0x11280, 0x11286, 1 }, + { 0x11288, 0x1128A, 2 }, + { 0x1128B, 0x1128D, 1 }, + { 0x1128F, 0x1129D, 1 }, + { 0x1129F, 0x112A9, 1 }, + { 0x112B0, 0x112DE, 1 }, + { 0x112E0, 0x112E2, 1 }, + { 0x112F0, 0x112F9, 1 }, + { 0x11302, 0x11303, 1 }, + { 0x11305, 0x1130C, 1 }, + { 0x1130F, 0x11310, 1 }, + { 0x11313, 0x11328, 1 }, + { 0x1132A, 0x11330, 1 }, + { 0x11332, 0x11333, 1 }, + { 0x11335, 0x11339, 1 }, + { 0x1133D, 0x11341, 2 }, + { 0x11342, 0x11344, 1 }, + { 0x11347, 0x11348, 1 }, + { 0x1134B, 0x1134D, 1 }, + { 0x11350, 0x1135D, 13 }, + { 0x1135E, 0x11363, 1 }, + { 0x11400, 0x11437, 1 }, + { 0x11440, 0x11441, 1 }, + { 0x11445, 0x11447, 2 }, + { 0x11448, 0x1145B, 1 }, + { 0x1145D, 0x1145F, 2 }, + { 0x11460, 0x11461, 1 }, + { 0x11480, 0x114AF, 1 }, + { 0x114B1, 0x114B2, 1 }, + { 0x114B9, 0x114BB, 2 }, + { 0x114BC, 0x114BE, 2 }, + { 0x114C1, 0x114C4, 3 }, + { 0x114C5, 0x114C7, 1 }, + { 0x114D0, 0x114D9, 1 }, + { 0x11580, 0x115AE, 1 }, + { 0x115B0, 0x115B1, 1 }, + { 0x115B8, 0x115BB, 1 }, + { 0x115BE, 0x115C1, 3 }, + { 0x115C2, 0x115DB, 1 }, + { 0x11600, 0x11632, 1 }, + { 0x1163B, 0x1163C, 1 }, + { 0x1163E, 0x11641, 3 }, + { 0x11642, 0x11644, 1 }, + { 0x11650, 0x11659, 1 }, + { 0x11660, 0x1166C, 1 }, + { 0x11680, 0x116AA, 1 }, + { 0x116AC, 0x116AE, 2 }, + { 0x116AF, 0x116B6, 7 }, + { 0x116B8, 0x116B9, 1 }, + { 0x116C0, 0x116C9, 1 }, + { 0x11700, 0x1171A, 1 }, + { 0x11720, 0x11721, 1 }, + { 0x11726, 0x11730, 10 }, + { 0x11731, 0x11746, 1 }, + { 0x11800, 0x1182E, 1 }, + { 0x11838, 0x1183B, 3 }, + { 0x118A0, 0x118F2, 1 }, + { 0x118FF, 0x11906, 1 }, + { 0x11909, 0x1190C, 3 }, + { 0x1190D, 0x11913, 1 }, + { 0x11915, 0x11916, 1 }, + { 0x11918, 0x1192F, 1 }, + { 0x11931, 0x11935, 1 }, + { 0x11937, 0x11938, 1 }, + { 0x1193D, 0x1193F, 2 }, + { 0x11940, 0x11942, 1 }, + { 0x11944, 0x11946, 1 }, + { 0x11950, 0x11959, 1 }, + { 0x119A0, 0x119A7, 1 }, + { 0x119AA, 0x119D3, 1 }, + { 0x119DC, 0x119DF, 1 }, + { 0x119E1, 0x119E4, 1 }, + { 0x11A00, 0x11A0B, 11 }, + { 0x11A0C, 0x11A32, 1 }, + { 0x11A39, 0x11A3A, 1 }, + { 0x11A3F, 0x11A46, 1 }, + { 0x11A50, 0x11A57, 7 }, + { 0x11A58, 0x11A5C, 4 }, + { 0x11A5D, 0x11A89, 1 }, + { 0x11A97, 0x11A9A, 3 }, + { 0x11A9B, 0x11AA2, 1 }, + { 0x11AB0, 0x11AF8, 1 }, + { 0x11B00, 0x11B09, 1 }, + { 0x11C00, 0x11C08, 1 }, + { 0x11C0A, 0x11C2F, 1 }, + { 0x11C3E, 0x11C40, 2 }, + { 0x11C41, 0x11C45, 1 }, + { 0x11C50, 0x11C6C, 1 }, + { 0x11C70, 0x11C8F, 1 }, + { 0x11CA9, 0x11CB1, 8 }, + { 0x11CB4, 0x11D00, 76 }, + { 0x11D01, 0x11D06, 1 }, + { 0x11D08, 0x11D09, 1 }, + { 0x11D0B, 0x11D30, 1 }, + { 0x11D46, 0x11D50, 10 }, + { 0x11D51, 0x11D59, 1 }, + { 0x11D60, 0x11D65, 1 }, + { 0x11D67, 0x11D68, 1 }, + { 0x11D6A, 0x11D8E, 1 }, + { 0x11D93, 0x11D94, 1 }, + { 0x11D96, 0x11D98, 2 }, + { 0x11DA0, 0x11DA9, 1 }, + { 0x11EE0, 0x11EF2, 1 }, + { 0x11EF5, 0x11EF8, 1 }, + { 0x11F02, 0x11F10, 1 }, + { 0x11F12, 0x11F35, 1 }, + { 0x11F3E, 0x11F3F, 1 }, + { 0x11F41, 0x11F43, 2 }, + { 0x11F44, 0x11F59, 1 }, + { 0x11FB0, 0x11FC0, 16 }, + { 0x11FC1, 0x11FF1, 1 }, + { 0x11FFF, 0x12399, 1 }, + { 0x12400, 0x1246E, 1 }, + { 0x12470, 0x12474, 1 }, + { 0x12480, 0x12543, 1 }, + { 0x12F90, 0x12FF2, 1 }, + { 0x13000, 0x1342F, 1 }, + { 0x13441, 0x13446, 1 }, + { 0x14400, 0x14646, 1 }, + { 0x16800, 0x16A38, 1 }, + { 0x16A40, 0x16A5E, 1 }, + { 0x16A60, 0x16A69, 1 }, + { 0x16A6E, 0x16ABE, 1 }, + { 0x16AC0, 0x16AC9, 1 }, + { 0x16AD0, 0x16AED, 1 }, + { 0x16AF5, 0x16B00, 11 }, + { 0x16B01, 0x16B2F, 1 }, + { 0x16B37, 0x16B45, 1 }, + { 0x16B50, 0x16B59, 1 }, + { 0x16B5B, 0x16B61, 1 }, + { 0x16B63, 0x16B77, 1 }, + { 0x16B7D, 0x16B8F, 1 }, + { 0x16E40, 0x16E9A, 1 }, + { 0x16F00, 0x16F4A, 1 }, + { 0x16F50, 0x16F87, 1 }, + { 0x16F93, 0x16F9F, 1 }, + { 0x16FE0, 0x16FE3, 1 }, + { 0x16FF0, 0x16FF1, 1 }, + { 0x17000, 0x187F7, 1 }, + { 0x18800, 0x18CD5, 1 }, + { 0x18D00, 0x18D08, 1 }, + { 0x1AFF0, 0x1AFF3, 1 }, + { 0x1AFF5, 0x1AFFB, 1 }, + { 0x1AFFD, 0x1AFFE, 1 }, + { 0x1B000, 0x1B122, 1 }, + { 0x1B132, 0x1B150, 30 }, + { 0x1B151, 0x1B152, 1 }, + { 0x1B155, 0x1B164, 15 }, + { 0x1B165, 0x1B167, 1 }, + { 0x1B170, 0x1B2FB, 1 }, + { 0x1BC00, 0x1BC6A, 1 }, + { 0x1BC70, 0x1BC7C, 1 }, + { 0x1BC80, 0x1BC88, 1 }, + { 0x1BC90, 0x1BC99, 1 }, + { 0x1BC9C, 0x1BC9F, 3 }, + { 0x1CF50, 0x1CFC3, 1 }, + { 0x1D000, 0x1D0F5, 1 }, + { 0x1D100, 0x1D126, 1 }, + { 0x1D129, 0x1D164, 1 }, + { 0x1D166, 0x1D16A, 4 }, + { 0x1D16B, 0x1D16D, 1 }, + { 0x1D183, 0x1D184, 1 }, + { 0x1D18C, 0x1D1A9, 1 }, + { 0x1D1AE, 0x1D1EA, 1 }, + { 0x1D200, 0x1D241, 1 }, + { 0x1D245, 0x1D2C0, 123 }, + { 0x1D2C1, 0x1D2D3, 1 }, + { 0x1D2E0, 0x1D2F3, 1 }, + { 0x1D300, 0x1D356, 1 }, + { 0x1D360, 0x1D378, 1 }, + { 0x1D400, 0x1D454, 1 }, + { 0x1D456, 0x1D49C, 1 }, + { 0x1D49E, 0x1D49F, 1 }, + { 0x1D4A2, 0x1D4A5, 3 }, + { 0x1D4A6, 0x1D4A9, 3 }, + { 0x1D4AA, 0x1D4AC, 1 }, + { 0x1D4AE, 0x1D4B9, 1 }, + { 0x1D4BB, 0x1D4BD, 2 }, + { 0x1D4BE, 0x1D4C3, 1 }, + { 0x1D4C5, 0x1D505, 1 }, + { 0x1D507, 0x1D50A, 1 }, + { 0x1D50D, 0x1D514, 1 }, + { 0x1D516, 0x1D51C, 1 }, + { 0x1D51E, 0x1D539, 1 }, + { 0x1D53B, 0x1D53E, 1 }, + { 0x1D540, 0x1D544, 1 }, + { 0x1D546, 0x1D54A, 4 }, + { 0x1D54B, 0x1D550, 1 }, + { 0x1D552, 0x1D6A5, 1 }, + { 0x1D6A8, 0x1D7CB, 1 }, + { 0x1D7CE, 0x1D9FF, 1 }, + { 0x1DA37, 0x1DA3A, 1 }, + { 0x1DA6D, 0x1DA74, 1 }, + { 0x1DA76, 0x1DA83, 1 }, + { 0x1DA85, 0x1DA8B, 1 }, + { 0x1DF00, 0x1DF1E, 1 }, + { 0x1DF25, 0x1DF2A, 1 }, + { 0x1E030, 0x1E06D, 1 }, + { 0x1E100, 0x1E12C, 1 }, + { 0x1E137, 0x1E13D, 1 }, + { 0x1E140, 0x1E149, 1 }, + { 0x1E14E, 0x1E14F, 1 }, + { 0x1E290, 0x1E2AD, 1 }, + { 0x1E2C0, 0x1E2EB, 1 }, + { 0x1E2F0, 0x1E2F9, 1 }, + { 0x1E2FF, 0x1E4D0, 465 }, + { 0x1E4D1, 0x1E4EB, 1 }, + { 0x1E4F0, 0x1E4F9, 1 }, + { 0x1E7E0, 0x1E7E6, 1 }, + { 0x1E7E8, 0x1E7EB, 1 }, + { 0x1E7ED, 0x1E7EE, 1 }, + { 0x1E7F0, 0x1E7FE, 1 }, + { 0x1E800, 0x1E8C4, 1 }, + { 0x1E8C7, 0x1E8CF, 1 }, + { 0x1E900, 0x1E943, 1 }, + { 0x1E94B, 0x1E950, 5 }, + { 0x1E951, 0x1E959, 1 }, + { 0x1E95E, 0x1E95F, 1 }, + { 0x1EC71, 0x1ECB4, 1 }, + { 0x1ED01, 0x1ED3D, 1 }, + { 0x1EE00, 0x1EE03, 1 }, + { 0x1EE05, 0x1EE1F, 1 }, + { 0x1EE21, 0x1EE22, 1 }, + { 0x1EE24, 0x1EE27, 3 }, + { 0x1EE29, 0x1EE32, 1 }, + { 0x1EE34, 0x1EE37, 1 }, + { 0x1EE39, 0x1EE3B, 2 }, + { 0x1EE42, 0x1EE47, 5 }, + { 0x1EE49, 0x1EE4D, 2 }, + { 0x1EE4E, 0x1EE4F, 1 }, + { 0x1EE51, 0x1EE52, 1 }, + { 0x1EE54, 0x1EE57, 3 }, + { 0x1EE59, 0x1EE61, 2 }, + { 0x1EE62, 0x1EE64, 2 }, + { 0x1EE67, 0x1EE6A, 1 }, + { 0x1EE6C, 0x1EE72, 1 }, + { 0x1EE74, 0x1EE77, 1 }, + { 0x1EE79, 0x1EE7C, 1 }, + { 0x1EE7E, 0x1EE80, 2 }, + { 0x1EE81, 0x1EE89, 1 }, + { 0x1EE8B, 0x1EE9B, 1 }, + { 0x1EEA1, 0x1EEA3, 1 }, + { 0x1EEA5, 0x1EEA9, 1 }, + { 0x1EEAB, 0x1EEBB, 1 }, + { 0x1EEF0, 0x1EEF1, 1 }, + { 0x1F000, 0x1F02B, 1 }, + { 0x1F030, 0x1F093, 1 }, + { 0x1F0A0, 0x1F0AE, 1 }, + { 0x1F0B1, 0x1F0BF, 1 }, + { 0x1F0C1, 0x1F0CF, 1 }, + { 0x1F0D1, 0x1F0F5, 1 }, + { 0x1F100, 0x1F1AD, 1 }, + { 0x1F1E6, 0x1F202, 1 }, + { 0x1F210, 0x1F23B, 1 }, + { 0x1F240, 0x1F248, 1 }, + { 0x1F250, 0x1F251, 1 }, + { 0x1F260, 0x1F265, 1 }, + { 0x1F300, 0x1F6D7, 1 }, + { 0x1F6DC, 0x1F6EC, 1 }, + { 0x1F6F0, 0x1F6FC, 1 }, + { 0x1F700, 0x1F776, 1 }, + { 0x1F77B, 0x1F7D9, 1 }, + { 0x1F7E0, 0x1F7EB, 1 }, + { 0x1F7F0, 0x1F800, 16 }, + { 0x1F801, 0x1F80B, 1 }, + { 0x1F810, 0x1F847, 1 }, + { 0x1F850, 0x1F859, 1 }, + { 0x1F860, 0x1F887, 1 }, + { 0x1F890, 0x1F8AD, 1 }, + { 0x1F8B0, 0x1F8B1, 1 }, + { 0x1F900, 0x1FA53, 1 }, + { 0x1FA60, 0x1FA6D, 1 }, + { 0x1FA70, 0x1FA7C, 1 }, + { 0x1FA80, 0x1FA88, 1 }, + { 0x1FA90, 0x1FABD, 1 }, + { 0x1FABF, 0x1FAC5, 1 }, + { 0x1FACE, 0x1FADB, 1 }, + { 0x1FAE0, 0x1FAE8, 1 }, + { 0x1FAF0, 0x1FAF8, 1 }, + { 0x1FB00, 0x1FB92, 1 }, + { 0x1FB94, 0x1FBCA, 1 }, + { 0x1FBF0, 0x1FBF9, 1 }, + { 0x20000, 0x2A6DF, 1 }, + { 0x2A700, 0x2B739, 1 }, + { 0x2B740, 0x2B81D, 1 }, + { 0x2B820, 0x2CEA1, 1 }, + { 0x2CEB0, 0x2EBE0, 1 }, + { 0x2F800, 0x2FA1D, 1 }, + { 0x30000, 0x3134A, 1 }, + { 0x31350, 0x323AF, 1 }, +}; + +static struct range_table compose_table[] = { + { 0x300, 0x36F, 1 }, + { 0x483, 0x489, 1 }, + { 0x591, 0x5BD, 1 }, + { 0x5BF, 0x5C1, 2 }, + { 0x5C2, 0x5C4, 2 }, + { 0x5C5, 0x5C7, 2 }, + { 0x610, 0x61A, 1 }, + { 0x64B, 0x65F, 1 }, + { 0x670, 0x6D6, 102 }, + { 0x6D7, 0x6DC, 1 }, + { 0x6DF, 0x6E4, 1 }, + { 0x6E7, 0x6E8, 1 }, + { 0x6EA, 0x6ED, 1 }, + { 0x711, 0x730, 31 }, + { 0x731, 0x74A, 1 }, + { 0x7A6, 0x7B0, 1 }, + { 0x7EB, 0x7F3, 1 }, + { 0x7FD, 0x816, 25 }, + { 0x817, 0x819, 1 }, + { 0x81B, 0x823, 1 }, + { 0x825, 0x827, 1 }, + { 0x829, 0x82D, 1 }, + { 0x859, 0x85B, 1 }, + { 0x898, 0x89F, 1 }, + { 0x8CA, 0x8E1, 1 }, + { 0x8E3, 0x902, 1 }, + { 0x93A, 0x93C, 2 }, + { 0x941, 0x948, 1 }, + { 0x94D, 0x951, 4 }, + { 0x952, 0x957, 1 }, + { 0x962, 0x963, 1 }, + { 0x981, 0x9BC, 59 }, + { 0x9BE, 0x9C1, 3 }, + { 0x9C2, 0x9C4, 1 }, + { 0x9CD, 0x9D7, 10 }, + { 0x9E2, 0x9E3, 1 }, + { 0x9FE, 0xA01, 3 }, + { 0xA02, 0xA3C, 58 }, + { 0xA41, 0xA42, 1 }, + { 0xA47, 0xA48, 1 }, + { 0xA4B, 0xA4D, 1 }, + { 0xA51, 0xA70, 31 }, + { 0xA71, 0xA75, 4 }, + { 0xA81, 0xA82, 1 }, + { 0xABC, 0xAC1, 5 }, + { 0xAC2, 0xAC5, 1 }, + { 0xAC7, 0xAC8, 1 }, + { 0xACD, 0xAE2, 21 }, + { 0xAE3, 0xAFA, 23 }, + { 0xAFB, 0xAFF, 1 }, + { 0xB01, 0xB3C, 59 }, + { 0xB3E, 0xB3F, 1 }, + { 0xB41, 0xB44, 1 }, + { 0xB4D, 0xB55, 8 }, + { 0xB56, 0xB57, 1 }, + { 0xB62, 0xB63, 1 }, + { 0xB82, 0xBBE, 60 }, + { 0xBC0, 0xBCD, 13 }, + { 0xBD7, 0xC00, 41 }, + { 0xC04, 0xC3C, 56 }, + { 0xC3E, 0xC40, 1 }, + { 0xC46, 0xC48, 1 }, + { 0xC4A, 0xC4D, 1 }, + { 0xC55, 0xC56, 1 }, + { 0xC62, 0xC63, 1 }, + { 0xC81, 0xCBC, 59 }, + { 0xCBF, 0xCC2, 3 }, + { 0xCC6, 0xCCC, 6 }, + { 0xCCD, 0xCD5, 8 }, + { 0xCD6, 0xCE2, 12 }, + { 0xCE3, 0xD00, 29 }, + { 0xD01, 0xD3B, 58 }, + { 0xD3C, 0xD3E, 2 }, + { 0xD41, 0xD44, 1 }, + { 0xD4D, 0xD57, 10 }, + { 0xD62, 0xD63, 1 }, + { 0xD81, 0xDCA, 73 }, + { 0xDCF, 0xDD2, 3 }, + { 0xDD3, 0xDD4, 1 }, + { 0xDD6, 0xDDF, 9 }, + { 0xE31, 0xE34, 3 }, + { 0xE35, 0xE3A, 1 }, + { 0xE47, 0xE4E, 1 }, + { 0xEB1, 0xEB4, 3 }, + { 0xEB5, 0xEBC, 1 }, + { 0xEC8, 0xECE, 1 }, + { 0xF18, 0xF19, 1 }, + { 0xF35, 0xF39, 2 }, + { 0xF71, 0xF7E, 1 }, + { 0xF80, 0xF84, 1 }, + { 0xF86, 0xF87, 1 }, + { 0xF8D, 0xF97, 1 }, + { 0xF99, 0xFBC, 1 }, + { 0xFC6, 0x102D, 103 }, + { 0x102E, 0x1030, 1 }, + { 0x1032, 0x1037, 1 }, + { 0x1039, 0x103A, 1 }, + { 0x103D, 0x103E, 1 }, + { 0x1058, 0x1059, 1 }, + { 0x105E, 0x1060, 1 }, + { 0x1071, 0x1074, 1 }, + { 0x1082, 0x1085, 3 }, + { 0x1086, 0x108D, 7 }, + { 0x109D, 0x135D, 704 }, + { 0x135E, 0x135F, 1 }, + { 0x1712, 0x1714, 1 }, + { 0x1732, 0x1733, 1 }, + { 0x1752, 0x1753, 1 }, + { 0x1772, 0x1773, 1 }, + { 0x17B4, 0x17B5, 1 }, + { 0x17B7, 0x17BD, 1 }, + { 0x17C6, 0x17C9, 3 }, + { 0x17CA, 0x17D3, 1 }, + { 0x17DD, 0x180B, 46 }, + { 0x180C, 0x180D, 1 }, + { 0x180F, 0x1885, 118 }, + { 0x1886, 0x18A9, 35 }, + { 0x1920, 0x1922, 1 }, + { 0x1927, 0x1928, 1 }, + { 0x1932, 0x1939, 7 }, + { 0x193A, 0x193B, 1 }, + { 0x1A17, 0x1A18, 1 }, + { 0x1A1B, 0x1A56, 59 }, + { 0x1A58, 0x1A5E, 1 }, + { 0x1A60, 0x1A62, 2 }, + { 0x1A65, 0x1A6C, 1 }, + { 0x1A73, 0x1A7C, 1 }, + { 0x1A7F, 0x1AB0, 49 }, + { 0x1AB1, 0x1ACE, 1 }, + { 0x1B00, 0x1B03, 1 }, + { 0x1B34, 0x1B3A, 1 }, + { 0x1B3C, 0x1B42, 6 }, + { 0x1B6B, 0x1B73, 1 }, + { 0x1B80, 0x1B81, 1 }, + { 0x1BA2, 0x1BA5, 1 }, + { 0x1BA8, 0x1BA9, 1 }, + { 0x1BAB, 0x1BAD, 1 }, + { 0x1BE6, 0x1BE8, 2 }, + { 0x1BE9, 0x1BED, 4 }, + { 0x1BEF, 0x1BF1, 1 }, + { 0x1C2C, 0x1C33, 1 }, + { 0x1C36, 0x1C37, 1 }, + { 0x1CD0, 0x1CD2, 1 }, + { 0x1CD4, 0x1CE0, 1 }, + { 0x1CE2, 0x1CE8, 1 }, + { 0x1CED, 0x1CF4, 7 }, + { 0x1CF8, 0x1CF9, 1 }, + { 0x1DC0, 0x1DFF, 1 }, + { 0x200C, 0x20D0, 196 }, + { 0x20D1, 0x20F0, 1 }, + { 0x2CEF, 0x2CF1, 1 }, + { 0x2D7F, 0x2DE0, 97 }, + { 0x2DE1, 0x2DFF, 1 }, + { 0x302A, 0x302F, 1 }, + { 0x3099, 0x309A, 1 }, + { 0xA66F, 0xA672, 1 }, + { 0xA674, 0xA67D, 1 }, + { 0xA69E, 0xA69F, 1 }, + { 0xA6F0, 0xA6F1, 1 }, + { 0xA802, 0xA806, 4 }, + { 0xA80B, 0xA825, 26 }, + { 0xA826, 0xA82C, 6 }, + { 0xA8C4, 0xA8C5, 1 }, + { 0xA8E0, 0xA8F1, 1 }, + { 0xA8FF, 0xA926, 39 }, + { 0xA927, 0xA92D, 1 }, + { 0xA947, 0xA951, 1 }, + { 0xA980, 0xA982, 1 }, + { 0xA9B3, 0xA9B6, 3 }, + { 0xA9B7, 0xA9B9, 1 }, + { 0xA9BC, 0xA9BD, 1 }, + { 0xA9E5, 0xAA29, 68 }, + { 0xAA2A, 0xAA2E, 1 }, + { 0xAA31, 0xAA32, 1 }, + { 0xAA35, 0xAA36, 1 }, + { 0xAA43, 0xAA4C, 9 }, + { 0xAA7C, 0xAAB0, 52 }, + { 0xAAB2, 0xAAB4, 1 }, + { 0xAAB7, 0xAAB8, 1 }, + { 0xAABE, 0xAABF, 1 }, + { 0xAAC1, 0xAAEC, 43 }, + { 0xAAED, 0xAAF6, 9 }, + { 0xABE5, 0xABE8, 3 }, + { 0xABED, 0xFB1E, 20273 }, + { 0xFE00, 0xFE0F, 1 }, + { 0xFE20, 0xFE2F, 1 }, + { 0xFF9E, 0xFF9F, 1 }, + { 0x101FD, 0x102E0, 227 }, + { 0x10376, 0x1037A, 1 }, + { 0x10A01, 0x10A03, 1 }, + { 0x10A05, 0x10A06, 1 }, + { 0x10A0C, 0x10A0F, 1 }, + { 0x10A38, 0x10A3A, 1 }, + { 0x10A3F, 0x10AE5, 166 }, + { 0x10AE6, 0x10D24, 574 }, + { 0x10D25, 0x10D27, 1 }, + { 0x10EAB, 0x10EAC, 1 }, + { 0x10EFD, 0x10EFF, 1 }, + { 0x10F46, 0x10F50, 1 }, + { 0x10F82, 0x10F85, 1 }, + { 0x11001, 0x11038, 55 }, + { 0x11039, 0x11046, 1 }, + { 0x11070, 0x11073, 3 }, + { 0x11074, 0x1107F, 11 }, + { 0x11080, 0x11081, 1 }, + { 0x110B3, 0x110B6, 1 }, + { 0x110B9, 0x110BA, 1 }, + { 0x110C2, 0x11100, 62 }, + { 0x11101, 0x11102, 1 }, + { 0x11127, 0x1112B, 1 }, + { 0x1112D, 0x11134, 1 }, + { 0x11173, 0x11180, 13 }, + { 0x11181, 0x111B6, 53 }, + { 0x111B7, 0x111BE, 1 }, + { 0x111C9, 0x111CC, 1 }, + { 0x111CF, 0x1122F, 96 }, + { 0x11230, 0x11231, 1 }, + { 0x11234, 0x11236, 2 }, + { 0x11237, 0x1123E, 7 }, + { 0x11241, 0x112DF, 158 }, + { 0x112E3, 0x112EA, 1 }, + { 0x11300, 0x11301, 1 }, + { 0x1133B, 0x1133C, 1 }, + { 0x1133E, 0x11340, 2 }, + { 0x11357, 0x11366, 15 }, + { 0x11367, 0x1136C, 1 }, + { 0x11370, 0x11374, 1 }, + { 0x11438, 0x1143F, 1 }, + { 0x11442, 0x11444, 1 }, + { 0x11446, 0x1145E, 24 }, + { 0x114B0, 0x114B3, 3 }, + { 0x114B4, 0x114B8, 1 }, + { 0x114BA, 0x114BD, 3 }, + { 0x114BF, 0x114C0, 1 }, + { 0x114C2, 0x114C3, 1 }, + { 0x115AF, 0x115B2, 3 }, + { 0x115B3, 0x115B5, 1 }, + { 0x115BC, 0x115BD, 1 }, + { 0x115BF, 0x115C0, 1 }, + { 0x115DC, 0x115DD, 1 }, + { 0x11633, 0x1163A, 1 }, + { 0x1163D, 0x1163F, 2 }, + { 0x11640, 0x116AB, 107 }, + { 0x116AD, 0x116B0, 3 }, + { 0x116B1, 0x116B5, 1 }, + { 0x116B7, 0x1171D, 102 }, + { 0x1171E, 0x1171F, 1 }, + { 0x11722, 0x11725, 1 }, + { 0x11727, 0x1172B, 1 }, + { 0x1182F, 0x11837, 1 }, + { 0x11839, 0x1183A, 1 }, + { 0x11930, 0x1193B, 11 }, + { 0x1193C, 0x1193E, 2 }, + { 0x11943, 0x119D4, 145 }, + { 0x119D5, 0x119D7, 1 }, + { 0x119DA, 0x119DB, 1 }, + { 0x119E0, 0x11A01, 33 }, + { 0x11A02, 0x11A0A, 1 }, + { 0x11A33, 0x11A38, 1 }, + { 0x11A3B, 0x11A3E, 1 }, + { 0x11A47, 0x11A51, 10 }, + { 0x11A52, 0x11A56, 1 }, + { 0x11A59, 0x11A5B, 1 }, + { 0x11A8A, 0x11A96, 1 }, + { 0x11A98, 0x11A99, 1 }, + { 0x11C30, 0x11C36, 1 }, + { 0x11C38, 0x11C3D, 1 }, + { 0x11C3F, 0x11C92, 83 }, + { 0x11C93, 0x11CA7, 1 }, + { 0x11CAA, 0x11CB0, 1 }, + { 0x11CB2, 0x11CB3, 1 }, + { 0x11CB5, 0x11CB6, 1 }, + { 0x11D31, 0x11D36, 1 }, + { 0x11D3A, 0x11D3C, 2 }, + { 0x11D3D, 0x11D3F, 2 }, + { 0x11D40, 0x11D45, 1 }, + { 0x11D47, 0x11D90, 73 }, + { 0x11D91, 0x11D95, 4 }, + { 0x11D97, 0x11EF3, 348 }, + { 0x11EF4, 0x11F00, 12 }, + { 0x11F01, 0x11F36, 53 }, + { 0x11F37, 0x11F3A, 1 }, + { 0x11F40, 0x11F42, 2 }, + { 0x13440, 0x13447, 7 }, + { 0x13448, 0x13455, 1 }, + { 0x16AF0, 0x16AF4, 1 }, + { 0x16B30, 0x16B36, 1 }, + { 0x16F4F, 0x16F8F, 64 }, + { 0x16F90, 0x16F92, 1 }, + { 0x16FE4, 0x1BC9D, 19641 }, + { 0x1BC9E, 0x1CF00, 4706 }, + { 0x1CF01, 0x1CF2D, 1 }, + { 0x1CF30, 0x1CF46, 1 }, + { 0x1D165, 0x1D167, 2 }, + { 0x1D168, 0x1D169, 1 }, + { 0x1D16E, 0x1D172, 1 }, + { 0x1D17B, 0x1D182, 1 }, + { 0x1D185, 0x1D18B, 1 }, + { 0x1D1AA, 0x1D1AD, 1 }, + { 0x1D242, 0x1D244, 1 }, + { 0x1DA00, 0x1DA36, 1 }, + { 0x1DA3B, 0x1DA6C, 1 }, + { 0x1DA75, 0x1DA84, 15 }, + { 0x1DA9B, 0x1DA9F, 1 }, + { 0x1DAA1, 0x1DAAF, 1 }, + { 0x1E000, 0x1E006, 1 }, + { 0x1E008, 0x1E018, 1 }, + { 0x1E01B, 0x1E021, 1 }, + { 0x1E023, 0x1E024, 1 }, + { 0x1E026, 0x1E02A, 1 }, + { 0x1E08F, 0x1E130, 161 }, + { 0x1E131, 0x1E136, 1 }, + { 0x1E2AE, 0x1E2EC, 62 }, + { 0x1E2ED, 0x1E2EF, 1 }, + { 0x1E4EC, 0x1E4EF, 1 }, + { 0x1E8D0, 0x1E8D6, 1 }, + { 0x1E944, 0x1E94A, 1 }, + { 0xE0020, 0xE007F, 1 }, + { 0xE0100, 0xE01EF, 1 }, +}; + +static struct range_table cntrl_table[] = { + { 0x0, 0x1F, 1 }, + { 0x7F, 0x9F, 1 }, + { 0xAD, 0x600, 1363 }, + { 0x601, 0x605, 1 }, + { 0x61C, 0x6DD, 193 }, + { 0x70F, 0x890, 385 }, + { 0x891, 0x8E2, 81 }, + { 0x180E, 0x200B, 2045 }, + { 0x200C, 0x200F, 1 }, + { 0x202A, 0x202E, 1 }, + { 0x2060, 0x2064, 1 }, + { 0x2066, 0x206F, 1 }, + { 0xE000, 0xE000, 0 }, + { 0xE001, 0xF8FF, 1 }, + { 0xFEFF, 0xFFF9, 250 }, + { 0xFFFA, 0xFFFB, 1 }, + { 0x110BD, 0x110CD, 16 }, + { 0x13430, 0x1343F, 1 }, + { 0x1BCA0, 0x1BCA3, 1 }, + { 0x1D173, 0x1D17A, 1 }, + { 0xE0001, 0xE0020, 31 }, + { 0xE0021, 0xE007F, 1 }, + { 0xF0000, 0xF0000, 0 }, + { 0xF0001, 0xFFFFD, 1 }, + { 0x100000, 0x100000, 0 }, + { 0x100001, 0x10FFFD, 1 }, +}; + +static struct range_table digit_table[] = { + { 0x30, 0x39, 1 }, + { 0x660, 0x669, 1 }, + { 0x6F0, 0x6F9, 1 }, + { 0x7C0, 0x7C9, 1 }, + { 0x966, 0x96F, 1 }, + { 0x9E6, 0x9EF, 1 }, + { 0xA66, 0xA6F, 1 }, + { 0xAE6, 0xAEF, 1 }, + { 0xB66, 0xB6F, 1 }, + { 0xBE6, 0xBEF, 1 }, + { 0xC66, 0xC6F, 1 }, + { 0xCE6, 0xCEF, 1 }, + { 0xD66, 0xD6F, 1 }, + { 0xDE6, 0xDEF, 1 }, + { 0xE50, 0xE59, 1 }, + { 0xED0, 0xED9, 1 }, + { 0xF20, 0xF29, 1 }, + { 0x1040, 0x1049, 1 }, + { 0x1090, 0x1099, 1 }, + { 0x17E0, 0x17E9, 1 }, + { 0x1810, 0x1819, 1 }, + { 0x1946, 0x194F, 1 }, + { 0x19D0, 0x19D9, 1 }, + { 0x1A80, 0x1A89, 1 }, + { 0x1A90, 0x1A99, 1 }, + { 0x1B50, 0x1B59, 1 }, + { 0x1BB0, 0x1BB9, 1 }, + { 0x1C40, 0x1C49, 1 }, + { 0x1C50, 0x1C59, 1 }, + { 0xA620, 0xA629, 1 }, + { 0xA8D0, 0xA8D9, 1 }, + { 0xA900, 0xA909, 1 }, + { 0xA9D0, 0xA9D9, 1 }, + { 0xA9F0, 0xA9F9, 1 }, + { 0xAA50, 0xAA59, 1 }, + { 0xABF0, 0xABF9, 1 }, + { 0xFF10, 0xFF19, 1 }, + { 0x104A0, 0x104A9, 1 }, + { 0x10D30, 0x10D39, 1 }, + { 0x11066, 0x1106F, 1 }, + { 0x110F0, 0x110F9, 1 }, + { 0x11136, 0x1113F, 1 }, + { 0x111D0, 0x111D9, 1 }, + { 0x112F0, 0x112F9, 1 }, + { 0x11450, 0x11459, 1 }, + { 0x114D0, 0x114D9, 1 }, + { 0x11650, 0x11659, 1 }, + { 0x116C0, 0x116C9, 1 }, + { 0x11730, 0x11739, 1 }, + { 0x118E0, 0x118E9, 1 }, + { 0x11950, 0x11959, 1 }, + { 0x11C50, 0x11C59, 1 }, + { 0x11D50, 0x11D59, 1 }, + { 0x11DA0, 0x11DA9, 1 }, + { 0x11F50, 0x11F59, 1 }, + { 0x16A60, 0x16A69, 1 }, + { 0x16AC0, 0x16AC9, 1 }, + { 0x16B50, 0x16B59, 1 }, + { 0x1D7CE, 0x1D7FF, 1 }, + { 0x1E140, 0x1E149, 1 }, + { 0x1E2F0, 0x1E2F9, 1 }, + { 0x1E4F0, 0x1E4F9, 1 }, + { 0x1E950, 0x1E959, 1 }, + { 0x1FBF0, 0x1FBF9, 1 }, +}; + +static struct range_table alnum_extend_table[] = { + { 0x30, 0x39, 1 }, + { 0xB2, 0xB3, 1 }, + { 0xB9, 0xBC, 3 }, + { 0xBD, 0xBE, 1 }, + { 0x660, 0x669, 1 }, + { 0x6F0, 0x6F9, 1 }, + { 0x7C0, 0x7C9, 1 }, + { 0x966, 0x96F, 1 }, + { 0x9E6, 0x9EF, 1 }, + { 0x9F4, 0x9F9, 1 }, + { 0xA66, 0xA6F, 1 }, + { 0xAE6, 0xAEF, 1 }, + { 0xB66, 0xB6F, 1 }, + { 0xB72, 0xB77, 1 }, + { 0xBE6, 0xBF2, 1 }, + { 0xC66, 0xC6F, 1 }, + { 0xC78, 0xC7E, 1 }, + { 0xCE6, 0xCEF, 1 }, + { 0xD58, 0xD5E, 1 }, + { 0xD66, 0xD78, 1 }, + { 0xDE6, 0xDEF, 1 }, + { 0xE50, 0xE59, 1 }, + { 0xED0, 0xED9, 1 }, + { 0xF20, 0xF33, 1 }, + { 0x1040, 0x1049, 1 }, + { 0x1090, 0x1099, 1 }, + { 0x1369, 0x137C, 1 }, + { 0x16EE, 0x16F0, 1 }, + { 0x17E0, 0x17E9, 1 }, + { 0x17F0, 0x17F9, 1 }, + { 0x1810, 0x1819, 1 }, + { 0x1946, 0x194F, 1 }, + { 0x19D0, 0x19DA, 1 }, + { 0x1A80, 0x1A89, 1 }, + { 0x1A90, 0x1A99, 1 }, + { 0x1B50, 0x1B59, 1 }, + { 0x1BB0, 0x1BB9, 1 }, + { 0x1C40, 0x1C49, 1 }, + { 0x1C50, 0x1C59, 1 }, + { 0x2070, 0x2074, 4 }, + { 0x2075, 0x2079, 1 }, + { 0x2080, 0x2089, 1 }, + { 0x2150, 0x2182, 1 }, + { 0x2185, 0x2189, 1 }, + { 0x2460, 0x249B, 1 }, + { 0x24EA, 0x24FF, 1 }, + { 0x2776, 0x2793, 1 }, + { 0x2CFD, 0x3007, 778 }, + { 0x3021, 0x3029, 1 }, + { 0x3038, 0x303A, 1 }, + { 0x3192, 0x3195, 1 }, + { 0x3220, 0x3229, 1 }, + { 0x3248, 0x324F, 1 }, + { 0x3251, 0x325F, 1 }, + { 0x3280, 0x3289, 1 }, + { 0x32B1, 0x32BF, 1 }, + { 0xA620, 0xA629, 1 }, + { 0xA6E6, 0xA6EF, 1 }, + { 0xA830, 0xA835, 1 }, + { 0xA8D0, 0xA8D9, 1 }, + { 0xA900, 0xA909, 1 }, + { 0xA9D0, 0xA9D9, 1 }, + { 0xA9F0, 0xA9F9, 1 }, + { 0xAA50, 0xAA59, 1 }, + { 0xABF0, 0xABF9, 1 }, + { 0xFF10, 0xFF19, 1 }, + { 0x10107, 0x10133, 1 }, + { 0x10140, 0x10178, 1 }, + { 0x1018A, 0x1018B, 1 }, + { 0x102E1, 0x102FB, 1 }, + { 0x10320, 0x10323, 1 }, + { 0x10341, 0x1034A, 9 }, + { 0x103D1, 0x103D5, 1 }, + { 0x104A0, 0x104A9, 1 }, + { 0x10858, 0x1085F, 1 }, + { 0x10879, 0x1087F, 1 }, + { 0x108A7, 0x108AF, 1 }, + { 0x108FB, 0x108FF, 1 }, + { 0x10916, 0x1091B, 1 }, + { 0x109BC, 0x109BD, 1 }, + { 0x109C0, 0x109CF, 1 }, + { 0x109D2, 0x109FF, 1 }, + { 0x10A40, 0x10A48, 1 }, + { 0x10A7D, 0x10A7E, 1 }, + { 0x10A9D, 0x10A9F, 1 }, + { 0x10AEB, 0x10AEF, 1 }, + { 0x10B58, 0x10B5F, 1 }, + { 0x10B78, 0x10B7F, 1 }, + { 0x10BA9, 0x10BAF, 1 }, + { 0x10CFA, 0x10CFF, 1 }, + { 0x10D30, 0x10D39, 1 }, + { 0x10E60, 0x10E7E, 1 }, + { 0x10F1D, 0x10F26, 1 }, + { 0x10F51, 0x10F54, 1 }, + { 0x10FC5, 0x10FCB, 1 }, + { 0x11052, 0x1106F, 1 }, + { 0x110F0, 0x110F9, 1 }, + { 0x11136, 0x1113F, 1 }, + { 0x111D0, 0x111D9, 1 }, + { 0x111E1, 0x111F4, 1 }, + { 0x112F0, 0x112F9, 1 }, + { 0x11450, 0x11459, 1 }, + { 0x114D0, 0x114D9, 1 }, + { 0x11650, 0x11659, 1 }, + { 0x116C0, 0x116C9, 1 }, + { 0x11730, 0x1173B, 1 }, + { 0x118E0, 0x118F2, 1 }, + { 0x11950, 0x11959, 1 }, + { 0x11C50, 0x11C6C, 1 }, + { 0x11D50, 0x11D59, 1 }, + { 0x11DA0, 0x11DA9, 1 }, + { 0x11F50, 0x11F59, 1 }, + { 0x11FC0, 0x11FD4, 1 }, + { 0x12400, 0x1246E, 1 }, + { 0x16A60, 0x16A69, 1 }, + { 0x16AC0, 0x16AC9, 1 }, + { 0x16B50, 0x16B59, 1 }, + { 0x16B5B, 0x16B61, 1 }, + { 0x16E80, 0x16E96, 1 }, + { 0x1D2C0, 0x1D2D3, 1 }, + { 0x1D2E0, 0x1D2F3, 1 }, + { 0x1D360, 0x1D378, 1 }, + { 0x1D7CE, 0x1D7FF, 1 }, + { 0x1E140, 0x1E149, 1 }, + { 0x1E2F0, 0x1E2F9, 1 }, + { 0x1E4F0, 0x1E4F9, 1 }, + { 0x1E8C7, 0x1E8CF, 1 }, + { 0x1E950, 0x1E959, 1 }, + { 0x1EC71, 0x1ECAB, 1 }, + { 0x1ECAD, 0x1ECAF, 1 }, + { 0x1ECB1, 0x1ECB4, 1 }, + { 0x1ED01, 0x1ED2D, 1 }, + { 0x1ED2F, 0x1ED3D, 1 }, + { 0x1F100, 0x1F10C, 1 }, + { 0x1FBF0, 0x1FBF9, 1 }, +}; + +static struct range_table punct_table[] = { + { 0x21, 0x2F, 1 }, + { 0x3A, 0x40, 1 }, + { 0x5B, 0x60, 1 }, + { 0x7B, 0x7E, 1 }, + { 0xA1, 0xA5, 1 }, + { 0xA7, 0xA8, 1 }, + { 0xAB, 0xAC, 1 }, + { 0xAF, 0xB1, 2 }, + { 0xB4, 0xB6, 2 }, + { 0xB7, 0xB8, 1 }, + { 0xBB, 0xBF, 4 }, + { 0xD7, 0xF7, 32 }, + { 0x2C2, 0x2C5, 1 }, + { 0x2D2, 0x2DF, 1 }, + { 0x2E5, 0x2EB, 1 }, + { 0x2ED, 0x2EF, 2 }, + { 0x2F0, 0x2FF, 1 }, + { 0x375, 0x37E, 9 }, + { 0x384, 0x385, 1 }, + { 0x387, 0x3F6, 111 }, + { 0x55A, 0x55F, 1 }, + { 0x589, 0x58A, 1 }, + { 0x58F, 0x5BE, 47 }, + { 0x5C0, 0x5C6, 3 }, + { 0x5F3, 0x5F4, 1 }, + { 0x606, 0x60D, 1 }, + { 0x61B, 0x61D, 2 }, + { 0x61E, 0x61F, 1 }, + { 0x66A, 0x66D, 1 }, + { 0x6D4, 0x700, 44 }, + { 0x701, 0x70D, 1 }, + { 0x7F7, 0x7F9, 1 }, + { 0x7FE, 0x7FF, 1 }, + { 0x830, 0x83E, 1 }, + { 0x85E, 0x888, 42 }, + { 0x964, 0x965, 1 }, + { 0x970, 0x9F2, 130 }, + { 0x9F3, 0x9FB, 8 }, + { 0x9FD, 0xA76, 121 }, + { 0xAF0, 0xAF1, 1 }, + { 0xBF9, 0xC77, 126 }, + { 0xC84, 0xDF4, 368 }, + { 0xE3F, 0xE4F, 16 }, + { 0xE5A, 0xE5B, 1 }, + { 0xF04, 0xF12, 1 }, + { 0xF14, 0xF3A, 38 }, + { 0xF3B, 0xF3D, 1 }, + { 0xF85, 0xFD0, 75 }, + { 0xFD1, 0xFD4, 1 }, + { 0xFD9, 0xFDA, 1 }, + { 0x104A, 0x104F, 1 }, + { 0x10FB, 0x1360, 613 }, + { 0x1361, 0x1368, 1 }, + { 0x1400, 0x166E, 622 }, + { 0x169B, 0x169C, 1 }, + { 0x16EB, 0x16ED, 1 }, + { 0x1735, 0x1736, 1 }, + { 0x17D4, 0x17D6, 1 }, + { 0x17D8, 0x17DB, 1 }, + { 0x1800, 0x180A, 1 }, + { 0x1944, 0x1945, 1 }, + { 0x1A1E, 0x1A1F, 1 }, + { 0x1AA0, 0x1AA6, 1 }, + { 0x1AA8, 0x1AAD, 1 }, + { 0x1B5A, 0x1B60, 1 }, + { 0x1B7D, 0x1B7E, 1 }, + { 0x1BFC, 0x1BFF, 1 }, + { 0x1C3B, 0x1C3F, 1 }, + { 0x1C7E, 0x1C7F, 1 }, + { 0x1CC0, 0x1CC7, 1 }, + { 0x1CD3, 0x1FBD, 746 }, + { 0x1FBF, 0x1FC1, 1 }, + { 0x1FCD, 0x1FCF, 1 }, + { 0x1FDD, 0x1FDF, 1 }, + { 0x1FED, 0x1FEF, 1 }, + { 0x1FFD, 0x1FFE, 1 }, + { 0x2010, 0x2027, 1 }, + { 0x2030, 0x205E, 1 }, + { 0x207A, 0x207E, 1 }, + { 0x208A, 0x208E, 1 }, + { 0x20A0, 0x20C0, 1 }, + { 0x2118, 0x2140, 40 }, + { 0x2141, 0x2144, 1 }, + { 0x214B, 0x2190, 69 }, + { 0x2191, 0x2194, 1 }, + { 0x219A, 0x219B, 1 }, + { 0x21A0, 0x21A6, 3 }, + { 0x21AE, 0x21CE, 32 }, + { 0x21CF, 0x21D2, 3 }, + { 0x21D4, 0x21F4, 32 }, + { 0x21F5, 0x22FF, 1 }, + { 0x2308, 0x230B, 1 }, + { 0x2320, 0x2321, 1 }, + { 0x2329, 0x232A, 1 }, + { 0x237C, 0x239B, 31 }, + { 0x239C, 0x23B3, 1 }, + { 0x23DC, 0x23E1, 1 }, + { 0x25B7, 0x25C1, 10 }, + { 0x25F8, 0x25FF, 1 }, + { 0x266F, 0x2768, 249 }, + { 0x2769, 0x2775, 1 }, + { 0x27C0, 0x27FF, 1 }, + { 0x2900, 0x2AFF, 1 }, + { 0x2B30, 0x2B44, 1 }, + { 0x2B47, 0x2B4C, 1 }, + { 0x2CF9, 0x2CFC, 1 }, + { 0x2CFE, 0x2CFF, 1 }, + { 0x2D70, 0x2E00, 144 }, + { 0x2E01, 0x2E2E, 1 }, + { 0x2E30, 0x2E4F, 1 }, + { 0x2E52, 0x2E5D, 1 }, + { 0x3001, 0x3003, 1 }, + { 0x3008, 0x3011, 1 }, + { 0x3014, 0x301F, 1 }, + { 0x3030, 0x303D, 13 }, + { 0x309B, 0x309C, 1 }, + { 0x30A0, 0x30FB, 91 }, + { 0xA4FE, 0xA4FF, 1 }, + { 0xA60D, 0xA60F, 1 }, + { 0xA673, 0xA67E, 11 }, + { 0xA6F2, 0xA6F7, 1 }, + { 0xA700, 0xA716, 1 }, + { 0xA720, 0xA721, 1 }, + { 0xA789, 0xA78A, 1 }, + { 0xA838, 0xA874, 60 }, + { 0xA875, 0xA877, 1 }, + { 0xA8CE, 0xA8CF, 1 }, + { 0xA8F8, 0xA8FA, 1 }, + { 0xA8FC, 0xA92E, 50 }, + { 0xA92F, 0xA95F, 48 }, + { 0xA9C1, 0xA9CD, 1 }, + { 0xA9DE, 0xA9DF, 1 }, + { 0xAA5C, 0xAA5F, 1 }, + { 0xAADE, 0xAADF, 1 }, + { 0xAAF0, 0xAAF1, 1 }, + { 0xAB5B, 0xAB6A, 15 }, + { 0xAB6B, 0xABEB, 128 }, + { 0xFB29, 0xFBB2, 137 }, + { 0xFBB3, 0xFBC2, 1 }, + { 0xFD3E, 0xFD3F, 1 }, + { 0xFDFC, 0xFE10, 20 }, + { 0xFE11, 0xFE19, 1 }, + { 0xFE30, 0xFE52, 1 }, + { 0xFE54, 0xFE66, 1 }, + { 0xFE68, 0xFE6B, 1 }, + { 0xFF01, 0xFF0F, 1 }, + { 0xFF1A, 0xFF20, 1 }, + { 0xFF3B, 0xFF40, 1 }, + { 0xFF5B, 0xFF65, 1 }, + { 0xFFE0, 0xFFE3, 1 }, + { 0xFFE5, 0xFFE6, 1 }, + { 0xFFE9, 0xFFEC, 1 }, + { 0x10100, 0x10102, 1 }, + { 0x1039F, 0x103D0, 49 }, + { 0x1056F, 0x10857, 744 }, + { 0x1091F, 0x1093F, 32 }, + { 0x10A50, 0x10A58, 1 }, + { 0x10A7F, 0x10AF0, 113 }, + { 0x10AF1, 0x10AF6, 1 }, + { 0x10B39, 0x10B3F, 1 }, + { 0x10B99, 0x10B9C, 1 }, + { 0x10EAD, 0x10F55, 168 }, + { 0x10F56, 0x10F59, 1 }, + { 0x10F86, 0x10F89, 1 }, + { 0x11047, 0x1104D, 1 }, + { 0x110BB, 0x110BC, 1 }, + { 0x110BE, 0x110C1, 1 }, + { 0x11140, 0x11143, 1 }, + { 0x11174, 0x11175, 1 }, + { 0x111C5, 0x111C8, 1 }, + { 0x111CD, 0x111DB, 14 }, + { 0x111DD, 0x111DF, 1 }, + { 0x11238, 0x1123D, 1 }, + { 0x112A9, 0x1144B, 418 }, + { 0x1144C, 0x1144F, 1 }, + { 0x1145A, 0x1145B, 1 }, + { 0x1145D, 0x114C6, 105 }, + { 0x115C1, 0x115D7, 1 }, + { 0x11641, 0x11643, 1 }, + { 0x11660, 0x1166C, 1 }, + { 0x116B9, 0x1173C, 131 }, + { 0x1173D, 0x1173E, 1 }, + { 0x1183B, 0x11944, 265 }, + { 0x11945, 0x11946, 1 }, + { 0x119E2, 0x11A3F, 93 }, + { 0x11A40, 0x11A46, 1 }, + { 0x11A9A, 0x11A9C, 1 }, + { 0x11A9E, 0x11AA2, 1 }, + { 0x11B00, 0x11B09, 1 }, + { 0x11C41, 0x11C45, 1 }, + { 0x11C70, 0x11C71, 1 }, + { 0x11EF7, 0x11EF8, 1 }, + { 0x11F43, 0x11F4F, 1 }, + { 0x11FDD, 0x11FE0, 1 }, + { 0x11FFF, 0x12470, 1137 }, + { 0x12471, 0x12474, 1 }, + { 0x12FF1, 0x12FF2, 1 }, + { 0x16A6E, 0x16A6F, 1 }, + { 0x16AF5, 0x16B37, 66 }, + { 0x16B38, 0x16B3B, 1 }, + { 0x16B44, 0x16E97, 851 }, + { 0x16E98, 0x16E9A, 1 }, + { 0x16FE2, 0x1BC9F, 19645 }, + { 0x1D6C1, 0x1D6DB, 26 }, + { 0x1D6FB, 0x1D715, 26 }, + { 0x1D735, 0x1D74F, 26 }, + { 0x1D76F, 0x1D789, 26 }, + { 0x1D7A9, 0x1D7C3, 26 }, + { 0x1DA87, 0x1DA8B, 1 }, + { 0x1E2FF, 0x1E95E, 1631 }, + { 0x1E95F, 0x1ECB0, 849 }, + { 0x1EEF0, 0x1EEF1, 1 }, + { 0x1F3FB, 0x1F3FF, 1 }, +}; + +static struct conv_table tolower_table[] = { + { 0x41, 0x5A, 1, 32 }, + { 0xC0, 0xD6, 1, 32 }, + { 0xD8, 0xDE, 1, 32 }, + { 0x100, 0x12E, 2, 1 }, + { 0x130, 0x130, 1, -199 }, + { 0x132, 0x136, 2, 1 }, + { 0x139, 0x147, 2, 1 }, + { 0x14A, 0x176, 2, 1 }, + { 0x178, 0x178, 1, -121 }, + { 0x179, 0x17D, 2, 1 }, + { 0x181, 0x181, 1, 210 }, + { 0x182, 0x184, 2, 1 }, + { 0x186, 0x186, 1, 206 }, + { 0x187, 0x187, 1, 1 }, + { 0x189, 0x18A, 1, 205 }, + { 0x18B, 0x18B, 1, 1 }, + { 0x18E, 0x18E, 1, 79 }, + { 0x18F, 0x18F, 1, 202 }, + { 0x190, 0x190, 1, 203 }, + { 0x191, 0x191, 1, 1 }, + { 0x193, 0x193, 1, 205 }, + { 0x194, 0x194, 1, 207 }, + { 0x196, 0x196, 1, 211 }, + { 0x197, 0x197, 1, 209 }, + { 0x198, 0x198, 1, 1 }, + { 0x19C, 0x19C, 1, 211 }, + { 0x19D, 0x19D, 1, 213 }, + { 0x19F, 0x19F, 1, 214 }, + { 0x1A0, 0x1A4, 2, 1 }, + { 0x1A6, 0x1A6, 1, 218 }, + { 0x1A7, 0x1A7, 1, 1 }, + { 0x1A9, 0x1A9, 1, 218 }, + { 0x1AC, 0x1AC, 1, 1 }, + { 0x1AE, 0x1AE, 1, 218 }, + { 0x1AF, 0x1AF, 1, 1 }, + { 0x1B1, 0x1B2, 1, 217 }, + { 0x1B3, 0x1B5, 2, 1 }, + { 0x1B7, 0x1B7, 1, 219 }, + { 0x1B8, 0x1BC, 4, 1 }, + { 0x1C4, 0x1C4, 1, 2 }, + { 0x1C5, 0x1C5, 1, 1 }, + { 0x1C7, 0x1C7, 1, 2 }, + { 0x1C8, 0x1C8, 1, 1 }, + { 0x1CA, 0x1CA, 1, 2 }, + { 0x1CB, 0x1DB, 2, 1 }, + { 0x1DE, 0x1EE, 2, 1 }, + { 0x1F1, 0x1F1, 1, 2 }, + { 0x1F2, 0x1F4, 2, 1 }, + { 0x1F6, 0x1F6, 1, -97 }, + { 0x1F7, 0x1F7, 1, -56 }, + { 0x1F8, 0x21E, 2, 1 }, + { 0x220, 0x220, 1, -130 }, + { 0x222, 0x232, 2, 1 }, + { 0x23A, 0x23A, 1, 10795 }, + { 0x23B, 0x23B, 1, 1 }, + { 0x23D, 0x23D, 1, -163 }, + { 0x23E, 0x23E, 1, 10792 }, + { 0x241, 0x241, 1, 1 }, + { 0x243, 0x243, 1, -195 }, + { 0x244, 0x244, 1, 69 }, + { 0x245, 0x245, 1, 71 }, + { 0x246, 0x24E, 2, 1 }, + { 0x370, 0x372, 2, 1 }, + { 0x376, 0x376, 1, 1 }, + { 0x37F, 0x37F, 1, 116 }, + { 0x386, 0x386, 1, 38 }, + { 0x388, 0x38A, 1, 37 }, + { 0x38C, 0x38C, 1, 64 }, + { 0x38E, 0x38F, 1, 63 }, + { 0x391, 0x3A1, 1, 32 }, + { 0x3A3, 0x3AB, 1, 32 }, + { 0x3CF, 0x3CF, 1, 8 }, + { 0x3D8, 0x3EE, 2, 1 }, + { 0x3F4, 0x3F4, 1, -60 }, + { 0x3F7, 0x3F7, 1, 1 }, + { 0x3F9, 0x3F9, 1, -7 }, + { 0x3FA, 0x3FA, 1, 1 }, + { 0x3FD, 0x3FF, 1, -130 }, + { 0x400, 0x40F, 1, 80 }, + { 0x410, 0x42F, 1, 32 }, + { 0x460, 0x480, 2, 1 }, + { 0x48A, 0x4BE, 2, 1 }, + { 0x4C0, 0x4C0, 1, 15 }, + { 0x4C1, 0x4CD, 2, 1 }, + { 0x4D0, 0x52E, 2, 1 }, + { 0x531, 0x556, 1, 48 }, + { 0x10A0, 0x10C5, 1, 7264 }, + { 0x10C7, 0x10CD, 6, 7264 }, + { 0x13A0, 0x13EF, 1, 38864 }, + { 0x13F0, 0x13F5, 1, 8 }, + { 0x1C90, 0x1CBA, 1, -3008 }, + { 0x1CBD, 0x1CBF, 1, -3008 }, + { 0x1E00, 0x1E94, 2, 1 }, + { 0x1E9E, 0x1E9E, 1, -7615 }, + { 0x1EA0, 0x1EFE, 2, 1 }, + { 0x1F08, 0x1F0F, 1, -8 }, + { 0x1F18, 0x1F1D, 1, -8 }, + { 0x1F28, 0x1F2F, 1, -8 }, + { 0x1F38, 0x1F3F, 1, -8 }, + { 0x1F48, 0x1F4D, 1, -8 }, + { 0x1F59, 0x1F5F, 2, -8 }, + { 0x1F68, 0x1F6F, 1, -8 }, + { 0x1F88, 0x1F8F, 1, -8 }, + { 0x1F98, 0x1F9F, 1, -8 }, + { 0x1FA8, 0x1FAF, 1, -8 }, + { 0x1FB8, 0x1FB9, 1, -8 }, + { 0x1FBA, 0x1FBB, 1, -74 }, + { 0x1FBC, 0x1FBC, 1, -9 }, + { 0x1FC8, 0x1FCB, 1, -86 }, + { 0x1FCC, 0x1FCC, 1, -9 }, + { 0x1FD8, 0x1FD9, 1, -8 }, + { 0x1FDA, 0x1FDB, 1, -100 }, + { 0x1FE8, 0x1FE9, 1, -8 }, + { 0x1FEA, 0x1FEB, 1, -112 }, + { 0x1FEC, 0x1FEC, 1, -7 }, + { 0x1FF8, 0x1FF9, 1, -128 }, + { 0x1FFA, 0x1FFB, 1, -126 }, + { 0x1FFC, 0x1FFC, 1, -9 }, + { 0x2126, 0x2126, 1, -7517 }, + { 0x212A, 0x212A, 1, -8383 }, + { 0x212B, 0x212B, 1, -8262 }, + { 0x2132, 0x2132, 1, 28 }, + { 0x2160, 0x216F, 1, 16 }, + { 0x2183, 0x2183, 1, 1 }, + { 0x24B6, 0x24CF, 1, 26 }, + { 0x2C00, 0x2C2F, 1, 48 }, + { 0x2C60, 0x2C60, 1, 1 }, + { 0x2C62, 0x2C62, 1, -10743 }, + { 0x2C63, 0x2C63, 1, -3814 }, + { 0x2C64, 0x2C64, 1, -10727 }, + { 0x2C67, 0x2C6B, 2, 1 }, + { 0x2C6D, 0x2C6D, 1, -10780 }, + { 0x2C6E, 0x2C6E, 1, -10749 }, + { 0x2C6F, 0x2C6F, 1, -10783 }, + { 0x2C70, 0x2C70, 1, -10782 }, + { 0x2C72, 0x2C75, 3, 1 }, + { 0x2C7E, 0x2C7F, 1, -10815 }, + { 0x2C80, 0x2CE2, 2, 1 }, + { 0x2CEB, 0x2CED, 2, 1 }, + { 0x2CF2, 0xA640, 31054, 1 }, + { 0xA642, 0xA66C, 2, 1 }, + { 0xA680, 0xA69A, 2, 1 }, + { 0xA722, 0xA72E, 2, 1 }, + { 0xA732, 0xA76E, 2, 1 }, + { 0xA779, 0xA77B, 2, 1 }, + { 0xA77D, 0xA77D, 1, -35332 }, + { 0xA77E, 0xA786, 2, 1 }, + { 0xA78B, 0xA78B, 1, 1 }, + { 0xA78D, 0xA78D, 1, -42280 }, + { 0xA790, 0xA792, 2, 1 }, + { 0xA796, 0xA7A8, 2, 1 }, + { 0xA7AA, 0xA7AA, 1, -42308 }, + { 0xA7AB, 0xA7AB, 1, -42319 }, + { 0xA7AC, 0xA7AC, 1, -42315 }, + { 0xA7AD, 0xA7AD, 1, -42305 }, + { 0xA7AE, 0xA7AE, 1, -42308 }, + { 0xA7B0, 0xA7B0, 1, -42258 }, + { 0xA7B1, 0xA7B1, 1, -42282 }, + { 0xA7B2, 0xA7B2, 1, -42261 }, + { 0xA7B3, 0xA7B3, 1, 928 }, + { 0xA7B4, 0xA7C2, 2, 1 }, + { 0xA7C4, 0xA7C4, 1, -48 }, + { 0xA7C5, 0xA7C5, 1, -42307 }, + { 0xA7C6, 0xA7C6, 1, -35384 }, + { 0xA7C7, 0xA7C9, 2, 1 }, + { 0xA7D0, 0xA7D6, 6, 1 }, + { 0xA7D8, 0xA7F5, 29, 1 }, + { 0xFF21, 0xFF3A, 1, 32 }, + { 0x10400, 0x10427, 1, 40 }, + { 0x104B0, 0x104D3, 1, 40 }, + { 0x10570, 0x1057A, 1, 39 }, + { 0x1057C, 0x1058A, 1, 39 }, + { 0x1058C, 0x10592, 1, 39 }, + { 0x10594, 0x10595, 1, 39 }, + { 0x10C80, 0x10CB2, 1, 64 }, + { 0x118A0, 0x118BF, 1, 32 }, + { 0x16E40, 0x16E5F, 1, 32 }, + { 0x1E900, 0x1E921, 1, 34 }, +}; + +static struct conv_table toupper_table[] = { + { 0x61, 0x7A, 1, -32 }, + { 0xB5, 0xB5, 1, 743 }, + { 0xE0, 0xF6, 1, -32 }, + { 0xF8, 0xFE, 1, -32 }, + { 0xFF, 0xFF, 1, 121 }, + { 0x101, 0x12F, 2, -1 }, + { 0x131, 0x131, 1, -232 }, + { 0x133, 0x137, 2, -1 }, + { 0x13A, 0x148, 2, -1 }, + { 0x14B, 0x177, 2, -1 }, + { 0x17A, 0x17E, 2, -1 }, + { 0x17F, 0x17F, 1, -300 }, + { 0x180, 0x180, 1, 195 }, + { 0x183, 0x185, 2, -1 }, + { 0x188, 0x18C, 4, -1 }, + { 0x192, 0x192, 1, -1 }, + { 0x195, 0x195, 1, 97 }, + { 0x199, 0x199, 1, -1 }, + { 0x19A, 0x19A, 1, 163 }, + { 0x19E, 0x19E, 1, 130 }, + { 0x1A1, 0x1A5, 2, -1 }, + { 0x1A8, 0x1AD, 5, -1 }, + { 0x1B0, 0x1B4, 4, -1 }, + { 0x1B6, 0x1B9, 3, -1 }, + { 0x1BD, 0x1BD, 1, -1 }, + { 0x1BF, 0x1BF, 1, 56 }, + { 0x1C5, 0x1C5, 1, -1 }, + { 0x1C6, 0x1C6, 1, -2 }, + { 0x1C8, 0x1C8, 1, -1 }, + { 0x1C9, 0x1C9, 1, -2 }, + { 0x1CB, 0x1CB, 1, -1 }, + { 0x1CC, 0x1CC, 1, -2 }, + { 0x1CE, 0x1DC, 2, -1 }, + { 0x1DD, 0x1DD, 1, -79 }, + { 0x1DF, 0x1EF, 2, -1 }, + { 0x1F2, 0x1F2, 1, -1 }, + { 0x1F3, 0x1F3, 1, -2 }, + { 0x1F5, 0x1F9, 4, -1 }, + { 0x1FB, 0x21F, 2, -1 }, + { 0x223, 0x233, 2, -1 }, + { 0x23C, 0x23C, 1, -1 }, + { 0x23F, 0x240, 1, 10815 }, + { 0x242, 0x247, 5, -1 }, + { 0x249, 0x24F, 2, -1 }, + { 0x250, 0x250, 1, 10783 }, + { 0x251, 0x251, 1, 10780 }, + { 0x252, 0x252, 1, 10782 }, + { 0x253, 0x253, 1, -210 }, + { 0x254, 0x254, 1, -206 }, + { 0x256, 0x257, 1, -205 }, + { 0x259, 0x259, 1, -202 }, + { 0x25B, 0x25B, 1, -203 }, + { 0x25C, 0x25C, 1, 42319 }, + { 0x260, 0x260, 1, -205 }, + { 0x261, 0x261, 1, 42315 }, + { 0x263, 0x263, 1, -207 }, + { 0x265, 0x265, 1, 42280 }, + { 0x266, 0x266, 1, 42308 }, + { 0x268, 0x268, 1, -209 }, + { 0x269, 0x269, 1, -211 }, + { 0x26A, 0x26A, 1, 42308 }, + { 0x26B, 0x26B, 1, 10743 }, + { 0x26C, 0x26C, 1, 42305 }, + { 0x26F, 0x26F, 1, -211 }, + { 0x271, 0x271, 1, 10749 }, + { 0x272, 0x272, 1, -213 }, + { 0x275, 0x275, 1, -214 }, + { 0x27D, 0x27D, 1, 10727 }, + { 0x280, 0x280, 1, -218 }, + { 0x282, 0x282, 1, 42307 }, + { 0x283, 0x283, 1, -218 }, + { 0x287, 0x287, 1, 42282 }, + { 0x288, 0x288, 1, -218 }, + { 0x289, 0x289, 1, -69 }, + { 0x28A, 0x28B, 1, -217 }, + { 0x28C, 0x28C, 1, -71 }, + { 0x292, 0x292, 1, -219 }, + { 0x29D, 0x29D, 1, 42261 }, + { 0x29E, 0x29E, 1, 42258 }, + { 0x345, 0x345, 1, 84 }, + { 0x371, 0x373, 2, -1 }, + { 0x377, 0x377, 1, -1 }, + { 0x37B, 0x37D, 1, 130 }, + { 0x3AC, 0x3AC, 1, -38 }, + { 0x3AD, 0x3AF, 1, -37 }, + { 0x3B1, 0x3C1, 1, -32 }, + { 0x3C2, 0x3C2, 1, -31 }, + { 0x3C3, 0x3CB, 1, -32 }, + { 0x3CC, 0x3CC, 1, -64 }, + { 0x3CD, 0x3CE, 1, -63 }, + { 0x3D0, 0x3D0, 1, -62 }, + { 0x3D1, 0x3D1, 1, -57 }, + { 0x3D5, 0x3D5, 1, -47 }, + { 0x3D6, 0x3D6, 1, -54 }, + { 0x3D7, 0x3D7, 1, -8 }, + { 0x3D9, 0x3EF, 2, -1 }, + { 0x3F0, 0x3F0, 1, -86 }, + { 0x3F1, 0x3F1, 1, -80 }, + { 0x3F2, 0x3F2, 1, 7 }, + { 0x3F3, 0x3F3, 1, -116 }, + { 0x3F5, 0x3F5, 1, -96 }, + { 0x3F8, 0x3FB, 3, -1 }, + { 0x430, 0x44F, 1, -32 }, + { 0x450, 0x45F, 1, -80 }, + { 0x461, 0x481, 2, -1 }, + { 0x48B, 0x4BF, 2, -1 }, + { 0x4C2, 0x4CE, 2, -1 }, + { 0x4CF, 0x4CF, 1, -15 }, + { 0x4D1, 0x52F, 2, -1 }, + { 0x561, 0x586, 1, -48 }, + { 0x10D0, 0x10FA, 1, 3008 }, + { 0x10FD, 0x10FF, 1, 3008 }, + { 0x13F8, 0x13FD, 1, -8 }, + { 0x1C80, 0x1C80, 1, -6254 }, + { 0x1C81, 0x1C81, 1, -6253 }, + { 0x1C82, 0x1C82, 1, -6244 }, + { 0x1C83, 0x1C84, 1, -6242 }, + { 0x1C85, 0x1C85, 1, -6243 }, + { 0x1C86, 0x1C86, 1, -6236 }, + { 0x1C87, 0x1C87, 1, -6181 }, + { 0x1C88, 0x1C88, 1, 35266 }, + { 0x1D79, 0x1D79, 1, 35332 }, + { 0x1D7D, 0x1D7D, 1, 3814 }, + { 0x1D8E, 0x1D8E, 1, 35384 }, + { 0x1E01, 0x1E95, 2, -1 }, + { 0x1E9B, 0x1E9B, 1, -59 }, + { 0x1EA1, 0x1EFF, 2, -1 }, + { 0x1F00, 0x1F07, 1, 8 }, + { 0x1F10, 0x1F15, 1, 8 }, + { 0x1F20, 0x1F27, 1, 8 }, + { 0x1F30, 0x1F37, 1, 8 }, + { 0x1F40, 0x1F45, 1, 8 }, + { 0x1F51, 0x1F57, 2, 8 }, + { 0x1F60, 0x1F67, 1, 8 }, + { 0x1F70, 0x1F71, 1, 74 }, + { 0x1F72, 0x1F75, 1, 86 }, + { 0x1F76, 0x1F77, 1, 100 }, + { 0x1F78, 0x1F79, 1, 128 }, + { 0x1F7A, 0x1F7B, 1, 112 }, + { 0x1F7C, 0x1F7D, 1, 126 }, + { 0x1F80, 0x1F87, 1, 8 }, + { 0x1F90, 0x1F97, 1, 8 }, + { 0x1FA0, 0x1FA7, 1, 8 }, + { 0x1FB0, 0x1FB1, 1, 8 }, + { 0x1FB3, 0x1FB3, 1, 9 }, + { 0x1FBE, 0x1FBE, 1, -7205 }, + { 0x1FC3, 0x1FC3, 1, 9 }, + { 0x1FD0, 0x1FD1, 1, 8 }, + { 0x1FE0, 0x1FE1, 1, 8 }, + { 0x1FE5, 0x1FE5, 1, 7 }, + { 0x1FF3, 0x1FF3, 1, 9 }, + { 0x214E, 0x214E, 1, -28 }, + { 0x2170, 0x217F, 1, -16 }, + { 0x2184, 0x2184, 1, -1 }, + { 0x24D0, 0x24E9, 1, -26 }, + { 0x2C30, 0x2C5F, 1, -48 }, + { 0x2C61, 0x2C61, 1, -1 }, + { 0x2C65, 0x2C65, 1, -10795 }, + { 0x2C66, 0x2C66, 1, -10792 }, + { 0x2C68, 0x2C6C, 2, -1 }, + { 0x2C73, 0x2C76, 3, -1 }, + { 0x2C81, 0x2CE3, 2, -1 }, + { 0x2CEC, 0x2CEE, 2, -1 }, + { 0x2CF3, 0x2CF3, 1, -1 }, + { 0x2D00, 0x2D25, 1, -7264 }, + { 0x2D27, 0x2D2D, 6, -7264 }, + { 0xA641, 0xA66D, 2, -1 }, + { 0xA681, 0xA69B, 2, -1 }, + { 0xA723, 0xA72F, 2, -1 }, + { 0xA733, 0xA76F, 2, -1 }, + { 0xA77A, 0xA77C, 2, -1 }, + { 0xA77F, 0xA787, 2, -1 }, + { 0xA78C, 0xA791, 5, -1 }, + { 0xA793, 0xA793, 1, -1 }, + { 0xA794, 0xA794, 1, 48 }, + { 0xA797, 0xA7A9, 2, -1 }, + { 0xA7B5, 0xA7C3, 2, -1 }, + { 0xA7C8, 0xA7CA, 2, -1 }, + { 0xA7D1, 0xA7D7, 6, -1 }, + { 0xA7D9, 0xA7F6, 29, -1 }, + { 0xAB53, 0xAB53, 1, -928 }, + { 0xAB70, 0xABBF, 1, -38864 }, + { 0xFF41, 0xFF5A, 1, -32 }, + { 0x10428, 0x1044F, 1, -40 }, + { 0x104D8, 0x104FB, 1, -40 }, + { 0x10597, 0x105A1, 1, -39 }, + { 0x105A3, 0x105B1, 1, -39 }, + { 0x105B3, 0x105B9, 1, -39 }, + { 0x105BB, 0x105BC, 1, -39 }, + { 0x10CC0, 0x10CF2, 1, -64 }, + { 0x118C0, 0x118DF, 1, -32 }, + { 0x16E60, 0x16E7F, 1, -32 }, + { 0x1E922, 0x1E943, 1, -34 }, +}; + +static struct conv_table totitle_table[] = { + { 0x61, 0x7A, 1, -32 }, + { 0xB5, 0xB5, 1, 743 }, + { 0xE0, 0xF6, 1, -32 }, + { 0xF8, 0xFE, 1, -32 }, + { 0xFF, 0xFF, 1, 121 }, + { 0x101, 0x12F, 2, -1 }, + { 0x131, 0x131, 1, -232 }, + { 0x133, 0x137, 2, -1 }, + { 0x13A, 0x148, 2, -1 }, + { 0x14B, 0x177, 2, -1 }, + { 0x17A, 0x17E, 2, -1 }, + { 0x17F, 0x17F, 1, -300 }, + { 0x180, 0x180, 1, 195 }, + { 0x183, 0x185, 2, -1 }, + { 0x188, 0x18C, 4, -1 }, + { 0x192, 0x192, 1, -1 }, + { 0x195, 0x195, 1, 97 }, + { 0x199, 0x199, 1, -1 }, + { 0x19A, 0x19A, 1, 163 }, + { 0x19E, 0x19E, 1, 130 }, + { 0x1A1, 0x1A5, 2, -1 }, + { 0x1A8, 0x1AD, 5, -1 }, + { 0x1B0, 0x1B4, 4, -1 }, + { 0x1B6, 0x1B9, 3, -1 }, + { 0x1BD, 0x1BD, 1, -1 }, + { 0x1BF, 0x1BF, 1, 56 }, + { 0x1C4, 0x1C4, 1, 1 }, + { 0x1C5, 0x1C5, 1, 0 }, + { 0x1C6, 0x1C6, 1, -1 }, + { 0x1C7, 0x1C7, 1, 1 }, + { 0x1C8, 0x1C8, 1, 0 }, + { 0x1C9, 0x1C9, 1, -1 }, + { 0x1CA, 0x1CA, 1, 1 }, + { 0x1CB, 0x1CB, 1, 0 }, + { 0x1CC, 0x1DC, 2, -1 }, + { 0x1DD, 0x1DD, 1, -79 }, + { 0x1DF, 0x1EF, 2, -1 }, + { 0x1F1, 0x1F1, 1, 1 }, + { 0x1F2, 0x1F2, 1, 0 }, + { 0x1F3, 0x1F5, 2, -1 }, + { 0x1F9, 0x21F, 2, -1 }, + { 0x223, 0x233, 2, -1 }, + { 0x23C, 0x23C, 1, -1 }, + { 0x23F, 0x240, 1, 10815 }, + { 0x242, 0x247, 5, -1 }, + { 0x249, 0x24F, 2, -1 }, + { 0x250, 0x250, 1, 10783 }, + { 0x251, 0x251, 1, 10780 }, + { 0x252, 0x252, 1, 10782 }, + { 0x253, 0x253, 1, -210 }, + { 0x254, 0x254, 1, -206 }, + { 0x256, 0x257, 1, -205 }, + { 0x259, 0x259, 1, -202 }, + { 0x25B, 0x25B, 1, -203 }, + { 0x25C, 0x25C, 1, 42319 }, + { 0x260, 0x260, 1, -205 }, + { 0x261, 0x261, 1, 42315 }, + { 0x263, 0x263, 1, -207 }, + { 0x265, 0x265, 1, 42280 }, + { 0x266, 0x266, 1, 42308 }, + { 0x268, 0x268, 1, -209 }, + { 0x269, 0x269, 1, -211 }, + { 0x26A, 0x26A, 1, 42308 }, + { 0x26B, 0x26B, 1, 10743 }, + { 0x26C, 0x26C, 1, 42305 }, + { 0x26F, 0x26F, 1, -211 }, + { 0x271, 0x271, 1, 10749 }, + { 0x272, 0x272, 1, -213 }, + { 0x275, 0x275, 1, -214 }, + { 0x27D, 0x27D, 1, 10727 }, + { 0x280, 0x280, 1, -218 }, + { 0x282, 0x282, 1, 42307 }, + { 0x283, 0x283, 1, -218 }, + { 0x287, 0x287, 1, 42282 }, + { 0x288, 0x288, 1, -218 }, + { 0x289, 0x289, 1, -69 }, + { 0x28A, 0x28B, 1, -217 }, + { 0x28C, 0x28C, 1, -71 }, + { 0x292, 0x292, 1, -219 }, + { 0x29D, 0x29D, 1, 42261 }, + { 0x29E, 0x29E, 1, 42258 }, + { 0x345, 0x345, 1, 84 }, + { 0x371, 0x373, 2, -1 }, + { 0x377, 0x377, 1, -1 }, + { 0x37B, 0x37D, 1, 130 }, + { 0x3AC, 0x3AC, 1, -38 }, + { 0x3AD, 0x3AF, 1, -37 }, + { 0x3B1, 0x3C1, 1, -32 }, + { 0x3C2, 0x3C2, 1, -31 }, + { 0x3C3, 0x3CB, 1, -32 }, + { 0x3CC, 0x3CC, 1, -64 }, + { 0x3CD, 0x3CE, 1, -63 }, + { 0x3D0, 0x3D0, 1, -62 }, + { 0x3D1, 0x3D1, 1, -57 }, + { 0x3D5, 0x3D5, 1, -47 }, + { 0x3D6, 0x3D6, 1, -54 }, + { 0x3D7, 0x3D7, 1, -8 }, + { 0x3D9, 0x3EF, 2, -1 }, + { 0x3F0, 0x3F0, 1, -86 }, + { 0x3F1, 0x3F1, 1, -80 }, + { 0x3F2, 0x3F2, 1, 7 }, + { 0x3F3, 0x3F3, 1, -116 }, + { 0x3F5, 0x3F5, 1, -96 }, + { 0x3F8, 0x3FB, 3, -1 }, + { 0x430, 0x44F, 1, -32 }, + { 0x450, 0x45F, 1, -80 }, + { 0x461, 0x481, 2, -1 }, + { 0x48B, 0x4BF, 2, -1 }, + { 0x4C2, 0x4CE, 2, -1 }, + { 0x4CF, 0x4CF, 1, -15 }, + { 0x4D1, 0x52F, 2, -1 }, + { 0x561, 0x586, 1, -48 }, + { 0x10D0, 0x10FA, 1, 0 }, + { 0x10FD, 0x10FF, 1, 0 }, + { 0x13F8, 0x13FD, 1, -8 }, + { 0x1C80, 0x1C80, 1, -6254 }, + { 0x1C81, 0x1C81, 1, -6253 }, + { 0x1C82, 0x1C82, 1, -6244 }, + { 0x1C83, 0x1C84, 1, -6242 }, + { 0x1C85, 0x1C85, 1, -6243 }, + { 0x1C86, 0x1C86, 1, -6236 }, + { 0x1C87, 0x1C87, 1, -6181 }, + { 0x1C88, 0x1C88, 1, 35266 }, + { 0x1D79, 0x1D79, 1, 35332 }, + { 0x1D7D, 0x1D7D, 1, 3814 }, + { 0x1D8E, 0x1D8E, 1, 35384 }, + { 0x1E01, 0x1E95, 2, -1 }, + { 0x1E9B, 0x1E9B, 1, -59 }, + { 0x1EA1, 0x1EFF, 2, -1 }, + { 0x1F00, 0x1F07, 1, 8 }, + { 0x1F10, 0x1F15, 1, 8 }, + { 0x1F20, 0x1F27, 1, 8 }, + { 0x1F30, 0x1F37, 1, 8 }, + { 0x1F40, 0x1F45, 1, 8 }, + { 0x1F51, 0x1F57, 2, 8 }, + { 0x1F60, 0x1F67, 1, 8 }, + { 0x1F70, 0x1F71, 1, 74 }, + { 0x1F72, 0x1F75, 1, 86 }, + { 0x1F76, 0x1F77, 1, 100 }, + { 0x1F78, 0x1F79, 1, 128 }, + { 0x1F7A, 0x1F7B, 1, 112 }, + { 0x1F7C, 0x1F7D, 1, 126 }, + { 0x1F80, 0x1F87, 1, 8 }, + { 0x1F90, 0x1F97, 1, 8 }, + { 0x1FA0, 0x1FA7, 1, 8 }, + { 0x1FB0, 0x1FB1, 1, 8 }, + { 0x1FB3, 0x1FB3, 1, 9 }, + { 0x1FBE, 0x1FBE, 1, -7205 }, + { 0x1FC3, 0x1FC3, 1, 9 }, + { 0x1FD0, 0x1FD1, 1, 8 }, + { 0x1FE0, 0x1FE1, 1, 8 }, + { 0x1FE5, 0x1FE5, 1, 7 }, + { 0x1FF3, 0x1FF3, 1, 9 }, + { 0x214E, 0x214E, 1, -28 }, + { 0x2170, 0x217F, 1, -16 }, + { 0x2184, 0x2184, 1, -1 }, + { 0x24D0, 0x24E9, 1, -26 }, + { 0x2C30, 0x2C5F, 1, -48 }, + { 0x2C61, 0x2C61, 1, -1 }, + { 0x2C65, 0x2C65, 1, -10795 }, + { 0x2C66, 0x2C66, 1, -10792 }, + { 0x2C68, 0x2C6C, 2, -1 }, + { 0x2C73, 0x2C76, 3, -1 }, + { 0x2C81, 0x2CE3, 2, -1 }, + { 0x2CEC, 0x2CEE, 2, -1 }, + { 0x2CF3, 0x2CF3, 1, -1 }, + { 0x2D00, 0x2D25, 1, -7264 }, + { 0x2D27, 0x2D2D, 6, -7264 }, + { 0xA641, 0xA66D, 2, -1 }, + { 0xA681, 0xA69B, 2, -1 }, + { 0xA723, 0xA72F, 2, -1 }, + { 0xA733, 0xA76F, 2, -1 }, + { 0xA77A, 0xA77C, 2, -1 }, + { 0xA77F, 0xA787, 2, -1 }, + { 0xA78C, 0xA791, 5, -1 }, + { 0xA793, 0xA793, 1, -1 }, + { 0xA794, 0xA794, 1, 48 }, + { 0xA797, 0xA7A9, 2, -1 }, + { 0xA7B5, 0xA7C3, 2, -1 }, + { 0xA7C8, 0xA7CA, 2, -1 }, + { 0xA7D1, 0xA7D7, 6, -1 }, + { 0xA7D9, 0xA7F6, 29, -1 }, + { 0xAB53, 0xAB53, 1, -928 }, + { 0xAB70, 0xABBF, 1, -38864 }, + { 0xFF41, 0xFF5A, 1, -32 }, + { 0x10428, 0x1044F, 1, -40 }, + { 0x104D8, 0x104FB, 1, -40 }, + { 0x10597, 0x105A1, 1, -39 }, + { 0x105A3, 0x105B1, 1, -39 }, + { 0x105B3, 0x105B9, 1, -39 }, + { 0x105BB, 0x105BC, 1, -39 }, + { 0x10CC0, 0x10CF2, 1, -64 }, + { 0x118C0, 0x118DF, 1, -32 }, + { 0x16E60, 0x16E7F, 1, -32 }, + { 0x1E922, 0x1E943, 1, -34 }, +}; + +static struct conv_table tofold_table[] = { + { 0x41, 0x5A, 1, 32 }, + { 0xB5, 0xB5, 1, 775 }, + { 0xC0, 0xD6, 1, 32 }, + { 0xD8, 0xDE, 1, 32 }, + { 0x100, 0x12E, 2, 1 }, + { 0x132, 0x136, 2, 1 }, + { 0x139, 0x147, 2, 1 }, + { 0x14A, 0x176, 2, 1 }, + { 0x178, 0x178, 1, -121 }, + { 0x179, 0x17D, 2, 1 }, + { 0x17F, 0x17F, 1, -268 }, + { 0x181, 0x181, 1, 210 }, + { 0x182, 0x184, 2, 1 }, + { 0x186, 0x186, 1, 206 }, + { 0x187, 0x187, 1, 1 }, + { 0x189, 0x18A, 1, 205 }, + { 0x18B, 0x18B, 1, 1 }, + { 0x18E, 0x18E, 1, 79 }, + { 0x18F, 0x18F, 1, 202 }, + { 0x190, 0x190, 1, 203 }, + { 0x191, 0x191, 1, 1 }, + { 0x193, 0x193, 1, 205 }, + { 0x194, 0x194, 1, 207 }, + { 0x196, 0x196, 1, 211 }, + { 0x197, 0x197, 1, 209 }, + { 0x198, 0x198, 1, 1 }, + { 0x19C, 0x19C, 1, 211 }, + { 0x19D, 0x19D, 1, 213 }, + { 0x19F, 0x19F, 1, 214 }, + { 0x1A0, 0x1A4, 2, 1 }, + { 0x1A6, 0x1A6, 1, 218 }, + { 0x1A7, 0x1A7, 1, 1 }, + { 0x1A9, 0x1A9, 1, 218 }, + { 0x1AC, 0x1AC, 1, 1 }, + { 0x1AE, 0x1AE, 1, 218 }, + { 0x1AF, 0x1AF, 1, 1 }, + { 0x1B1, 0x1B2, 1, 217 }, + { 0x1B3, 0x1B5, 2, 1 }, + { 0x1B7, 0x1B7, 1, 219 }, + { 0x1B8, 0x1BC, 4, 1 }, + { 0x1C4, 0x1C4, 1, 2 }, + { 0x1C5, 0x1C5, 1, 1 }, + { 0x1C7, 0x1C7, 1, 2 }, + { 0x1C8, 0x1C8, 1, 1 }, + { 0x1CA, 0x1CA, 1, 2 }, + { 0x1CB, 0x1DB, 2, 1 }, + { 0x1DE, 0x1EE, 2, 1 }, + { 0x1F1, 0x1F1, 1, 2 }, + { 0x1F2, 0x1F4, 2, 1 }, + { 0x1F6, 0x1F6, 1, -97 }, + { 0x1F7, 0x1F7, 1, -56 }, + { 0x1F8, 0x21E, 2, 1 }, + { 0x220, 0x220, 1, -130 }, + { 0x222, 0x232, 2, 1 }, + { 0x23A, 0x23A, 1, 10795 }, + { 0x23B, 0x23B, 1, 1 }, + { 0x23D, 0x23D, 1, -163 }, + { 0x23E, 0x23E, 1, 10792 }, + { 0x241, 0x241, 1, 1 }, + { 0x243, 0x243, 1, -195 }, + { 0x244, 0x244, 1, 69 }, + { 0x245, 0x245, 1, 71 }, + { 0x246, 0x24E, 2, 1 }, + { 0x345, 0x345, 1, 116 }, + { 0x370, 0x372, 2, 1 }, + { 0x376, 0x376, 1, 1 }, + { 0x37F, 0x37F, 1, 116 }, + { 0x386, 0x386, 1, 38 }, + { 0x388, 0x38A, 1, 37 }, + { 0x38C, 0x38C, 1, 64 }, + { 0x38E, 0x38F, 1, 63 }, + { 0x391, 0x3A1, 1, 32 }, + { 0x3A3, 0x3AB, 1, 32 }, + { 0x3C2, 0x3C2, 1, 1 }, + { 0x3CF, 0x3CF, 1, 8 }, + { 0x3D0, 0x3D0, 1, -30 }, + { 0x3D1, 0x3D1, 1, -25 }, + { 0x3D5, 0x3D5, 1, -15 }, + { 0x3D6, 0x3D6, 1, -22 }, + { 0x3D8, 0x3EE, 2, 1 }, + { 0x3F0, 0x3F0, 1, -54 }, + { 0x3F1, 0x3F1, 1, -48 }, + { 0x3F4, 0x3F4, 1, -60 }, + { 0x3F5, 0x3F5, 1, -64 }, + { 0x3F7, 0x3F7, 1, 1 }, + { 0x3F9, 0x3F9, 1, -7 }, + { 0x3FA, 0x3FA, 1, 1 }, + { 0x3FD, 0x3FF, 1, -130 }, + { 0x400, 0x40F, 1, 80 }, + { 0x410, 0x42F, 1, 32 }, + { 0x460, 0x480, 2, 1 }, + { 0x48A, 0x4BE, 2, 1 }, + { 0x4C0, 0x4C0, 1, 15 }, + { 0x4C1, 0x4CD, 2, 1 }, + { 0x4D0, 0x52E, 2, 1 }, + { 0x531, 0x556, 1, 48 }, + { 0x10A0, 0x10C5, 1, 7264 }, + { 0x10C7, 0x10CD, 6, 7264 }, + { 0x13F8, 0x13FD, 1, -8 }, + { 0x1C80, 0x1C80, 1, -6222 }, + { 0x1C81, 0x1C81, 1, -6221 }, + { 0x1C82, 0x1C82, 1, -6212 }, + { 0x1C83, 0x1C84, 1, -6210 }, + { 0x1C85, 0x1C85, 1, -6211 }, + { 0x1C86, 0x1C86, 1, -6204 }, + { 0x1C87, 0x1C87, 1, -6180 }, + { 0x1C88, 0x1C88, 1, 35267 }, + { 0x1C90, 0x1CBA, 1, -3008 }, + { 0x1CBD, 0x1CBF, 1, -3008 }, + { 0x1E00, 0x1E94, 2, 1 }, + { 0x1E9B, 0x1E9B, 1, -58 }, + { 0x1E9E, 0x1E9E, 1, -7615 }, + { 0x1EA0, 0x1EFE, 2, 1 }, + { 0x1F08, 0x1F0F, 1, -8 }, + { 0x1F18, 0x1F1D, 1, -8 }, + { 0x1F28, 0x1F2F, 1, -8 }, + { 0x1F38, 0x1F3F, 1, -8 }, + { 0x1F48, 0x1F4D, 1, -8 }, + { 0x1F59, 0x1F5F, 2, -8 }, + { 0x1F68, 0x1F6F, 1, -8 }, + { 0x1F88, 0x1F8F, 1, -8 }, + { 0x1F98, 0x1F9F, 1, -8 }, + { 0x1FA8, 0x1FAF, 1, -8 }, + { 0x1FB8, 0x1FB9, 1, -8 }, + { 0x1FBA, 0x1FBB, 1, -74 }, + { 0x1FBC, 0x1FBC, 1, -9 }, + { 0x1FBE, 0x1FBE, 1, -7173 }, + { 0x1FC8, 0x1FCB, 1, -86 }, + { 0x1FCC, 0x1FCC, 1, -9 }, + { 0x1FD8, 0x1FD9, 1, -8 }, + { 0x1FDA, 0x1FDB, 1, -100 }, + { 0x1FE8, 0x1FE9, 1, -8 }, + { 0x1FEA, 0x1FEB, 1, -112 }, + { 0x1FEC, 0x1FEC, 1, -7 }, + { 0x1FF8, 0x1FF9, 1, -128 }, + { 0x1FFA, 0x1FFB, 1, -126 }, + { 0x1FFC, 0x1FFC, 1, -9 }, + { 0x2126, 0x2126, 1, -7517 }, + { 0x212A, 0x212A, 1, -8383 }, + { 0x212B, 0x212B, 1, -8262 }, + { 0x2132, 0x2132, 1, 28 }, + { 0x2160, 0x216F, 1, 16 }, + { 0x2183, 0x2183, 1, 1 }, + { 0x24B6, 0x24CF, 1, 26 }, + { 0x2C00, 0x2C2F, 1, 48 }, + { 0x2C60, 0x2C60, 1, 1 }, + { 0x2C62, 0x2C62, 1, -10743 }, + { 0x2C63, 0x2C63, 1, -3814 }, + { 0x2C64, 0x2C64, 1, -10727 }, + { 0x2C67, 0x2C6B, 2, 1 }, + { 0x2C6D, 0x2C6D, 1, -10780 }, + { 0x2C6E, 0x2C6E, 1, -10749 }, + { 0x2C6F, 0x2C6F, 1, -10783 }, + { 0x2C70, 0x2C70, 1, -10782 }, + { 0x2C72, 0x2C75, 3, 1 }, + { 0x2C7E, 0x2C7F, 1, -10815 }, + { 0x2C80, 0x2CE2, 2, 1 }, + { 0x2CEB, 0x2CED, 2, 1 }, + { 0x2CF2, 0xA640, 31054, 1 }, + { 0xA642, 0xA66C, 2, 1 }, + { 0xA680, 0xA69A, 2, 1 }, + { 0xA722, 0xA72E, 2, 1 }, + { 0xA732, 0xA76E, 2, 1 }, + { 0xA779, 0xA77B, 2, 1 }, + { 0xA77D, 0xA77D, 1, -35332 }, + { 0xA77E, 0xA786, 2, 1 }, + { 0xA78B, 0xA78B, 1, 1 }, + { 0xA78D, 0xA78D, 1, -42280 }, + { 0xA790, 0xA792, 2, 1 }, + { 0xA796, 0xA7A8, 2, 1 }, + { 0xA7AA, 0xA7AA, 1, -42308 }, + { 0xA7AB, 0xA7AB, 1, -42319 }, + { 0xA7AC, 0xA7AC, 1, -42315 }, + { 0xA7AD, 0xA7AD, 1, -42305 }, + { 0xA7AE, 0xA7AE, 1, -42308 }, + { 0xA7B0, 0xA7B0, 1, -42258 }, + { 0xA7B1, 0xA7B1, 1, -42282 }, + { 0xA7B2, 0xA7B2, 1, -42261 }, + { 0xA7B3, 0xA7B3, 1, 928 }, + { 0xA7B4, 0xA7C2, 2, 1 }, + { 0xA7C4, 0xA7C4, 1, -48 }, + { 0xA7C5, 0xA7C5, 1, -42307 }, + { 0xA7C6, 0xA7C6, 1, -35384 }, + { 0xA7C7, 0xA7C9, 2, 1 }, + { 0xA7D0, 0xA7D6, 6, 1 }, + { 0xA7D8, 0xA7F5, 29, 1 }, + { 0xAB70, 0xABBF, 1, -38864 }, + { 0xFF21, 0xFF3A, 1, 32 }, + { 0x10400, 0x10427, 1, 40 }, + { 0x104B0, 0x104D3, 1, 40 }, + { 0x10570, 0x1057A, 1, 39 }, + { 0x1057C, 0x1058A, 1, 39 }, + { 0x1058C, 0x10592, 1, 39 }, + { 0x10594, 0x10595, 1, 39 }, + { 0x10C80, 0x10CB2, 1, 64 }, + { 0x118A0, 0x118BF, 1, 32 }, + { 0x16E40, 0x16E5F, 1, 32 }, + { 0x1E900, 0x1E921, 1, 34 }, +}; + +static struct range_table doublewidth_table[] = { + { 0x1100, 0x115F, 1 }, + { 0x231A, 0x231B, 1 }, + { 0x2329, 0x232A, 1 }, + { 0x23E9, 0x23EC, 1 }, + { 0x23F0, 0x23F3, 3 }, + { 0x25FD, 0x25FE, 1 }, + { 0x2614, 0x2615, 1 }, + { 0x2648, 0x2653, 1 }, + { 0x267F, 0x2693, 20 }, + { 0x26A1, 0x26AA, 9 }, + { 0x26AB, 0x26BD, 18 }, + { 0x26BE, 0x26C4, 6 }, + { 0x26C5, 0x26CE, 9 }, + { 0x26D4, 0x26EA, 22 }, + { 0x26F2, 0x26F3, 1 }, + { 0x26F5, 0x26FA, 5 }, + { 0x26FD, 0x2705, 8 }, + { 0x270A, 0x270B, 1 }, + { 0x2728, 0x274C, 36 }, + { 0x274E, 0x2753, 5 }, + { 0x2754, 0x2755, 1 }, + { 0x2757, 0x2795, 62 }, + { 0x2796, 0x2797, 1 }, + { 0x27B0, 0x27BF, 15 }, + { 0x2B1B, 0x2B1C, 1 }, + { 0x2B50, 0x2B55, 5 }, + { 0x2E80, 0x2E99, 1 }, + { 0x2E9B, 0x2EF3, 1 }, + { 0x2F00, 0x2FD5, 1 }, + { 0x2FF0, 0x2FFB, 1 }, + { 0x3000, 0x303E, 1 }, + { 0x3041, 0x3096, 1 }, + { 0x3099, 0x30FF, 1 }, + { 0x3105, 0x312F, 1 }, + { 0x3131, 0x318E, 1 }, + { 0x3190, 0x31E3, 1 }, + { 0x31F0, 0x321E, 1 }, + { 0x3220, 0x3247, 1 }, + { 0x3250, 0x4DBF, 1 }, + { 0x4E00, 0xA48C, 1 }, + { 0xA490, 0xA4C6, 1 }, + { 0xA960, 0xA97C, 1 }, + { 0xAC00, 0xD7A3, 1 }, + { 0xF900, 0xFAFF, 1 }, + { 0xFE10, 0xFE19, 1 }, + { 0xFE30, 0xFE52, 1 }, + { 0xFE54, 0xFE66, 1 }, + { 0xFE68, 0xFE6B, 1 }, + { 0xFF01, 0xFF60, 1 }, + { 0xFFE0, 0xFFE6, 1 }, + { 0x16FE0, 0x16FE4, 1 }, + { 0x16FF0, 0x16FF1, 1 }, + { 0x17000, 0x187F7, 1 }, + { 0x18800, 0x18CD5, 1 }, + { 0x18D00, 0x18D08, 1 }, + { 0x1AFF0, 0x1AFF3, 1 }, + { 0x1AFF5, 0x1AFFB, 1 }, + { 0x1AFFD, 0x1AFFE, 1 }, + { 0x1B000, 0x1B122, 1 }, + { 0x1B132, 0x1B150, 30 }, + { 0x1B151, 0x1B152, 1 }, + { 0x1B155, 0x1B164, 15 }, + { 0x1B165, 0x1B167, 1 }, + { 0x1B170, 0x1B2FB, 1 }, + { 0x1F004, 0x1F0CF, 203 }, + { 0x1F18E, 0x1F191, 3 }, + { 0x1F192, 0x1F19A, 1 }, + { 0x1F200, 0x1F202, 1 }, + { 0x1F210, 0x1F23B, 1 }, + { 0x1F240, 0x1F248, 1 }, + { 0x1F250, 0x1F251, 1 }, + { 0x1F260, 0x1F265, 1 }, + { 0x1F300, 0x1F320, 1 }, + { 0x1F32D, 0x1F335, 1 }, + { 0x1F337, 0x1F37C, 1 }, + { 0x1F37E, 0x1F393, 1 }, + { 0x1F3A0, 0x1F3CA, 1 }, + { 0x1F3CF, 0x1F3D3, 1 }, + { 0x1F3E0, 0x1F3F0, 1 }, + { 0x1F3F4, 0x1F3F8, 4 }, + { 0x1F3F9, 0x1F43E, 1 }, + { 0x1F440, 0x1F442, 2 }, + { 0x1F443, 0x1F4FC, 1 }, + { 0x1F4FF, 0x1F53D, 1 }, + { 0x1F54B, 0x1F54E, 1 }, + { 0x1F550, 0x1F567, 1 }, + { 0x1F57A, 0x1F595, 27 }, + { 0x1F596, 0x1F5A4, 14 }, + { 0x1F5FB, 0x1F64F, 1 }, + { 0x1F680, 0x1F6C5, 1 }, + { 0x1F6CC, 0x1F6D0, 4 }, + { 0x1F6D1, 0x1F6D2, 1 }, + { 0x1F6D5, 0x1F6D7, 1 }, + { 0x1F6DC, 0x1F6DF, 1 }, + { 0x1F6EB, 0x1F6EC, 1 }, + { 0x1F6F4, 0x1F6FC, 1 }, + { 0x1F7E0, 0x1F7EB, 1 }, + { 0x1F7F0, 0x1F90C, 284 }, + { 0x1F90D, 0x1F93A, 1 }, + { 0x1F93C, 0x1F945, 1 }, + { 0x1F947, 0x1F9FF, 1 }, + { 0x1FA70, 0x1FA7C, 1 }, + { 0x1FA80, 0x1FA88, 1 }, + { 0x1FA90, 0x1FABD, 1 }, + { 0x1FABF, 0x1FAC5, 1 }, + { 0x1FACE, 0x1FADB, 1 }, + { 0x1FAE0, 0x1FAE8, 1 }, + { 0x1FAF0, 0x1FAF8, 1 }, + { 0x20000, 0x2FFFD, 1 }, + { 0x30000, 0x3FFFD, 1 }, +}; + +static struct range_table ambiwidth_table[] = { + { 0xA1, 0xA7, 3 }, + { 0xA8, 0xAA, 2 }, + { 0xAD, 0xAE, 1 }, + { 0xB0, 0xB4, 1 }, + { 0xB6, 0xBA, 1 }, + { 0xBC, 0xBF, 1 }, + { 0xC6, 0xD0, 10 }, + { 0xD7, 0xD8, 1 }, + { 0xDE, 0xE1, 1 }, + { 0xE6, 0xE8, 2 }, + { 0xE9, 0xEA, 1 }, + { 0xEC, 0xED, 1 }, + { 0xF0, 0xF2, 2 }, + { 0xF3, 0xF7, 4 }, + { 0xF8, 0xFA, 1 }, + { 0xFC, 0xFE, 2 }, + { 0x101, 0x111, 16 }, + { 0x113, 0x11B, 8 }, + { 0x126, 0x127, 1 }, + { 0x12B, 0x131, 6 }, + { 0x132, 0x133, 1 }, + { 0x138, 0x13F, 7 }, + { 0x140, 0x142, 1 }, + { 0x144, 0x148, 4 }, + { 0x149, 0x14B, 1 }, + { 0x14D, 0x152, 5 }, + { 0x153, 0x166, 19 }, + { 0x167, 0x16B, 4 }, + { 0x1CE, 0x1DC, 2 }, + { 0x251, 0x261, 16 }, + { 0x2C4, 0x2C7, 3 }, + { 0x2C9, 0x2CB, 1 }, + { 0x2CD, 0x2D0, 3 }, + { 0x2D8, 0x2DB, 1 }, + { 0x2DD, 0x2DF, 2 }, + { 0x300, 0x36F, 1 }, + { 0x391, 0x3A1, 1 }, + { 0x3A3, 0x3A9, 1 }, + { 0x3B1, 0x3C1, 1 }, + { 0x3C3, 0x3C9, 1 }, + { 0x401, 0x410, 15 }, + { 0x411, 0x44F, 1 }, + { 0x451, 0x2010, 7103 }, + { 0x2013, 0x2016, 1 }, + { 0x2018, 0x2019, 1 }, + { 0x201C, 0x201D, 1 }, + { 0x2020, 0x2022, 1 }, + { 0x2024, 0x2027, 1 }, + { 0x2030, 0x2032, 2 }, + { 0x2033, 0x2035, 2 }, + { 0x203B, 0x203E, 3 }, + { 0x2074, 0x207F, 11 }, + { 0x2081, 0x2084, 1 }, + { 0x20AC, 0x2103, 87 }, + { 0x2105, 0x2109, 4 }, + { 0x2113, 0x2116, 3 }, + { 0x2121, 0x2122, 1 }, + { 0x2126, 0x212B, 5 }, + { 0x2153, 0x2154, 1 }, + { 0x215B, 0x215E, 1 }, + { 0x2160, 0x216B, 1 }, + { 0x2170, 0x2179, 1 }, + { 0x2189, 0x2190, 7 }, + { 0x2191, 0x2199, 1 }, + { 0x21B8, 0x21B9, 1 }, + { 0x21D2, 0x21D4, 2 }, + { 0x21E7, 0x2200, 25 }, + { 0x2202, 0x2203, 1 }, + { 0x2207, 0x2208, 1 }, + { 0x220B, 0x220F, 4 }, + { 0x2211, 0x2215, 4 }, + { 0x221A, 0x221D, 3 }, + { 0x221E, 0x2220, 1 }, + { 0x2223, 0x2227, 2 }, + { 0x2228, 0x222C, 1 }, + { 0x222E, 0x2234, 6 }, + { 0x2235, 0x2237, 1 }, + { 0x223C, 0x223D, 1 }, + { 0x2248, 0x224C, 4 }, + { 0x2252, 0x2260, 14 }, + { 0x2261, 0x2264, 3 }, + { 0x2265, 0x2267, 1 }, + { 0x226A, 0x226B, 1 }, + { 0x226E, 0x226F, 1 }, + { 0x2282, 0x2283, 1 }, + { 0x2286, 0x2287, 1 }, + { 0x2295, 0x2299, 4 }, + { 0x22A5, 0x22BF, 26 }, + { 0x2312, 0x2460, 334 }, + { 0x2461, 0x24E9, 1 }, + { 0x24EB, 0x254B, 1 }, + { 0x2550, 0x2573, 1 }, + { 0x2580, 0x258F, 1 }, + { 0x2592, 0x2595, 1 }, + { 0x25A0, 0x25A1, 1 }, + { 0x25A3, 0x25A9, 1 }, + { 0x25B2, 0x25B3, 1 }, + { 0x25B6, 0x25B7, 1 }, + { 0x25BC, 0x25BD, 1 }, + { 0x25C0, 0x25C1, 1 }, + { 0x25C6, 0x25C8, 1 }, + { 0x25CB, 0x25CE, 3 }, + { 0x25CF, 0x25D1, 1 }, + { 0x25E2, 0x25E5, 1 }, + { 0x25EF, 0x2605, 22 }, + { 0x2606, 0x2609, 3 }, + { 0x260E, 0x260F, 1 }, + { 0x261C, 0x261E, 2 }, + { 0x2640, 0x2642, 2 }, + { 0x2660, 0x2661, 1 }, + { 0x2663, 0x2665, 1 }, + { 0x2667, 0x266A, 1 }, + { 0x266C, 0x266D, 1 }, + { 0x266F, 0x269E, 47 }, + { 0x269F, 0x26BF, 32 }, + { 0x26C6, 0x26CD, 1 }, + { 0x26CF, 0x26D3, 1 }, + { 0x26D5, 0x26E1, 1 }, + { 0x26E3, 0x26E8, 5 }, + { 0x26E9, 0x26EB, 2 }, + { 0x26EC, 0x26F1, 1 }, + { 0x26F4, 0x26F6, 2 }, + { 0x26F7, 0x26F9, 1 }, + { 0x26FB, 0x26FC, 1 }, + { 0x26FE, 0x26FF, 1 }, + { 0x273D, 0x2776, 57 }, + { 0x2777, 0x277F, 1 }, + { 0x2B56, 0x2B59, 1 }, + { 0x3248, 0x324F, 1 }, + { 0xE000, 0xF8FF, 1 }, + { 0xFE00, 0xFE0F, 1 }, + { 0xFFFD, 0x1F100, 61699 }, + { 0x1F101, 0x1F10A, 1 }, + { 0x1F110, 0x1F12D, 1 }, + { 0x1F130, 0x1F169, 1 }, + { 0x1F170, 0x1F18D, 1 }, + { 0x1F18F, 0x1F190, 1 }, + { 0x1F19B, 0x1F1AC, 1 }, + { 0xE0100, 0xE01EF, 1 }, + { 0xF0000, 0xFFFFD, 1 }, + { 0x100000, 0x10FFFD, 1 }, +}; + +#endif /* unidata_h */ diff --git a/src/utfconv.h b/src/utfconv.h new file mode 100644 index 00000000..059b3071 --- /dev/null +++ b/src/utfconv.h @@ -0,0 +1,57 @@ +#ifndef MBSEC_H +#define MBSEC_H + +#ifdef _WIN32 + +#include +#include + +#define UTFCONV_ERROR_INVALID_CONVERSION "Input contains invalid byte sequences." + +LPWSTR utfconv_utf8towc(const char *str) { + LPWSTR output; + int len; + + // len includes \0 + len = MultiByteToWideChar(CP_UTF8, 0, str, -1, NULL, 0); + if (len == 0) + return NULL; + + output = (LPWSTR) malloc(sizeof(WCHAR) * len); + if (output == NULL) + return NULL; + + len = MultiByteToWideChar(CP_UTF8, 0, str, -1, output, len); + if (len == 0) { + free(output); + return NULL; + } + + return output; +} + +char *utfconv_wctoutf8(LPCWSTR str) { + char *output; + int len; + + // len includes \0 + len = WideCharToMultiByte(CP_UTF8, 0, str, -1, NULL, 0, NULL, NULL); + if (len == 0) + return NULL; + + output = (char *) malloc(sizeof(char) * len); + if (output == NULL) + return NULL; + + len = WideCharToMultiByte(CP_UTF8, 0, str, -1, output, len, NULL, NULL); + if (len == 0) { + free(output); + return NULL; + } + + return output; +} + +#endif + +#endif diff --git a/subprojects/freetype2.wrap b/subprojects/freetype2.wrap new file mode 100644 index 00000000..6d7f449a --- /dev/null +++ b/subprojects/freetype2.wrap @@ -0,0 +1,9 @@ +[wrap-file] +directory = freetype-2.11.1 +source_url = https://download.savannah.gnu.org/releases/freetype/freetype-2.11.1.tar.gz +source_filename = freetype-2.11.1.tar.gz +source_hash = f8db94d307e9c54961b39a1cc799a67d46681480696ed72ecf78d4473770f09b + +[provide] +freetype2 = freetype_dep + diff --git a/subprojects/lua.wrap b/subprojects/lua.wrap index 1be7895b..bd6ee5eb 100644 --- a/subprojects/lua.wrap +++ b/subprojects/lua.wrap @@ -1,4 +1,12 @@ -[wrap-git] -directory = lua -url = https://github.com/franko/lua -revision = v5.2.4-7 +[wrap-file] +directory = lua-5.4.3 +source_url = https://www.lua.org/ftp/lua-5.4.3.tar.gz +source_filename = lua-5.4.3.tar.gz +source_hash = f8612276169e3bfcbcfb8f226195bfc6e466fe13042f1076cbde92b7ec96bbfb +patch_filename = lua_5.4.3-2_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/lua_5.4.3-2/get_patch +patch_hash = 3c23ec14a3f000d80fe2e2fdddba63a56e13c758d74195daa4ff0da7bfdb02da + +[provide] +lua-5.4 = lua_dep + diff --git a/subprojects/pcre2.wrap b/subprojects/pcre2.wrap new file mode 100644 index 00000000..99e82cf4 --- /dev/null +++ b/subprojects/pcre2.wrap @@ -0,0 +1,15 @@ +[wrap-file] +directory = pcre2-10.39 +source_url = https://github.com/PhilipHazel/pcre2/releases/download/pcre2-10.39/pcre2-10.39.tar.bz2 +source_filename = pcre2-10.39.tar.bz2 +source_hash = 0f03caf57f81d9ff362ac28cd389c055ec2bf0678d277349a1a4bee00ad6d440 +patch_filename = pcre2_10.39-2_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/pcre2_10.39-2/get_patch +patch_hash = c4cfffff83e7bb239c8c330339b08f4367b019f79bf810f10c415e35fb09cf14 + +[provide] +libpcre2-8 = -libpcre2_8 +libpcre2-16 = -libpcre2_16 +libpcre2-32 = -libpcre2_32 +libpcre2-posix = -libpcre2_posix + diff --git a/subprojects/sdl2.wrap b/subprojects/sdl2.wrap new file mode 100644 index 00000000..aafa1fcc --- /dev/null +++ b/subprojects/sdl2.wrap @@ -0,0 +1,12 @@ +[wrap-file] +directory = SDL2-2.24.0 +source_url = https://libsdl.org/release/SDL2-2.24.0.tar.gz +source_filename = SDL2-2.24.0.tar.gz +source_hash = 91e4c34b1768f92d399b078e171448c6af18cafda743987ed2064a28954d6d97 +patch_filename = sdl2_2.24.0-2_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/sdl2_2.24.0-2/get_patch +patch_hash = ec296ed9a577b42131d2fdbfe5ca73a0cf133793c0290e1ccd825675464bfe32 +wrapdb_version = 2.24.0-2 + +[provide] +sdl2 = sdl2_dep