Merge branch 'master' into lineguide-config

This commit is contained in:
Takase 2021-11-17 11:59:59 +08:00 committed by GitHub
commit d7c309d8e2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
112 changed files with 5428 additions and 4429 deletions

View File

@ -6,3 +6,6 @@ indent_size = 2
indent_style = space indent_style = space
insert_final_newline = true insert_final_newline = true
trim_trailing_whitespace = true trim_trailing_whitespace = true
[meson.build]
indent_size = 4

35
.github/labeler.yml vendored Normal file
View File

@ -0,0 +1,35 @@
"Category: CI":
- .github/workflows/*
"Category: Meta":
- ./*
- .github/*
- .github/ISSUE_TEMPLATE/*
- .github/PULL_REQUEST_TEMPLATE/*
- .gitignore
"Category: Build System":
- meson.build
- meson_options.txt
- subprojects/*
"Category: Documentation":
- docs/**/*
"Category: Resources":
- resources/**/*
"Category: Themes":
- data/colors/*
"Category: Lua Core":
- data/core/**/*
"Category: Fonts":
- data/fonts/*
"Category: Plugins":
- data/plugins/*
"Category: C Core":
- src/**/*

16
.github/workflows/auto_labeler_pr.yml vendored Normal file
View File

@ -0,0 +1,16 @@
name: "Pull Request Labeler"
on:
- pull_request_target
permissions:
pull-requests: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Apply Type Label
uses: actions/labeler@v3
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
sync-labels: "" # works around actions/labeler#104

View File

@ -1,16 +1,44 @@
name: CI name: CI
# All builds use lhelper only for releases,
# otherwise for normal builds dependencies are dynamically linked.
on: on:
push: push:
branches: branches:
- '*' - '*'
# tags:
# - 'v[0-9]*'
pull_request: pull_request:
branches: branches:
- '*' - '*'
jobs: jobs:
build-linux: archive_source_code:
name: Build Linux name: Source Code Tarball
runs-on: ubuntu-18.04
# Only on tags/releases
if: startsWith(github.ref, 'refs/tags/')
steps:
- uses: actions/checkout@v2
- name: Python Setup
uses: actions/setup-python@v2
with:
python-version: 3.6
- name: Install Dependencies
run: |
sudo apt-get install -qq ninja-build
pip3 install meson
- name: Package
shell: bash
run: bash scripts/package.sh --version ${GITHUB_REF##*/} --debug --source
- uses: actions/upload-artifact@v2
with:
name: Source Code Tarball
path: "lite-xl-*-src.tar.gz"
build_linux:
name: Linux
runs-on: ubuntu-18.04 runs-on: ubuntu-18.04
strategy: strategy:
matrix: matrix:
@ -21,48 +49,204 @@ jobs:
CC: ${{ matrix.config.cc }} CC: ${{ matrix.config.cc }}
CXX: ${{ matrix.config.cxx }} CXX: ${{ matrix.config.cxx }}
steps: steps:
- uses: actions/checkout@v2 - name: Set Environment Variables
- name: Set up Python if: ${{ matrix.config.cc == 'gcc' }}
uses: actions/setup-python@v2 run: |
with: echo "$HOME/.local/bin" >> "$GITHUB_PATH"
python-version: 3.6 echo "INSTALL_REF=${GITHUB_REF##*/}" >> "$GITHUB_ENV"
- name: Install dependencies echo "INSTALL_NAME=lite-xl-${GITHUB_REF##*/}-linux-$(uname -m)" >> "$GITHUB_ENV"
run: | - uses: actions/checkout@v2
sudo apt-get install -qq libsdl2-dev libfreetype6 ninja-build - name: Python Setup
pip3 install meson uses: actions/setup-python@v2
- name: Build package with:
run: bash build-packages.sh x86-64 python-version: 3.6
- name: upload packages - name: Update Packages
uses: actions/upload-artifact@v2 run: sudo apt-get update
with: - name: Install Dependencies
name: Ubuntu Package if: ${{ !startsWith(github.ref, 'refs/tags/') }}
path: lite-xl-linux-*.tar.gz 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: build_macos:
name: Build Mac OS X name: macOS (x86_64)
runs-on: macos-10.15 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: strategy:
matrix: matrix:
config: msystem: [MINGW32, MINGW64]
# - { name: "GCC", cc: gcc-10, cxx: g++-10 } defaults:
- { name: "clang", cc: clang, cxx: clang++ } run:
env: shell: msys2 {0}
CC: ${{ matrix.config.cc }}
CXX: ${{ matrix.config.cxx }}
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python - uses: msys2/setup-msys2@v2
uses: actions/setup-python@v2 with:
with: #msystem: MINGW64
python-version: 3.9 msystem: ${{ matrix.msystem }}
- name: Install dependencies update: true
run: | install: >-
pip3 install meson base-devel
brew install ninja sdl2 git
- name: Build package zip
run: bash build-packages.sh x86-64 - name: Set Environment Variables
- name: upload packages run: |
uses: actions/upload-artifact@v2 echo "$HOME/.local/bin" >> "$GITHUB_PATH"
with: echo "INSTALL_NAME=lite-xl-${GITHUB_REF##*/}-windows-$(uname -m)" >> "$GITHUB_ENV"
name: Mac OS X Package echo "INSTALL_REF=${GITHUB_REF##*/}" >> "$GITHUB_ENV"
path: lite-xl-macosx-*.zip - name: Install Dependencies
if: ${{ !startsWith(github.ref, 'refs/tags/') }}
run: bash scripts/install-dependencies.sh --debug
- name: Install Release Dependencies
if: ${{ startsWith(github.ref, 'refs/tags/') }}
run: bash scripts/install-dependencies.sh --debug --lhelper
- name: Build
run: |
bash --version
bash scripts/build.sh --debug --forcefallback
- name: Error Logs
if: failure()
run: |
mkdir ${INSTALL_NAME}
cp /usr/var/lhenv/lite-xl/logs/* ${INSTALL_NAME}
tar czvf ${INSTALL_NAME}.tar.gz ${INSTALL_NAME}
- name: Package
run: bash scripts/package.sh --version ${INSTALL_REF} --debug --addons --binary
- name: Build Installer
if: ${{ startsWith(github.ref, 'refs/tags/') }}
run: bash scripts/innosetup/innosetup.sh --debug
- name: Upload Artifacts
uses: actions/upload-artifact@v2
with:
name: Windows Artifacts
path: |
LiteXL*.exe
${{ env.INSTALL_NAME }}.zip
- name: Upload Error Logs
uses: actions/upload-artifact@v2
if: failure()
with:
name: Error Logs
path: ${{ env.INSTALL_NAME }}.tar.gz
deploy:
name: Deployment
runs-on: ubuntu-18.04
# if: startsWith(github.ref, 'refs/tags/')
if: false
needs:
- archive_source_code
- build_linux
- build_macos
- build_windows_msys2
steps:
- name: Set Environment Variables
run: echo "INSTALL_REF=${GITHUB_REF##*/}" >> "$GITHUB_ENV"
- uses: actions/download-artifact@v2
with:
name: Linux Artifacts
- uses: actions/download-artifact@v2
with:
name: macOS DMG Image
- uses: actions/download-artifact@v2
with:
name: Source Code Tarball
- uses: actions/download-artifact@v2
with:
name: Windows Artifacts
- name: Display File Information
shell: bash
run: ls -lR
# Note: not using `actions/create-release@v1`
# because it cannot update an existing release
# see https://github.com/actions/create-release/issues/29
- uses: softprops/action-gh-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ env.INSTALL_REF }}
name: Release ${{ env.INSTALL_REF }}
draft: false
prerelease: false
files: |
lite-xl-${{ env.INSTALL_REF }}-*
LiteXL*.AppImage
LiteXL*.exe

29
.gitignore vendored
View File

@ -1,13 +1,20 @@
build* build*/
.build* .build*/
.run* lhelper/
*.zip submodules/
*.tar.gz subprojects/lua/
.lite-debug.log subprojects/reproc/
subprojects/lua /appimage*
subprojects/libagg
subprojects/reproc
lite-xl
error.txt
.ccls-cache .ccls-cache
.lite-debug.log
.run*
*.diff
*.exe
*.tar.gz
*.zip
*.DS_Store
*App*
compile_commands.json compile_commands.json
error.txt
lite-xl*
LiteXL*

View File

