diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..f942842a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +root = true + +[*] +charset = utf-8 +indent_size = 2 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[meson.build] +indent_size = 4 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c4bf3358..2778de79 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,11 +1,44 @@ name: CI +# All builds use lhelper only for releases, +# otherwise for normal builds dependencies are dynamically linked. + on: - workflow_dispatch: + push: + branches: + - '*' +# tags: +# - 'v[0-9]*' + pull_request: + branches: + - '*' jobs: - build-linux: - name: Build Linux + 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 strategy: matrix: @@ -16,48 +49,204 @@ jobs: CC: ${{ matrix.config.cc }} CXX: ${{ matrix.config.cxx }} steps: - - uses: actions/checkout@v2 - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: 3.6 - - name: Install dependencies - run: | - sudo apt-get install -qq libsdl2-dev libfreetype6 ninja-build - pip3 install meson - - name: Build package - run: bash build-packages.sh x86-64 - - name: upload packages - uses: actions/upload-artifact@v2 - with: - name: Ubuntu Package - path: lite-xl-linux-*.tar.gz + - 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 - build-macox: - name: Build Mac OS X + build_macos: + name: macOS (x86_64) runs-on: macos-10.15 + 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 + + build_windows_msys2: + name: Windows + runs-on: windows-2019 strategy: matrix: - config: - # - { name: "GCC", cc: gcc-10, cxx: g++-10 } - - { name: "clang", cc: clang, cxx: clang++ } - env: - CC: ${{ matrix.config.cc }} - CXX: ${{ matrix.config.cxx }} + msystem: [MINGW32, MINGW64] + defaults: + run: + shell: msys2 {0} steps: - - uses: actions/checkout@v2 - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: 3.9 - - name: Install dependencies - run: | - pip3 install meson - brew install ninja sdl2 - - name: Build package - run: bash build-packages.sh x86-64 - - name: upload packages - uses: actions/upload-artifact@v2 - with: - name: Mac OS X Package - path: lite-xl-macosx-*.zip + - uses: actions/checkout@v2 + - uses: msys2/setup-msys2@v2 + with: + #msystem: MINGW64 + msystem: ${{ matrix.msystem }} + update: true + install: >- + base-devel + git + zip + - 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" + - 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} + - 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 + - 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 diff --git a/.gitignore b/.gitignore index 92d20437..16974405 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,21 @@ -build* -.build* -.run* -*.zip -*.tar.gz +build*/ +.build*/ +lhelper/ +submodules/ +subprojects/lua/ +subprojects/libagg/ +subprojects/reproc/ +/appimage* +.ccls-cache .lite-debug.log -subprojects/lua -subprojects/libagg -subprojects/reproc -lite-xl +.run* +*.diff +*.exe +*.tar.gz +*.zip +*.DS_Store +*App* +compile_commands.json error.txt +lite-xl* +LiteXL* diff --git a/README.md b/README.md index b96c9dca..73204c57 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,18 @@ # Lite XL +[![CI]](https://github.com/lite-xl/lite-xl/actions/workflows/build.yml) [![Discord Badge Image]](https://discord.gg/RWzqC3nx7K) ![screenshot-dark] A lightweight text editor written in Lua, adapted from [lite]. -* **[Get Lite XL]** — Download for Windows, Linux and Mac OS (notarized app). +* **[Get Lite XL]** — Download for Windows, Linux and Mac OS. * **[Get plugins]** — Add additional functionality, adapted for Lite XL. * **[Get color themes]** — Add additional colors themes. Please refer to our [website] for the user and developer documentation, -including [build] instructions. +including [build] instructions details. A quick build guide is described below. Lite XL has support for high DPI display on Windows and Linux and, since 1.16.7 release, it supports **retina displays** on macOS. @@ -42,6 +43,40 @@ the [plugins repository] or in the [Lite XL plugins repository]. Additional color themes can be found in the [colors repository]. These color themes are bundled with all releases of Lite XL by default. +## Quick Build Guide + +If you compile Lite XL yourself, it is recommended to use the script +`build-packages.sh`: + +```sh +bash build-packages.sh -h +``` + +The script will run Meson and create a tar compressed archive with the application or, +for Windows, a zip file. Lite XL can be easily installed +by unpacking the archive in any directory of your choice. + +Otherwise the following is an example of basic commands if you want to customize +the build: + +```sh +meson setup --buildtype=release --prefix build +meson compile -C build +DESTDIR="$(pwd)/lite-xl" meson install --skip-subprojects -C build +``` + +where `` might be one of `/`, `/usr` or `/opt`, the default is `/`. +To build a bundle application on macOS: + +```sh +meson setup --buildtype=release --Dbundle=true --prefix / build +meson compile -C build +DESTDIR="$(pwd)/Lite XL.app" meson install --skip-subprojects -C build +``` + +Please note that the package is relocatable to any prefix and the option prefix +affects only the place where the application is actually installed. + ## Contributing Any additional functionality that can be added through a plugin should be done @@ -60,6 +95,7 @@ the terms of the MIT license. See [LICENSE] for details. See the [licenses] file for details on licenses used by the required dependencies. +[CI]: https://github.com/lite-xl/lite-xl/actions/workflows/build.yml/badge.svg [Discord Badge Image]: https://img.shields.io/discord/847122429742809208?label=discord&logo=discord [screenshot-dark]: https://user-images.githubusercontent.com/433545/111063905-66943980-84b1-11eb-9040-3876f1133b20.png [lite]: https://github.com/rxi/lite diff --git a/build-packages.sh b/build-packages.sh index 3701ac53..4ecda0a0 100755 --- a/build-packages.sh +++ b/build-packages.sh @@ -1,216 +1,164 @@ #!/bin/bash +set -e -# strip-components is normally set to 1 to strip the initial "data" from the -# directory path. -copy_directory_from_repo () { - local tar_options=() - if [[ $1 == --strip-components=* ]]; then - tar_options+=($1) - shift - fi - local dirname="$1" - local destdir="$2" - git archive "$use_branch" "$dirname" --format=tar | tar xf - -C "$destdir" "${tar_options[@]}" +if [ ! -e "src/api/api.h" ]; then + echo "Please run this script from the root directory of Lite XL."; exit 1 +fi + +source scripts/common.sh + +show_help() { + echo + echo "Usage: $0 " + echo + echo "Common options:" + echo + echo "-h --help Show this help and exit." + echo "-b --builddir DIRNAME Set the name of the build directory (not path)." + echo " Default: '$(get_default_build_dir)'." + echo "-p --prefix PREFIX Install directory prefix." + echo " Default: '/'." + echo " --debug Debug this script." + echo + echo "Build options:" + echo + echo "-f --forcefallback Force to build subprojects dependencies statically." + echo "-B --bundle Create an App bundle (macOS only)" + echo "-P --portable Create a portable package." + echo "-O --pgo Use profile guided optimizations (pgo)." + echo " Requires running the application iteractively." + echo + echo "Package options:" + echo + echo "-d --destdir DIRNAME Set the name of the package directory (not path)." + echo " Default: 'lite-xl'." + echo "-v --version VERSION Sets the version on the package name." + echo "-A --appimage Create an AppImage (Linux only)." + 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 "-S --source Create a source code package," + echo " including subprojects dependencies." + echo } -# Check if build directory is ok to be used to build. -build_dir_is_usable () { - local build="$1" - if [[ $build == */* || -z "$build" ]]; then - echo "invalid build directory, no path allowed: \"$build\"" - return 1 - fi - git ls-files --error-unmatch "$build" &> /dev/null - if [ $? == 0 ]; then - echo "invalid path, \"$build\" is under revision control" - return 1 - fi -} +main() { + local build_dir + local build_dir_option=() + local dest_dir + local dest_dir_option=() + local prefix + local prefix_option=() + local version + local version_option=() + local debug + local force_fallback + local appimage + local bundle + local innosetup + local portable + local pgo -# Ordinary release build -lite_build () { - local build="$1" - build_dir_is_usable "$build" || exit 1 - rm -fr "$build" - meson setup --buildtype=release "$build" || exit 1 - ninja -C "$build" || exit 1 -} - -# Build using Profile Guided Optimizations (PGO) -lite_build_pgo () { - local build="$1" - build_dir_is_usable "$build" || exit 1 - rm -fr "$build" - meson setup --buildtype=release -Db_pgo=generate "$build" || exit 1 - ninja -C "$build" || exit 1 - copy_directory_from_repo data "$build/src" - "$build/src/lite-xl" - meson configure -Db_pgo=use "$build" - ninja -C "$build" || exit 1 -} - -lite_build_package_windows () { - local portable="-msys" - if [ "$1" == "-portable" ]; then - portable="" - shift - fi - local build="$1" - local arch="$2" - local os="win" - local pdir=".package-build/lite-xl" - if [ -z "$portable" ]; then - local bindir="$pdir" - local datadir="$pdir/data" - else - local bindir="$pdir/bin" - local datadir="$pdir/share/lite-xl" - fi - mkdir -p "$bindir" - mkdir -p "$datadir" - for module_name in core plugins colors fonts; do - copy_directory_from_repo --strip-components=1 "data/$module_name" "$datadir" + for i in "$@"; do + case $i in + -h|--help) + show_help + exit 0 + ;; + -b|--builddir) + build_dir="$2" + shift + shift + ;; + -d|--destdir) + dest_dir="$2" + shift + shift + ;; + -f|--forcefallback) + force_fallback="--forcefallback" + shift + ;; + -p|--prefix) + prefix="$2" + shift + shift + ;; + -v|--version) + version="$2" + shift + shift + ;; + -A|--appimage) + appimage="--appimage" + shift + ;; + -B|--bundle) + bundle="--bundle" + shift + ;; + -D|--dmg) + dmg="--dmg" + shift + ;; + -I|--innosetup) + innosetup="--innosetup" + shift + ;; + -P|--portable) + portable="--portable" + shift + ;; + -S|--source) + source="--source" + shift + ;; + -O|--pgo) + pgo="--pgo" + shift + ;; + --debug) + debug="--debug" + set -x + shift + ;; + *) + # unknown option + ;; + esac done - for module_name in plugins colors; do - cp -r "$build/third/data/$module_name" "$datadir" - done - cp "$build/src/lite-xl.exe" "$bindir" - strip --strip-all "$bindir/lite-xl.exe" - pushd ".package-build" - local package_name="lite-xl-$os-$arch$portable.zip" - zip "$package_name" -r "lite-xl" - mv "$package_name" .. - popd - rm -fr ".package-build" - echo "created package $package_name" -} -lite_build_package_macos () { - local build="$1" - local arch="$2" - local os="macos" - - local appdir=".package-build/lite-xl.app" - local bindir="$appdir/Contents/MacOS" - local datadir="$appdir/Contents/Resources" - mkdir -p "$bindir" "$datadir" - for module_name in core plugins colors fonts; do - copy_directory_from_repo --strip-components=1 "data/$module_name" "$datadir" - done - for module_name in plugins colors; do - cp -r "$build/third/data/$module_name" "$datadir" - done - cp resources/icons/icon.icns "$appdir/Contents/Resources/icon.icns" - cp resources/macos/Info.plist "$appdir/Contents/Info.plist" - cp "$build/src/lite-xl" "$bindir/lite-xl" - strip "$bindir/lite-xl" - pushd ".package-build" - local package_name="lite-xl-$os-$arch.zip" - zip "$package_name" -r "lite-xl.app" - mv "$package_name" .. - popd - rm -fr ".package-build" - echo "created package $package_name" -} - -lite_build_package_linux () { - local portable="" - if [ "$1" == "-portable" ]; then - portable="-portable" - shift - fi - local build="$1" - local arch="$2" - local os="linux" - local pdir=".package-build/lite-xl" - if [ "$portable" == "-portable" ]; then - local bindir="$pdir" - local datadir="$pdir/data" - else - local bindir="$pdir/bin" - local datadir="$pdir/share/lite-xl" - fi - mkdir -p "$bindir" - mkdir -p "$datadir" - for module_name in core plugins colors fonts; do - copy_directory_from_repo --strip-components=1 "data/$module_name" "$datadir" - done - for module_name in plugins colors; do - cp -r "$build/third/data/$module_name" "$datadir" - done - cp "$build/src/lite-xl" "$bindir" - strip "$bindir/lite-xl" - if [ -z "$portable" ]; then - mkdir -p "$pdir/share/applications" "$pdir/share/icons/hicolor/scalable/apps" - cp "resources/linux/lite-xl.desktop" "$pdir/share/applications" - cp "resources/icons/lite-xl.svg" "$pdir/share/icons/hicolor/scalable/apps/lite-xl.svg" - fi - pushd ".package-build" - local package_name="lite-xl-$os-$arch$portable.tar.gz" - tar czf "$package_name" "lite-xl" - mv "$package_name" .. - popd - rm -fr ".package-build" - echo "created package $package_name" -} - -lite_build_package () { - if [[ "$OSTYPE" == msys || "$OSTYPE" == win32 ]]; then - lite_build_package_windows "$@" - elif [[ "$OSTYPE" == "darwin"* ]]; then - lite_build_package_macos "$@" - elif [[ "$OSTYPE" == "linux"* || "$OSTYPE" == "freebsd"* ]]; then - lite_build_package_linux "$@" - else - echo "Unknown OS type \"$OSTYPE\"" + if [[ -n $1 ]]; then + show_help exit 1 fi + + if [[ -n $build_dir ]]; then build_dir_option=("--builddir" "${build_dir}"); fi + if [[ -n $dest_dir ]]; then dest_dir_option=("--destdir" "${dest_dir}"); fi + if [[ -n $prefix ]]; then prefix_option=("--prefix" "${prefix}"); fi + if [[ -n $version ]]; then version_option=("--version" "${version}"); fi + + source scripts/build.sh \ + ${build_dir_option[@]} \ + ${prefix_option[@]} \ + $debug \ + $force_fallback \ + $bundle \ + $portable \ + $pgo + + source scripts/package.sh \ + ${build_dir_option[@]} \ + ${dest_dir_option[@]} \ + ${prefix_option[@]} \ + ${version_option[@]} \ + --binary \ + --addons \ + $debug \ + $appimage \ + $dmg \ + $innosetup \ + $source } -lite_copy_third_party_modules () { - local build="$1" - curl --insecure -L "https://github.com/rxi/lite-colors/archive/master.zip" -o "$build/rxi-lite-colors.zip" - mkdir -p "$build/third/data/colors" "$build/third/data/plugins" - unzip "$build/rxi-lite-colors.zip" -d "$build" - mv "$build/lite-colors-master/colors" "$build/third/data" - rm -fr "$build/lite-colors-master" -} - -unset arch -while [ ! -z {$1+x} ]; do - case $1 in - -pgo) - pgo=true - shift - ;; - -branch=*) - use_branch="${1#-branch=}" - shift - ;; - *) - arch="$1" - break - esac -done - -if [ -z ${arch+set} ]; then - echo "usage: $0 [options] " - exit 1 -fi - -if [ -z ${use_branch+set} ]; then - use_branch="$(git rev-parse --abbrev-ref HEAD)" -fi - -build_dir=".build-$arch" - -if [ -z ${pgo+set} ]; then - lite_build "$build_dir" -else - lite_build_pgo "$build_dir" -fi -lite_copy_third_party_modules "$build_dir" -lite_build_package "$build_dir" "$arch" -if [[ ! ( "$OSTYPE" == "linux"* || "$OSTYPE" == "freebsd"* || "$OSTYPE" == "darwin"* ) ]]; then - lite_build_package -portable "$build_dir" "$arch" -fi +main "$@" diff --git a/build.sh b/build.sh deleted file mode 100755 index b49dc1bf..00000000 --- a/build.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/bin/bash - -cflags+="-Wall -O3 -g -std=gnu11 -fno-strict-aliasing -Isrc -Ilib/font_renderer" -cflags+=" $(pkg-config --cflags lua5.2) $(sdl2-config --cflags)" -lflags="-static-libgcc -static-libstdc++" -for package in libagg freetype2 lua5.2 x11 libpcre2-8 reproc; do - lflags+=" $(pkg-config --libs $package)" -done -lflags+=" $(sdl2-config --libs) -lm" - -if [[ $* == *windows* ]]; then - echo "cross compiling for windows is not yet supported" - exit 1 -else - outfile="lite-xl" - compiler="gcc" - cxxcompiler="g++" -fi - -lib/font_renderer/build.sh || exit 1 -libs=libfontrenderer.a - -echo "compiling lite-xl..." -for f in `find src -name "*.c"`; do - $compiler -c $cflags $f -o "${f//\//_}.o" - if [[ $? -ne 0 ]]; then - got_error=true - fi -done - -if [[ ! $got_error ]]; then - echo "linking..." - $cxxcompiler -o $outfile *.o $libs $lflags -fi - -echo "cleaning up..." -rm *.o *.a -echo "done" - diff --git a/changelog.md b/changelog.md index eb203747..57ab9646 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,85 @@ This files document the changes done in Lite XL for each release. +### 2.0.2 + +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. + +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`. + +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. + +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. + +Build package script rewrite with many improvements. + +Use bigger fonts by default. + +Other minor improvements and fixes. + +With many thanks to the contributors: @adamharrison, @takase1121, @Guldoman, @redtide, @Timofffee, @boppyt, @Jan200101. + +### 2.0.1 + +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 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 + +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. + +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. + +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. + +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 +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. + +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. + ### 1.16.11 When opening directories with too many files lite-xl now keep diplaying files and directories in the treeview. diff --git a/data/colors/textadept.lua b/data/colors/textadept.lua new file mode 100644 index 00000000..276406d9 --- /dev/null +++ b/data/colors/textadept.lua @@ -0,0 +1,51 @@ +local b05 = 'rgba(0,0,0,0.5)' local red = '#994D4D' +local b80 = '#333333' local orange = '#B3661A' +local b60 = '#808080' local green = '#52994D' +local b40 = '#ADADAD' local teal = '#4D9999' +local b20 = '#CECECE' local blue = '#1A66B3' +local b00 = '#E6E6E6' local magenta = '#994D99' +--------------------------=-------------------------- +local style = require 'core.style' +local common = require 'core.common' +--------------------------=-------------------------- +style.line_highlight = { common.color(b20) } +style.background = { common.color(b00) } +style.background2 = { common.color(b20) } +style.background3 = { common.color(b20) } +style.text = { common.color(b60) } +style.caret = { common.color(b80) } +style.accent = { common.color(b80) } +style.dim = { common.color(b60) } +style.divider = { common.color(b40) } +style.selection = { common.color(b40) } +style.line_number = { common.color(b60) } +style.line_number2 = { common.color(b80) } +style.scrollbar = { common.color(b40) } +style.scrollbar2 = { common.color(b60) } +style.nagbar = { common.color(red) } +style.nagbar_text = { common.color(b00) } +style.nagbar_dim = { common.color(b05) } +--------------------------=-------------------------- +style.syntax = {} +style.syntax['normal'] = { common.color(b80) } +style.syntax['symbol'] = { common.color(b80) } +style.syntax['comment'] = { common.color(b60) } +style.syntax['keyword'] = { common.color(blue) } +style.syntax['keyword2'] = { common.color(red) } +style.syntax['number'] = { common.color(teal) } +style.syntax['literal'] = { common.color(blue) } +style.syntax['string'] = { common.color(green) } +style.syntax['operator'] = { common.color(magenta) } +style.syntax['function'] = { common.color(blue) } +--------------------------=-------------------------- +style.syntax.paren1 = { common.color(magenta) } +style.syntax.paren2 = { common.color(orange) } +style.syntax.paren3 = { common.color(teal) } +style.syntax.paren4 = { common.color(blue) } +style.syntax.paren5 = { common.color(red) } +--------------------------=-------------------------- +style.lint = {} +style.lint.info = { common.color(blue) } +style.lint.hint = { common.color(green) } +style.lint.warning = { common.color(red) } +style.lint.error = { common.color(orange) } diff --git a/data/core/commands/core.lua b/data/core/commands/core.lua index feca573e..e836ea2f 100644 --- a/data/core/commands/core.lua +++ b/data/core/commands/core.lua @@ -66,9 +66,8 @@ command.add(nil, { end, ["core:find-file"] = function() - -- FIXME: core.project_files_limit was removed! - if core.project_files_limit then - return command.perform "core:open-file" + if not core.project_files_number() then + return command.perform "core:open-file" end local files = {} for dir, item in core.get_project_files() do @@ -105,11 +104,20 @@ command.add(nil, { end, function (text) return common.home_encode_list(common.path_suggest(common.home_expand(text))) end, nil, function(text) - local path_stat, err = system.get_file_info(common.home_expand(text)) + local filename = common.home_expand(text) + local path_stat, err = system.get_file_info(filename) if err then - core.error("Cannot open file %q: %q", text, err) + 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 %q, is a folder", text) + core.error("Cannot open %s, is a folder", text) else return true end @@ -139,6 +147,10 @@ command.add(nil, { end, ["core:change-project-folder"] = function() + local dirname = common.dirname(core.project_dir) + if dirname then + core.command_view:set_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 @@ -147,11 +159,15 @@ command.add(nil, { core.error("Cannot open folder %q", text) return end - core.confirm_close_all(core.open_folder_project, text) + core.confirm_close_docs(core.docs, core.open_folder_project, text) end, suggest_directory) end, ["core:open-project-folder"] = function() + local dirname = common.dirname(core.project_dir) + if dirname then + core.command_view:set_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) diff --git a/data/core/commands/doc.lua b/data/core/commands/doc.lua index 815d9511..fb17e674 100644 --- a/data/core/commands/doc.lua +++ b/data/core/commands/doc.lua @@ -43,9 +43,13 @@ local function append_line_if_last_line(line) end local function save(filename) - doc():save(filename and core.normalize_to_project_dir(filename)) + local abs_filename + if filename then + filename = core.normalize_to_project_dir(filename) + abs_filename = core.project_absolute_path(filename) + end + doc():save(filename, abs_filename) local saved_filename = doc().filename - core.on_doc_save(saved_filename) core.log("Saved \"%s\"", saved_filename) end @@ -63,13 +67,14 @@ local function cut_or_copy(delete) doc().cursor_clipboard[idx] = "" end end + doc().cursor_clipboard["full"] = full_text system.set_clipboard(full_text) end local function split_cursor(direction) local new_cursors = {} for _, line1, col1 in doc():get_selections() do - if line1 > 1 and line1 < #doc().lines then + if line1 + direction >= 1 and line1 + direction <= #doc().lines then table.insert(new_cursors, { line1 + direction, col1 }) end end @@ -95,8 +100,13 @@ local commands = { end, ["doc:paste"] = function() + 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 = {} + end for idx, line1, col1, line2, col2 in doc():get_selections() do - local value = doc().cursor_clipboard[idx] or system.get_clipboard() + local value = doc().cursor_clipboard[idx] or clipboard doc():text_input(value:gsub("\r", ""), idx) end end, @@ -364,12 +374,14 @@ local commands = { end core.command_view:set_text(old_filename) core.command_view:enter("Rename", function(filename) - doc():save(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, common.path_suggest) + end, function (text) + return common.home_encode_list(common.path_suggest(common.home_expand(text))) + end) end, @@ -412,6 +424,7 @@ local translations = { ["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, diff --git a/data/core/commands/findreplace.lua b/data/core/commands/findreplace.lua index e5d7c70b..f8e8e45a 100644 --- a/data/core/commands/findreplace.lua +++ b/data/core/commands/findreplace.lua @@ -10,48 +10,62 @@ local StatusView = require "core.statusview" local max_last_finds = 50 local last_finds, last_view, last_fn, last_text, last_sel -local case_insensitive = config.find_case_insensitive or false -local plain = config.find_plain or false +local case_sensitive = config.find_case_sensitive or false +local find_regex = config.find_regex or false +local found_expression local function doc() - return last_view and last_view.doc or core.active_view.doc + return core.active_view:is(DocView) and core.active_view.doc or last_view.doc end local function get_find_tooltip() local rf = keymap.get_binding("find-replace:repeat-find") - local ti = keymap.get_binding("find-replace:toggle-insensitivity") - local tr = keymap.get_binding("find-replace:toggle-plain") - return (plain and "[Plain] " or "") .. - (case_insensitive and "[Insensitive] " or "") .. + local ti = keymap.get_binding("find-replace:toggle-sensitivity") + local tr = keymap.get_binding("find-replace:toggle-regex") + return (find_regex and "[Regex] " or "") .. + (case_sensitive and "[Sensitive] " or "") .. (rf and ("Press " .. rf .. " to select the next match.") or "") .. (ti and (" " .. ti .. " toggles case sensitivity.") or "") .. - (tr and (" " .. tr .. " toggles plain find.") or "") + (tr and (" " .. tr .. " toggles regex find.") or "") end local function update_preview(sel, search_fn, text) - local ok, line1, col1, line2, col2 = - pcall(search_fn, last_view.doc, sel[1], sel[2], text, case_insensitive, plain) + local ok, line1, col1, line2, col2 = pcall(search_fn, last_view.doc, + sel[1], sel[2], text, case_sensitive, find_regex) if ok and line1 and text ~= "" then last_view.doc:set_selection(line2, col2, line1, col1) last_view:scroll_to_line(line2, true) - return true + found_expression = true else last_view.doc:set_selection(unpack(sel)) - return false + found_expression = false end end + +local function insert_unique(t, v) + local n = #t + for i = 1, n do + if t[i] == v then return end + end + t[n + 1] = v +end + + local function find(label, search_fn) last_view, last_sel, last_finds = core.active_view, { core.active_view.doc:get_selection() }, {} - local text, found = last_view.doc:get_text(unpack(last_sel)), false + local text = last_view.doc:get_text(unpack(last_sel)) + found_expression = false core.command_view:set_text(text, true) core.status_view:show_tooltip(get_find_tooltip()) - core.command_view:enter(label, function(text) + 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 then + if found_expression then last_fn, last_text = search_fn, text else core.error("Couldn't find %q", text) @@ -59,8 +73,9 @@ local function find(label, search_fn) last_view:scroll_to_make_visible(unpack(last_sel)) end end, function(text) - found = update_preview(last_sel, search_fn, 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 @@ -75,18 +90,25 @@ 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) local s = string.format("Replace %s %q With", kind, old) + core.command_view:set_hidden_suggestions() core.command_view:enter(s, function(new) + 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() end, function() + 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 @@ -94,29 +116,78 @@ local function has_selection() return core.active_view:is(DocView) and core.active_view.doc:has_selection() end -command.add(has_selection, { +local function has_unique_selection() + if not core.active_view:is(DocView) then return false end + local text = nil + for idx, line1, col1, line2, col2 in doc():get_selections(true, true) do + if line1 == line2 and col1 == col2 then return false end + local selection = doc():get_text(line1, col1, line2, col2) + if text ~= nil and text ~= selection then return false end + text = selection + end + return text ~= nil +end + +local function is_in_selection(line, col, l1, c1, l2, c2) + if line < l1 or line > l2 then return false end + if line == l1 and col <= c1 then return false end + if line == l2 and col > c2 then return false end + return true +end + +local function is_in_any_selection(line, col) + for idx, l1, c1, l2, c2 in doc():get_selections(true, false) do + if is_in_selection(line, col, l1, c1, l2, c2) then return true end + end + return false +end + +local function select_next(all) + local il1, ic1 = doc():get_selection(true) + for idx, l1, c1, l2, c2 in doc():get_selections(true, true) do + local text = doc():get_text(l1, c1, l2, c2) + repeat + l1, c1, l2, c2 = search.find(doc(), l2, c2, text, { wrap = true }) + if l1 == il1 and c1 == ic1 then break end + if l2 and (all or not is_in_any_selection(l2, c2)) then + doc():add_selection(l2, c2, l1, c1) + if not all then + core.active_view:scroll_to_make_visible(l2, c2) + return + end + end + until not all or not l2 + if all then break end + end +end + +command.add(has_unique_selection, { ["find-replace:select-next"] = function() local l1, c1, l2, c2 = doc():get_selection(true) local text = doc():get_text(l1, c1, l2, c2) l1, c1, l2, c2 = search.find(doc(), l2, c2, text, { wrap = true }) if l2 then doc():set_selection(l2, c2, l1, c1) end - end + end, + ["find-replace:select-add-next"] = function() select_next(false) end, + ["find-replace:select-add-all"] = function() select_next(true) end }) command.add("core.docview", { ["find-replace:find"] = function() - find("Find Text", function(doc, line, col, text, case_insensitive, plain) - local opt = { wrap = true, no_case = case_insensitive, regex = not plain } + find("Find Text", function(doc, line, col, text, case_sensitive, find_regex) + local opt = { wrap = true, no_case = not case_sensitive, regex = find_regex } return search.find(doc, line, col, text, opt) end) end, ["find-replace:replace"] = function() - replace("Text", doc():get_text(doc():get_selection(true)), function(text, old, new) - if plain then + local l1, c1, l2, c2 = doc():get_selection() + local selected_text = doc():get_text(l1, c1, l2, c2) + replace("Text", l1 == l2 and selected_text or "", function(text, old, new) + if not find_regex then return text:gsub(old:gsub("%W", "%%%1"), new:gsub("%%", "%%%%"), nil) end - local result, matches = regex.gsub(regex.compile(old), text, new) + local result, matches = regex.gsub(regex.compile(old, "m"), text, new) return result, #matches end) end, @@ -150,7 +221,7 @@ command.add(valid_for_finding, { core.error("No find to continue from") else local sl1, sc1, sl2, sc2 = doc():get_selection(true) - local line1, col1, line2, col2 = last_fn(doc(), sl1, sc2, last_text, case_insensitive, plain) + local line1, col1, line2, col2 = last_fn(doc(), sl1, sc2, last_text, case_sensitive, find_regex) if line1 then if last_view.doc ~= doc() then last_finds = {} @@ -177,15 +248,15 @@ command.add(valid_for_finding, { }) command.add("core.commandview", { - ["find-replace:toggle-insensitivity"] = function() - case_insensitive = not case_insensitive + ["find-replace:toggle-sensitivity"] = function() + case_sensitive = not case_sensitive core.status_view:show_tooltip(get_find_tooltip()) - update_preview(last_sel, last_fn, last_text) + if last_sel then update_preview(last_sel, last_fn, last_text) end end, - ["find-replace:toggle-plain"] = function() - plain = not plain + ["find-replace:toggle-regex"] = function() + find_regex = not find_regex core.status_view:show_tooltip(get_find_tooltip()) - update_preview(last_sel, last_fn, last_text) + if last_sel then update_preview(last_sel, last_fn, last_text) end end }) diff --git a/data/core/commands/root.lua b/data/core/commands/root.lua index 7bc13283..e41c723d 100644 --- a/data/core/commands/root.lua +++ b/data/core/commands/root.lua @@ -21,9 +21,15 @@ local t = { end, ["root:close-all"] = function() - core.confirm_close_all(core.root_view.close_all_docviews, core.root_view) + core.confirm_close_docs(core.docs, core.root_view.close_all_docviews, core.root_view) end, + ["root:close-all-others"] = function() + local active_doc, docs = core.active_view and core.active_view.doc, {} + 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() local idx = node:get_view_idx(core.active_view) diff --git a/data/core/commandview.lua b/data/core/commandview.lua index 4d518d02..b91f1394 100644 --- a/data/core/commandview.lua +++ b/data/core/commandview.lua @@ -15,6 +15,8 @@ end local CommandView = DocView:extend() +CommandView.context = "application" + local max_suggestions = 10 local noop = function() end @@ -32,6 +34,7 @@ function CommandView:new() self.suggestion_idx = 1 self.suggestions = {} self.suggestions_height = 0 + self.show_suggestions = true self.last_change_id = 0 self.gutter_width = 0 self.gutter_text_brightness = 0 @@ -43,6 +46,11 @@ function CommandView:new() end +function CommandView:set_hidden_suggestions() + self.show_suggestions = false +end + + function CommandView:get_name() return View.get_name(self) end @@ -81,10 +89,29 @@ end function CommandView:move_suggestion_idx(dir) - local n = self.suggestion_idx + dir - self.suggestion_idx = common.clamp(n, 1, #self.suggestions) - self:complete() - self.last_change_id = self.doc:get_change_id() + if self.show_suggestions then + local n = self.suggestion_idx + dir + self.suggestion_idx = common.clamp(n, 1, #self.suggestions) + self:complete() + self.last_change_id = self.doc:get_change_id() + else + local current_suggestion = #self.suggestions > 0 and self.suggestions[self.suggestion_idx].text + local text = self:get_text() + if text == current_suggestion then + local n = self.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:complete() + end + else + self.save_suggestion = text + self:complete() + end + self.last_change_id = self.doc:get_change_id() + self.state.suggest(self:get_text()) + end end @@ -132,6 +159,8 @@ 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 end @@ -185,7 +214,7 @@ function CommandView:update() -- update suggestions box height local lh = self:get_suggestion_line_height() - local dest = math.min(#self.suggestions, max_suggestions) * lh + local dest = self.show_suggestions and math.min(#self.suggestions, max_suggestions) * lh or 0 self:move_towards("suggestions_height", dest) -- update suggestion cursor offset @@ -254,7 +283,9 @@ end function CommandView:draw() CommandView.super.draw(self) - core.root_view:defer_draw(draw_suggestions_box, self) + if self.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 3df3e8f1..9f3102bb 100644 --- a/data/core/common.lua +++ b/data/core/common.lua @@ -230,6 +230,12 @@ function common.basename(path) end +-- can return nil if there is no directory part in the path +function common.dirname(path) + return path:match("(.+)[\\/][^\\/]+$") +end + + function common.home_encode(text) if HOME and string.find(text, HOME, 1, true) == 1 then local dir_pos = #HOME + 1 @@ -257,18 +263,11 @@ function common.home_expand(text) end -function common.normalize_path(filename) - if filename and PATHSEP == '\\' then - filename = filename:gsub('[/\\]', '\\') - local drive, rem = filename:match('^([a-zA-Z])(:.*)') - return drive and drive:upper() .. rem or filename - end - return filename -end - - local function split_on_slash(s, sep_pattern) local t = {} + if s:match("^[/\\]") then + t[#t + 1] = "" + end for fragment in string.gmatch(s, "([^/\\]+)") do t[#t + 1] = fragment end @@ -276,12 +275,39 @@ local function split_on_slash(s, sep_pattern) end +function common.normalize_path(filename) + if not filename then return end + if PATHSEP == '\\' then + filename = filename:gsub('[/\\]', '\\') + local drive, rem = filename:match('^([a-zA-Z])(:.*)') + filename = drive and drive:upper() .. rem or filename + end + local parts = split_on_slash(filename, PATHSEP) + local accu = {} + for _, part in ipairs(parts) do + if part == '..' and #accu > 0 and accu[#accu] ~= ".." then + table.remove(accu) + elseif part ~= '.' then + table.insert(accu, part) + end + end + local npath = table.concat(accu, PATHSEP) + return npath == "" and PATHSEP or npath +end + + function common.path_belongs_to(filename, path) - return filename and string.find(filename, path .. PATHSEP, 1, true) == 1 + return string.find(filename, path .. PATHSEP, 1, true) == 1 end function common.relative_path(ref_dir, dir) + local drive_pattern = "^(%a):\\" + local drive, ref_drive = dir:match(drive_pattern), ref_dir:match(drive_pattern) + if drive and ref_drive and drive ~= ref_drive then + -- Windows, different drives, system.absolute_path fails for C:\..\D:\ + return dir + end local ref_ls = split_on_slash(ref_dir) local dir_ls = split_on_slash(dir) local i = 1 diff --git a/data/core/config.lua b/data/core/config.lua index d9f2362e..689968d5 100644 --- a/data/core/config.lua +++ b/data/core/config.lua @@ -11,6 +11,7 @@ config.non_word_chars = " \t\n/\\()\"':,.;<>~!@#$%^&*|+=[]{}`?-" config.undo_merge_timeout = 0.3 config.max_undos = 10000 config.max_tabs = 10 +config.always_show_tabs = false config.highlight_current_line = true config.line_height = 1.2 config.indent_size = 2 @@ -27,7 +28,9 @@ config.tab_close_button = true -- Disable plugin loading setting to false the config entry -- of the same name. -config.trimwhitespace = false -config.lineguide = false +config.plugins = {} + +config.plugins.trimwhitespace = false +config.plugins.lineguide = false return config diff --git a/data/core/contextmenu.lua b/data/core/contextmenu.lua new file mode 100644 index 00000000..36247597 --- /dev/null +++ b/data/core/contextmenu.lua @@ -0,0 +1,218 @@ +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 Object = require "core.object" + +local border_width = 1 +local divider_width = 1 +local DIVIDER = {} + +local ContextMenu = Object:extend() + +ContextMenu.DIVIDER = DIVIDER + +function ContextMenu:new() + self.itemset = {} + self.show_context_menu = false + self.selected = -1 + self.height = 0 + self.position = { x = 0, y = 0 } +end + +local function get_item_size(item) + local lw, lh + if item == DIVIDER then + lw = 0 + lh = divider_width + else + lw = style.font:get_width(item.text) + if item.info then + lw = lw + style.padding.x + style.font:get_width(item.info) + end + lh = style.font:get_height() + style.padding.y + end + 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 + item.info = keymap.reverse_map[item.command] + end + local lw, lh = get_item_size(item) + width = math.max(width, lw) + height = height + lh + end + width = width + style.padding.x * 2 + items.width, items.height = width, height + table.insert(self.itemset, { predicate = predicate, items = items }) +end + +function ContextMenu:show(x, y) + self.items = nil + local items_list = { width = 0, height = 0 } + for _, items in ipairs(self.itemset) do + if items.predicate(x, y) then + items_list.width = math.max(items_list.width, items.items.width) + items_list.height = items_list.height + items.items.height + for _, subitems in ipairs(items.items) do + table.insert(items_list, subitems) + end + end + end + + if #items_list > 0 then + self.items = items_list + local w, h = self.items.width, self.items.height + + -- by default the box is opened on the right and below + if x + w >= core.root_view.size.x then + x = x - w + end + if y + h >= core.root_view.size.y then + y = y - h + end + + self.position.x, self.position.y = x, y + self.show_context_menu = true + return true + end + return false +end + +function ContextMenu:hide() + self.show_context_menu = false + self.items = nil + self.selected = -1 + self.height = 0 +end + +function ContextMenu:each_item() + local x, y, w = self.position.x, self.position.y, self.items.width + local oy = y + return coroutine.wrap(function() + for i, item in ipairs(self.items) do + local _, lh = get_item_size(item) + if y - oy > self.height then break end + coroutine.yield(i, item, x, y, w, lh) + y = y + lh + end + end) +end + +function ContextMenu:on_mouse_moved(px, py) + if not self.show_context_menu then return end + + self.selected = -1 + for i, item, x, y, w, h in self:each_item() do + if px > x and px <= x + w and py > y and py <= y + h then + self.selected = i + break + end + end + if self.selected >= 0 then + core.request_cursor("arrow") + end + return true +end + +function ContextMenu:on_selected(item) + if type(item.command) == "string" then + command.perform(item.command) + else + item.command() + end +end + +function ContextMenu:on_mouse_pressed(button, x, y, clicks) + local selected = (self.items or {})[self.selected] + local caught = false + + self:hide() + if button == "left" then + if selected then + self:on_selected(selected) + caught = true + end + end + + if button == "right" then + caught = self:show(x, y) + 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 + +function ContextMenu:update() + if self.show_context_menu then + self:move_towards("height", self.items.height) + end +end + +function ContextMenu:draw() + if not self.show_context_menu then return end + core.root_view:defer_draw(self.draw_context_menu, self) +end + +function ContextMenu:draw_context_menu() + if not self.items then return end + local bx, by, bw, bh = self.position.x, self.position.y, self.items.width, self.height + + renderer.draw_rect( + bx - border_width, + by - border_width, + bw + (border_width * 2), + bh + (border_width * 2), + style.divider + ) + renderer.draw_rect(bx, by, bw, bh, style.background3) + + for i, item, x, y, w, h in self:each_item() do + if item == DIVIDER then + renderer.draw_rect(x, y, w, h, style.caret) + else + if i == self.selected then + renderer.draw_rect(x, y, w, h, style.selection) + end + + common.draw_text(style.font, style.text, item.text, "left", x + style.padding.x, y, w, h) + if item.info then + common.draw_text(style.font, style.dim, item.info, "right", x, y, w - style.padding.x, h) + end + end + end +end + +return ContextMenu diff --git a/data/core/doc/init.lua b/data/core/doc/init.lua index c33ade5a..aff31e94 100644 --- a/data/core/doc/init.lua +++ b/data/core/doc/init.lua @@ -17,10 +17,15 @@ local function split_lines(text) return res end -function Doc:new(filename) + +function Doc:new(filename, abs_filename, new_file) + self.new_file = new_file self:reset() if filename then - self:load(filename) + self:set_filename(filename, abs_filename) + if not new_file then + self:load(filename) + end end end @@ -47,16 +52,15 @@ function Doc:reset_syntax() end -function Doc:set_filename(filename) +function Doc:set_filename(filename, abs_filename) self.filename = filename - self.abs_filename = system.absolute_path(filename) + self.abs_filename = abs_filename end function Doc:load(filename) local fp = assert( io.open(filename, "rb") ) self:reset() - self:set_filename(filename) self.lines = {} for line in fp:lines() do if line:byte(-1) == 13 then @@ -73,17 +77,20 @@ function Doc:load(filename) end -function Doc:save(filename) - filename = filename or assert(self.filename, "no filename set to default to") +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 + end local fp = assert( io.open(filename, "wb") ) for _, line in ipairs(self.lines) do if self.crlf then line = line:gsub("\n", "\r\n") end fp:write(line) end fp:close() - if filename then - self:set_filename(filename) - end + self:set_filename(filename, abs_filename) + self.new_file = false self:reset_syntax() self:clean() end @@ -95,7 +102,7 @@ end function Doc:is_dirty() - return self.clean_change_id ~= self:get_change_id() + return self.clean_change_id ~= self:get_change_id() or self.new_file end @@ -117,6 +124,19 @@ function Doc:get_selection(sort) return line1, col1, line2, col2, sort end +function Doc:get_selection_text(limit) + limit = limit or math.huge + local result = {} + for idx, line1, col1, line2, col2 in self:get_selections() do + if idx > limit then break end + if line1 ~= line2 or col1 ~= col2 then + local text = self:get_text(line1, col1, line2, col2) + if text ~= "" then result[#result + 1] = text end + end + end + return table.concat(result, "\n") +end + function Doc:has_selection() local line1, col1, line2, col2 = self:get_selection(false) return line1 ~= line2 or col1 ~= col2 @@ -166,10 +186,12 @@ 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) @@ -300,6 +322,7 @@ end function Doc:raw_insert(line, col, text, undo_stack, time) -- split text into lines and merge with line at insertion point local lines = split_lines(text) + local len = #lines[#lines] local before = self.lines[line]:sub(1, col - 1) local after = self.lines[line]:sub(col) for i = 1, #lines - 1 do @@ -310,6 +333,14 @@ 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 + local line_addition = (line < cline1 or col < ccol1) and #lines - 1 or 0 + local column_addition = line == cline1 and ccol1 > col and len or 0 + self:set_selections(idx, cline1 + line_addition, ccol1 + column_addition, cline2 + line_addition, ccol2 + column_addition) + end -- push undo local line2, col2 = self:position_offset(line, col, #text) @@ -334,6 +365,14 @@ 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 + local line_removal = line2 - line1 + local column_removal = line2 == cline2 and col2 < ccol1 and (line2 == line1 and col2 - col1 or col2) or 0 + self:set_selections(idx, cline1 - line_removal, ccol1 - column_removal, cline2 - line_removal, ccol2 - column_removal) + end -- update highlighter and assure selection is in bounds self.highlighter:invalidate(line1) @@ -370,7 +409,7 @@ end function Doc:text_input(text, idx) - for sidx, line1, col1, line2, col2 in self:get_selections(true, idx) do + for sidx, line1, col1, line2, col2 in self:get_selections(true, idx or true) do if line1 ~= line2 or col1 ~= col2 then self:delete_to_cursor(sidx) end @@ -379,12 +418,7 @@ function Doc:text_input(text, idx) end end - -function Doc:replace(fn) - local line1, col1, line2, col2 = self:get_selection(true) - if line1 == line2 and col1 == col2 then - line1, col1, line2, col2 = 1, 1, #self.lines, #self.lines[#self.lines] - 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) if old_text ~= new_text then @@ -392,12 +426,27 @@ function Doc:replace(fn) self:remove(line1, col1, line2, col2) if line1 == line2 and col1 == col2 then line2, col2 = self:position_offset(line1, col1, #new_text) - self:set_selection(line1, col1, line2, col2) + self:set_selections(idx, line1, col1, line2, col2) end end return n end +function Doc:replace(fn) + local has_selection, n = false, 0 + 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) + 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) + end + return n +end + function Doc:delete_to_cursor(idx, ...) for sidx, line1, col1, line2, col2 in self:get_selections(true, idx) do diff --git a/data/core/doc/translate.lua b/data/core/doc/translate.lua index b084e89a..d1bde5f0 100644 --- a/data/core/doc/translate.lua +++ b/data/core/doc/translate.lua @@ -117,6 +117,10 @@ function translate.start_of_line(doc, line, col) return line, 1 end +function translate.start_of_indentation(doc, line, col) + local s, e = doc.lines[line]:find("^%s*") + return line, col > e + 1 and e + 1 or 1 +end function translate.end_of_line(doc, line, col) return line, math.huge diff --git a/data/core/docview.lua b/data/core/docview.lua index ceed8636..161eac47 100644 --- a/data/core/docview.lua +++ b/data/core/docview.lua @@ -9,6 +9,7 @@ local View = require "core.view" local DocView = View:extend() +DocView.context = "session" local function move_to_line_offset(dv, line, col, offset) local xo = dv.last_x_offset @@ -112,7 +113,8 @@ end function DocView:get_gutter_width() - return self:get_font():get_width(#self.doc.lines) + style.padding.x * 2 + local padding = style.padding.x * 2 + return self:get_font():get_width(#self.doc.lines) + padding, padding end @@ -365,23 +367,27 @@ function DocView:draw_line_body(idx, x, y) 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() - renderer.draw_rect(x1, y, x2 - x1, lh, style.selection) + if x1 ~= x2 then + renderer.draw_rect(x1, y, x2 - x1, lh, style.selection) + 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 config.highlight_current_line and (line1 == line2 and col1 == col2) + if draw_highlight ~= false and config.highlight_current_line and line1 == idx and core.active_view == self then - self:draw_line_highlight(x + self.scroll.x, y) + draw_highlight = (line1 == line2 and col1 == col2) end end + if draw_highlight then self:draw_line_highlight(x + self.scroll.x, y) end -- draw line's text self:draw_line_text(idx, x, y) end -function DocView:draw_line_gutter(idx, x, y) +function DocView:draw_line_gutter(idx, 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 @@ -391,7 +397,7 @@ function DocView:draw_line_gutter(idx, x, y) end local yoffset = self:get_line_text_y_offset() x = x + style.padding.x - renderer.draw_text(self:get_font(), idx, x, y + yoffset, color) + common.draw_text(self:get_font(), color, idx, "right", x, y + yoffset, width, self:get_line_height()) end @@ -420,12 +426,12 @@ function DocView:draw() local lh = self:get_line_height() 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) + self:draw_line_gutter(i, self.position.x, y, gpad and gw - gpad or gw) y = y + lh end - local gw = self:get_gutter_width() local pos = self.position x, y = self:get_line_screen_position(minline) core.push_clip_rect(pos.x + gw, pos.y, self.size.x, self.size.y) diff --git a/data/core/init.lua b/data/core/init.lua index 2939d966..7593c5e2 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -17,10 +17,7 @@ local core = {} local function load_session() local ok, t = pcall(dofile, USERDIR .. "/session.lua") - if ok then - return t.recents, t.window, t.window_mode - end - return {} + return ok and t or {} end @@ -30,20 +27,16 @@ local function save_session() fp:write("return {recents=", common.serialize(core.recent_projects), ", window=", common.serialize(table.pack(system.get_window_size())), ", window_mode=", common.serialize(system.get_window_mode()), + ", previous_find=", common.serialize(core.previous_find), + ", previous_replace=", common.serialize(core.previous_replace), "}\n") fp:close() end end -local function normalize_path(s) - local drive, path = s:match("^([a-z]):([/\\].*)") - return drive and drive:upper() .. ":" .. path or s -end - - local function update_recents_project(action, dir_path_abs) - local dirname = normalize_path(dir_path_abs) + local dirname = common.normalize_path(dir_path_abs) if not dirname then return end local recents = core.recent_projects local n = #recents @@ -63,7 +56,7 @@ function core.set_project_dir(new_dir, change_project_fn) local chdir_ok = pcall(system.chdir, new_dir) if chdir_ok then if change_project_fn then change_project_fn() end - core.project_dir = normalize_path(new_dir) + core.project_dir = common.normalize_path(new_dir) core.project_directories = {} core.add_project_directory(new_dir) return true @@ -153,17 +146,14 @@ end function core.project_subdir_set_show(dir, filename, show) dir.shown_subdir[filename] = show - if dir.files_limit then + if dir.files_limit and PLATFORM == "Linux" then local fullpath = dir.name .. PATHSEP .. filename - if PLATFORM == "Linux" then - if show then - local success = system.watch_dir_add(dir.watch_id, fullpath) - print("DEBUG: watch_dir_add", fullpath, "success:", success) - else - print("DEBUG dir", dir.name, "filename", filename, "watch_id:", dir.watch_id) - local success = system.watch_dir_rm(dir.watch_id, fullpath) - print("DEBUG: watch_dir_rm", fullpath, "success:", success) - end + local watch_fn = show and system.watch_dir_add or system.watch_dir_rm + print("DEBUG dir", dir.name, "filename", filename, "watch_id:", dir.watch_id, "show:", show) + local success = watch_fn(dir.watch_id, fullpath) + print("DEBUG: ", show and "watch_dir_add" or "watch_dir_rm", fullpath, "success:", success) + if not success then + core.log("Internal warning: error calling system.watch_dir_%s", show and "add" or "rm") end end end @@ -208,14 +198,12 @@ 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 = normalize_path(path) - -- local watch_id = system.watch_dir(path) + path = common.normalize_path(path) local dir = { name = path, item = {filename = common.basename(path), type = "dir", topdir = true}, files_limit = false, is_dirty = true, - -- watch_id = watch_id, shown_subdir = {}, } table.insert(core.project_directories, dir) @@ -490,8 +478,8 @@ local style = require "core.style" ------------------------------- Fonts ---------------------------------------- -- customize fonts: --- style.font = renderer.font.load(DATADIR .. "/fonts/FiraSans-Regular.ttf", 13 * SCALE) --- style.code_font = renderer.font.load(DATADIR .. "/fonts/JetBrainsMono-Regular.ttf", 13 * SCALE) +-- 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) -- -- font names used by lite: -- style.font : user interface @@ -512,11 +500,11 @@ local style = require "core.style" -- enable or disable plugin loading setting config entries: --- enable trimwhitespace, otherwise it is disable by default: --- config.trimwhitespace = true +-- enable plugins.trimwhitespace, otherwise it is disable by default: +-- config.plugins.trimwhitespace = true -- -- disable detectindent, otherwise it is enabled by default --- config.detectindent = false +-- config.plugins.detectindent = false ]]) init_file:close() end @@ -592,13 +580,15 @@ function core.init() end do - local recent_projects, window_position, window_mode = load_session() - if window_mode == "normal" then - system.set_window_size(table.unpack(window_position)) - elseif window_mode == "maximized" then + local session = load_session() + if session.window_mode == "normal" then + system.set_window_size(table.unpack(session.window)) + elseif session.window_mode == "maximized" then system.set_window_mode("maximized") end - core.recent_projects = recent_projects + core.recent_projects = session.recents or {} + core.previous_find = session.previous_find or {} + core.previous_replace = session.previous_replace or {} end local project_dir = core.recent_projects[1] or "." @@ -618,7 +608,10 @@ function core.init() project_dir = arg_filename project_dir_explicit = true else - delayed_error = string.format("error: invalid file or directory %q", ARGS[i]) + -- 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]) + end end end @@ -651,6 +644,7 @@ function core.init() core.redraw = true core.visited_files = {} core.restart_request = false + core.quit_request = false core.replacements = whitespace_replacements() core.root_view = RootView() @@ -726,10 +720,10 @@ function core.init() end -function core.confirm_close_all(close_fn, ...) +function core.confirm_close_docs(docs, close_fn, ...) local dirty_count = 0 local dirty_name - for _, doc in ipairs(core.docs) do + for _, doc in ipairs(docs or core.docs) do if doc:is_dirty() then dirty_count = dirty_count + 1 dirty_name = doc:get_name() @@ -782,24 +776,6 @@ do end --- DEPRECATED function -core.doc_save_hooks = {} -function core.add_save_hook(fn) - core.error("The function core.add_save_hook is deprecated." .. - " Modules should now directly override the Doc:save function.") - core.doc_save_hooks[#core.doc_save_hooks + 1] = fn -end - - --- DEPRECATED function -function core.on_doc_save(filename) - -- for backward compatibility in modules. Hooks are deprecated, the function Doc:save - -- should be directly overidded. - for _, hook in ipairs(core.doc_save_hooks) do - hook(filename) - end -end - local function quit_with_function(quit_fn, force) if force then delete_temp_files() @@ -807,12 +783,12 @@ local function quit_with_function(quit_fn, force) save_session() quit_fn() else - core.confirm_close_all(quit_with_function, quit_fn, true) + core.confirm_close_docs(core.docs, quit_with_function, quit_fn, true) end end function core.quit(force) - quit_with_function(os.exit, force) + quit_with_function(function() core.quit_request = true end, force) end @@ -826,8 +802,8 @@ local function check_plugin_version(filename) if info ~= nil and info.type == "dir" then filename = filename .. "/init.lua" info = system.get_file_info(filename) - if not info then return true end end + 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 version_match = false @@ -841,13 +817,13 @@ local function check_plugin_version(filename) -- 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 1.16 equivalent to mod-version:1 - version_match = (version == '1.16' and MOD_VERSION == "1") + -- we consider the version tag 2.0 equivalent to mod-version:2 + version_match = (version == '2.0' and MOD_VERSION == "2") break end end f:close() - return version_match + return true, version_match end @@ -857,20 +833,25 @@ function core.load_plugins() userdir = {dir = USERDIR, plugins = {}}, datadir = {dir = DATADIR, plugins = {}}, } - for _, root_dir in ipairs {USERDIR, DATADIR} do + local files = {} + for _, root_dir in ipairs {DATADIR, USERDIR} do local plugin_dir = root_dir .. "/plugins" - local files = system.list_dir(plugin_dir) - for _, filename in ipairs(files or {}) do - local basename = filename:match("(.-)%.lua$") or filename - local version_match = check_plugin_version(plugin_dir .. '/' .. filename) + for _, filename in ipairs(system.list_dir(plugin_dir) or {}) do + files[filename] = plugin_dir -- user plugins will always replace system plugins + end + end + + for filename, plugin_dir in pairs(files) do + local basename = 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 ls = refused_list[root_dir == USERDIR and 'userdir' or 'datadir'].plugins - ls[#ls + 1] = filename + 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[basename] ~= false then - local modname = "plugins." .. basename - local ok = core.try(require, modname) + 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 if not ok then no_errors = false @@ -920,8 +901,12 @@ end function core.set_active_view(view) assert(view, "Tried to set active view to nil") - if core.active_view and core.active_view.force_focus then return end if view ~= core.active_view then + if core.active_view and core.active_view.force_focus then + core.next_active_view = view + return + end + core.next_active_view = nil if view.doc and view.doc.filename then core.set_visited(view.doc.filename) end @@ -971,10 +956,30 @@ 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: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 if filename then + -- normalize filename and set absolute filename then -- try to find existing doc for filename - local abs_filename = system.absolute_path(filename) + filename = core.normalize_to_project_dir(filename) + abs_filename = core.project_absolute_path(filename) for _, doc in ipairs(core.docs) do if doc.abs_filename and abs_filename == doc.abs_filename then return doc @@ -982,8 +987,7 @@ function core.open_doc(filename) end end -- no existing doc for filename; create new - filename = core.normalize_to_project_dir(filename) - local doc = Doc(filename) + local doc = Doc(filename, abs_filename, new_file) table.insert(core.docs, doc) core.log_quiet(filename and "Opened doc \"%s\"" or "Opened new doc", filename) return doc @@ -1032,6 +1036,23 @@ function core.error(...) end +function core.get_log(i) + if i == nil then + local r = {} + for _, item in ipairs(core.log_items) do + table.insert(r, core.get_log(item)) + end + 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) + if item.info then + text = string.format("%s\n%s\n", text, item.info) + end + return text +end + + function core.try(fn, ...) local err local ok, res = xpcall(fn, function(msg) @@ -1061,9 +1082,12 @@ function core.dir_rescan_add_job(dir, filepath) 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"}) - local dir_filename = dir.files[dir_index].filename - if not dir_match or not core.project_subdir_is_shown(dir, dir_filename) then - print("DEBUG do not start a rescan job for", abs_dirpath); return end + -- 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 + print("DEBUG do not start a rescan job for", abs_dirpath) + return + end end local new_time = system.get_time() + 1 @@ -1263,9 +1287,9 @@ function core.run() while true do core.frame_start = system.get_time() local did_redraw = core.step() - local need_more_work = run_threads() - if core.restart_request then break end - if not did_redraw and not need_more_work and not core.has_pending_rescan() then + 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 -- do not wait of events at idle_iterations = 1 to give a chance at core.step to run -- and set "redraw" flag. diff --git a/data/core/keymap-macos.lua b/data/core/keymap-macos.lua index e233bb2e..53a20468 100644 --- a/data/core/keymap-macos.lua +++ b/data/core/keymap-macos.lua @@ -52,23 +52,27 @@ local function keymap_macos(keymap) ["shift+tab"] = "doc:unindent", ["backspace"] = "doc:backspace", ["shift+backspace"] = "doc:backspace", - ["cmd+backspace"] = "doc:delete-to-previous-word-start", + ["option+backspace"] = "doc:delete-to-previous-word-start", ["cmd+shift+backspace"] = "doc:delete-to-previous-word-start", + ["cmd+backspace"] = "doc:delete-to-start-of-indentation", ["delete"] = "doc:delete", ["shift+delete"] = "doc:delete", - ["cmd+delete"] = "doc:delete-to-next-word-end", + ["option+delete"] = "doc:delete-to-next-word-end", ["cmd+shift+delete"] = "doc:delete-to-next-word-end", + ["cmd+delete"] = "doc:delete-to-end-of-line", ["return"] = { "command:submit", "doc:newline", "dialog:select" }, ["keypad enter"] = { "command:submit", "doc:newline", "dialog:select" }, ["cmd+return"] = "doc:newline-below", ["cmd+shift+return"] = "doc:newline-above", ["cmd+j"] = "doc:join-lines", ["cmd+a"] = "doc:select-all", - ["cmd+d"] = { "find-replace:select-next", "doc:select-word" }, + ["cmd+d"] = { "find-replace:select-add-next", "doc:select-word" }, + ["cmd+f3"] = "find-replace:select-next", ["cmd+l"] = "doc:select-lines", + ["cmd+shift+l"] = { "find-replace:select-add-all", "doc:select-word" }, ["cmd+/"] = "doc:toggle-line-comments", - ["cmd+up"] = "doc:move-lines-up", - ["cmd+down"] = "doc:move-lines-down", + ["option+up"] = "doc:move-lines-up", + ["option+down"] = "doc:move-lines-down", ["cmd+shift+d"] = "doc:duplicate-lines", ["cmd+shift+k"] = "doc:delete-lines", @@ -76,14 +80,16 @@ local function keymap_macos(keymap) ["right"] = { "doc:move-to-next-char", "dialog:next-entry"}, ["up"] = { "command:select-previous", "doc:move-to-previous-line" }, ["down"] = { "command:select-next", "doc:move-to-next-line" }, - ["cmd+left"] = "doc:move-to-previous-word-start", - ["cmd+right"] = "doc:move-to-next-word-end", + ["option+left"] = "doc:move-to-previous-word-start", + ["option+right"] = "doc:move-to-next-word-end", + ["cmd+left"] = "doc:move-to-start-of-indentation", + ["cmd+right"] = "doc:move-to-end-of-line", ["cmd+["] = "doc:move-to-previous-block-start", ["cmd+]"] = "doc:move-to-next-block-end", - ["home"] = "doc:move-to-start-of-line", + ["home"] = "doc:move-to-start-of-indentation", ["end"] = "doc:move-to-end-of-line", - ["cmd+home"] = "doc:move-to-start-of-doc", - ["cmd+end"] = "doc:move-to-end-of-doc", + ["cmd+up"] = "doc:move-to-start-of-doc", + ["cmd+down"] = "doc:move-to-end-of-doc", ["pageup"] = "doc:move-to-previous-page", ["pagedown"] = "doc:move-to-next-page", @@ -91,18 +97,20 @@ local function keymap_macos(keymap) ["shift+right"] = "doc:select-to-next-char", ["shift+up"] = "doc:select-to-previous-line", ["shift+down"] = "doc:select-to-next-line", - ["cmd+shift+left"] = "doc:select-to-previous-word-start", - ["cmd+shift+right"] = "doc:select-to-next-word-end", + ["option+shift+left"] = "doc:select-to-previous-word-start", + ["option+shift+right"] = "doc:select-to-next-word-end", + ["cmd+shift+left"] = "doc:select-to-start-of-indentation", + ["cmd+shift+right"] = "doc:select-to-end-of-line", ["cmd+shift+["] = "doc:select-to-previous-block-start", ["cmd+shift+]"] = "doc:select-to-next-block-end", - ["shift+home"] = "doc:select-to-start-of-line", + ["shift+home"] = "doc:select-to-start-of-indentation", ["shift+end"] = "doc:select-to-end-of-line", - ["cmd+shift+home"] = "doc:select-to-start-of-doc", - ["cmd+shift+end"] = "doc:select-to-end-of-doc", + ["cmd+shift+up"] = "doc:select-to-start-of-doc", + ["cmd+shift+down"] = "doc:select-to-end-of-doc", ["shift+pageup"] = "doc:select-to-previous-page", ["shift+pagedown"] = "doc:select-to-next-page", - ["cmd+shift+up"] = "doc:create-cursor-previous-line", - ["cmd+shift+down"] = "doc:create-cursor-next-line" + ["cmd+option+up"] = "doc:create-cursor-previous-line", + ["cmd+option+down"] = "doc:create-cursor-next-line" } end diff --git a/data/core/keymap.lua b/data/core/keymap.lua index cfbd9efc..cb2aa876 100644 --- a/data/core/keymap.lua +++ b/data/core/keymap.lua @@ -108,6 +108,7 @@ keymap.add_direct { ["ctrl+shift+c"] = "core:change-project-folder", ["ctrl+shift+o"] = "core:open-project-folder", ["alt+return"] = "core:toggle-fullscreen", + ["f11"] = "core:toggle-fullscreen", ["alt+shift+j"] = "root:split-left", ["alt+shift+l"] = "root:split-right", @@ -137,8 +138,8 @@ keymap.add_direct { ["ctrl+r"] = "find-replace:replace", ["f3"] = "find-replace:repeat-find", ["shift+f3"] = "find-replace:previous-find", - ["ctrl+i"] = "find-replace:toggle-insensitivity", - ["ctrl+shift+i"] = "find-replace:toggle-plain", + ["ctrl+i"] = "find-replace:toggle-sensitivity", + ["ctrl+shift+i"] = "find-replace:toggle-regex", ["ctrl+g"] = "doc:go-to-line", ["ctrl+s"] = "doc:save", ["ctrl+shift+s"] = "doc:save-as", @@ -167,8 +168,10 @@ keymap.add_direct { ["ctrl+shift+return"] = "doc:newline-above", ["ctrl+j"] = "doc:join-lines", ["ctrl+a"] = "doc:select-all", - ["ctrl+d"] = { "find-replace:select-next", "doc:select-word" }, + ["ctrl+d"] = { "find-replace:select-add-next", "doc:select-word" }, + ["ctrl+f3"] = "find-replace:select-next", ["ctrl+l"] = "doc:select-lines", + ["ctrl+shift+l"] = { "find-replace:select-add-all", "doc:select-word" }, ["ctrl+/"] = "doc:toggle-line-comments", ["ctrl+up"] = "doc:move-lines-up", ["ctrl+down"] = "doc:move-lines-down", @@ -183,7 +186,7 @@ keymap.add_direct { ["ctrl+right"] = "doc:move-to-next-word-end", ["ctrl+["] = "doc:move-to-previous-block-start", ["ctrl+]"] = "doc:move-to-next-block-end", - ["home"] = "doc:move-to-start-of-line", + ["home"] = "doc:move-to-start-of-indentation", ["end"] = "doc:move-to-end-of-line", ["ctrl+home"] = "doc:move-to-start-of-doc", ["ctrl+end"] = "doc:move-to-end-of-doc", @@ -198,7 +201,7 @@ keymap.add_direct { ["ctrl+shift+right"] = "doc:select-to-next-word-end", ["ctrl+shift+["] = "doc:select-to-previous-block-start", ["ctrl+shift+]"] = "doc:select-to-next-block-end", - ["shift+home"] = "doc:select-to-start-of-line", + ["shift+home"] = "doc:select-to-start-of-indentation", ["shift+end"] = "doc:select-to-end-of-line", ["ctrl+shift+home"] = "doc:select-to-start-of-doc", ["ctrl+shift+end"] = "doc:select-to-end-of-doc", diff --git a/data/core/logview.lua b/data/core/logview.lua index d7142fb5..1ea0e43e 100644 --- a/data/core/logview.lua +++ b/data/core/logview.lua @@ -1,14 +1,45 @@ local core = require "core" +local common = require "core.common" local style = require "core.style" local View = require "core.view" +local function lines(text) + if text == "" then return 0 end + local l = 1 + for _ in string.gmatch(text, "\n") do + l = l + 1 + end + return l +end + + +local item_height_result = {} + + +local function get_item_height(item) + local h = item_height_result[item] + if not h then + h = {} + local l = 1 + lines(item.text) + lines(item.info or "") + h.normal = style.font:get_height() + style.padding.y + h.expanded = l * style.font:get_height() + style.padding.y + h.current = h.normal + h.target = h.current + item_height_result[item] = h + end + return h +end + + 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 end @@ -19,6 +50,55 @@ function LogView:get_name() end +local function is_expanded(item) + local item_height = get_item_height(item) + return item_height.target == item_height.expanded +end + + +function LogView:expand_item(item) + item = get_item_height(item) + item.target = item.target == item.expanded and item.normal or item.expanded + table.insert(self.expanding, item) +end + + +function LogView:each_item() + local x, y = self:get_content_offset() + y = y + style.padding.y + self.yoffset + return coroutine.wrap(function() + for i = #core.log_items, 1, -1 do + local item = core.log_items[i] + local h = get_item_height(item).current + coroutine.yield(i, item, x, y, self.size.x, h) + y = y + h + end + end) +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 + end + if not hovered then self.hovered_item = nil end +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) + end +end + + function LogView:update() local item = core.log_items[#core.log_items] if self.last_item ~= item then @@ -27,6 +107,14 @@ function LogView:update() self.yoffset = -(style.font:get_height() + style.padding.y) end + local expanding = self.expanding[1] + if expanding then + self:move_towards(expanding, "current", expanding.target) + if expanding.current == expanding.target then + table.remove(self.expanding, 1) + end + end + self:move_towards("yoffset", 0) LogView.super.update(self) @@ -35,38 +123,48 @@ end local function draw_text_multiline(font, text, x, y, color) local th = font:get_height() - local resx, resy = x, y + local resx = x for line in text:gmatch("[^\n]+") do - resy = y resx = renderer.draw_text(style.font, line, x, y, color) y = y + th end - return resx, resy + return resx, y end function LogView:draw() self:draw_background(style.background) - local ox, oy = self:get_content_offset() local th = style.font:get_height() - local y = oy + style.padding.y + self.yoffset - - for i = #core.log_items, 1, -1 do - local x = ox + style.padding.x - local item = core.log_items[i] - local time = os.date(nil, item.time) - x = renderer.draw_text(style.font, time, x, y, style.dim) + 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 subx = x - x, y = draw_text_multiline(style.font, item.text, x, y, style.text) - renderer.draw_text(style.font, " at " .. item.at, x, y, style.dim) - y = y + th - if item.info then - subx, y = draw_text_multiline(style.font, item.info, subx, y, style.dim) - y = y + th + + 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 + + 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()) + + 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 - y = y + style.padding.y end end diff --git a/data/core/nagview.lua b/data/core/nagview.lua index 6d6f89f4..3d448cd4 100644 --- a/data/core/nagview.lua +++ b/data/core/nagview.lua @@ -193,7 +193,8 @@ function NagView:next() self:change_hovered(common.find_index(self.options, "default_yes")) end self.force_focus = self.message ~= nil - core.set_active_view(self.message ~= nil and self or core.last_active_view) + 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) diff --git a/data/core/object.lua b/data/core/object.lua index af41b7e9..0941ce5d 100644 --- a/data/core/object.lua +++ b/data/core/object.lua @@ -20,17 +20,6 @@ function Object:extend() end -function Object:implement(...) - for _, cls in pairs({...}) do - for k, v in pairs(cls) do - if self[k] == nil and type(v) == "function" then - self[k] = v - end - end - end -end - - function Object:is(T) local mt = getmetatable(self) while mt do diff --git a/data/core/regex.lua b/data/core/regex.lua index 19c59164..69203cbd 100644 --- a/data/core/regex.lua +++ b/data/core/regex.lua @@ -1,5 +1,5 @@ --- So that in addition to regex.gsub(pattern, string), we can also do +-- So that in addition to regex.gsub(pattern, string), we can also do -- pattern:gsub(string). regex.__index = function(table, key) return regex[key]; end @@ -9,7 +9,7 @@ regex.match = function(pattern_string, string, offset, options) return regex.cmatch(pattern, string, offset or 1, options or 0) end --- Will iterate back through any UTF-8 bytes so that we don't replace bits +-- Will iterate back through any UTF-8 bytes so that we don't replace bits -- mid character. local function previous_character(str, index) local byte @@ -23,7 +23,7 @@ end -- Moves to the end of the identified character. local function end_character(str, index) local byte = string.byte(str, index + 1) - while byte >= 128 and byte < 192 do + while byte and byte >= 128 and byte < 192 do index = index + 1 byte = string.byte(str, index + 1) end @@ -32,7 +32,7 @@ end -- Build off matching. For now, only support basic replacements, but capture -- groupings should be doable. We can even have custom group replacements and --- transformations and stuff in lua. Currently, this takes group replacements +-- transformations and stuff in lua. Currently, this takes group replacements -- as \1 - \9. -- Should work on UTF-8 text. regex.gsub = function(pattern_string, str, replacement) @@ -48,8 +48,8 @@ regex.gsub = function(pattern_string, str, replacement) if #indices > 2 then for i = 1, (#indices/2 - 1) do currentReplacement = string.gsub( - currentReplacement, - "\\" .. i, + currentReplacement, + "\\" .. i, str:sub(indices[i*2+1], end_character(str,indices[i*2+2]-1)) ) end @@ -57,10 +57,10 @@ regex.gsub = function(pattern_string, str, replacement) currentReplacement = string.gsub(currentReplacement, "\\%d", "") table.insert(replacements, { indices[1], #currentReplacement+indices[1] }) if indices[1] > 1 then - result = result .. + result = result .. str:sub(1, previous_character(str, indices[1])) .. currentReplacement else - result = result .. currentReplacement + result = result .. currentReplacement end str = str:sub(indices[2]) end diff --git a/data/core/rootview.lua b/data/core/rootview.lua index 36ab148d..9d017268 100644 --- a/data/core/rootview.lua +++ b/data/core/rootview.lua @@ -5,7 +5,6 @@ local style = require "core.style" local keymap = require "core.keymap" local Object = require "core.object" local View = require "core.view" -local CommandView = require "core.commandview" local NagView = require "core.nagview" local DocView = require "core.docview" @@ -240,12 +239,15 @@ end function Node:get_divider_overlapping_point(px, py) if self.type ~= "leaf" then - local p = 6 - local x, y, w, h = self:get_divider_rect() - x, y = x - p, y - p - w, h = w + p * 2, h + p * 2 - if px > x and py > y and px < x + w and py < y + h then - return self + local axis = self.type == "hsplit" and "x" or "y" + if self.a:is_resizable(axis) and self.b:is_resizable(axis) then + local p = 6 + local x, y, w, h = self:get_divider_rect() + x, y = x - p, y - p + w, h = w + p * 2, h + p * 2 + if px > x and py > y and px < x + w and py < y + h then + return self + end end return self.a:get_divider_overlapping_point(px, py) or self.b:get_divider_overlapping_point(px, py) @@ -259,7 +261,7 @@ end function Node:get_tab_overlapping_point(px, py) - if #self.views == 1 then return nil end + if not self:should_show_tabs() then return nil end local tabs_number = self:get_visible_tabs_number() local x1, y1, w, h = self:get_tab_rect(self.tab_offset) local x2, y2 = self:get_tab_rect(self.tab_offset + tabs_number) @@ -269,6 +271,16 @@ function Node:get_tab_overlapping_point(px, py) end +function Node:should_show_tabs() + if self.locked then return false end + if #self.views > 1 then return true + elseif config.always_show_tabs then + return not self.views[1]:is(EmptyView) + end + return false +end + + local function close_button_location(x, w) local cw = style.icon_font:get_width("C") local pad = style.padding.y @@ -412,7 +424,7 @@ end function Node:update_layout() if self.type == "leaf" then local av = self.active_view - if #self.views > 1 then + if self:should_show_tabs() then local _, _, _, th = self:get_tab_rect(1) av.position.x, av.position.y = self.position.x, self.position.y + th av.size.x, av.size.y = self.size.x, self.size.y - th @@ -567,7 +579,7 @@ end function Node:draw() if self.type == "leaf" then - if #self.views > 1 then + if self:should_show_tabs() then self:draw_tabs() end local pos, size = self.active_view.position, self.active_view.size @@ -591,23 +603,37 @@ function Node:is_empty() end -function Node:close_all_docviews() +function Node:close_all_docviews(keep_active) + local node_active_view = self.active_view + local lost_active_view = false if self.type == "leaf" then local i = 1 while i <= #self.views do local view = self.views[i] - if view:is(DocView) and not view:is(CommandView) then + if view.context == "session" and (not keep_active or view ~= self.active_view) then table.remove(self.views, i) + if view == node_active_view then + lost_active_view = true + end else i = i + 1 end end + self.tab_offset = 1 if #self.views == 0 and self.is_primary_node then + -- if we are not the primary view and we had the active view it doesn't + -- matter to reattribute the active view because, within the close_all_docviews + -- top call, the primary node will take the active view anyway. + -- Set the empty view and takes the active view. self:add_view(EmptyView()) + elseif #self.views > 0 and lost_active_view then + -- In practice we never get there but if a view remain we need + -- to reset the Node's active view. + self:set_active_view(self.views[1]) end else - self.a:close_all_docviews() - self.b:close_all_docviews() + self.a:close_all_docviews(keep_active) + self.b:close_all_docviews(keep_active) if self.a:is_empty() and not self.a.is_primary_node then self:consume(self.b) elseif self.b:is_empty() and not self.b.is_primary_node then @@ -733,8 +759,8 @@ function RootView:open_doc(doc) end -function RootView:close_all_docviews() - self.root_node:close_all_docviews() +function RootView:close_all_docviews(keep_active) + self.root_node:close_all_docviews(keep_active) end @@ -823,10 +849,7 @@ function RootView:on_mouse_moved(x, y, dx, dy) if node and node:get_scroll_button_index(x, y) then core.request_cursor("arrow") elseif div then - local axis = (div.type == "hsplit" and "x" or "y") - if div.a:is_resizable(axis) and div.b:is_resizable(axis) then - core.request_cursor(div.type == "hsplit" and "sizeh" or "sizev") - end + core.request_cursor(div.type == "hsplit" and "sizeh" or "sizev") elseif tab_index then core.request_cursor("arrow") elseif node then diff --git a/data/core/start.lua b/data/core/start.lua index 6090df4e..71050057 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 = "1.16.11" -MOD_VERSION = "1" +VERSION = "@PROJECT_VERSION@" +MOD_VERSION = "2" SCALE = tonumber(os.getenv("LITE_SCALE")) or SCALE PATHSEP = package.config:sub(1, 1) @@ -19,4 +19,3 @@ package.path = DATADIR .. '/?.lua;' .. package.path package.path = DATADIR .. '/?/init.lua;' .. package.path package.path = USERDIR .. '/?.lua;' .. package.path package.path = USERDIR .. '/?/init.lua;' .. package.path - diff --git a/data/core/style.lua b/data/core/style.lua index 7cf16eb1..faca166e 100644 --- a/data/core/style.lua +++ b/data/core/style.lua @@ -21,11 +21,11 @@ style.tab_width = common.round(170 * SCALE) -- -- On High DPI monitor or non RGB monitor you may consider using antialiasing grayscale instead. -- The antialiasing grayscale with full hinting is interesting for crisp font rendering. -style.font = renderer.font.load(DATADIR .. "/fonts/FiraSans-Regular.ttf", 13 * SCALE) +style.font = renderer.font.load(DATADIR .. "/fonts/FiraSans-Regular.ttf", 14 * SCALE) style.big_font = style.font:copy(40 * SCALE) -style.icon_font = renderer.font.load(DATADIR .. "/fonts/icons.ttf", 14 * SCALE, {antialiasing="grayscale", hinting="full"}) -style.icon_big_font = style.icon_font:copy(20 * SCALE) -style.code_font = renderer.font.load(DATADIR .. "/fonts/JetBrainsMono-Regular.ttf", 13 * SCALE) +style.icon_font = renderer.font.load(DATADIR .. "/fonts/icons.ttf", 16 * SCALE, {antialiasing="grayscale", hinting="full"}) +style.icon_big_font = style.icon_font:copy(24 * SCALE) +style.code_font = renderer.font.load(DATADIR .. "/fonts/JetBrainsMono-Regular.ttf", 14 * SCALE) style.background = { common.color "#2e2e32" } style.background2 = { common.color "#252529" } diff --git a/data/core/view.lua b/data/core/view.lua index 2fb431d6..d1374ee4 100644 --- a/data/core/view.lua +++ b/data/core/view.lua @@ -7,6 +7,10 @@ local Object = require "core.object" local View = Object:extend() +-- context can be "application" or "session". The instance of objects +-- with context "session" will be closed when a project session is +-- terminated. The context "application" is for functional UI elements. +View.context = "application" function View:new() self.position = { x = 0, y = 0 } diff --git a/data/plugins/autocomplete.lua b/data/plugins/autocomplete.lua index c76eed02..484199a9 100644 --- a/data/plugins/autocomplete.lua +++ b/data/plugins/autocomplete.lua @@ -1,4 +1,4 @@ --- mod-version:1 -- lite-xl 1.16 +-- mod-version:2 -- lite-xl 2.0 local core = require "core" local common = require "core.common" local config = require "core.config" @@ -8,25 +8,65 @@ local keymap = require "core.keymap" local translate = require "core.doc.translate" local RootView = require "core.rootview" local DocView = require "core.docview" +local Doc = require "core.doc" -config.autocomplete_max_suggestions = 6 +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, +} local autocomplete = {} -autocomplete.map = {} +autocomplete.map = {} +autocomplete.map_manually = {} +autocomplete.on_close = nil + +-- Flag that indicates if the autocomplete box was manually triggered +-- with the autocomplete.complete() function to prevent the suggestions +-- from getting cluttered with arbitrary document symbols by using the +-- autocomplete.map_manually table. +local triggered_manually = false local mt = { __tostring = function(t) return t.text end } -function autocomplete.add(t) +function autocomplete.add(t, triggered_manually) local items = {} for text, info in pairs(t.items) do - info = (type(info) == "string") and info - table.insert(items, setmetatable({ text = text, info = info }, mt)) + if type(info) == "table" then + table.insert( + items, + setmetatable( + { + 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 + }, + mt + ) + ) + else + info = (type(info) == "string") and info + table.insert(items, setmetatable({ text = text, info = info }, mt)) + end + end + + if not triggered_manually then + autocomplete.map[t.name] = { files = t.files or ".*", items = items } + else + autocomplete.map_manually[t.name] = { files = t.files or ".*", items = items } end - autocomplete.map[t.name] = { files = t.files or ".*", items = items } end -local max_symbols = config.max_symbols or 2000 +-- +-- Thread that scans open document symbols and cache them +-- +local max_symbols = config.max_symbols core.add_thread(function() local cache = setmetatable({}, { __mode = "k" }) @@ -109,16 +149,39 @@ local last_line, last_col local function reset_suggestions() suggestions_idx = 1 suggestions = {} + + triggered_manually = false + + local doc = core.active_view.doc + if autocomplete.on_close then + autocomplete.on_close(doc, suggestions[suggestions_idx]) + autocomplete.on_close = nil + 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 "" + local map = autocomplete.map + + if triggered_manually then + map = autocomplete.map_manually + end + -- get all relevant suggestions for given filename local items = {} - for _, v in pairs(autocomplete.map) do + for _, v in pairs(map) do if common.match_pattern(filename, v.files) then for _, item in pairs(v.items) do table.insert(items, item) @@ -129,7 +192,7 @@ local function update_suggestions() -- fuzzy match, remove duplicates and store items = common.fuzzy_match(items, partial) local j = 1 - for i = 1, config.autocomplete_max_suggestions do + for i = 1, config.plugins.autocomplete.max_suggestions do suggestions[i] = items[j] while items[j] and items[i].text == items[j].text do items[i].info = items[i].info or items[j].info @@ -138,7 +201,6 @@ local function update_suggestions() end end - local function get_partial_symbol() local doc = core.active_view.doc local line2, col2 = doc:get_selection() @@ -146,14 +208,12 @@ local function get_partial_symbol() return doc:get_text(line1, col1, line2, col2) end - local function get_active_view() if getmetatable(core.active_view) == DocView then return core.active_view end end - local function get_suggestions_rect(av) if #suggestions == 0 then return 0, 0, 0, 0 @@ -175,15 +235,67 @@ local function get_suggestions_rect(av) max_width = math.max(max_width, w) end + local ah = config.plugins.autocomplete.max_height + + local max_items = #suggestions + if max_items > ah then + max_items = ah + end + + -- additional line to display total items + max_items = max_items + 1 + + if max_width < 150 then + max_width = 150 + end + return x - style.padding.x, y - style.padding.y, max_width + style.padding.x * 2, - #suggestions * (th + style.padding.y) + style.padding.y + max_items * (th + style.padding.y) + style.padding.y end +local function draw_description_box(text, av, sx, sy, sw, sh) + local width = 0 + + local lines = {} + for line in string.gmatch(text.."\n", "(.-)\n") do + width = math.max(width, style.font:get_width(line)) + table.insert(lines, line) + end + + local height = #lines * style.font:get_height() + + -- draw background rect + renderer.draw_rect( + sx + sw + style.padding.x / 4, + sy, + width + style.padding.x * 2, + height + style.padding.y * 2, + style.background3 + ) + + -- 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 + ) + y = y + lh + end +end local function draw_suggestions_box(av) + if #suggestions <= 0 then + return + end + + local ah = config.plugins.autocomplete.max_height + -- draw background rect local rx, ry, rw, rh = get_suggestions_rect(av) renderer.draw_rect(rx, ry, rw, rh, style.background3) @@ -192,7 +304,14 @@ local function draw_suggestions_box(av) local font = av:get_font() local lh = font:get_height() + style.padding.y local y = ry + style.padding.y / 2 - for i, s in ipairs(suggestions) do + local show_count = #suggestions <= ah and #suggestions or ah + local start_index = suggestions_idx > ah and (suggestions_idx-(ah-1)) or 1 + + for i=start_index, start_index+show_count-1, 1 do + if not suggestions[i] then + break + end + local s = suggestions[i] local color = (i == suggestions_idx) and style.accent or style.text common.draw_text(font, color, s.text, "left", rx + style.padding.x, y, rw, lh) if s.info then @@ -200,26 +319,55 @@ local function draw_suggestions_box(av) common.draw_text(style.font, color, s.info, "right", rx, y, rw - style.padding.x, lh) end y = y + lh + if suggestions_idx == i then + if s.cb then + s.cb(suggestions_idx, s) + s.cb = nil + s.data = nil + end + if s.desc and #s.desc > 0 then + draw_description_box(s.desc, av, rx, ry, rw, rh) + end + end end + + renderer.draw_rect(rx, y, rw, 2, style.caret) + renderer.draw_rect(rx, y+2, rw, lh, style.background) + common.draw_text( + style.font, + style.accent, + "Items", + "left", + rx + style.padding.x, y, rw, lh + ) + common.draw_text( + style.font, + style.accent, + tostring(suggestions_idx) .. "/" .. tostring(#suggestions), + "right", + rx, y, rw - style.padding.x, lh + ) end - --- patch event logic into RootView -local on_text_input = RootView.on_text_input -local update = RootView.update -local draw = RootView.draw - - -RootView.on_text_input = function(...) - on_text_input(...) - +local function show_autocomplete() local av = get_active_view() if av then -- update partial symbol and suggestions partial = get_partial_symbol() - if #partial >= 3 then + + if #partial >= config.plugins.autocomplete.min_len or triggered_manually then update_suggestions() - last_line, last_col = av.doc:get_selection() + + if not triggered_manually then + last_line, last_col = av.doc:get_selection() + else + local line, col = av.doc:get_selection() + local char = av.doc:get_char(line, col-1, line, col-1) + + if char:match("%s") or (char:match("%p") and col ~= last_col) then + reset_suggestions() + end + end else reset_suggestions() end @@ -233,6 +381,30 @@ RootView.on_text_input = function(...) end end +-- +-- Patch event logic into RootView and Doc +-- +local on_text_input = RootView.on_text_input +local on_text_remove = Doc.remove +local update = RootView.update +local draw = RootView.draw + +RootView.on_text_input = function(...) + on_text_input(...) + show_autocomplete() +end + +Doc.remove = function(self, line1, col1, line2, col2) + on_text_remove(self, line1, col1, line2, col2) + + if triggered_manually and line1 == line2 then + if last_col >= col1 then + reset_suggestions() + else + show_autocomplete() + end + end +end RootView.update = function(...) update(...) @@ -241,13 +413,19 @@ RootView.update = function(...) if av then -- reset suggestions if caret was moved local line, col = av.doc:get_selection() - if line ~= last_line or col ~= last_col then - reset_suggestions() + + if not triggered_manually then + if line ~= last_line or col ~= last_col then + reset_suggestions() + end + else + if line ~= last_line or col < last_col then + reset_suggestions() + end end end end - RootView.draw = function(...) draw(...) @@ -258,12 +436,53 @@ RootView.draw = function(...) end end +-- +-- Public functions +-- +function autocomplete.open(on_close) + triggered_manually = true + if on_close then + autocomplete.on_close = on_close + end + + local av = get_active_view() + last_line, last_col = av.doc:get_selection() + update_suggestions() +end + +function autocomplete.close() + reset_suggestions() +end + +function autocomplete.is_open() + return #suggestions > 0 +end + +function autocomplete.complete(completions, on_close) + reset_suggestions() + + autocomplete.map_manually = {} + autocomplete.add(completions, true) + + autocomplete.open(on_close) +end + +function autocomplete.can_complete() + if #partial >= config.plugins.autocomplete.min_len then + return true + end + return false +end + + +-- +-- Commands +-- local function predicate() return get_active_view() and #suggestions > 0 end - command.add(predicate, { ["autocomplete:complete"] = function() local doc = core.active_view.doc @@ -288,7 +507,9 @@ command.add(predicate, { end, }) - +-- +-- Keymaps +-- keymap.add { ["tab"] = "autocomplete:complete", ["up"] = "autocomplete:previous", diff --git a/data/plugins/autoreload.lua b/data/plugins/autoreload.lua index 6b9af8b5..55a2d99e 100644 --- a/data/plugins/autoreload.lua +++ b/data/plugins/autoreload.lua @@ -1,4 +1,4 @@ --- mod-version:1 -- lite-xl 1.16 +-- mod-version:2 -- lite-xl 2.0 local core = require "core" local config = require "core.config" local Doc = require "core.doc" diff --git a/data/plugins/contextmenu.lua b/data/plugins/contextmenu.lua index 4de46080..dc95567f 100644 --- a/data/plugins/contextmenu.lua +++ b/data/plugins/contextmenu.lua @@ -1,223 +1,10 @@ --- mod-version:1 -- lite-xl 1.16 +-- mod-version:2 -- lite-xl 2.0 local core = require "core" -local common = require "core.common" -local config = require "core.config" local command = require "core.command" local keymap = require "core.keymap" -local style = require "core.style" -local Object = require "core.object" +local ContextMenu = require "core.contextmenu" local RootView = require "core.rootview" -local border_width = 1 -local divider_width = 1 -local DIVIDER = {} - -local ContextMenu = Object:extend() - -ContextMenu.DIVIDER = DIVIDER - -function ContextMenu:new() - self.itemset = {} - self.show_context_menu = false - self.selected = -1 - self.height = 0 - self.position = { x = 0, y = 0 } -end - -local function get_item_size(item) - local lw, lh - if item == DIVIDER then - lw = 0 - lh = divider_width - else - lw = style.font:get_width(item.text) - if item.info then - lw = lw + style.padding.x + style.font:get_width(item.info) - end - lh = style.font:get_height() + style.padding.y - end - 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 - item.info = keymap.reverse_map[item.command] - end - local lw, lh = get_item_size(item) - width = math.max(width, lw) - height = height + lh - end - width = width + style.padding.x * 2 - items.width, items.height = width, height - table.insert(self.itemset, { predicate = predicate, items = items }) -end - -function ContextMenu:show(x, y) - self.items = nil - local items_list = { width = 0, height = 0 } - for _, items in ipairs(self.itemset) do - if items.predicate(x, y) then - items_list.width = math.max(items_list.width, items.items.width) - items_list.height = items_list.height + items.items.height - for _, subitems in ipairs(items.items) do - table.insert(items_list, subitems) - end - end - end - - if #items_list > 0 then - self.items = items_list - local w, h = self.items.width, self.items.height - - -- by default the box is opened on the right and below - if x + w >= core.root_view.size.x then - x = x - w - end - if y + h >= core.root_view.size.y then - y = y - h - end - - self.position.x, self.position.y = x, y - self.show_context_menu = true - return true - end - return false -end - -function ContextMenu:hide() - self.show_context_menu = false - self.items = nil - self.selected = -1 - self.height = 0 -end - -function ContextMenu:each_item() - local x, y, w = self.position.x, self.position.y, self.items.width - local oy = y - return coroutine.wrap(function() - for i, item in ipairs(self.items) do - local _, lh = get_item_size(item) - if y - oy > self.height then break end - coroutine.yield(i, item, x, y, w, lh) - y = y + lh - end - end) -end - -function ContextMenu:on_mouse_moved(px, py) - if not self.show_context_menu then return end - - self.selected = -1 - for i, item, x, y, w, h in self:each_item() do - if px > x and px <= x + w and py > y and py <= y + h then - self.selected = i - break - end - end - if self.selected >= 0 then - core.request_cursor("arrow") - end - return true -end - -function ContextMenu:on_selected(item) - if type(item.command) == "string" then - command.perform(item.command) - else - item.command() - end -end - -function ContextMenu:on_mouse_pressed(button, x, y, clicks) - local selected = (self.items or {})[self.selected] - local caught = false - - self:hide() - if button == "left" then - if selected then - self:on_selected(selected) - caught = true - end - end - - if button == "right" then - caught = self:show(x, y) - 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 - -function ContextMenu:update() - if self.show_context_menu then - self:move_towards("height", self.items.height) - end -end - -function ContextMenu:draw() - if not self.show_context_menu then return end - core.root_view:defer_draw(self.draw_context_menu, self) -end - -function ContextMenu:draw_context_menu() - if not self.items then return end - local bx, by, bw, bh = self.position.x, self.position.y, self.items.width, self.height - - renderer.draw_rect( - bx - border_width, - by - border_width, - bw + (border_width * 2), - bh + (border_width * 2), - style.divider - ) - renderer.draw_rect(bx, by, bw, bh, style.background3) - - for i, item, x, y, w, h in self:each_item() do - if item == DIVIDER then - renderer.draw_rect(x, y, w, h, style.caret) - else - if i == self.selected then - renderer.draw_rect(x, y, w, h, style.selection) - end - - common.draw_text(style.font, style.text, item.text, "left", x + style.padding.x, y, w, h) - if item.info then - common.draw_text(style.font, style.dim, item.info, "right", x, y, w - style.padding.x, h) - end - end - end -end - - local menu = ContextMenu() local on_view_mouse_pressed = RootView.on_view_mouse_pressed local on_mouse_moved = RootView.on_mouse_moved @@ -255,15 +42,33 @@ 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 } +}) + if require("plugins.scale") then menu:register("core.docview", { { text = "Font +", command = "scale:increase" }, { text = "Font -", command = "scale:decrease" }, { text = "Font Reset", command = "scale:reset" }, - DIVIDER, + ContextMenu.DIVIDER, { text = "Find", command = "find-replace:find" }, { text = "Replace", command = "find-replace:replace" }, - DIVIDER, + ContextMenu.DIVIDER, { text = "Find Pattern", command = "find-replace:find-pattern" }, { text = "Replace Pattern", command = "find-replace:replace-pattern" }, }) diff --git a/data/plugins/detectindent.lua b/data/plugins/detectindent.lua index 9e7ed93c..45ebaee6 100644 --- a/data/plugins/detectindent.lua +++ b/data/plugins/detectindent.lua @@ -1,4 +1,4 @@ --- mod-version:1 -- lite-xl 1.16 +-- mod-version:2 -- lite-xl 2.0 local core = require "core" local command = require "core.command" local common = require "core.common" diff --git a/data/plugins/language_c.lua b/data/plugins/language_c.lua index b311884b..44c3b895 100644 --- a/data/plugins/language_c.lua +++ b/data/plugins/language_c.lua @@ -1,8 +1,8 @@ --- mod-version:1 -- lite-xl 1.16 +-- mod-version:2 -- lite-xl 2.0 local syntax = require "core.syntax" syntax.add { - files = { "%.c$", "%.h$", "%.inl$", "%.cpp$", "%.hpp$" }, + files = { "%.c$", "%.h$", "%.inl$" }, comment = "//", patterns = { { pattern = "//.-\n", type = "comment" }, @@ -55,6 +55,17 @@ syntax.add { ["true"] = "literal", ["false"] = "literal", ["NULL"] = "literal", + ["#include"] = "keyword", + ["#if"] = "keyword", + ["#ifdef"] = "keyword", + ["#ifndef"] = "keyword", + ["#else"] = "keyword", + ["#elseif"] = "keyword", + ["#endif"] = "keyword", + ["#define"] = "keyword", + ["#warning"] = "keyword", + ["#error"] = "keyword", + ["#pragma"] = "keyword", }, } diff --git a/data/plugins/language_cpp.lua b/data/plugins/language_cpp.lua new file mode 100644 index 00000000..499a09db --- /dev/null +++ b/data/plugins/language_cpp.lua @@ -0,0 +1,122 @@ +-- mod-version:2 -- lite-xl 2.0 +pcall(require, "plugins.language_c") + +local syntax = require "core.syntax" + +syntax.add { + files = { + "%.h$", "%.inl$", "%.cpp$", "%.cc$", "%.C$", "%.cxx$", + "%.c++$", "%.hh$", "%.H$", "%.hxx$", "%.hpp$", "%.h++$" + }, + 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 = "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" }, + }, + symbols = { + ["alignof"] = "keyword", + ["alignas"] = "keyword", + ["private"] = "keyword", + ["protected"] = "keyword", + ["public"] = "keyword", + ["register"] = "keyword", + ["nullptr"] = "keyword", + ["operator"] = "keyword", + ["asm"] = "keyword", + ["catch"] = "keyword", + ["throw"] = "keyword", + ["try"] = "keyword", + ["compl"] = "keyword", + ["explicit"] = "keyword", + ["export"] = "keyword", + ["concept"] = "keyword", + ["consteval"] = "keyword", + ["constexpr"] = "keyword", + ["constinit"] = "keyword", + ["const_cast"] = "keyword", + ["dynamic_cast"] = "keyword", + ["reinterpret_cast"] = "keyword", + ["static_cast"] = "keyword", + ["static_assert"] = "keyword", + ["template"] = "keyword", + ["this"] = "keyword", + ["thread_local"] = "keyword", + ["requires"] = "keyword", + ["co_wait"] = "keyword", + ["co_return"] = "keyword", + ["co_yield"] = "keyword", + ["decltype"] = "keyword", + ["delete"] = "keyword", + ["export"] = "keyword", + ["friend"] = "keyword", + ["typeid"] = "keyword", + ["typename"] = "keyword", + ["mutable"] = "keyword", + ["override"] = "keyword", + ["virtual"] = "keyword", + ["using"] = "keyword", + ["new"] = "keyword", + ["noexcept"] = "keyword", + ["if"] = "keyword", + ["then"] = "keyword", + ["else"] = "keyword", + ["elseif"] = "keyword", + ["do"] = "keyword", + ["while"] = "keyword", + ["for"] = "keyword", + ["break"] = "keyword", + ["continue"] = "keyword", + ["return"] = "keyword", + ["goto"] = "keyword", + ["typedef"] = "keyword", + ["enum"] = "keyword", + ["extern"] = "keyword", + ["static"] = "keyword", + ["volatile"] = "keyword", + ["const"] = "keyword", + ["inline"] = "keyword", + ["switch"] = "keyword", + ["case"] = "keyword", + ["default"] = "keyword", + ["auto"] = "keyword", + ["const"] = "keyword", + ["void"] = "keyword", + ["int"] = "keyword2", + ["short"] = "keyword2", + ["long"] = "keyword2", + ["float"] = "keyword2", + ["double"] = "keyword2", + ["char"] = "keyword2", + ["unsigned"] = "keyword2", + ["bool"] = "keyword2", + ["true"] = "keyword2", + ["false"] = "keyword2", + ["#include"] = "keyword", + ["#if"] = "keyword", + ["#ifdef"] = "keyword", + ["#ifndef"] = "keyword", + ["#else"] = "keyword", + ["#elseif"] = "keyword", + ["#endif"] = "keyword", + ["#define"] = "keyword", + ["#warning"] = "keyword", + ["#error"] = "keyword", + ["#pragma"] = "keyword", + }, +} + diff --git a/data/plugins/language_css.lua b/data/plugins/language_css.lua index 08a256f9..222e2f94 100644 --- a/data/plugins/language_css.lua +++ b/data/plugins/language_css.lua @@ -1,4 +1,4 @@ --- mod-version:1 -- lite-xl 1.16 +-- mod-version:2 -- lite-xl 2.0 local syntax = require "core.syntax" syntax.add { diff --git a/data/plugins/language_html.lua b/data/plugins/language_html.lua index c45b43a3..cebb3f1a 100644 --- a/data/plugins/language_html.lua +++ b/data/plugins/language_html.lua @@ -1,4 +1,4 @@ --- mod-version:1 -- lite-xl 1.16 +-- mod-version:2 -- lite-xl 2.0 local syntax = require "core.syntax" syntax.add { diff --git a/data/plugins/language_js.lua b/data/plugins/language_js.lua index 671e1bd8..7556b00b 100644 --- a/data/plugins/language_js.lua +++ b/data/plugins/language_js.lua @@ -1,4 +1,4 @@ --- mod-version:1 -- lite-xl 1.16 +-- mod-version:2 -- lite-xl 2.0 local syntax = require "core.syntax" syntax.add { diff --git a/data/plugins/language_lua.lua b/data/plugins/language_lua.lua index 5df3d29f..165633b6 100644 --- a/data/plugins/language_lua.lua +++ b/data/plugins/language_lua.lua @@ -1,4 +1,4 @@ --- mod-version:1 -- lite-xl 1.16 +-- mod-version:2 -- lite-xl 2.0 local syntax = require "core.syntax" syntax.add { diff --git a/data/plugins/language_md.lua b/data/plugins/language_md.lua index ab2a7d8b..6e6e4255 100644 --- a/data/plugins/language_md.lua +++ b/data/plugins/language_md.lua @@ -1,4 +1,4 @@ --- mod-version:1 -- lite-xl 1.16 +-- mod-version:2 -- lite-xl 2.0 local syntax = require "core.syntax" syntax.add { diff --git a/data/plugins/language_python.lua b/data/plugins/language_python.lua index 849bafc1..e19caa63 100644 --- a/data/plugins/language_python.lua +++ b/data/plugins/language_python.lua @@ -1,4 +1,4 @@ --- mod-version:1 -- lite-xl 1.16 +-- mod-version:2 -- lite-xl 2.0 local syntax = require "core.syntax" syntax.add { diff --git a/data/plugins/language_xml.lua b/data/plugins/language_xml.lua index d97fa9a8..95e310bb 100644 --- a/data/plugins/language_xml.lua +++ b/data/plugins/language_xml.lua @@ -1,4 +1,4 @@ --- mod-version:1 -- lite-xl 1.16 +-- mod-version:2 -- lite-xl 2.0 local syntax = require "core.syntax" syntax.add { diff --git a/data/plugins/lineguide.lua b/data/plugins/lineguide.lua index 8ef3ee68..61debbff 100644 --- a/data/plugins/lineguide.lua +++ b/data/plugins/lineguide.lua @@ -1,4 +1,4 @@ --- mod-version:1 -- lite-xl 1.16 +-- mod-version:2 -- lite-xl 2.0 local config = require "core.config" local style = require "core.style" local DocView = require "core.docview" diff --git a/data/plugins/macro.lua b/data/plugins/macro.lua index 15d8a75e..2678363a 100644 --- a/data/plugins/macro.lua +++ b/data/plugins/macro.lua @@ -1,4 +1,4 @@ --- mod-version:1 -- lite-xl 1.16 +-- mod-version:2 -- lite-xl 2.0 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 45399ed0..dda3a2d0 100644 --- a/data/plugins/projectsearch.lua +++ b/data/plugins/projectsearch.lua @@ -1,4 +1,4 @@ --- mod-version:1 -- lite-xl 1.16 +-- mod-version:2 -- lite-xl 2.0 local core = require "core" local common = require "core.common" local keymap = require "core.keymap" @@ -9,6 +9,7 @@ local View = require "core.view" local ResultsView = View:extend() +ResultsView.context = "session" function ResultsView:new(text, fn) ResultsView.super.new(self) @@ -170,7 +171,7 @@ function ResultsView:draw() local ox, oy = self:get_content_offset() local x, y = ox + style.padding.x, oy + style.padding.y local files_number = core.project_files_number() - local per = files_number and self.last_file_idx / files_number or 1 + local per = common.clamp(files_number and self.last_file_idx / files_number or 1, 0, 1) local text if self.searching then if files_number then diff --git a/data/plugins/quote.lua b/data/plugins/quote.lua index 85a5874c..c714cbf8 100644 --- a/data/plugins/quote.lua +++ b/data/plugins/quote.lua @@ -1,4 +1,4 @@ --- mod-version:1 -- lite-xl 1.16 +-- mod-version:2 -- lite-xl 2.0 local core = require "core" local command = require "core.command" local keymap = require "core.keymap" diff --git a/data/plugins/reflow.lua b/data/plugins/reflow.lua index f0051c12..cbaa31ef 100644 --- a/data/plugins/reflow.lua +++ b/data/plugins/reflow.lua @@ -1,4 +1,4 @@ --- mod-version:1 -- lite-xl 1.16 +-- mod-version:2 -- lite-xl 2.0 local core = require "core" local config = require "core.config" local command = require "core.command" diff --git a/data/plugins/scale.lua b/data/plugins/scale.lua index a5f5aaee..8d16304b 100644 --- a/data/plugins/scale.lua +++ b/data/plugins/scale.lua @@ -1,4 +1,4 @@ --- mod-version:1 -- lite-xl 1.16 +-- mod-version:2 -- lite-xl 2.0 local core = require "core" local common = require "core.common" local command = require "core.command" @@ -8,8 +8,12 @@ local style = require "core.style" local RootView = require "core.rootview" local CommandView = require "core.commandview" -config.scale_mode = "code" -config.scale_use_mousewheel = true +config.plugins.scale = { + mode = "code", + use_mousewheel = true +} + +local MINIMUM_SCALE = 0.25; local scale_level = 0 local scale_steps = 0.05 @@ -35,7 +39,7 @@ local function set_scale(scale) -- we set scale_level in case this was called by user scale_level = (scale - default_scale) / scale_steps - if config.scale_mode == "ui" then + if config.plugins.scale.mode == "ui" then SCALE = scale style.padding.x = style.padding.x * s @@ -68,7 +72,7 @@ end local on_mouse_wheel = RootView.on_mouse_wheel function RootView:on_mouse_wheel(d, ...) - if keymap.modkeys["ctrl"] and config.scale_use_mousewheel then + if keymap.modkeys["ctrl"] and config.plugins.scale.use_mousewheel then if d < 0 then command.perform "scale:decrease" end if d > 0 then command.perform "scale:increase" end else @@ -77,18 +81,18 @@ function RootView:on_mouse_wheel(d, ...) end local function res_scale() - scale_level = 0 - set_scale(default_scale) + scale_level = 0 + set_scale(default_scale) end local function inc_scale() - scale_level = scale_level + 1 - set_scale(default_scale + scale_level * scale_steps) + scale_level = scale_level + 1 + set_scale(default_scale + scale_level * scale_steps) end -local function dec_scale() - scale_level = scale_level - 1 - set_scale(default_scale + scale_level * scale_steps) +local function dec_scale() + scale_level = scale_level - 1 + set_scale(math.max(default_scale + scale_level * scale_steps), MINIMUM_SCALE) end diff --git a/data/plugins/tabularize.lua b/data/plugins/tabularize.lua index 2fa06d69..4cdae6ea 100644 --- a/data/plugins/tabularize.lua +++ b/data/plugins/tabularize.lua @@ -1,4 +1,4 @@ --- mod-version:1 -- lite-xl 1.16 +-- mod-version:2 -- lite-xl 2.0 local core = require "core" local command = require "core.command" local translate = require "core.doc.translate" diff --git a/data/plugins/toolbarview.lua b/data/plugins/toolbarview.lua index 93102df2..bfd71138 100644 --- a/data/plugins/toolbarview.lua +++ b/data/plugins/toolbarview.lua @@ -1,4 +1,4 @@ --- mod-version:1 -- lite-xl 1.16 +-- mod-version:2 -- lite-xl 2.0 local core = require "core" local common = require "core.common" local command = require "core.command" diff --git a/data/plugins/treeview.lua b/data/plugins/treeview.lua index d2392fc8..3a84b8fc 100644 --- a/data/plugins/treeview.lua +++ b/data/plugins/treeview.lua @@ -1,4 +1,4 @@ --- mod-version:1 -- lite-xl 1.16 +-- mod-version:2 -- lite-xl 2.0 local core = require "core" local common = require "core.common" local command = require "core.command" @@ -6,9 +6,12 @@ local config = require "core.config" local keymap = require "core.keymap" local style = require "core.style" local View = require "core.view" +local ContextMenu = require "core.contextmenu" +local RootView = require "core.rootview" + local default_treeview_size = 200 * SCALE -local tooltip_offset = style.font:get_height("A") +local tooltip_offset = style.font:get_height() local tooltip_border = 1 local tooltip_delay = 0.5 local tooltip_alpha = 255 @@ -168,13 +171,13 @@ end function TreeView:on_mouse_moved(px, py, ...) TreeView.super.on_mouse_moved(self, px, py, ...) if self.dragging_scrollbar then return end - + local item_changed, tooltip_changed 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 item_changed = true self.hovered_item = item - + x,y,w,h = self:get_text_bounding_box(item, x,y,w,h) if px > x and py > y and px <= x + w and py <= y + h then tooltip_changed = true @@ -204,7 +207,7 @@ end function TreeView:on_mouse_pressed(button, x, y, clicks) local caught = TreeView.super.on_mouse_pressed(self, button, x, y, clicks) - if caught then + if caught or button ~= "left" then return end local hovered_item = self.hovered_item @@ -239,7 +242,7 @@ function TreeView:update() else self:move_towards(self.size, "x", dest) 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) @@ -336,9 +339,10 @@ local treeview_node = node:split("left", view, {x = true}, true) -- plugin to be independent of each other. In addition it is not the -- 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") -if config.toolbarview ~= false and toolbar_plugin then - local toolbar_view = ToolbarView() +if config.plugins.toolbarview ~= false and toolbar_plugin then + toolbar_view = ToolbarView() treeview_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)) @@ -349,12 +353,182 @@ if config.toolbarview ~= false and toolbar_plugin then }) end +-- Add a context menu to the treeview +local menu = ContextMenu() --- register commands and keymap +local on_view_mouse_pressed = RootView.on_view_mouse_pressed +local on_mouse_moved = RootView.on_mouse_moved +local root_view_update = RootView.update +local root_view_draw = RootView.draw + +function RootView:on_mouse_moved(...) + if menu:on_mouse_moved(...) then return end + on_mouse_moved(self, ...) +end + +function RootView.on_view_mouse_pressed(button, x, y, clicks) + -- We give the priority to the menu to process mouse pressed events. + if button == "right" then + view.tooltip.alpha = 0 + view.tooltip.x, view.tooltip.y = nil, nil + end + local handled = menu:on_mouse_pressed(button, x, y, clicks) + return handled or on_view_mouse_pressed(button, x, y, clicks) +end + +function RootView:update(...) + root_view_update(self, ...) + menu:update() +end + +function RootView:draw(...) + root_view_draw(self, ...) + menu:draw() +end + +local function is_project_folder(path) + return common.basename(core.project_dir) == path +end + +menu:register(function() return view.hovered_item end, { + { text = "Open in System", command = "treeview:open-in-system" }, + ContextMenu.DIVIDER +}) + +menu:register( + function() + return view.hovered_item + and not is_project_folder(view.hovered_item.filename) + end, + { + { text = "Rename", command = "treeview:rename" }, + { text = "Delete", command = "treeview:delete" }, + } +) + +menu:register( + function() + return view.hovered_item and view.hovered_item.type == "dir" + end, + { + { text = "New File", command = "treeview:new-file" }, + { text = "New Folder", command = "treeview:new-folder" }, + } +) + +-- Register the TreeView commands and keymap command.add(nil, { ["treeview:toggle"] = function() view.visible = not view.visible + 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 + 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, common.path_suggest) + end, + + ["treeview:new-file"] = function() + local dir_name = view.hovered_item.filename + if not is_project_folder(dir_name) then + core.command_view:set_text(dir_name .. "/") + 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, + + ["treeview:new-folder"] = function() + local dir_name = view.hovered_item.filename + if not is_project_folder(dir_name) then + core.command_view:set_text(dir_name .. "/") + 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 + local file_info = system.get_file_info(filename) + local file_type = file_info.type == "dir" and "Directory" or "File" + -- Ask before deleting + local opt = { + { font = style.font, text = "Yes", default_yes = true }, + { font = style.font, text = "No" , default_no = true } + } + core.nag_view:show( + string.format("Delete %s", file_type), + string.format( + "Are you sure you want to delete the %s?\n%s: %s", + file_type:lower(), file_type, relfilename + ), + opt, + function(item) + if item.text == "Yes" then + if file_info.type == "dir" then + local deleted, error, path = common.rm(filename, true) + if not deleted then + core.error("Error: %s - \"%s\" ", error, path) + return + end + else + local removed, error = os.remove(filename) + if not removed then + core.error("Error: %s - \"%s\"", error, filename) + return + end + end + core.log("Deleted \"%s\"", filename) + end + 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)) + end end, }) keymap.add { ["ctrl+\\"] = "treeview:toggle" } + +-- Return the treeview with toolbar and contextmenu to allow +-- user or plugin modifications +view.toolbar = toolbar_view +view.contextmenu = menu + +return view diff --git a/data/plugins/trimwhitespace.lua b/data/plugins/trimwhitespace.lua index a6d3d140..79886c67 100644 --- a/data/plugins/trimwhitespace.lua +++ b/data/plugins/trimwhitespace.lua @@ -1,4 +1,4 @@ --- mod-version:1 -- lite-xl 1.16 +-- mod-version:2 -- lite-xl 2.0 local core = require "core" local command = require "core.command" local Doc = require "core.doc" diff --git a/data/plugins/workspace.lua b/data/plugins/workspace.lua index 9c1e20c8..1edfbe1e 100644 --- a/data/plugins/workspace.lua +++ b/data/plugins/workspace.lua @@ -1,4 +1,4 @@ --- mod-version:1 -- lite-xl 1.16 +-- mod-version:2 -- lite-xl 2.0 local core = require "core" local common = require "core.common" local DocView = require "core.docview" diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..7191e3f5 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,28 @@ +# Interface Files + +This directory holds the documentation for the Lua C API that +is hidden in the C source files of Lite. The idea of these files +is to serve you as a quick reference about the functionality +that is not written in Lua it self. Please note that they +don't have any real code, just metadata or annotations. + +Also, these interfaces are using +[EmmyLua annotation syntax](https://emmylua.github.io/annotation.html) +which is supported by LSP servers like the +[Sumneko Lua LSP](https://github.com/sumneko/lua-language-server). +This means that you can get nice code autocompletion and descriptions +of Lite core libraries and symbols when developing plugins or adding +any options to your **User Module File** (init.lua). + +## The Base Core + +Most of the code that is written in Lua for Lite is powered by the exposed +C API in the four namespaces that follow: + +* [system](api/system.lua) +* [renderer](api/renderer.lua) +* [regex](api/regex.lua) +* [process](api/process.lua) + +Finally, all global variables are documented in the file named +[globals.lua](api/globals.lua). diff --git a/docs/api/globals.lua b/docs/api/globals.lua new file mode 100644 index 00000000..98fe61b1 --- /dev/null +++ b/docs/api/globals.lua @@ -0,0 +1,21 @@ +---@meta + +---The command line arguments given to lite. +---@type table +ARGS = {} + +---The current operating system. +---@type string | "'Windows'" | "'Mac OS X'" | "'Linux'" | "'iOS'" | "'Android'" +PLATFORM = "Operating System" + +---The current text or ui scale. +---@type number +SCALE = 1.0 + +---Full path of lite executable. +---@type string +EXEFILE = "/path/to/lite" + +---Path to the users home directory. +---@type string +HOME = "/path/to/user/dir" diff --git a/docs/api/process.lua b/docs/api/process.lua new file mode 100644 index 00000000..abba67ae --- /dev/null +++ b/docs/api/process.lua @@ -0,0 +1,232 @@ +---@meta + +--- +---Functionality that allows you to launch subprocesses and read +---or write to them in a non-blocking fashion. +---@class process +process = {} + +---Error triggered when the stdout, stderr or stdin fails while reading +---or writing, its value is platform dependent, so the value declared on this +---interface does not represents the real one. +---@type integer +process.ERROR_PIPE = -1 + +---Error triggered when a read or write action is blocking, +---its value is platform dependent, so the value declared on this +---interface does not represents the real one. +---@type integer +process.ERROR_WOULDBLOCK = -2 + +---Error triggered when a process takes more time than that specified +---by the deadline parameter given on process:start(), +---its value is platform dependent, so the value declared on this +---interface does not represents the real one. +---@type integer +process.ERROR_TIMEDOUT = -3 + +---Error triggered when trying to terminate or kill a non running process, +---its value is platform dependent, so the value declared on this +---interface does not represents the real one. +---@type integer +process.ERROR_INVAL = -4 + +---Error triggered when no memory is available to allocate the process, +---its value is platform dependent, so the value declared on this +---interface does not represents the real one. +---@type integer +process.ERROR_NOMEM = -5 + +---Used for the process:close_stream() method to close stdin. +---@type integer +process.STREAM_STDIN = 0 + +---Used for the process:close_stream() method to close stdout. +---@type integer +process.STREAM_STDOUT = 1 + +---Used for the process:close_stream() method to close stderr. +---@type integer +process.STREAM_STDERR = 2 + +---Instruct process:wait() to wait until the process ends. +---@type integer +process.WAIT_INFINITE = -1 + +---Instruct process:wait() to wait until the deadline given on process:start() +---@type integer +process.WAIT_DEADLINE = -2 + +---Used for the process.options stdin, stdout and stderr fields. +---@type integer +process.REDIRECT_DEFAULT = 0 + +---Used for the process.options stdin, stdout and stderr fields. +---@type integer +process.REDIRECT_PIPE = 1 + +---Used for the process.options stdin, stdout and stderr fields. +---@type integer +process.REDIRECT_PARENT = 2 + +---Used for the process.options stdin, stdout and stderr fields. +---@type integer +process.REDIRECT_DISCARD = 3 + +---Used for the process.options stdin, stdout and stderr fields. +---@type integer +process.REDIRECT_STDOUT = 4 + +---@alias process.errortype +---|>'process.ERROR_PIPE' +---| 'process.ERROR_WOULDBLOCK' +---| 'process.ERROR_TIMEDOUT' +---| 'process.ERROR_INVAL' +---| 'process.ERROR_NOMEM' + +---@alias process.streamtype +---|>'process.STREAM_STDIN' +---| 'process.STREAM_STDOUT' +---| 'process.STREAM_STDERR' + +---@alias process.waittype +---|>'process.WAIT_INFINITE' +---| 'process.WAIT_DEADLINE' + +---@alias process.redirecttype +---|>'process.REDIRECT_DEFAULT' +---| 'process.REDIRECT_PIPE' +---| 'process.REDIRECT_PARENT' +---| 'process.REDIRECT_DISCARD' +---| 'process.REDIRECT_STDOUT' + +--- +--- Options that can be passed to process.start() +---@class process.options +---@field public timeout number +---@field public cwd string +---@field public stdin process.redirecttype +---@field public stdout process.redirecttype +---@field public stderr process.redirecttype +---@field public env table +process.options = {} + +--- +---Create and start a new process +--- +---@param command_and_params table First index is the command to execute +---and subsequente elements are parameters for the command. +---@param options process.options +--- +---@return process | nil +---@return string errmsg +---@return process.errortype | integer errcode +function process:start(command_and_params, options) end + +--- +---Translates an error code into a useful text message +--- +---@param code integer +--- +---@return string | nil +function process.strerror(code) end + +--- +---Get the process id. +--- +---@return integer id Process id or 0 if not running. +function process:pid() end + +--- +---Read from the given stream type, if the process fails with a ERROR_PIPE it is +---automatically destroyed returning nil along error message and code. +--- +---@param stream process.streamtype +---@param len? integer Amount of bytes to read, defaults to 2048. +--- +---@return string | nil +---@return string errmsg +---@return process.errortype | integer errcode +function process:read(stream, len) end + +--- +---Read from stdout, if the process fails with a ERROR_PIPE it is +---automatically destroyed returning nil along error message and code. +--- +---@param len? integer Amount of bytes to read, defaults to 2048. +--- +---@return string | nil +---@return string errmsg +---@return process.errortype | integer errcode +function process:read_stdout(len) end + +--- +---Read from stderr, if the process fails with a ERROR_PIPE it is +---automatically destroyed returning nil along error message and code. +--- +---@param len? integer Amount of bytes to read, defaults to 2048. +--- +---@return string | nil +---@return string errmsg +---@return process.errortype | integer errcode +function process:read_stderr(len) end + +--- +---Write to the stdin, if the process fails with a ERROR_PIPE it is +---automatically destroyed returning nil along error message and code. +--- +---@param data string +--- +---@return integer | nil bytes The amount of bytes written or nil if error +---@return string errmsg +---@return process.errortype | integer errcode +function process:write(data) end + +--- +---Allows you to close a stream pipe that you will not be using. +--- +---@param stream process.streamtype +--- +---@return integer | nil +---@return string errmsg +---@return process.errortype | integer errcode +function process:close_stream(stream) end + +--- +---Wait the specified amount of time for the process to exit. +--- +---@param timeout integer | process.waittype Time to wait in milliseconds, +---if 0, the function will only check if process is running without waiting. +--- +---@return integer | nil exit_status The process exit status or nil on error +---@return string errmsg +---@return process.errortype | integer errcode +function process:wait(timeout) end + +--- +---Sends SIGTERM to the process +--- +---@return boolean | nil +---@return string errmsg +---@return process.errortype | integer errcode +function process:terminate() end + +--- +---Sends SIGKILL to the process +--- +---@return boolean | nil +---@return string errmsg +---@return process.errortype | integer errcode +function process:kill() end + +--- +---Get the exit code of the process or nil if still running. +--- +---@return number | nil +function process:returncode() end + +--- +---Check if the process is running +--- +---@return boolean +function process:running() end diff --git a/docs/api/regex.lua b/docs/api/regex.lua new file mode 100644 index 00000000..02d8c796 --- /dev/null +++ b/docs/api/regex.lua @@ -0,0 +1,57 @@ +---@meta + +--- +---Provides the base functionality for regular expressions matching. +---@class regex +regex = {} + +---Instruct regex:cmatch() to match only at the first position. +---@type integer +regex.ANCHORED = 0x80000000 + +---Tell regex:cmatch() that the pattern can match only at end of subject. +---@type integer +regex.ENDANCHORED = 0x20000000 + +---Tell regex:cmatch() that subject string is not the beginning of a line. +---@type integer +regex.NOTBOL = 0x00000001 + +---Tell regex:cmatch() that subject string is not the end of a line. +---@type integer +regex.NOTEOL = 0x00000002 + +---Tell regex:cmatch() that an empty string is not a valid match. +---@type integer +regex.NOTEMPTY = 0x00000004 + +---Tell regex:cmatch() that an empty string at the start of the +---subject is not a valid match. +---@type integer +regex.NOTEMPTY_ATSTART = 0x00000008 + +---@alias regex.modifiers +---|>'"i"' # Case insesitive matching +---| '"m"' # Multiline matching +---| '"s"' # Match all characters with dot (.) metacharacter even new lines + +--- +---Compiles a regular expression pattern that can be used to search in strings. +--- +---@param pattern string +---@param options? regex.modifiers A string of one or more pattern modifiers. +--- +---@return regex|string regex Ready to use regular expression object or error +---message if compiling the pattern failed. +function regex.compile(pattern, options) end + +--- +---Search a string for valid matches and returns a list of matching offsets. +--- +---@param subject string The string to search for valid matches. +---@param offset? integer The position on the subject to start searching. +---@param options? integer A bit field of matching options, eg: +---regex.NOTBOL | regex.NOTEMPTY +--- +---@return table list List of offsets where a match was found. +function regex:cmatch(subject, offset, options) end diff --git a/docs/api/renderer.lua b/docs/api/renderer.lua new file mode 100644 index 00000000..bb622131 --- /dev/null +++ b/docs/api/renderer.lua @@ -0,0 +1,181 @@ +---@meta + +--- +---Core functionality to render or draw elements into the screen. +---@class renderer +renderer = {} + +--- +---Represents a color used by the rendering functions. +---@class renderer.color +---@field public r number Red +---@field public g number Green +---@field public b number Blue +---@field public a number Alpha +renderer.color = {} + +--- +---Represent options that affect a font's rendering. +---@class renderer.fontoptions +---@field public antialiasing "'grayscale'" | "'subpixel'" +---@field public hinting "'slight'" | "'none'" | '"full"' +renderer.fontoptions = {} + +--- +---@class renderer.font +renderer.font = {} + +--- +---Create a new font object. +--- +---@param path string +---@param size number +---@param options renderer.fontoptions +--- +---@return renderer.font +function renderer.font.load(path, size, options) end + +--- +---Clones a font object into a new one. +--- +---@param size? number Optional new size for cloned font. +--- +---@return renderer.font +function renderer.font:copy(size) end + +--- +---Set the amount of characters that represent a tab. +--- +---@param chars integer Also known as tab width. +function renderer.font:set_tab_size(chars) end + +--- +---Get the width in pixels of the given text when +---rendered with this font. +--- +---@param text string +--- +---@return number +function renderer.font:get_width(text) end + +--- +---Get the width in subpixels of the given text when +---rendered with this font. +--- +---@param text string +--- +---@return number +function renderer.font:get_width_subpixel(text) end + +--- +---Get the height in pixels that occupies a single character +---when rendered with this font. +--- +---@return number +function renderer.font:get_height() end + +--- +---Gets the font subpixel scale. +--- +---@return number +function renderer.font:subpixel_scale() end + +--- +---Get the current size of the font. +--- +---@return number +function renderer.font:get_size() end + +--- +---Set a new size for the font. +--- +---@param size number +function renderer.font:set_size(size) end + +--- +---Assistive functionality to replace characters in a +---rendered text with other characters. +---@class renderer.replacements +renderer.replacements = {} + +--- +---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 + +--- +---Toggles drawing debugging rectangles on the currently rendered sections +---of the window to help troubleshoot the renderer. +--- +---@param enable boolean +function renderer.show_debug(enable) end + +--- +---Get the size of the screen area been rendered. +--- +---@return number width +---@return number height +function renderer.get_size() end + +--- +---Tell the rendering system that we want to build a new frame to render. +function renderer.begin_frame() end + +--- +---Tell the rendering system that we finished building the frame. +function renderer.end_frame() end + +--- +---Set the region of the screen where draw operations will take effect. +--- +---@param x number +---@param y number +---@param width number +---@param height number +function renderer.set_clip_rect(x, y, width, height) end + +--- +---Draw a rectangle. +--- +---@param x number +---@param y number +---@param width number +---@param height number +---@param color renderer.color +function renderer.draw_rect(x, y, width, height, color) end + +--- +---Draw text. +--- +---@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 diff --git a/docs/api/system.lua b/docs/api/system.lua new file mode 100644 index 00000000..a655099b --- /dev/null +++ b/docs/api/system.lua @@ -0,0 +1,234 @@ +---@meta + +--- +---Utilites for managing current window, files and more. +---@class system +system = {} + +---@alias system.fileinfotype +---|>'"file"' # It is a file. +---| '"dir"' # It is a directory. + +--- +---@class system.fileinfo +---@field public modified number A timestamp in seconds. +---@field public size number Size in bytes. +---@field public type system.fileinfotype Type of file +system.fileinfo = {} + +--- +---Core function used to retrieve the current event been triggered by SDL. +--- +---The following is a list of event types emitted by this function and +---the arguments for each of them if applicable. +--- +---Window events: +--- * "quit" +--- * "resized" -> width, height +--- * "exposed" +--- * "minimized" +--- * "maximized" +--- * "restored" +--- * "focuslost" +--- +---File events: +--- * "filedropped" -> filename, x, y +--- +---Keyboard events: +--- * "keypressed" -> key_name +--- * "keyreleased" -> key_name +--- * "textinput" -> text +--- +---Mouse events: +--- * "mousepressed" -> button_name, x, y, amount_of_clicks +--- * "mousereleased" -> button_name, x, y +--- * "mousemoved" -> x, y, relative_x, relative_y +--- * "mousewheel" -> y +--- +---@return string type +---@return any? arg1 +---@return any? arg2 +---@return any? arg3 +---@return any? arg4 +function system.poll_event() end + +--- +---Wait until an event is triggered. +--- +---@param timeout number Amount of seconds, also supports fractions +---of a second, eg: 0.01 +--- +---@return boolean status True on success or false if there was an error. +function system.wait_event(timeout) end + +--- +---Change the cursor type displayed on screen. +--- +---@param type string | "'arrow'" | "'ibeam'" | "'sizeh'" | "'sizev'" | "'hand'" +function system.set_cursor(type) end + +--- +---Change the window title. +--- +---@param title string +function system.set_window_title(title) end + +---@alias system.windowmode +---|>'"normal"' +---| '"minimized"' +---| '"maximized"' +---| '"fullscreen"' + +--- +---Change the window mode. +--- +---@param mode system.windowmode +function system.set_window_mode(mode) end + +--- +---Retrieve the current window mode. +--- +---@return system.windowmode mode +function system.get_window_mode() end + +--- +---Toggle between bordered and borderless. +--- +---@param bordered boolean +function system.set_window_bordered(bordered) end + +--- +---When then window is run borderless (without system decorations), this +---function allows to set the size of the different regions that allow +---for custom window management. +--- +---@param title_height number +---@param controls_width number This is for minimize, maximize, close, etc... +---@param resize_border number The amount of pixels reserved for resizing +function system.set_window_hit_test(title_height, controls_width, resize_border) end + +--- +---Get the size and coordinates of the window. +--- +---@return number width +---@return number height +---@return number x +---@return number y +function system.get_window_size() end + +--- +---Sets the size and coordinates of the window. +--- +---@param width number +---@param height number +---@param x number +---@param y number +function system.set_window_size(width, height, x, y) end + +--- +---Check if the window currently has focus. +--- +---@return boolean +function system.window_has_focus() end + +--- +---Opens a message box to display an error message. +--- +---@param title string +---@param message string +function system.show_fatal_error(title, message) end + +--- +---Change the current directory path which affects relative file operations. +---This function raises an error if the path doesn't exists. +--- +---@param path string +function system.chdir(path) end + +--- +---Create a new directory, note that this function doesn't recursively +---creates the directories on the given path. +--- +---@param directory_path string +--- +---@return boolean created True on success or false on failure. +function system.mkdir(directory_path) end + +--- +---Gets a list of files and directories for a given path. +--- +---@param path string +--- +---@return table|nil list List of directories or nil if empty or error. +---@return string? message Error message in case of error. +function system.list_dir(path) end + +--- +---Converts a relative path from current directory to the absolute one. +--- +---@param path string +--- +---@return string +function system.absolute_path(path) end + +--- +---Get details about a given file or path. +--- +---@param path string Can be a file or a directory path +--- +---@return system.fileinfo|nil info Path details or nil if empty or error. +---@return string? message Error message in case of error. +function system.get_file_info(path) end + +--- +---Retrieve the text currently stored on the clipboard. +--- +---@return string +function system.get_clipboard() end + +--- +---Set the content of the clipboard. +--- +---@param text string +function system.set_clipboard(text) end + +--- +---Get amount of iterations since the application was launched +---also known as SDL_GetPerformanceCounter() / SDL_GetPerformanceFrequency() +--- +---@return number +function system.get_time() end + +--- +---Sleep for the given amount of seconds. +--- +---@param seconds number Also supports fractions of a second, eg: 0.01 +function system.sleep(seconds) end + +--- +---Similar to os.execute() but does not return the exit status of the +---executed command and executes the process in a non blocking way by +---forking it to the background. +--- +---@param command string The command to execute. +function system.exec(command) end + +--- +---Generates a matching score depending on how well the value of the +---given needle compares to that of the value in the haystack. +--- +---@param haystack string +---@param needle string +---@param file boolean Reverse the algorithm to prioritize the end +---of the haystack, eg: with a haystack "/my/path/to/file" and a needle +---"file", will get better score than with this option not set to true. +--- +---@return integer score +function system.fuzzy_match(haystack, needle, file) end + +--- +---Change the opacity (also known as transparency) of the window. +--- +---@param opacity number A value from 0.0 to 1.0, the lower the value +---the less visible the window will be. +function system.set_window_opacity(opacity) end diff --git a/lib/font_renderer/build.sh b/lib/font_renderer/build.sh deleted file mode 100755 index 364c4b6a..00000000 --- a/lib/font_renderer/build.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/bash - -cxxcompiler="g++" -cxxflags="-Wall -O3 -g -std=c++03 -fno-exceptions -fno-rtti -Isrc -Ilib/font_renderer" -cxxflags+=" -DFONT_RENDERER_HEIGHT_HACK" -for package in libagg freetype2; do - cxxflags+=" $(pkg-config --cflags $package)" -done - -echo "compiling font renderer library..." - -for f in `find lib -name "*.cpp"`; do - $cxxcompiler -c $cxxflags $f -o "${f//\//_}.o" - if [[ $? -ne 0 ]]; then - got_error=true - fi -done - -if [[ $got_error ]]; then - rm -f *.o - exit 1 -fi - -ar -rcs libfontrenderer.a *.o - -rm *.o -echo "font renderer library created" diff --git a/lib/font_renderer/font_renderer.cpp b/lib/font_renderer/font_renderer.cpp index 8026a89d..14110107 100644 --- a/lib/font_renderer/font_renderer.cpp +++ b/lib/font_renderer/font_renderer.cpp @@ -245,7 +245,7 @@ FR_Bitmap *FR_Bake_Font_Bitmap(FR_Renderer *font_renderer, int font_height, } const int glyph_avg_width = glyph_count > 0 ? x_size_sum / (glyph_count * subpixel_scale) : font_height; - const int pixels_width = glyph_avg_width * 28; + const int pixels_width = glyph_avg_width > 0 ? glyph_avg_width * 28 : 28; // dry run simulating pixel position to estimate required image's height int x = x_start, y = 0, y_bottom = y; diff --git a/lib/font_renderer/meson.build b/lib/font_renderer/meson.build index 7724d584..d596e152 100644 --- a/lib/font_renderer/meson.build +++ b/lib/font_renderer/meson.build @@ -1,10 +1,6 @@ freetype_dep = dependency('freetype2') -libagg_dep = dependency('libagg', required: false) -if not libagg_dep.found() - libagg_subproject = subproject('libagg') - libagg_dep = libagg_subproject.get_variable('libagg_dep') -endif +libagg_dep = dependency('libagg', fallback: ['libagg', 'libagg_dep']) font_renderer_sources = [ 'agg_font_freetype.cpp', diff --git a/meson.build b/meson.build index 8100cc87..209677ba 100644 --- a/meson.build +++ b/meson.build @@ -1,68 +1,128 @@ -project('lite-xl', 'c', 'cpp', default_options : ['c_std=gnu11', 'cpp_std=c++03']) +project('lite-xl', + ['c', 'cpp'], + version : '2.0.2', + license : 'MIT', + meson_version : '>= 0.54', + default_options : ['c_std=gnu11', 'cpp_std=c++03'] +) +#=============================================================================== +# 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()) + +#=============================================================================== +# Compiler Settings +#=============================================================================== if host_machine.system() == 'darwin' add_languages('objc') endif cc = meson.get_compiler('c') -libm = cc.find_library('m', required : false) -libdl = cc.find_library('dl', required : false) -libx11 = dependency('x11', required : false) -lua_dep = dependency('lua5.2', required : false) -pcre2_dep = dependency('libpcre2-8') -sdl_dep = dependency('sdl2', method: 'config-tool') -threads_dep = dependency('threads') - -if not lua_dep.found() - lua_subproject = subproject('lua', default_options: ['shared=false', 'use_readline=false', 'app=false']) - lua_dep = lua_subproject.get_variable('lua_dep') -endif - -reproc_subproject = subproject('reproc', default_options: ['default_library=static', 'multithreaded=false', 'reproc-cpp=false', 'examples=false']) -reproc_dep = reproc_subproject.get_variable('reproc_dep') - -lite_deps = [lua_dep, sdl_dep, reproc_dep, pcre2_dep, libm, libdl, threads_dep, libx11] - -if host_machine.system() == 'windows' - # Note that we need to explicitly add the windows socket DLL because - # the pkg-config file from reproc does not include it. - lite_deps += meson.get_compiler('cpp').find_library('ws2_32', required: true) -endif lite_cargs = [] -if get_option('portable') - lite_docdir = 'doc' - lite_datadir = 'data' -else - lite_docdir = 'share/doc/lite-xl' - lite_datadir = 'share/lite-xl' -endif - -lite_include = include_directories('src') -foreach data_module : ['core', 'fonts', 'plugins', 'colors'] - install_subdir('data' / data_module , install_dir : lite_datadir) -endforeach - -install_data('licenses/licenses.md', install_dir : lite_docdir) - -lite_link_args = [] -if cc.get_id() == 'gcc' and get_option('buildtype') == 'release' - lite_link_args += ['-static-libgcc', '-static-libstdc++'] -endif -if host_machine.system() == 'darwin' - lite_link_args += ['-framework', 'CoreServices', '-framework', 'Foundation'] -endif - -lite_rc = [] -if host_machine.system() == 'windows' - windows = import('windows') - lite_rc += windows.compile_resources('resources/icons/icon.rc') -endif - # 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' endif +#=============================================================================== +# Linker Settings +#=============================================================================== +lite_link_args = [] +if cc.get_id() == 'gcc' and get_option('buildtype') == 'release' + lite_link_args += ['-static-libgcc', '-static-libstdc++'] +endif -subdir('lib/font_renderer') -subdir('src') +if host_machine.system() == 'darwin' + lite_link_args += ['-framework', 'CoreServices', '-framework', 'Foundation'] +endif +#=============================================================================== +# Dependencies +#=============================================================================== +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'] + ) + pcre2_dep = dependency('libpcre2-8') + sdl_dep = dependency('sdl2', method: 'config-tool') + reproc_dep = dependency('reproc', fallback: ['reproc', 'reproc_dep'], + default_options: [ + 'default_library=static', 'multithreaded=false', + 'reproc-cpp=false', 'examples=false' + ] + ) + + lite_deps = [lua_dep, sdl_dep, reproc_dep, pcre2_dep, libm, libdl, threads_dep] + + if host_machine.system() == 'windows' + # Note that we need to explicitly add the windows socket DLL because + # the pkg-config file from reproc does not include it. + lite_deps += meson.get_compiler('cpp').find_library('ws2_32', required: true) + endif +endif +#=============================================================================== +# Install Configuration +#=============================================================================== +if get_option('portable') or host_machine.system() == 'windows' + lite_bindir = '/' + lite_docdir = '/doc' + lite_datadir = '/data' +elif get_option('bundle') and host_machine.system() == 'darwin' + lite_cargs += '-DMACOS_USE_BUNDLE' + lite_bindir = 'Contents/MacOS' + lite_docdir = 'Contents/Resources' + lite_datadir = 'Contents/Resources' + install_data('resources/icons/icon.icns', install_dir : 'Contents/Resources') + configure_file( + input : 'resources/macos/Info.plist.in', + output : 'Info.plist', + configuration : conf_data, + install : true, + install_dir : 'Contents' + ) +else + lite_bindir = 'bin' + lite_docdir = 'share/doc/lite-xl' + lite_datadir = 'share/lite-xl' + if host_machine.system() == 'linux' + install_data('resources/icons/lite-xl.svg', + install_dir : 'share/icons/hicolor/scalable/apps' + ) + install_data('resources/linux/org.lite_xl.lite_xl.desktop', + install_dir : 'share/applications' + ) + install_data('resources/linux/org.lite_xl.lite_xl.appdata.xml', + install_dir : 'share/metainfo' + ) + endif +endif + +install_data('licenses/licenses.md', install_dir : lite_docdir) + +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) +endforeach + +configure_file( + input : 'data/core/start.lua', + output : 'start.lua', + configuration : conf_data, + install : true, + install_dir : lite_datadir / 'core', +) + +#=============================================================================== +# Targets +#=============================================================================== +if not get_option('source-only') + subdir('lib/font_renderer') + subdir('src') + subdir('scripts') +endif diff --git a/meson_options.txt b/meson_options.txt index 73a542f2..1cf3e22f 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -1,3 +1,4 @@ +option('bundle', type : 'boolean', value : false, description: 'Build a macOS bundle') +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') - diff --git a/resources/linux/org.lite_xl.lite_xl.appdata.xml b/resources/linux/org.lite_xl.lite_xl.appdata.xml new file mode 100644 index 00000000..c5895178 --- /dev/null +++ b/resources/linux/org.lite_xl.lite_xl.appdata.xml @@ -0,0 +1,33 @@ + + + org.lite_xl.lite_xl + MIT + MIT + Lite XL + A lightweight text editor written in Lua + + + +

