aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.editorconfig11
-rw-r--r--.github/workflows/build.yml275
-rw-r--r--.gitignore26
-rw-r--r--README.md40
-rwxr-xr-xbuild-packages.sh350
-rwxr-xr-xbuild.sh39
-rw-r--r--changelog.md80
-rw-r--r--data/colors/textadept.lua51
-rw-r--r--data/core/commands/core.lua30
-rw-r--r--data/core/commands/doc.lua25
-rw-r--r--data/core/commands/findreplace.lua133
-rw-r--r--data/core/commands/root.lua8
-rw-r--r--data/core/commandview.lua43
-rw-r--r--data/core/common.lua48
-rw-r--r--data/core/config.lua7
-rw-r--r--data/core/contextmenu.lua218
-rw-r--r--data/core/doc/init.lua87
-rw-r--r--data/core/doc/translate.lua4
-rw-r--r--data/core/docview.lua22
-rw-r--r--data/core/init.lua188
-rw-r--r--data/core/keymap-macos.lua42
-rw-r--r--data/core/keymap.lua13
-rw-r--r--data/core/logview.lua132
-rw-r--r--data/core/nagview.lua3
-rw-r--r--data/core/object.lua11
-rw-r--r--data/core/regex.lua16
-rw-r--r--data/core/rootview.lua63
-rw-r--r--data/core/start.lua5
-rw-r--r--data/core/style.lua8
-rw-r--r--data/core/view.lua4
-rw-r--r--data/plugins/autocomplete.lua285
-rw-r--r--data/plugins/autoreload.lua2
-rw-r--r--data/plugins/contextmenu.lua239
-rw-r--r--data/plugins/detectindent.lua2
-rw-r--r--data/plugins/language_c.lua15
-rw-r--r--data/plugins/language_cpp.lua122
-rw-r--r--data/plugins/language_css.lua2
-rw-r--r--data/plugins/language_html.lua2
-rw-r--r--data/plugins/language_js.lua2
-rw-r--r--data/plugins/language_lua.lua2
-rw-r--r--data/plugins/language_md.lua2
-rw-r--r--data/plugins/language_python.lua2
-rw-r--r--data/plugins/language_xml.lua2
-rw-r--r--data/plugins/lineguide.lua2
-rw-r--r--data/plugins/macro.lua2
-rw-r--r--data/plugins/projectsearch.lua5
-rw-r--r--data/plugins/quote.lua2
-rw-r--r--data/plugins/reflow.lua2
-rw-r--r--data/plugins/scale.lua28
-rw-r--r--data/plugins/tabularize.lua2
-rw-r--r--data/plugins/toolbarview.lua2
-rw-r--r--data/plugins/treeview.lua192
-rw-r--r--data/plugins/trimwhitespace.lua2
-rw-r--r--data/plugins/workspace.lua2
-rw-r--r--docs/README.md28
-rw-r--r--docs/api/globals.lua21
-rw-r--r--docs/api/process.lua232
-rw-r--r--docs/api/regex.lua57
-rw-r--r--docs/api/renderer.lua181
-rw-r--r--docs/api/system.lua234
-rwxr-xr-xlib/font_renderer/build.sh27
-rw-r--r--lib/font_renderer/font_renderer.cpp2
-rw-r--r--lib/font_renderer/meson.build6
-rw-r--r--meson.build152
-rw-r--r--meson_options.txt3
-rw-r--r--resources/linux/org.lite_xl.lite_xl.appdata.xml33
-rw-r--r--resources/linux/org.lite_xl.lite_xl.desktop (renamed from resources/linux/lite-xl.desktop)4
-rw-r--r--resources/macos/Info.plist.in (renamed from resources/macos/Info.plist)21
-rw-r--r--resources/macos/appdmg.pngbin0 -> 6167 bytes
-rw-r--r--scripts/README.md28
-rw-r--r--scripts/appdmg.sh30
-rw-r--r--scripts/appimage.sh162
-rw-r--r--scripts/build.sh117
-rw-r--r--scripts/common.sh25
-rw-r--r--scripts/innosetup/innosetup.iss.in88
-rw-r--r--scripts/innosetup/innosetup.sh65
-rw-r--r--scripts/innosetup/litexl-55px.bmpbin0 -> 12238 bytes
-rw-r--r--scripts/innosetup/wizard-modern-image.bmpbin0 -> 52574 bytes
-rw-r--r--scripts/install-dependencies.sh74
-rw-r--r--scripts/lhelper.sh75
-rw-r--r--scripts/meson.build8
-rw-r--r--scripts/package.sh259
-rw-r--r--scripts/repackage.sh10
-rwxr-xr-xscripts/run-local10
-rw-r--r--src/api/api.h1
-rw-r--r--src/api/process.c590
-rw-r--r--src/api/regex.c8
-rw-r--r--src/bundle_open.m24
-rw-r--r--src/dmon.h7
-rw-r--r--src/main.c48
-rw-r--r--src/meson.build10
-rw-r--r--subprojects/reproc.wrap2
92 files changed, 4229 insertions, 1315 deletions
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
+ - 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:
- - 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
+ - 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*
+build*/
+.build*/
+lhelper/
+submodules/
+subprojects/lua/
+subprojects/libagg/
+subprojects/reproc/
+/appimage*
+.ccls-cache
+.lite-debug.log
.run*
-*.zip
+*.diff
+*.exe
*.tar.gz
-.lite-debug.log
-subprojects/lua
-subprojects/libagg
-subprojects/reproc
-lite-xl
+*.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 <prefix> build
+meson compile -C build
+DESTDIR="$(pwd)/lite-xl" meson install --skip-subprojects -C build
+```
+
+where `<prefix>` 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[@]}"
-}
-
-# 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
-}
-
-# 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
-}
+if [ ! -e "src/api/api.h" ]; then
+ echo "Please run this script from the root directory of Lite XL."; exit 1
+fi
-# 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
-}
+source scripts/common.sh
-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"
- 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"
+show_help() {
+ echo
+ echo "Usage: $0 <OPTIONS>"
+ 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
}
-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"
-}
+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
-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"
+ 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" "$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
-}
-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] <arch>"
- 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
-if [ -z ${use_branch+set} ]; then
- use_branch="$(git rev-parse --abbrev-ref HEAD)"
-fi
+ source scripts/build.sh \
+ ${build_dir_option[@]} \
+ ${prefix_option[@]} \
+ $debug \
+ $force_fallback \
+ $bundle \
+ $portable \
+ $pgo
-build_dir=".build-$arch"
+ source scripts/package.sh \
+ ${build_dir_option[@]} \
+ ${dest_dir_option[@]} \
+ ${prefix_option[@]} \
+ ${version_option[@]} \
+ --binary \
+ --addons \
+ $debug \
+ $appimage \
+ $dmg \
+ $innosetup \
+ $source
+}
-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,9 +426,24 @@ 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
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
+ local lh = th + style.padding.y -- for one line
+ for _, item, x, y, w in self:each_item() do
+ x = x + style.padding.x
- 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)
+ x = common.draw_text(style.font, style.dim, time, "left", x, y, w, lh)
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
+
+ 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"
-
-config.autocomplete_max_suggestions = 6
+local Doc = require "core.doc"
+
+config.plugins.autocomplete = {
+ -- Amount of characters that need to be written for autocomplete
+ min_len = 3,
+ -- The max amount of visible items
+ max_height = 6,
+ -- The max amount of scrollable items
+ max_suggestions = 100,
+}
local autocomplete = {}
+
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
-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(...)
+ 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
+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()
+
+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
--- register commands and keymap
+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<integer, string>
+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<string, string>
+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<integer, integer> 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')
+lite_cargs = []
+# 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
-reproc_subproject = subproject('reproc', default_options: ['default_library=static', 'multithreaded=false', 'reproc-cpp=false', 'examples=false'])
-reproc_dep = reproc_subproject.get_variable('reproc_dep')
+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, libx11]
+ 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)
+ 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
-
-lite_cargs = []
-if get_option('portable')
- lite_docdir = 'doc'
- lite_datadir = 'data'
+#===============================================================================
+# 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
-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
+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
-lite_rc = []
-if host_machine.system() == 'windows'
- windows = import('windows')
- lite_rc += windows.compile_resources('resources/icons/icon.rc')
-endif
+configure_file(
+ input : 'data/core/start.lua',
+ output : 'start.lua',
+ configuration : conf_data,
+ install : true,
+ install_dir : lite_datadir / 'core',
+)
-# 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'
+#===============================================================================
+# Targets
+#===============================================================================
+if not get_option('source-only')
+ subdir('lib/font_renderer')
+ subdir('src')
+ subdir('scripts')
endif
-
-subdir('lib/font_renderer')
-subdir('src')
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 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<component type="desktop">
+ <id>org.lite_xl.lite_xl</id>
+ <metadata_license>MIT</metadata_license>
+ <project_license>MIT</project_license>
+ <name>Lite XL</name>
+ <summary>A lightweight text editor written in Lua</summary>
+ <content_rating type="oars-1.0" />
+
+ <description>
+ <p>
+ 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.
+ </p>
+ </description>
+
+ <screenshots>
+ <screenshot type="default">
+ <caption>The editor window</caption>
+ <image>https://lite-xl.github.io/assets/img/screenshots/editor.png</image>
+ </screenshot>
+ </screenshots>
+
+ <url type="homepage">https://lite-xl.github.io</url>
+
+ <provides>
+ <binary>lite-xl</binary>
+ </provides>
+
+ <releases>
+ <release version="2.0.1" date="2021-08-28" />
+ </releases>
+</component>
diff --git a/resources/linux/lite-xl.desktop b/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
index cc369cd0..4d715f2f 100644
--- a/resources/macos/Info.plist
+++ b/resources/macos/Info.plist.in
@@ -2,25 +2,30 @@
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
-<key>CFBundleExecutable</key>
+ <key>CFBundleExecutable</key>
<string>lite-xl</string>
<key>CFBundleGetInfoString</key>
<string>lite-xl</string>
<key>CFBundleIconFile</key>
- <string>icon</string>
+ <string>icon.icns</string>
<key>CFBundleName</key>
- <string>lite-xl</string>
+ <string>Lite XL</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>NSHighResolutionCapable</key>
<true/>
- <key>MinimumOSVersion</key><string>10.13</string>
- <key>NSDocumentsFolderUsageDescription</key><string>To access, edit and index your projects.</string>
- <key>NSDesktopFolderUsageDescription</key><string>To access, edit and index your projects.</string>
- <key>NSDownloadsFolderUsageDescription</key><string>To access, edit and index your projects.</string>
+ <key>LSMinimumSystemVersion</key>
+ <string>10.11</string>
+ <key>NSDocumentsFolderUsageDescription</key>
+ <string>To access, edit and index your projects.</string>
+ <key>NSDesktopFolderUsageDescription</key>
+ <string>To access, edit and index your projects.</string>
+ <key>NSDownloadsFolderUsageDescription</key>
+ <string>To access, edit and index your projects.</string>
<key>CFBundleShortVersionString</key>
- <string>1.16.10</string>
+ <string>@PROJECT_VERSION@</string>
<key>NSHumanReadableCopyright</key>
<string>© 2019-2021 Francesco Abbate</string>
</dict>
</plist>
+
diff --git a/resources/macos/appdmg.png b/resources/macos/appdmg.png
new file mode 100644
index 00000000..1df7b60d
--- /dev/null
+++ b/resources/macos/appdmg.png
Binary files 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 <OPTIONS>"
+ 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 <OPTIONS>"
+ 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 <OPTIONS>"
+ 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
--- /dev/null
+++ b/scripts/innosetup/litexl-55px.bmp
Binary files differ
diff --git a/scripts/innosetup/wizard-modern-image.bmp b/scripts/innosetup/wizard-modern-image.bmp
new file mode 100644
index 00000000..cf844e09
--- /dev/null
+++ b/scripts/innosetup/wizard-modern-image.bmp
Binary files 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 <OPTIONS>"
+ 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 <OPTIONS>"
+ 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 <OPTIONS>"
+ 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,444 +5,404 @@
*/
#include <string.h>
-#include <lua.h>
-#include <lualib.h>
-#include <lauxlib.h>
-#include <reproc/reproc.h>
#include <stdbool.h>
#include <stdlib.h>
+#include <reproc/reproc.h>
+#include "api.h"
-typedef struct {
- reproc_t * process;
- lua_State* L;
+#define READ_BUF_SIZE 2048
-} process_t;
+#define L_GETTABLE(L, idx, key, conv, def) ( \
+ lua_getfield(L, idx, key), \
+ conv(L, -1, def) \
+)
-static int process_new(lua_State* L)
-{
- process_t* self = (process_t*) lua_newuserdata(
- L, sizeof(process_t)
- );
+#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)
- memset(self, 0, sizeof(process_t));
+#define L_SETNUM(L, idx, key, n) (lua_pushnumber(L, n), lua_setfield(L, idx - 1, key))
- self->process = NULL;
- self->L = L;
+#define L_RETURN_REPROC_ERROR(L, code) { \
+ lua_pushnil(L); \
+ lua_pushstring(L, reproc_strerror(code)); \
+ lua_pushnumber(L, code); \
+ return 3; \
+}
- luaL_getmetatable(L, "PROCESS");
- lua_setmetatable(L, -2);
+#define ASSERT_MALLOC(ptr) \
+ if (ptr == NULL) \
+ L_RETURN_REPROC_ERROR(L, REPROC_ENOMEM)
- return 1;
+#define ASSERT_REPROC_ERRNO(L, code) { \
+ if (code < 0) \
+ L_RETURN_REPROC_ERROR(L, code) \
}
-static int process_strerror(lua_State* L)
-{
- int error_code = luaL_checknumber(L, 1);
+typedef struct {
+ reproc_t * process;
+ bool running;
+ int returncode;
+} process_t;
- if(error_code){
- lua_pushstring(
- L,
- reproc_strerror(error_code)
- );
- } else {
- lua_pushnil(L);
+// this function should be called instead of reproc_wait
+static int poll_process(process_t* proc, int timeout)
+{
+ int ret = reproc_wait(proc->process, timeout);
+ if (ret != REPROC_ETIMEDOUT) {
+ proc->running = false;
+ proc->returncode = ret;
}
-
- return 1;
+ return ret;
}
-static int process_gc(lua_State* L)
+static int kill_process(process_t* proc)
{
- process_t* self = (process_t*) luaL_checkudata(L, 1, "PROCESS");
+ int ret = reproc_stop(
+ proc->process,
+ (reproc_stop_actions) {
+ {REPROC_STOP_KILL, 0},
+ {REPROC_STOP_TERMINATE, 0},
+ {REPROC_STOP_NOOP, 0}
+ }
+ );
- if(self->process){
- reproc_kill(self->process);
- reproc_destroy(self->process);
- self->process = NULL;
+ if (ret != REPROC_ETIMEDOUT) {
+ proc->running = false;
+ proc->returncode = ret;
}
- return 0;
+ return ret;
}
static int process_start(lua_State* L)
{
- process_t* self = (process_t*) lua_touserdata(L, 1);
-
+ 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);
- char* path = NULL;
- size_t path_len = 0;
+ int cmd_len = lua_rawlen(L, 1);
+ const char** cmd = malloc(sizeof(char *) * (cmd_len + 1));
+ ASSERT_MALLOC(cmd);
+ cmd[cmd_len] = NULL;
- if(lua_type(L, 3) == LUA_TSTRING){
- path = (char*) lua_tolstring(L, 3, &path_len);
- }
+ for(int i = 0; i < cmd_len; i++) {
+ lua_rawgeti(L, 1, i + 1);
- size_t deadline = 0;
+ cmd[i] = luaL_checkstring(L, -1);
+ lua_pop(L, 1);
+ }
- if(lua_type(L, 4) == LUA_TNUMBER){
- deadline = lua_tonumber(L, 4);
+ 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;
}
- size_t table_len = luaL_len(L, 2);
- char* command[table_len+1];
- command[table_len] = NULL;
+ // env
+ luaL_getsubtable(L, 2, "env");
+ const char **env = NULL;
+ int env_len = 0;
- int i;
- for(i=1; i<=table_len; i++){
- lua_pushnumber(L, i);
- lua_gettable(L, 2);
+ lua_pushnil(L);
+ while (lua_next(L, -2) != 0) {
+ env_len++;
+ lua_pop(L, 1);
+ }
- command[i-1] = (char*) lua_tostring(L, -1);
+ if (env_len > 0) {
+ env = malloc(sizeof(char*) * (env_len + 1));
+ env[env_len] = NULL;
- lua_remove(L, -1);
- }
+ 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
- if(self->process){
- reproc_kill(self->process);
- reproc_destroy(self->process);
+ env[i++] = luaL_checkstring(L, -1);
+ lua_pop(L, 1);
+ }
}
- self->process = reproc_new();
-
+ reproc_t* proc = reproc_new();
int out = reproc_start(
- self->process,
- (const char* const*) command,
- (reproc_options){
- .working_directory = path,
+ proc,
+ (const char* const*) cmd,
+ (reproc_options) {
+ .working_directory = cwd,
.deadline = deadline,
- .nonblocking=true,
- .redirect.err.type=REPROC_REDIRECT_PIPE
+ .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) {
- lua_pushboolean(L, 1);
- }
- else {
- reproc_destroy(self->process);
- self->process = NULL;
- lua_pushnumber(L, out);
+ 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;
}
-static int process_pid(lua_State* L)
+static int process_strerror(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);
- }
+ int error_code = luaL_checknumber(L, 1);
+ if (error_code < 0)
+ lua_pushstring(L, reproc_strerror(error_code));
+ else
+ lua_pushnil(L);
+
return 1;
}
-static int process_read(lua_State* L)
+static int f_gc(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);
- }
+ process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS);
- int out = 0;
- uint8_t buffer[read_size];
-
- int runs;
- for (runs=0; runs<tries; runs++){
- out = reproc_read(
- self->process,
- 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);
+ if(self->process) {
+ kill_process(self);
+ reproc_destroy(self->process);
+ self->process = NULL;
}
- return 1;
+ return 0;
}
-static int process_read_errors(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);
- 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);
- }
+ lua_pushliteral(L, API_TYPE_PROCESS);
+ return 1;
+}
- int out = 0;
- uint8_t buffer[read_size];
+static int f_pid(lua_State* L)
+{
+ process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS);
- int runs;
- for (runs=0; runs<tries; runs++){
- out = reproc_read(
- self->process,
- REPROC_STREAM_ERR,
- buffer,
- read_size
- );
+ lua_pushnumber(L, reproc_pid(self->process));
+ return 1;
+}
- if (out >= 0)
- break;
- }
+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 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 {
+ if (self->running)
lua_pushnil(L);
- }
+ else
+ lua_pushnumber(L, ret);
return 1;
}
-static int process_write(lua_State* L)
+static int g_read(lua_State* L, int stream)
{
- process_t* self = (process_t*) lua_touserdata(L, 1);
+ process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS);
+ unsigned long read_size = luaL_optunsigned(L, 2, READ_BUF_SIZE);
- if(self->process){
- size_t data_size = 0;
- const char* data = luaL_checklstring(L, 2, &data_size);
+ luaL_Buffer b;
+ uint8_t* buffer = (uint8_t*) luaL_buffinitsize(L, &b, read_size);
- int out = 0;
+ int out = reproc_read(
+ self->process,
+ stream,
+ buffer,
+ read_size
+ );
- out = reproc_write(
- self->process,
- (uint8_t*) data,
- data_size
- );
+ if (out >= 0)
+ luaL_addsize(&b, out);
+ luaL_pushresult(&b);
- 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);
+ if (out == REPROC_EPIPE) {
+ kill_process(self);
+ ASSERT_REPROC_ERRNO(L, out);
}
return 1;
}
-static int process_close_stream(lua_State* L)
+static int f_read_stdout(lua_State* L)
+{
+ return g_read(L, REPROC_STREAM_OUT);
+}
+
+static int f_read_stderr(lua_State* L)
+{
+ return g_read(L, REPROC_STREAM_ERR);
+}
+
+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){
- size_t stream = luaL_checknumber(L, 2);
+ return g_read(L, stream);
+}
+
+static int f_write(lua_State* L)
+{
+ process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS);
- int out = reproc_close(self->process, stream);
+ size_t data_size = 0;
+ const char* data = luaL_checklstring(L, 2, &data_size);
- lua_pushnumber(L, out);
- } else {
- lua_pushnumber(L, REPROC_EINVAL);
+ 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_wait(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 timeout = luaL_checknumber(L, 2);
+ int stream = luaL_checknumber(L, 2);
+ int out = reproc_close(self->process, stream);
+ ASSERT_REPROC_ERRNO(L, out);
- 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);
- }
+ lua_pushboolean(L, 1);
+ return 1;
+}
+static int f_wait(lua_State* L)
+{
+ 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);
-
- lua_pushnumber(L, REPROC_EPIPE);
- lua_setfield(L, -2, "ERROR_PIPE");
-
- lua_pushnumber(L, REPROC_EWOULDBLOCK);
- lua_setfield(L, -2, "ERROR_WOULDBLOCK");
-
- 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");
+ // 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);
+
+ L_SETNUM(L, -1, "WAIT_INFINITE", REPROC_INFINITE);
+ L_SETNUM(L, -1, "WAIT_DEADLINE", REPROC_DEADLINE);
+
+ 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);
+
+ 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 <Foundation/Foundation.h>
#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 <windows.h>
#elif __linux__
#include <unistd.h>
- #include <SDL_syswm.h>
- #include <X11/Xlib.h>
- #include <X11/Xatom.h>
- #include <X11/Xresource.h>
#include <signal.h>
#elif __APPLE__
#include <mach-o/dyld.h>
@@ -24,34 +20,12 @@
SDL_Window *window;
static double get_scale(void) {
-#ifdef _WIN32
- float dpi;
- 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)
+#ifdef __APPLE__
return 1.0;
-
- return atof(value.addr) / 96.0;
#else
- return 1.0;
+ float dpi = 96.0;
+ SDL_GetDisplayDPI(0, NULL, &dpi, NULL);
+ return dpi / 96.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