@ -1,17 +1,18 @@
# Lite XL # Lite XL
[![CI]](https://github.com/lite-xl/lite-xl/actions/workflows/build.yml)
[![Discord Badge Image]](https://discord.gg/RWzqC3nx7K) [![Discord Badge Image]](https://discord.gg/RWzqC3nx7K)
![screenshot-dark] ![screenshot-dark]
A lightweight text editor written in Lua, adapted from [lite]. 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 plugins]** — Add additional functionality, adapted for Lite XL.
* **[Get color themes]** — Add additional colors themes. * **[Get color themes]** — Add additional colors themes.
Please refer to our [website] for the user and developer documentation, 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, Lite XL has support for high DPI display on Windows and Linux and,
since 1.16.7 release, it supports **retina displays** on macOS. since 1.16.7 release, it supports **retina displays** on macOS.
@ -42,6 +43,91 @@ the [plugins repository] or in the [Lite XL plugins repository].
Additional color themes can be found in the [colors repository]. Additional color themes can be found in the [colors repository].
These color themes are bundled with all releases of Lite XL by default. 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.
## Installing Prebuilt
Head over to [releases](https://github.com/lite-xl/lite-xl/releases) and download the version for your operating system.
### Linux
Unzip the file and `cd` into the `lite-xl` directory:
```sh
tar -xzf <file>
cd lite-xl
```
To run lite-xl without installing:
```sh
cd bin
./lite-xl
```
To install lite-xl copy files over into appropriate directories:
```sh
mkdir -p $HOME/.local/bin && cp bin/lite-xl $HOME/.local/bin
cp -r share $HOME/.local
```
If `$HOME/.local/bin` is not in PATH:
```sh
echo -e 'export PATH=$PATH:$HOME/.local/bin' >> $HOME/.bashrc
```
To get the icon to show up in app launcher:
```sh
xdg-desktop-menu forceupdate
```
You may need to logout and login again to see icon in app launcher.
To uninstall just run:
```sh
rm -f $HOME/.local/bin/lite-xl
rm -rf $HOME/.local/share/icons/hicolor/scalable/apps/lite-xl.svg \
$HOME/.local/share/applications/org.lite_xl.lite_xl.desktop \
$HOME/.local/share/metainfo/org.lite_xl.lite_xl.appdata.xml \
$HOME/.local/share/lite-xl
```
## Contributing ## Contributing
Any additional functionality that can be added through a plugin should be done Any additional functionality that can be added through a plugin should be done
@ -60,11 +146,12 @@ the terms of the MIT license. See [LICENSE] for details.
See the [licenses] file for details on licenses used by the required dependencies. 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 [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 [screenshot-dark]: https://user-images.githubusercontent.com/433545/111063905-66943980-84b1-11eb-9040-3876f1133b20.png
[lite]: https://github.com/rxi/lite [lite]: https://github.com/rxi/lite
[website]: https://lite-xl.github.io [website]: https://lite-xl.github.io
[build]: https://lite-xl.github.io/en/build [build]: https://lite-xl.github.io/en/documentation/build/
[Get Lite XL]: https://github.com/franko/lite-xl/releases/latest [Get Lite XL]: https://github.com/franko/lite-xl/releases/latest
[Get plugins]: https://github.com/franko/lite-plugins [Get plugins]: https://github.com/franko/lite-plugins
[Get color themes]: https://github.com/rxi/lite-colors [Get color themes]: https://github.com/rxi/lite-colors

View File

@ -1,216 +1,164 @@
#!/bin/bash #!/bin/bash
set -e
# strip-components is normally set to 1 to strip the initial "data" from the if [ ! -e "src/api/api.h" ]; then
# directory path. echo "Please run this script from the root directory of Lite XL."; exit 1
copy_directory_from_repo () { fi
local tar_options=()
if [[ $1 == --strip-components=* ]]; then source scripts/common.sh
tar_options+=($1)
shift show_help() {
fi echo
local dirname="$1" echo "Usage: $0 <OPTIONS>"
local destdir="$2" echo
git archive "$use_branch" "$dirname" --format=tar | tar xf - -C "$destdir" "${tar_options[@]}" echo "Common options:"
echo
echo "-h --help Show this help and exit."
echo "-b --builddir DIRNAME Set the name of the build directory (not path)."
echo " Default: '$(get_default_build_dir)'."
echo "-p --prefix PREFIX Install directory prefix."
echo " Default: '/'."
echo " --debug Debug this script."
echo
echo "Build options:"
echo
echo "-f --forcefallback Force to build subprojects dependencies statically."
echo "-B --bundle Create an App bundle (macOS only)"
echo "-P --portable Create a portable package."
echo "-O --pgo Use profile guided optimizations (pgo)."
echo " Requires running the application iteractively."
echo
echo "Package options:"
echo
echo "-d --destdir DIRNAME Set the name of the package directory (not path)."
echo " Default: 'lite-xl'."
echo "-v --version VERSION Sets the version on the package name."
echo "-A --appimage Create an AppImage (Linux only)."
echo "-D --dmg Create a DMG disk image (macOS only)."
echo " Requires NPM and AppDMG."
echo "-I --innosetup Create an InnoSetup installer (Windows only)."
echo "-S --source Create a source code package,"
echo " including subprojects dependencies."
echo
} }
# Check if build directory is ok to be used to build. main() {
build_dir_is_usable () { local build_dir
local build="$1" local build_dir_option=()
if [[ $build == */* || -z "$build" ]]; then local dest_dir
echo "invalid build directory, no path allowed: \"$build\"" local dest_dir_option=()
return 1 local prefix
fi local prefix_option=()
git ls-files --error-unmatch "$build" &> /dev/null local version
if [ $? == 0 ]; then local version_option=()
echo "invalid path, \"$build\" is under revision control" local debug
return 1 local force_fallback
fi local appimage
} local bundle
local innosetup
local portable
local pgo
# Ordinary release build for i in "$@"; do
lite_build () { case $i in
local build="$1" -h|--help)
build_dir_is_usable "$build" || exit 1 show_help
rm -fr "$build" exit 0
meson setup --buildtype=release "$build" || exit 1 ;;
ninja -C "$build" || exit 1 -b|--builddir)
} build_dir="$2"
shift
# Build using Profile Guided Optimizations (PGO) shift
lite_build_pgo () { ;;
local build="$1" -d|--destdir)
build_dir_is_usable "$build" || exit 1 dest_dir="$2"
rm -fr "$build" shift
meson setup --buildtype=release -Db_pgo=generate "$build" || exit 1 shift
ninja -C "$build" || exit 1 ;;
copy_directory_from_repo data "$build/src" -f|--forcefallback)
"$build/src/lite-xl" force_fallback="--forcefallback"
meson configure -Db_pgo=use "$build" shift
ninja -C "$build" || exit 1 ;;
} -p|--prefix)
prefix="$2"
lite_build_package_windows () { shift
local portable="-msys" shift
if [ "$1" == "-portable" ]; then ;;
portable="" -v|--version)
shift version="$2"
fi shift
local build="$1" shift
local arch="$2" ;;
local os="win" -A|--appimage)
local pdir=".package-build/lite-xl" appimage="--appimage"
if [ -z "$portable" ]; then shift
local bindir="$pdir" ;;
local datadir="$pdir/data" -B|--bundle)
else bundle="--bundle"
local bindir="$pdir/bin" shift
local datadir="$pdir/share/lite-xl" ;;
fi -D|--dmg)
mkdir -p "$bindir" dmg="--dmg"
mkdir -p "$datadir" shift
for module_name in core plugins colors fonts; do ;;
copy_directory_from_repo --strip-components=1 "data/$module_name" "$datadir" -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 done
for module_name in plugins colors; do
cp -r "$build/third/data/$module_name" "$datadir"
done
cp "$build/src/lite-xl.exe" "$bindir"
strip --strip-all "$bindir/lite-xl.exe"
pushd ".package-build"
local package_name="lite-xl-$os-$arch$portable.zip"
zip "$package_name" -r "lite-xl"
mv "$package_name" ..
popd
rm -fr ".package-build"
echo "created package $package_name"
}
lite_build_package_macos () { if [[ -n $1 ]]; then
local build="$1" show_help
local arch="$2"
local os="macos"
local appdir=".package-build/lite-xl.app"
local bindir="$appdir/Contents/MacOS"
local datadir="$appdir/Contents/Resources"
mkdir -p "$bindir" "$datadir"
for module_name in core plugins colors fonts; do
copy_directory_from_repo --strip-components=1 "data/$module_name" "$datadir"
done
for module_name in plugins colors; do
cp -r "$build/third/data/$module_name" "$datadir"
done
cp resources/icons/icon.icns "$appdir/Contents/Resources/icon.icns"
cp resources/macos/Info.plist "$appdir/Contents/Info.plist"
cp "$build/src/lite-xl" "$bindir/lite-xl"
strip "$bindir/lite-xl"
pushd ".package-build"
local package_name="lite-xl-$os-$arch.zip"
zip "$package_name" -r "lite-xl.app"
mv "$package_name" ..
popd
rm -fr ".package-build"
echo "created package $package_name"
}
lite_build_package_linux () {
local portable=""
if [ "$1" == "-portable" ]; then
portable="-portable"
shift
fi
local build="$1"
local arch="$2"
local os="linux"
local pdir=".package-build/lite-xl"
if [ "$portable" == "-portable" ]; then
local bindir="$pdir"
local datadir="$pdir/data"
else
local bindir="$pdir/bin"
local datadir="$pdir/share/lite-xl"
fi
mkdir -p "$bindir"
mkdir -p "$datadir"
for module_name in core plugins colors fonts; do
copy_directory_from_repo --strip-components=1 "data/$module_name" "$datadir"
done
for module_name in plugins colors; do
cp -r "$build/third/data/$module_name" "$datadir"
done
cp "$build/src/lite-xl" "$bindir"
strip "$bindir/lite-xl"
if [ -z "$portable" ]; then
mkdir -p "$pdir/share/applications" "$pdir/share/icons/hicolor/scalable/apps"
cp "resources/linux/lite-xl.desktop" "$pdir/share/applications"
cp "resources/icons/lite-xl.svg" "$pdir/share/icons/hicolor/scalable/apps/lite-xl.svg"
fi
pushd ".package-build"
local package_name="lite-xl-$os-$arch$portable.tar.gz"
tar czf "$package_name" "lite-xl"
mv "$package_name" ..
popd
rm -fr ".package-build"
echo "created package $package_name"
}
lite_build_package () {
if [[ "$OSTYPE" == msys || "$OSTYPE" == win32 ]]; then
lite_build_package_windows "$@"
elif [[ "$OSTYPE" == "darwin"* ]]; then
lite_build_package_macos "$@"
elif [[ "$OSTYPE" == "linux"* || "$OSTYPE" == "freebsd"* ]]; then
lite_build_package_linux "$@"
else
echo "Unknown OS type \"$OSTYPE\""
exit 1 exit 1
fi fi
if [[ -n $build_dir ]]; then build_dir_option=("--builddir" "${build_dir}"); fi
if [[ -n $dest_dir ]]; then dest_dir_option=("--destdir" "${dest_dir}"); fi
if [[ -n $prefix ]]; then prefix_option=("--prefix" "${prefix}"); fi
if [[ -n $version ]]; then version_option=("--version" "${version}"); fi
source scripts/build.sh \
${build_dir_option[@]} \
${prefix_option[@]} \
$debug \
$force_fallback \
$bundle \
$portable \
$pgo
source scripts/package.sh \
${build_dir_option[@]} \
${dest_dir_option[@]} \
${prefix_option[@]} \
${version_option[@]} \
--binary \
--addons \
$debug \
$appimage \
$dmg \
$innosetup \
$source
} }
lite_copy_third_party_modules () { main "$@"
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 [ -z ${use_branch+set} ]; then
use_branch="$(git rev-parse --abbrev-ref HEAD)"
fi
build_dir=".build-$arch"
if [ -z ${pgo+set} ]; then
lite_build "$build_dir"
else
lite_build_pgo "$build_dir"
fi
lite_copy_third_party_modules "$build_dir"
lite_build_package "$build_dir" "$arch"
if [[ ! ( "$OSTYPE" == "linux"* || "$OSTYPE" == "freebsd"* || "$OSTYPE" == "darwin"* ) ]]; then
lite_build_package -portable "$build_dir" "$arch"
fi

View File

@ -1,5 +1,85 @@
This files document the changes done in Lite XL for each release. 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 ### 1.16.11
When opening directories with too many files lite-xl now keep diplaying files and directories in the treeview. When opening directories with too many files lite-xl now keep diplaying files and directories in the treeview.

51
data/colors/textadept.lua Normal file
View File

@ -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) }

View File

@ -42,10 +42,10 @@ function command.get_all_valid()
end end
local function perform(name) local function perform(name, ...)
local cmd = command.map[name] local cmd = command.map[name]
if cmd and cmd.predicate() then if cmd and cmd.predicate(...) then
cmd.perform() cmd.perform(...)
return true return true
end end
return false return false

View File

@ -104,11 +104,20 @@ command.add(nil, {
end, function (text) end, function (text)
return common.home_encode_list(common.path_suggest(common.home_expand(text))) return common.home_encode_list(common.path_suggest(common.home_expand(text)))
end, nil, function(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 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 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 else
return true return true
end end
@ -138,6 +147,10 @@ command.add(nil, {
end, end,
["core:change-project-folder"] = function() ["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) core.command_view:enter("Change Project Folder", function(text, item)
text = system.absolute_path(common.home_expand(item and item.text or text)) text = system.absolute_path(common.home_expand(item and item.text or text))
if text == core.project_dir then return end if text == core.project_dir then return end
@ -146,11 +159,15 @@ command.add(nil, {
core.error("Cannot open folder %q", text) core.error("Cannot open folder %q", text)
return return
end 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, suggest_directory)
end, end,
["core:open-project-folder"] = function() ["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) core.command_view:enter("Open Project", function(text, item)
text = common.home_expand(item and item.text or text) text = common.home_expand(item and item.text or text)
local path_stat = system.get_file_info(text) local path_stat = system.get_file_info(text)

View File

@ -43,7 +43,12 @@ local function append_line_if_last_line(line)
end end
local function save(filename) 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 local saved_filename = doc().filename
core.log("Saved \"%s\"", saved_filename) core.log("Saved \"%s\"", saved_filename)
end end
@ -62,13 +67,14 @@ local function cut_or_copy(delete)
doc().cursor_clipboard[idx] = "" doc().cursor_clipboard[idx] = ""
end end
end end
doc().cursor_clipboard["full"] = full_text
system.set_clipboard(full_text) system.set_clipboard(full_text)
end end
local function split_cursor(direction) local function split_cursor(direction)
local new_cursors = {} local new_cursors = {}
for _, line1, col1 in doc():get_selections() do 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 }) table.insert(new_cursors, { line1 + direction, col1 })
end end
end end
@ -76,6 +82,16 @@ local function split_cursor(direction)
core.blink_reset() core.blink_reset()
end end
local function set_cursor(x, y, type)
local line, col = dv():resolve_screen_position(x, y)
doc():set_selection(line, col, line, col)
if type == "word" or type == "lines" then
command.perform("doc:select-" .. type)
end
dv().mouse_selecting = { line, col }
core.blink_reset()
end
local commands = { local commands = {
["doc:undo"] = function() ["doc:undo"] = function()
doc():undo() doc():undo()
@ -94,8 +110,13 @@ local commands = {
end, end,
["doc:paste"] = function() ["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 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) doc():text_input(value:gsub("\r", ""), idx)
end end
end, end,
@ -157,16 +178,6 @@ local commands = {
doc():set_selection(line, col) doc():set_selection(line, col)
end, end,
["doc:indent"] = function()
for idx, line1, col1, line2, col2 in doc_multiline_selections(true) do
local l1, c1, l2, c2 = doc():indent_text(false, line1, col1, line2, col2)
if l1 then
doc():set_selections(idx, l1, c1, l2, c2)
end
end
end,
["doc:select-lines"] = function() ["doc:select-lines"] = function()
for idx, line1, _, line2 in doc():get_selections(true) do for idx, line1, _, line2 in doc():get_selections(true) do
append_line_if_last_line(line2) append_line_if_last_line(line2)
@ -363,12 +374,14 @@ local commands = {
end end
core.command_view:set_text(old_filename) core.command_view:set_text(old_filename)
core.command_view:enter("Rename", function(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) core.log("Renamed \"%s\" to \"%s\"", old_filename, filename)
if filename ~= old_filename then if filename ~= old_filename then
os.remove(old_filename) os.remove(old_filename)
end end
end, common.path_suggest) end, function (text)
return common.home_encode_list(common.path_suggest(common.home_expand(text)))
end)
end, end,
@ -385,6 +398,30 @@ local commands = {
os.remove(filename) os.remove(filename)
core.log("Removed \"%s\"", filename) core.log("Removed \"%s\"", filename)
end, end,
["doc:select-to-cursor"] = function(x, y, clicks)
local line1, col1 = select(3, doc():get_selection())
local line2, col2 = dv():resolve_screen_position(x, y)
dv().mouse_selecting = { line1, col1 }
doc():set_selection(line2, col2, line1, col1)
end,
["doc:set-cursor"] = function(x, y)
set_cursor(x, y, "set")
end,
["doc:set-cursor-word"] = function(x, y)
set_cursor(x, y, "word")
end,
["doc:set-cursor-line"] = function(x, y, clicks)
set_cursor(x, y, "lines")
end,
["doc:split-cursor"] = function(x, y, clicks)
local line, col = dv():resolve_screen_position(x, y)
doc():add_selection(line, col, line, col)
end,
["doc:create-cursor-previous-line"] = function() ["doc:create-cursor-previous-line"] = function()
split_cursor(-1) split_cursor(-1)
@ -411,6 +448,7 @@ local translations = {
["start-of-line"] = translate.start_of_line, ["start-of-line"] = translate.start_of_line,
["end-of-line"] = translate.end_of_line, ["end-of-line"] = translate.end_of_line,
["start-of-word"] = translate.start_of_word, ["start-of-word"] = translate.start_of_word,
["start-of-indentation"] = translate.start_of_indentation,
["end-of-word"] = translate.end_of_word, ["end-of-word"] = translate.end_of_word,
["previous-line"] = DocView.translate.previous_line, ["previous-line"] = DocView.translate.previous_line,
["next-line"] = DocView.translate.next_line, ["next-line"] = DocView.translate.next_line,

View File

@ -12,6 +12,7 @@ local last_finds, last_view, last_fn, last_text, last_sel
local case_sensitive = config.find_case_sensitive or false local case_sensitive = config.find_case_sensitive or false
local find_regex = config.find_regex or false local find_regex = config.find_regex or false
local found_expression
local function doc() local function doc()
return core.active_view:is(DocView) and core.active_view.doc or last_view.doc return core.active_view:is(DocView) and core.active_view.doc or last_view.doc
@ -29,43 +30,57 @@ local function get_find_tooltip()
end end
local function update_preview(sel, search_fn, text) local function update_preview(sel, search_fn, text)
local ok, line1, col1, line2, col2 = local ok, line1, col1, line2, col2 = pcall(search_fn, last_view.doc,
pcall(search_fn, last_view.doc, sel[1], sel[2], text, case_sensitive, find_regex) sel[1], sel[2], text, case_sensitive, find_regex)
if ok and line1 and text ~= "" then if ok and line1 and text ~= "" then
last_view.doc:set_selection(line2, col2, line1, col1) last_view.doc:set_selection(line2, col2, line1, col1)
last_view:scroll_to_line(line2, true) last_view:scroll_to_line(line2, true)
return true found_expression = true
else else
last_view.doc:set_selection(unpack(sel)) last_view.doc:set_selection(table.unpack(sel))
return false found_expression = false
end end
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) local function find(label, search_fn)
last_view, last_sel, last_finds = core.active_view, last_view, last_sel, last_finds = core.active_view,
{ core.active_view.doc:get_selection() }, {} { 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(table.unpack(last_sel))
found_expression = false
core.command_view:set_text(text, true) core.command_view:set_text(text, true)
core.status_view:show_tooltip(get_find_tooltip()) 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() core.status_view:remove_tooltip()
if found then if found_expression then
last_fn, last_text = search_fn, text last_fn, last_text = search_fn, text
else else
core.error("Couldn't find %q", text) core.error("Couldn't find %q", text)
last_view.doc:set_selection(unpack(last_sel)) last_view.doc:set_selection(table.unpack(last_sel))
last_view:scroll_to_make_visible(unpack(last_sel)) last_view:scroll_to_make_visible(table.unpack(last_sel))
end end
end, function(text) 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 last_fn, last_text = search_fn, text
return core.previous_find
end, function(explicit) end, function(explicit)
core.status_view:remove_tooltip() core.status_view:remove_tooltip()
if explicit then if explicit then
last_view.doc:set_selection(unpack(last_sel)) last_view.doc:set_selection(table.unpack(last_sel))
last_view:scroll_to_make_visible(unpack(last_sel)) last_view:scroll_to_make_visible(table.unpack(last_sel))
end end
end) end)
end end
@ -75,18 +90,25 @@ local function replace(kind, default, fn)
core.command_view:set_text(default, true) core.command_view:set_text(default, true)
core.status_view:show_tooltip(get_find_tooltip()) core.status_view:show_tooltip(get_find_tooltip())
core.command_view:set_hidden_suggestions()
core.command_view:enter("Find To Replace " .. kind, function(old) core.command_view:enter("Find To Replace " .. kind, function(old)
insert_unique(core.previous_find, old)
core.command_view:set_text(old, true) core.command_view:set_text(old, true)
local s = string.format("Replace %s %q With", kind, old) local s = string.format("Replace %s %q With", kind, old)
core.command_view:set_hidden_suggestions()
core.command_view:enter(s, function(new) core.command_view:enter(s, function(new)
core.status_view:remove_tooltip()
insert_unique(core.previous_replace, new)
local n = doc():replace(function(text) local n = doc():replace(function(text)
return fn(text, old, new) return fn(text, old, new)
end) end)
core.log("Replaced %d instance(s) of %s %q with %q", n, kind, old, new) 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() core.status_view:remove_tooltip()
end) end)
end, function() return core.previous_find end, function()
core.status_view:remove_tooltip()
end) end)
end end
@ -94,13 +116,60 @@ local function has_selection()
return core.active_view:is(DocView) and core.active_view.doc:has_selection() return core.active_view:is(DocView) and core.active_view.doc:has_selection()
end 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() ["find-replace:select-next"] = function()
local l1, c1, l2, c2 = doc():get_selection(true) local l1, c1, l2, c2 = doc():get_selection(true)
local text = doc():get_text(l1, c1, l2, c2) local text = doc():get_text(l1, c1, l2, c2)
l1, c1, l2, c2 = search.find(doc(), l2, c2, text, { wrap = true }) l1, c1, l2, c2 = search.find(doc(), l2, c2, text, { wrap = true })
if l2 then doc():set_selection(l2, c2, l1, c1) end 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", { command.add("core.docview", {
@ -112,11 +181,13 @@ command.add("core.docview", {
end, end,
["find-replace:replace"] = function() ["find-replace:replace"] = function()
replace("Text", doc():get_text(doc():get_selection(true)), function(text, old, new) 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 if not find_regex then
return text:gsub(old:gsub("%W", "%%%1"), new:gsub("%%", "%%%%"), nil) return text:gsub(old:gsub("%W", "%%%1"), new:gsub("%%", "%%%%"), nil)
end 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 return result, #matches
end) end)
end, end,
@ -180,12 +251,12 @@ command.add("core.commandview", {
["find-replace:toggle-sensitivity"] = function() ["find-replace:toggle-sensitivity"] = function()
case_sensitive = not case_sensitive case_sensitive = not case_sensitive
core.status_view:show_tooltip(get_find_tooltip()) 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, end,
["find-replace:toggle-regex"] = function() ["find-replace:toggle-regex"] = function()
find_regex = not find_regex find_regex = not find_regex
core.status_view:show_tooltip(get_find_tooltip()) 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 end
}) })

View File

@ -3,6 +3,7 @@ local style = require "core.style"
local DocView = require "core.docview" local DocView = require "core.docview"
local command = require "core.command" local command = require "core.command"
local common = require "core.common" local common = require "core.common"
local config = require "core.config"
local t = { local t = {
@ -21,9 +22,15 @@ local t = {
end, end,
["root:close-all"] = function() ["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, 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() ["root:switch-to-previous-tab"] = function()
local node = core.root_view:get_active_node() local node = core.root_view:get_active_node()
local idx = node:get_view_idx(core.active_view) local idx = node:get_view_idx(core.active_view)
@ -57,7 +64,7 @@ local t = {
table.insert(node.views, idx + 1, core.active_view) table.insert(node.views, idx + 1, core.active_view)
end end
end, end,
["root:shrink"] = function() ["root:shrink"] = function()
local node = core.root_view:get_active_node() local node = core.root_view:get_active_node()
local parent = node:get_parent_node(core.root_view.root_node) local parent = node:get_parent_node(core.root_view.root_node)
@ -70,7 +77,7 @@ local t = {
local parent = node:get_parent_node(core.root_view.root_node) local parent = node:get_parent_node(core.root_view.root_node)
local n = (parent.a == node) and 0.1 or -0.1 local n = (parent.a == node) and 0.1 or -0.1
parent.divider = common.clamp(parent.divider + n, 0.1, 0.9) parent.divider = common.clamp(parent.divider + n, 0.1, 0.9)
end, end
} }
@ -116,3 +123,14 @@ command.add(function()
local node = core.root_view:get_active_node() local node = core.root_view:get_active_node()
return not node:get_locked_size() return not node:get_locked_size()
end, t) end, t)
command.add(nil, {
["root:scroll"] = function(delta)
local view = (core.root_view.overlapping_node and core.root_view.overlapping_node.active_view) or core.active_view
if view and view.scrollable then
view.scroll.to.y = view.scroll.to.y + delta * -config.mouse_wheel_scroll
return true
end
return false
end
})

View File

@ -15,6 +15,8 @@ end
local CommandView = DocView:extend() local CommandView = DocView:extend()
CommandView.context = "application"
local max_suggestions = 10 local max_suggestions = 10
local noop = function() end local noop = function() end
@ -32,6 +34,7 @@ function CommandView:new()
self.suggestion_idx = 1 self.suggestion_idx = 1
self.suggestions = {} self.suggestions = {}
self.suggestions_height = 0 self.suggestions_height = 0
self.show_suggestions = true
self.last_change_id = 0 self.last_change_id = 0
self.gutter_width = 0 self.gutter_width = 0
self.gutter_text_brightness = 0 self.gutter_text_brightness = 0
@ -43,6 +46,11 @@ function CommandView:new()
end end
function CommandView:set_hidden_suggestions()
self.show_suggestions = false
end
function CommandView:get_name() function CommandView:get_name()
return View.get_name(self) return View.get_name(self)
end end
@ -81,10 +89,29 @@ end
function CommandView:move_suggestion_idx(dir) function CommandView:move_suggestion_idx(dir)
local n = self.suggestion_idx + dir if self.show_suggestions then
self.suggestion_idx = common.clamp(n, 1, #self.suggestions) local n = self.suggestion_idx + dir
self:complete() self.suggestion_idx = common.clamp(n, 1, #self.suggestions)
self.last_change_id = self.doc:get_change_id() 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 end
@ -132,6 +159,8 @@ function CommandView:exit(submitted, inexplicit)
self.doc:reset() self.doc:reset()
self.suggestions = {} self.suggestions = {}
if not submitted then cancel(not inexplicit) end if not submitted then cancel(not inexplicit) end
self.show_suggestions = true
self.save_suggestion = nil
end end
@ -185,7 +214,7 @@ function CommandView:update()
-- update suggestions box height -- update suggestions box height
local lh = self:get_suggestion_line_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) self:move_towards("suggestions_height", dest)
-- update suggestion cursor offset -- update suggestion cursor offset
@ -254,7 +283,9 @@ end
function CommandView:draw() function CommandView:draw()
CommandView.super.draw(self) 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 end

View File

@ -41,6 +41,11 @@ function common.lerp(a, b, t)
end end
function common.distance(x1, y1, x2, y2)
return math.sqrt(math.pow(x2-x1, 2)+math.pow(y2-y1, 2))
end
function common.color(str) function common.color(str)
local r, g, b, a = str:match("#(%x%x)(%x%x)(%x%x)") local r, g, b, a = str:match("#(%x%x)(%x%x)(%x%x)")
if r then if r then
@ -230,6 +235,12 @@ function common.basename(path)
end 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) function common.home_encode(text)
if HOME and string.find(text, HOME, 1, true) == 1 then if HOME and string.find(text, HOME, 1, true) == 1 then
local dir_pos = #HOME + 1 local dir_pos = #HOME + 1
@ -257,16 +268,6 @@ function common.home_expand(text)
end end
function common.normalize_path(filename)
if 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 function split_on_slash(s, sep_pattern)
local t = {} local t = {}
if s:match("^[/\\]") then if s:match("^[/\\]") then
@ -279,8 +280,29 @@ local function split_on_slash(s, sep_pattern)
end 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) 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 end

View File

@ -5,6 +5,7 @@ config.fps = 60
config.max_log_items = 80 config.max_log_items = 80
config.message_timeout = 5 config.message_timeout = 5
config.mouse_wheel_scroll = 50 * SCALE config.mouse_wheel_scroll = 50 * SCALE
config.scroll_past_end = true
config.file_size_limit = 10 config.file_size_limit = 10
config.ignore_files = "^%." config.ignore_files = "^%."
config.symbol_pattern = "[%a_][%w_]*" config.symbol_pattern = "[%a_][%w_]*"
@ -12,6 +13,7 @@ config.non_word_chars = " \t\n/\\()\"':,.;<>~!@#$%^&*|+=[]{}`?-"
config.undo_merge_timeout = 0.3 config.undo_merge_timeout = 0.3
config.max_undos = 10000 config.max_undos = 10000
config.max_tabs = 10 config.max_tabs = 10
config.always_show_tabs = false
config.highlight_current_line = true config.highlight_current_line = true
config.line_height = 1.2 config.line_height = 1.2
config.indent_size = 2 config.indent_size = 2
@ -22,9 +24,11 @@ config.max_project_files = 2000
config.transitions = true config.transitions = true
config.animation_rate = 1.0 config.animation_rate = 1.0
config.blink_period = 0.8 config.blink_period = 0.8
config.disable_blink = false
config.draw_whitespace = false config.draw_whitespace = false
config.borderless = false config.borderless = false
config.tab_close_button = true config.tab_close_button = true
config.max_clicks = 3
-- Disable plugin loading setting to false the config entry -- Disable plugin loading setting to false the config entry
-- of the same name. -- of the same name.
@ -32,5 +36,6 @@ config.plugins = {}
config.plugins.trimwhitespace = false config.plugins.trimwhitespace = false
config.plugins.lineguide = false config.plugins.lineguide = false
config.plugins.drawwhitespace = false
return config return config

View File

@ -49,7 +49,7 @@ function ContextMenu:register(predicate, items)
local width, height = 0, 0 --precalculate the size of context menu local width, height = 0, 0 --precalculate the size of context menu
for i, item in ipairs(items) do for i, item in ipairs(items) do
if item ~= DIVIDER then if item ~= DIVIDER then
item.info = keymap.reverse_map[item.command] item.info = keymap.get_binding(item.command)
end end
local lw, lh = get_item_size(item) local lw, lh = get_item_size(item)
width = math.max(width, lw) width = math.max(width, lw)

View File

@ -24,7 +24,7 @@ function Highlighter:new(doc)
for i = self.first_invalid_line, max do for i = self.first_invalid_line, max do
local state = (i > 1) and self.lines[i - 1].state local state = (i > 1) and self.lines[i - 1].state
local line = self.lines[i] local line = self.lines[i]
if not (line and line.init_state == state) then if not (line and line.init_state == state and line.text == self.doc.lines[i]) then
self.lines[i] = self:tokenize_line(i, state) self.lines[i] = self:tokenize_line(i, state)
end end
end end
@ -44,12 +44,25 @@ function Highlighter:reset()
self.max_wanted_line = 0 self.max_wanted_line = 0
end end
function Highlighter:invalidate(idx) function Highlighter:invalidate(idx)
self.first_invalid_line = math.min(self.first_invalid_line, idx) self.first_invalid_line = math.min(self.first_invalid_line, idx)
self.max_wanted_line = math.min(self.max_wanted_line, #self.doc.lines) self.max_wanted_line = math.min(self.max_wanted_line, #self.doc.lines)
end end
function Highlighter:insert_notify(line, n)
self:invalidate(line)
for i = 1, n do
table.insert(self.lines, line, nil)
end
end
function Highlighter:remove_notify(line, n)
self:invalidate(line)
for i = 1, n do
table.remove(self.lines, line)
end
end
function Highlighter:tokenize_line(idx, state) function Highlighter:tokenize_line(idx, state)
local res = {} local res = {}

View File

@ -17,10 +17,15 @@ local function split_lines(text)
return res return res
end end
function Doc:new(filename)
function Doc:new(filename, abs_filename, new_file)
self.new_file = new_file
self:reset() self:reset()
if filename then if filename then
self:load(filename) self:set_filename(filename, abs_filename)
if not new_file then
self:load(filename)
end
end end
end end
@ -47,16 +52,15 @@ function Doc:reset_syntax()
end end
function Doc:set_filename(filename) function Doc:set_filename(filename, abs_filename)
self.filename = filename self.filename = filename
self.abs_filename = system.absolute_path(filename) self.abs_filename = abs_filename
end end
function Doc:load(filename) function Doc:load(filename)
local fp = assert( io.open(filename, "rb") ) local fp = assert( io.open(filename, "rb") )
self:reset() self:reset()
self:set_filename(filename)
self.lines = {} self.lines = {}
for line in fp:lines() do for line in fp:lines() do
if line:byte(-1) == 13 then if line:byte(-1) == 13 then
@ -73,17 +77,20 @@ function Doc:load(filename)
end end
function Doc:save(filename) function Doc:save(filename, abs_filename)
filename = filename or assert(self.filename, "no filename set to default to") 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") ) local fp = assert( io.open(filename, "wb") )
for _, line in ipairs(self.lines) do for _, line in ipairs(self.lines) do
if self.crlf then line = line:gsub("\n", "\r\n") end if self.crlf then line = line:gsub("\n", "\r\n") end
fp:write(line) fp:write(line)
end end
fp:close() fp:close()
if filename then self:set_filename(filename, abs_filename)
self:set_filename(filename) self.new_file = false
end
self:reset_syntax() self:reset_syntax()
self:clean() self:clean()
end end
@ -95,7 +102,11 @@ end
function Doc:is_dirty() function Doc:is_dirty()
return self.clean_change_id ~= self:get_change_id() if self.new_file then
return #self.lines > 1 or #self.lines[1] > 1
else
return self.clean_change_id ~= self:get_change_id()
end
end end
@ -117,6 +128,19 @@ function Doc:get_selection(sort)
return line1, col1, line2, col2, sort return line1, col1, line2, col2, sort
end 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() function Doc:has_selection()
local line1, col1, line2, col2 = self:get_selection(false) local line1, col1, line2, col2 = self:get_selection(false)
return line1 ~= line2 or col1 ~= col2 return line1 ~= line2 or col1 ~= col2
@ -166,19 +190,21 @@ function Doc:merge_cursors(idx)
if self.selections[i] == self.selections[j] and if self.selections[i] == self.selections[j] and
self.selections[i+1] == self.selections[j+1] then self.selections[i+1] == self.selections[j+1] then
common.splice(self.selections, i, 4) common.splice(self.selections, i, 4)
common.splice(self.cursor_clipboard, i, 1)
break break
end end
end end
end end
if #self.selections <= 4 then self.cursor_clipboard = {} end
end end
local function selection_iterator(invariant, idx) local function selection_iterator(invariant, idx)
local target = invariant[3] and (idx*4 - 7) or (idx*4 + 1) local target = invariant[3] and (idx*4 - 7) or (idx*4 + 1)
if target > #invariant[1] or target <= 0 or (type(invariant[3]) == "number" and invariant[3] ~= idx - 1) then return end if target > #invariant[1] or target <= 0 or (type(invariant[3]) == "number" and invariant[3] ~= idx - 1) then return end
if invariant[2] then if invariant[2] then
return idx+(invariant[3] and -1 or 1), sort_positions(unpack(invariant[1], target, target+4)) return idx+(invariant[3] and -1 or 1), sort_positions(table.unpack(invariant[1], target, target+4))
else else
return idx+(invariant[3] and -1 or 1), unpack(invariant[1], target, target+4) return idx+(invariant[3] and -1 or 1), table.unpack(invariant[1], target, target+4)
end end
end end
@ -279,7 +305,7 @@ local function pop_undo(self, undo_stack, redo_stack, modified)
local line1, col1, line2, col2 = table.unpack(cmd) local line1, col1, line2, col2 = table.unpack(cmd)
self:raw_remove(line1, col1, line2, col2, redo_stack, cmd.time) self:raw_remove(line1, col1, line2, col2, redo_stack, cmd.time)
elseif cmd.type == "selection" then elseif cmd.type == "selection" then
self.selections = { unpack(cmd) } self.selections = { table.unpack(cmd) }
end end
modified = modified or (cmd.type ~= "selection") modified = modified or (cmd.type ~= "selection")
@ -300,6 +326,7 @@ end
function Doc:raw_insert(line, col, text, undo_stack, time) function Doc:raw_insert(line, col, text, undo_stack, time)
-- split text into lines and merge with line at insertion point -- split text into lines and merge with line at insertion point
local lines = split_lines(text) local lines = split_lines(text)
local len = #lines[#lines]
local before = self.lines[line]:sub(1, col - 1) local before = self.lines[line]:sub(1, col - 1)
local after = self.lines[line]:sub(col) local after = self.lines[line]:sub(col)
for i = 1, #lines - 1 do for i = 1, #lines - 1 do
@ -310,14 +337,22 @@ function Doc:raw_insert(line, col, text, undo_stack, time)
-- splice lines into line array -- splice lines into line array
common.splice(self.lines, line, 1, lines) 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 -- push undo
local line2, col2 = self:position_offset(line, col, #text) local line2, col2 = self:position_offset(line, col, #text)
push_undo(undo_stack, time, "selection", unpack(self.selections)) push_undo(undo_stack, time, "selection", table.unpack(self.selections))
push_undo(undo_stack, time, "remove", line, col, line2, col2) push_undo(undo_stack, time, "remove", line, col, line2, col2)
-- update highlighter and assure selection is in bounds -- update highlighter and assure selection is in bounds
self.highlighter:invalidate(line) self.highlighter:insert_notify(line, #lines - 1)
self:sanitize_selection() self:sanitize_selection()
end end
@ -325,7 +360,7 @@ end
function Doc:raw_remove(line1, col1, line2, col2, undo_stack, time) function Doc:raw_remove(line1, col1, line2, col2, undo_stack, time)
-- push undo -- push undo
local text = self:get_text(line1, col1, line2, col2) local text = self:get_text(line1, col1, line2, col2)
push_undo(undo_stack, time, "selection", unpack(self.selections)) push_undo(undo_stack, time, "selection", table.unpack(self.selections))
push_undo(undo_stack, time, "insert", line1, col1, text) push_undo(undo_stack, time, "insert", line1, col1, text)
-- get line content before/after removed text -- get line content before/after removed text
@ -334,9 +369,17 @@ function Doc:raw_remove(line1, col1, line2, col2, undo_stack, time)
-- splice line into line array -- splice line into line array
common.splice(self.lines, line1, line2 - line1 + 1, { before .. after }) 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 -- update highlighter and assure selection is in bounds
self.highlighter:invalidate(line1) self.highlighter:remove_notify(line1, line2 - line1)
self:sanitize_selection() self:sanitize_selection()
end end
@ -370,7 +413,7 @@ end
function Doc:text_input(text, idx) 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 if line1 ~= line2 or col1 ~= col2 then
self:delete_to_cursor(sidx) self:delete_to_cursor(sidx)
end end
@ -379,12 +422,7 @@ function Doc:text_input(text, idx)
end end
end end
function Doc:replace_cursor(idx, line1, col1, line2, col2, fn)
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
local old_text = self:get_text(line1, col1, line2, col2) local old_text = self:get_text(line1, col1, line2, col2)
local new_text, n = fn(old_text) local new_text, n = fn(old_text)
if old_text ~= new_text then if old_text ~= new_text then
@ -392,12 +430,27 @@ function Doc:replace(fn)
self:remove(line1, col1, line2, col2) self:remove(line1, col1, line2, col2)
if line1 == line2 and col1 == col2 then if line1 == line2 and col1 == col2 then
line2, col2 = self:position_offset(line1, col1, #new_text) 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
end end
return n return n
end end
function Doc:replace(fn)
local has_selection, n = false, 0
for idx, line1, col1, line2, col2 in self:get_selections(true) do
if line1 ~= line2 or col1 ~= col2 then
n = n + self:replace_cursor(idx, line1, col1, line2, col2, fn)
has_selection = true
end
end
if not has_selection then
self:set_selection(table.unpack(self.selections))
n = n + self:replace_cursor(1, 1, 1, #self.lines, #self.lines[#self.lines], fn)
end
return n
end
function Doc:delete_to_cursor(idx, ...) function Doc:delete_to_cursor(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) do

View File

@ -117,6 +117,10 @@ function translate.start_of_line(doc, line, col)
return line, 1 return line, 1
end 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) function translate.end_of_line(doc, line, col)
return line, math.huge return line, math.huge

View File

@ -9,6 +9,7 @@ local View = require "core.view"
local DocView = View:extend() local DocView = View:extend()
DocView.context = "session"
local function move_to_line_offset(dv, line, col, offset) local function move_to_line_offset(dv, line, col, offset)
local xo = dv.last_x_offset local xo = dv.last_x_offset
@ -97,6 +98,9 @@ end
function DocView:get_scrollable_size() function DocView:get_scrollable_size()
if not config.scroll_past_end then
return self:get_line_height() * (#self.doc.lines) + style.padding.y * 2
end
return self:get_line_height() * (#self.doc.lines - 1) + self.size.y return self:get_line_height() * (#self.doc.lines - 1) + self.size.y
end end
@ -149,14 +153,14 @@ function DocView:get_col_x_offset(line, col)
local font = style.syntax_fonts[type] or default_font local font = style.syntax_fonts[type] or default_font
for char in common.utf8_chars(text) do for char in common.utf8_chars(text) do
if column == col then if column == col then
return xoffset / font:subpixel_scale() return xoffset
end end
xoffset = xoffset + font:get_width_subpixel(char) xoffset = xoffset + font:get_width(char)
column = column + #char column = column + #char
end end
end end
return xoffset / default_font:subpixel_scale() return xoffset
end end
@ -165,14 +169,12 @@ function DocView:get_x_offset_col(line, x)
local xoffset, last_i, i = 0, 1, 1 local xoffset, last_i, i = 0, 1, 1
local default_font = self:get_font() local default_font = self:get_font()
local subpixel_scale = default_font:subpixel_scale()
local x_subpixel = subpixel_scale * x + subpixel_scale / 2
for _, type, text in self.doc.highlighter:each_token(line) do for _, type, text in self.doc.highlighter:each_token(line) do
local font = style.syntax_fonts[type] or default_font local font = style.syntax_fonts[type] or default_font
for char in common.utf8_chars(text) do for char in common.utf8_chars(text) do
local w = font:get_width_subpixel(char) local w = font:get_width(char)
if xoffset >= subpixel_scale * x then if xoffset >= x then
return (xoffset - x_subpixel > w / 2) and last_i or i return (xoffset - x > w / 2) and last_i or i
end end
xoffset = xoffset + w xoffset = xoffset + w
last_i = i last_i = i
@ -222,52 +224,6 @@ function DocView:scroll_to_make_visible(line, col)
end end
end end
local function mouse_selection(doc, clicks, line1, col1, line2, col2)
local swap = line2 < line1 or line2 == line1 and col2 <= col1
if swap then
line1, col1, line2, col2 = line2, col2, line1, col1
end
if clicks % 4 == 2 then
line1, col1 = translate.start_of_word(doc, line1, col1)
line2, col2 = translate.end_of_word(doc, line2, col2)
elseif clicks % 4 == 3 then
if line2 == #doc.lines and doc.lines[#doc.lines] ~= "\n" then
doc:insert(math.huge, math.huge, "\n")
end
line1, col1, line2, col2 = line1, 1, line2 + 1, 1
end
if swap then
return line2, col2, line1, col1
end
return line1, col1, line2, col2
end
function DocView:on_mouse_pressed(button, x, y, clicks)
local caught = DocView.super.on_mouse_pressed(self, button, x, y, clicks)
if caught then
return
end
if keymap.modkeys["shift"] then
if clicks % 2 == 1 then
local line1, col1 = select(3, self.doc:get_selection())
local line2, col2 = self:resolve_screen_position(x, y)
self.doc:set_selection(line2, col2, line1, col1)
end
else
local line, col = self:resolve_screen_position(x, y)
if keymap.modkeys["ctrl"] then
self.doc:add_selection(mouse_selection(self.doc, clicks, line, col, line, col))
else
self.doc:set_selection(mouse_selection(self.doc, clicks, line, col, line, col))
end
self.mouse_selecting = { line, col, clicks = clicks }
end
core.blink_reset()
end
function DocView:on_mouse_moved(x, y, ...) function DocView:on_mouse_moved(x, y, ...)
DocView.super.on_mouse_moved(self, x, y, ...) DocView.super.on_mouse_moved(self, x, y, ...)
@ -280,7 +236,6 @@ function DocView:on_mouse_moved(x, y, ...)
if self.mouse_selecting then if self.mouse_selecting then
local l1, c1 = self:resolve_screen_position(x, y) local l1, c1 = self:resolve_screen_position(x, y)
local l2, c2 = table.unpack(self.mouse_selecting) local l2, c2 = table.unpack(self.mouse_selecting)
local clicks = self.mouse_selecting.clicks
if keymap.modkeys["ctrl"] then if keymap.modkeys["ctrl"] then
if l1 > l2 then l1, l2 = l2, l1 end if l1 > l2 then l1, l2 = l2, l1 end
self.doc.selections = { } self.doc.selections = { }
@ -288,7 +243,7 @@ function DocView:on_mouse_moved(x, y, ...)
self.doc:set_selections(i - l1 + 1, i, math.min(c1, #self.doc.lines[i]), i, math.min(c2, #self.doc.lines[i])) self.doc:set_selections(i - l1 + 1, i, math.min(c1, #self.doc.lines[i]), i, math.min(c2, #self.doc.lines[i]))
end end
else else
self.doc:set_selection(mouse_selection(self.doc, clicks, l1, c1, l2, c2)) self.doc:set_selection(l1, c1, l2, c2)
end end
end end
end end
@ -338,16 +293,11 @@ end
function DocView:draw_line_text(idx, x, y) function DocView:draw_line_text(idx, x, y)
local default_font = self:get_font() local default_font = self:get_font()
local subpixel_scale = default_font:subpixel_scale() local tx, ty = x, y + self:get_line_text_y_offset()
local tx, ty = subpixel_scale * x, y + self:get_line_text_y_offset()
for _, type, text in self.doc.highlighter:each_token(idx) do for _, type, text in self.doc.highlighter:each_token(idx) do
local color = style.syntax[type] local color = style.syntax[type]
local font = style.syntax_fonts[type] or default_font local font = style.syntax_fonts[type] or default_font
if config.draw_whitespace then tx = renderer.draw_text(font, text, tx, ty, color)
tx = renderer.draw_text_subpixel(font, text, tx, ty, color, core.replacements, style.syntax.comment)
else
tx = renderer.draw_text_subpixel(font, text, tx, ty, color)
end
end end
end end
@ -357,6 +307,18 @@ function DocView:draw_caret(x, y)
end end
function DocView:draw_line_body(idx, x, y) function DocView:draw_line_body(idx, x, y)
-- draw highlight if any selection ends on this line
local draw_highlight = false
for lidx, line1, col1, line2, col2 in self.doc:get_selections(false) do
if line1 == idx then
draw_highlight = true
break
end
end
if draw_highlight and config.highlight_current_line and core.active_view == self then
self:draw_line_highlight(x + self.scroll.x, y)
end
-- draw selection if it overlaps this line -- draw selection if it overlaps this line
for lidx, line1, col1, line2, col2 in self.doc:get_selections(true) do for lidx, line1, col1, line2, col2 in self.doc:get_selections(true) do
if idx >= line1 and idx <= line2 then if idx >= line1 and idx <= line2 then
@ -366,14 +328,9 @@ function DocView:draw_line_body(idx, x, y)
local x1 = x + self:get_col_x_offset(idx, col1) local x1 = x + self:get_col_x_offset(idx, col1)
local x2 = x + self:get_col_x_offset(idx, col2) local x2 = x + self:get_col_x_offset(idx, col2)
local lh = self:get_line_height() local lh = self:get_line_height()
renderer.draw_rect(x1, y, x2 - x1, lh, style.selection) if x1 ~= x2 then
end renderer.draw_rect(x1, y, x2 - x1, lh, style.selection)
end end
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)
and line1 == idx and core.active_view == self then
self:draw_line_highlight(x + self.scroll.x, y)
end end
end end
@ -403,10 +360,12 @@ function DocView:draw_overlay()
local T = config.blink_period local T = config.blink_period
for _, line, col in self.doc:get_selections() do for _, line, col in self.doc:get_selections() do
if line >= minline and line <= maxline if line >= minline and line <= maxline
and (core.blink_timer - core.blink_start) % T < T / 2
and system.window_has_focus() then and system.window_has_focus() then
local x, y = self:get_line_screen_position(line) if config.disable_blink
self:draw_caret(x + self:get_col_x_offset(line, col), y) or (core.blink_timer - core.blink_start) % T < T / 2 then
local x, y = self:get_line_screen_position(line)
self:draw_caret(x + self:get_col_x_offset(line, col), y)
end
end end
end end
end end

View File

@ -17,10 +17,7 @@ local core = {}
local function load_session() local function load_session()
local ok, t = pcall(dofile, USERDIR .. "/session.lua") local ok, t = pcall(dofile, USERDIR .. "/session.lua")
if ok then return ok and t or {}
return t.recents, t.window, t.window_mode
end
return {}
end end
@ -30,6 +27,8 @@ local function save_session()
fp:write("return {recents=", common.serialize(core.recent_projects), fp:write("return {recents=", common.serialize(core.recent_projects),
", window=", common.serialize(table.pack(system.get_window_size())), ", window=", common.serialize(table.pack(system.get_window_size())),
", window_mode=", common.serialize(system.get_window_mode()), ", window_mode=", common.serialize(system.get_window_mode()),
", previous_find=", common.serialize(core.previous_find),
", previous_replace=", common.serialize(core.previous_replace),
"}\n") "}\n")
fp:close() fp:close()
end end
@ -80,6 +79,9 @@ function core.open_folder_project(dir_path_abs)
if core.set_project_dir(dir_path_abs, core.on_quit_project) then if core.set_project_dir(dir_path_abs, core.on_quit_project) then
core.root_view:close_all_docviews() core.root_view:close_all_docviews()
update_recents_project("add", dir_path_abs) update_recents_project("add", dir_path_abs)
if not core.load_project_module() then
command.perform("core:open-log")
end
core.on_enter_project(dir_path_abs) core.on_enter_project(dir_path_abs)
end end
end end
@ -320,8 +322,8 @@ local style = require "core.style"
------------------------------- Fonts ---------------------------------------- ------------------------------- Fonts ----------------------------------------
-- customize fonts: -- customize fonts:
-- 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.code_font = renderer.font.load(DATADIR .. "/fonts/JetBrainsMono-Regular.ttf", 13 * SCALE) -- style.code_font = renderer.font.load(DATADIR .. "/fonts/JetBrainsMono-Regular.ttf", 14 * SCALE)
-- --
-- font names used by lite: -- font names used by lite:
-- style.font : user interface -- style.font : user interface
@ -342,11 +344,11 @@ local style = require "core.style"
-- enable or disable plugin loading setting config entries: -- enable or disable plugin loading setting config entries:
-- enable trimwhitespace, otherwise it is disable by default: -- enable plugins.trimwhitespace, otherwise it is disable by default:
-- config.trimwhitespace = true -- config.plugins.trimwhitespace = true
-- --
-- disable detectindent, otherwise it is enabled by default -- disable detectindent, otherwise it is enabled by default
-- config.detectindent = false -- config.plugins.detectindent = false
]]) ]])
init_file:close() init_file:close()
end end
@ -394,15 +396,6 @@ function core.remove_project_directory(path)
return false return false
end end
local function whitespace_replacements()
local r = renderer.replacements.new()
r:add(" ", "·")
r:add("\t", "»")
return r
end
local function reload_on_user_module_save() local function reload_on_user_module_save()
-- auto-realod style when user's module is saved by overriding Doc:Save() -- auto-realod style when user's module is saved by overriding Doc:Save()
local doc_save = Doc.save local doc_save = Doc.save
@ -435,13 +428,15 @@ function core.init()
end end
do do
local recent_projects, window_position, window_mode = load_session() local session = load_session()
if window_mode == "normal" then if session.window_mode == "normal" then
system.set_window_size(table.unpack(window_position)) system.set_window_size(table.unpack(session.window))
elseif window_mode == "maximized" then elseif session.window_mode == "maximized" then
system.set_window_mode("maximized") system.set_window_mode("maximized")
end 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 end
local project_dir = core.recent_projects[1] or "." local project_dir = core.recent_projects[1] or "."
@ -461,7 +456,10 @@ function core.init()
project_dir = arg_filename project_dir = arg_filename
project_dir_explicit = true project_dir_explicit = true
else 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
end end
@ -494,7 +492,7 @@ function core.init()
core.redraw = true core.redraw = true
core.visited_files = {} core.visited_files = {}
core.restart_request = false core.restart_request = false
core.replacements = whitespace_replacements() core.quit_request = false
core.root_view = RootView() core.root_view = RootView()
core.command_view = CommandView() core.command_view = CommandView()
@ -564,10 +562,10 @@ function core.init()
end end
function core.confirm_close_all(close_fn, ...) function core.confirm_close_docs(docs, close_fn, ...)
local dirty_count = 0 local dirty_count = 0
local dirty_name local dirty_name
for _, doc in ipairs(core.docs) do for _, doc in ipairs(docs or core.docs) do
if doc:is_dirty() then if doc:is_dirty() then
dirty_count = dirty_count + 1 dirty_count = dirty_count + 1
dirty_name = doc:get_name() dirty_name = doc:get_name()
@ -620,24 +618,6 @@ do
end 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) local function quit_with_function(quit_fn, force)
if force then if force then
delete_temp_files() delete_temp_files()
@ -645,12 +625,12 @@ local function quit_with_function(quit_fn, force)
save_session() save_session()
quit_fn() quit_fn()
else 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
end end
function core.quit(force) function core.quit(force)
quit_with_function(os.exit, force) quit_with_function(function() core.quit_request = true end, force)
end end
@ -679,8 +659,8 @@ local function check_plugin_version(filename)
-- Future versions will look only at the mod-version tag. -- Future versions will look only at the mod-version tag.
local version = line:match('%-%-%s*lite%-xl%s*(%d+%.%d+)$') local version = line:match('%-%-%s*lite%-xl%s*(%d+%.%d+)$')
if version then if version then
-- we consider the version tag 1.16 equivalent to mod-version:1 -- we consider the version tag 2.0 equivalent to mod-version:2
version_match = (version == '1.16' and MOD_VERSION == "1") version_match = (version == '2.0' and MOD_VERSION == "2")
break break
end end
end end
@ -695,24 +675,30 @@ function core.load_plugins()
userdir = {dir = USERDIR, plugins = {}}, userdir = {dir = USERDIR, plugins = {}},
datadir = {dir = DATADIR, plugins = {}}, datadir = {dir = DATADIR, plugins = {}},
} }
for _, root_dir in ipairs {USERDIR, DATADIR} do local files, ordered = {}, {}
for _, root_dir in ipairs {DATADIR, USERDIR} do
local plugin_dir = root_dir .. "/plugins" local plugin_dir = root_dir .. "/plugins"
local files = system.list_dir(plugin_dir) for _, filename in ipairs(system.list_dir(plugin_dir) or {}) do
for _, filename in ipairs(files or {}) do if not files[filename] then table.insert(ordered, filename) end
local basename = filename:match("(.-)%.lua$") or filename files[filename] = plugin_dir -- user plugins will always replace system plugins
local is_lua_file, version_match = check_plugin_version(plugin_dir .. '/' .. filename) end
if is_lua_file then end
if not version_match then table.sort(ordered)
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 for _, filename in ipairs(ordered) do
ls[#ls + 1] = filename local plugin_dir, basename = files[filename], filename:match("(.-)%.lua$") or filename
elseif config.plugins[basename] ~= false then local is_lua_file, version_match = check_plugin_version(plugin_dir .. '/' .. filename)
local modname = "plugins." .. basename if is_lua_file then
local ok = core.try(require, modname) if not version_match then
if ok then core.log_quiet("Loaded plugin %q from %s", basename, plugin_dir) end core.log_quiet("Version mismatch for plugin %q from %s", basename, plugin_dir)
if not ok then local list = refused_list[plugin_dir:find(USERDIR, 1, true) == 1 and 'userdir' or 'datadir'].plugins
no_errors = false table.insert(list, filename)
end end
if version_match and config.plugins[basename] ~= false then
local ok = core.try(require, "plugins." .. basename)
if ok then core.log_quiet("Loaded plugin %q from %s", basename, plugin_dir) end
if not ok then
no_errors = false
end end
end end
end end
@ -759,8 +745,12 @@ end
function core.set_active_view(view) function core.set_active_view(view)
assert(view, "Tried to set active view to nil") 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 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 if view.doc and view.doc.filename then
core.set_visited(view.doc.filename) core.set_visited(view.doc.filename)
end end
@ -810,10 +800,30 @@ function core.normalize_to_project_dir(filename)
end end
-- The function below works like system.absolute_path except it
-- doesn't fail if the file does not exist. We consider that the
-- current dir is core.project_dir so relative filename are considered
-- to be in core.project_dir.
-- Please note that .. or . in the filename are not taken into account.
-- This function should get only filenames normalized using
-- common.normalize_path function.
function core.project_absolute_path(filename)
if 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) function core.open_doc(filename)
local new_file = not filename or not system.get_file_info(filename)
local abs_filename
if filename then if filename then
-- normalize filename and set absolute filename then
-- try to find existing doc for filename -- 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 for _, doc in ipairs(core.docs) do
if doc.abs_filename and abs_filename == doc.abs_filename then if doc.abs_filename and abs_filename == doc.abs_filename then
return doc return doc
@ -821,8 +831,7 @@ function core.open_doc(filename)
end end
end end
-- no existing doc for filename; create new -- no existing doc for filename; create new
filename = filename and core.normalize_to_project_dir(filename) local doc = Doc(filename, abs_filename, new_file)
local doc = Doc(filename)
table.insert(core.docs, doc) table.insert(core.docs, doc)
core.log_quiet(filename and "Opened doc \"%s\"" or "Opened new doc", filename) core.log_quiet(filename and "Opened doc \"%s\"" or "Opened new doc", filename)
return doc return doc
@ -871,6 +880,23 @@ function core.error(...)
end 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, ...) function core.try(fn, ...)
local err local err
local ok, res = xpcall(fn, function(msg) local ok, res = xpcall(fn, function(msg)
@ -896,11 +922,15 @@ function core.on_event(type, ...)
elseif type == "mousemoved" then elseif type == "mousemoved" then
core.root_view:on_mouse_moved(...) core.root_view:on_mouse_moved(...)
elseif type == "mousepressed" then elseif type == "mousepressed" then
core.root_view:on_mouse_pressed(...) if not core.root_view:on_mouse_pressed(...) then
did_keymap = keymap.on_mouse_pressed(...)
end
elseif type == "mousereleased" then elseif type == "mousereleased" then
core.root_view:on_mouse_released(...) core.root_view:on_mouse_released(...)
elseif type == "mousewheel" then elseif type == "mousewheel" then
core.root_view:on_mouse_wheel(...) if not core.root_view:on_mouse_wheel(...) then
did_keymap = keymap.on_mouse_wheel(...)
end
elseif type == "resized" then elseif type == "resized" then
core.window_mode = system.get_window_mode() core.window_mode = system.get_window_mode()
elseif type == "minimized" or type == "maximized" or type == "restored" then elseif type == "minimized" or type == "maximized" or type == "restored" then
@ -1027,7 +1057,7 @@ function core.run()
core.frame_start = system.get_time() core.frame_start = system.get_time()
local did_redraw = core.step() local did_redraw = core.step()
local need_more_work = run_threads() local need_more_work = run_threads()
if core.restart_request then break end if core.restart_request or core.quit_request then break end
if not did_redraw and not need_more_work then if not did_redraw and not need_more_work then
idle_iterations = idle_iterations + 1 idle_iterations = idle_iterations + 1
-- do not wait of events at idle_iterations = 1 to give a chance at core.step to run -- do not wait of events at idle_iterations = 1 to give a chance at core.step to run

View File

@ -32,6 +32,8 @@ local function keymap_macos(keymap)
["cmd+7"] = "root:switch-to-tab-7", ["cmd+7"] = "root:switch-to-tab-7",
["cmd+8"] = "root:switch-to-tab-8", ["cmd+8"] = "root:switch-to-tab-8",
["cmd+9"] = "root:switch-to-tab-9", ["cmd+9"] = "root:switch-to-tab-9",
["wheel"] = "root:scroll",
["cmd+f"] = "find-replace:find", ["cmd+f"] = "find-replace:find",
["cmd+r"] = "find-replace:replace", ["cmd+r"] = "find-replace:replace",
["f3"] = "find-replace:repeat-find", ["f3"] = "find-replace:repeat-find",
@ -52,23 +54,27 @@ local function keymap_macos(keymap)
["shift+tab"] = "doc:unindent", ["shift+tab"] = "doc:unindent",
["backspace"] = "doc:backspace", ["backspace"] = "doc:backspace",
["shift+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+shift+backspace"] = "doc:delete-to-previous-word-start",
["cmd+backspace"] = "doc:delete-to-start-of-indentation",
["delete"] = "doc:delete", ["delete"] = "doc:delete",
["shift+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+shift+delete"] = "doc:delete-to-next-word-end",
["cmd+delete"] = "doc:delete-to-end-of-line",
["return"] = { "command:submit", "doc:newline", "dialog:select" }, ["return"] = { "command:submit", "doc:newline", "dialog:select" },
["keypad enter"] = { "command:submit", "doc:newline", "dialog:select" }, ["keypad enter"] = { "command:submit", "doc:newline", "dialog:select" },
["cmd+return"] = "doc:newline-below", ["cmd+return"] = "doc:newline-below",
["cmd+shift+return"] = "doc:newline-above", ["cmd+shift+return"] = "doc:newline-above",
["cmd+j"] = "doc:join-lines", ["cmd+j"] = "doc:join-lines",
["cmd+a"] = "doc:select-all", ["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+l"] = "doc:select-lines",
["cmd+shift+l"] = { "find-replace:select-add-all", "doc:select-word" },
["cmd+/"] = "doc:toggle-line-comments", ["cmd+/"] = "doc:toggle-line-comments",
["cmd+up"] = "doc:move-lines-up", ["option+up"] = "doc:move-lines-up",
["cmd+down"] = "doc:move-lines-down", ["option+down"] = "doc:move-lines-down",
["cmd+shift+d"] = "doc:duplicate-lines", ["cmd+shift+d"] = "doc:duplicate-lines",
["cmd+shift+k"] = "doc:delete-lines", ["cmd+shift+k"] = "doc:delete-lines",
@ -76,33 +82,42 @@ local function keymap_macos(keymap)
["right"] = { "doc:move-to-next-char", "dialog:next-entry"}, ["right"] = { "doc:move-to-next-char", "dialog:next-entry"},
["up"] = { "command:select-previous", "doc:move-to-previous-line" }, ["up"] = { "command:select-previous", "doc:move-to-previous-line" },
["down"] = { "command:select-next", "doc:move-to-next-line" }, ["down"] = { "command:select-next", "doc:move-to-next-line" },
["cmd+left"] = "doc:move-to-previous-word-start", ["option+left"] = "doc:move-to-previous-word-start",
["cmd+right"] = "doc:move-to-next-word-end", ["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-previous-block-start",
["cmd+]"] = "doc:move-to-next-block-end", ["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", ["end"] = "doc:move-to-end-of-line",
["cmd+home"] = "doc:move-to-start-of-doc", ["cmd+up"] = "doc:move-to-start-of-doc",
["cmd+end"] = "doc:move-to-end-of-doc", ["cmd+down"] = "doc:move-to-end-of-doc",
["pageup"] = "doc:move-to-previous-page", ["pageup"] = "doc:move-to-previous-page",
["pagedown"] = "doc:move-to-next-page", ["pagedown"] = "doc:move-to-next-page",
["shift+1lclick"] = "doc:select-to-cursor",
["ctrl+1lclick"] = "doc:split-cursor",
["1lclick"] = "doc:set-cursor",
["2lclick"] = "doc:set-cursor-word",
["3lclick"] = "doc:set-cursor-line",
["shift+left"] = "doc:select-to-previous-char", ["shift+left"] = "doc:select-to-previous-char",
["shift+right"] = "doc:select-to-next-char", ["shift+right"] = "doc:select-to-next-char",
["shift+up"] = "doc:select-to-previous-line", ["shift+up"] = "doc:select-to-previous-line",
["shift+down"] = "doc:select-to-next-line", ["shift+down"] = "doc:select-to-next-line",
["cmd+shift+left"] = "doc:select-to-previous-word-start", ["option+shift+left"] = "doc:select-to-previous-word-start",
["cmd+shift+right"] = "doc:select-to-next-word-end", ["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-previous-block-start",
["cmd+shift+]"] = "doc:select-to-next-block-end", ["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", ["shift+end"] = "doc:select-to-end-of-line",
["cmd+shift+home"] = "doc:select-to-start-of-doc", ["cmd+shift+up"] = "doc:select-to-start-of-doc",
["cmd+shift+end"] = "doc:select-to-end-of-doc", ["cmd+shift+down"] = "doc:select-to-end-of-doc",
["shift+pageup"] = "doc:select-to-previous-page", ["shift+pageup"] = "doc:select-to-previous-page",
["shift+pagedown"] = "doc:select-to-next-page", ["shift+pagedown"] = "doc:select-to-next-page",
["cmd+shift+up"] = "doc:create-cursor-previous-line", ["cmd+option+up"] = "doc:create-cursor-previous-line",
["cmd+shift+down"] = "doc:create-cursor-next-line" ["cmd+option+down"] = "doc:create-cursor-next-line"
} }
end end

View File

@ -1,11 +1,12 @@
local command = require "core.command" local command = require "core.command"
local config = require "core.config"
local keymap = {} local keymap = {}
keymap.modkeys = {} keymap.modkeys = {}
keymap.map = {} keymap.map = {}
keymap.reverse_map = {} keymap.reverse_map = {}
local macos = rawget(_G, "MACOS_RESOURCES") local macos = PLATFORM == "Mac OS X"
-- Thanks to mathewmariani, taken from his lite-macos github repository. -- Thanks to mathewmariani, taken from his lite-macos github repository.
local modkeys_os = require("core.modkeys-" .. (macos and "macos" or "generic")) local modkeys_os = require("core.modkeys-" .. (macos and "macos" or "generic"))
@ -30,7 +31,8 @@ function keymap.add_direct(map)
end end
keymap.map[stroke] = commands keymap.map[stroke] = commands
for _, cmd in ipairs(commands) do for _, cmd in ipairs(commands) do
keymap.reverse_map[cmd] = stroke keymap.reverse_map[cmd] = keymap.reverse_map[cmd] or {}
table.insert(keymap.reverse_map[cmd], stroke)
end end
end end
end end
@ -52,18 +54,43 @@ function keymap.add(map, overwrite)
end end
end end
for _, cmd in ipairs(commands) do for _, cmd in ipairs(commands) do
keymap.reverse_map[cmd] = stroke keymap.reverse_map[cmd] = keymap.reverse_map[cmd] or {}
table.insert(keymap.reverse_map[cmd], stroke)
end end
end end
end end
function keymap.get_binding(cmd) local function remove_only(tbl, k, v)
return keymap.reverse_map[cmd] for key, values in pairs(tbl) do
if key == k then
if v then
for i, value in ipairs(values) do
if value == v then
table.remove(values, i)
end
end
else
tbl[key] = nil
end
break
end
end
end end
function keymap.on_key_pressed(k) function keymap.unbind(key, cmd)
remove_only(keymap.map, key, cmd)
remove_only(keymap.reverse_map, cmd, key)
end
function keymap.get_binding(cmd)
return table.unpack(keymap.reverse_map[cmd] or {})
end
function keymap.on_key_pressed(k, ...)
local mk = modkey_map[k] local mk = modkey_map[k]
if mk then if mk then
keymap.modkeys[mk] = true keymap.modkeys[mk] = true
@ -73,18 +100,30 @@ function keymap.on_key_pressed(k)
end end
else else
local stroke = key_to_stroke(k) local stroke = key_to_stroke(k)
local commands = keymap.map[stroke] local commands, performed = keymap.map[stroke]
if commands then if commands then
for _, cmd in ipairs(commands) do for _, cmd in ipairs(commands) do
local performed = command.perform(cmd) performed = command.perform(cmd, ...)
if performed then break end if performed then break end
end end
return true return performed
end end
end end
return false return false
end end
function keymap.on_mouse_wheel(delta, ...)
return not (keymap.on_key_pressed("wheel" .. (delta > 0 and "up" or "down"), delta, ...)
or keymap.on_key_pressed("wheel", delta, ...))
end
function keymap.on_mouse_pressed(button, x, y, clicks)
local click_number = (((clicks - 1) % config.max_clicks) + 1)
return not (keymap.on_key_pressed(click_number .. button:sub(1,1) .. "click", x, y, clicks) or
keymap.on_key_pressed(button:sub(1,1) .. "click", x, y, clicks) or
keymap.on_key_pressed(click_number .. "click", x, y, clicks) or
keymap.on_key_pressed("click", x, y, clicks))
end
function keymap.on_key_released(k) function keymap.on_key_released(k)
local mk = modkey_map[k] local mk = modkey_map[k]
@ -108,6 +147,7 @@ keymap.add_direct {
["ctrl+shift+c"] = "core:change-project-folder", ["ctrl+shift+c"] = "core:change-project-folder",
["ctrl+shift+o"] = "core:open-project-folder", ["ctrl+shift+o"] = "core:open-project-folder",
["alt+return"] = "core:toggle-fullscreen", ["alt+return"] = "core:toggle-fullscreen",
["f11"] = "core:toggle-fullscreen",
["alt+shift+j"] = "root:split-left", ["alt+shift+j"] = "root:split-left",
["alt+shift+l"] = "root:split-right", ["alt+shift+l"] = "root:split-right",
@ -132,6 +172,7 @@ keymap.add_direct {
["alt+7"] = "root:switch-to-tab-7", ["alt+7"] = "root:switch-to-tab-7",
["alt+8"] = "root:switch-to-tab-8", ["alt+8"] = "root:switch-to-tab-8",
["alt+9"] = "root:switch-to-tab-9", ["alt+9"] = "root:switch-to-tab-9",
["wheel"] = "root:scroll",
["ctrl+f"] = "find-replace:find", ["ctrl+f"] = "find-replace:find",
["ctrl+r"] = "find-replace:replace", ["ctrl+r"] = "find-replace:replace",
@ -167,8 +208,10 @@ keymap.add_direct {
["ctrl+shift+return"] = "doc:newline-above", ["ctrl+shift+return"] = "doc:newline-above",
["ctrl+j"] = "doc:join-lines", ["ctrl+j"] = "doc:join-lines",
["ctrl+a"] = "doc:select-all", ["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+l"] = "doc:select-lines",
["ctrl+shift+l"] = { "find-replace:select-add-all", "doc:select-word" },
["ctrl+/"] = "doc:toggle-line-comments", ["ctrl+/"] = "doc:toggle-line-comments",
["ctrl+up"] = "doc:move-lines-up", ["ctrl+up"] = "doc:move-lines-up",
["ctrl+down"] = "doc:move-lines-down", ["ctrl+down"] = "doc:move-lines-down",
@ -183,13 +226,18 @@ keymap.add_direct {
["ctrl+right"] = "doc:move-to-next-word-end", ["ctrl+right"] = "doc:move-to-next-word-end",
["ctrl+["] = "doc:move-to-previous-block-start", ["ctrl+["] = "doc:move-to-previous-block-start",
["ctrl+]"] = "doc:move-to-next-block-end", ["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", ["end"] = "doc:move-to-end-of-line",
["ctrl+home"] = "doc:move-to-start-of-doc", ["ctrl+home"] = "doc:move-to-start-of-doc",
["ctrl+end"] = "doc:move-to-end-of-doc", ["ctrl+end"] = "doc:move-to-end-of-doc",
["pageup"] = "doc:move-to-previous-page", ["pageup"] = "doc:move-to-previous-page",
["pagedown"] = "doc:move-to-next-page", ["pagedown"] = "doc:move-to-next-page",
["shift+1lclick"] = "doc:select-to-cursor",
["ctrl+1lclick"] = "doc:split-cursor",
["1lclick"] = "doc:set-cursor",
["2lclick"] = "doc:set-cursor-word",
["3lclick"] = "doc:set-cursor-line",
["shift+left"] = "doc:select-to-previous-char", ["shift+left"] = "doc:select-to-previous-char",
["shift+right"] = "doc:select-to-next-char", ["shift+right"] = "doc:select-to-next-char",
["shift+up"] = "doc:select-to-previous-line", ["shift+up"] = "doc:select-to-previous-line",
@ -198,7 +246,7 @@ keymap.add_direct {
["ctrl+shift+right"] = "doc:select-to-next-word-end", ["ctrl+shift+right"] = "doc:select-to-next-word-end",
["ctrl+shift+["] = "doc:select-to-previous-block-start", ["ctrl+shift+["] = "doc:select-to-previous-block-start",
["ctrl+shift+]"] = "doc:select-to-next-block-end", ["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", ["shift+end"] = "doc:select-to-end-of-line",
["ctrl+shift+home"] = "doc:select-to-start-of-doc", ["ctrl+shift+home"] = "doc:select-to-start-of-doc",
["ctrl+shift+end"] = "doc:select-to-end-of-doc", ["ctrl+shift+end"] = "doc:select-to-end-of-doc",

View File

@ -1,14 +1,45 @@
local core = require "core" local core = require "core"
local common = require "core.common"
local style = require "core.style" local style = require "core.style"
local View = require "core.view" 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() local LogView = View:extend()
LogView.context = "session"
function LogView:new() function LogView:new()
LogView.super.new(self) LogView.super.new(self)
self.last_item = core.log_items[#core.log_items] self.last_item = core.log_items[#core.log_items]
self.expanding = {}
self.scrollable = true self.scrollable = true
self.yoffset = 0 self.yoffset = 0
end end
@ -19,6 +50,55 @@ function LogView:get_name()
end 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() function LogView:update()
local item = core.log_items[#core.log_items] local item = core.log_items[#core.log_items]
if self.last_item ~= item then if self.last_item ~= item then
@ -27,6 +107,14 @@ function LogView:update()
self.yoffset = -(style.font:get_height() + style.padding.y) self.yoffset = -(style.font:get_height() + style.padding.y)
end 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) self:move_towards("yoffset", 0)
LogView.super.update(self) LogView.super.update(self)
@ -35,38 +123,48 @@ end
local function draw_text_multiline(font, text, x, y, color) local function draw_text_multiline(font, text, x, y, color)
local th = font:get_height() local th = font:get_height()
local resx, resy = x, y local resx = x
for line in text:gmatch("[^\n]+") do for line in text:gmatch("[^\n]+") do
resy = y
resx = renderer.draw_text(style.font, line, x, y, color) resx = renderer.draw_text(style.font, line, x, y, color)
y = y + th y = y + th
end end
return resx, resy return resx, y
end end
function LogView:draw() function LogView:draw()
self:draw_background(style.background) self:draw_background(style.background)
local ox, oy = self:get_content_offset()
local th = style.font:get_height() 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
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 = x + style.padding.x x = x + style.padding.x
local subx = x
x, y = draw_text_multiline(style.font, item.text, x, y, style.text) local time = os.date(nil, item.time)
renderer.draw_text(style.font, " at " .. item.at, x, y, style.dim) x = common.draw_text(style.font, style.dim, time, "left", x, y, w, lh)
y = y + th x = x + style.padding.x
if item.info then
subx, y = draw_text_multiline(style.font, item.info, subx, y, style.dim) x = common.draw_text(style.code_font, style.dim, is_expanded(item) and "-" or "+", "left", x, y, w, lh)
y = y + th 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 end
y = y + style.padding.y
end end
end end

View File

@ -193,7 +193,8 @@ function NagView:next()
self:change_hovered(common.find_index(self.options, "default_yes")) self:change_hovered(common.find_index(self.options, "default_yes"))
end end
self.force_focus = self.message ~= nil 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 end
function NagView:show(title, message, options, on_select) function NagView:show(title, message, options, on_select)

View File

@ -20,17 +20,6 @@ function Object:extend()
end 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) function Object:is(T)
local mt = getmetatable(self) local mt = getmetatable(self)
while mt do while mt do

View File

@ -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). -- pattern:gsub(string).
regex.__index = function(table, key) return regex[key]; end 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) return regex.cmatch(pattern, string, offset or 1, options or 0)
end 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. -- mid character.
local function previous_character(str, index) local function previous_character(str, index)
local byte local byte
@ -23,7 +23,7 @@ end
-- Moves to the end of the identified character. -- Moves to the end of the identified character.
local function end_character(str, index) local function end_character(str, index)
local byte = string.byte(str, index + 1) 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 index = index + 1
byte = string.byte(str, index + 1) byte = string.byte(str, index + 1)
end end
@ -32,7 +32,7 @@ end
-- Build off matching. For now, only support basic replacements, but capture -- Build off matching. For now, only support basic replacements, but capture
-- groupings should be doable. We can even have custom group replacements and -- 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. -- as \1 - \9.
-- Should work on UTF-8 text. -- Should work on UTF-8 text.
regex.gsub = function(pattern_string, str, replacement) regex.gsub = function(pattern_string, str, replacement)
@ -48,8 +48,8 @@ regex.gsub = function(pattern_string, str, replacement)
if #indices > 2 then if #indices > 2 then
for i = 1, (#indices/2 - 1) do for i = 1, (#indices/2 - 1) do
currentReplacement = string.gsub( currentReplacement = string.gsub(
currentReplacement, currentReplacement,
"\\" .. i, "\\" .. i,
str:sub(indices[i*2+1], end_character(str,indices[i*2+2]-1)) str:sub(indices[i*2+1], end_character(str,indices[i*2+2]-1))
) )
end end
@ -57,10 +57,10 @@ regex.gsub = function(pattern_string, str, replacement)
currentReplacement = string.gsub(currentReplacement, "\\%d", "") currentReplacement = string.gsub(currentReplacement, "\\%d", "")
table.insert(replacements, { indices[1], #currentReplacement+indices[1] }) table.insert(replacements, { indices[1], #currentReplacement+indices[1] })
if indices[1] > 1 then if indices[1] > 1 then
result = result .. result = result ..
str:sub(1, previous_character(str, indices[1])) .. currentReplacement str:sub(1, previous_character(str, indices[1])) .. currentReplacement
else else
result = result .. currentReplacement result = result .. currentReplacement
end end
str = str:sub(indices[2]) str = str:sub(indices[2])
end end

View File

@ -5,7 +5,6 @@ local style = require "core.style"
local keymap = require "core.keymap" local keymap = require "core.keymap"
local Object = require "core.object" local Object = require "core.object"
local View = require "core.view" local View = require "core.view"
local CommandView = require "core.commandview"
local NagView = require "core.nagview" local NagView = require "core.nagview"
local DocView = require "core.docview" local DocView = require "core.docview"
@ -143,7 +142,14 @@ function Node:remove_view(root, view)
local parent = self:get_parent_node(root) local parent = self:get_parent_node(root)
local is_a = (parent.a == self) local is_a = (parent.a == self)
local other = parent[is_a and "b" or "a"] local other = parent[is_a and "b" or "a"]
if other:get_locked_size() then local locked_size_x, locked_size_y = other:get_locked_size()
local locked_size
if parent.type == "hsplit" then
locked_size = locked_size_x
else
locked_size = locked_size_y
end
if self.is_primary_node or locked_size then
self.views = {} self.views = {}
self:add_view(EmptyView()) self:add_view(EmptyView())
else else
@ -240,12 +246,15 @@ end
function Node:get_divider_overlapping_point(px, py) function Node:get_divider_overlapping_point(px, py)
if self.type ~= "leaf" then if self.type ~= "leaf" then
local p = 6 local axis = self.type == "hsplit" and "x" or "y"
local x, y, w, h = self:get_divider_rect() if self.a:is_resizable(axis) and self.b:is_resizable(axis) then
x, y = x - p, y - p local p = 6
w, h = w + p * 2, h + p * 2 local x, y, w, h = self:get_divider_rect()
if px > x and py > y and px < x + w and py < y + h then x, y = x - p, y - p
return self 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 end
return self.a:get_divider_overlapping_point(px, py) return self.a:get_divider_overlapping_point(px, py)
or self.b:get_divider_overlapping_point(px, py) or self.b:get_divider_overlapping_point(px, py)
@ -259,7 +268,7 @@ end
function Node:get_tab_overlapping_point(px, py) 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 tabs_number = self:get_visible_tabs_number()
local x1, y1, w, h = self:get_tab_rect(self.tab_offset) local x1, y1, w, h = self:get_tab_rect(self.tab_offset)
local x2, y2 = self:get_tab_rect(self.tab_offset + tabs_number) local x2, y2 = self:get_tab_rect(self.tab_offset + tabs_number)
@ -269,6 +278,19 @@ function Node:get_tab_overlapping_point(px, py)
end end
function Node:should_show_tabs()
if self.locked then return false end
local dn = core.root_view.dragged_node
if #self.views > 1
or (dn and dn.dragging) then -- show tabs while dragging
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 function close_button_location(x, w)
local cw = style.icon_font:get_width("C") local cw = style.icon_font:get_width("C")
local pad = style.padding.y local pad = style.padding.y
@ -412,7 +434,7 @@ end
function Node:update_layout() function Node:update_layout()
if self.type == "leaf" then if self.type == "leaf" then
local av = self.active_view local av = self.active_view
if #self.views > 1 then if self:should_show_tabs() then
local _, _, _, th = self:get_tab_rect(1) local _, _, _, th = self:get_tab_rect(1)
av.position.x, av.position.y = self.position.x, self.position.y + th 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 av.size.x, av.size.y = self.size.x, self.size.y - th
@ -494,6 +516,53 @@ function Node:update()
end end
end end
function Node:draw_tab(text, is_active, is_hovered, is_close_hovered, x, y, w, h, standalone)
local ds = style.divider_size
local dots_width = style.font:get_width("")
local color = style.dim
local padding_y = style.padding.y
renderer.draw_rect(x + w, y + padding_y, ds, h - padding_y * 2, style.dim)
if standalone then
renderer.draw_rect(x-1, y-1, w+2, h+2, style.background2)
end
if is_active then
color = style.text
renderer.draw_rect(x, y, w, h, style.background)
renderer.draw_rect(x + w, y, ds, h, style.divider)
renderer.draw_rect(x - ds, y, ds, h, style.divider)
end
local cx, cw, cspace = close_button_location(x, w)
local show_close_button = ((is_active or is_hovered) and not standalone and config.tab_close_button)
if show_close_button then
local close_style = is_close_hovered and style.text or style.dim
common.draw_text(style.icon_font, close_style, "C", nil, cx, y, 0, h)
end
if is_hovered then
color = style.text
end
local padx = style.padding.x
-- Normally we should substract "cspace" from text_avail_width and from the
-- clipping width. It is the padding space we give to the left and right of the
-- close button. However, since we are using dots to terminate filenames, we
-- choose to ignore "cspace" accepting that the text can possibly "touch" the
-- close button.
local text_avail_width = cx - x - padx
core.push_clip_rect(x, y, cx - x, h)
x, w = x + padx, w - padx * 2
local align = "center"
if style.font:get_width(text) > text_avail_width then
align = "left"
for i = 1, #text do
local reduced_text = text:sub(1, #text - i)
if style.font:get_width(reduced_text) + dots_width <= text_avail_width then
text = reduced_text .. ""
break
end
end
end
common.draw_text(style.font, color, text, align, x, y, w, h)
core.pop_clip_rect()
end
function Node:draw_tabs() function Node:draw_tabs()
local x, y, w, h, scroll_padding = self:get_scroll_button_rect(1) local x, y, w, h, scroll_padding = self:get_scroll_button_rect(1)
@ -518,47 +587,9 @@ function Node:draw_tabs()
for i = self.tab_offset, self.tab_offset + tabs_number - 1 do for i = self.tab_offset, self.tab_offset + tabs_number - 1 do
local view = self.views[i] local view = self.views[i]
local x, y, w, h = self:get_tab_rect(i) local x, y, w, h = self:get_tab_rect(i)
local text = view:get_name() self:draw_tab(view:get_name(), view == self.active_view,
local color = style.dim i == self.hovered_tab, i == self.hovered_close,
local padding_y = style.padding.y x, y, w, h)
renderer.draw_rect(x + w, y + padding_y, ds, h - padding_y * 2, style.dim)
if view == self.active_view then
color = style.text
renderer.draw_rect(x, y, w, h, style.background)
renderer.draw_rect(x + w, y, ds, h, style.divider)
renderer.draw_rect(x - ds, y, ds, h, style.divider)
end
local cx, cw, cspace = close_button_location(x, w)
local show_close_button = ((view == self.active_view or i == self.hovered_tab) and config.tab_close_button)
if show_close_button then
local close_style = self.hovered_close == i and style.text or style.dim
common.draw_text(style.icon_font, close_style, "C", nil, cx, y, 0, h)
end
if i == self.hovered_tab then
color = style.text
end
local padx = style.padding.x
-- Normally we should substract "cspace" from text_avail_width and from the
-- clipping width. It is the padding space we give to the left and right of the
-- close button. However, since we are using dots to terminate filenames, we
-- choose to ignore "cspace" accepting that the text can possibly "touch" the
-- close button.
local text_avail_width = cx - x - padx
core.push_clip_rect(x, y, cx - x, h)
x, w = x + padx, w - padx * 2
local align = "center"
if style.font:get_width(text) > text_avail_width then
align = "left"
for i = 1, #text do
local reduced_text = text:sub(1, #text - i)
if style.font:get_width(reduced_text) + dots_width <= text_avail_width then
text = reduced_text .. ""
break
end
end
end
common.draw_text(style.font, color, text, align, x, y, w, h)
core.pop_clip_rect()
end end
core.pop_clip_rect() core.pop_clip_rect()
@ -567,7 +598,7 @@ end
function Node:draw() function Node:draw()
if self.type == "leaf" then if self.type == "leaf" then
if #self.views > 1 then if self:should_show_tabs() then
self:draw_tabs() self:draw_tabs()
end end
local pos, size = self.active_view.position, self.active_view.size local pos, size = self.active_view.position, self.active_view.size
@ -591,23 +622,37 @@ function Node:is_empty()
end 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 if self.type == "leaf" then
local i = 1 local i = 1
while i <= #self.views do while i <= #self.views do
local view = self.views[i] 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) table.remove(self.views, i)
if view == node_active_view then
lost_active_view = true
end
else else
i = i + 1 i = i + 1
end end
end end
self.tab_offset = 1
if #self.views == 0 and self.is_primary_node then 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()) 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 end
else else
self.a:close_all_docviews() self.a:close_all_docviews(keep_active)
self.b:close_all_docviews() self.b:close_all_docviews(keep_active)
if self.a:is_empty() and not self.a.is_primary_node then if self.a:is_empty() and not self.a.is_primary_node then
self:consume(self.b) self:consume(self.b)
elseif self.b:is_empty() and not self.b.is_primary_node then elseif self.b:is_empty() and not self.b.is_primary_node then
@ -670,6 +715,62 @@ function Node:resize(axis, value)
end end
function Node:get_split_type(mouse_x, mouse_y)
local x, y = self.position.x, self.position.y
local w, h = self.size.x, self.size.y
local _, _, _, tab_h = self:get_scroll_button_rect(1)
y = y + tab_h
h = h - tab_h
local local_mouse_x = mouse_x - x
local local_mouse_y = mouse_y - y
if local_mouse_y < 0 then
return "tab"
else
local left_pct = local_mouse_x * 100 / w
local top_pct = local_mouse_y * 100 / h
if left_pct <= 30 then
return "left"
elseif left_pct >= 70 then
return "right"
elseif top_pct <= 30 then
return "up"
elseif top_pct >= 70 then
return "down"
end
return "middle"
end
end
function Node:get_drag_overlay_tab_position(x, y, dragged_node, dragged_index)
local tab_index = self:get_tab_overlapping_point(x, y)
if not tab_index then
local first_tab_x = self:get_tab_rect(1)
if x < first_tab_x then
-- mouse before first visible tab
tab_index = self.tab_offset or 1
else
-- mouse after last visible tab
tab_index = self:get_visible_tabs_number() + (self.tab_offset - 1 or 0)
end
end
local tab_x, tab_y, tab_w, tab_h = self:get_tab_rect(tab_index)
if x > tab_x + tab_w / 2 and tab_index <= #self.views then
-- use next tab
tab_x = tab_x + tab_w
tab_index = tab_index + 1
end
if self == dragged_node and dragged_index and tab_index > dragged_index then
-- the tab we are moving is counted in tab_index
tab_index = tab_index - 1
tab_x = tab_x - tab_w
end
return tab_index, tab_x, tab_y, tab_w, tab_h
end
local RootView = View:extend() local RootView = View:extend()
function RootView:new() function RootView:new()
@ -677,6 +778,14 @@ function RootView:new()
self.root_node = Node() self.root_node = Node()
self.deferred_draws = {} self.deferred_draws = {}
self.mouse = { x = 0, y = 0 } self.mouse = { x = 0, y = 0 }
self.drag_overlay = { x = 0, y = 0, w = 0, h = 0, visible = false, opacity = 0,
base_color = style.drag_overlay,
color = { table.unpack(style.drag_overlay) } }
self.drag_overlay.to = { x = 0, y = 0, w = 0, h = 0 }
self.drag_overlay_tab = { x = 0, y = 0, w = 0, h = 0, visible = false, opacity = 0,
base_color = style.drag_overlay_tab,
color = { table.unpack(style.drag_overlay_tab) } }
self.drag_overlay_tab.to = { x = 0, y = 0, w = 0, h = 0 }
end end
@ -733,8 +842,8 @@ function RootView:open_doc(doc)
end end
function RootView:close_all_docviews() function RootView:close_all_docviews(keep_active)
self.root_node:close_all_docviews() self.root_node:close_all_docviews(keep_active)
end end
@ -748,38 +857,101 @@ function RootView:on_mouse_pressed(button, x, y, clicks)
local div = self.root_node:get_divider_overlapping_point(x, y) local div = self.root_node:get_divider_overlapping_point(x, y)
if div then if div then
self.dragged_divider = div self.dragged_divider = div
return return true
end end
local node = self.root_node:get_child_overlapping_point(x, y) local node = self.root_node:get_child_overlapping_point(x, y)
if node.hovered_scroll_button > 0 then if node.hovered_scroll_button > 0 then
node:scroll_tabs(node.hovered_scroll_button) node:scroll_tabs(node.hovered_scroll_button)
return return true
end end
local idx = node:get_tab_overlapping_point(x, y) local idx = node:get_tab_overlapping_point(x, y)
if idx then if idx then
if button == "middle" or node.hovered_close == idx then if button == "middle" or node.hovered_close == idx then
node:close_view(self.root_node, node.views[idx]) node:close_view(self.root_node, node.views[idx])
return true
else else
self.dragged_node = { node, idx } if button == "left" then
self.dragged_node = { node = node, idx = idx, dragging = false, drag_start_x = x, drag_start_y = y}
end
node:set_active_view(node.views[idx]) node:set_active_view(node.views[idx])
return true
end end
else elseif not self.dragged_node then -- avoid sending on_mouse_pressed events when dragging tabs
core.set_active_view(node.active_view) core.set_active_view(node.active_view)
if not self.on_view_mouse_pressed(button, x, y, clicks) then if not self.on_view_mouse_pressed(button, x, y, clicks) then
node.active_view:on_mouse_pressed(button, x, y, clicks) return node.active_view:on_mouse_pressed(button, x, y, clicks)
end end
end end
end end
function RootView:on_mouse_released(...) function RootView:get_overlay_base_color(overlay)
if overlay == self.drag_overlay then
return style.drag_overlay
else
return style.drag_overlay_tab
end
end
function RootView:set_show_overlay(overlay, status)
overlay.visible = status
if status then -- reset colors
-- reload base_color
overlay.base_color = self:get_overlay_base_color(overlay)
overlay.color[1] = overlay.base_color[1]
overlay.color[2] = overlay.base_color[2]
overlay.color[3] = overlay.base_color[3]
overlay.color[4] = overlay.base_color[4]
overlay.opacity = 0
end
end
function RootView:on_mouse_released(button, x, y, ...)
if self.dragged_divider then if self.dragged_divider then
self.dragged_divider = nil self.dragged_divider = nil
end end
if self.dragged_node then if self.dragged_node then
self.dragged_node = nil if button == "left" then
if self.dragged_node.dragging then
local node = self.root_node:get_child_overlapping_point(self.mouse.x, self.mouse.y)
local dragged_node = self.dragged_node.node
if node and not node.locked
-- don't do anything if dragging onto own node, with only one view
and (node ~= dragged_node or #node.views > 1) then
local split_type = node:get_split_type(self.mouse.x, self.mouse.y)
local view = dragged_node.views[self.dragged_node.idx]
if split_type ~= "middle" and split_type ~= "tab" then -- needs splitting
local new_node = node:split(split_type)
self.root_node:get_node_for_view(view):remove_view(self.root_node, view)
new_node:add_view(view)
elseif split_type == "middle" and node ~= dragged_node then -- move to other node
dragged_node:remove_view(self.root_node, view)
node:add_view(view)
self.root_node:get_node_for_view(view):set_active_view(view)
elseif split_type == "tab" then -- move besides other tabs
local tab_index = node:get_drag_overlay_tab_position(self.mouse.x, self.mouse.y, dragged_node, self.dragged_node.idx)
dragged_node:remove_view(self.root_node, view)
node:add_view(view, tab_index)
self.root_node:get_node_for_view(view):set_active_view(view)
end
self.root_node:update_layout()
core.redraw = true
end
end
self:set_show_overlay(self.drag_overlay, false)
self:set_show_overlay(self.drag_overlay_tab, false)
if self.dragged_node and self.dragged_node.dragging then
core.request_cursor("arrow")
end
self.dragged_node = nil
end
else -- avoid sending on_mouse_released events when dragging tabs
self.root_node:on_mouse_released(button, x, y, ...)
end end
self.root_node:on_mouse_released(...)
end end
@ -815,40 +987,33 @@ function RootView:on_mouse_moved(x, y, dx, dy)
end end
self.mouse.x, self.mouse.y = x, y self.mouse.x, self.mouse.y = x, y
local dn = self.dragged_node
if dn and not dn.dragging then
-- start dragging only after enough movement
dn.dragging = common.distance(x, y, dn.drag_start_x, dn.drag_start_y) > style.tab_width * .05
if dn.dragging then
core.request_cursor("hand")
end
end
-- avoid sending on_mouse_moved events when dragging tabs
if dn then return end
self.root_node:on_mouse_moved(x, y, dx, dy) self.root_node:on_mouse_moved(x, y, dx, dy)
local node = self.root_node:get_child_overlapping_point(x, y) self.overlapping_node = self.root_node:get_child_overlapping_point(x, y)
local div = self.root_node:get_divider_overlapping_point(x, y) local div = self.root_node:get_divider_overlapping_point(x, y)
local tab_index = node and node:get_tab_overlapping_point(x, y) local tab_index = self.overlapping_node and self.overlapping_node:get_tab_overlapping_point(x, y)
if node and node:get_scroll_button_index(x, y) then if self.overlapping_node and self.overlapping_node:get_scroll_button_index(x, y) then
core.request_cursor("arrow") core.request_cursor("arrow")
elseif div then elseif div then
local axis = (div.type == "hsplit" and "x" or "y") core.request_cursor(div.type == "hsplit" and "sizeh" or "sizev")
if div.a:is_resizable(axis) and div.b:is_resizable(axis) then
core.request_cursor(div.type == "hsplit" and "sizeh" or "sizev")
end
elseif tab_index then elseif tab_index then
core.request_cursor("arrow") core.request_cursor("arrow")
elseif node then elseif self.overlapping_node then
core.request_cursor(node.active_view.cursor) core.request_cursor(self.overlapping_node.active_view.cursor)
end
if node and self.dragged_node and (self.dragged_node[1] ~= node or (tab_index and self.dragged_node[2] ~= tab_index))
and node.type == "leaf" and #node.views > 0 and node.views[1]:is(DocView) then
local tab = self.dragged_node[1].views[self.dragged_node[2]]
if self.dragged_node[1] ~= node then
for i, v in ipairs(node.views) do if v.doc == tab.doc then tab = nil break end end
if tab then
self.dragged_node[1]:remove_view(self.root_node, tab)
node:add_view(tab, tab_index)
self.root_node:update_layout()
self.dragged_node = { node, tab_index or #node.views }
core.redraw = true
end
else
table.remove(self.dragged_node[1].views, self.dragged_node[2])
table.insert(node.views, tab_index, tab)
self.dragged_node = { node, tab_index }
end
end end
end end
@ -856,7 +1021,7 @@ end
function RootView:on_mouse_wheel(...) function RootView:on_mouse_wheel(...)
local x, y = self.mouse.x, self.mouse.y local x, y = self.mouse.x, self.mouse.y
local node = self.root_node:get_child_overlapping_point(x, y) local node = self.root_node:get_child_overlapping_point(x, y)
node.active_view:on_mouse_wheel(...) return node.active_view:on_mouse_wheel(...)
end end
@ -870,10 +1035,110 @@ function RootView:on_focus_lost(...)
core.redraw = true core.redraw = true
end end
function RootView:interpolate_drag_overlay(overlay)
self:move_towards(overlay, "x", overlay.to.x)
self:move_towards(overlay, "y", overlay.to.y)
self:move_towards(overlay, "w", overlay.to.w)
self:move_towards(overlay, "h", overlay.to.h)
self:move_towards(overlay, "opacity", overlay.visible and 100 or 0)
overlay.color[4] = overlay.base_color[4] * overlay.opacity / 100
end
function RootView:update() function RootView:update()
copy_position_and_size(self.root_node, self) copy_position_and_size(self.root_node, self)
self.root_node:update() self.root_node:update()
self.root_node:update_layout() self.root_node:update_layout()
self:update_drag_overlay()
self:interpolate_drag_overlay(self.drag_overlay)
self:interpolate_drag_overlay(self.drag_overlay_tab)
end
function RootView:set_drag_overlay(overlay, x, y, w, h, immediate)
overlay.to.x = x
overlay.to.y = y
overlay.to.w = w
overlay.to.h = h
if immediate then
overlay.x = x
overlay.y = y
overlay.w = w
overlay.h = h
end
if not overlay.visible then
self:set_show_overlay(overlay, true)
end
end
local function get_split_sizes(split_type, x, y, w, h)
if split_type == "left" then
w = w * .5
elseif split_type == "right" then
x = x + w * .5
w = w * .5
elseif split_type == "up" then
h = h * .5
elseif split_type == "down" then
y = y + h * .5
h = h * .5
end
return x, y, w, h
end
function RootView:update_drag_overlay()
if not (self.dragged_node and self.dragged_node.dragging) then return end
local over = self.root_node:get_child_overlapping_point(self.mouse.x, self.mouse.y)
if over and not over.locked then
local _, _, _, tab_h = over:get_scroll_button_rect(1)
local x, y = over.position.x, over.position.y
local w, h = over.size.x, over.size.y
local split_type = over:get_split_type(self.mouse.x, self.mouse.y)
if split_type == "tab" and (over ~= self.dragged_node.node or #over.views > 1) then
local tab_index, tab_x, tab_y, tab_w, tab_h = over:get_drag_overlay_tab_position(self.mouse.x, self.mouse.y)
self:set_drag_overlay(self.drag_overlay_tab,
tab_x + (tab_index and 0 or tab_w), tab_y,
style.caret_width, tab_h,
-- avoid showing tab overlay moving between nodes
over ~= self.drag_overlay_tab.last_over)
self:set_show_overlay(self.drag_overlay, false)
self.drag_overlay_tab.last_over = over
else
if (over ~= self.dragged_node.node or #over.views > 1) then
y = y + tab_h
h = h - tab_h
x, y, w, h = get_split_sizes(split_type, x, y, w, h)
end
self:set_drag_overlay(self.drag_overlay, x, y, w, h)
self:set_show_overlay(self.drag_overlay_tab, false)
end
else
self:set_show_overlay(self.drag_overlay, false)
self:set_show_overlay(self.drag_overlay_tab, false)
end
end
function RootView:draw_grabbed_tab()
local dn = self.dragged_node
local _,_, w, h = dn.node:get_tab_rect(dn.idx)
local x = self.mouse.x - w / 2
local y = self.mouse.y - h / 2
local text = dn.node.views[dn.idx] and dn.node.views[dn.idx]:get_name() or ""
self.root_node:draw_tab(text, true, true, false, x, y, w, h, true)
end
function RootView:draw_drag_overlay(ov)
if ov.opacity > 0 then
renderer.draw_rect(ov.x, ov.y, ov.w, ov.h, ov.color)
end
end end
@ -883,6 +1148,12 @@ function RootView:draw()
local t = table.remove(self.deferred_draws) local t = table.remove(self.deferred_draws)
t.fn(table.unpack(t)) t.fn(table.unpack(t))
end end
self:draw_drag_overlay(self.drag_overlay)
self:draw_drag_overlay(self.drag_overlay_tab)
if self.dragged_node and self.dragged_node.dragging then
self:draw_grabbed_tab()
end
if core.cursor_change_req then if core.cursor_change_req then
system.set_cursor(core.cursor_change_req) system.set_cursor(core.cursor_change_req)
core.cursor_change_req = nil core.cursor_change_req = nil

View File

@ -1,6 +1,6 @@
-- this file is used by lite-xl to setup the Lua environment when starting -- this file is used by lite-xl to setup the Lua environment when starting
VERSION = "2.0-beta1" VERSION = "@PROJECT_VERSION@"
MOD_VERSION = "1" MOD_VERSION = "2"
SCALE = tonumber(os.getenv("LITE_SCALE")) or SCALE SCALE = tonumber(os.getenv("LITE_SCALE")) or SCALE
PATHSEP = package.config:sub(1, 1) PATHSEP = package.config:sub(1, 1)
@ -20,3 +20,14 @@ package.path = DATADIR .. '/?/init.lua;' .. package.path
package.path = USERDIR .. '/?.lua;' .. package.path package.path = USERDIR .. '/?.lua;' .. package.path
package.path = USERDIR .. '/?/init.lua;' .. package.path package.path = USERDIR .. '/?/init.lua;' .. package.path
local dynamic_suffix = PLATFORM == "Mac OS X" and 'lib' or (PLATFORM == "Windows" and 'dll' or 'so')
package.cpath = DATADIR .. '/?.' .. dynamic_suffix .. ";" .. USERDIR .. '/?.' .. dynamic_suffix
package.native_plugins = {}
package.searchers = { package.searchers[1], package.searchers[2], function(modname)
local path = package.searchpath(modname, package.cpath)
if not path then return nil end
return system.load_native_plugin, path
end }
table.pack = table.pack or pack or function(...) return {...} end
table.unpack = table.unpack or unpack

View File

@ -6,6 +6,7 @@ local style = require "core.style"
local DocView = require "core.docview" local DocView = require "core.docview"
local LogView = require "core.logview" local LogView = require "core.logview"
local View = require "core.view" local View = require "core.view"
local Object = require "core.object"
local StatusView = View:extend() local StatusView = View:extend()
@ -70,7 +71,7 @@ local function draw_items(self, items, x, y, draw_fn)
local color = style.text local color = style.text
for _, item in ipairs(items) do for _, item in ipairs(items) do
if type(item) == "userdata" then if Object.is(item, renderer.font) then
font = item font = item
elseif type(item) == "table" then elseif type(item) == "table" then
color = item color = item

View File

@ -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. -- 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. -- 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", 15 * SCALE)
style.big_font = style.font:copy(40 * SCALE) style.big_font = style.font:copy(46 * SCALE)
style.icon_font = renderer.font.load(DATADIR .. "/fonts/icons.ttf", 14 * SCALE, {antialiasing="grayscale", hinting="full"}) style.icon_font = renderer.font.load(DATADIR .. "/fonts/icons.ttf", 16 * SCALE, {antialiasing="grayscale", hinting="full"})
style.icon_big_font = style.icon_font:copy(20 * SCALE) style.icon_big_font = style.icon_font:copy(23 * SCALE)
style.code_font = renderer.font.load(DATADIR .. "/fonts/JetBrainsMono-Regular.ttf", 13 * SCALE) style.code_font = renderer.font.load(DATADIR .. "/fonts/JetBrainsMono-Regular.ttf", 15 * SCALE)
style.background = { common.color "#2e2e32" } style.background = { common.color "#2e2e32" }
style.background2 = { common.color "#252529" } style.background2 = { common.color "#252529" }
@ -44,6 +44,8 @@ style.scrollbar2 = { common.color "#4b4b52" }
style.nagbar = { common.color "#FF0000" } style.nagbar = { common.color "#FF0000" }
style.nagbar_text = { common.color "#FFFFFF" } style.nagbar_text = { common.color "#FFFFFF" }
style.nagbar_dim = { common.color "rgba(0, 0, 0, 0.45)" } style.nagbar_dim = { common.color "rgba(0, 0, 0, 0.45)" }
style.drag_overlay = { common.color "rgba(255,255,255,0.1)" }
style.drag_overlay_tab = { common.color "#93DDFA" }
style.syntax = {} style.syntax = {}
style.syntax["normal"] = { common.color "#e1e1e6" } style.syntax["normal"] = { common.color "#e1e1e6" }

View File

@ -155,7 +155,7 @@ function tokenizer.tokenize(incoming_syntax, text, state)
if count % 2 == 0 then break end if count % 2 == 0 then break end
end end
until not res[1] or not close or not target[3] until not res[1] or not close or not target[3]
return unpack(res) return table.unpack(res)
end end
while i <= #text do while i <= #text do

View File

@ -7,6 +7,10 @@ local Object = require "core.object"
local View = Object:extend() 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() function View:new()
self.position = { x = 0, y = 0 } self.position = { x = 0, y = 0 }
@ -98,13 +102,9 @@ function View:on_text_input(text)
-- no-op -- no-op
end end
function View:on_mouse_wheel(y) function View:on_mouse_wheel(y)
if self.scrollable then
self.scroll.to.y = self.scroll.to.y + y * -config.mouse_wheel_scroll
end
end
end
function View:get_content_bounds() function View:get_content_bounds()
local x = self.scroll.x local x = self.scroll.x

View File

@ -1,4 +1,4 @@
-- mod-version:1 -- lite-xl 1.16 -- mod-version:2 -- lite-xl 2.0
local core = require "core" local core = require "core"
local common = require "core.common" local common = require "core.common"
local config = require "core.config" local config = require "core.config"
@ -8,25 +8,65 @@ local keymap = require "core.keymap"
local translate = require "core.doc.translate" local translate = require "core.doc.translate"
local RootView = require "core.rootview" local RootView = require "core.rootview"
local DocView = require "core.docview" local DocView = require "core.docview"
local Doc = require "core.doc"
config.plugins.autocomplete = { max_suggestions = 6 } config.plugins.autocomplete = {
-- Amount of characters that need to be written for autocomplete
min_len = 3,
-- The max amount of visible items
max_height = 6,
-- The max amount of scrollable items
max_suggestions = 100,
}
local autocomplete = {} local autocomplete = {}
autocomplete.map = {}
autocomplete.map = {}
autocomplete.map_manually = {}
autocomplete.on_close = nil
-- Flag that indicates if the autocomplete box was manually triggered
-- with the autocomplete.complete() function to prevent the suggestions
-- from getting cluttered with arbitrary document symbols by using the
-- autocomplete.map_manually table.
local triggered_manually = false
local mt = { __tostring = function(t) return t.text end } local mt = { __tostring = function(t) return t.text end }
function autocomplete.add(t) function autocomplete.add(t, triggered_manually)
local items = {} local items = {}
for text, info in pairs(t.items) do for text, info in pairs(t.items) do
info = (type(info) == "string") and info if type(info) == "table" then
table.insert(items, setmetatable({ text = text, info = info }, mt)) 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 end
autocomplete.map[t.name] = { files = t.files or ".*", items = items }
end 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() core.add_thread(function()
local cache = setmetatable({}, { __mode = "k" }) local cache = setmetatable({}, { __mode = "k" })
@ -109,16 +149,39 @@ local last_line, last_col
local function reset_suggestions() local function reset_suggestions()
suggestions_idx = 1 suggestions_idx = 1
suggestions = {} 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 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 function update_suggestions()
local doc = core.active_view.doc local doc = core.active_view.doc
local filename = doc and doc.filename or "" 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 -- get all relevant suggestions for given filename
local items = {} local items = {}
for _, v in pairs(autocomplete.map) do for _, v in pairs(map) do
if common.match_pattern(filename, v.files) then if common.match_pattern(filename, v.files) then
for _, item in pairs(v.items) do for _, item in pairs(v.items) do
table.insert(items, item) table.insert(items, item)
@ -138,7 +201,6 @@ local function update_suggestions()
end end
end end
local function get_partial_symbol() local function get_partial_symbol()
local doc = core.active_view.doc local doc = core.active_view.doc
local line2, col2 = doc:get_selection() local line2, col2 = doc:get_selection()
@ -146,14 +208,12 @@ local function get_partial_symbol()
return doc:get_text(line1, col1, line2, col2) return doc:get_text(line1, col1, line2, col2)
end end
local function get_active_view() local function get_active_view()
if getmetatable(core.active_view) == DocView then if getmetatable(core.active_view) == DocView then
return core.active_view return core.active_view
end end
end end
local function get_suggestions_rect(av) local function get_suggestions_rect(av)
if #suggestions == 0 then if #suggestions == 0 then
return 0, 0, 0, 0 return 0, 0, 0, 0
@ -175,15 +235,67 @@ local function get_suggestions_rect(av)
max_width = math.max(max_width, w) max_width = math.max(max_width, w)
end 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 return
x - style.padding.x, x - style.padding.x,
y - style.padding.y, y - style.padding.y,
max_width + style.padding.x * 2, max_width + style.padding.x * 2,
#suggestions * (th + style.padding.y) + style.padding.y max_items * (th + style.padding.y) + style.padding.y
end 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) local function draw_suggestions_box(av)
if #suggestions <= 0 then
return
end
local ah = config.plugins.autocomplete.max_height
-- draw background rect -- draw background rect
local rx, ry, rw, rh = get_suggestions_rect(av) local rx, ry, rw, rh = get_suggestions_rect(av)
renderer.draw_rect(rx, ry, rw, rh, style.background3) 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 font = av:get_font()
local lh = font:get_height() + style.padding.y local lh = font:get_height() + style.padding.y
local y = ry + style.padding.y / 2 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 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) common.draw_text(font, color, s.text, "left", rx + style.padding.x, y, rw, lh)
if s.info then 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) common.draw_text(style.font, color, s.info, "right", rx, y, rw - style.padding.x, lh)
end end
y = y + lh 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
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 end
local function show_autocomplete()
-- patch event logic into RootView
local on_text_input = RootView.on_text_input
local update = RootView.update
local draw = RootView.draw
RootView.on_text_input = function(...)
on_text_input(...)
local av = get_active_view() local av = get_active_view()
if av then if av then
-- update partial symbol and suggestions -- update partial symbol and suggestions
partial = get_partial_symbol() partial = get_partial_symbol()
if #partial >= 3 then
if #partial >= config.plugins.autocomplete.min_len or triggered_manually then
update_suggestions() 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 else
reset_suggestions() reset_suggestions()
end end
@ -233,6 +381,30 @@ RootView.on_text_input = function(...)
end end
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(...) RootView.update = function(...)
update(...) update(...)
@ -241,13 +413,19 @@ RootView.update = function(...)
if av then if av then
-- reset suggestions if caret was moved -- reset suggestions if caret was moved
local line, col = av.doc:get_selection() 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 end
end end
RootView.draw = function(...) RootView.draw = function(...)
draw(...) draw(...)
@ -258,12 +436,53 @@ RootView.draw = function(...)
end end
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() local function predicate()
return get_active_view() and #suggestions > 0 return get_active_view() and #suggestions > 0
end end
command.add(predicate, { command.add(predicate, {
["autocomplete:complete"] = function() ["autocomplete:complete"] = function()
local doc = core.active_view.doc local doc = core.active_view.doc
@ -283,12 +502,19 @@ command.add(predicate, {
suggestions_idx = math.min(suggestions_idx + 1, #suggestions) suggestions_idx = math.min(suggestions_idx + 1, #suggestions)
end, end,
["autocomplete:cycle"] = function()
local newidx = suggestions_idx + 1
suggestions_idx = newidx > #suggestions and 1 or newidx
end,
["autocomplete:cancel"] = function() ["autocomplete:cancel"] = function()
reset_suggestions() reset_suggestions()
end, end,
}) })
--
-- Keymaps
--
keymap.add { keymap.add {
["tab"] = "autocomplete:complete", ["tab"] = "autocomplete:complete",
["up"] = "autocomplete:previous", ["up"] = "autocomplete:previous",

View File

@ -1,4 +1,4 @@
-- mod-version:1 -- lite-xl 1.16 -- mod-version:2 -- lite-xl 2.0
local core = require "core" local core = require "core"
local config = require "core.config" local config = require "core.config"
local Doc = require "core.doc" local Doc = require "core.doc"

View File

@ -1,4 +1,4 @@
-- mod-version:1 -- lite-xl 1.16 -- mod-version:2 -- lite-xl 2.0
local core = require "core" local core = require "core"
local command = require "core.command" local command = require "core.command"
local keymap = require "core.keymap" local keymap = require "core.keymap"
@ -42,6 +42,24 @@ keymap.add {
["menu"] = "context:show" ["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 if require("plugins.scale") then
menu:register("core.docview", { menu:register("core.docview", {
{ text = "Font +", command = "scale:increase" }, { text = "Font +", command = "scale:increase" },

View File

@ -1,4 +1,4 @@
-- mod-version:1 -- lite-xl 1.16 -- mod-version:2 -- lite-xl 2.0
local core = require "core" local core = require "core"
local command = require "core.command" local command = require "core.command"
local common = require "core.common" local common = require "core.common"
@ -102,6 +102,11 @@ end
local function update_cache(doc) local function update_cache(doc)
local type, size, score = detect_indent_stat(doc) local type, size, score = detect_indent_stat(doc)
local score_threshold = 4 local score_threshold = 4
if score < score_threshold then
-- use default values
type = config.tab_type
size = config.indent_size
end
cache[doc] = { type = type, size = size, confirmed = (score >= score_threshold) } cache[doc] = { type = type, size = size, confirmed = (score >= score_threshold) }
doc.indent_info = cache[doc] doc.indent_info = cache[doc]
end end
@ -111,20 +116,14 @@ local new = Doc.new
function Doc:new(...) function Doc:new(...)
new(self, ...) new(self, ...)
update_cache(self) update_cache(self)
if not cache[self].confirmed then
core.add_thread(function ()
while not cache[self].confirmed do
update_cache(self)
coroutine.yield(1)
end
end, self)
end
end end
local clean = Doc.clean local clean = Doc.clean
function Doc:clean(...) function Doc:clean(...)
clean(self, ...) clean(self, ...)
update_cache(self) if not cache[self].confirmed then
update_cache(self)
end
end end
@ -152,3 +151,78 @@ function DocView:draw(...)
return with_indent_override(self.doc, draw, self, ...) return with_indent_override(self.doc, draw, self, ...)
end end
local function set_indent_type(doc, type)
cache[doc] = {type = type,
size = cache[doc].value or config.indent_size,
confirmed = true}
doc.indent_info = cache[doc]
end
local function set_indent_type_command()
core.command_view:enter(
"Specify indent style for this file",
function(value) -- submit
local doc = core.active_view.doc
value = value:lower()
set_indent_type(doc, value == "tabs" and "hard" or "soft")
end,
function(text) -- suggest
return common.fuzzy_match({"tabs", "spaces"}, text)
end,
nil, -- cancel
function(text) -- validate
local t = text:lower()
return t == "tabs" or t == "spaces"
end
)
end
local function set_indent_size(doc, size)
cache[doc] = {type = cache[doc].type or config.tab_type,
size = size,
confirmed = true}
doc.indent_info = cache[doc]
end
local function set_indent_size_command()
core.command_view:enter(
"Specify indent size for current file",
function(value) -- submit
local value = math.floor(tonumber(value))
local doc = core.active_view.doc
set_indent_size(doc, value)
end,
nil, -- suggest
nil, -- cancel
function(value) -- validate
local value = tonumber(value)
return value ~= nil and value >= 1
end
)
end
command.add("core.docview", {
["indent:set-file-indent-type"] = set_indent_type_command,
["indent:set-file-indent-size"] = set_indent_size_command
})
command.add(function()
return core.active_view:is(DocView)
and cache[core.active_view.doc]
and cache[core.active_view.doc].type == "soft"
end, {
["indent:switch-file-to-tabs-indentation"] = function() set_indent_type(core.active_view.doc, "hard") end
})
command.add(function()
return core.active_view:is(DocView)
and cache[core.active_view.doc]
and cache[core.active_view.doc].type == "hard"
end, {
["indent:switch-file-to-spaces-indentation"] = function() set_indent_type(core.active_view.doc, "soft") end
})

View File

@ -0,0 +1,36 @@
-- mod-version:2 -- lite-xl 2.0
local style = require "core.style"
local DocView = require "core.docview"
local common = require "core.common"
local draw_line_text = DocView.draw_line_text
function DocView:draw_line_text(idx, x, y)
local font = (self:get_font() or style.syntax_fonts["comment"])
local color = style.syntax.comment
local ty = y + self:get_line_text_y_offset()
local tx
local text, offset, s, e = self.doc.lines[idx], 1
local x1, _, x2, _ = self:get_content_bounds()
local _offset = self:get_x_offset_col(idx, x1)
offset = _offset
while true do
s, e = text:find(" +", offset)
if not s then break end
tx = self:get_col_x_offset(idx, s) + x
renderer.draw_text(font, string.rep("·", e - s + 1), tx, ty, color)
if tx > x + x2 then break end
offset = e + 1
end
offset = _offset
while true do
s, e = text:find("\t", offset)
if not s then break end
tx = self:get_col_x_offset(idx, s) + x
renderer.draw_text(font, "»", tx, ty, color)
if tx > x + x2 then break end
offset = e + 1
end
draw_line_text(self, idx, x, y)
end

View File

@ -1,4 +1,4 @@
-- mod-version:1 -- lite-xl 1.16 -- mod-version:2 -- lite-xl 2.0
local syntax = require "core.syntax" local syntax = require "core.syntax"
syntax.add { syntax.add {
@ -55,6 +55,17 @@ syntax.add {
["true"] = "literal", ["true"] = "literal",
["false"] = "literal", ["false"] = "literal",
["NULL"] = "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",
}, },
} }

View File

@ -1,4 +1,4 @@
-- mod-version:1 -- lite-xl 1.16 -- mod-version:2 -- lite-xl 2.0
pcall(require, "plugins.language_c") pcall(require, "plugins.language_c")
local syntax = require "core.syntax" local syntax = require "core.syntax"

View File

@ -1,4 +1,4 @@
-- mod-version:1 -- lite-xl 1.16 -- mod-version:2 -- lite-xl 2.0
local syntax = require "core.syntax" local syntax = require "core.syntax"
syntax.add { syntax.add {

View File

@ -1,4 +1,4 @@
-- mod-version:1 -- lite-xl 1.16 -- mod-version:2 -- lite-xl 2.0
local syntax = require "core.syntax" local syntax = require "core.syntax"
syntax.add { syntax.add {

View File

@ -1,4 +1,4 @@
-- mod-version:1 -- lite-xl 1.16 -- mod-version:2 -- lite-xl 2.0
local syntax = require "core.syntax" local syntax = require "core.syntax"
syntax.add { syntax.add {

View File

@ -1,4 +1,4 @@
-- mod-version:1 -- lite-xl 1.16 -- mod-version:2 -- lite-xl 2.0
local syntax = require "core.syntax" local syntax = require "core.syntax"
syntax.add { syntax.add {

View File

@ -1,22 +1,41 @@
-- mod-version:1 -- lite-xl 1.16 -- mod-version:2 -- lite-xl 2.0
local syntax = require "core.syntax" local syntax = require "core.syntax"
syntax.add { syntax.add {
files = { "%.md$", "%.markdown$" }, files = { "%.md$", "%.markdown$" },
patterns = { patterns = {
{ pattern = "\\.", type = "normal" }, { pattern = "\\.", type = "normal" },
{ pattern = { "<!%-%-", "%-%->" }, type = "comment" }, { pattern = { "<!%-%-", "%-%->" }, type = "comment" },
{ pattern = { "```", "```" }, type = "string" }, { pattern = { "```c", "```" }, type = "string", syntax = ".c" },
{ pattern = { "``", "``", "\\" }, type = "string" }, { pattern = { "```c++", "```" }, type = "string", syntax = ".cpp" },
{ pattern = { "`", "`", "\\" }, type = "string" }, { pattern = { "```python", "```" }, type = "string", syntax = ".py" },
{ pattern = { "~~", "~~", "\\" }, type = "keyword2" }, { pattern = { "```ruby", "```" }, type = "string", syntax = ".rb" },
{ pattern = "%-%-%-+", type = "comment" }, { pattern = { "```perl", "```" }, type = "string", syntax = ".pl" },
{ pattern = "%*%s+", type = "operator" }, { pattern = { "```php", "```" }, type = "string", syntax = ".php" },
{ pattern = { "%*", "[%*\n]", "\\" }, type = "operator" }, { pattern = { "```javascript", "```" }, type = "string", syntax = ".js" },
{ pattern = { "%_", "[%_\n]", "\\" }, type = "keyword2" }, { pattern = { "```html", "```" }, type = "string", syntax = ".html" },
{ pattern = "#.-\n", type = "keyword" }, { pattern = { "```xml", "```" }, type = "string", syntax = ".xml" },
{ pattern = "!?%[.-%]%(.-%)", type = "function" }, { pattern = { "```css", "```" }, type = "string", syntax = ".css" },
{ pattern = "https?://%S+", type = "function" }, { pattern = { "```lua", "```" }, type = "string", syntax = ".lua" },
{ pattern = { "```bash", "```" }, type = "string", syntax = ".sh" },
{ pattern = { "```java", "```" }, type = "string", syntax = ".java" },
{ pattern = { "```c#", "```" }, type = "string", syntax = ".cs" },
{ pattern = { "```cmake", "```" }, type = "string", syntax = ".cmake" },
{ pattern = { "```d", "```" }, type = "string", syntax = ".d" },
{ pattern = { "```glsl", "```" }, type = "string", syntax = ".glsl" },
{ pattern = { "```", "```" }, type = "string" },
{ pattern = { "``", "``", "\\" }, type = "string" },
{ pattern = { "`", "`", "\\" }, type = "string" },
{ pattern = { "~~", "~~", "\\" }, type = "keyword2" },
{ pattern = "%-%-%-+", type = "comment" },
{ pattern = "%*%s+", type = "operator" },
{ pattern = { "%*", "[%*\n]", "\\" }, type = "operator" },
{ pattern = { "%_", "[%_\n]", "\\" }, type = "keyword2" },
{ pattern = "#.-\n", type = "keyword" },
{ pattern = "!?%[.-%]%(.-%)", type = "function" },
{ pattern = "https?://%S+", type = "function" },
}, },
symbols = { }, symbols = { },
} }

View File

@ -1,8 +1,8 @@
-- mod-version:1 -- lite-xl 1.16 -- mod-version:2 -- lite-xl 2.0
local syntax = require "core.syntax" local syntax = require "core.syntax"
syntax.add { syntax.add {
files = { "%.py$", "%.pyw$" }, files = { "%.py$", "%.pyw$", "%.rpy$" },
headers = "^#!.*[ /]python", headers = "^#!.*[ /]python",
comment = "#", comment = "#",
patterns = { patterns = {

View File

@ -1,4 +1,4 @@
-- mod-version:1 -- lite-xl 1.16 -- mod-version:2 -- lite-xl 2.0
local syntax = require "core.syntax" local syntax = require "core.syntax"
syntax.add { syntax.add {

View File

@ -1,4 +1,4 @@
-- mod-version:1 -- lite-xl 1.16 -- mod-version:2 -- lite-xl 2.0
local config = require "core.config" local config = require "core.config"
local style = require "core.style" local style = require "core.style"
local DocView = require "core.docview" local DocView = require "core.docview"
@ -6,9 +6,7 @@ local DocView = require "core.docview"
local draw_overlay = DocView.draw_overlay local draw_overlay = DocView.draw_overlay
function DocView:draw_overlay(...) function DocView:draw_overlay(...)
local ns = self:get_font():get_width_subpixel("n") * config.line_limit local offset = self:get_font():get_width("n") * config.line_limit
local ss = self:get_font():subpixel_scale()
local offset = ns / ss
local x = self:get_line_screen_position(1) + offset local x = self:get_line_screen_position(1) + offset
local y = self.position.y local y = self.position.y
local w = math.ceil(SCALE * 1) local w = math.ceil(SCALE * 1)

View File

@ -1,4 +1,4 @@
-- mod-version:1 -- lite-xl 1.16 -- mod-version:2 -- lite-xl 2.0
local core = require "core" local core = require "core"
local command = require "core.command" local command = require "core.command"
local keymap = require "core.keymap" local keymap = require "core.keymap"

View File

@ -1,4 +1,4 @@
-- mod-version:1 -- lite-xl 1.16 -- mod-version:2 -- lite-xl 2.0
local core = require "core" local core = require "core"
local common = require "core.common" local common = require "core.common"
local keymap = require "core.keymap" local keymap = require "core.keymap"
@ -9,6 +9,7 @@ local View = require "core.view"
local ResultsView = View:extend() local ResultsView = View:extend()
ResultsView.context = "session"
function ResultsView:new(text, fn) function ResultsView:new(text, fn)
ResultsView.super.new(self) ResultsView.super.new(self)
@ -91,7 +92,7 @@ end
function ResultsView:on_mouse_pressed(...) function ResultsView:on_mouse_pressed(...)
local caught = ResultsView.super.on_mouse_pressed(self, ...) local caught = ResultsView.super.on_mouse_pressed(self, ...)
if not caught then if not caught then
self:open_selected_result() return self:open_selected_result()
end end
end end
@ -107,6 +108,7 @@ function ResultsView:open_selected_result()
dv.doc:set_selection(res.line, res.col) dv.doc:set_selection(res.line, res.col)
dv:scroll_to_line(res.line, false, true) dv:scroll_to_line(res.line, false, true)
end) end)
return true
end end
@ -170,7 +172,7 @@ function ResultsView:draw()
local ox, oy = self:get_content_offset() local ox, oy = self:get_content_offset()
local x, y = ox + style.padding.x, oy + style.padding.y local x, y = ox + style.padding.x, oy + style.padding.y
local files_number = core.project_files_number() 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 local text
if self.searching then if self.searching then
if files_number then if files_number then

View File

@ -1,4 +1,4 @@
-- mod-version:1 -- lite-xl 1.16 -- mod-version:2 -- lite-xl 2.0
local core = require "core" local core = require "core"
local command = require "core.command" local command = require "core.command"
local keymap = require "core.keymap" local keymap = require "core.keymap"

View File

@ -1,4 +1,4 @@
-- mod-version:1 -- lite-xl 1.16 -- mod-version:2 -- lite-xl 2.0
local core = require "core" local core = require "core"
local config = require "core.config" local config = require "core.config"
local command = require "core.command" local command = require "core.command"

View File

@ -1,4 +1,4 @@
-- mod-version:1 -- lite-xl 1.16 -- mod-version:2 -- lite-xl 2.0
local core = require "core" local core = require "core"
local common = require "core.common" local common = require "core.common"
local command = require "core.command" local command = require "core.command"
@ -13,7 +13,6 @@ config.plugins.scale = {
use_mousewheel = true use_mousewheel = true
} }
local scale_level = 0
local scale_steps = 0.05 local scale_steps = 0.05
local current_scale = SCALE local current_scale = SCALE
@ -34,9 +33,6 @@ local function set_scale(scale)
local s = scale / current_scale local s = scale / current_scale
current_scale = scale current_scale = scale
-- we set scale_level in case this was called by user
scale_level = (scale - default_scale) / scale_steps
if config.plugins.scale.mode == "ui" then if config.plugins.scale.mode == "ui" then
SCALE = scale SCALE = scale
@ -48,10 +44,14 @@ local function set_scale(scale)
style.tab_width = style.tab_width * s style.tab_width = style.tab_width * s
for _, name in ipairs {"font", "big_font", "icon_font", "icon_big_font", "code_font"} do for _, name in ipairs {"font", "big_font", "icon_font", "icon_big_font", "code_font"} do
renderer.font.set_size(style[name], s * style[name]:get_size()) style[name] = renderer.font.copy(style[name], s * style[name]:get_size())
end end
else else
renderer.font.set_size(style.code_font, s * style.code_font:get_size()) style.code_font = renderer.font.copy(style.code_font, s * style.code_font:get_size())
end
for _, font in pairs(style.syntax_fonts) do
renderer.font.set_size(font, s * font:get_size())
end end
-- restore scroll positions -- restore scroll positions
@ -67,30 +67,16 @@ local function get_scale()
return current_scale return current_scale
end end
local on_mouse_wheel = RootView.on_mouse_wheel
function RootView:on_mouse_wheel(d, ...)
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
return on_mouse_wheel(self, d, ...)
end
end
local function res_scale() local function res_scale()
scale_level = 0 set_scale(default_scale)
set_scale(default_scale)
end end
local function inc_scale() local function inc_scale()
scale_level = scale_level + 1 set_scale(current_scale + scale_steps)
set_scale(default_scale + scale_level * scale_steps)
end end
local function dec_scale() local function dec_scale()
scale_level = scale_level - 1 set_scale(current_scale - scale_steps)
set_scale(default_scale + scale_level * scale_steps)
end end
@ -104,6 +90,8 @@ keymap.add {
["ctrl+0"] = "scale:reset", ["ctrl+0"] = "scale:reset",
["ctrl+-"] = "scale:decrease", ["ctrl+-"] = "scale:decrease",
["ctrl+="] = "scale:increase", ["ctrl+="] = "scale:increase",
["ctrl+wheelup"] = "scale:increase",
["ctrl+wheeldown"] = "scale:decrease"
} }
return { return {

View File

@ -1,4 +1,4 @@
-- mod-version:1 -- lite-xl 1.16 -- mod-version:2 -- lite-xl 2.0
local core = require "core" local core = require "core"
local command = require "core.command" local command = require "core.command"
local translate = require "core.doc.translate" local translate = require "core.doc.translate"

View File

@ -1,4 +1,4 @@
-- mod-version:1 -- lite-xl 1.16 -- mod-version:2 -- lite-xl 2.0
local core = require "core" local core = require "core"
local common = require "core.common" local common = require "core.common"
local command = require "core.command" local command = require "core.command"

View File

@ -1,4 +1,4 @@
-- mod-version:1 -- lite-xl 1.16 -- mod-version:2 -- lite-xl 2.0
local core = require "core" local core = require "core"
local common = require "core.common" local common = require "core.common"
local command = require "core.command" local command = require "core.command"
@ -243,7 +243,7 @@ function TreeView:on_mouse_pressed(button, x, y, clicks)
end end
else else
core.try(function() core.try(function()
local doc_filename = common.relative_path(core.project_dir, hovered_item.abs_filename) local doc_filename = core.normalize_to_project_dir(hovered_item.abs_filename)
core.root_view:open_doc(core.open_doc(doc_filename)) core.root_view:open_doc(core.open_doc(doc_filename))
end) end)
end end
@ -437,15 +437,31 @@ menu:register(
command.add(nil, { command.add(nil, {
["treeview:toggle"] = function() ["treeview:toggle"] = function()
view.visible = not view.visible view.visible = not view.visible
end, end})
command.add(function() return view.hovered_item ~= nil end, {
["treeview:rename"] = function() ["treeview:rename"] = function()
local old_filename = view.hovered_item.filename 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:set_text(old_filename)
core.command_view:enter("Rename", function(filename) core.command_view:enter("Rename", function(filename)
os.rename(old_filename, filename) filename = core.normalize_to_project_dir(filename)
local abs_filename = core.project_absolute_path(filename)
local res, err = os.rename(old_abs_filename, abs_filename)
if res then -- successfully renamed
for _, doc in ipairs(core.docs) do
if doc.abs_filename and old_abs_filename == doc.abs_filename then
doc:set_filename(filename, abs_filename) -- make doc point to the new filename
doc:reset_syntax()
break -- only first needed
end
end
core.log("Renamed \"%s\" to \"%s\"", old_filename, filename)
else
core.error("Error while renaming \"%s\" to \"%s\": %s", old_abs_filename, abs_filename, err)
end
core.reschedule_project_scan() core.reschedule_project_scan()
core.log("Renamed \"%s\" to \"%s\"", old_filename, filename)
end, common.path_suggest) end, common.path_suggest)
end, end,
@ -521,7 +537,7 @@ command.add(nil, {
local hovered_item = view.hovered_item local hovered_item = view.hovered_item
if PLATFORM == "Windows" then if PLATFORM == "Windows" then
system.exec("start " .. hovered_item.abs_filename) system.exec(string.format("start \"\" %q", hovered_item.abs_filename))
elseif string.find(PLATFORM, "Mac") then elseif string.find(PLATFORM, "Mac") then
system.exec(string.format("open %q", hovered_item.abs_filename)) system.exec(string.format("open %q", hovered_item.abs_filename))
elseif PLATFORM == "Linux" then elseif PLATFORM == "Linux" then

View File

@ -1,4 +1,4 @@
-- mod-version:1 -- lite-xl 1.16 -- mod-version:2 -- lite-xl 2.0
local core = require "core" local core = require "core"
local command = require "core.command" local command = require "core.command"
local Doc = require "core.doc" local Doc = require "core.doc"

View File

@ -1,4 +1,4 @@
-- mod-version:1 -- lite-xl 1.16 -- mod-version:2 -- lite-xl 2.0
local core = require "core" local core = require "core"
local common = require "core.common" local common = require "core.common"
local DocView = require "core.docview" local DocView = require "core.docview"

View File

@ -19,6 +19,9 @@ renderer.color = {}
---@class renderer.fontoptions ---@class renderer.fontoptions
---@field public antialiasing "'grayscale'" | "'subpixel'" ---@field public antialiasing "'grayscale'" | "'subpixel'"
---@field public hinting "'slight'" | "'none'" | '"full"' ---@field public hinting "'slight'" | "'none'" | '"full"'
-- @field public bold boolean
-- @field public italic boolean
-- @field public underline boolean
renderer.fontoptions = {} renderer.fontoptions = {}
--- ---

File diff suppressed because it is too large Load Diff

View File

@ -1,198 +0,0 @@
//----------------------------------------------------------------------------
// Anti-Grain Geometry - Version 2.4
// Copyright (C) 2002-2005 Maxim Shemanarev (http://www.antigrain.com)
//
// Permission to copy, use, modify, sell and distribute this software
// is granted provided this copyright notice appears in all copies.
// This software is provided "as is" without express or implied
// warranty, and with no claim as to its suitability for any purpose.
//
//----------------------------------------------------------------------------
// Contact: mcseem@antigrain.com
// mcseemagg@yahoo.com
// http://www.antigrain.com
//----------------------------------------------------------------------------
//
// See implementation agg_font_freetype.cpp
//
//----------------------------------------------------------------------------
#ifndef AGG_FONT_FREETYPE_INCLUDED
#define AGG_FONT_FREETYPE_INCLUDED
#include <ft2build.h>
#include FT_FREETYPE_H
#include "agg_scanline_storage_aa.h"
#include "agg_scanline_storage_bin.h"
#include "agg_scanline_u.h"
#include "agg_scanline_bin.h"
#include "agg_path_storage_integer.h"
#include "agg_rasterizer_scanline_aa.h"
#include "agg_conv_curve.h"
#include "agg_font_cache_manager.h"
#include "agg_trans_affine.h"
namespace agg
{
//-----------------------------------------------font_engine_freetype_base
class font_engine_freetype_base
{
public:
//--------------------------------------------------------------------
typedef serialized_scanlines_adaptor_aa<int8u> gray8_adaptor_type;
typedef serialized_scanlines_adaptor_bin mono_adaptor_type;
typedef scanline_storage_aa8 scanlines_aa_type;
typedef scanline_storage_bin scanlines_bin_type;
//--------------------------------------------------------------------
~font_engine_freetype_base();
font_engine_freetype_base(bool flag32, unsigned max_faces = 32);
// Set font parameters
//--------------------------------------------------------------------
void resolution(unsigned dpi);
bool load_font(const char* font_name, unsigned face_index, glyph_rendering ren_type,
const char* font_mem = 0, const long font_mem_size = 0);
bool attach(const char* file_name);
bool char_map(FT_Encoding map);
bool height(double h);
bool width(double w);
void hinting(bool h);
void flip_y(bool f);
void transform(const trans_affine& affine);
// Set Gamma
//--------------------------------------------------------------------
template<class GammaF> void gamma(const GammaF& f)
{
m_rasterizer.gamma(f);
}
// Accessors
//--------------------------------------------------------------------
int last_error() const { return m_last_error; }
unsigned resolution() const { return m_resolution; }
const char* name() const { return m_name; }
unsigned num_faces() const;
FT_Encoding char_map() const { return m_char_map; }
double height() const { return double(m_height) / 64.0; }
double width() const { return double(m_width) / 64.0; }
double ascender() const;
double descender() const;
int face_height() const;
int face_units_em()const;
bool hinting() const { return m_hinting; }
bool flip_y() const { return m_flip_y; }
// Interface mandatory to implement for font_cache_manager
//--------------------------------------------------------------------
const char* font_signature() const { return m_signature; }
int change_stamp() const { return m_change_stamp; }
bool prepare_glyph(unsigned glyph_code);
unsigned glyph_index() const { return m_glyph_index; }
unsigned data_size() const { return m_data_size; }
glyph_data_type data_type() const { return m_data_type; }
const rect_i& bounds() const { return m_bounds; }
double advance_x() const { return m_advance_x; }
double advance_y() const { return m_advance_y; }
void write_glyph_to(int8u* data) const;
bool add_kerning(unsigned first, unsigned second,
double* x, double* y);
private:
font_engine_freetype_base(const font_engine_freetype_base&);
const font_engine_freetype_base& operator = (const font_engine_freetype_base&);
void update_char_size();
void update_signature();
int find_face(const char* face_name) const;
bool m_flag32;
int m_change_stamp;
int m_last_error;
char* m_name;
unsigned m_name_len;
unsigned m_face_index;
FT_Encoding m_char_map;
char* m_signature;
unsigned m_height;
unsigned m_width;
bool m_hinting;
bool m_flip_y;
bool m_library_initialized;
FT_Library m_library; // handle to library
FT_Face* m_faces; // A pool of font faces
char** m_face_names;
unsigned m_num_faces;
unsigned m_max_faces;
FT_Face m_cur_face; // handle to the current face object
int m_resolution;
glyph_rendering m_glyph_rendering;
unsigned m_glyph_index;
unsigned m_data_size;
glyph_data_type m_data_type;
rect_i m_bounds;
double m_advance_x;
double m_advance_y;
trans_affine m_affine;
path_storage_integer<int16, 6> m_path16;
path_storage_integer<int32, 6> m_path32;
conv_curve<path_storage_integer<int16, 6> > m_curves16;
conv_curve<path_storage_integer<int32, 6> > m_curves32;
scanline_u8 m_scanline_aa;
scanline_bin m_scanline_bin;
scanlines_aa_type m_scanlines_aa;
scanlines_bin_type m_scanlines_bin;
rasterizer_scanline_aa<> m_rasterizer;
};
//------------------------------------------------font_engine_freetype_int16
// This class uses values of type int16 (10.6 format) for the vector cache.
// The vector cache is compact, but when rendering glyphs of height
// more that 200 there integer overflow can occur.
//
class font_engine_freetype_int16 : public font_engine_freetype_base
{
public:
typedef serialized_integer_path_adaptor<int16, 6> path_adaptor_type;
typedef font_engine_freetype_base::gray8_adaptor_type gray8_adaptor_type;
typedef font_engine_freetype_base::mono_adaptor_type mono_adaptor_type;
typedef font_engine_freetype_base::scanlines_aa_type scanlines_aa_type;
typedef font_engine_freetype_base::scanlines_bin_type scanlines_bin_type;
font_engine_freetype_int16(unsigned max_faces = 32) :
font_engine_freetype_base(false, max_faces) {}
};
//------------------------------------------------font_engine_freetype_int32
// This class uses values of type int32 (26.6 format) for the vector cache.
// The vector cache is twice larger than in font_engine_freetype_int16,
// but it allows you to render glyphs of very large sizes.
//
class font_engine_freetype_int32 : public font_engine_freetype_base
{
public:
typedef serialized_integer_path_adaptor<int32, 6> path_adaptor_type;
typedef font_engine_freetype_base::gray8_adaptor_type gray8_adaptor_type;
typedef font_engine_freetype_base::mono_adaptor_type mono_adaptor_type;
typedef font_engine_freetype_base::scanlines_aa_type scanlines_aa_type;
typedef font_engine_freetype_base::scanlines_bin_type scanlines_bin_type;
font_engine_freetype_int32(unsigned max_faces = 32) :
font_engine_freetype_base(true, max_faces) {}
};
}
#endif

View File

@ -1,73 +0,0 @@
// Adapted by Francesco Abbate for GSL Shell
// Original code's copyright below.
//----------------------------------------------------------------------------
// Anti-Grain Geometry - Version 2.4
// Copyright (C) 2002-2005 Maxim Shemanarev (http://www.antigrain.com)
//
// Permission to copy, use, modify, sell and distribute this software
// is granted provided this copyright notice appears in all copies.
// This software is provided "as is" without express or implied
// warranty, and with no claim as to its suitability for any purpose.
//
//----------------------------------------------------------------------------
// Contact: mcseem@antigrain.com
// mcseemagg@yahoo.com
// http://www.antigrain.com
//----------------------------------------------------------------------------
#ifndef AGG_LCD_DISTRIBUTION_LUT_INCLUDED
#define AGG_LCD_DISTRIBUTION_LUT_INCLUDED
#include <cstdlib>
#include "agg_basics.h"
namespace agg
{
//=====================================================lcd_distribution_lut
class lcd_distribution_lut
{
public:
lcd_distribution_lut(double prim, double second, double tert)
{
double norm = 1.0 / (prim + second*2 + tert*2);
prim *= norm;
second *= norm;
tert *= norm;
for(unsigned i = 0; i < 256; i++)
{
unsigned b = (i << 8);
unsigned s = round(second * b);
unsigned t = round(tert * b);
unsigned p = b - (2*s + 2*t);
m_data[3*i + 1] = s; /* secondary */
m_data[3*i + 2] = t; /* tertiary */
m_data[3*i ] = p; /* primary */
}
}
unsigned convolution(const int8u* covers, int i0, int i_min, int i_max) const
{
unsigned sum = 0;
int k_min = (i0 >= i_min + 2 ? -2 : i_min - i0);
int k_max = (i0 <= i_max - 2 ? 2 : i_max - i0);
for (int k = k_min; k <= k_max; k++)
{
/* select the primary, secondary or tertiary channel */
int channel = std::abs(k) % 3;
int8u c = covers[i0 + k];
sum += m_data[3*c + channel];
}
return (sum + 128) >> 8;
}
private:
unsigned short m_data[256*3];
};
}
#endif

View File

@ -1,93 +0,0 @@
#pragma once
#include <string.h>
#include "agg_basics.h"
#include "agg_rendering_buffer.h"
namespace agg
{
// This is a special purpose color type that only has the alpha channel.
// It can be thought as a gray color but with the intensity always on.
// It is actually used to store coverage information.
struct alpha8
{
typedef int8u value_type;
typedef int32u calc_type;
typedef int32 long_type;
enum base_scale_e
{
base_shift = 8,
base_scale = 1 << base_shift,
base_mask = base_scale - 1
};
value_type a;
//--------------------------------------------------------------------
alpha8(unsigned a_=base_mask) :
a(int8u(a_)) {}
};
// Pixer format to store coverage information.
class pixfmt_alpha8
{
public:
typedef alpha8 color_type;
typedef int8u value_type;
typedef int32u calc_type;
typedef agg::rendering_buffer::row_data row_data;
//--------------------------------------------------------------------
pixfmt_alpha8(rendering_buffer& rb): m_rbuf(&rb)
{
}
//--------------------------------------------------------------------
unsigned width() const {
return m_rbuf->width();
}
unsigned height() const {
return m_rbuf->height();
}
// This method should never be called when using the scanline_u8.
// The use of scanline_p8 should be avoided because if does not works
// properly for rendering fonts because single hspan are split in many
// hline/hspan elements and pixel whitening happens.
void blend_hline(int x, int y, unsigned len,
const color_type& c, int8u cover)
{ }
void copy_hline(int x, int y, unsigned len, const color_type& c)
{
value_type* p = (value_type*) m_rbuf->row_ptr(y) + x;
do
{
*p = c.a;
p++;
}
while(--len);
}
//--------------------------------------------------------------------
void blend_solid_hspan(int x, int y,
unsigned len,
const color_type& c,
const int8u* covers)
{
value_type* p = (value_type*) m_rbuf->row_ptr(y) + x;
do
{
calc_type alpha = (calc_type(c.a) * (calc_type(*covers) + 1)) >> 8;
*p = alpha;
p++;
++covers;
}
while(--len);
}
private:
rendering_buffer* m_rbuf;
};
}

View File

@ -1,445 +0,0 @@
#include "font_renderer.h"
#include "agg_lcd_distribution_lut.h"
#include "agg_pixfmt_rgb.h"
#include "agg_pixfmt_rgba.h"
#include "font_renderer_alpha.h"
// Important: when a subpixel scale is used the width below will be the width in logical pixel.
// As each logical pixel contains 3 subpixels it means that the 'pixels' pointer
// will hold enough space for '3 * width' uint8_t values.
struct FR_Bitmap {
agg::int8u *pixels;
int width, height;
};
class FR_Renderer {
public:
// Conventional LUT values: (1./3., 2./9., 1./9.)
// The values below are fine tuned as in the Elementary Plot library.
FR_Renderer(bool hinting, bool kerning, bool subpixel, bool prescale_x) :
m_renderer(hinting, kerning, subpixel, prescale_x),
m_lcd_lut(0.448, 0.184, 0.092),
m_subpixel(subpixel)
{ }
font_renderer_alpha& renderer_alpha() { return m_renderer; }
agg::lcd_distribution_lut& lcd_distribution_lut() { return m_lcd_lut; }
int subpixel_scale() const { return (m_subpixel ? 3 : 1); }
private:
font_renderer_alpha m_renderer;
agg::lcd_distribution_lut m_lcd_lut;
int m_subpixel;
};
FR_Renderer *FR_Renderer_New(unsigned int flags) {
bool hinting = ((flags & FR_HINTING) != 0);
bool kerning = ((flags & FR_KERNING) != 0);
bool subpixel = ((flags & FR_SUBPIXEL) != 0);
bool prescale_x = ((flags & FR_PRESCALE_X) != 0);
return new FR_Renderer(hinting, kerning, subpixel, prescale_x);
}
FR_Bitmap* FR_Bitmap_New(FR_Renderer *font_renderer, int width, int height) {
const int subpixel_scale = font_renderer->subpixel_scale();
FR_Bitmap *image = (FR_Bitmap *) malloc(sizeof(FR_Bitmap) + width * height * subpixel_scale);
if (!image) { return NULL; }
image->pixels = (agg::int8u *) (image + 1);
image->width = width;
image->height = height;
return image;
}
void FR_Bitmap_Free(FR_Bitmap *image) {
free(image);
}
void FR_Renderer_Free(FR_Renderer *font_renderer) {
delete font_renderer;
}
int FR_Subpixel_Scale(FR_Renderer *font_renderer) {
return font_renderer->subpixel_scale();
}
int FR_Load_Font(FR_Renderer *font_renderer, const char *filename) {
bool success = font_renderer->renderer_alpha().load_font(filename);
return (success ? 0 : 1);
}
int FR_Get_Font_Height(FR_Renderer *font_renderer, float size) {
font_renderer_alpha& renderer_alpha = font_renderer->renderer_alpha();
double ascender, descender;
renderer_alpha.get_font_vmetrics(ascender, descender);
int face_height = renderer_alpha.get_face_height();
float scale = renderer_alpha.scale_for_em_to_pixels(size);
return int((ascender - descender) * face_height * scale + 0.5);
}
static void glyph_trim_rect(agg::rendering_buffer& ren_buf, FR_Bitmap_Glyph_Metrics& gli, int subpixel_scale) {
const int height = ren_buf.height();
int x0 = gli.x0 * subpixel_scale, x1 = gli.x1 * subpixel_scale;
int y0 = gli.y0, y1 = gli.y1;
for (int y = gli.y0; y < gli.y1; y++) {
const uint8_t *row = ren_buf.row_ptr(height - 1 - y);
unsigned int row_bitsum = 0;
for (int x = x0; x < x1; x++) {
row_bitsum |= row[x];
}
if (row_bitsum == 0) {
y0++;
} else {
break;
}
}
for (int y = gli.y1 - 1; y >= y0; y--) {
const uint8_t *row = ren_buf.row_ptr(height - 1 - y);
unsigned int row_bitsum = 0;
for (int x = x0; x < x1; x++) {
row_bitsum |= row[x];
}
if (row_bitsum == 0) {
y1--;
} else {
break;
}
}
for (int x = gli.x0 * subpixel_scale; x < gli.x1 * subpixel_scale; x += subpixel_scale) {
unsigned int xaccu = 0;
for (int y = y0; y < y1; y++) {
const uint8_t *row = ren_buf.row_ptr(height - 1 - y);
for (int i = 0; i < subpixel_scale; i++) {
xaccu |= row[x + i];
}
}
if (xaccu == 0) {
x0 += subpixel_scale;
} else {
break;
}
}
for (int x = (gli.x1 - 1) * subpixel_scale; x >= x0; x -= subpixel_scale) {
unsigned int xaccu = 0;
for (int y = y0; y < y1; y++) {
const uint8_t *row = ren_buf.row_ptr(height - 1 - y);
for (int i = 0; i < subpixel_scale; i++) {
xaccu |= row[x + i];
}
}
if (xaccu == 0) {
x1 -= subpixel_scale;
} else {
break;
}
}
gli.xoff += (x0 / subpixel_scale) - gli.x0;
gli.yoff += (y0 - gli.y0);
gli.x0 = x0 / subpixel_scale;
gli.y0 = y0;
gli.x1 = x1 / subpixel_scale;
gli.y1 = y1;
}
static void glyph_lut_convolution(agg::rendering_buffer ren_buf, agg::lcd_distribution_lut& lcd_lut, agg::int8u *covers_buf, FR_Bitmap_Glyph_Metrics& gli) {
const int subpixel = 3;
const int x0 = gli.x0, y0 = gli.y0, x1 = gli.x1, y1 = gli.y1;
const int len = (x1 - x0) * subpixel;
const int height = ren_buf.height();
for (int y = y0; y < y1; y++) {
agg::int8u *covers = ren_buf.row_ptr(height - 1 - y) + x0 * subpixel;
memcpy(covers_buf, covers, len);
for (int x = x0 - 1; x < x1 + 1; x++) {
for (int i = 0; i < subpixel; i++) {
const int cx = (x - x0) * subpixel + i;
covers[cx] = lcd_lut.convolution(covers_buf, cx, 0, len - 1);
}
}
}
gli.x0 -= 1;
gli.x1 += 1;
gli.xoff -= 1;
}
// The two functions below are needed because in C and C++ integer division
// is rounded toward zero.
// euclidean division rounded toward positive infinite
static int div_pos(int n, int p) {
return n >= 0 ? (n + p - 1) / p : (n / p);
}
// euclidean division rounded toward negative infinite
static int div_neg(int n, int p) {
return n >= 0 ? (n / p) : ((n - p + 1) / p);
}
FR_Bitmap *FR_Bake_Font_Bitmap(FR_Renderer *font_renderer, int font_height,
int first_char, int num_chars, FR_Bitmap_Glyph_Metrics *glyphs)
{
font_renderer_alpha& renderer_alpha = font_renderer->renderer_alpha();
agg::lcd_distribution_lut& lcd_lut = font_renderer->lcd_distribution_lut();
const int subpixel_scale = font_renderer->subpixel_scale();
double ascender, descender;
renderer_alpha.get_font_vmetrics(ascender, descender);
const int ascender_px = int(ascender * font_height);
const int pad_y = 1;
// When using subpixel font rendering it is needed to leave a padding pixel on the left and on the right.
// Since each pixel is composed by n subpixel we set below x_start to subpixel_scale instead than zero.
// In addition we need one more pixel on the left because of subpixel positioning so
// it adds up to 2 * subpixel_scale.
// Note about the coordinates: they are AGG-like so x is positive toward the right and
// y is positive in the upper direction.
const int x_start = 2 * subpixel_scale;
const agg::alpha8 text_color(0xff);
#ifdef FONT_RENDERER_HEIGHT_HACK
const int font_height_reduced = (font_height * 86) / 100;
#else
const int font_height_reduced = font_height;
#endif
renderer_alpha.set_font_height(font_height_reduced);
int *index = (int *) malloc(num_chars * sizeof(int));
agg::rect_i *bounds = (agg::rect_i *) malloc(num_chars * sizeof(agg::rect_i));
if (!index || !bounds) {
free(index);
free(bounds);
return NULL;
}
int x_size_sum = 0, glyph_count = 0;
for (int i = 0; i < num_chars; i++) {
int codepoint = first_char + i;
index[i] = i;
if (renderer_alpha.codepoint_bounds(codepoint, subpixel_scale, bounds[i])) {
// Invalid glyph
bounds[i].x1 = 0;
bounds[i].y1 = 0;
bounds[i].x2 = -1;
bounds[i].y2 = -1;
} else {
if (bounds[i].x2 > bounds[i].x1) {
x_size_sum += bounds[i].x2 - bounds[i].x1;
glyph_count++;
}
bounds[i].x1 = subpixel_scale * div_neg(bounds[i].x1, subpixel_scale);
bounds[i].x2 = subpixel_scale * div_pos(bounds[i].x2, subpixel_scale);
}
}
// Simple insertion sort algorithm: https://en.wikipedia.org/wiki/Insertion_sort
int i = 1;
while (i < num_chars) {
int j = i;
while (j > 0 && bounds[index[j-1]].y2 - bounds[index[j-1]].y1 > bounds[index[j]].y2 - bounds[index[j]].y1) {
int tmp = index[j];
index[j] = index[j-1];
index[j-1] = tmp;
j = j - 1;
}
i = i + 1;
}
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;
// dry run simulating pixel position to estimate required image's height
int x = x_start, y = 0, y_bottom = y;
for (int i = 0; i < num_chars; i++) {
const agg::rect_i& gbounds = bounds[index[i]];
if (gbounds.x2 < gbounds.x1) continue;
// 1. It is very important to ensure that the x's increment below (1) and in
// (2), (3) and (4) are perfectly the same.
// Note that x_step below is always an integer multiple of subpixel_scale.
const int x_step = gbounds.x2 + 3 * subpixel_scale;
if (x + x_step >= pixels_width * subpixel_scale) {
x = x_start;
y = y_bottom;
}
// 5. Ensure that y's increment below is exactly the same to the one used in (6)
const int glyph_y_bottom = y - 2 * pad_y - (gbounds.y2 - gbounds.y1);
y_bottom = (y_bottom > glyph_y_bottom ? glyph_y_bottom : y_bottom);
// 2. Ensure x's increment is aligned with (1)
x = x + x_step;
}
agg::int8u *cover_swap_buffer = (agg::int8u *) malloc(sizeof(agg::int8u) * (pixels_width * subpixel_scale));
if (!cover_swap_buffer) {
free(index);
free(bounds);
return NULL;
}
const int pixels_height = -y_bottom + 1;
const int pixel_size = 1;
FR_Bitmap *image = FR_Bitmap_New(font_renderer, pixels_width, pixels_height);
if (!image) {
free(index);
free(bounds);
free(cover_swap_buffer);
return NULL;
}
agg::int8u *pixels = image->pixels;
memset(pixels, 0x00, pixels_width * pixels_height * subpixel_scale * pixel_size);
agg::rendering_buffer ren_buf(pixels, pixels_width * subpixel_scale, pixels_height, -pixels_width * subpixel_scale * pixel_size);
// The variable y_bottom will be used to go down to the next row by taking into
// account the space occupied by each glyph of the current row along the y direction.
x = x_start;
// Set y to the image's height minus one to begin writing glyphs in the upper part of the image.
y = pixels_height - 1;
y_bottom = y;
for (int i = 0; i < num_chars; i++) {
// Important: the variable x in this loop should always be an integer multiple
// of subpixel_scale.
int codepoint = first_char + index[i];
const agg::rect_i& gbounds = bounds[index[i]];
if (gbounds.x2 < gbounds.x1) continue;
// 3. Ensure x's increment is aligned with (1)
// Note that x_step below is always an integer multiple of subpixel_scale.
// We need 3 * subpixel_scale because:
// . +1 pixel on the left, because of RGB color filter
// . +1 pixel on the right, because of RGB color filter
// . +1 pixel on the right, because of subpixel positioning
// and each pixel requires "subpixel_scale" sub-pixels.
const int x_step = gbounds.x2 + 3 * subpixel_scale;
if (x + x_step >= pixels_width * subpixel_scale) {
// No more space along x, begin writing the row below.
x = x_start;
y = y_bottom;
}
const int y_baseline = y - pad_y - gbounds.y2;
// 6. Ensure the y's increment below is aligned with the increment used in (5)
const int glyph_y_bottom = y - 2 * pad_y - (gbounds.y2 - gbounds.y1);
y_bottom = (y_bottom > glyph_y_bottom ? glyph_y_bottom : y_bottom);
double x_next = x, y_next = y_baseline;
renderer_alpha.render_codepoint(ren_buf, text_color, x_next, y_next, codepoint, subpixel_scale);
// The y coordinate for the glyph below is positive in the bottom direction,
// like is used by Lite's drawing system.
FR_Bitmap_Glyph_Metrics& glyph_info = glyphs[index[i]];
glyph_info.x0 = x / subpixel_scale;
glyph_info.y0 = pixels_height - 1 - (y_baseline + gbounds.y2 + pad_y);
glyph_info.x1 = div_pos(x_next + 0.5, subpixel_scale);
glyph_info.y1 = pixels_height - 1 - (y_baseline + gbounds.y1 - pad_y);
glyph_info.xoff = 0;
glyph_info.yoff = -pad_y - gbounds.y2 + ascender_px;
// Note that below the xadvance is in pixels times the subpixel_scale.
// This is meant for subpixel positioning.
glyph_info.xadvance = roundf(x_next - x);
if (subpixel_scale != 1 && glyph_info.x1 > glyph_info.x0) {
glyph_lut_convolution(ren_buf, lcd_lut, cover_swap_buffer, glyph_info);
}
glyph_trim_rect(ren_buf, glyph_info, subpixel_scale);
// When subpixel is activated we need one padding pixel on the left and on the right
// and one more because of subpixel positioning.
// 4. Ensure x's increment is aligned with (1)
x = x + x_step;
}
free(index);
free(bounds);
free(cover_swap_buffer);
return image;
}
template <typename Order>
void blend_solid_hspan(agg::rendering_buffer& rbuf, int x, int y, unsigned len,
const agg::rgba8& c, const agg::int8u* covers)
{
const int pixel_size = 4;
agg::int8u* p = rbuf.row_ptr(y) + x * pixel_size;
do
{
const unsigned alpha = *covers;
const unsigned r = p[Order::R], g = p[Order::G], b = p[Order::B];
p[Order::R] = (((unsigned(c.r) - r) * alpha) >> 8) + r;
p[Order::G] = (((unsigned(c.g) - g) * alpha) >> 8) + g;
p[Order::B] = (((unsigned(c.b) - b) * alpha) >> 8) + b;
// Leave p[3], the alpha channel value unmodified.
p += 4;
++covers;
}
while(--len);
}
template <typename Order>
void blend_solid_hspan_subpixel(agg::rendering_buffer& rbuf, agg::lcd_distribution_lut& lcd_lut,
const int x, const int y, unsigned len,
const agg::rgba8& c,
const agg::int8u* covers)
{
const int pixel_size = 4;
const unsigned rgb[3] = { c.r, c.g, c.b };
agg::int8u* p = rbuf.row_ptr(y) + x * pixel_size;
// Indexes to adress RGB colors in a BGRA32 format.
const int pixel_index[3] = {Order::R, Order::G, Order::B};
for (unsigned cx = 0; cx < len; cx += 3)
{
for (int i = 0; i < 3; i++) {
const unsigned cover_value = covers[cx + i];
const unsigned alpha = (cover_value + 1) * (c.a + 1);
const unsigned src_col = *(p + pixel_index[i]);
*(p + pixel_index[i]) = (((rgb[i] - src_col) * alpha) + (src_col << 16)) >> 16;
}
// Leave p[3], the alpha channel value unmodified.
p += 4;
}
}
// destination implicitly BGRA32. Source implictly single-byte renderer_alpha coverage with subpixel scale = 3.
// FIXME: consider using something like RenColor* instead of uint8_t * for dst.
void FR_Blend_Glyph(FR_Renderer *font_renderer, FR_Clip_Area *clip, int x_mult, int y, uint8_t *dst, int dst_width, const FR_Bitmap *glyphs_bitmap, const FR_Bitmap_Glyph_Metrics *glyph, FR_Color color) {
agg::lcd_distribution_lut& lcd_lut = font_renderer->lcd_distribution_lut();
const int subpixel_scale = font_renderer->subpixel_scale();
const int pixel_size = 4; // Pixel size for BGRA32 format.
int x = x_mult / subpixel_scale;
x += glyph->xoff;
y += glyph->yoff;
int glyph_x = glyph->x0, glyph_y = glyph->y0;
int glyph_x_subpixel = -(x_mult % subpixel_scale);
int glyph_width = glyph->x1 - glyph->x0;
int glyph_height = glyph->y1 - glyph->y0;
int n;
if ((n = clip->left - x) > 0) { glyph_width -= n; glyph_x += n; x += n; }
if ((n = clip->top - y) > 0) { glyph_height -= n; glyph_y += n; y += n; }
if ((n = x + glyph_width - clip->right ) > 0) { glyph_width -= n; }
if ((n = y + glyph_height - clip->bottom) > 0) { glyph_height -= n; }
if (glyph_width <= 0 || glyph_height <= 0) {
return;
}
dst += (x + y * dst_width) * pixel_size;
agg::rendering_buffer dst_ren_buf(dst, glyph_width, glyph_height, dst_width * pixel_size);
uint8_t *src = glyphs_bitmap->pixels + (glyph_x + glyph_y * glyphs_bitmap->width) * subpixel_scale + glyph_x_subpixel;
int src_stride = glyphs_bitmap->width * subpixel_scale;
const agg::rgba8 color_a(color.r, color.g, color.b);
for (int x = 0, y = 0; y < glyph_height; y++) {
agg::int8u *covers = src + y * src_stride;
if (subpixel_scale == 1) {
blend_solid_hspan<agg::order_bgra>(dst_ren_buf, x, y, glyph_width, color_a, covers);
} else {
blend_solid_hspan_subpixel<agg::order_bgra>(dst_ren_buf, lcd_lut, x, y, glyph_width * subpixel_scale, color_a, covers);
}
}
}

View File

@ -1,58 +0,0 @@
#ifndef FONT_RENDERER_H
#define FONT_RENDERER_H
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef struct {
unsigned short x0, y0, x1, y1;
float xoff, yoff, xadvance;
} FR_Bitmap_Glyph_Metrics;
typedef struct FR_Bitmap FR_Bitmap;
#ifdef __cplusplus
class FR_Renderer;
#else
struct FR_Renderer;
typedef struct FR_Renderer FR_Renderer;
#endif
enum {
FR_HINTING = 1 << 0,
FR_KERNING = 1 << 1,
FR_SUBPIXEL = 1 << 2,
FR_PRESCALE_X = 1 << 3,
};
typedef struct {
uint8_t r, g, b;
} FR_Color;
typedef struct {
int left, top, right, bottom;
} FR_Clip_Area;
FR_Renderer * FR_Renderer_New(unsigned int flags);
void FR_Renderer_Free(FR_Renderer *);
int FR_Load_Font(FR_Renderer *, const char *filename);
FR_Bitmap* FR_Bitmap_New(FR_Renderer *, int width, int height);
void FR_Bitmap_Free(FR_Bitmap *image);
int FR_Get_Font_Height(FR_Renderer *, float size);
FR_Bitmap * FR_Bake_Font_Bitmap(FR_Renderer *, int font_height,
int first_char, int num_chars, FR_Bitmap_Glyph_Metrics *glyph_info);
void FR_Blend_Glyph(FR_Renderer *font_renderer,
FR_Clip_Area *clip, int x, int y,
uint8_t *dst, int dst_width,
const FR_Bitmap *glyphs_bitmap,
const FR_Bitmap_Glyph_Metrics *glyph, FR_Color color);
int FR_Subpixel_Scale(FR_Renderer *);
#ifdef __cplusplus
}
#endif
#endif

View File

@ -1,164 +0,0 @@
#pragma once
#include "agg_basics.h"
#include "agg_conv_curve.h"
#include "agg_conv_transform.h"
#include "agg_gamma_lut.h"
#include "agg_font_freetype.h"
#include "agg_pixfmt_alpha8.h"
#include "agg_rasterizer_scanline_aa.h"
#include "agg_renderer_primitives.h"
#include "agg_renderer_scanline.h"
#include "agg_rendering_buffer.h"
#include "agg_scanline_u.h"
class font_renderer_alpha
{
typedef agg::pixfmt_alpha8 pixfmt_type;
typedef agg::renderer_base<pixfmt_type> base_ren_type;
typedef agg::renderer_scanline_aa_solid<base_ren_type> renderer_solid;
typedef agg::font_engine_freetype_int32 font_engine_type;
typedef agg::font_cache_manager<font_engine_type> font_manager_type;
font_engine_type m_feng;
font_manager_type m_fman;
// Font rendering options.
bool m_hinting;
bool m_kerning;
bool m_subpixel;
bool m_prescale_x;
bool m_font_loaded;
// Pipeline to process the vectors glyph paths (curves + contour)
agg::trans_affine m_mtx;
agg::conv_curve<font_manager_type::path_adaptor_type> m_curves;
agg::conv_transform<agg::conv_curve<font_manager_type::path_adaptor_type> > m_trans;
public:
typedef agg::pixfmt_alpha8::color_type color_type;
font_renderer_alpha(bool hinting, bool kerning, bool subpixel, bool prescale_x):
m_feng(),
m_fman(m_feng),
m_hinting(hinting),
m_kerning(kerning),
m_subpixel(subpixel),
m_prescale_x(prescale_x),
m_font_loaded(false),
m_curves(m_fman.path_adaptor()),
m_trans(m_curves, m_mtx)
{ }
int get_face_height() const {
return m_feng.face_height();
}
void get_font_vmetrics(double& ascender, double& descender) {
double current_height = m_feng.height();
m_feng.height(1.0);
ascender = m_feng.ascender();
descender = m_feng.descender();
m_feng.height(current_height);
}
float scale_for_em_to_pixels(float size) {
int units_per_em = m_feng.face_units_em();
if (units_per_em > 0) {
return size / units_per_em;
}
return 0.0;
}
bool load_font(const char *font_filename) {
if(m_feng.load_font(font_filename, 0, agg::glyph_ren_outline)) {
m_font_loaded = true;
m_feng.hinting(m_hinting);
}
return m_font_loaded;
}
void set_font_height(double height) {
const double scale_x = (m_prescale_x ? 100.0 : 1.0);
m_feng.height(height);
m_feng.width(height * scale_x);
}
template<class Rasterizer, class Scanline, class RenSolid>
void draw_codepoint(Rasterizer& ras, Scanline& sl,
RenSolid& ren_solid, const color_type color,
int codepoint, double& x, double& y, const int subpixel_scale)
{
const double scale_x = (m_prescale_x ? 100.0 : 1.0);
// Coefficient to scale back the glyph to the final scale.
const double cx_inv_scale = subpixel_scale / scale_x;
// Represent the delta in x scaled by scale_x.
double x_delta = 0;
double start_x = x;
const agg::glyph_cache* glyph = m_fman.glyph(codepoint);
if(glyph)
{
if(m_kerning)
{
m_fman.add_kerning(&x_delta, &y);
}
m_fman.init_embedded_adaptors(glyph, 0, 0);
if(glyph->data_type == agg::glyph_data_outline)
{
double ty = m_hinting ? floor(y + 0.5) : y;
ras.reset();
m_mtx.reset();
m_mtx *= agg::trans_affine_scaling(cx_inv_scale, 1);
m_mtx *= agg::trans_affine_translation(start_x + cx_inv_scale * x_delta, ty);
ras.add_path(m_trans);
ren_solid.color(color);
agg::render_scanlines(ras, sl, ren_solid);
}
y += glyph->advance_y;
x += cx_inv_scale * (x_delta + glyph->advance_x);
}
}
void clear(agg::rendering_buffer& ren_buf, const color_type color) {
pixfmt_type pf(ren_buf);
base_ren_type ren_base(pf);
ren_base.clear(color);
}
void render_codepoint(agg::rendering_buffer& ren_buf,
const color_type text_color,
double& x, double& y,
int codepoint, const int subpixel_scale)
{
if (!m_font_loaded) {
return;
}
agg::scanline_u8 sl;
agg::rasterizer_scanline_aa<> ras;
ras.clip_box(0, 0, ren_buf.width(), ren_buf.height());
agg::pixfmt_alpha8 pf(ren_buf);
base_ren_type ren_base(pf);
renderer_solid ren_solid(ren_base);
draw_codepoint(ras, sl, ren_solid, text_color, codepoint, x, y, subpixel_scale);
}
int codepoint_bounds(int codepoint, const int subpixel_scale, agg::rect_i& bounds)
{
if (!m_font_loaded) return 1;
const double scale_x = (m_prescale_x ? 100.0 : 1.0);
const double cx_inv_scale = subpixel_scale / scale_x;
const agg::glyph_cache* glyph = m_fman.glyph(codepoint);
if (glyph) {
bounds = glyph->bounds;
bounds.x1 *= cx_inv_scale;
bounds.x2 *= cx_inv_scale;
return 0;
}
return 1;
}
};

View File

@ -1,23 +0,0 @@
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
font_renderer_sources = [
'agg_font_freetype.cpp',
'font_renderer.cpp',
]
font_renderer_cdefs = ['-DFONT_RENDERER_HEIGHT_HACK']
font_renderer_include = include_directories('.')
libfontrenderer = static_library('fontrenderer',
font_renderer_sources,
dependencies: [libagg_dep, freetype_dep],
cpp_args: font_renderer_cdefs,
)

View File

@ -1,125 +0,0 @@
```c
stbtt_InitFont
stbtt_ScaleForMappingEmToPixels x 3
stbtt_ScaleForPixelHeight
stbtt_BakeFontBitmap
stbtt_GetFontVMetrics x 2
typedef struct {
unsigned short x0, y0, x1, y1; // coordinates of bbox in bitmap
float xoff, yoff, xadvance;
} stbtt_bakedchar;
struct RenImage {
RenColor *pixels;
int width, height;
};
typedef struct {
RenImage *image;
stbtt_bakedchar glyphs[256];
} GlyphSet;
struct RenFont {
void *data;
stbtt_fontinfo stbfont;
GlyphSet *sets[MAX_GLYPHSET];
float size;
int height;
};
```
The function stbtt_BakeFontBitmap is used to write bitmap data into set->image->pixels (where set is a GlyphSet).
Note that set->image->pixels need data in RGB format. After stbtt_BakeFontBitmap call the bitmap data are converted into RGB.
With a single call many glyphs corresponding to a range of codepoints, all in a
single image.
## STB truetype font metrics
stbtt_ScaleForPixelHeight takes a float 'height' and returns height / (ascent - descent).
stbtt_ScaleForMappingEmToPixels take a float 'pixels' and returns pixels / unitsPerEm.
### Computing RenFont
When loading a font, in renderer.c, the font->height is determined as:
```c
int ascent, descent, linegap;
stbtt_GetFontVMetrics(&font->stbfont, &ascent, &descent, &linegap);
float scale = stbtt_ScaleForMappingEmToPixels(&font->stbfont, font->size);
font->height = (ascent - descent + linegap) * scale + 0.5;
```
so, mathematically
```c
font->height = (ascent - descent + linegap) * font->size / unitsPerEm + 0.5;
```
**TO DO**: find out for what font->height is actually used.
### Call to BakeFontBitmap
In the same file, renderer.c, to create the glyphset image it computes:
```c
// Using stbtt functions
float s = ScaleForMappingEmToPixels(1) / ScaleForPixelHeight(1);
```
so 's' is actually equal to (ascent - descent) / unitsPerEm.
Then BakeFontBitmap is called and `font->size * s` is used for the pixel_height argument.
So BakeFontBitmap gets
```c
pixel_height = (ascent - descent) * font->size / unitsPerEm;
```
In turns BakeFontBitmap passes pixel_height to ScaleForPixelHeight() and uses the
resulting 'scale' to scale the glyphs by passing it to MakeGlyphBitmap().
This is equal almost equal to font->height except the 0.5, the missing linegap calculation
and the fact that this latter is an integer instead of a float.
## AGG Font Engine
Calls
`FT_Init_FreeType` (initialize the library)
In `load_font()` method:
`FT_New_Face` or `FT_New_Memory_Face` (use `FT_Done_Face` when done)
`FT_Attach_File`
`FT_Select_Charmap`
In `update_char_size()` method:
`FT_Set_Char_Size` or `FT_Set_Pixel_Sizes`
In `prepare_glyph()` method:
`FT_Get_Char_Index`
`FT_Load_Glyph`
`FT_Render_Glyph` (if glyph_render_native_mono or native_gray8)
in `add_kerning()` method
`FT_Get_Kerning`
`FT_Done_FreeType` (end with library)
## Freetype2's metrics related function and structs
`FT_New_Face` return a `FT_Face` (a pointer) with font face information.
## AGG font engine's font size
The variable `m_height` is the size of the font muliplied by 64.
It will be used to set font size with:
- `FT_Set_Char_Size` if m_resolution is set (> 0)
- `FT_Set_Pixel_Sizes`, divided by 64, if m_resolution is not set (= 0)
The method height() returns m_height / 64;

View File

@ -1,74 +1,127 @@
project('lite-xl', 'c', 'cpp', default_options : ['c_std=gnu11', 'cpp_std=c++03']) project('lite-xl',
['c'],
version : '2.0.2',
license : 'MIT',
meson_version : '>= 0.54',
default_options : ['c_std=gnu11']
)
version = get_option('version') #===============================================================================
# Configuration
#===============================================================================
conf_data = configuration_data() conf_data = configuration_data()
conf_data.set('PROJECT_VERSION', version) 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' if host_machine.system() == 'darwin'
add_languages('objc') add_languages('objc')
endif endif
cc = meson.get_compiler('c') 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')
if not lua_dep.found()
lua_subproject = subproject('lua', default_options: ['shared=false', 'use_readline=false', 'app=false'])
lua_dep = lua_subproject.get_variable('lua_dep')
endif
reproc_subproject = subproject('reproc', default_options: ['default_library=static', 'multithreaded=false', 'reproc-cpp=false', 'examples=false'])
reproc_dep = reproc_subproject.get_variable('reproc_dep')
lite_deps = [lua_dep, sdl_dep, reproc_dep, pcre2_dep, libm, libdl, libx11]
if host_machine.system() == 'windows'
# Note that we need to explicitly add the windows socket DLL because
# the pkg-config file from reproc does not include it.
lite_deps += meson.get_compiler('cpp').find_library('ws2_32', required: true)
endif
lite_cargs = [] lite_cargs = []
if get_option('portable')
lite_docdir = 'doc'
lite_datadir = 'data'
else
lite_docdir = 'share/doc/lite-xl'
lite_datadir = 'share/lite-xl'
endif
lite_include = include_directories('src')
foreach data_module : ['core', 'fonts', 'plugins', 'colors']
install_subdir('data' / data_module , install_dir : lite_datadir)
endforeach
install_data('licenses/licenses.md', install_dir : lite_docdir)
lite_link_args = []
if cc.get_id() == 'gcc' and get_option('buildtype') == 'release'
lite_link_args += ['-static-libgcc', '-static-libstdc++']
endif
if host_machine.system() == 'darwin'
lite_link_args += ['-framework', 'CoreServices', '-framework', 'Foundation']
endif
lite_rc = []
if host_machine.system() == 'windows'
windows = import('windows')
lite_rc += windows.compile_resources('resources/icons/icon.rc')
iss = configure_file(input : 'scripts/innosetup/innosetup.iss.in',
output : 'innosetup.iss',
configuration : conf_data)
endif
# On macos we need to use the SDL renderer to support retina displays # On macos we need to use the SDL renderer to support retina displays
if get_option('renderer') or host_machine.system() == 'darwin' if get_option('renderer') or host_machine.system() == 'darwin'
lite_cargs += '-DLITE_USE_SDL_RENDERER' lite_cargs += '-DLITE_USE_SDL_RENDERER'
endif endif
#===============================================================================
# Linker Settings
#===============================================================================
lite_link_args = []
if cc.get_id() == 'gcc' and get_option('buildtype') == 'release'
lite_link_args += ['-static-libgcc']
endif
subdir('lib/font_renderer') if host_machine.system() == 'darwin'
subdir('src') 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)
lua_dep = dependency('lua5.2', fallback: ['lua', 'lua_dep'],
default_options: ['shared=false', 'use_readline=false', 'app=false']
)
pcre2_dep = dependency('libpcre2-8')
freetype_dep = dependency('freetype2')
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, freetype_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('c').find_library('ws2_32', required: true)
endif
endif
#===============================================================================
# Install Configuration
#===============================================================================
if get_option('portable') or host_machine.system() == 'windows'
lite_bindir = '/'
lite_docdir = '/doc'
lite_datadir = '/data'
elif get_option('bundle') and host_machine.system() == 'darwin'
lite_cargs += '-DMACOS_USE_BUNDLE'
lite_bindir = 'Contents/MacOS'
lite_docdir = 'Contents/Resources'
lite_datadir = 'Contents/Resources'
install_data('resources/icons/icon.icns', install_dir : 'Contents/Resources')
configure_file(
input : 'resources/macos/Info.plist.in',
output : 'Info.plist',
configuration : conf_data,
install : true,
install_dir : 'Contents'
)
else
lite_bindir = 'bin'
lite_docdir = 'share/doc/lite-xl'
lite_datadir = 'share/lite-xl'
if host_machine.system() == 'linux'
install_data('resources/icons/lite-xl.svg',
install_dir : 'share/icons/hicolor/scalable/apps'
)
install_data('resources/linux/org.lite_xl.lite_xl.desktop',
install_dir : 'share/applications'
)
install_data('resources/linux/org.lite_xl.lite_xl.appdata.xml',
install_dir : 'share/metainfo'
)
endif
endif
install_data('licenses/licenses.md', install_dir : lite_docdir)
install_subdir('data' / 'core' , install_dir : lite_datadir, exclude_files : 'start.lua')
foreach data_module : ['fonts', 'plugins', 'colors']
install_subdir('data' / data_module , install_dir : lite_datadir)
endforeach
configure_file(
input : 'data/core/start.lua',
output : 'start.lua',
configuration : conf_data,
install : true,
install_dir : lite_datadir / 'core',
)
#===============================================================================
# Targets
#===============================================================================
if not get_option('source-only')
subdir('src')
subdir('scripts')
endif