+ Lite XL is a text editor and development tool written mainly in Lua, + on top of a minimalistic C core using the SDL2 graphics library. +

+
+ + + + The editor window + https://lite-xl.github.io/assets/img/screenshots/editor.png + + + + https://lite-xl.github.io + + + lite-xl + + + + + +
diff --git a/resources/linux/lite-xl.desktop b/resources/linux/org.lite_xl.lite_xl.desktop similarity index 71% rename from resources/linux/lite-xl.desktop rename to resources/linux/org.lite_xl.lite_xl.desktop index f2fa9610..d251c4dc 100644 --- a/resources/linux/lite-xl.desktop +++ b/resources/linux/org.lite_xl.lite_xl.desktop @@ -5,6 +5,6 @@ Comment=A lightweight text editor written in Lua Exec=lite-xl %F Icon=lite-xl Terminal=false -StartupNotify=false -Categories=Utility;TextEditor;Development; +StartupWMClass=lite-xl +Categories=Development;IDE; MimeType=text/plain; diff --git a/resources/macos/Info.plist b/resources/macos/Info.plist.in similarity index 53% rename from resources/macos/Info.plist rename to resources/macos/Info.plist.in index cc369cd0..4d715f2f 100644 --- a/resources/macos/Info.plist +++ b/resources/macos/Info.plist.in @@ -2,25 +2,30 @@ -CFBundleExecutable + CFBundleExecutable lite-xl CFBundleGetInfoString lite-xl CFBundleIconFile - icon + icon.icns CFBundleName - lite-xl + Lite XL CFBundlePackageType APPL NSHighResolutionCapable - MinimumOSVersion10.13 - NSDocumentsFolderUsageDescriptionTo access, edit and index your projects. - NSDesktopFolderUsageDescriptionTo access, edit and index your projects. - NSDownloadsFolderUsageDescriptionTo access, edit and index your projects. + LSMinimumSystemVersion + 10.11 + NSDocumentsFolderUsageDescription + To access, edit and index your projects. + NSDesktopFolderUsageDescription + To access, edit and index your projects. + NSDownloadsFolderUsageDescription + To access, edit and index your projects. CFBundleShortVersionString - 1.16.10 + @PROJECT_VERSION@ NSHumanReadableCopyright © 2019-2021 Francesco Abbate + diff --git a/resources/macos/appdmg.png b/resources/macos/appdmg.png new file mode 100644 index 00000000..1df7b60d Binary files /dev/null and b/resources/macos/appdmg.png differ diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 00000000..a236599c --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,28 @@ +# Scripts + +Various scripts and configurations used to configure, build, and package Lite XL. + +### Build + +- **build.sh** +- **build-packages.sh**: In root directory, as all in one script; relies to the + ones in this directory. + +### Package + +- **appdmg.sh**: Create a macOS DMG image using [AppDMG][1]. +- **appimage.sh**: [AppImage][2] builder. +- **innosetup.sh**: Creates a 32/64 bit [InnoSetup][3] installer package. +- **package.sh**: Creates all binary / DMG image / installer / source packages. + +### Utility + +- **common.sh**: Common functions used by other scripts. +- **install-dependencies.sh**: Installs required applications to build, package + and run Lite XL, mainly useful for CI and documentation purpose. + Preferably not to be used in user systems. +- **fontello-config.json**: Used by the icons generator. + +[1]: https://github.com/LinusU/node-appdmg +[2]: https://docs.appimage.org/ +[3]: https://jrsoftware.org/isinfo.php diff --git a/scripts/appdmg.sh b/scripts/appdmg.sh new file mode 100644 index 00000000..840f518b --- /dev/null +++ b/scripts/appdmg.sh @@ -0,0 +1,30 @@ +#!/bin/bash +set -ex + +if [ ! -e "src/api/api.h" ]; then + echo "Please run this script from the root directory of Lite XL." + exit 1 +fi + +cat > lite-xl-dmg.json << EOF +{ + "title": "Lite XL", + "icon": "$(pwd)/resources/icons/icon.icns", + "background": "$(pwd)/resources/macos/appdmg.png", + "window": { + "position": { + "x": 360, + "y": 360 + }, + "size": { + "width": 480, + "height": 360 + } + }, + "contents": [ + { "x": 144, "y": 248, "type": "file", "path": "$(pwd)/Lite XL.app" }, + { "x": 336, "y": 248, "type": "link", "path": "/Applications" } + ] +} +EOF +~/node_modules/appdmg/bin/appdmg.js lite-xl-dmg.json "$(pwd)/$1.dmg" diff --git a/scripts/appimage.sh b/scripts/appimage.sh new file mode 100644 index 00000000..8844fafe --- /dev/null +++ b/scripts/appimage.sh @@ -0,0 +1,162 @@ +#!/bin/env bash +set -ex + +if [ ! -e "src/api/api.h" ]; then + echo "Please run this script from the root directory of Lite XL." + exit 1 +fi + +source scripts/common.sh + +show_help(){ + echo + echo "Usage: $0 " + echo + echo "Available options:" + 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 "-n --nobuild Skips the build step, use existing files." + echo "-s --static Specify if building using static libraries" + echo " by using lhelper tool." + echo "-v --version VERSION Specify a version, non whitespace separated string." + echo +} + +ARCH="$(uname -m)" +BUILD_DIR="$(get_default_build_dir)" +RUN_BUILD=true +STATIC_BUILD=false + +for i in "$@"; do + case $i in + -h|--belp) + show_help + exit 0 + ;; + -b|--builddir) + BUILD_DIR="$2" + shift + shift + ;; + -n|--nobuild) + RUN_BUILD=false + shift + ;; + -s|--static) + STATIC_BUILD=true + shift + ;; + -v|--version) + VERSION="$2" + shift + shift + ;; + *) + # unknown option + ;; + 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 + 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 + fi + fi +} + +download_appimage_apprun() { + if [ ! -e AppRun ]; then + if ! wget -O AppRun "https://github.com/AppImage/AppImageKit/releases/download/continuous/AppRun-${ARCH}" ; then + echo "Could not download AppRun for the arch '${ARCH}'." + exit 1 + else + chmod 0755 AppRun + fi + fi +} + +build_litexl() { + if [ -e build ]; then + rm -rf build + fi + + if [ -e ${BUILD_DIR} ]; then + rm -rf ${BUILD_DIR} + fi + + echo "Build lite-xl..." + sleep 1 + meson setup --buildtype=release --prefix /usr ${BUILD_DIR} + meson compile -C ${BUILD_DIR} +} + +generate_appimage() { + if [ -e LiteXL.AppDir ]; then + rm -rf LiteXL.AppDir + fi + + echo "Creating LiteXL.AppDir..." + + DESTDIR="$(realpath LiteXL.AppDir)" meson install --skip-subprojects -C ${BUILD_DIR} + mv AppRun LiteXL.AppDir/ + # These could be symlinks but it seems they doesn't work with AppimageLauncher + cp resources/icons/lite-xl.svg LiteXL.AppDir/ + cp resources/linux/org.lite_xl.lite_xl.desktop LiteXL.AppDir/ + + if [[ $STATIC_BUILD == false ]]; then + echo "Copying libraries..." + + mkdir -p LiteXL.AppDir/usr/lib/ + + local allowed_libs=( + libfreetype + libpcre2 + libSDL2 + libsndio + liblua + ) + + while read line; do + local libname="$(echo $line | cut -d' ' -f1)" + local libpath="$(echo $line | cut -d' ' -f2)" + for lib in "${allowed_libs[@]}" ; do + if echo "$libname" | grep "$lib" > /dev/null ; then + cp "$libpath" LiteXL.AppDir/usr/lib/ + continue 2 + fi + done + echo " Ignoring: $libname" + done < <(ldd build/src/lite-xl | awk '{print $1 " " $3}') + fi + + echo "Generating AppImage..." + local version="" + if [ -n "$VERSION" ]; then + version="-$VERSION" + fi + + ./appimagetool LiteXL.AppDir LiteXL${version}-${ARCH}.AppImage +} + +setup_appimagetool +download_appimage_apprun +if [[ $RUN_BUILD == true ]]; then build_litexl; fi +generate_appimage $1 diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100644 index 00000000..75212468 --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,117 @@ +#!/bin/bash +set -e + +if [ ! -e "src/api/api.h" ]; then + echo "Please run this script from the root directory of Lite XL."; exit 1 +fi + +source scripts/common.sh + +show_help() { + echo + echo "Usage: $0 " + echo + echo "Available options:" + echo + echo "-b --builddir DIRNAME Sets the name of the build directory (not path)." + echo " Default: '$(get_default_build_dir)'." + echo " --debug Debug this script." + echo "-f --forcefallback Force to build dependencies statically." + echo "-h --help Show this help and exit." + echo "-p --prefix PREFIX Install directory prefix. Default: '/'." + 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 " macOS: disabled when used with --bundle," + echo " Windows: Implicit being the only option." + echo +} + +main() { + local platform="$(get_platform_name)" + local build_dir="$(get_default_build_dir)" + local prefix=/ + local force_fallback + local bundle + local portable + local pgo + + for i in "$@"; do + case $i in + -h|--help) + show_help + exit 0 + ;; + -b|--builddir) + build_dir="$2" + shift + shift + ;; + --debug) + set -x + shift + ;; + -f|--forcefallback) + force_fallback="--wrap-mode=forcefallback" + shift + ;; + -p|--prefix) + prefix="$2" + shift + shift + ;; + -B|--bundle) + if [[ "$platform" != "macos" ]]; then + echo "Warning: ignoring --bundle option, works only under macOS." + else + bundle="-Dbundle=true" + fi + shift + ;; + -P|--portable) + portable="-Dportable=true" + shift + ;; + -O|--pgo) + pgo="-Db_pgo=generate" + shift + ;; + *) + # unknown option + ;; + esac + done + + if [[ -n $1 ]]; then + show_help + exit 1 + fi + + if [[ $platform == "macos" && -n $bundle && -n $portable ]]; then + echo "Warning: \"bundle\" and \"portable\" specified; excluding portable package." + portable="" + fi + + rm -rf "${build_dir}" + + CFLAGS=$CFLAGS LDFLAGS=$LDFLAGS meson setup \ + --buildtype=release \ + --prefix "$prefix" \ + $force_fallback \ + $bundle \ + $portable \ + $pgo \ + "${build_dir}" + + meson compile -C "${build_dir}" + + if [ ! -z ${pgo+x} ]; then + cp -r data "${build_dir}/src" + "${build_dir}/src/lite-xl" + meson configure -Db_pgo=use "${build_dir}" + meson compile -C "${build_dir}" + rm -fr "${build_dir}/data" + fi +} + +main "$@" diff --git a/scripts/common.sh b/scripts/common.sh new file mode 100644 index 00000000..2b49d362 --- /dev/null +++ b/scripts/common.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +set -e + +get_platform_name() { + if [[ "$OSTYPE" == "msys" ]]; then + echo "windows" + elif [[ "$OSTYPE" == "darwin"* ]]; then + echo "macos" + elif [[ "$OSTYPE" == "linux"* || "$OSTYPE" == "freebsd"* ]]; then + echo "linux" + else + echo "UNSUPPORTED-OS" + fi +} + +get_default_build_dir() { + platform=$(get_platform_name) + echo "build-$platform-$(uname -m)" +} + +if [[ $(get_platform_name) == "UNSUPPORTED-OS" ]]; then + echo "Error: unknown OS type: \"$OSTYPE\"" + exit 1 +fi diff --git a/scripts/innosetup/innosetup.iss.in b/scripts/innosetup/innosetup.iss.in new file mode 100644 index 00000000..2b669fc0 --- /dev/null +++ b/scripts/innosetup/innosetup.iss.in @@ -0,0 +1,88 @@ +#define MyAppName "Lite XL" +#define MyAppVersion "@PROJECT_VERSION@" +#define MyAppPublisher "Lite XL Team" +#define MyAppURL "https://lite-xl.github.io" +#define MyAppExeName "lite-xl.exe" +#define BuildDir "@PROJECT_BUILD_DIR@" +#define SourceDir "@PROJECT_SOURCE_DIR@" + +; Use /dArch option to create a setup for a different architecture, e.g.: +; iscc /dArch=x86 innosetup.iss +#ifndef Arch + #define Arch "x64" +#endif + +[Setup] +; NOTE: The value of AppId uniquely identifies this application. +; Do not use the same AppId value in installers for other applications. +; To generate a new GUID, click Tools | Generate GUID inside the InnoSetup IDE. +AppId={{06761240-D97C-43DE-B9ED-C15F765A2D65} + +AppName={#MyAppName} +AppVersion={#MyAppVersion} +;AppVerName={#MyAppName} {#MyAppVersion} +AppPublisher={#MyAppPublisher} +AppPublisherURL={#MyAppURL} +AppSupportURL={#MyAppURL} +AppUpdatesURL={#MyAppURL} + +#if Arch=="x64" + ArchitecturesAllowed=x64 + ArchitecturesInstallIn64BitMode=x64 + #define ArchInternal "x86_64" +#else + #define ArchInternal "i686" +#endif + +AllowNoIcons=yes +Compression=lzma +SolidCompression=yes +DefaultDirName={autopf}/{#MyAppName} +DefaultGroupName={#MyAppPublisher} +UninstallFilesDir={app} + +; Uncomment the following line to run in non administrative install mode +; (install for current user only.) +;PrivilegesRequired=lowest +PrivilegesRequiredOverridesAllowed=dialog + +; The [Icons] "quicklaunchicon" entry uses {userappdata} +; but its [Tasks] entry has a proper IsAdminInstallMode Check. +UsedUserAreasWarning=no + +OutputDir=. +OutputBaseFilename=LiteXL-{#MyAppVersion}-{#ArchInternal}-setup +;DisableDirPage=yes +;DisableProgramGroupPage=yes + +LicenseFile={#SourceDir}/LICENSE +SetupIconFile={#SourceDir}/resources/icons/icon.ico +WizardImageFile="{#SourceDir}/scripts/innosetup/wizard-modern-image.bmp" +WizardSmallImageFile="{#SourceDir}/scripts/innosetup/litexl-55px.bmp" + +[Languages] +Name: "english"; MessagesFile: "compiler:Default.isl" + +[Tasks] +Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked +Name: "quicklaunchicon"; Description: "{cm:CreateQuickLaunchIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked; OnlyBelowVersion: 6.1; Check: not IsAdminInstallMode +Name: "portablemode"; Description: "Portable Mode"; Flags: unchecked + +[Files] +Source: "{#BuildDir}/src/lite-xl.exe"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#BuildDir}/mingwLibs{#Arch}/*"; DestDir: "{app}"; Flags: ignoreversion ; Check: DirExists(ExpandConstant('{#BuildDir}/mingwLibs{#Arch}')) +Source: "{#SourceDir}/data/*"; DestDir: "{app}/data"; Flags: ignoreversion recursesubdirs +; NOTE: Don't use "Flags: ignoreversion" on any shared system files + +[Icons] +Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon +Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Check: not WizardIsTaskSelected('portablemode') +Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}"; Check: not WizardIsTaskSelected('portablemode') +Name: "{userappdata}\Microsoft\Internet Explorer\Quick Launch\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: quicklaunchicon; Check: not WizardIsTaskSelected('portablemode') +; Name: "{usersendto}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" + +[Run] +Filename: "{app}/{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent + +[Setup] +Uninstallable=not WizardIsTaskSelected('portablemode') diff --git a/scripts/innosetup/innosetup.sh b/scripts/innosetup/innosetup.sh new file mode 100644 index 00000000..4384d13c --- /dev/null +++ b/scripts/innosetup/innosetup.sh @@ -0,0 +1,65 @@ +#!/bin/bash +set -e + +if [ ! -e "src/api/api.h" ]; then + echo "Please run this script from the root directory of Lite XL."; exit 1 +fi + +source scripts/common.sh + +show_help() { + echo + echo "Usage: $0 " + echo + echo "Available options:" + echo + echo "-b --builddir DIRNAME Sets the name of the build directory (not path)." + echo " Default: '$(get_default_build_dir)'." + echo " --debug Debug this script." + echo +} + +main() { + local build_dir=$(get_default_build_dir) + local arch + + if [[ $MSYSTEM == "MINGW64" ]]; then arch=x64; else arch=Win32; fi + + for i in "$@"; do + case $i in + -h|--help) + show_help + exit 0 + ;; + -b|--builddir) + build_dir="$2" + shift + shift + ;; + --debug) + set -x + shift + ;; + *) + # unknown option + ;; + esac + done + + if [[ -n $1 ]]; 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 + + "/c/Program Files (x86)/Inno Setup 6/ISCC.exe" -dARCH=$arch "${build_dir}/scripts/innosetup.iss" + pushd "${build_dir}/scripts"; mv LiteXL*.exe "./../../"; popd +} + +main "$@" diff --git a/scripts/innosetup/litexl-55px.bmp b/scripts/innosetup/litexl-55px.bmp new file mode 100644 index 00000000..d3424a9b Binary files /dev/null and b/scripts/innosetup/litexl-55px.bmp differ diff --git a/scripts/innosetup/wizard-modern-image.bmp b/scripts/innosetup/wizard-modern-image.bmp new file mode 100644 index 00000000..cf844e09 Binary files /dev/null and b/scripts/innosetup/wizard-modern-image.bmp differ diff --git a/scripts/install-dependencies.sh b/scripts/install-dependencies.sh new file mode 100644 index 00000000..2f9519b1 --- /dev/null +++ b/scripts/install-dependencies.sh @@ -0,0 +1,74 @@ +#!/bin/bash +set -ex + +if [ ! -e "src/api/api.h" ]; then + echo "Please run this script from the root directory of Lite XL."; exit 1 +fi + +show_help() { + echo + echo "Lite XL dependecies installer. Mainly used for CI but can also work on users systems." + echo "USE IT AT YOUR OWN RISK!" + echo + echo "Usage: $0 " + echo + echo "Available options:" + echo + echo "-l --lhelper Install tools required by LHelper and doesn't" + echo " install external libraries." + echo " --debug Debug this script." + echo +} + +main() { + local lhelper=false + + for i in "$@"; do + case $i in + -s|--lhelper) + lhelper=true + shift + ;; + --debug) + set -x + shift + ;; + *) + # unknown option + ;; + esac + done + + if [[ -n $1 ]]; then + show_help + exit 1 + fi + + if [[ "$OSTYPE" == "linux"* ]]; then + if [[ $lhelper == true ]]; then + sudo apt-get install -qq ninja-build + else + sudo apt-get install -qq ninja-build libsdl2-dev libfreetype6 + fi + pip3 install meson + elif [[ "$OSTYPE" == "darwin"* ]]; then + if [[ $lhelper == true ]]; then + brew install bash md5sha1sum ninja + else + brew install bash ninja sdl2 + fi + pip3 install meson + cd ~; npm install appdmg; cd - + ~/node_modules/appdmg/bin/appdmg.js --version + elif [[ "$OSTYPE" == "msys" ]]; then + if [[ $lhelper == true ]]; then + pacman --noconfirm -S \ + ${MINGW_PACKAGE_PREFIX}-{gcc,meson,ninja,ntldd,pkg-config} unzip + else + pacman --noconfirm -S \ + ${MINGW_PACKAGE_PREFIX}-{gcc,meson,ninja,ntldd,pkg-config,freetype,pcre2,SDL2} unzip + fi + fi +} + +main "$@" diff --git a/scripts/lhelper.sh b/scripts/lhelper.sh new file mode 100644 index 00000000..af6ae158 --- /dev/null +++ b/scripts/lhelper.sh @@ -0,0 +1,75 @@ +#!/bin/bash +set -e + +show_help() { + echo + echo "Usage: $0 " + echo + echo "Available options:" + echo + echo " --debug Debug this script." + echo "-h --help Show this help and exit." + echo "-p --prefix PREFIX Install directory prefix." + echo " Default: '$HOME/.local'." + echo +} + +main() { + local lhelper_prefix="$HOME/.local" + + for i in "$@"; do + case $i in + -h|--help) + show_help + exit 0 + ;; + -p|--prefix) + lhelper_prefix="$2" + echo "LHelper prefix set to: \"${lhelper_prefix}\"" + shift + shift + ;; + --debug) + set -x + shift + ;; + *) + # unknown option + ;; + esac + done + + if [[ -n $1 ]]; then show_help; exit 1; fi + + if [[ ! -f ${lhelper_prefix}/bin/lhelper ]]; then + + git clone https://github.com/franko/lhelper.git + + # FIXME: This should be set in ~/.bash_profile if not using CI + # export PATH="${HOME}/.local/bin:${PATH}" + mkdir -p "${lhelper_prefix}/bin" + pushd lhelper; bash install "${lhelper_prefix}"; popd + + if [[ "$OSTYPE" == "darwin"* ]]; then + CC=clang CXX=clang++ lhelper create lite-xl -n + else + lhelper create lite-xl -n + 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 + + # 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 +} + +main diff --git a/scripts/meson.build b/scripts/meson.build new file mode 100644 index 00000000..8b45814d --- /dev/null +++ b/scripts/meson.build @@ -0,0 +1,8 @@ +if host_machine.system() == 'windows' + configure_file( + input : 'innosetup/innosetup.iss.in', + output : 'innosetup.iss', + configuration : conf_data + ) +endif + diff --git a/scripts/package.sh b/scripts/package.sh new file mode 100644 index 00000000..1370aee8 --- /dev/null +++ b/scripts/package.sh @@ -0,0 +1,259 @@ +#!/bin/bash +set -e + +if [ ! -e "src/api/api.h" ]; then + echo "Please run this script from the root directory of Lite XL."; exit 1 +fi + +source scripts/common.sh + +show_help() { + echo + echo "Usage: $0 " + echo + echo "Available options:" + echo + echo "-b --builddir DIRNAME Sets the name of the build directory (not path)." + echo " Default: '$(get_default_build_dir)'." + echo "-d --destdir DIRNAME Set the name of the package directory (not path)." + echo " Default: 'lite-xl'." + 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 RXI colors)." + 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 "-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/rxi/lite-colors/archive/master.zip" \ + -o "${build_dir}/rxi-lite-colors.zip" + + mkdir -p "${build_dir}/third/data/colors" + unzip "${build_dir}/rxi-lite-colors.zip" -d "${build_dir}" + mv "${build_dir}/lite-colors-master/colors" "${build_dir}/third/data" + rm -rf "${build_dir}/lite-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 + + rm -rf ${build_dir} + rm -rf ${package_name} + rm -f ${package_name}.tar.gz + + meson subprojects download + meson setup ${build_dir} -Dsource-only=true + + # Note: not using git-archive(-all) because it can't include subprojects ignored by git + rsync -arv \ + --exclude /*build*/ \ + --exclude *.git* \ + --exclude lhelper \ + --exclude lite-xl* \ + --exclude submodules \ + . ${package_name} + + cp "${build_dir}/start.lua" "${package_name}/data/core" + + tar rf ${package_name}.tar ${package_name} + gzip -9 ${package_name}.tar +} + +main() { + local arch="$(uname -m)" + local platform="$(get_platform_name)" + local build_dir="$(get_default_build_dir)" + local dest_dir=lite-xl + local prefix=/ + local version + local addons=false + local appimage=false + local binary=false + local dmg=false + local innosetup=false + local source=false + + for i in "$@"; do + case $i in + -b|--builddir) + build_dir="$2" + shift + shift + ;; + -d|--destdir) + dest_dir="$2" + shift + shift + ;; + -h|--help) + show_help + exit 0 + ;; + -p|--prefix) + prefix="$2" + shift + shift + ;; + -v|--version) + if [[ -n $2 ]]; then version="-$2"; fi + shift + shift + ;; + -A|--appimage) + if [[ "$platform" != "linux" ]]; then + echo "Warning: ignoring --appimage option, works only under Linux." + else + appimage=true + fi + shift + ;; + -B|--binary) + binary=true + shift + ;; + -D|--dmg) + if [[ "$platform" != "macos" ]]; then + echo "Warning: ignoring --dmg option, works only under macOS." + else + dmg=true + fi + shift + ;; + -I|--innosetup) + if [[ "$platform" != "windows" ]]; then + echo "Warning: ignoring --innosetup option, works only under Windows." + else + innosetup=true + fi + shift + ;; + -S|--source) + source=true + shift + ;; + --addons) + addons=true + shift + ;; + --debug) + set -x + shift + ;; + *) + # unknown option + ;; + esac + done + + if [[ -n $1 ]]; then show_help; exit 1; fi + + # The source package doesn't require a previous build, + # nor the following install step, so run it now. + if [[ $source == true ]]; then source_package "lite-xl$version-src"; fi + + # No packages request + if [[ $appimage == false && $binary == false && $dmg == false && $innosetup == false ]]; then + # Source only, return. + if [[ $source == true ]]; then return 0; fi + # Build the binary package as default instead doing nothing. + binary=true + fi + + rm -rf "${dest_dir}" + + DESTDIR="$(pwd)/${dest_dir}" meson install -C "${build_dir}" + + 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 + local stripcmd="strip" + + if [[ -d "${data_dir}" ]]; then + echo "Creating a portable, compressed archive..." + portable=true + exe_file="$(pwd)/${dest_dir}/lite-xl" + if [[ $platform == "windows" ]]; then + exe_file="${exe_file}.exe" + stripcmd="strip --strip-all" + else + # Windows archive is always portable + package_name+="-portable" + fi + elif [[ $platform == "macos" && ! -d "${data_dir}" ]]; then + data_dir="$(pwd)/${dest_dir}/Contents/Resources" + if [[ -d "${data_dir}" ]]; then + echo "Creating a macOS bundle application..." + bundle=true + # Specify "bundle" on compressed archive only, implicit on images + if [[ $dmg == false ]]; then package_name+="-bundle"; fi + 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" + 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 + + # TODO: use --skip-subprojects when 0.58.0 will be available on supported + # distributions to avoid subprojects' include and lib directories to be copied. + # Install Meson with PIP to get the latest version is not always possible. + pushd "${dest_dir}" + find . -type d -name 'include' -prune -exec rm -rf {} \; + find . -type d -name 'lib' -prune -exec rm -rf {} \; + find . -type d -empty -delete + popd + + $stripcmd "${exe_file}" + + if [[ $binary == true ]]; then + rm -f "${package_name}".tar.gz + rm -f "${package_name}".zip + + if [[ $platform == "windows" ]]; then + zip -9rv ${package_name}.zip ${dest_dir}/* + else + tar czvf "${package_name}".tar.gz "${dest_dir}" + 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 +} + +main "$@" diff --git a/scripts/repackage.sh b/scripts/repackage.sh index 99368582..f8da579f 100644 --- a/scripts/repackage.sh +++ b/scripts/repackage.sh @@ -10,7 +10,7 @@ copy_directory_from_repo () { fi local dirname="$1" local destdir="$2" - git archive master "$dirname" --format=tar | tar xf - -C "$destdir" "${tar_options[@]}" + git archive "$lite_branch" "$dirname" --format=tar | tar xf - -C "$destdir" "${tar_options[@]}" } lite_copy_third_party_modules () { @@ -23,12 +23,17 @@ lite_copy_third_party_modules () { rm "$build/rxi-lite-colors.zip" } +lite_branch=master while [ ! -z ${1+x} ]; do case "$1" in -dir) use_dir="$(realpath $2)" shift 2 ;; + -branch) + lite_branch="$2" + shift 2 + ;; *) echo "unknown option: $1" exit 1 @@ -73,6 +78,8 @@ for filename in $(ls -1 *.zip *.tar.*); do fi rm "$filename" find lite-xl -name lite -exec chmod a+x '{}' \; + start_file=$(find lite-xl -name start.lua) + lite_version=$(cat "$start_file" | awk 'match($0, /^\s*VERSION\s*=\s*"(.+)"/, a) { print(a[1]) }') xcoredir="$(find lite-xl -type d -name 'core')" coredir="$(dirname $xcoredir)" echo "coredir: $coredir" @@ -81,6 +88,7 @@ for filename in $(ls -1 *.zip *.tar.*); do rm -fr "$coredir/$module_name" (cd .. && copy_directory_from_repo --strip-components=1 "data/$module_name" "$workdir/$coredir") done + sed -i "s/@PROJECT_VERSION@/$lite_version/g" "$start_file" for module_name in plugins colors; do cp -r "third/data/$module_name" "$coredir" done diff --git a/scripts/run-local b/scripts/run-local index d5de23aa..8a32e7fa 100755 --- a/scripts/run-local +++ b/scripts/run-local @@ -47,10 +47,7 @@ if [[ "$OSTYPE" == "msys"* || "$OSTYPE" == "mingw"* ]]; then fi rundir=".run" -if [[ "$OSTYPE" == "darwin"* ]]; then - bindir="$rundir" - datadir="$rundir" -elif [ "$option_portable" == on ]; then +if [ "$option_portable" == on ]; then bindir="$rundir" datadir="$rundir/data" else @@ -75,9 +72,14 @@ copy_lite_build () { else cp "$builddir/src/lite-xl" "$bindir" fi + mkdir -p "$datadir/core" for module_name in core plugins colors fonts; do cp -r "data/$module_name" "$datadir" done + # The start.lua file is generated by meson in $builddir but + # there is already a start.lua file in data/core so the command below + # should be executed after we copy the data/core directory. + cp "$builddir/start.lua" "$datadir/core" } run_lite () { diff --git a/src/api/api.h b/src/api/api.h index 4b0e14f0..51ebb9a8 100644 --- a/src/api/api.h +++ b/src/api/api.h @@ -7,6 +7,7 @@ #define API_TYPE_FONT "Font" #define API_TYPE_REPLACE "Replace" +#define API_TYPE_PROCESS "Process" void api_load_libs(lua_State *L); diff --git a/src/api/process.c b/src/api/process.c index 111667a1..4b018e4c 100644 --- a/src/api/process.c +++ b/src/api/process.c @@ -5,33 +5,171 @@ */ #include -#include -#include -#include -#include #include #include +#include +#include "api.h" + +#define READ_BUF_SIZE 2048 + +#define L_GETTABLE(L, idx, key, conv, def) ( \ + lua_getfield(L, idx, key), \ + conv(L, -1, def) \ +) + +#define L_GETNUM(L, idx, key, def) L_GETTABLE(L, idx, key, luaL_optnumber, def) +#define L_GETSTR(L, idx, key, def) L_GETTABLE(L, idx, key, luaL_optstring, def) + +#define L_SETNUM(L, idx, key, n) (lua_pushnumber(L, n), lua_setfield(L, idx - 1, key)) + +#define L_RETURN_REPROC_ERROR(L, code) { \ + lua_pushnil(L); \ + lua_pushstring(L, reproc_strerror(code)); \ + lua_pushnumber(L, code); \ + return 3; \ +} + +#define ASSERT_MALLOC(ptr) \ + if (ptr == NULL) \ + L_RETURN_REPROC_ERROR(L, REPROC_ENOMEM) + +#define ASSERT_REPROC_ERRNO(L, code) { \ + if (code < 0) \ + L_RETURN_REPROC_ERROR(L, code) \ +} typedef struct { reproc_t * process; - lua_State* L; - + bool running; + int returncode; } process_t; -static int process_new(lua_State* L) +// this function should be called instead of reproc_wait +static int poll_process(process_t* proc, int timeout) { - process_t* self = (process_t*) lua_newuserdata( - L, sizeof(process_t) + int ret = reproc_wait(proc->process, timeout); + if (ret != REPROC_ETIMEDOUT) { + proc->running = false; + proc->returncode = ret; + } + return ret; +} + +static int kill_process(process_t* proc) +{ + int ret = reproc_stop( + proc->process, + (reproc_stop_actions) { + {REPROC_STOP_KILL, 0}, + {REPROC_STOP_TERMINATE, 0}, + {REPROC_STOP_NOOP, 0} + } ); - memset(self, 0, sizeof(process_t)); + if (ret != REPROC_ETIMEDOUT) { + proc->running = false; + proc->returncode = ret; + } - self->process = NULL; - self->L = L; + return ret; +} - luaL_getmetatable(L, "PROCESS"); - lua_setmetatable(L, -2); +static int process_start(lua_State* L) +{ + luaL_checktype(L, 1, LUA_TTABLE); + if (lua_isnoneornil(L, 2)) { + lua_settop(L, 1); // remove the nil if it's there + lua_newtable(L); + } + luaL_checktype(L, 2, LUA_TTABLE); + int cmd_len = lua_rawlen(L, 1); + const char** cmd = malloc(sizeof(char *) * (cmd_len + 1)); + ASSERT_MALLOC(cmd); + cmd[cmd_len] = NULL; + + for(int i = 0; i < cmd_len; i++) { + lua_rawgeti(L, 1, i + 1); + + cmd[i] = luaL_checkstring(L, -1); + lua_pop(L, 1); + } + + int deadline = L_GETNUM(L, 2, "timeout", 0); + const char* cwd =L_GETSTR(L, 2, "cwd", NULL); + int redirect_in = L_GETNUM(L, 2, "stdin", REPROC_REDIRECT_DEFAULT); + int redirect_out = L_GETNUM(L, 2, "stdout", REPROC_REDIRECT_DEFAULT); + int redirect_err = L_GETNUM(L, 2, "stderr", REPROC_REDIRECT_DEFAULT); + lua_pop(L, 5); // remove args we just read + + if ( + redirect_in > REPROC_REDIRECT_STDOUT + || redirect_out > REPROC_REDIRECT_STDOUT + || redirect_err > REPROC_REDIRECT_STDOUT) + { + lua_pushnil(L); + lua_pushliteral(L, "redirect to handles, FILE* and paths are not supported"); + return 2; + } + + // env + luaL_getsubtable(L, 2, "env"); + const char **env = NULL; + int env_len = 0; + + lua_pushnil(L); + while (lua_next(L, -2) != 0) { + env_len++; + lua_pop(L, 1); + } + + if (env_len > 0) { + env = malloc(sizeof(char*) * (env_len + 1)); + env[env_len] = NULL; + + int i = 0; + lua_pushnil(L); + while (lua_next(L, -2) != 0) { + lua_pushliteral(L, "="); + lua_pushvalue(L, -3); // push the key to the top + lua_concat(L, 3); // key=value + + env[i++] = luaL_checkstring(L, -1); + lua_pop(L, 1); + } + } + + reproc_t* proc = reproc_new(); + int out = reproc_start( + proc, + (const char* const*) cmd, + (reproc_options) { + .working_directory = cwd, + .deadline = deadline, + .nonblocking = true, + .env = { + .behavior = REPROC_ENV_EXTEND, + .extra = env + }, + .redirect = { + .in.type = redirect_in, + .out.type = redirect_out, + .err.type = redirect_err + } + } + ); + + if (out < 0) { + reproc_destroy(proc); + L_RETURN_REPROC_ERROR(L, out); + } + + process_t* self = lua_newuserdata(L, sizeof(process_t)); + self->process = proc; + self->running = true; + + // this is equivalent to using lua_setmetatable() + luaL_setmetatable(L, API_TYPE_PROCESS); return 1; } @@ -39,24 +177,20 @@ static int process_strerror(lua_State* L) { int error_code = luaL_checknumber(L, 1); - if(error_code){ - lua_pushstring( - L, - reproc_strerror(error_code) - ); - } else { + if (error_code < 0) + lua_pushstring(L, reproc_strerror(error_code)); + else lua_pushnil(L); - } - + return 1; } -static int process_gc(lua_State* L) +static int f_gc(lua_State* L) { - process_t* self = (process_t*) luaL_checkudata(L, 1, "PROCESS"); + process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS); - if(self->process){ - reproc_kill(self->process); + if(self->process) { + kill_process(self); reproc_destroy(self->process); self->process = NULL; } @@ -64,385 +198,211 @@ static int process_gc(lua_State* L) return 0; } -static int process_start(lua_State* L) +static int f_tostring(lua_State* L) { - process_t* self = (process_t*) lua_touserdata(L, 1); + luaL_checkudata(L, 1, API_TYPE_PROCESS); - luaL_checktype(L, 2, LUA_TTABLE); + lua_pushliteral(L, API_TYPE_PROCESS); + return 1; +} - char* path = NULL; - size_t path_len = 0; +static int f_pid(lua_State* L) +{ + process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS); - if(lua_type(L, 3) == LUA_TSTRING){ - path = (char*) lua_tolstring(L, 3, &path_len); - } + lua_pushnumber(L, reproc_pid(self->process)); + return 1; +} - size_t deadline = 0; +static int f_returncode(lua_State *L) +{ + process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS); + int ret = poll_process(self, 0); - if(lua_type(L, 4) == LUA_TNUMBER){ - deadline = lua_tonumber(L, 4); - } + if (self->running) + lua_pushnil(L); + else + lua_pushnumber(L, ret); - size_t table_len = luaL_len(L, 2); - char* command[table_len+1]; - command[table_len] = NULL; + return 1; +} - int i; - for(i=1; i<=table_len; i++){ - lua_pushnumber(L, i); - lua_gettable(L, 2); +static int g_read(lua_State* L, int stream) +{ + process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS); + unsigned long read_size = luaL_optunsigned(L, 2, READ_BUF_SIZE); - command[i-1] = (char*) lua_tostring(L, -1); + luaL_Buffer b; + uint8_t* buffer = (uint8_t*) luaL_buffinitsize(L, &b, read_size); - lua_remove(L, -1); - } - - if(self->process){ - reproc_kill(self->process); - reproc_destroy(self->process); - } - - self->process = reproc_new(); - - int out = reproc_start( + int out = reproc_read( self->process, - (const char* const*) command, - (reproc_options){ - .working_directory = path, - .deadline = deadline, - .nonblocking=true, - .redirect.err.type=REPROC_REDIRECT_PIPE - } + stream, + buffer, + read_size ); - if(out > 0) { - lua_pushboolean(L, 1); - } - else { - reproc_destroy(self->process); - self->process = NULL; - lua_pushnumber(L, out); + if (out >= 0) + luaL_addsize(&b, out); + luaL_pushresult(&b); + + if (out == REPROC_EPIPE) { + kill_process(self); + ASSERT_REPROC_ERRNO(L, out); } return 1; } -static int process_pid(lua_State* L) +static int f_read_stdout(lua_State* L) { - process_t* self = (process_t*) lua_touserdata(L, 1); - - if(self->process){ - int id = reproc_pid(self->process); - - if(id > 0){ - lua_pushnumber(L, id); - } else { - lua_pushnumber(L, 0); - } - } else { - lua_pushnumber(L, 0); - } - - return 1; + return g_read(L, REPROC_STREAM_OUT); } -static int process_read(lua_State* L) +static int f_read_stderr(lua_State* L) { - process_t* self = (process_t*) lua_touserdata(L, 1); - - if(self->process){ - int read_size = 4096; - if (lua_type(L, 2) == LUA_TNUMBER){ - read_size = (int) lua_tonumber(L, 2); - } - - int tries = 1; - if (lua_type(L, 3) == LUA_TNUMBER){ - tries = (int) lua_tonumber(L, 3); - } - - int out = 0; - uint8_t buffer[read_size]; - - int runs; - for (runs=0; runsprocess, - REPROC_STREAM_OUT, - buffer, - read_size - ); - - if (out >= 0) - break; - } - - // if request for tries was set and nothing - // read kill the process - if(tries > 1 && out < 0) - out = REPROC_EPIPE; - - if(out == REPROC_EPIPE){ - reproc_kill(self->process); - reproc_destroy(self->process); - self->process = NULL; - - lua_pushnil(L); - } else if(out > 0) { - lua_pushlstring(L, (const char*) buffer, out); - } else { - lua_pushnil(L); - } - } else { - lua_pushnil(L); - } - - return 1; + return g_read(L, REPROC_STREAM_ERR); } -static int process_read_errors(lua_State* L) +static int f_read(lua_State* L) { - process_t* self = (process_t*) lua_touserdata(L, 1); + int stream = luaL_checknumber(L, 2); + lua_remove(L, 2); + if (stream > REPROC_STREAM_ERR) + L_RETURN_REPROC_ERROR(L, REPROC_EINVAL); - if(self->process){ - int read_size = 4096; - if (lua_type(L, 2) == LUA_TNUMBER){ - read_size = (int) lua_tonumber(L, 2); - } - - int tries = 1; - if (lua_type(L, 3) == LUA_TNUMBER){ - tries = (int) lua_tonumber(L, 3); - } - - int out = 0; - uint8_t buffer[read_size]; - - int runs; - for (runs=0; runsprocess, - REPROC_STREAM_ERR, - buffer, - read_size - ); - - if (out >= 0) - break; - } - - // if request for tries was set and nothing - // read kill the process - if(tries > 1 && out < 0) - out = REPROC_EPIPE; - - if(out == REPROC_EPIPE){ - reproc_kill(self->process); - reproc_destroy(self->process); - self->process = NULL; - - lua_pushnil(L); - } else if(out > 0) { - lua_pushlstring(L, (const char*) buffer, out); - } else { - lua_pushnil(L); - } - } else { - lua_pushnil(L); - } - - return 1; + return g_read(L, stream); } -static int process_write(lua_State* L) +static int f_write(lua_State* L) { - process_t* self = (process_t*) lua_touserdata(L, 1); + process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS); - if(self->process){ - size_t data_size = 0; - const char* data = luaL_checklstring(L, 2, &data_size); + size_t data_size = 0; + const char* data = luaL_checklstring(L, 2, &data_size); - int out = 0; - - out = reproc_write( - self->process, - (uint8_t*) data, - data_size - ); - - if(out == REPROC_EPIPE){ - reproc_kill(self->process); - reproc_destroy(self->process); - self->process = NULL; - } - - lua_pushnumber(L, out); - } else { - lua_pushnumber(L, REPROC_EPIPE); + int out = reproc_write( + self->process, + (uint8_t*) data, + data_size + ); + if (out == REPROC_EPIPE) { + kill_process(self); + L_RETURN_REPROC_ERROR(L, out); } + lua_pushnumber(L, out); return 1; } -static int process_close_stream(lua_State* L) +static int f_close_stream(lua_State* L) { - process_t* self = (process_t*) lua_touserdata(L, 1); + process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS); - if(self->process){ - size_t stream = luaL_checknumber(L, 2); - - int out = reproc_close(self->process, stream); - - lua_pushnumber(L, out); - } else { - lua_pushnumber(L, REPROC_EINVAL); - } + int stream = luaL_checknumber(L, 2); + int out = reproc_close(self->process, stream); + ASSERT_REPROC_ERRNO(L, out); + lua_pushboolean(L, 1); return 1; } -static int process_wait(lua_State* L) +static int f_wait(lua_State* L) { - process_t* self = (process_t*) lua_touserdata(L, 1); - - if(self->process){ - size_t timeout = luaL_checknumber(L, 2); - - int out = reproc_wait(self->process, timeout); - - if(out >= 0){ - reproc_destroy(self->process); - self->process = NULL; - } - - lua_pushnumber(L, out); - } else { - lua_pushnumber(L, REPROC_EINVAL); - } + process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS); + + int timeout = luaL_optnumber(L, 2, 0); + + int ret = poll_process(self, timeout); + // negative returncode is also used for signals on POSIX + if (ret == REPROC_ETIMEDOUT) + L_RETURN_REPROC_ERROR(L, ret); + lua_pushnumber(L, ret); return 1; } -static int process_terminate(lua_State* L) +static int f_terminate(lua_State* L) { - process_t* self = (process_t*) lua_touserdata(L, 1); + process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS); - if(self->process){ - int out = reproc_terminate(self->process); + int out = reproc_terminate(self->process); + ASSERT_REPROC_ERRNO(L, out); - if(out < 0){ - lua_pushnumber(L, out); - } else { - reproc_destroy(self->process); - self->process = NULL; - lua_pushboolean(L, 1); - } - } else { - lua_pushnumber(L, REPROC_EINVAL); - } + poll_process(self, 0); + lua_pushboolean(L, 1); return 1; } -static int process_kill(lua_State* L) +static int f_kill(lua_State* L) { - process_t* self = (process_t*) lua_touserdata(L, 1); + process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS); - if(self->process){ - int out = reproc_kill(self->process); + int out = reproc_kill(self->process); + ASSERT_REPROC_ERRNO(L, out); - if(out < 0){ - lua_pushnumber(L, out); - } else { - reproc_destroy(self->process); - self->process = NULL; - lua_pushboolean(L, 1); - } - } else { - lua_pushnumber(L, REPROC_EINVAL); - } + poll_process(self, 0); + lua_pushboolean(L, 1); return 1; } -static int process_running(lua_State* L) +static int f_running(lua_State* L) { - process_t* self = (process_t*) lua_touserdata(L, 1); - - if(self->process){ - lua_pushboolean(L, 1); - } else { - lua_pushboolean(L, 0); - } + process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS); + + poll_process(self, 0); + lua_pushboolean(L, self->running); return 1; } -static const struct luaL_Reg process_methods[] = { - { "__gc", process_gc}, +static const struct luaL_Reg lib[] = { {"start", process_start}, - {"pid", process_pid}, - {"read", process_read}, - {"read_errors", process_read_errors}, - {"write", process_write}, - {"close_stream", process_close_stream}, - {"wait", process_wait}, - {"terminate", process_terminate}, - {"kill", process_kill}, - {"running", process_running}, - {NULL, NULL} -}; - -static const struct luaL_Reg process[] = { - {"new", process_new}, {"strerror", process_strerror}, - {"ERROR_PIPE", NULL}, - {"ERROR_WOULDBLOCK", NULL}, - {"ERROR_TIMEDOUT", NULL}, - {"ERROR_INVALID", NULL}, - {"STREAM_STDIN", NULL}, - {"STREAM_STDOUT", NULL}, - {"STREAM_STDERR", NULL}, - {"WAIT_INFINITE", NULL}, - {"WAIT_DEADLINE", NULL}, + {"__gc", f_gc}, + {"__tostring", f_tostring}, + {"pid", f_pid}, + {"returncode", f_returncode}, + {"read", f_read}, + {"read_stdout", f_read_stdout}, + {"read_stderr", f_read_stderr}, + {"write", f_write}, + {"close_stream", f_close_stream}, + {"wait", f_wait}, + {"terminate", f_terminate}, + {"kill", f_kill}, + {"running", f_running}, {NULL, NULL} }; int luaopen_process(lua_State *L) { - luaL_newmetatable(L, "PROCESS"); - luaL_setfuncs(L, process_methods, 0); + luaL_newmetatable(L, API_TYPE_PROCESS); + luaL_setfuncs(L, lib, 0); lua_pushvalue(L, -1); lua_setfield(L, -2, "__index"); - luaL_newlib(L, process); + // constants + L_SETNUM(L, -1, "ERROR_INVAL", REPROC_EINVAL); + L_SETNUM(L, -1, "ERROR_TIMEDOUT", REPROC_ETIMEDOUT); + L_SETNUM(L, -1, "ERROR_PIPE", REPROC_EPIPE); + L_SETNUM(L, -1, "ERROR_NOMEM", REPROC_ENOMEM); + L_SETNUM(L, -1, "ERROR_WOULDBLOCK", REPROC_EWOULDBLOCK); - lua_pushnumber(L, REPROC_EPIPE); - lua_setfield(L, -2, "ERROR_PIPE"); + L_SETNUM(L, -1, "WAIT_INFINITE", REPROC_INFINITE); + L_SETNUM(L, -1, "WAIT_DEADLINE", REPROC_DEADLINE); - lua_pushnumber(L, REPROC_EWOULDBLOCK); - lua_setfield(L, -2, "ERROR_WOULDBLOCK"); + L_SETNUM(L, -1, "STREAM_STDIN", REPROC_STREAM_IN); + L_SETNUM(L, -1, "STREAM_STDOUT", REPROC_STREAM_OUT); + L_SETNUM(L, -1, "STREAM_STDERR", REPROC_STREAM_ERR); - lua_pushnumber(L, REPROC_ETIMEDOUT); - lua_setfield(L, -2, "ERROR_TIMEDOUT"); - - lua_pushnumber(L, REPROC_EINVAL); - lua_setfield(L, -2, "ERROR_INVALID"); - - lua_pushnumber(L, REPROC_STREAM_IN); - lua_setfield(L, -2, "STREAM_STDIN"); - - lua_pushnumber(L, REPROC_STREAM_OUT); - lua_setfield(L, -2, "STREAM_STDOUT"); - - lua_pushnumber(L, REPROC_STREAM_ERR); - lua_setfield(L, -2, "STREAM_STDERR"); - - lua_pushnumber(L, REPROC_INFINITE); - lua_setfield(L, -2, "WAIT_INFINITE"); - - lua_pushnumber(L, REPROC_DEADLINE); - lua_setfield(L, -2, "WAIT_DEADLINE"); + L_SETNUM(L, -1, "REDIRECT_DEFAULT", REPROC_REDIRECT_DEFAULT); + L_SETNUM(L, -1, "REDIRECT_PIPE", REPROC_REDIRECT_PIPE); + L_SETNUM(L, -1, "REDIRECT_PARENT", REPROC_REDIRECT_PARENT); + L_SETNUM(L, -1, "REDIRECT_DISCARD", REPROC_REDIRECT_DISCARD); + L_SETNUM(L, -1, "REDIRECT_STDOUT", REPROC_REDIRECT_STDOUT); return 1; } diff --git a/src/api/regex.c b/src/api/regex.c index a5d17604..1043b1c5 100644 --- a/src/api/regex.c +++ b/src/api/regex.c @@ -74,11 +74,11 @@ static int f_pcre_match(lua_State *L) { } PCRE2_SIZE* ovector = pcre2_get_ovector_pointer(md); if (ovector[0] > ovector[1]) { - /* We must guard against patterns such as /(?=.\K)/ that use \K in an + /* We must guard against patterns such as /(?=.\K)/ that use \K in an assertion to set the start of a match later than its end. In the editor, we just detect this case and give up. */ luaL_error(L, "regex matching error: \\K was used in an assertion to " - " set the match start after its end"); + " set the match start after its end"); pcre2_match_data_free(md); return 0; } @@ -103,8 +103,8 @@ int luaopen_regex(lua_State *L) { lua_setfield(L, LUA_REGISTRYINDEX, "regex"); lua_pushnumber(L, PCRE2_ANCHORED); lua_setfield(L, -2, "ANCHORED"); - lua_pushnumber(L, PCRE2_ANCHORED) ; - lua_setfield(L, -2, "ENDANCHORED"); + lua_pushnumber(L, PCRE2_ANCHORED) ; + lua_setfield(L, -2, "ENDANCHORED"); lua_pushnumber(L, PCRE2_NOTBOL); lua_setfield(L, -2, "NOTBOL"); lua_pushnumber(L, PCRE2_NOTEOL); diff --git a/src/bundle_open.m b/src/bundle_open.m index f4f0b94c..2ba10da7 100644 --- a/src/bundle_open.m +++ b/src/bundle_open.m @@ -1,31 +1,15 @@ #import #include "lua.h" +#ifdef MACOS_USE_BUNDLE void set_macos_bundle_resources(lua_State *L) { @autoreleasepool { - /* Use resolved executablePath instead of resourcePath to allow lanching - the lite-xl binary via a symlink, like typically done by Homebrew: - - /usr/local/bin/lite-xl -> /Applications/lite-xl.app/Contents/MacOS/lite-xl - - The resourcePath returns /usr/local in this case instead of - /Applications/lite-xl.app/Contents/Resources, which makes later - access to the resource files fail. Resolving the symlink to the - executable and then the relative path to the expected directory - Resources is a workaround for starting the application from both - the launcher directly and the command line via the symlink. - */ - NSString* executable_path = [[NSBundle mainBundle] executablePath]; - char resolved_path[PATH_MAX + 16 + 1]; - realpath([executable_path UTF8String], resolved_path); - strcat(resolved_path, "/../../Resources"); - char resource_path[PATH_MAX + 1]; - realpath(resolved_path, resource_path); - lua_pushstring(L, resource_path); + NSString* resource_path = [[NSBundle mainBundle] resourcePath]; + lua_pushstring(L, [resource_path UTF8String]); lua_setglobal(L, "MACOS_RESOURCES"); }} - +#endif /* Thanks to mathewmariani, taken from his lite-macos github repository. */ void enable_momentum_scroll() { diff --git a/src/dmon.h b/src/dmon.h index db39a6c1..fdbac3f4 100644 --- a/src/dmon.h +++ b/src/dmon.h @@ -1578,8 +1578,11 @@ _DMON_PRIVATE void dmon__fsevent_callback(ConstFSEventStreamRef stream_ref, void dmon__unixpath(abs_filepath, sizeof(abs_filepath), abs_filepath)); // strip the root dir - DMON_ASSERT(strstr(abs_filepath, watch->rootdir) == abs_filepath); - dmon__strcpy(ev.filepath, sizeof(ev.filepath), abs_filepath + strlen(watch->rootdir)); + size_t len = strlen(watch->rootdir); + // FIXME: filesystems on macOS can be case sensitive or not. The check below + // ignore case but we should check if the filesystem is case sensitive. + DMON_ASSERT(strncasecmp(abs_filepath, watch->rootdir, len) == 0); + dmon__strcpy(ev.filepath, sizeof(ev.filepath), abs_filepath + len); ev.event_flags = flags; ev.event_id = event_id; diff --git a/src/main.c b/src/main.c index ccc33054..3918cf0d 100644 --- a/src/main.c +++ b/src/main.c @@ -9,10 +9,6 @@ #include #elif __linux__ #include - #include - #include - #include - #include #include #elif __APPLE__ #include @@ -24,34 +20,12 @@ SDL_Window *window; static double get_scale(void) { -#ifdef _WIN32 - float dpi; +#ifdef __APPLE__ + return 1.0; +#else + float dpi = 96.0; SDL_GetDisplayDPI(0, NULL, &dpi, NULL); return dpi / 96.0; -#elif __linux__ - SDL_SysWMinfo info; - XrmDatabase db; - XrmValue value; - char *type = NULL; - - SDL_VERSION(&info.version); - if (!SDL_GetWindowWMInfo(window, &info) - || info.subsystem != SDL_SYSWM_X11) - return 1.0; - - char *resource = XResourceManagerString(info.info.x11.display); - if (resource == NULL) - return 1.0; - - XrmInitialize(); - db = XrmGetStringDatabase(resource); - if (XrmGetResource(db, "Xft.dpi", "String", &type, &value) == False - || value.addr == NULL) - return 1.0; - - return atof(value.addr) / 96.0; -#else - return 1.0; #endif } @@ -106,8 +80,10 @@ static void init_window_icon(void) { #endif #ifdef __APPLE__ -void set_macos_bundle_resources(lua_State *L); void enable_momentum_scroll(); +#ifdef MACOS_USE_BUNDLE +void set_macos_bundle_resources(lua_State *L); +#endif #endif int main(int argc, char **argv) { @@ -168,16 +144,20 @@ init_lua: lua_setglobal(L, "EXEFILE"); #ifdef __APPLE__ - set_macos_bundle_resources(L); + lua_pushboolean(L, true); + lua_setglobal(L, "MACOS"); enable_momentum_scroll(); + #ifdef MACOS_USE_BUNDLE + set_macos_bundle_resources(L); + #endif #endif const char *init_lite_code = \ "local core\n" "xpcall(function()\n" " HOME = os.getenv('" LITE_OS_HOME "')\n" - " local exedir = EXEFILE:match(\"^(.*)" LITE_PATHSEP_PATTERN LITE_NONPATHSEP_PATTERN "$\")\n" - " local prefix = exedir:match(\"^(.*)" LITE_PATHSEP_PATTERN "bin$\")\n" + " local exedir = EXEFILE:match('^(.*)" LITE_PATHSEP_PATTERN LITE_NONPATHSEP_PATTERN "$')\n" + " local prefix = exedir:match('^(.*)" LITE_PATHSEP_PATTERN "bin$')\n" " dofile((MACOS_RESOURCES or (prefix and prefix .. '/share/lite-xl' or exedir .. '/data')) .. '/core/start.lua')\n" " core = require('core')\n" " core.init()\n" diff --git a/src/meson.build b/src/meson.build index c5e618f3..2da04fda 100644 --- a/src/meson.build +++ b/src/meson.build @@ -14,17 +14,25 @@ lite_sources = [ 'main.c', ] -if host_machine.system() == 'darwin' +lite_rc = [] +if host_machine.system() == 'windows' + windows = import('windows') + lite_rc += windows.compile_resources('../resources/icons/icon.rc') +elif host_machine.system() == 'darwin' lite_sources += 'bundle_open.m' endif +lite_include = include_directories('.') + executable('lite-xl', lite_sources + lite_rc, include_directories: [lite_include, font_renderer_include], dependencies: lite_deps, c_args: lite_cargs, + objc_args: lite_cargs, link_with: libfontrenderer, link_args: lite_link_args, + install_dir: lite_bindir, install: true, gui_app: true, ) diff --git a/subprojects/reproc.wrap b/subprojects/reproc.wrap index f1afb4fa..9ff98b7e 100644 --- a/subprojects/reproc.wrap +++ b/subprojects/reproc.wrap @@ -1,4 +1,4 @@ [wrap-git] directory = reproc url = https://github.com/franko/reproc -revision = v14.2.2-meson-1 +revision = v14.2.3-meson-1