View File

@ -1,4 +1,4 @@
option('innosetup', type : 'boolean', value : false, description: 'Build Windows setup package') 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('portable', type : 'boolean', value : false, description: 'Portable install')
option('renderer', type : 'boolean', value : false, description: 'Use SDL renderer') option('renderer', type : 'boolean', value : false, description: 'Use SDL renderer')
option('version', type : 'string', value : '0.0.0', description: 'Project version')

Binary file not shown.

View File

@ -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>

View File

@ -5,6 +5,6 @@ Comment=A lightweight text editor written in Lua
Exec=lite-xl %F Exec=lite-xl %F
Icon=lite-xl Icon=lite-xl
Terminal=false Terminal=false
StartupNotify=false StartupWMClass=lite-xl
Categories=Utility;TextEditor;Development; Categories=Development;IDE;
MimeType=text/plain; MimeType=text/plain;

View File

@ -0,0 +1,360 @@
#ifndef LITE_XL_PLUGIN_API
#define LITE_XL_PLUGIN_API
/*
The lite_xl plugin API is quite simple. Any shared library can be a plugin file, so long
as it has an entrypoint that looks like the following, where xxxxx is the plugin name:
#include "lite_xl_plugin_api.h"
int lua_open_lite_xl_xxxxx(lua_State* L, void* XL) {
lite_xl_plugin_init(XL);
...
return 1;
}
In linux, to compile this file, you'd do: 'gcc -o xxxxx.so -shared xxxxx.c'. Simple!
Due to the way the API is structured, you *should not* link or include lua libraries.
This file was automatically generated by the below code. Do NOT MODIFY DIRECTLY.
#!/bin/sh
echo "#ifndef LITE_XL_PLUGIN_API"
echo "#define LITE_XL_PLUGIN_API"
echo "/* "
echo "The lite_xl plugin API is quite simple. Any shared library can be a plugin file, so long"
echo "as it has an entrypoint that looks like the following, where xxxxx is the plugin name:"
echo
echo '#include "lite_xl_plugin_api.h"'
echo
echo "int lua_open_lite_xl_xxxxx(lua_State* L, void* XL) {"
echo " lite_xl_plugin_init(XL);"
echo " ..."
echo " return 1;"
echo "}"
echo
echo "In linux, to compile this file, you'd do: 'gcc -o xxxxx.so -shared xxxxx.c'. Simple!"
echo "Due to the way the API is structured, you *should not* link or include lua libraries."
echo
echo "This file was automatically generated by the below code. Do NOT MODIFY DIRECTLY."
echo
echo
cat $0
echo "*""/"
echo "#include <stddef.h>"
echo "typedef struct lua_State lua_State; typedef double lua_Number; typedef int (*lua_CFunction)(lua_State*); typedef ptrdiff_t lua_Integer;"
echo "typedef unsigned long lua_Unsigned; typedef struct luaL_Buffer luaL_Buffer; typedef struct luaL_Reg luaL_Reg; typedef struct lua_Debug lua_Debug;"
echo "typedef void (*lua_Hook) (lua_State *L, lua_Debug *ar);"
echo "typedef void * (*lua_Alloc) (void *ud, void *ptr, size_t osize, size_t nsize);"
echo "typedef int (*lua_Writer) (lua_State *L, const void* p, size_t sz, void* ud);"
LUA_HEADERS=`pkg-config --cflags lua5.2 | sed 's/^-I//' | sed 's/$/\/*.h/'`
grep -h "^LUA\(LIB\)*_API" $LUA_HEADERS | sed "s/LUA\(LIB\)*_API //" | sed "s/(lua/(*lua/" | grep -v ",\s*$" | sed "s/^/static /"
grep -h "#define luaL*_" $LUA_HEADERS | grep -v "\\\s*$" | grep -v "\(assert\|lock\)" | grep -v "\(asm\|int32\)" | grep -v "#define lua_number2integer(i,n)\s*lua_number2int(i, n)"
echo "#define lua_pushliteral(L, s) lua_pushlstring(L, "" s, (sizeof(s)/sizeof(char))-1)"
echo "#define lua_pushglobaltable(L) lua_rawgeti(L, LUA_REGISTRYINDEX, LUA_RIDX_GLOBALS)"
echo "#define IMPORT_SYMBOL(name, ret, ...) name = (ret (*)(__VA_ARGS__))symbol(#name)"
echo "static void lite_xl_plugin_init(void* XL) {"
echo "\tvoid* (*symbol)(const char*) = (void* (*)(const char*))XL;"
grep -h "^LUA\(LIB\)*_API" $LUA_HEADERS | sed "s/LUA\(LIB\)*_API //" | sed "s/(lua/(*lua/" | grep -v ",\s*$" | sed "s/^\([^)]*\)(\*\(lua\w*\))\s*(/\tIMPORT_SYMBOL(\2, \1,/"
echo "}"
echo "#endif"
*/
#include <stddef.h>
typedef struct lua_State lua_State; typedef double lua_Number; typedef int (*lua_CFunction)(lua_State*); typedef ptrdiff_t lua_Integer;
typedef unsigned long lua_Unsigned; typedef struct luaL_Buffer luaL_Buffer; typedef struct luaL_Reg luaL_Reg; typedef struct lua_Debug lua_Debug;
typedef void (*lua_Hook) (lua_State *L, lua_Debug *ar);
typedef void * (*lua_Alloc) (void *ud, void *ptr, size_t osize, size_t nsize);
typedef int (*lua_Writer) (lua_State *L, const void* p, size_t sz, void* ud);
static void (*luaL_checkversion_) (lua_State *L, lua_Number ver);
static int (*luaL_getmetafield) (lua_State *L, int obj, const char *e);
static int (*luaL_callmeta) (lua_State *L, int obj, const char *e);
static const char *(*luaL_tolstring) (lua_State *L, int idx, size_t *len);
static int (*luaL_argerror) (lua_State *L, int numarg, const char *extramsg);
static lua_Number (*luaL_checknumber) (lua_State *L, int numArg);
static lua_Number (*luaL_optnumber) (lua_State *L, int nArg, lua_Number def);
static lua_Integer (*luaL_checkinteger) (lua_State *L, int numArg);
static lua_Unsigned (*luaL_checkunsigned) (lua_State *L, int numArg);
static void (*luaL_checkstack) (lua_State *L, int sz, const char *msg);
static void (*luaL_checktype) (lua_State *L, int narg, int t);
static void (*luaL_checkany) (lua_State *L, int narg);
static int (*luaL_newmetatable) (lua_State *L, const char *tname);
static void (*luaL_setmetatable) (lua_State *L, const char *tname);
static void *(*luaL_testudata) (lua_State *L, int ud, const char *tname);
static void *(*luaL_checkudata) (lua_State *L, int ud, const char *tname);
static void (*luaL_where) (lua_State *L, int lvl);
static int (*luaL_error) (lua_State *L, const char *fmt, ...);
static int (*luaL_fileresult) (lua_State *L, int stat, const char *fname);
static int (*luaL_execresult) (lua_State *L, int stat);
static int (*luaL_ref) (lua_State *L, int t);
static void (*luaL_unref) (lua_State *L, int t, int ref);
static int (*luaL_loadstring) (lua_State *L, const char *s);
static lua_State *(*luaL_newstate) (void);
static int (*luaL_len) (lua_State *L, int idx);
static void (*luaL_setfuncs) (lua_State *L, const luaL_Reg *l, int nup);
static int (*luaL_getsubtable) (lua_State *L, int idx, const char *fname);
static void (*luaL_buffinit) (lua_State *L, luaL_Buffer *B);
static char *(*luaL_prepbuffsize) (luaL_Buffer *B, size_t sz);
static void (*luaL_addlstring) (luaL_Buffer *B, const char *s, size_t l);
static void (*luaL_addstring) (luaL_Buffer *B, const char *s);
static void (*luaL_addvalue) (luaL_Buffer *B);
static void (*luaL_pushresult) (luaL_Buffer *B);
static void (*luaL_pushresultsize) (luaL_Buffer *B, size_t sz);
static char *(*luaL_buffinitsize) (lua_State *L, luaL_Buffer *B, size_t sz);
static lua_State *(*lua_newstate) (lua_Alloc f, void *ud);
static void (*lua_close) (lua_State *L);
static lua_State *(*lua_newthread) (lua_State *L);
static lua_CFunction (*lua_atpanic) (lua_State *L, lua_CFunction panicf);
static const lua_Number *(*lua_version) (lua_State *L);
static int (*lua_absindex) (lua_State *L, int idx);
static int (*lua_gettop) (lua_State *L);
static void (*lua_settop) (lua_State *L, int idx);
static void (*lua_pushvalue) (lua_State *L, int idx);
static void (*lua_remove) (lua_State *L, int idx);
static void (*lua_insert) (lua_State *L, int idx);
static void (*lua_replace) (lua_State *L, int idx);
static void (*lua_copy) (lua_State *L, int fromidx, int toidx);
static int (*lua_checkstack) (lua_State *L, int sz);
static void (*lua_xmove) (lua_State *from, lua_State *to, int n);
static int (*lua_isnumber) (lua_State *L, int idx);
static int (*lua_isstring) (lua_State *L, int idx);
static int (*lua_iscfunction) (lua_State *L, int idx);
static int (*lua_isuserdata) (lua_State *L, int idx);
static int (*lua_type) (lua_State *L, int idx);
static const char *(*lua_typename) (lua_State *L, int tp);
static lua_Number (*lua_tonumberx) (lua_State *L, int idx, int *isnum);
static lua_Integer (*lua_tointegerx) (lua_State *L, int idx, int *isnum);
static lua_Unsigned (*lua_tounsignedx) (lua_State *L, int idx, int *isnum);
static int (*lua_toboolean) (lua_State *L, int idx);
static const char *(*lua_tolstring) (lua_State *L, int idx, size_t *len);
static size_t (*lua_rawlen) (lua_State *L, int idx);
static lua_CFunction (*lua_tocfunction) (lua_State *L, int idx);
static void *(*lua_touserdata) (lua_State *L, int idx);
static lua_State *(*lua_tothread) (lua_State *L, int idx);
static const void *(*lua_topointer) (lua_State *L, int idx);
static void (*lua_arith) (lua_State *L, int op);
static int (*lua_rawequal) (lua_State *L, int idx1, int idx2);
static int (*lua_compare) (lua_State *L, int idx1, int idx2, int op);
static void (*lua_pushnil) (lua_State *L);
static void (*lua_pushnumber) (lua_State *L, lua_Number n);
static void (*lua_pushinteger) (lua_State *L, lua_Integer n);
static void (*lua_pushunsigned) (lua_State *L, lua_Unsigned n);
static const char *(*lua_pushlstring) (lua_State *L, const char *s, size_t l);
static const char *(*lua_pushstring) (lua_State *L, const char *s);
static const char *(*lua_pushfstring) (lua_State *L, const char *fmt, ...);
static void (*lua_pushcclosure) (lua_State *L, lua_CFunction fn, int n);
static void (*lua_pushboolean) (lua_State *L, int b);
static void (*lua_pushlightuserdata) (lua_State *L, void *p);
static int (*lua_pushthread) (lua_State *L);
static void (*lua_getglobal) (lua_State *L, const char *var);
static void (*lua_gettable) (lua_State *L, int idx);
static void (*lua_getfield) (lua_State *L, int idx, const char *k);
static void (*lua_rawget) (lua_State *L, int idx);
static void (*lua_rawgeti) (lua_State *L, int idx, int n);
static void (*lua_rawgetp) (lua_State *L, int idx, const void *p);
static void (*lua_createtable) (lua_State *L, int narr, int nrec);
static void *(*lua_newuserdata) (lua_State *L, size_t sz);
static int (*lua_getmetatable) (lua_State *L, int objindex);
static void (*lua_getuservalue) (lua_State *L, int idx);
static void (*lua_setglobal) (lua_State *L, const char *var);
static void (*lua_settable) (lua_State *L, int idx);
static void (*lua_setfield) (lua_State *L, int idx, const char *k);
static void (*lua_rawset) (lua_State *L, int idx);
static void (*lua_rawseti) (lua_State *L, int idx, int n);
static void (*lua_rawsetp) (lua_State *L, int idx, const void *p);
static int (*lua_setmetatable) (lua_State *L, int objindex);
static void (*lua_setuservalue) (lua_State *L, int idx);
static int (*lua_getctx) (lua_State *L, int *ctx);
static int (*lua_dump) (lua_State *L, lua_Writer writer, void *data);
static int (*lua_resume) (lua_State *L, lua_State *from, int narg);
static int (*lua_status) (lua_State *L);
static int (*lua_gc) (lua_State *L, int what, int data);
static int (*lua_error) (lua_State *L);
static int (*lua_next) (lua_State *L, int idx);
static void (*lua_concat) (lua_State *L, int n);
static void (*lua_len) (lua_State *L, int idx);
static lua_Alloc (*lua_getallocf) (lua_State *L, void **ud);
static void (*lua_setallocf) (lua_State *L, lua_Alloc f, void *ud);
static int (*lua_getstack) (lua_State *L, int level, lua_Debug *ar);
static int (*lua_getinfo) (lua_State *L, const char *what, lua_Debug *ar);
static const char *(*lua_getlocal) (lua_State *L, const lua_Debug *ar, int n);
static const char *(*lua_setlocal) (lua_State *L, const lua_Debug *ar, int n);
static const char *(*lua_getupvalue) (lua_State *L, int funcindex, int n);
static const char *(*lua_setupvalue) (lua_State *L, int funcindex, int n);
static void *(*lua_upvalueid) (lua_State *L, int fidx, int n);
static int (*lua_sethook) (lua_State *L, lua_Hook func, int mask, int count);
static lua_Hook (*lua_gethook) (lua_State *L);
static int (*lua_gethookmask) (lua_State *L);
static int (*lua_gethookcount) (lua_State *L);
static void (*luaL_openlibs) (lua_State *L);
#define luaL_checkversion(L) luaL_checkversion_(L, LUA_VERSION_NUM)
#define luaL_loadfile(L,f) luaL_loadfilex(L,f,NULL)
#define luaL_newlib(L,l) (luaL_newlibtable(L,l), luaL_setfuncs(L,l,0))
#define luaL_checkstring(L,n) (luaL_checklstring(L, (n), NULL))
#define luaL_optstring(L,n,d) (luaL_optlstring(L, (n), (d), NULL))
#define luaL_checkint(L,n) ((int)luaL_checkinteger(L, (n)))
#define luaL_optint(L,n,d) ((int)luaL_optinteger(L, (n), (d)))
#define luaL_checklong(L,n) ((long)luaL_checkinteger(L, (n)))
#define luaL_optlong(L,n,d) ((long)luaL_optinteger(L, (n), (d)))
#define luaL_typename(L,i) lua_typename(L, lua_type(L,(i)))
#define luaL_getmetatable(L,n) (lua_getfield(L, LUA_REGISTRYINDEX, (n)))
#define luaL_opt(L,f,n,d) (lua_isnoneornil(L,(n)) ? (d) : f(L,(n)))
#define luaL_loadbuffer(L,s,sz,n) luaL_loadbufferx(L,s,sz,n,NULL)
#define luaL_addsize(B,s) ((B)->n += (s))
#define luaL_prepbuffer(B) luaL_prepbuffsize(B, LUAL_BUFFERSIZE)
#define luaL_register(L,n,l) (luaL_openlib(L,(n),(l),0))
#define lua_h
#define lua_upvalueindex(i) (LUA_REGISTRYINDEX - (i))
#define lua_call(L,n,r) lua_callk(L, (n), (r), 0, NULL)
#define lua_pcall(L,n,r,f) lua_pcallk(L, (n), (r), (f), 0, NULL)
#define lua_yield(L,n) lua_yieldk(L, (n), 0, NULL)
#define lua_tonumber(L,i) lua_tonumberx(L,i,NULL)
#define lua_tointeger(L,i) lua_tointegerx(L,i,NULL)
#define lua_tounsigned(L,i) lua_tounsignedx(L,i,NULL)
#define lua_pop(L,n) lua_settop(L, -(n)-1)
#define lua_newtable(L) lua_createtable(L, 0, 0)
#define lua_register(L,n,f) (lua_pushcfunction(L, (f)), lua_setglobal(L, (n)))
#define lua_pushcfunction(L,f) lua_pushcclosure(L, (f), 0)
#define lua_isfunction(L,n) (lua_type(L, (n)) == LUA_TFUNCTION)
#define lua_istable(L,n) (lua_type(L, (n)) == LUA_TTABLE)
#define lua_islightuserdata(L,n) (lua_type(L, (n)) == LUA_TLIGHTUSERDATA)
#define lua_isnil(L,n) (lua_type(L, (n)) == LUA_TNIL)
#define lua_isboolean(L,n) (lua_type(L, (n)) == LUA_TBOOLEAN)
#define lua_isthread(L,n) (lua_type(L, (n)) == LUA_TTHREAD)
#define lua_isnone(L,n) (lua_type(L, (n)) == LUA_TNONE)
#define lua_isnoneornil(L, n) (lua_type(L, (n)) <= 0)
#define lua_tostring(L,i) lua_tolstring(L, (i), NULL)
#define lua_strlen(L,i) lua_rawlen(L, (i))
#define lua_objlen(L,i) lua_rawlen(L, (i))
#define lua_equal(L,idx1,idx2) lua_compare(L,(idx1),(idx2),LUA_OPEQ)
#define lua_lessthan(L,idx1,idx2) lua_compare(L,(idx1),(idx2),LUA_OPLT)
#define lua_number2str(s,n) sprintf((s), LUA_NUMBER_FMT, (n))
#define lua_str2number(s,p) strtod((s), (p))
#define lua_strx2number(s,p) strtod((s), (p))
#define lua_pushliteral(L, s) lua_pushlstring(L, s, (sizeof(s)/sizeof(char))-1)
#define lua_pushglobaltable(L) lua_rawgeti(L, LUA_REGISTRYINDEX, LUA_RIDX_GLOBALS)
#define IMPORT_SYMBOL(name, ret, ...) name = (ret (*)(__VA_ARGS__))symbol(#name)
static void lite_xl_plugin_init(void* XL) {
void* (*symbol)(const char*) = (void* (*)(const char*))XL;
IMPORT_SYMBOL(luaL_checkversion_, void ,lua_State *L, lua_Number ver);
IMPORT_SYMBOL(luaL_getmetafield, int ,lua_State *L, int obj, const char *e);
IMPORT_SYMBOL(luaL_callmeta, int ,lua_State *L, int obj, const char *e);
IMPORT_SYMBOL(luaL_tolstring, const char *,lua_State *L, int idx, size_t *len);
IMPORT_SYMBOL(luaL_argerror, int ,lua_State *L, int numarg, const char *extramsg);
IMPORT_SYMBOL(luaL_checknumber, lua_Number ,lua_State *L, int numArg);
IMPORT_SYMBOL(luaL_optnumber, lua_Number ,lua_State *L, int nArg, lua_Number def);
IMPORT_SYMBOL(luaL_checkinteger, lua_Integer ,lua_State *L, int numArg);
IMPORT_SYMBOL(luaL_checkunsigned, lua_Unsigned ,lua_State *L, int numArg);
IMPORT_SYMBOL(luaL_checkstack, void ,lua_State *L, int sz, const char *msg);
IMPORT_SYMBOL(luaL_checktype, void ,lua_State *L, int narg, int t);
IMPORT_SYMBOL(luaL_checkany, void ,lua_State *L, int narg);
IMPORT_SYMBOL(luaL_newmetatable, int ,lua_State *L, const char *tname);
IMPORT_SYMBOL(luaL_setmetatable, void ,lua_State *L, const char *tname);
IMPORT_SYMBOL(luaL_testudata, void *,lua_State *L, int ud, const char *tname);
IMPORT_SYMBOL(luaL_checkudata, void *,lua_State *L, int ud, const char *tname);
IMPORT_SYMBOL(luaL_where, void ,lua_State *L, int lvl);
IMPORT_SYMBOL(luaL_error, int ,lua_State *L, const char *fmt, ...);
IMPORT_SYMBOL(luaL_fileresult, int ,lua_State *L, int stat, const char *fname);
IMPORT_SYMBOL(luaL_execresult, int ,lua_State *L, int stat);
IMPORT_SYMBOL(luaL_ref, int ,lua_State *L, int t);
IMPORT_SYMBOL(luaL_unref, void ,lua_State *L, int t, int ref);
IMPORT_SYMBOL(luaL_loadstring, int ,lua_State *L, const char *s);
IMPORT_SYMBOL(luaL_newstate, lua_State *,void);
IMPORT_SYMBOL(luaL_len, int ,lua_State *L, int idx);
IMPORT_SYMBOL(luaL_setfuncs, void ,lua_State *L, const luaL_Reg *l, int nup);
IMPORT_SYMBOL(luaL_getsubtable, int ,lua_State *L, int idx, const char *fname);
IMPORT_SYMBOL(luaL_buffinit, void ,lua_State *L, luaL_Buffer *B);
IMPORT_SYMBOL(luaL_prepbuffsize, char *,luaL_Buffer *B, size_t sz);
IMPORT_SYMBOL(luaL_addlstring, void ,luaL_Buffer *B, const char *s, size_t l);
IMPORT_SYMBOL(luaL_addstring, void ,luaL_Buffer *B, const char *s);
IMPORT_SYMBOL(luaL_addvalue, void ,luaL_Buffer *B);
IMPORT_SYMBOL(luaL_pushresult, void ,luaL_Buffer *B);
IMPORT_SYMBOL(luaL_pushresultsize, void ,luaL_Buffer *B, size_t sz);
IMPORT_SYMBOL(luaL_buffinitsize, char *,lua_State *L, luaL_Buffer *B, size_t sz);
IMPORT_SYMBOL(lua_newstate, lua_State *,lua_Alloc f, void *ud);
IMPORT_SYMBOL(lua_close, void ,lua_State *L);
IMPORT_SYMBOL(lua_newthread, lua_State *,lua_State *L);
IMPORT_SYMBOL(lua_atpanic, lua_CFunction ,lua_State *L, lua_CFunction panicf);
IMPORT_SYMBOL(lua_version, const lua_Number *,lua_State *L);
IMPORT_SYMBOL(lua_absindex, int ,lua_State *L, int idx);
IMPORT_SYMBOL(lua_gettop, int ,lua_State *L);
IMPORT_SYMBOL(lua_settop, void ,lua_State *L, int idx);
IMPORT_SYMBOL(lua_pushvalue, void ,lua_State *L, int idx);
IMPORT_SYMBOL(lua_remove, void ,lua_State *L, int idx);
IMPORT_SYMBOL(lua_insert, void ,lua_State *L, int idx);
IMPORT_SYMBOL(lua_replace, void ,lua_State *L, int idx);
IMPORT_SYMBOL(lua_copy, void ,lua_State *L, int fromidx, int toidx);
IMPORT_SYMBOL(lua_checkstack, int ,lua_State *L, int sz);
IMPORT_SYMBOL(lua_xmove, void ,lua_State *from, lua_State *to, int n);
IMPORT_SYMBOL(lua_isnumber, int ,lua_State *L, int idx);
IMPORT_SYMBOL(lua_isstring, int ,lua_State *L, int idx);
IMPORT_SYMBOL(lua_iscfunction, int ,lua_State *L, int idx);
IMPORT_SYMBOL(lua_isuserdata, int ,lua_State *L, int idx);
IMPORT_SYMBOL(lua_type, int ,lua_State *L, int idx);
IMPORT_SYMBOL(lua_typename, const char *,lua_State *L, int tp);
IMPORT_SYMBOL(lua_tonumberx, lua_Number ,lua_State *L, int idx, int *isnum);
IMPORT_SYMBOL(lua_tointegerx, lua_Integer ,lua_State *L, int idx, int *isnum);
IMPORT_SYMBOL(lua_tounsignedx, lua_Unsigned ,lua_State *L, int idx, int *isnum);
IMPORT_SYMBOL(lua_toboolean, int ,lua_State *L, int idx);
IMPORT_SYMBOL(lua_tolstring, const char *,lua_State *L, int idx, size_t *len);
IMPORT_SYMBOL(lua_rawlen, size_t ,lua_State *L, int idx);
IMPORT_SYMBOL(lua_tocfunction, lua_CFunction ,lua_State *L, int idx);
IMPORT_SYMBOL(lua_touserdata, void *,lua_State *L, int idx);
IMPORT_SYMBOL(lua_tothread, lua_State *,lua_State *L, int idx);
IMPORT_SYMBOL(lua_topointer, const void *,lua_State *L, int idx);
IMPORT_SYMBOL(lua_arith, void ,lua_State *L, int op);
IMPORT_SYMBOL(lua_rawequal, int ,lua_State *L, int idx1, int idx2);
IMPORT_SYMBOL(lua_compare, int ,lua_State *L, int idx1, int idx2, int op);
IMPORT_SYMBOL(lua_pushnil, void ,lua_State *L);
IMPORT_SYMBOL(lua_pushnumber, void ,lua_State *L, lua_Number n);
IMPORT_SYMBOL(lua_pushinteger, void ,lua_State *L, lua_Integer n);
IMPORT_SYMBOL(lua_pushunsigned, void ,lua_State *L, lua_Unsigned n);
IMPORT_SYMBOL(lua_pushlstring, const char *,lua_State *L, const char *s, size_t l);
IMPORT_SYMBOL(lua_pushstring, const char *,lua_State *L, const char *s);
IMPORT_SYMBOL(lua_pushfstring, const char *,lua_State *L, const char *fmt, ...);
IMPORT_SYMBOL(lua_pushcclosure, void ,lua_State *L, lua_CFunction fn, int n);
IMPORT_SYMBOL(lua_pushboolean, void ,lua_State *L, int b);
IMPORT_SYMBOL(lua_pushlightuserdata, void ,lua_State *L, void *p);
IMPORT_SYMBOL(lua_pushthread, int ,lua_State *L);
IMPORT_SYMBOL(lua_getglobal, void ,lua_State *L, const char *var);
IMPORT_SYMBOL(lua_gettable, void ,lua_State *L, int idx);
IMPORT_SYMBOL(lua_getfield, void ,lua_State *L, int idx, const char *k);
IMPORT_SYMBOL(lua_rawget, void ,lua_State *L, int idx);
IMPORT_SYMBOL(lua_rawgeti, void ,lua_State *L, int idx, int n);
IMPORT_SYMBOL(lua_rawgetp, void ,lua_State *L, int idx, const void *p);
IMPORT_SYMBOL(lua_createtable, void ,lua_State *L, int narr, int nrec);
IMPORT_SYMBOL(lua_newuserdata, void *,lua_State *L, size_t sz);
IMPORT_SYMBOL(lua_getmetatable, int ,lua_State *L, int objindex);
IMPORT_SYMBOL(lua_getuservalue, void ,lua_State *L, int idx);
IMPORT_SYMBOL(lua_setglobal, void ,lua_State *L, const char *var);
IMPORT_SYMBOL(lua_settable, void ,lua_State *L, int idx);
IMPORT_SYMBOL(lua_setfield, void ,lua_State *L, int idx, const char *k);
IMPORT_SYMBOL(lua_rawset, void ,lua_State *L, int idx);
IMPORT_SYMBOL(lua_rawseti, void ,lua_State *L, int idx, int n);
IMPORT_SYMBOL(lua_rawsetp, void ,lua_State *L, int idx, const void *p);
IMPORT_SYMBOL(lua_setmetatable, int ,lua_State *L, int objindex);
IMPORT_SYMBOL(lua_setuservalue, void ,lua_State *L, int idx);
IMPORT_SYMBOL(lua_getctx, int ,lua_State *L, int *ctx);
IMPORT_SYMBOL(lua_dump, int ,lua_State *L, lua_Writer writer, void *data);
IMPORT_SYMBOL(lua_resume, int ,lua_State *L, lua_State *from, int narg);
IMPORT_SYMBOL(lua_status, int ,lua_State *L);
IMPORT_SYMBOL(lua_gc, int ,lua_State *L, int what, int data);
IMPORT_SYMBOL(lua_error, int ,lua_State *L);
IMPORT_SYMBOL(lua_next, int ,lua_State *L, int idx);
IMPORT_SYMBOL(lua_concat, void ,lua_State *L, int n);
IMPORT_SYMBOL(lua_len, void ,lua_State *L, int idx);
IMPORT_SYMBOL(lua_getallocf, lua_Alloc ,lua_State *L, void **ud);
IMPORT_SYMBOL(lua_setallocf, void ,lua_State *L, lua_Alloc f, void *ud);
IMPORT_SYMBOL(lua_getstack, int ,lua_State *L, int level, lua_Debug *ar);
IMPORT_SYMBOL(lua_getinfo, int ,lua_State *L, const char *what, lua_Debug *ar);
IMPORT_SYMBOL(lua_getlocal, const char *,lua_State *L, const lua_Debug *ar, int n);
IMPORT_SYMBOL(lua_setlocal, const char *,lua_State *L, const lua_Debug *ar, int n);
IMPORT_SYMBOL(lua_getupvalue, const char *,lua_State *L, int funcindex, int n);
IMPORT_SYMBOL(lua_setupvalue, const char *,lua_State *L, int funcindex, int n);
IMPORT_SYMBOL(lua_upvalueid, void *,lua_State *L, int fidx, int n);
IMPORT_SYMBOL(lua_sethook, int ,lua_State *L, lua_Hook func, int mask, int count);
IMPORT_SYMBOL(lua_gethook, lua_Hook ,lua_State *L);
IMPORT_SYMBOL(lua_gethookmask, int ,lua_State *L);
IMPORT_SYMBOL(lua_gethookcount, int ,lua_State *L);
IMPORT_SYMBOL(luaL_openlibs, void ,lua_State *L);
}
#endif

View File

@ -2,25 +2,30 @@
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>lite-xl</string> <string>lite-xl</string>
<key>CFBundleGetInfoString</key> <key>CFBundleGetInfoString</key>
<string>lite-xl</string> <string>lite-xl</string>
<key>CFBundleIconFile</key> <key>CFBundleIconFile</key>
<string>icon</string> <string>icon.icns</string>
<key>CFBundleName</key> <key>CFBundleName</key>
<string>lite-xl</string> <string>Lite XL</string>
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>NSHighResolutionCapable</key> <key>NSHighResolutionCapable</key>
<true/> <true/>
<key>MinimumOSVersion</key><string>10.13</string> <key>LSMinimumSystemVersion</key>
<key>NSDocumentsFolderUsageDescription</key><string>To access, edit and index your projects.</string> <string>10.11</string>
<key>NSDesktopFolderUsageDescription</key><string>To access, edit and index your projects.</string> <key>NSDocumentsFolderUsageDescription</key>
<key>NSDownloadsFolderUsageDescription</key><string>To access, edit and index your projects.</string> <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> <key>CFBundleShortVersionString</key>
<string>1.16.10</string> <string>@PROJECT_VERSION@</string>
<key>NSHumanReadableCopyright</key> <key>NSHumanReadableCopyright</key>
<string>© 2019-2021 Francesco Abbate</string> <string>© 2019-2021 Francesco Abbate</string>
</dict> </dict>
</plist> </plist>

BIN
resources/macos/appdmg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

29
scripts/README.md Normal file
View File

@ -0,0 +1,29 @@
# 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.
- **keymap-generator**: Generates a JSON file containing the keymap
[1]: https://github.com/LinusU/node-appdmg
[2]: https://docs.appimage.org/
[3]: https://jrsoftware.org/isinfo.php

30
scripts/appdmg.sh Normal file
View File

@ -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"

162
scripts/appimage.sh Normal file
View File

@ -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

117
scripts/build.sh Normal file
View File

@ -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 "$@"

25
scripts/common.sh Normal file
View File

@ -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

View File

@ -4,12 +4,12 @@
#define MyAppURL "https://lite-xl.github.io" #define MyAppURL "https://lite-xl.github.io"
#define MyAppExeName "lite-xl.exe" #define MyAppExeName "lite-xl.exe"
#define BuildDir "@PROJECT_BUILD_DIR@" #define BuildDir "@PROJECT_BUILD_DIR@"
#define SourceDir "." #define SourceDir "@PROJECT_SOURCE_DIR@"
; Use /dArch option to create a setup for a different architecture, e.g.: ; Use /dArch option to create a setup for a different architecture, e.g.:
; iscc /dArch=x86 innosetup.iss ; iscc /dArch=x86 innosetup.iss
#ifndef Arch #ifndef Arch
#define Arch "x64" #define Arch "x64"
#endif #endif
[Setup] [Setup]
@ -27,14 +27,17 @@ AppSupportURL={#MyAppURL}
AppUpdatesURL={#MyAppURL} AppUpdatesURL={#MyAppURL}
#if Arch=="x64" #if Arch=="x64"
ArchitecturesAllowed=x64 ArchitecturesAllowed=x64
ArchitecturesInstallIn64BitMode={#Arch} ArchitecturesInstallIn64BitMode=x64
#define ArchInternal "x86_64"
#else
#define ArchInternal "i686"
#endif #endif
AllowNoIcons=yes AllowNoIcons=yes
Compression=lzma Compression=lzma
SolidCompression=yes SolidCompression=yes
DefaultDirName={autopf}\{#MyAppName} DefaultDirName={autopf}/{#MyAppName}
DefaultGroupName={#MyAppPublisher} DefaultGroupName={#MyAppPublisher}
UninstallFilesDir={app} UninstallFilesDir={app}
@ -48,14 +51,14 @@ PrivilegesRequiredOverridesAllowed=dialog
UsedUserAreasWarning=no UsedUserAreasWarning=no
OutputDir=. OutputDir=.
OutputBaseFilename=LiteXL-{#MyAppVersion}-{#Arch}-setup OutputBaseFilename=LiteXL-{#MyAppVersion}-{#ArchInternal}-setup
;DisableDirPage=yes ;DisableDirPage=yes
;DisableProgramGroupPage=yes ;DisableProgramGroupPage=yes
LicenseFile={#SourceDir}\LICENSE LicenseFile={#SourceDir}/LICENSE
SetupIconFile={#SourceDir}\icon.ico SetupIconFile={#SourceDir}/resources/icons/icon.ico
WizardImageFile="wizard-modern-image.bmp" WizardImageFile="{#SourceDir}/scripts/innosetup/wizard-modern-image.bmp"
WizardSmallImageFile="litexl-55px.bmp" WizardSmallImageFile="{#SourceDir}/scripts/innosetup/litexl-55px.bmp"
[Languages] [Languages]
Name: "english"; MessagesFile: "compiler:Default.isl" Name: "english"; MessagesFile: "compiler:Default.isl"
@ -66,18 +69,20 @@ Name: "quicklaunchicon"; Description: "{cm:CreateQuickLaunchIcon}"; GroupDescrip
Name: "portablemode"; Description: "Portable Mode"; Flags: unchecked Name: "portablemode"; Description: "Portable Mode"; Flags: unchecked
[Files] [Files]
Source: "{#BuildDir}\lite-xl.exe"; DestDir: "{app}"; Flags: ignoreversion Source: "{#BuildDir}/src/lite-xl.exe"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#BuildDir}\data\*"; DestDir: "{app}\data"; Flags: ignoreversion recursesubdirs 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 ; NOTE: Don't use "Flags: ignoreversion" on any shared system files
[Icons] [Icons]
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Check: not IsTaskSelected('portablemode')
Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}"; Check: not IsTaskSelected('portablemode')
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
Name: "{userappdata}\Microsoft\Internet Explorer\Quick Launch\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: quicklaunchicon; Check: not IsTaskSelected('portablemode') 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] [Run]
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent Filename: "{app}/{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
[Setup] [Setup]
Uninstallable=not IsTaskSelected('portablemode') Uninstallable=not WizardIsTaskSelected('portablemode')

View File

@ -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 "$@"

View File

@ -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 "$@"

View File

@ -0,0 +1,714 @@
-- Module options:
local always_try_using_lpeg = true
local register_global_module_table = false
local global_module_name = 'json'
--[==[
David Kolf's JSON module for Lua 5.1/5.2
Version 2.5
For the documentation see the corresponding readme.txt or visit
<http://dkolf.de/src/dkjson-lua.fsl/>.
You can contact the author by sending an e-mail to 'david' at the
domain 'dkolf.de'.
Copyright (C) 2010-2013 David Heiko Kolf
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
--]==]
-- global dependencies:
local pairs, type, tostring, tonumber, getmetatable, setmetatable, rawset =
pairs, type, tostring, tonumber, getmetatable, setmetatable, rawset
local error, require, pcall, select = error, require, pcall, select
local floor, huge = math.floor, math.huge
local strrep, gsub, strsub, strbyte, strchar, strfind, strlen, strformat =
string.rep, string.gsub, string.sub, string.byte, string.char,
string.find, string.len, string.format
local strmatch = string.match
local concat = table.concat
local json = { version = "dkjson 2.5" }
if register_global_module_table then
_G[global_module_name] = json
end
local _ENV = nil -- blocking globals in Lua 5.2
pcall (function()
-- Enable access to blocked metatables.
-- Don't worry, this module doesn't change anything in them.
local debmeta = require "debug".getmetatable
if debmeta then getmetatable = debmeta end
end)
json.null = setmetatable ({}, {
__tojson = function () return "null" end
})
local function isarray (tbl)
local max, n, arraylen = 0, 0, 0
for k,v in pairs (tbl) do
if k == 'n' and type(v) == 'number' then
arraylen = v
if v > max then
max = v
end
else
if type(k) ~= 'number' or k < 1 or floor(k) ~= k then
return false
end
if k > max then
max = k
end
n = n + 1
end
end
if max > 10 and max > arraylen and max > n * 2 then
return false -- don't create an array with too many holes
end
return true, max
end
local escapecodes = {
["\""] = "\\\"", ["\\"] = "\\\\", ["\b"] = "\\b", ["\f"] = "\\f",
["\n"] = "\\n", ["\r"] = "\\r", ["\t"] = "\\t"
}
local function escapeutf8 (uchar)
local value = escapecodes[uchar]
if value then
return value
end
local a, b, c, d = strbyte (uchar, 1, 4)
a, b, c, d = a or 0, b or 0, c or 0, d or 0
if a <= 0x7f then
value = a
elseif 0xc0 <= a and a <= 0xdf and b >= 0x80 then
value = (a - 0xc0) * 0x40 + b - 0x80
elseif 0xe0 <= a and a <= 0xef and b >= 0x80 and c >= 0x80 then
value = ((a - 0xe0) * 0x40 + b - 0x80) * 0x40 + c - 0x80
elseif 0xf0 <= a and a <= 0xf7 and b >= 0x80 and c >= 0x80 and d >= 0x80 then
value = (((a - 0xf0) * 0x40 + b - 0x80) * 0x40 + c - 0x80) * 0x40 + d - 0x80
else
return ""
end
if value <= 0xffff then
return strformat ("\\u%.4x", value)
elseif value <= 0x10ffff then
-- encode as UTF-16 surrogate pair
value = value - 0x10000
local highsur, lowsur = 0xD800 + floor (value/0x400), 0xDC00 + (value % 0x400)
return strformat ("\\u%.4x\\u%.4x", highsur, lowsur)
else
return ""
end
end
local function fsub (str, pattern, repl)
-- gsub always builds a new string in a buffer, even when no match
-- exists. First using find should be more efficient when most strings
-- don't contain the pattern.
if strfind (str, pattern) then
return gsub (str, pattern, repl)
else
return str
end
end
local function quotestring (value)
-- based on the regexp "escapable" in https://github.com/douglascrockford/JSON-js
value = fsub (value, "[%z\1-\31\"\\\127]", escapeutf8)
if strfind (value, "[\194\216\220\225\226\239]") then
value = fsub (value, "\194[\128-\159\173]", escapeutf8)
value = fsub (value, "\216[\128-\132]", escapeutf8)
value = fsub (value, "\220\143", escapeutf8)
value = fsub (value, "\225\158[\180\181]", escapeutf8)
value = fsub (value, "\226\128[\140-\143\168-\175]", escapeutf8)
value = fsub (value, "\226\129[\160-\175]", escapeutf8)
value = fsub (value, "\239\187\191", escapeutf8)
value = fsub (value, "\239\191[\176-\191]", escapeutf8)
end
return "\"" .. value .. "\""
end
json.quotestring = quotestring
local function replace(str, o, n)
local i, j = strfind (str, o, 1, true)
if i then
return strsub(str, 1, i-1) .. n .. strsub(str, j+1, -1)
else
return str
end
end
-- locale independent num2str and str2num functions
local decpoint, numfilter
local function updatedecpoint ()
decpoint = strmatch(tostring(0.5), "([^05+])")
-- build a filter that can be used to remove group separators
numfilter = "[^0-9%-%+eE" .. gsub(decpoint, "[%^%$%(%)%%%.%[%]%*%+%-%?]", "%%%0") .. "]+"
end
updatedecpoint()
local function num2str (num)
return replace(fsub(tostring(num), numfilter, ""), decpoint, ".")
end
local function str2num (str)
local num = tonumber(replace(str, ".", decpoint))
if not num then
updatedecpoint()
num = tonumber(replace(str, ".", decpoint))
end
return num
end
local function addnewline2 (level, buffer, buflen)
buffer[buflen+1] = "\n"
buffer[buflen+2] = strrep (" ", level)
buflen = buflen + 2
return buflen
end
function json.addnewline (state)
if state.indent then
state.bufferlen = addnewline2 (state.level or 0,
state.buffer, state.bufferlen or #(state.buffer))
end
end
local encode2 -- forward declaration
local function addpair (key, value, prev, indent, level, buffer, buflen, tables, globalorder, state)
local kt = type (key)
if kt ~= 'string' and kt ~= 'number' then
return nil, "type '" .. kt .. "' is not supported as a key by JSON."
end
if prev then
buflen = buflen + 1
buffer[buflen] = ","
end
if indent then
buflen = addnewline2 (level, buffer, buflen)
end
buffer[buflen+1] = quotestring (key)
buffer[buflen+2] = ":"
return encode2 (value, indent, level, buffer, buflen + 2, tables, globalorder, state)
end
local function appendcustom(res, buffer, state)
local buflen = state.bufferlen
if type (res) == 'string' then
buflen = buflen + 1
buffer[buflen] = res
end
return buflen
end
local function exception(reason, value, state, buffer, buflen, defaultmessage)
defaultmessage = defaultmessage or reason
local handler = state.exception
if not handler then
return nil, defaultmessage
else
state.bufferlen = buflen
local ret, msg = handler (reason, value, state, defaultmessage)
if not ret then return nil, msg or defaultmessage end
return appendcustom(ret, buffer, state)
end
end
function json.encodeexception(reason, value, state, defaultmessage)
return quotestring("<" .. defaultmessage .. ">")
end
encode2 = function (value, indent, level, buffer, buflen, tables, globalorder, state)
local valtype = type (value)
local valmeta = getmetatable (value)
valmeta = type (valmeta) == 'table' and valmeta -- only tables
local valtojson = valmeta and valmeta.__tojson
if valtojson then
if tables[value] then
return exception('reference cycle', value, state, buffer, buflen)
end
tables[value] = true
state.bufferlen = buflen
local ret, msg = valtojson (value, state)
if not ret then return exception('custom encoder failed', value, state, buffer, buflen, msg) end
tables[value] = nil
buflen = appendcustom(ret, buffer, state)
elseif value == nil then
buflen = buflen + 1
buffer[buflen] = "null"
elseif valtype == 'number' then
local s
if value ~= value or value >= huge or -value >= huge then
-- This is the behaviour of the original JSON implementation.
s = "null"
else
s = num2str (value)
end
buflen = buflen + 1
buffer[buflen] = s
elseif valtype == 'boolean' then
buflen = buflen + 1
buffer[buflen] = value and "true" or "false"
elseif valtype == 'string' then
buflen = buflen + 1
buffer[buflen] = quotestring (value)
elseif valtype == 'table' then
if tables[value] then
return exception('reference cycle', value, state, buffer, buflen)
end
tables[value] = true
level = level + 1
local isa, n = isarray (value)
if n == 0 and valmeta and valmeta.__jsontype == 'object' then
isa = false
end
local msg
if isa then -- JSON array
buflen = buflen + 1
buffer[buflen] = "["
for i = 1, n do
buflen, msg = encode2 (value[i], indent, level, buffer, buflen, tables, globalorder, state)
if not buflen then return nil, msg end
if i < n then
buflen = buflen + 1
buffer[buflen] = ","
end
end
buflen = buflen + 1
buffer[buflen] = "]"
else -- JSON object
local prev = false
buflen = buflen + 1
buffer[buflen] = "{"
local order = valmeta and valmeta.__jsonorder or globalorder
if order then
local used = {}
n = #order
for i = 1, n do
local k = order[i]
local v = value[k]
if v then
used[k] = true
buflen, msg = addpair (k, v, prev, indent, level, buffer, buflen, tables, globalorder, state)
prev = true -- add a seperator before the next element
end
end
for k,v in pairs (value) do
if not used[k] then
buflen, msg = addpair (k, v, prev, indent, level, buffer, buflen, tables, globalorder, state)
if not buflen then return nil, msg end
prev = true -- add a seperator before the next element
end
end
else -- unordered
for k,v in pairs (value) do
buflen, msg = addpair (k, v, prev, indent, level, buffer, buflen, tables, globalorder, state)
if not buflen then return nil, msg end
prev = true -- add a seperator before the next element
end
end
if indent then
buflen = addnewline2 (level - 1, buffer, buflen)
end
buflen = buflen + 1
buffer[buflen] = "}"
end
tables[value] = nil
else
return exception ('unsupported type', value, state, buffer, buflen,
"type '" .. valtype .. "' is not supported by JSON.")
end
return buflen
end
function json.encode (value, state)
state = state or {}
local oldbuffer = state.buffer
local buffer = oldbuffer or {}
state.buffer = buffer
updatedecpoint()
local ret, msg = encode2 (value, state.indent, state.level or 0,
buffer, state.bufferlen or 0, state.tables or {}, state.keyorder, state)
if not ret then
error (msg, 2)
elseif oldbuffer == buffer then
state.bufferlen = ret
return true
else
state.bufferlen = nil
state.buffer = nil
return concat (buffer)
end
end
local function loc (str, where)
local line, pos, linepos = 1, 1, 0
while true do
pos = strfind (str, "\n", pos, true)
if pos and pos < where then
line = line + 1
linepos = pos
pos = pos + 1
else
break
end
end
return "line " .. line .. ", column " .. (where - linepos)
end
local function unterminated (str, what, where)
return nil, strlen (str) + 1, "unterminated " .. what .. " at " .. loc (str, where)
end
local function scanwhite (str, pos)
while true do
pos = strfind (str, "%S", pos)
if not pos then return nil end
local sub2 = strsub (str, pos, pos + 1)
if sub2 == "\239\187" and strsub (str, pos + 2, pos + 2) == "\191" then
-- UTF-8 Byte Order Mark
pos = pos + 3
elseif sub2 == "//" then
pos = strfind (str, "[\n\r]", pos + 2)
if not pos then return nil end
elseif sub2 == "/*" then
pos = strfind (str, "*/", pos + 2)
if not pos then return nil end
pos = pos + 2
else
return pos
end
end
end
local escapechars = {
["\""] = "\"", ["\\"] = "\\", ["/"] = "/", ["b"] = "\b", ["f"] = "\f",
["n"] = "\n", ["r"] = "\r", ["t"] = "\t"
}
local function unichar (value)
if value < 0 then
return nil
elseif value <= 0x007f then
return strchar (value)
elseif value <= 0x07ff then
return strchar (0xc0 + floor(value/0x40),
0x80 + (floor(value) % 0x40))
elseif value <= 0xffff then
return strchar (0xe0 + floor(value/0x1000),
0x80 + (floor(value/0x40) % 0x40),
0x80 + (floor(value) % 0x40))
elseif value <= 0x10ffff then
return strchar (0xf0 + floor(value/0x40000),
0x80 + (floor(value/0x1000) % 0x40),
0x80 + (floor(value/0x40) % 0x40),
0x80 + (floor(value) % 0x40))
else
return nil
end
end
local function scanstring (str, pos)
local lastpos = pos + 1
local buffer, n = {}, 0
while true do
local nextpos = strfind (str, "[\"\\]", lastpos)
if not nextpos then
return unterminated (str, "string", pos)
end
if nextpos > lastpos then
n = n + 1
buffer[n] = strsub (str, lastpos, nextpos - 1)
end
if strsub (str, nextpos, nextpos) == "\"" then
lastpos = nextpos + 1
break
else
local escchar = strsub (str, nextpos + 1, nextpos + 1)
local value
if escchar == "u" then
value = tonumber (strsub (str, nextpos + 2, nextpos + 5), 16)
if value then
local value2
if 0xD800 <= value and value <= 0xDBff then
-- we have the high surrogate of UTF-16. Check if there is a
-- low surrogate escaped nearby to combine them.
if strsub (str, nextpos + 6, nextpos + 7) == "\\u" then
value2 = tonumber (strsub (str, nextpos + 8, nextpos + 11), 16)
if value2 and 0xDC00 <= value2 and value2 <= 0xDFFF then
value = (value - 0xD800) * 0x400 + (value2 - 0xDC00) + 0x10000
else
value2 = nil -- in case it was out of range for a low surrogate
end
end
end
value = value and unichar (value)
if value then
if value2 then
lastpos = nextpos + 12
else
lastpos = nextpos + 6
end
end
end
end
if not value then
value = escapechars[escchar] or escchar
lastpos = nextpos + 2
end
n = n + 1
buffer[n] = value
end
end
if n == 1 then
return buffer[1], lastpos
elseif n > 1 then
return concat (buffer), lastpos
else
return "", lastpos
end
end
local scanvalue -- forward declaration
local function scantable (what, closechar, str, startpos, nullval, objectmeta, arraymeta)
local len = strlen (str)
local tbl, n = {}, 0
local pos = startpos + 1
if what == 'object' then
setmetatable (tbl, objectmeta)
else
setmetatable (tbl, arraymeta)
end
while true do
pos = scanwhite (str, pos)
if not pos then return unterminated (str, what, startpos) end
local char = strsub (str, pos, pos)
if char == closechar then
return tbl, pos + 1
end
local val1, err
val1, pos, err = scanvalue (str, pos, nullval, objectmeta, arraymeta)
if err then return nil, pos, err end
pos = scanwhite (str, pos)
if not pos then return unterminated (str, what, startpos) end
char = strsub (str, pos, pos)
if char == ":" then
if val1 == nil then
return nil, pos, "cannot use nil as table index (at " .. loc (str, pos) .. ")"
end
pos = scanwhite (str, pos + 1)
if not pos then return unterminated (str, what, startpos) end
local val2
val2, pos, err = scanvalue (str, pos, nullval, objectmeta, arraymeta)
if err then return nil, pos, err end
tbl[val1] = val2
pos = scanwhite (str, pos)
if not pos then return unterminated (str, what, startpos) end
char = strsub (str, pos, pos)
else
n = n + 1
tbl[n] = val1
end
if char == "," then
pos = pos + 1
end
end
end
scanvalue = function (str, pos, nullval, objectmeta, arraymeta)
pos = pos or 1
pos = scanwhite (str, pos)
if not pos then
return nil, strlen (str) + 1, "no valid JSON value (reached the end)"
end
local char = strsub (str, pos, pos)
if char == "{" then
return scantable ('object', "}", str, pos, nullval, objectmeta, arraymeta)
elseif char == "[" then
return scantable ('array', "]", str, pos, nullval, objectmeta, arraymeta)
elseif char == "\"" then
return scanstring (str, pos)
else
local pstart, pend = strfind (str, "^%-?[%d%.]+[eE]?[%+%-]?%d*", pos)
if pstart then
local number = str2num (strsub (str, pstart, pend))
if number then
return number, pend + 1
end
end
pstart, pend = strfind (str, "^%a%w*", pos)
if pstart then
local name = strsub (str, pstart, pend)
if name == "true" then
return true, pend + 1
elseif name == "false" then
return false, pend + 1
elseif name == "null" then
return nullval, pend + 1
end
end
return nil, pos, "no valid JSON value at " .. loc (str, pos)
end
end
local function optionalmetatables(...)
if select("#", ...) > 0 then
return ...
else
return {__jsontype = 'object'}, {__jsontype = 'array'}
end
end
function json.decode (str, pos, nullval, ...)
local objectmeta, arraymeta = optionalmetatables(...)
return scanvalue (str, pos, nullval, objectmeta, arraymeta)
end
function json.use_lpeg ()
local g = require ("lpeg")
if g.version() == "0.11" then
error "due to a bug in LPeg 0.11, it cannot be used for JSON matching"
end
local pegmatch = g.match
local P, S, R = g.P, g.S, g.R
local function ErrorCall (str, pos, msg, state)
if not state.msg then
state.msg = msg .. " at " .. loc (str, pos)
state.pos = pos
end
return false
end
local function Err (msg)
return g.Cmt (g.Cc (msg) * g.Carg (2), ErrorCall)
end
local SingleLineComment = P"//" * (1 - S"\n\r")^0
local MultiLineComment = P"/*" * (1 - P"*/")^0 * P"*/"
local Space = (S" \n\r\t" + P"\239\187\191" + SingleLineComment + MultiLineComment)^0
local PlainChar = 1 - S"\"\\\n\r"
local EscapeSequence = (P"\\" * g.C (S"\"\\/bfnrt" + Err "unsupported escape sequence")) / escapechars
local HexDigit = R("09", "af", "AF")
local function UTF16Surrogate (match, pos, high, low)
high, low = tonumber (high, 16), tonumber (low, 16)
if 0xD800 <= high and high <= 0xDBff and 0xDC00 <= low and low <= 0xDFFF then
return true, unichar ((high - 0xD800) * 0x400 + (low - 0xDC00) + 0x10000)
else
return false
end
end
local function UTF16BMP (hex)
return unichar (tonumber (hex, 16))
end
local U16Sequence = (P"\\u" * g.C (HexDigit * HexDigit * HexDigit * HexDigit))
local UnicodeEscape = g.Cmt (U16Sequence * U16Sequence, UTF16Surrogate) + U16Sequence/UTF16BMP
local Char = UnicodeEscape + EscapeSequence + PlainChar
local String = P"\"" * g.Cs (Char ^ 0) * (P"\"" + Err "unterminated string")
local Integer = P"-"^(-1) * (P"0" + (R"19" * R"09"^0))
local Fractal = P"." * R"09"^0
local Exponent = (S"eE") * (S"+-")^(-1) * R"09"^1
local Number = (Integer * Fractal^(-1) * Exponent^(-1))/str2num
local Constant = P"true" * g.Cc (true) + P"false" * g.Cc (false) + P"null" * g.Carg (1)
local SimpleValue = Number + String + Constant
local ArrayContent, ObjectContent
-- The functions parsearray and parseobject parse only a single value/pair
-- at a time and store them directly to avoid hitting the LPeg limits.
local function parsearray (str, pos, nullval, state)
local obj, cont
local npos
local t, nt = {}, 0
repeat
obj, cont, npos = pegmatch (ArrayContent, str, pos, nullval, state)
if not npos then break end
pos = npos
nt = nt + 1
t[nt] = obj
until cont == 'last'
return pos, setmetatable (t, state.arraymeta)
end
local function parseobject (str, pos, nullval, state)
local obj, key, cont
local npos
local t = {}
repeat
key, obj, cont, npos = pegmatch (ObjectContent, str, pos, nullval, state)
if not npos then break end
pos = npos
t[key] = obj
until cont == 'last'
return pos, setmetatable (t, state.objectmeta)
end
local Array = P"[" * g.Cmt (g.Carg(1) * g.Carg(2), parsearray) * Space * (P"]" + Err "']' expected")
local Object = P"{" * g.Cmt (g.Carg(1) * g.Carg(2), parseobject) * Space * (P"}" + Err "'}' expected")
local Value = Space * (Array + Object + SimpleValue)
local ExpectedValue = Value + Space * Err "value expected"
ArrayContent = Value * Space * (P"," * g.Cc'cont' + g.Cc'last') * g.Cp()
local Pair = g.Cg (Space * String * Space * (P":" + Err "colon expected") * ExpectedValue)
ObjectContent = Pair * Space * (P"," * g.Cc'cont' + g.Cc'last') * g.Cp()
local DecodeValue = ExpectedValue * g.Cp ()
function json.decode (str, pos, nullval, ...)
local state = {}
state.objectmeta, state.arraymeta = optionalmetatables(...)
local obj, retpos = pegmatch (DecodeValue, str, pos, nullval, state)
if state.msg then
return nil, state.pos, state.msg
else
return obj, retpos
end
end
-- use this function only once:
json.use_lpeg = function () return json end
json.using_lpeg = true
return json -- so you can get the module using json = require "dkjson".use_lpeg()
end
if always_try_using_lpeg then
pcall (json.use_lpeg)
end
return json

View File

@ -0,0 +1,75 @@
#!/usr/bin/env lua
local dkjson = require "dkjson"
local function load_keymap(target, target_map, macos)
_G.MACOS = macos
package.loaded["core.keymap"] = nil
local keymap = require "core.keymap"
if target then
keymap.map = {}
require(target)
end
target_map = target_map or {}
-- keymap.reverse_map does not do this?
for key, actions in pairs(keymap.map) do
for _, action in ipairs(actions) do
target_map[action] = target_map[action] or {}
table.insert(target_map[action], key)
end
end
return target_map
end
local function normalize(map)
local result = {}
for action, keys in pairs(map) do
local uniq = {}
local r = { combination = {}, action = action }
for _, v in ipairs(keys) do
if not uniq[v] then
uniq[v] = true
r.combination[#r.combination+1] = v
end
end
result[#result+1] = r
end
table.sort(result, function(a, b) return a.action < b.action end)
return result
end
local function process_module(mod, filename)
local map = {}
load_keymap(mod, map)
load_keymap(mod, map, true)
map = normalize(map)
local f = assert(io.open(filename, "wb"))
f:write(dkjson.encode(map, { indent = true }))
f:close()
end
print("Warning: this is not guaranteed to work outside lite-xl's own keymap. Proceed with caution")
local LITE_ROOT = arg[1]
if not LITE_ROOT then
error("LITE_ROOT is not given")
end
package.path = package.path .. ";" .. LITE_ROOT .. "/?.lua;" .. LITE_ROOT .. "/?/init.lua"
-- fix core.command (because we don't want load the entire thing)
package.loaded["core.command"] = {}
if #arg > 1 then
for i = 2, #arg do
process_module(arg[i], arg[i] .. ".json")
print(string.format("Exported keymap in %q.", arg[i]))
end
else
process_module(nil, "core.keymap.json")
print("Exported the default keymap.")
end

75
scripts/lhelper.sh Normal file
View File

@ -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

8
scripts/meson.build Normal file
View File

@ -0,0 +1,8 @@
if host_machine.system() == 'windows'
configure_file(
input : 'innosetup/innosetup.iss.in',
output : 'innosetup.iss',
configuration : conf_data
)
endif

259
scripts/package.sh Normal file
View File

@ -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 "$@"

View File

@ -10,7 +10,7 @@ copy_directory_from_repo () {
fi fi
local dirname="$1" local dirname="$1"
local destdir="$2" 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 () { lite_copy_third_party_modules () {
@ -23,12 +23,17 @@ lite_copy_third_party_modules () {
rm "$build/rxi-lite-colors.zip" rm "$build/rxi-lite-colors.zip"
} }
lite_branch=master
while [ ! -z ${1+x} ]; do while [ ! -z ${1+x} ]; do
case "$1" in case "$1" in
-dir) -dir)
use_dir="$(realpath $2)" use_dir="$(realpath $2)"
shift 2 shift 2
;; ;;
-branch)
lite_branch="$2"
shift 2
;;
*) *)
echo "unknown option: $1" echo "unknown option: $1"
exit 1 exit 1
@ -73,6 +78,8 @@ for filename in $(ls -1 *.zip *.tar.*); do
fi fi
rm "$filename" rm "$filename"
find lite-xl -name lite -exec chmod a+x '{}' \; 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')" xcoredir="$(find lite-xl -type d -name 'core')"
coredir="$(dirname $xcoredir)" coredir="$(dirname $xcoredir)"
echo "coredir: $coredir" echo "coredir: $coredir"
@ -81,6 +88,7 @@ for filename in $(ls -1 *.zip *.tar.*); do
rm -fr "$coredir/$module_name" rm -fr "$coredir/$module_name"
(cd .. && copy_directory_from_repo --strip-components=1 "data/$module_name" "$workdir/$coredir") (cd .. && copy_directory_from_repo --strip-components=1 "data/$module_name" "$workdir/$coredir")
done done
sed -i "s/@PROJECT_VERSION@/$lite_version/g" "$start_file"
for module_name in plugins colors; do for module_name in plugins colors; do
cp -r "third/data/$module_name" "$coredir" cp -r "third/data/$module_name" "$coredir"
done done

View File

@ -47,10 +47,7 @@ if [[ "$OSTYPE" == "msys"* || "$OSTYPE" == "mingw"* ]]; then
fi fi
rundir=".run" rundir=".run"
if [[ "$OSTYPE" == "darwin"* ]]; then if [ "$option_portable" == on ]; then
bindir="$rundir"
datadir="$rundir"
elif [ "$option_portable" == on ]; then
bindir="$rundir" bindir="$rundir"
datadir="$rundir/data" datadir="$rundir/data"
else else
@ -75,9 +72,14 @@ copy_lite_build () {
else else
cp "$builddir/src/lite-xl" "$bindir" cp "$builddir/src/lite-xl" "$bindir"
fi fi
mkdir -p "$datadir/core"
for module_name in core plugins colors fonts; do for module_name in core plugins colors fonts; do
cp -r "data/$module_name" "$datadir" cp -r "data/$module_name" "$datadir"
done 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 () { run_lite () {

View File

@ -6,7 +6,6 @@ int luaopen_renderer(lua_State *L);
int luaopen_regex(lua_State *L); int luaopen_regex(lua_State *L);
int luaopen_process(lua_State *L); int luaopen_process(lua_State *L);
static const luaL_Reg libs[] = { static const luaL_Reg libs[] = {
{ "system", luaopen_system }, { "system", luaopen_system },
{ "renderer", luaopen_renderer }, { "renderer", luaopen_renderer },
@ -16,7 +15,6 @@ static const luaL_Reg libs[] = {
}; };
void api_load_libs(lua_State *L) { void api_load_libs(lua_State *L) {
for (int i = 0; libs[i].name; i++) { for (int i = 0; libs[i].name; i++)
luaL_requiref(L, libs[i].name, libs[i].func, 1); luaL_requiref(L, libs[i].name, libs[i].func, 1);
}
} }

View File

@ -1,12 +1,11 @@
#ifndef API_H #ifndef API_H
#define API_H #define API_H
#include "lua.h" #include <lua.h>
#include "lauxlib.h" #include <lauxlib.h>
#include "lualib.h" #include <lualib.h>
#define API_TYPE_FONT "Font" #define API_TYPE_FONT "Font"
#define API_TYPE_REPLACE "Replace"
#define API_TYPE_PROCESS "Process" #define API_TYPE_PROCESS "Process"
void api_load_libs(lua_State *L); void api_load_libs(lua_State *L);

View File

@ -1,42 +0,0 @@
#include "api.h"
#include "renderer.h"
static int f_new(lua_State *L) {
CPReplaceTable *rep_table = lua_newuserdata(L, sizeof(CPReplaceTable));
luaL_setmetatable(L, API_TYPE_REPLACE);
ren_cp_replace_init(rep_table);
return 1;
}
static int f_gc(lua_State *L) {
CPReplaceTable *rep_table = luaL_checkudata(L, 1, API_TYPE_REPLACE);
ren_cp_replace_free(rep_table);
return 0;
}
static int f_add(lua_State *L) {
CPReplaceTable *rep_table = luaL_checkudata(L, 1, API_TYPE_REPLACE);
const char *src = luaL_checkstring(L, 2);
const char *dst = luaL_checkstring(L, 3);
ren_cp_replace_add(rep_table, src, dst);
return 0;
}
static const luaL_Reg lib[] = {
{ "__gc", f_gc },
{ "new", f_new },
{ "add", f_add },
{ NULL, NULL }
};
int luaopen_renderer_replacements(lua_State *L) {
luaL_newmetatable(L, API_TYPE_REPLACE);
luaL_setfuncs(L, lib, 0);
lua_pushvalue(L, -1);
lua_setfield(L, -2, "__index");
return 1;
}

View File

@ -10,28 +10,166 @@
#include <reproc/reproc.h> #include <reproc/reproc.h>
#include "api.h" #include "api.h"
#define READ_BUF_SIZE 4096 #define READ_BUF_SIZE 2048
#define L_GETTABLE(L, idx, key, conv, def) ( \
lua_getfield(L, idx, key), \
conv(L, -1, def) \
)
#define L_GETNUM(L, idx, key, def) L_GETTABLE(L, idx, key, luaL_optnumber, def)
#define L_GETSTR(L, idx, key, def) L_GETTABLE(L, idx, key, luaL_optstring, def)
#define L_SETNUM(L, idx, key, n) (lua_pushnumber(L, n), lua_setfield(L, idx - 1, key))
#define L_RETURN_REPROC_ERROR(L, code) { \
lua_pushnil(L); \
lua_pushstring(L, reproc_strerror(code)); \
lua_pushnumber(L, code); \
return 3; \
}
#define ASSERT_MALLOC(ptr) \
if (ptr == NULL) \
L_RETURN_REPROC_ERROR(L, REPROC_ENOMEM)
#define ASSERT_REPROC_ERRNO(L, code) { \
if (code < 0) \
L_RETURN_REPROC_ERROR(L, code) \
}
typedef struct { typedef struct {
reproc_t * process; reproc_t * process;
lua_State* L; bool running;
int returncode;
} process_t; } process_t;
static int process_new(lua_State* L) // this function should be called instead of reproc_wait
static int poll_process(process_t* proc, int timeout)
{ {
process_t* self = (process_t*) lua_newuserdata( int ret = reproc_wait(proc->process, timeout);
L, sizeof(process_t) if (ret != REPROC_ETIMEDOUT) {
proc->running = false;
proc->returncode = ret;
}
return ret;
}
static int kill_process(process_t* proc)
{
int ret = reproc_stop(
proc->process,
(reproc_stop_actions) {
{REPROC_STOP_KILL, 0},
{REPROC_STOP_TERMINATE, 0},
{REPROC_STOP_NOOP, 0}
}
); );
memset(self, 0, sizeof(process_t)); if (ret != REPROC_ETIMEDOUT) {
proc->running = false;
proc->returncode = ret;
}
self->process = NULL; return ret;
self->L = L; }
luaL_getmetatable(L, API_TYPE_PROCESS); static int process_start(lua_State* L)
lua_setmetatable(L, -2); {
luaL_checktype(L, 1, LUA_TTABLE);
if (lua_isnoneornil(L, 2)) {
lua_settop(L, 1); // remove the nil if it's there
lua_newtable(L);
}
luaL_checktype(L, 2, LUA_TTABLE);
int cmd_len = lua_rawlen(L, 1);
const char** cmd = malloc(sizeof(char *) * (cmd_len + 1));
ASSERT_MALLOC(cmd);
cmd[cmd_len] = NULL;
for(int i = 0; i < cmd_len; i++) {
lua_rawgeti(L, 1, i + 1);
cmd[i] = luaL_checkstring(L, -1);
lua_pop(L, 1);
}
int deadline = L_GETNUM(L, 2, "timeout", 0);
const char* cwd =L_GETSTR(L, 2, "cwd", NULL);
int redirect_in = L_GETNUM(L, 2, "stdin", REPROC_REDIRECT_DEFAULT);
int redirect_out = L_GETNUM(L, 2, "stdout", REPROC_REDIRECT_DEFAULT);
int redirect_err = L_GETNUM(L, 2, "stderr", REPROC_REDIRECT_DEFAULT);
lua_pop(L, 5); // remove args we just read
if (
redirect_in > REPROC_REDIRECT_STDOUT
|| redirect_out > REPROC_REDIRECT_STDOUT
|| redirect_err > REPROC_REDIRECT_STDOUT)
{
lua_pushnil(L);
lua_pushliteral(L, "redirect to handles, FILE* and paths are not supported");
return 2;
}
// env
luaL_getsubtable(L, 2, "env");
const char **env = NULL;
int env_len = 0;
lua_pushnil(L);
while (lua_next(L, -2) != 0) {
env_len++;
lua_pop(L, 1);
}
if (env_len > 0) {
env = malloc(sizeof(char*) * (env_len + 1));
env[env_len] = NULL;
int i = 0;
lua_pushnil(L);
while (lua_next(L, -2) != 0) {
lua_pushliteral(L, "=");
lua_pushvalue(L, -3); // push the key to the top
lua_concat(L, 3); // key=value
env[i++] = luaL_checkstring(L, -1);
lua_pop(L, 1);
}
}
reproc_t* proc = reproc_new();
int out = reproc_start(
proc,
(const char* const*) cmd,
(reproc_options) {
.working_directory = cwd,
.deadline = deadline,
.nonblocking = true,
.env = {
.behavior = REPROC_ENV_EXTEND,
.extra = env
},
.redirect = {
.in.type = redirect_in,
.out.type = redirect_out,
.err.type = redirect_err
}
}
);
if (out < 0) {
reproc_destroy(proc);
L_RETURN_REPROC_ERROR(L, out);
}
process_t* self = lua_newuserdata(L, sizeof(process_t));
self->process = proc;
self->running = true;
// this is equivalent to using lua_setmetatable()
luaL_setmetatable(L, API_TYPE_PROCESS);
return 1; return 1;
} }
@ -39,24 +177,20 @@ static int process_strerror(lua_State* L)
{ {
int error_code = luaL_checknumber(L, 1); int error_code = luaL_checknumber(L, 1);
if(error_code){ if (error_code < 0)
lua_pushstring( lua_pushstring(L, reproc_strerror(error_code));
L, else
reproc_strerror(error_code)
);
} else {
lua_pushnil(L); lua_pushnil(L);
}
return 1; return 1;
} }
static int process_gc(lua_State* L) static int f_gc(lua_State* L)
{ {
process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS); process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS);
if(self->process){ if(self->process) {
reproc_kill(self->process); kill_process(self);
reproc_destroy(self->process); reproc_destroy(self->process);
self->process = NULL; self->process = NULL;
} }
@ -64,330 +198,211 @@ static int process_gc(lua_State* L)
return 0; return 0;
} }
static int process_start(lua_State* L) static int f_tostring(lua_State* L)
{ {
process_t* self = (process_t*) lua_touserdata(L, 1); luaL_checkudata(L, 1, API_TYPE_PROCESS);
luaL_checktype(L, 2, LUA_TTABLE);
char* path = NULL;
size_t path_len = 0;
if(lua_type(L, 3) == LUA_TSTRING){
path = (char*) lua_tolstring(L, 3, &path_len);
}
size_t deadline = 0;
if(lua_type(L, 4) == LUA_TNUMBER){
deadline = lua_tonumber(L, 4);
}
size_t table_len = luaL_len(L, 2);
char* command[table_len+1];
command[table_len] = NULL;
int i;
for(i=1; i<=table_len; i++){
lua_pushnumber(L, i);
lua_gettable(L, 2);
command[i-1] = (char*) lua_tostring(L, -1);
lua_remove(L, -1);
}
if(self->process){
reproc_kill(self->process);
reproc_destroy(self->process);
}
self->process = reproc_new();
int out = reproc_start(
self->process,
(const char* const*) command,
(reproc_options){
.working_directory = path,
.deadline = deadline,
.nonblocking=true,
.redirect.err.type=REPROC_REDIRECT_PIPE
}
);
if(out > 0) {
lua_pushboolean(L, 1);
}
else {
reproc_destroy(self->process);
self->process = NULL;
lua_pushnumber(L, out);
}
lua_pushliteral(L, API_TYPE_PROCESS);
return 1; return 1;
} }
static int process_pid(lua_State* L) static int f_pid(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){ lua_pushnumber(L, reproc_pid(self->process));
int id = reproc_pid(self->process); return 1;
}
if(id > 0){
lua_pushnumber(L, id); static int f_returncode(lua_State *L)
} else { {
lua_pushnumber(L, 0); process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS);
} int ret = poll_process(self, 0);
} else {
lua_pushnumber(L, 0); if (self->running)
} lua_pushnil(L);
else
lua_pushnumber(L, ret);
return 1; return 1;
} }
static int g_read(lua_State* L, int stream) 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){ luaL_Buffer b;
int read_size = READ_BUF_SIZE; uint8_t* buffer = (uint8_t*) luaL_buffinitsize(L, &b, read_size);
if (lua_type(L, 2) == LUA_TNUMBER){
read_size = (int) lua_tonumber(L, 2);
}
int tries = 1; int out = reproc_read(
if (lua_type(L, 3) == LUA_TNUMBER){ self->process,
tries = (int) lua_tonumber(L, 3); stream,
} buffer,
read_size
);
int out = 0; if (out >= 0)
uint8_t buffer[read_size]; luaL_addsize(&b, out);
luaL_pushresult(&b);
int runs; if (out == REPROC_EPIPE) {
for (runs=0; runs<tries; runs++){ kill_process(self);
out = reproc_read( ASSERT_REPROC_ERRNO(L, out);
self->process,
REPROC_STREAM_OUT,
buffer,
read_size
);
if (out >= 0)
break;
}
if(out == REPROC_EPIPE){
reproc_kill(self->process);
reproc_destroy(self->process);
self->process = NULL;
lua_pushnil(L);
} else if(out > 0) {
lua_pushlstring(L, (const char*) buffer, out);
} else {
lua_pushnil(L);
}
} else {
lua_pushnil(L);
} }
return 1; return 1;
} }
static int process_read(lua_State* L) static int f_read_stdout(lua_State* L)
{ {
return g_read(L, REPROC_STREAM_OUT); return g_read(L, REPROC_STREAM_OUT);
} }
static int process_read_errors(lua_State* L) static int f_read_stderr(lua_State* L)
{ {
return g_read(L, REPROC_STREAM_ERR); return g_read(L, REPROC_STREAM_ERR);
} }
static int process_write(lua_State* L) static int f_read(lua_State* L)
{ {
process_t* self = (process_t*) lua_touserdata(L, 1); int stream = luaL_checknumber(L, 2);
lua_remove(L, 2);
if (stream > REPROC_STREAM_ERR)
L_RETURN_REPROC_ERROR(L, REPROC_EINVAL);
if(self->process){ return g_read(L, stream);
size_t data_size = 0; }
const char* data = luaL_checklstring(L, 2, &data_size);
int out = 0; static int f_write(lua_State* L)
{
process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS);
out = reproc_write( size_t data_size = 0;
self->process, const char* data = luaL_checklstring(L, 2, &data_size);
(uint8_t*) data,
data_size
);
if(out == REPROC_EPIPE){ int out = reproc_write(
reproc_kill(self->process); self->process,
reproc_destroy(self->process); (uint8_t*) data,
self->process = NULL; data_size
} );
if (out == REPROC_EPIPE) {
lua_pushnumber(L, out); kill_process(self);
} else { L_RETURN_REPROC_ERROR(L, out);
lua_pushnumber(L, REPROC_EPIPE);
} }
lua_pushnumber(L, out);
return 1;
}
static int f_close_stream(lua_State* L)
{
process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS);
int stream = luaL_checknumber(L, 2);
int out = reproc_close(self->process, stream);
ASSERT_REPROC_ERRNO(L, out);
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 f_terminate(lua_State* L)
{
process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS);
int out = reproc_terminate(self->process);
ASSERT_REPROC_ERRNO(L, out);
poll_process(self, 0);
lua_pushboolean(L, 1);
return 1; return 1;
} }
static int process_close_stream(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);
size_t stream = luaL_checknumber(L, 2); ASSERT_REPROC_ERRNO(L, out);
int out = reproc_close(self->process, stream); poll_process(self, 0);
lua_pushboolean(L, 1);
lua_pushnumber(L, out);
} else {
lua_pushnumber(L, REPROC_EINVAL);
}
return 1; return 1;
} }
static int process_wait(lua_State* L) static int f_running(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){ poll_process(self, 0);
size_t timeout = luaL_checknumber(L, 2); lua_pushboolean(L, self->running);
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);
}
return 1; return 1;
} }
static int process_terminate(lua_State* L) static const struct luaL_Reg lib[] = {
{
process_t* self = (process_t*) lua_touserdata(L, 1);
if(self->process){
int out = reproc_terminate(self->process);
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);
}
return 1;
}
static int process_kill(lua_State* L)
{
process_t* self = (process_t*) lua_touserdata(L, 1);
if(self->process){
int out = reproc_kill(self->process);
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);
}
return 1;
}
static int process_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);
}
return 1;
}
static const struct luaL_Reg process_methods[] = {
{ "__gc", process_gc},
{"start", process_start}, {"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}, {"strerror", process_strerror},
{"ERROR_PIPE", NULL}, {"__gc", f_gc},
{"ERROR_WOULDBLOCK", NULL}, {"__tostring", f_tostring},
{"ERROR_TIMEDOUT", NULL}, {"pid", f_pid},
{"ERROR_INVALID", NULL}, {"returncode", f_returncode},
{"STREAM_STDIN", NULL}, {"read", f_read},
{"STREAM_STDOUT", NULL}, {"read_stdout", f_read_stdout},
{"STREAM_STDERR", NULL}, {"read_stderr", f_read_stderr},
{"WAIT_INFINITE", NULL}, {"write", f_write},
{"WAIT_DEADLINE", NULL}, {"close_stream", f_close_stream},
{"wait", f_wait},
{"terminate", f_terminate},
{"kill", f_kill},
{"running", f_running},
{NULL, NULL} {NULL, NULL}
}; };
int luaopen_process(lua_State *L) int luaopen_process(lua_State *L)
{ {
luaL_newmetatable(L, API_TYPE_PROCESS); luaL_newmetatable(L, API_TYPE_PROCESS);
luaL_setfuncs(L, process_methods, 0); luaL_setfuncs(L, lib, 0);
lua_pushvalue(L, -1); lua_pushvalue(L, -1);
lua_setfield(L, -2, "__index"); lua_setfield(L, -2, "__index");
luaL_newlib(L, process); // constants
L_SETNUM(L, -1, "ERROR_INVAL", REPROC_EINVAL);
L_SETNUM(L, -1, "ERROR_TIMEDOUT", REPROC_ETIMEDOUT);
L_SETNUM(L, -1, "ERROR_PIPE", REPROC_EPIPE);
L_SETNUM(L, -1, "ERROR_NOMEM", REPROC_ENOMEM);
L_SETNUM(L, -1, "ERROR_WOULDBLOCK", REPROC_EWOULDBLOCK);
lua_pushnumber(L, REPROC_EPIPE); L_SETNUM(L, -1, "WAIT_INFINITE", REPROC_INFINITE);
lua_setfield(L, -2, "ERROR_PIPE"); L_SETNUM(L, -1, "WAIT_DEADLINE", REPROC_DEADLINE);
lua_pushnumber(L, REPROC_EWOULDBLOCK); L_SETNUM(L, -1, "STREAM_STDIN", REPROC_STREAM_IN);
lua_setfield(L, -2, "ERROR_WOULDBLOCK"); L_SETNUM(L, -1, "STREAM_STDOUT", REPROC_STREAM_OUT);
L_SETNUM(L, -1, "STREAM_STDERR", REPROC_STREAM_ERR);
lua_pushnumber(L, REPROC_ETIMEDOUT); L_SETNUM(L, -1, "REDIRECT_DEFAULT", REPROC_REDIRECT_DEFAULT);
lua_setfield(L, -2, "ERROR_TIMEDOUT"); L_SETNUM(L, -1, "REDIRECT_PIPE", REPROC_REDIRECT_PIPE);
L_SETNUM(L, -1, "REDIRECT_PARENT", REPROC_REDIRECT_PARENT);
lua_pushnumber(L, REPROC_EINVAL); L_SETNUM(L, -1, "REDIRECT_DISCARD", REPROC_REDIRECT_DISCARD);
lua_setfield(L, -2, "ERROR_INVALID"); L_SETNUM(L, -1, "REDIRECT_STDOUT", REPROC_REDIRECT_STDOUT);
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");
return 1; return 1;
} }

View File

@ -74,11 +74,11 @@ static int f_pcre_match(lua_State *L) {
} }
PCRE2_SIZE* ovector = pcre2_get_ovector_pointer(md); PCRE2_SIZE* ovector = pcre2_get_ovector_pointer(md);
if (ovector[0] > ovector[1]) { 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, assertion to set the start of a match later than its end. In the editor,
we just detect this case and give up. */ we just detect this case and give up. */
luaL_error(L, "regex matching error: \\K was used in an assertion to " 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); pcre2_match_data_free(md);
return 0; return 0;
} }
@ -103,8 +103,8 @@ int luaopen_regex(lua_State *L) {
lua_setfield(L, LUA_REGISTRYINDEX, "regex"); lua_setfield(L, LUA_REGISTRYINDEX, "regex");
lua_pushnumber(L, PCRE2_ANCHORED); lua_pushnumber(L, PCRE2_ANCHORED);
lua_setfield(L, -2, "ANCHORED"); lua_setfield(L, -2, "ANCHORED");
lua_pushnumber(L, PCRE2_ANCHORED) ; lua_pushnumber(L, PCRE2_ANCHORED) ;
lua_setfield(L, -2, "ENDANCHORED"); lua_setfield(L, -2, "ENDANCHORED");
lua_pushnumber(L, PCRE2_NOTBOL); lua_pushnumber(L, PCRE2_NOTBOL);
lua_setfield(L, -2, "NOTBOL"); lua_setfield(L, -2, "NOTBOL");
lua_pushnumber(L, PCRE2_NOTEOL); lua_pushnumber(L, PCRE2_NOTEOL);

View File

@ -2,6 +2,131 @@
#include "renderer.h" #include "renderer.h"
#include "rencache.h" #include "rencache.h"
static int f_font_load(lua_State *L) {
const char *filename = luaL_checkstring(L, 1);
float size = luaL_checknumber(L, 2);
unsigned int font_hinting = FONT_HINTING_SLIGHT, font_style = 0;
bool subpixel = true;
if (lua_gettop(L) > 2 && lua_istable(L, 3)) {
lua_getfield(L, 3, "antialiasing");
if (lua_isstring(L, -1)) {
const char *antialiasing = lua_tostring(L, -1);
if (antialiasing) {
if (strcmp(antialiasing, "grayscale") == 0) {
subpixel = false;
} else if (strcmp(antialiasing, "subpixel") == 0) {
subpixel = true;
} else {
return luaL_error(L, "error in renderer.font.load, unknown antialiasing option: \"%s\"", antialiasing);
}
}
}
lua_getfield(L, 3, "hinting");
if (lua_isstring(L, -1)) {
const char *hinting = lua_tostring(L, -1);
if (hinting) {
if (strcmp(hinting, "slight") == 0) {
font_hinting = FONT_HINTING_SLIGHT;
} else if (strcmp(hinting, "none") == 0) {
font_hinting = FONT_HINTING_NONE;
} else if (strcmp(hinting, "full") == 0) {
font_hinting = FONT_HINTING_FULL;
} else {
return luaL_error(L, "error in renderer.font.load, unknown hinting option: \"%s\"", hinting);
}
}
}
lua_getfield(L, 3, "italic");
if (lua_toboolean(L, -1))
font_style |= FONT_STYLE_ITALIC;
lua_getfield(L, 3, "bold");
if (lua_toboolean(L, -1))
font_style |= FONT_STYLE_BOLD;
lua_getfield(L, 3, "underline");
if (lua_toboolean(L, -1))
font_style |= FONT_STYLE_UNDERLINE;
lua_pop(L, 5);
}
RenFont** font = lua_newuserdata(L, sizeof(RenFont*));
*font = ren_font_load(filename, size, subpixel, font_hinting, font_style);
if (!*font)
return luaL_error(L, "failed to load font");
luaL_setmetatable(L, API_TYPE_FONT);
return 1;
}
static bool font_retrieve(lua_State* L, RenFont** fonts, int idx) {
memset(fonts, 0, sizeof(RenFont*)*FONT_FALLBACK_MAX);
if (lua_type(L, 1) != LUA_TTABLE) {
fonts[0] = *(RenFont**)luaL_checkudata(L, idx, API_TYPE_FONT);
return false;
}
int i = 0;
do {
lua_rawgeti(L, idx, i+1);
fonts[i] = !lua_isnil(L, -1) ? *(RenFont**)luaL_checkudata(L, -1, API_TYPE_FONT) : NULL;
lua_pop(L, 1);
} while (fonts[i] && i++ < FONT_FALLBACK_MAX);
return true;
}
static int f_font_copy(lua_State *L) {
RenFont* fonts[FONT_FALLBACK_MAX];
bool table = font_retrieve(L, fonts, 1);
float size = lua_gettop(L) >= 2 ? luaL_checknumber(L, 2) : ren_font_group_get_height(fonts);
if (table) {
lua_newtable(L);
luaL_setmetatable(L, API_TYPE_FONT);
}
for (int i = 0; i < FONT_FALLBACK_MAX && fonts[i]; ++i) {
RenFont** font = lua_newuserdata(L, sizeof(RenFont*));
*font = ren_font_copy(fonts[i], size);
if (!*font)
return luaL_error(L, "failed to copy font");
luaL_setmetatable(L, API_TYPE_FONT);
if (table)
lua_rawseti(L, -2, i+1);
}
return 1;
}
static int f_font_group(lua_State* L) {
luaL_checktype(L, 1, LUA_TTABLE);
luaL_setmetatable(L, API_TYPE_FONT);
return 1;
}
static int f_font_set_tab_size(lua_State *L) {
RenFont* fonts[FONT_FALLBACK_MAX]; font_retrieve(L, fonts, 1);
int n = luaL_checknumber(L, 2);
ren_font_group_set_tab_size(fonts, n);
return 0;
}
static int f_font_gc(lua_State *L) {
RenFont** self = luaL_checkudata(L, 1, API_TYPE_FONT);
ren_font_free(*self);
return 0;
}
static int f_font_get_width(lua_State *L) {
RenFont* fonts[FONT_FALLBACK_MAX]; font_retrieve(L, fonts, 1);
lua_pushnumber(L, ren_font_group_get_width(fonts, luaL_checkstring(L, 2)));
return 1;
}
static int f_font_get_height(lua_State *L) {
RenFont* fonts[FONT_FALLBACK_MAX]; font_retrieve(L, fonts, 1);
lua_pushnumber(L, ren_font_group_get_height(fonts));
return 1;
}
static int f_font_get_size(lua_State *L) {
RenFont* fonts[FONT_FALLBACK_MAX]; font_retrieve(L, fonts, 1);
lua_pushnumber(L, ren_font_group_get_size(fonts));
return 1;
}
static RenColor checkcolor(lua_State *L, int idx, int def) { static RenColor checkcolor(lua_State *L, int idx, int def) {
RenColor color; RenColor color;
@ -71,40 +196,18 @@ static int f_draw_rect(lua_State *L) {
return 0; return 0;
} }
static int draw_text_subpixel_impl(lua_State *L, bool draw_subpixel) { static int f_draw_text(lua_State *L) {
FontDesc *font_desc = luaL_checkudata(L, 1, API_TYPE_FONT); RenFont* fonts[FONT_FALLBACK_MAX];
font_retrieve(L, fonts, 1);
const char *text = luaL_checkstring(L, 2); const char *text = luaL_checkstring(L, 2);
/* The coordinate below will be in subpixel iff draw_subpixel is true. float x = luaL_checknumber(L, 3);
Otherwise it will be in pixels. */
int x_subpixel = luaL_checknumber(L, 3);
int y = luaL_checknumber(L, 4); int y = luaL_checknumber(L, 4);
RenColor color = checkcolor(L, 5, 255); RenColor color = checkcolor(L, 5, 255);
x = rencache_draw_text(L, fonts, text, x, y, color);
CPReplaceTable *rep_table; lua_pushnumber(L, x);
RenColor replace_color;
if (lua_gettop(L) >= 7) {
rep_table = luaL_checkudata(L, 6, API_TYPE_REPLACE);
replace_color = checkcolor(L, 7, 255);
} else {
rep_table = NULL;
replace_color = (RenColor) {0};
}
x_subpixel = rencache_draw_text(L, font_desc, 1, text, x_subpixel, y, color, draw_subpixel, rep_table, replace_color);
lua_pushnumber(L, x_subpixel);
return 1; return 1;
} }
static int f_draw_text(lua_State *L) {
return draw_text_subpixel_impl(L, false);
}
static int f_draw_text_subpixel(lua_State *L) {
return draw_text_subpixel_impl(L, true);
}
static const luaL_Reg lib[] = { static const luaL_Reg lib[] = {
{ "show_debug", f_show_debug }, { "show_debug", f_show_debug },
{ "get_size", f_get_size }, { "get_size", f_get_size },
@ -113,19 +216,27 @@ static const luaL_Reg lib[] = {
{ "set_clip_rect", f_set_clip_rect }, { "set_clip_rect", f_set_clip_rect },
{ "draw_rect", f_draw_rect }, { "draw_rect", f_draw_rect },
{ "draw_text", f_draw_text }, { "draw_text", f_draw_text },
{ "draw_text_subpixel", f_draw_text_subpixel },
{ NULL, NULL } { NULL, NULL }
}; };
static const luaL_Reg fontLib[] = {
int luaopen_renderer_font(lua_State *L); { "__gc", f_font_gc },
int luaopen_renderer_replacements(lua_State *L); { "load", f_font_load },
{ "copy", f_font_copy },
{ "group", f_font_group },
{ "set_tab_size", f_font_set_tab_size },
{ "get_width", f_font_get_width },
{ "get_height", f_font_get_height },
{ "get_size", f_font_get_size },
{ NULL, NULL }
};
int luaopen_renderer(lua_State *L) { int luaopen_renderer(lua_State *L) {
luaL_newlib(L, lib); luaL_newlib(L, lib);
luaopen_renderer_font(L); luaL_newmetatable(L, API_TYPE_FONT);
luaL_setfuncs(L, fontLib, 0);
lua_pushvalue(L, -1);
lua_setfield(L, -2, "__index");
lua_setfield(L, -2, "font"); lua_setfield(L, -2, "font");
luaopen_renderer_replacements(L);
lua_setfield(L, -2, "replacements");
return 1; return 1;
} }

View File

@ -1,158 +0,0 @@
#include <lua.h>
#include <lauxlib.h>
#include "api.h"
#include "fontdesc.h"
#include "renderer.h"
#include "rencache.h"
static int f_load(lua_State *L) {
const char *filename = luaL_checkstring(L, 1);
float size = luaL_checknumber(L, 2);
unsigned int font_options = 0;
if (lua_gettop(L) > 2 && lua_istable(L, 3)) {
lua_getfield(L, 3, "antialiasing");
if (lua_isstring(L, -1)) {
const char *antialiasing = lua_tostring(L, -1);
if (antialiasing) {
if (strcmp(antialiasing, "grayscale") == 0) {
font_options |= RenFontGrayscale;
} else if (strcmp(antialiasing, "subpixel") == 0) {
font_options |= RenFontSubpixel;
} else {
return luaL_error(L, "error in renderer.font.load, unknown antialiasing option: \"%s\"", antialiasing);
}
}
}
lua_pop(L, 1);
lua_getfield(L, 3, "hinting");
if (lua_isstring(L, -1)) {
const char *hinting = lua_tostring(L, -1);
if (hinting) {
if (strcmp(hinting, "slight") == 0) {
font_options |= RenFontHintingSlight;
} else if (strcmp(hinting, "none") == 0) {
font_options |= RenFontHintingNone;
} else if (strcmp(hinting, "full") == 0) {
font_options |= RenFontHintingFull;
} else {
return luaL_error(L, "error in renderer.font.load, unknown hinting option: \"%s\"", hinting);
}
}
}
lua_pop(L, 1);
}
if (ren_verify_font(filename)) {
luaL_error(L, "failed to load font");
}
FontDesc *font_desc = lua_newuserdata(L, font_desc_alloc_size(filename));
font_desc_init(font_desc, filename, size, font_options);
luaL_setmetatable(L, API_TYPE_FONT);
return 1;
}
static int f_copy(lua_State *L) {
FontDesc *self = luaL_checkudata(L, 1, API_TYPE_FONT);
float size;
if (lua_gettop(L) >= 2) {
size = luaL_checknumber(L, 2);
} else {
size = self->size;
}
FontDesc *new_font_desc = lua_newuserdata(L, font_desc_alloc_size(self->filename));
font_desc_init(new_font_desc, self->filename, size, self->options);
luaL_setmetatable(L, API_TYPE_FONT);
return 1;
}
static int f_set_tab_size(lua_State *L) {
FontDesc *self = luaL_checkudata(L, 1, API_TYPE_FONT);
int n = luaL_checknumber(L, 2);
font_desc_set_tab_size(self, n);
return 0;
}
static int f_gc(lua_State *L) {
FontDesc *self = luaL_checkudata(L, 1, API_TYPE_FONT);
font_desc_clear(self);
return 0;
}
static int f_get_width(lua_State *L) {
FontDesc *self = luaL_checkudata(L, 1, API_TYPE_FONT);
const char *text = luaL_checkstring(L, 2);
/* By calling ren_get_font_width with NULL as third arguments
we will obtain the width in points. */
int w = ren_get_font_width(self, text, NULL);
lua_pushnumber(L, w);
return 1;
}
static int f_subpixel_scale(lua_State *L) {
FontDesc *self = luaL_checkudata(L, 1, API_TYPE_FONT);
lua_pushnumber(L, ren_get_font_subpixel_scale(self));
return 1;
}
static int f_get_width_subpixel(lua_State *L) {
FontDesc *self = luaL_checkudata(L, 1, API_TYPE_FONT);
const char *text = luaL_checkstring(L, 2);
int subpixel_scale;
/* We need to pass a non-null subpixel_scale pointer to force
subpixel width calculation. */
lua_pushnumber(L, ren_get_font_width(self, text, &subpixel_scale));
return 1;
}
static int f_get_height(lua_State *L) {
FontDesc *self = luaL_checkudata(L, 1, API_TYPE_FONT);
lua_pushnumber(L, ren_get_font_height(self) );
return 1;
}
static int f_get_size(lua_State *L) {
FontDesc *self = luaL_checkudata(L, 1, API_TYPE_FONT);
lua_pushnumber(L, self->size);
return 1;
}
static int f_set_size(lua_State *L) {
FontDesc *self = luaL_checkudata(L, 1, API_TYPE_FONT);
float new_size = luaL_checknumber(L, 2);
font_desc_clear(self);
self->size = new_size;
return 0;
}
static const luaL_Reg lib[] = {
{ "__gc", f_gc },
{ "load", f_load },
{ "copy", f_copy },
{ "set_tab_size", f_set_tab_size },
{ "get_width", f_get_width },
{ "get_width_subpixel", f_get_width_subpixel },
{ "get_height", f_get_height },
{ "subpixel_scale", f_subpixel_scale },
{ "get_size", f_get_size },
{ "set_size", f_set_size },
{ NULL, NULL }
};
int luaopen_renderer_font(lua_State *L) {
luaL_newmetatable(L, API_TYPE_FONT);
luaL_setfuncs(L, lib, 0);
lua_pushvalue(L, -1);
lua_setfield(L, -2, "__index");
return 1;
}

Some files were not shown because too many files have changed in this diff Show More