Compare commits

..

1 Commits

Author SHA1 Message Date
Francesco Abbate f5cc91e400 WIP: testing IME text editing
This commit is a test for IME text editing.

For CJK text editing you may need to use a font that support
CHK characters like:

https://github.com/be5invis/Sarasa-Gothic

Currently on windows we receive no text editing events.
2021-10-04 11:18:02 +02:00
271 changed files with 7635 additions and 35028 deletions

38
.github/labeler.yml vendored
View File

@ -1,38 +0,0 @@
"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/**/*
"Category: Libraries":
- lib/**/*

View File

@ -1,16 +0,0 @@
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,20 +1,45 @@
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:
- '*' - '*'
workflow_dispatch:
jobs: jobs:
archive_source_code:
name: Source Code Tarball
runs-on: ubuntu-18.04
# Only on tags/releases
if: startsWith(github.ref, 'refs/tags/')
steps:
- uses: actions/checkout@v2
- name: Python Setup
uses: actions/setup-python@v2
with:
python-version: 3.6
- name: Install Dependencies
run: |
sudo apt-get install -qq ninja-build
pip3 install meson
- name: Package
shell: bash
run: bash scripts/package.sh --version ${GITHUB_REF##*/} --debug --source
- uses: actions/upload-artifact@v2
with:
name: Source Code Tarball
path: "lite-xl-*-src.tar.gz"
build_linux: build_linux:
name: Linux name: Linux
runs-on: ubuntu-22.04 runs-on: ubuntu-18.04
strategy: strategy:
matrix: matrix:
config: config:
@ -24,204 +49,204 @@ jobs:
CC: ${{ matrix.config.cc }} CC: ${{ matrix.config.cc }}
CXX: ${{ matrix.config.cxx }} CXX: ${{ matrix.config.cxx }}
steps: steps:
- name: Set Environment Variables - name: Set Environment Variables
if: ${{ matrix.config.cc == 'gcc' }} if: ${{ matrix.config.cc == 'gcc' }}
run: | run: |
echo "$HOME/.local/bin" >> "$GITHUB_PATH" echo "$HOME/.local/bin" >> "$GITHUB_PATH"
echo "INSTALL_REF=${GITHUB_REF##*/}" >> "$GITHUB_ENV" echo "INSTALL_REF=${GITHUB_REF##*/}" >> "$GITHUB_ENV"
echo "INSTALL_NAME=lite-xl-${GITHUB_REF##*/}-linux-$(uname -m)-portable" >> "$GITHUB_ENV" echo "INSTALL_NAME=lite-xl-${GITHUB_REF##*/}-linux-$(uname -m)" >> "$GITHUB_ENV"
- uses: actions/checkout@v3 - uses: actions/checkout@v2
- name: Python Setup - name: Python Setup
uses: actions/setup-python@v4 uses: actions/setup-python@v2
with: with:
python-version: 3.9 python-version: 3.6
- name: Update Packages - name: Update Packages
run: sudo apt-get update run: sudo apt-get update
- name: Install Dependencies - name: Install Dependencies
run: bash scripts/install-dependencies.sh --debug if: ${{ !startsWith(github.ref, 'refs/tags/') }}
- name: Build run: bash scripts/install-dependencies.sh --debug
run: | - name: Install Release Dependencies
bash --version if: ${{ startsWith(github.ref, 'refs/tags/') }}
bash scripts/build.sh --debug --forcefallback --portable run: |
- name: Package bash scripts/install-dependencies.sh --debug --lhelper
if: ${{ matrix.config.cc == 'gcc' }} bash scripts/lhelper.sh --debug
run: bash scripts/package.sh --version ${INSTALL_REF} --debug --binary - name: Build
- name: Upload Artifacts run: |
uses: actions/upload-artifact@v3 bash --version
if: ${{ matrix.config.cc == 'gcc' }} bash scripts/build.sh --debug --forcefallback
with: - name: Package
name: Linux Artifacts if: ${{ matrix.config.cc == 'gcc' }}
path: ${{ env.INSTALL_NAME }}.tar.gz 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_macos: build_macos:
name: macOS name: macOS (x86_64)
runs-on: macos-11 runs-on: macos-10.15
env: env:
CC: clang CC: clang
CXX: clang++ CXX: clang++
strategy:
matrix:
arch: ['x86_64', 'arm64']
steps: steps:
- name: System Information - name: System Information
run: | run: |
system_profiler SPSoftwareDataType system_profiler SPSoftwareDataType
bash --version bash --version
gcc -v gcc -v
xcodebuild -version xcodebuild -version
- name: Set Environment Variables - name: Set Environment Variables
run: | run: |
echo "$HOME/.local/bin" >> "$GITHUB_PATH" echo "$HOME/.local/bin" >> "$GITHUB_PATH"
echo "INSTALL_REF=${GITHUB_REF##*/}" >> "$GITHUB_ENV" echo "INSTALL_REF=${GITHUB_REF##*/}" >> "$GITHUB_ENV"
echo "INSTALL_NAME=lite-xl-${GITHUB_REF##*/}-macos-${{ matrix.arch }}" >> "$GITHUB_ENV" echo "INSTALL_NAME=lite-xl-${GITHUB_REF##*/}-macos-$(uname -m)" >> "$GITHUB_ENV"
if [[ $(uname -m) != ${{ matrix.arch }} ]]; then echo "ARCH=--cross-arch ${{ matrix.arch }}" >> "$GITHUB_ENV"; fi - uses: actions/checkout@v2
- uses: actions/checkout@v3 - name: Python Setup
- name: Python Setup uses: actions/setup-python@v2
uses: actions/setup-python@v4 with:
with: python-version: 3.9
python-version: 3.9 - name: Install Dependencies
- name: Install Dependencies if: ${{ !startsWith(github.ref, 'refs/tags/') }}
# --lhelper will eliminate a warning with arm64 and libusb run: bash scripts/install-dependencies.sh --debug
run: bash scripts/install-dependencies.sh --debug --lhelper - name: Install Release Dependencies
- name: Build if: ${{ startsWith(github.ref, 'refs/tags/') }}
run: | run: |
bash --version bash scripts/install-dependencies.sh --debug --lhelper
bash scripts/build.sh --bundle --debug --forcefallback $ARCH bash scripts/lhelper.sh --debug
- name: Create DMG Image - name: Build
run: bash scripts/package.sh --version ${INSTALL_REF} $ARCH --debug --dmg run: |
- name: Upload DMG Image bash --version
uses: actions/upload-artifact@v3 bash scripts/build.sh --bundle --debug --forcefallback
with: - name: Error Logs
name: macOS DMG Images if: failure()
path: ${{ env.INSTALL_NAME }}.dmg run: |
mkdir ${INSTALL_NAME}
build_macos_universal: cp /usr/var/lhenv/lite-xl/logs/* ${INSTALL_NAME}
name: macOS (Universal) tar czvf ${INSTALL_NAME}.tar.gz ${INSTALL_NAME}
runs-on: macos-11 # - name: Package
needs: build_macos # if: ${{ !startsWith(github.ref, 'refs/tags/') }}
steps: # run: bash scripts/package.sh --version ${INSTALL_REF} --debug --addons
- name: System Information - name: Create DMG Image
run: | run: bash scripts/package.sh --version ${INSTALL_REF} --debug --addons --dmg
system_profiler SPSoftwareDataType - name: Upload DMG Image
bash --version uses: actions/upload-artifact@v2
gcc -v with:
xcodebuild -version name: macOS DMG Image
- name: Set Environment Variables path: ${{ env.INSTALL_NAME }}.dmg
run: | - name: Upload Error Logs
echo "$HOME/.local/bin" >> "$GITHUB_PATH" uses: actions/upload-artifact@v2
echo "INSTALL_NAME=lite-xl-${GITHUB_REF##*/}-macos-universal" >> "$GITHUB_ENV" if: failure()
- name: Setup Python with:
uses: actions/setup-python@v4 name: Error Logs
with: path: ${{ env.INSTALL_NAME }}.tar.gz
python-version: '3.9'
- name: Install dmgbuild
run: pip install dmgbuild
- uses: actions/checkout@v3
- name: Download artifacts
uses: actions/download-artifact@v3
id: download
with:
name: macOS DMG Images
path: dmgs-original
- name: Make universal bundles
run: |
bash --version
bash scripts/make-universal-binaries.sh ${{ steps.download.outputs.download-path }} "${INSTALL_NAME}"
- name: Upload DMG Image
uses: actions/upload-artifact@v3
with:
name: macOS Universal DMG Images
path: ${{ env.INSTALL_NAME }}.dmg
build_windows_msys2: build_windows_msys2:
name: Windows name: Windows
runs-on: windows-2019 runs-on: windows-2019
strategy: strategy:
matrix: matrix:
config: msystem: [MINGW32, MINGW64]
- {msystem: MINGW32, arch: i686}
- {msystem: MINGW64, arch: x86_64}
defaults: defaults:
run: run:
shell: msys2 {0} shell: msys2 {0}
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v2
- uses: msys2/setup-msys2@v2 - uses: msys2/setup-msys2@v2
with: with:
msystem: ${{ matrix.config.msystem }} #msystem: MINGW64
msystem: ${{ matrix.msystem }}
update: true
install: >- install: >-
base-devel base-devel
git git
zip zip
mingw-w64-${{ matrix.config.arch }}-gcc
mingw-w64-${{ matrix.config.arch }}-meson
mingw-w64-${{ matrix.config.arch }}-ninja
mingw-w64-${{ matrix.config.arch }}-ca-certificates
mingw-w64-${{ matrix.config.arch }}-ntldd
- name: Set Environment Variables - name: Set Environment Variables
run: | run: |
echo "$HOME/.local/bin" >> "$GITHUB_PATH" echo "$HOME/.local/bin" >> "$GITHUB_PATH"
echo "INSTALL_NAME=lite-xl-${GITHUB_REF##*/}-windows-$(uname -m)" >> "$GITHUB_ENV"
echo "INSTALL_REF=${GITHUB_REF##*/}" >> "$GITHUB_ENV" echo "INSTALL_REF=${GITHUB_REF##*/}" >> "$GITHUB_ENV"
if [[ "${MSYSTEM}" == "MINGW64" ]]; then
echo "INSTALL_NAME=lite-xl-${GITHUB_REF##*/}-windows-x86_64" >> "$GITHUB_ENV"
else
echo "INSTALL_NAME=lite-xl-${GITHUB_REF##*/}-windows-i686" >> "$GITHUB_ENV"
fi
- name: Install Dependencies - name: Install Dependencies
if: false if: ${{ !startsWith(github.ref, 'refs/tags/') }}
run: bash scripts/install-dependencies.sh --debug 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 - name: Build
run: | run: |
bash --version bash --version
bash scripts/build.sh -U --debug --forcefallback 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 - name: Package
run: bash scripts/package.sh --version ${INSTALL_REF} --debug --binary 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 - name: Upload Artifacts
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v2
with: with:
name: Windows Artifacts name: Windows Artifacts
path: ${{ env.INSTALL_NAME }}.zip 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
build_windows_msvc: deploy:
name: Windows (MSVC) name: Deployment
runs-on: windows-2019 runs-on: ubuntu-18.04
strategy: # if: startsWith(github.ref, 'refs/tags/')
matrix: if: false
arch: needs:
- { target: x86, name: i686 } - archive_source_code
- { target: x64, name: x86_64 } - build_linux
- build_macos
- build_windows_msys2
steps: steps:
- uses: actions/checkout@v3 - name: Set Environment Variables
- uses: ilammy/msvc-dev-cmd@v1 run: echo "INSTALL_REF=${GITHUB_REF##*/}" >> "$GITHUB_ENV"
with: - uses: actions/download-artifact@v2
arch: ${{ matrix.arch.target }} with:
- uses: actions/setup-python@v4 name: Linux Artifacts
with: - uses: actions/download-artifact@v2
python-version: '3.x' with:
- name: Install meson and ninja name: macOS DMG Image
run: pip install meson ninja - uses: actions/download-artifact@v2
- name: Set up environment variables with:
run: | name: Source Code Tarball
"INSTALL_NAME=lite-xl-$($env:GITHUB_REF -replace ".*/")-windows-msvc-${{ matrix.arch.name }}" >> $env:GITHUB_ENV - uses: actions/download-artifact@v2
"INSTALL_REF=$($env:GITHUB_REF -replace ".*/")" >> $env:GITHUB_ENV with:
"LUA_SUBPROJECT_PATH=subprojects/$(awk -F ' *= *' '/directory/ { printf $2 }' subprojects/lua.wrap)" >> $env:GITHUB_ENV name: Windows Artifacts
- name: Download and patch subprojects - name: Display File Information
shell: bash shell: bash
run: | run: ls -lR
meson subprojects download # Note: not using `actions/create-release@v1`
cat resources/windows/001-lua-unicode.diff | patch -Np1 -d "$LUA_SUBPROJECT_PATH" # because it cannot update an existing release
- name: Configure # see https://github.com/actions/create-release/issues/29
run: | - uses: softprops/action-gh-release@v1
meson setup --wrap-mode=forcefallback build env:
- name: Build GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: | with:
meson install -C build --destdir="../lite-xl" tag_name: ${{ env.INSTALL_REF }}
- name: Package name: Release ${{ env.INSTALL_REF }}
run: | draft: false
Remove-Item -Recurse -Force -Path "lite-xl/lib","lite-xl/include" prerelease: false
Compress-Archive -Path lite-xl -DestinationPath "$env:INSTALL_NAME.zip" files: |
- name: Upload Artifacts lite-xl-${{ env.INSTALL_REF }}-*
uses: actions/upload-artifact@v3 LiteXL*.AppImage
with: LiteXL*.exe
name: Windows Artifacts (MSVC)
path: ${{ env.INSTALL_NAME }}.zip

View File

@ -1,267 +0,0 @@
name: Release
on:
push:
tags:
- v[0-9]+.*
workflow_dispatch:
inputs:
version:
description: Release Version
default: v2.1.4
required: true
jobs:
release:
name: Create Release
runs-on: ubuntu-latest
permissions:
contents: write
outputs:
upload_url: ${{ steps.create_release.outputs.upload_url }}
version: ${{ steps.tag.outputs.version }}
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Fetch Version
id: tag
run: |
if [[ "${{ github.event.inputs.version }}" != "" ]]; then
echo ::set-output name=version::${{ github.event.inputs.version }}
else
echo ::set-output name=version::${GITHUB_REF/refs\/tags\//}
fi
- name: Update Tag
uses: richardsimko/update-tag@v1
with:
tag_name: ${{ steps.tag.outputs.version }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create Release
id: create_release
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ steps.tag.outputs.version }}
name: Lite XL ${{ steps.tag.outputs.version }}
draft: true
body_path: changelog.md
generate_release_notes: true
build_linux:
name: Linux
needs: release
runs-on: ubuntu-latest
container: ghcr.io/lite-xl/lite-xl-build-box:latest
env:
CC: gcc
CXX: g++
steps:
- name: Set Environment Variables
run: |
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
echo "INSTALL_REF=${{ needs.release.outputs.version }}" >> "$GITHUB_ENV"
- uses: actions/checkout@v3
# disabled because this will break our own Python install
- name: Python Setup
if: false
uses: actions/setup-python@v4
with:
python-version: 3.9
# disabled because the container has up-to-date packages
- name: Update Packages
if: false
run: sudo apt-get update
# disabled as the dependencies are already installed
- name: Install Dependencies
if: false
run: |
bash scripts/install-dependencies.sh --debug
sudo apt-get install -y ccache
- name: Build Portable
run: |
bash --version
bash scripts/build.sh --debug --forcefallback --portable --release
- name: Package Portables
run: |
bash scripts/package.sh --version ${INSTALL_REF} --debug --binary --release
bash scripts/package.sh --version ${INSTALL_REF} --debug --addons --binary --release
- name: Build AppImages
run: |
bash scripts/appimage.sh --debug --static --version ${INSTALL_REF} --release
bash scripts/appimage.sh --debug --nobuild --addons --version ${INSTALL_REF}
- name: Upload Files
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ needs.release.outputs.version }}
draft: true
files: |
lite-xl-${{ env.INSTALL_REF }}-linux-x86_64-portable.tar.gz
lite-xl-${{ env.INSTALL_REF }}-addons-linux-x86_64-portable.tar.gz
LiteXL-${{ env.INSTALL_REF }}-x86_64.AppImage
LiteXL-${{ env.INSTALL_REF }}-addons-x86_64.AppImage
build_macos:
name: macOS
needs: release
runs-on: macos-11
strategy:
matrix:
arch: [x86_64, arm64]
env:
CC: clang
CXX: clang++
steps:
- name: System Information
run: |
system_profiler SPSoftwareDataType
bash --version
gcc -v
xcodebuild -version
- name: Set Environment Variables
run: |
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
echo "INSTALL_REF=${{ needs.release.outputs.version }}" >> "$GITHUB_ENV"
echo "INSTALL_NAME=lite-xl-${{ needs.release.outputs.version }}-macos-${{ matrix.arch }}" >> "$GITHUB_ENV"
echo "INSTALL_NAME_ADDONS=lite-xl-${{ needs.release.outputs.version }}-addons-macos-${{ matrix.arch }}" >> "$GITHUB_ENV"
if [[ $(uname -m) != ${{ matrix.arch }} ]]; then echo "ARCH=--cross-arch ${{ matrix.arch }}" >> "$GITHUB_ENV"; fi
- uses: actions/checkout@v3
- name: Python Setup
uses: actions/setup-python@v4
with:
python-version: 3.9
- name: Install Dependencies
run: bash scripts/install-dependencies.sh --debug
- name: Build
run: |
bash --version
bash scripts/build.sh --bundle --debug --forcefallback --release $ARCH
- name: Create DMG Image
run: |
bash scripts/package.sh --version ${INSTALL_REF} $ARCH --debug --dmg --release
bash scripts/package.sh --version ${INSTALL_REF} $ARCH --debug --addons --dmg --release
- name: Upload Artifacts
uses: actions/upload-artifact@v3
with:
name: macOS DMG Images
path: |
${{ env.INSTALL_NAME }}.dmg
${{ env.INSTALL_NAME_ADDONS }}.dmg
- name: Upload Files
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ needs.release.outputs.version }}
draft: true
files: |
${{ env.INSTALL_NAME }}.dmg
${{ env.INSTALL_NAME_ADDONS }}.dmg
build_macos_universal:
name: macOS (Universal)
needs: [release, build_macos]
runs-on: macos-11
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_BASE=lite-xl-${{ needs.release.outputs.version }}-macos" >> "$GITHUB_ENV"
echo "INSTALL_BASE_ADDONS=lite-xl-${{ needs.release.outputs.version }}-addons-macos" >> "$GITHUB_ENV"
- uses: actions/checkout@v2
- name: Download Artifacts
uses: actions/download-artifact@v3
id: download
with:
name: macOS DMG Images
path: dmgs-original
- name: Python Setup
uses: actions/setup-python@v2
with:
python-version: 3.9
- name: Install dmgbuild
run: pip install dmgbuild
- name: Prepare DMG Images
run: |
mkdir -p dmgs-addons dmgs-normal
mv -v "${{ steps.download.outputs.download-path }}/$INSTALL_BASE-"{x86_64,arm64}.dmg dmgs-normal
mv -v "${{ steps.download.outputs.download-path }}/$INSTALL_BASE_ADDONS-"{x86_64,arm64}.dmg dmgs-addons
- name: Create Universal DMGs
run: |
bash --version
bash scripts/make-universal-binaries.sh dmgs-normal "$INSTALL_BASE-universal"
bash scripts/make-universal-binaries.sh dmgs-addons "$INSTALL_BASE_ADDONS-universal"
- name: Upload Files
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ needs.release.outputs.version }}
draft: true
files: |
${{ env.INSTALL_BASE }}-universal.dmg
${{ env.INSTALL_BASE_ADDONS }}-universal.dmg
build_windows_msys2:
name: Windows
needs: release
runs-on: windows-2019
strategy:
matrix:
msystem: [MINGW32, MINGW64]
defaults:
run:
shell: msys2 {0}
steps:
- uses: actions/checkout@v3
- uses: msys2/setup-msys2@v2
with:
msystem: ${{ matrix.msystem }}
update: true
install: >-
base-devel
git
zip
- name: Set Environment Variables
run: |
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
echo "INSTALL_REF=${{ needs.release.outputs.version }}" >> "$GITHUB_ENV"
if [[ "${MSYSTEM}" == "MINGW64" ]]; then
echo "BUILD_ARCH=x86_64" >> "$GITHUB_ENV"
echo "INSTALL_NAME=lite-xl-${{ needs.release.outputs.version }}-windows-x86_64" >> "$GITHUB_ENV"
echo "INSTALL_NAME_ADDONS=lite-xl-${{ needs.release.outputs.version }}-addons-windows-x86_64" >> "$GITHUB_ENV"
else
echo "BUILD_ARCH=i686" >> "$GITHUB_ENV"
echo "INSTALL_NAME=lite-xl-${{ needs.release.outputs.version }}-windows-i686" >> "$GITHUB_ENV"
echo "INSTALL_NAME_ADDONS=lite-xl-${{ needs.release.outputs.version }}-addons-windows-i686" >> "$GITHUB_ENV"
fi
- name: Install Dependencies
run: bash scripts/install-dependencies.sh --debug
- name: Build
run: |
bash --version
bash scripts/build.sh -U --debug --forcefallback --release
- name: Package
run: bash scripts/package.sh --version ${INSTALL_REF} --debug --binary --release
- name: Build Installer
run: bash scripts/innosetup/innosetup.sh --debug --version ${INSTALL_REF}
- name: Package With Addons
run: bash scripts/package.sh --version ${INSTALL_REF} --debug --addons --binary --release
- name: Build Installer With Addons
run: bash scripts/innosetup/innosetup.sh --debug --version ${INSTALL_REF} --addons
- name: Upload Files
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ needs.release.outputs.version }}
draft: true
files: |
${{ env.INSTALL_NAME }}.zip
${{ env.INSTALL_NAME_ADDONS }}.zip
LiteXL-${{ env.INSTALL_REF }}-${{ env.BUILD_ARCH }}-setup.exe
LiteXL-${{ env.INSTALL_REF }}-addons-${{ env.BUILD_ARCH }}-setup.exe

16
.gitignore vendored
View File

@ -2,10 +2,10 @@ build*/
.build*/ .build*/
lhelper/ lhelper/
submodules/ submodules/
subprojects/*/ subprojects/lua/
subprojects/libagg/
subprojects/reproc/
/appimage* /appimage*
.vscode
.cache
.ccls-cache .ccls-cache
.lite-debug.log .lite-debug.log
.run* .run*
@ -19,13 +19,3 @@ compile_commands.json
error.txt error.txt
lite-xl* lite-xl*
LiteXL* LiteXL*
lite
.config/
*.lha
*.o
*.snalyzerinfo
!resources/windows/*.diff
!resources/windows/*.exe.manifest.in
!resources/macos/*.py

View File

@ -1,6 +1,4 @@
Copyright (c) 2020 rxi Copyright (c) 2020-2021 Francesco Abbate
Copyright (c) 2020-2022 Francesco Abbate
Copyright (c) 2022-present Lite XL Team
Permission is hereby granted, free of charge, to any person obtaining a copy of 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 this software and associated documentation files (the "Software"), to deal in

View File

@ -1,89 +0,0 @@
#
# Project: Lite XL
#
# Created on: 26-12-2021
#
LiteXL_OBJ := \
src/main.o src/rencache.o src/renderer.o src/renwindow.o \
src/api/api.o src/api/dirmonitor.o \
src/api/regex.o src/api/renderer.o src/api/system.o \
src/api/utf8.o src/platform/morphos.o \
src/api/dirmonitor/mos.o src/platform/codesets.o
outfile := lite-xl
compiler := ppc-morphos-gcc-11
cxxcompiler := ppc-morphos-g++-11
INCPATH := -Isrc -Ilib/dmon -I/sdk/gg/usr/local/include/SDL2 \
-I/sdk/gg/usr/include/freetype -I/sdk/gg/usr/include/lua5.4
DFLAGS ?= -D__USE_INLINE__
CFLAGS ?= -Wall -Wwrite-strings -O2 -noixemul -g -std=gnu11 -fno-strict-aliasing
LFLAGS ?= -noixemul -lpcre2-8 -lSDL2 -llua54 -lagg -lfreetype -lm -lc -L/usr/local/lib
ifeq ($(DEBUG),1)
CFLAGS += -g -gstabs
LFLAGS += -gstabs
endif
.PHONY: LiteXL clean release
default: LiteXL
clean:
@echo "Cleaning compiler objects..."
@rm -f $(LiteXL_OBJ)
LiteXL: $(LiteXL_OBJ)
@echo "Linking LiteXL"
$(compiler) -o $(outfile) $(LiteXL_OBJ) $(LFLAGS)
.c.o:
@echo "Compiling $<"
$(compiler) -c $< -o $*.o $(CFLAGS) $(INCPATH) $(DFLAGS)
src/main.o: src/main.c src/api/api.h src/rencache.h \
src/renderer.h src/platform/morphos.h src/platform/codesets.h
src/rencache.o: src/rencache.c
src/renderer.o: src/renderer.c
src/renwindow.o: src/renwindow.c
src/api/api.o: src/api/api.c
src/api/regex.o: src/api/regex.c
src/api/renderer.o: src/api/renderer.c
src/api/system.o: src/api/system.c src/platform/morphos.h
src/platform/morphos.o: src/platform/morphos.c
src/api/dirmonitor.o: src/api/dirmonitor.c src/api/dirmonitor/mos.c
src/api/utf8.o: src/api/utf8.c
src/api/dirmonitor/mos.o: src/api/dirmonitor/mos.c
src/platform/codesets.o: src/platform/codesets.c
release: clean LiteXL
@echo "Creating release files..."
@mkdir -p release/LiteXL2
@cp -r resources/amiga/* release/LiteXL2/
@mv release/LiteXL2/LiteXL2.info release/
@rm release/LiteXL2/AutoInstall
@cp -r data release/LiteXL2/
@cp changelog.md release/LiteXL2/
@cp $(outfile) release/LiteXL2/
@strip release/LiteXL2/$(outfile)
@cp README.md release/LiteXL2/
@cp README_Amiga.md release/LiteXL2/
@cp LICENSE release/LiteXL2/
@cp -r licenses release/LiteXL2/
@echo "Creating release archive..."
@lha -aeqr3 a LiteXL2_MOS.lha release/
@echo "Clean release files..."
@delete release ALL QUIET FORCE

View File

@ -1,102 +0,0 @@
#
# Project: Lite XL
#
# Created on: 26-12-2021
#
LiteXL_OBJ := \
src/main.o src/rencache.o src/renderer.o src/renwindow.o \
src/api/api.o src/api/dirmonitor.o \
src/api/regex.o src/api/renderer.o src/api/system.o \
src/api/utf8.o src/platform/amigaos4.o \
src/api/dirmonitor/os4.o src/platform/codesets.o
outfile := lite-xl
compiler := gcc
cxxcompiler := g++
INCPATH := -Isrc -I/sdk/local/newlib/include/SDL2 \
-I/sdk/local/common/include/lua54 -I/sdk/local/common/include/freetype2
DFLAGS += -D__USE_INLINE__ -DLITE_XL_DATA_USE_EXEDIR
CFLAGS ?= -Werror -Wwrite-strings -O3 -std=gnu11 -fno-strict-aliasing
LFLAGS ?= -mcrt=newlib -lpcre2-8 -lSDL2 -llua54 -lfreetype -lpng -lz \
-lpthread -athread=native
ifeq ($(DEBUG),1)
CFLAGS += -g -gstabs
LFLAGS += -gstabs
ifeq ($(PROFYLER),1)
CFLAGS += -finstrument-functions -fno-inline -DPROFILING
LFLAGS += -lprofyle
endif
endif
.PHONY: LiteXL clean release
default: LiteXL
clean:
@echo "Cleaning compiler objects..."
@rm -f $(LiteXL_OBJ)
LiteXL: $(LiteXL_OBJ)
@echo "Linking LiteXL"
@$(compiler) -o $(outfile) $(LiteXL_OBJ) $(LFLAGS)
.c.o:
@echo "Compiling $<"
@$(compiler) -c $< -o $*.o $(CFLAGS) $(INCPATH) $(DFLAGS)
src/main.o: src/main.c src/api/api.h src/rencache.h \
src/renderer.h src/platform/amigaos4.h src/platform/codesets.h
src/rencache.o: src/rencache.c
src/renderer.o: src/renderer.c
src/renwindow.o: src/renwindow.c
src/api/api.o: src/api/api.c
src/api/regex.o: src/api/regex.c
src/api/renderer.o: src/api/renderer.c
src/api/system.o: src/api/system.c src/platform/amigaos4.h
src/platform/amigaos4.o: src/platform/amigaos4.c
src/platform/codesets.o: src/platform/codesets.c
src/api/dirmonitor.o: src/api/dirmonitor.c src/api/dirmonitor/os4.c
src/api/utf8.o: src/api/utf8.c src/platform/amigaos4.h
src/api/dirmonitor/os4.o: src/api/dirmonitor/os4.c
src/api/process.o: src/api/process.c
release: clean LiteXL
@echo "Creating release files..."
@mkdir -p release/LiteXL2
@cp -r resources/amiga/* release/LiteXL2/
@mv release/LiteXL2/LiteXL2.info release/
@mv release/LiteXL2/AutoInstall release/
@cp -r data release/LiteXL2/
@cp changelog.md release/LiteXL2/
@cp $(outfile) release/LiteXL2/
@strip release/LiteXL2/$(outfile)
@cp README.md release/LiteXL2/
@cp README_Amiga.md release/LiteXL2/
@cp LICENSE release/LiteXL2/
@cp -r licenses release/LiteXL2/
@echo "Creating release archive..."
@lha -aeqr3 a LiteXL2_OS4.lha release/
@echo "Clean release files..."
@delete release ALL QUIET FORCE

111
README.md
View File

@ -18,16 +18,16 @@ 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.
Please note that Lite XL is compatible with lite for most plugins and all color themes. Please note that Lite XL is compatible with lite for most plugins and all color themes.
We provide a separate lite-xl-plugins repository for Lite XL, because in some cases We provide a separate lite-plugins repository for Lite XL, because in some cases
some adaptations may be needed to make them work better with Lite XL. some adaptations may be needed to make them work better with Lite XL.
The repository with modified plugins is https://github.com/lite-xl/lite-xl-plugins. The repository with modified plugins is https://github.com/franko/lite-plugins.
The changes and differences between Lite XL and rxi/lite are listed in the The changes and differences between Lite XL and rxi/lite are listed in the
[changelog]. [changelog].
## Overview ## Overview
Lite XL is derived from [lite]. Lite XL is derived from lite.
It is a lightweight text editor written mostly in Lua — it aims to provide It is a lightweight text editor written mostly in Lua — it aims to provide
something practical, pretty, *small* and fast easy to modify and extend, something practical, pretty, *small* and fast easy to modify and extend,
or to use without doing either. or to use without doing either.
@ -77,94 +77,13 @@ 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 Please note that the package is relocatable to any prefix and the option prefix
affects only the place where the application is actually installed. 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.
### Windows
Lite XL comes with installers on Windows for typical installations.
Alternatively, we provide ZIP archives that you can download and extract anywhere and run directly.
To make Lite XL portable (e.g. running Lite XL from a thumb drive),
simply create a `user` folder where `lite-xl.exe` is located.
Lite XL will load and store all your configurations and plugins in the folder.
### macOS
We provide DMG files for macOS. Simply drag the program into your Applications folder.
> **Important**
> Newer versions of Lite XL are signed with a self-signed certificate,
> so you'll have to follow these steps when running Lite XL for the first time.
>
> 1. Find Lite XL in Finder (do not open it in Launchpad).
> 2. Control-click Lite XL, then choose `Open` from the shortcut menu.
> 3. Click `Open` in the popup menu.
>
> The correct steps may vary between macOS versions, so you should refer to
> the [macOS User Guide](https://support.apple.com/en-my/guide/mac-help/mh40616/mac).
>
> On an older version of Lite XL, you will need to run these commands instead:
>
> ```sh
> # clears attributes from the directory
> xattr -cr /Applications/Lite\ XL.app
> ```
>
> Otherwise, macOS will display a **very misleading error** saying that the application is damaged.
### 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
./lite-xl
```
To install lite-xl copy files over into appropriate directories:
```sh
rm -rf $HOME/.local/share/lite-xl $HOME/.local/bin/lite-xl
mkdir -p $HOME/.local/bin && cp lite-xl $HOME/.local/bin/
mkdir -p $HOME/.local/share/lite-xl && cp -r data/* $HOME/.local/share/lite-xl/
```
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
as a plugin, after which a pull request to the [Lite XL plugins repository] can be made. as a plugin, after which a pull request to the [plugins repository] can be made.
If the plugin uses any Lite XL-specific functionality,
please open a pull request to the [Lite XL plugins repository].
Pull requests to improve or modify the editor itself are welcome. Pull requests to improve or modify the editor itself are welcome.
@ -180,14 +99,14 @@ See the [licenses] file for details on licenses used by the required dependencie
[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.com [website]: https://lite-xl.github.io
[build]: https://lite-xl.com/en/documentation/build [build]: https://lite-xl.github.io/en/documentation/build/
[Get Lite XL]: https://github.com/lite-xl/lite-xl/releases/latest [Get Lite XL]: https://github.com/franko/lite-xl/releases/latest
[Get plugins]: https://github.com/lite-xl/lite-xl-plugins [Get plugins]: https://github.com/franko/lite-plugins
[Get color themes]: https://github.com/lite-xl/lite-xl-colors [Get color themes]: https://github.com/rxi/lite-colors
[changelog]: https://github.com/lite-xl/lite-xl/blob/master/changelog.md [changelog]: https://github.com/franko/lite-xl/blob/master/changelog.md
[Lite XL plugins repository]: https://github.com/lite-xl/lite-xl-plugins [Lite XL plugins repository]: https://github.com/franko/lite-plugins
[plugins repository]: https://github.com/rxi/lite-plugins [plugins repository]: https://github.com/rxi/lite-plugins
[colors repository]: https://github.com/lite-xl/lite-xl-colors [colors repository]: https://github.com/rxi/lite-colors
[LICENSE]: LICENSE [LICENSE]: LICENSE
[licenses]: licenses/licenses.md [licenses]: licenses/licenses.md

View File

@ -1,368 +0,0 @@
# Lite XL v2 for AmigaOS 4.1 FE & MorphOS 3
Lite XL is a lightweight text editor written in Lua and SDL2.
The port is not perfect and it might have issues here and there. It might
crash from time to time, if there is a path problem, but overall it works
pretty well. This is my daily editor for any kind of development.
If it crashes on your system, try to delete to `.config` folder.
## Installation
You can extract the Lite XL archive wherever you want and run the *lite*
editor.
## Configuration folder
This editor creates a `.config` folder where the configuration is saved, as
well as plugins, themes etc.. By default this version uses the installation
folder, but if you want to override it, you can create an ENV variable
named `HOME` and set there your prefferable path.
You can check if there is one already set by executing the following command
in a shell
```
GetEnv HOME
```
If there is one set, then you will see the path at the output.
Otherwise, you can set your home path be executing the following command.
Change the path to the one of your preference.
```
SetEnv SAVE HOME "Sys:home/"
```
## Addons
### Colors
Colors are lua files that set the color scheme of the editor. There are
light and dark themes for you to choose.
To install and use them you have to copy the ones you would like from
`addons/colors/light` or `addons/colors/dark` into the folder
`.config/lite-xl/colors/`. Don't add light or dark folders. Just copy the
.lua files in there.
Then you have to start Lite XL and open your configuration by clicking
at the cog icon at the toolbar (bottom left sixth icon). Go at the line
that looks like below
```
-- core.reload_module("colors.summer")
```
and change the `summer` with the name of your color theme. Also, remove
the two dashes `--` at the start of the line and save the file. If you
did everything right, the color schema should change instantly.
The themes can also be found at
https://github.com/lite-xl/lite-xl-colors
### Plugins
LiteXL is able to use plugins to extend its features. Those can be found
at https://github.com/lite-xl/lite-xl-plugins and other websites. Not all
of them will work fine on AmigaOS 4 or MorphOS, because of missing
dependencies or filesystem issues.
To make it easier for you, I gathered some of the plugins that are working
well, and I included them under `addons/plugins`. For you to install the
ones you would like to use, you have to copy the `.lua` files into the
folder `.config/lite-xl/plugins/` and restart the editor.
Please, choose wisely, because adding all the plugins might make the editor
slower on your system. I would recommend you add only those that you really
need.
The included plugins are the following:
**autoinsert**
Automatically inserts closing brackets and quotes. Also allows selected
text to be wrapped with brackets or quotes.
**autosaveonfocuslost**
Automatically saves files that were changed when the main window loses
focus by switching to another application
**autowrap**
Automatically hardwraps lines when typing
**bigclock**
Shows the current time and date in a view with large text
**bracketmatch**
Underlines matching pair for bracket under the caret
**codesets**
This plugin uses the codesets.library on AmigaOS 4 and the
charsets.library on MorphOS to translate ISO encoded files to unicode
and vice-versa. When this is enabled new menu items are added to
load/save the code with a different encoding. Also there is a new
section at the status bar that show the file encoding.
This plugin is **EXPERIMENTAL** and heavily inspired from the encoding
plugin at https://github.com/jgmdev/lite-xl-encoding
**colorpreview**
Underlays color values (eg. `#ff00ff` or `rgb(255, 0, 255)`) with their
resultant color.
**custom_caret**
Customize the caret in the editor setting it to *underline*, *block* or
*line* at the init.lua file in your config folder.
For example add:
`config.plugins.custom_caret.shape = "block"`
**EditorConfig**
EditorConfig (https://editorconfig.org/) implementation for Lite XL
**ephemeral_tabs**
Preview tabs. Opening a doc will replace the contents of the preview tab.
Marks tabs as non-preview on any change or tab double clicking.
**ghmarkdown**
Opens a preview of the current markdown file in a browser window.
On AmigaOS 4 it uses *urlopen* and on MorphOS it uses *openurl* to load
the generated html in the browser. It requires a GitHub application token
because it uses its Rest API. Add it at the init.lua file in your config
folder like below:
`config.plugins.ghmarkdown.github_token = "<token here>"`
**indentguide**
Adds indent guides
**language_guide**
Syntax for the AmigaGuide scripting language
**language_hws**
Syntax for the Hollywood language
**language_make**
Syntax for the Make build system language
**language_sh**
Syntax for shell scripting language
**lfautoinsert**
Automatically inserts indentation and closing bracket/text after newline
**markers**
Add markers to docs and jump between them quickly
**minimap**
Shows a minimap on the right-hand side of the docview. Please note that
this plugin will make the editor slower on file loading and scrolling.
**navigate**
Allows moving back and forward between document positions, reducing the
amount of scrolling
**nonicons**
File icons set for TreeView. Download TTF font to your config/fonts
folder from https://github.com/yamatsum/nonicons/tree/master/dist
**opacity**
Change the opaqueness/transparency of lite-xl using LAmiga+mousewheel
or a command.
**openfilelocation**
Opens the parent directory of the current file in the file manager
**rainbowparen**
Show nesting of parentheses with rainbow colours
**restoretabs**
Keep a list of recently closed tabs, and restore the tab in order on
cntrl+shift+t.
**select_colorscheme**
Select a color theme, like VScode, Sublime Text.
(plugin saves changes)
**selectionhighlight**
Highlights regions of code that match the current selection
**smallclock**
It adds a small clock at the bottom right corner.
**tetris**
Play Tetris inside Lite XL.
## Tips and tricks
### Transitions
If you want to disable the transitions and make the editor faster,
open your configuration file by clicking at the cog icon at the toolbar
(bottom left, 6th icon) and add the following line at the end of the file,
and then save it. You might need to restart your editor.
```
config.transitions = false
```
### Hide files from the file list
If you would like to hide files or whole folder from the left side bar list,
open your configuration by clicking at the cog icon at the toolbar
(bottom left sixth icon) and add the followline at the end of the file and
save it. This hides all the files that start with a dot, and all the `.info`
files. You might need to restart your editor.
```
config.ignore_files = {"^%.", "%.info$"}
```
You can add as many rules as you want in there, to hide files or
folders, as you like.
## I would like to thank
- IconDesigner for the proper glow icons that are included in the release
- Capehill for his tireless work on SDL port for AmigaOS 4.1 FE
- Michael Trebilcock for his port on liblua
- Bruno "BeWorld" Peloille for his great work on porting SDL to MorphOS
and for his valuable help
- Lite XL original team for being helpful and providing info
Without all the above Lite XL would not be possible
## Support
If you enjoy what I am doing and would like to keep me up during the night,
please consider to buy me a coffee at:
https://ko-fi.com/walkero
## Known issues
You can find the known issues at
https://git.walkero.gr/walkero/lite-xl/issues
# Changelog
## [2.1.4r1] - 2024-05-23
### Added
- Added the ability to open files and folders by drag 'n drop them on the
LiteXL icon when this is on the AmiDock (AmigaOS4) / Panel (MorphOS)
### Updated
- Updated the code to the upstream 2.1.4 release
### Fixed
- Fix opening files from the root of a device
## [2.1.3r1] - 2024-03-09
### Added
- Added AmiUpdate support
- Added the Tetris plugin
### Updated
- Updated the code to the upstream 2.1.3 release
- Compiled with SDL 2.30.0 that supports editing with ISO encodings, other
than English. Now the editor should be able to support any language
and in conjuction with the codesets plugin be able to make text
encodings conversions
- Now the codesets plugin supports MorphOS 3.18 and above
### Changed
- Changed the way the "Open in system" option executes the WBRun command
in AmigaOS 4, with a more secure way
- Did a lot of code cleanup in sync with the upstream, and parts of code
that were left over
- Compiled with pcre2 10.42 (MorphOS version only)
### Fixed
- I did a lot of changes on path manipulation and usage, fixing scanning
the root of a partition or an assign path
- Fixed an error with the codesets plugin, where an empty file could
not be opened
- Improved the folder suggestions when opening projects or changing paths.
Now even the root folders of a partition are presented
- Fixed ghmarkdown plugin, but now requires a GitHub token to be provided
## [2.1.2r1] - 2023-12-19
### Added
- Added the new experimental codesets plugin (AmigaOS4 version only).
MorphOS version is in WIP
### Changed
- Synced with the latest upstream v2.1.2 code
- Compiled with gcc 11.3.0
- Compiled with SDL 2.28.4
- Compiled with libfreetype 2.13.x
- Compiled with lua 5.4.6
- Compiled with linpng 1.6.40 (AmigaOS4 version only)
- Compiled with libz 1.2.13 (AmigaOS4 version only)
## [2.1.1r2] - 2022-05-14
### Changed
- Compiled with latest SDL v2.26.5-rc2
## [2.1.1r1] - 2022-01-29
### Changed
- Binary name changed to lite-xl
- Updated the colour themes and the plugins that are included in the release
- Compiled with latest SDL 2.26
- Compiled with gcc 11
- Synced the code with the upstream master branch at 8th January 2023
### Fixed
- Set the default locale on AmigaOS 4, so as to fix some issues with decimal
numbers
## [2.1.0r1] - 2022-10-10
### Added
- This version of LiteXL recognises changes that are done outside the editor
in files and folders, and updates the items when it gets focus again.
### Changed
- Synced the code with the latest upstream master branch, which means that
this version is based on the latest available source
- Now the plugins need to be version 3. The older versions will not work.
All the included plugins are updated to the latest available version.
- Compiled with SDL 2.24
- Compiled with Lua 5.4
### Fixed
- Fixed regex issues with some plugins
- Fixed "Open in System" on AmigaOS 4 and MorphOS. When you right click
at a file or a folder at the treeview at the left side, then depending
the type of the item opens on Workbench. A folder opens in a list view
and a file opens with its default tool
- Fixed markdown preview on MorphOS. Now, it calls openurl with the html
file (#20)
- Fixed locale issues on MorphOS (again), since the previous fix didn't
actually fixed the problem
## [2.0.3r3] - 2022-09-26
### Added
- Added plugin for AmigaGuide files
- Added plugin for Hollywood files
### Fixed
- Fixed non existing path crashes on OS4 and MorphOS
- Fixed editor refresh whenever init.lua is changed, no matter the working
folder
- Fixed an issue when the user added a directory in the project that
already existed
- Fixed locale issue on start for MorphOS. Now it should start just fine
no matter what locale the user has on his system.
- Fixed "Find" on MorphOS that was not working (shortcut CTRL+F)
- If the user selects to change the project folder and inserts Sys: or any
partition name, the included folders will be listed as suggestions
### Changed
- Removed linking with unix on OS4 build
- Makefiles updated
## [2.0.3r2] - 2022-06-18
### Added
- First public MorphOS version released
### Changed
- Merged source code for both AmigaOS 4 and MorphOS
- Moved the declaration of the $VER and $STACK for the AmigaOS 4 version,
so to happen only once (reported by capehill)
### Fixed
- Fixed the usage of NumPad (reported by root)
## [2.0.3r1] - 2022-03-30
### Changed
- Applied all the necessary changes to make it run under AmigaOS 4.1 FE
- Fixes and changes
# Disclaimer
YOU MAY USE IT AT YOUR OWN RISK!
I will not be held responsible for any data loss or problems you might get
by using this software.

Binary file not shown.

View File

@ -13,36 +13,32 @@ show_help() {
echo echo
echo "Common options:" echo "Common options:"
echo echo
echo "-h --help Show this help and exit." echo "-h --help Show this help and exit."
echo "-b --builddir DIRNAME Set the name of the build directory (not path)." echo "-b --builddir DIRNAME Set the name of the build directory (not path)."
echo " Default: '$(get_default_build_dir)'." echo " Default: '$(get_default_build_dir)'."
echo "-p --prefix PREFIX Install directory prefix." echo "-p --prefix PREFIX Install directory prefix."
echo " Default: '/'." echo " Default: '/'."
echo " --cross-platform PLATFORM The platform to cross compile for." echo " --debug Debug this script."
echo " --cross-arch ARCH The architecture to cross compile for."
echo " --debug Debug this script."
echo echo
echo "Build options:" echo "Build options:"
echo echo
echo "-f --forcefallback Force to build subprojects dependencies statically." echo "-f --forcefallback Force to build subprojects dependencies statically."
echo "-B --bundle Create an App bundle (macOS only)" echo "-B --bundle Create an App bundle (macOS only)"
echo "-P --portable Create a portable package." echo "-P --portable Create a portable package."
echo "-O --pgo Use profile guided optimizations (pgo)." echo "-O --pgo Use profile guided optimizations (pgo)."
echo " Requires running the application iteractively." echo " Requires running the application iteractively."
echo " --cross-file CROSS_FILE The cross file used for compiling."
echo echo
echo "Package options:" echo "Package options:"
echo echo
echo "-d --destdir DIRNAME Set the name of the package directory (not path)." echo "-d --destdir DIRNAME Set the name of the package directory (not path)."
echo " Default: 'lite-xl'." echo " Default: 'lite-xl'."
echo "-v --version VERSION Sets the version on the package name." echo "-v --version VERSION Sets the version on the package name."
echo "-A --appimage Create an AppImage (Linux only)." echo "-A --appimage Create an AppImage (Linux only)."
echo "-D --dmg Create a DMG disk image (macOS only)." echo "-D --dmg Create a DMG disk image (macOS only)."
echo " Requires dmgbuild." echo " Requires NPM and AppDMG."
echo "-I --innosetup Create an InnoSetup installer (Windows only)." echo "-I --innosetup Create an InnoSetup installer (Windows only)."
echo "-r --release Compile in release mode." echo "-S --source Create a source code package,"
echo "-S --source Create a source code package," echo " including subprojects dependencies."
echo " including subprojects dependencies."
echo echo
} }
@ -62,13 +58,6 @@ main() {
local innosetup local innosetup
local portable local portable
local pgo local pgo
local release
local cross_platform
local cross_platform_option=()
local cross_arch
local cross_arch_option=()
local cross_file
local cross_file_option=()
for i in "$@"; do for i in "$@"; do
case $i in case $i in
@ -120,10 +109,6 @@ main() {
portable="--portable" portable="--portable"
shift shift
;; ;;
-r|--release)
release="--release"
shift
;;
-S|--source) -S|--source)
source="--source" source="--source"
shift shift
@ -132,21 +117,6 @@ main() {
pgo="--pgo" pgo="--pgo"
shift shift
;; ;;
--cross-platform)
cross_platform="$2"
shift
shift
;;
--cross-arch)
cross_arch="$2"
shift
shift
;;
--cross-file)
cross_file="$2"
shift
shift
;;
--debug) --debug)
debug="--debug" debug="--debug"
set -x set -x
@ -167,23 +137,14 @@ main() {
if [[ -n $dest_dir ]]; then dest_dir_option=("--destdir" "${dest_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 $prefix ]]; then prefix_option=("--prefix" "${prefix}"); fi
if [[ -n $version ]]; then version_option=("--version" "${version}"); fi if [[ -n $version ]]; then version_option=("--version" "${version}"); fi
if [[ -n $cross_platform ]]; then cross_platform_option=("--cross-platform" "${cross_platform}"); fi
if [[ -n $cross_arch ]]; then cross_arch_option=("--cross-arch" "${cross_arch}"); fi
if [[ -n $cross_file ]]; then cross_file_option=("--cross-file" "${cross_file}"); fi
source scripts/build.sh \ source scripts/build.sh \
${build_dir_option[@]} \ ${build_dir_option[@]} \
${prefix_option[@]} \ ${prefix_option[@]} \
${cross_platform_option[@]} \
${cross_arch_option[@]} \
${cross_file_option[@]} \
$debug \ $debug \
$force_fallback \ $force_fallback \
$bundle \ $bundle \
$portable \ $portable \
$release \
$pgo $pgo
source scripts/package.sh \ source scripts/package.sh \
@ -191,15 +152,12 @@ main() {
${dest_dir_option[@]} \ ${dest_dir_option[@]} \
${prefix_option[@]} \ ${prefix_option[@]} \
${version_option[@]} \ ${version_option[@]} \
${cross_platform_option[@]} \
${cross_arch_option[@]} \
--binary \ --binary \
--addons \ --addons \
$debug \ $debug \
$appimage \ $appimage \
$dmg \ $dmg \
$innosetup \ $innosetup \
$release \
$source $source
} }

View File

@ -1,8 +0,0 @@
CC="${CC:-gcc}"
CXX="${CXX:-g++}"
CFLAGS=
CXXFLAGS=
LDFLAGS=
BUILD_TYPE=Release
packages=(pcre2 freetype2 sdl2 lua)

File diff suppressed because it is too large Load Diff

View File

@ -1,46 +0,0 @@
local style = require "core.style"
local common = require "core.common"
style.background = { common.color "#2e2e32" } -- Docview
style.background2 = { common.color "#252529" } -- Treeview
style.background3 = { common.color "#252529" } -- Command view
style.text = { common.color "#97979c" }
style.caret = { common.color "#93DDFA" }
style.accent = { common.color "#e1e1e6" }
-- style.dim - text color for nonactive tabs, tabs divider, prefix in log and
-- search result, hotkeys for context menu and command view
style.dim = { common.color "#525257" }
style.divider = { common.color "#202024" } -- Line between nodes
style.selection = { common.color "#48484f" }
style.line_number = { common.color "#525259" }
style.line_number2 = { common.color "#83838f" } -- With cursor
style.line_highlight = { common.color "#343438" }
style.scrollbar = { common.color "#414146" }
style.scrollbar2 = { common.color "#4b4b52" } -- Hovered
style.scrollbar_track = { common.color "#252529" }
style.nagbar = { common.color "#FF0000" }
style.nagbar_text = { common.color "#FFFFFF" }
style.nagbar_dim = { common.color "rgba(0, 0, 0, 0.45)" }
style.drag_overlay = { common.color "rgba(255,255,255,0.1)" }
style.drag_overlay_tab = { common.color "#93DDFA" }
style.good = { common.color "#72b886" }
style.warn = { common.color "#FFA94D" }
style.error = { common.color "#FF3333" }
style.modified = { common.color "#1c7c9c" }
style.syntax["normal"] = { common.color "#e1e1e6" }
style.syntax["symbol"] = { common.color "#e1e1e6" }
style.syntax["comment"] = { common.color "#676b6f" }
style.syntax["keyword"] = { common.color "#E58AC9" } -- local function end if case
style.syntax["keyword2"] = { common.color "#F77483" } -- self int float
style.syntax["number"] = { common.color "#FFA94D" }
style.syntax["literal"] = { common.color "#FFA94D" } -- true false nil
style.syntax["string"] = { common.color "#f7c95c" }
style.syntax["operator"] = { common.color "#93DDFA" } -- = + - / < >
style.syntax["function"] = { common.color "#93DDFA" }
style.log["INFO"] = { icon = "i", color = style.text }
style.log["WARN"] = { icon = "!", color = style.warn }
style.log["ERROR"] = { icon = "!", color = style.error }
return style

View File

@ -1,36 +0,0 @@
local bit = {}
local LUA_NBITS = 32
local ALLONES = (~(((~0) << (LUA_NBITS - 1)) << 1))
local function trim(x)
return (x & ALLONES)
end
local function mask(n)
return (~((ALLONES << 1) << ((n) - 1)))
end
local function check_args(field, width)
assert(field >= 0, "field cannot be negative")
assert(width > 0, "width must be positive")
assert(field + width < LUA_NBITS and field + width >= 0,
"trying to access non-existent bits")
end
function bit.extract(n, field, width)
local w = width or 1
check_args(field, w)
local m = trim(n)
return m >> field & mask(w)
end
function bit.replace(n, v, field, width)
local w = width or 1
check_args(field, w)
local m = trim(n)
local x = v & mask(width);
return m & ~(mask(w) << field) | (x << field)
end
return bit

View File

@ -1,93 +1,22 @@
local core = require "core" local core = require "core"
local command = {} local command = {}
---A predicate function accepts arguments from `command.perform()` and evaluates to a boolean. </br>
---If the function returns true, then the function associated with the command is executed.
---
---The predicate function can also return other values after the boolean, which will
---be passed into the function associated with the command.
---@alias core.command.predicate_function fun(...: any): boolean, ...
---A predicate is a string, an Object or a function, that is used to determine
---whether a command should be executed.
---
---If the predicate is a string, it is resolved into an `Object` via `require()`
---and checked against the active view with `Object:extends()`. </br>
---For example, `"core.docview"` will match any view that inherits from `DocView`. </br>
---A `!` can be appended to the predicate to strictly match the current view via `Object:is()`,
---instead of matching any view that inherits the predicate.
---
---If the predicate is a table, it is checked against the active view with `Object:extends()`.
---Strict matching via `Object:is()` is not available.
---
---If the predicate is a function, it must behave like a predicate function.
---@see core.command.predicate_function
---@alias core.command.predicate string|core.object|core.command.predicate_function
---A command is identified by a command name.
---The command name contains a category and the name itself, separated by a colon (':').
---
---All commands should be in lowercase and should not contain whitespaces; instead
---they should be replaced by a dash ('-').
---@alias core.command.command_name string
---The predicate and its associated function.
---@class core.command.command
---@field predicate core.command.predicate_function
---@field perform fun(...: any)
---@type { [string]: core.command.command }
command.map = {} command.map = {}
---@type core.command.predicate_function
local always_true = function() return true end local always_true = function() return true end
---This function takes in a predicate and produces a predicate function function command.add(predicate, map)
---that is internally used to dispatch and execute commands.
---
---This function should not be called manually.
---@see core.command.predicate
---@param predicate core.command.predicate|nil If nil, the predicate always evaluates to true.
---@return core.command.predicate_function
function command.generate_predicate(predicate)
predicate = predicate or always_true predicate = predicate or always_true
local strict = false
if type(predicate) == "string" then if type(predicate) == "string" then
if predicate:match("!$") then
strict = true
predicate = predicate:gsub("!$", "")
end
predicate = require(predicate) predicate = require(predicate)
end end
if type(predicate) == "table" then if type(predicate) == "table" then
local class = predicate local class = predicate
if not strict then predicate = function() return core.active_view:is(class) end
predicate = function(...) return core.active_view:extends(class), core.active_view, ... end
else
predicate = function(...) return core.active_view:is(class), core.active_view, ... end
end
end end
---@cast predicate core.command.predicate_function
return predicate
end
---Adds commands to the map.
---
---The function accepts a table containing a list of commands
---and their functions. </br>
---If a command already exists, it will be replaced.
---@see core.command.predicate
---@see core.command.command_name
---@param predicate core.command.predicate
---@param map { [core.command.command_name]: fun(...: any) }
function command.add(predicate, map)
predicate = command.generate_predicate(predicate)
for name, fn in pairs(map) do for name, fn in pairs(map) do
if command.map[name] then assert(not command.map[name], "command already exists: " .. name)
core.log_quiet("Replacing existing command \"%s\"", name)
end
command.map[name] = { predicate = predicate, perform = fn } command.map[name] = { predicate = predicate, perform = fn }
end end
end end
@ -97,85 +26,42 @@ local function capitalize_first(str)
return str:sub(1, 1):upper() .. str:sub(2) return str:sub(1, 1):upper() .. str:sub(2)
end end
---Prettifies the command name.
---
---This function adds a space between the colon and the command name,
---replaces dashes with spaces and capitalizes the command appropriately.
---@see core.command.command_name
---@param name core.command.command_name
---@return string
function command.prettify_name(name) function command.prettify_name(name)
---@diagnostic disable-next-line: redundant-return-value
return name:gsub(":", ": "):gsub("-", " "):gsub("%S+", capitalize_first) return name:gsub(":", ": "):gsub("-", " "):gsub("%S+", capitalize_first)
end end
---Returns all the commands that can be executed (their predicates evaluate to true).
---@return core.command.command_name[]
function command.get_all_valid() function command.get_all_valid()
local res = {} local res = {}
local memoized_predicates = {}
for name, cmd in pairs(command.map) do for name, cmd in pairs(command.map) do
if memoized_predicates[cmd.predicate] == nil then if cmd.predicate() then
memoized_predicates[cmd.predicate] = cmd.predicate()
end
if memoized_predicates[cmd.predicate] then
table.insert(res, name) table.insert(res, name)
end end
end end
return res return res
end end
---Checks whether a command can be executed (its predicate evaluates to true).
---@param name core.command.command_name
---@param ... any
---@return boolean
function command.is_valid(name, ...)
return command.map[name] and command.map[name].predicate(...)
end
local function perform(name, ...) local function perform(name)
local cmd = command.map[name] local cmd = command.map[name]
if not cmd then return false end if cmd and cmd.predicate() then
local res = { cmd.predicate(...) } cmd.perform()
if table.remove(res, 1) then
if #res > 0 then
-- send values returned from predicate
cmd.perform(table.unpack(res))
else
-- send original parameters
cmd.perform(...)
end
return true return true
end end
return false return false
end end
---Performs a command. function command.perform(...)
--- local ok, res = core.try(perform, ...)
---The arguments passed into this function are forwarded to the predicate function. </br>
---If the predicate function returns more than 1 value, the other values are passed
---to the command.
---
---Otherwise, the arguments passed into this function are passed directly
---to the command.
---@see core.command.predicate
---@see core.command.predicate_function
---@param name core.command.command_name
---@param ... any
---@return boolean # true if the command is performed successfully.
function command.perform(name, ...)
local ok, res = core.try(perform, name, ...)
return not ok or res return not ok or res
end end
---Inserts the default commands for Lite XL into the map.
function command.add_defaults() function command.add_defaults()
local reg = { local reg = {
"core", "root", "command", "doc", "findreplace", "core", "root", "command", "doc", "findreplace",
"files", "dialog", "log", "statusbar" "files", "drawwhitespace", "dialog"
} }
for _, name in ipairs(reg) do for _, name in ipairs(reg) do
require("core.commands." .. name) require("core.commands." .. name)

View File

@ -2,23 +2,23 @@ local core = require "core"
local command = require "core.command" local command = require "core.command"
command.add("core.commandview", { command.add("core.commandview", {
["command:submit"] = function(active_view) ["command:submit"] = function()
active_view:submit() core.active_view:submit()
end, end,
["command:complete"] = function(active_view) ["command:complete"] = function()
active_view:complete() core.active_view:complete()
end, end,
["command:escape"] = function(active_view) ["command:escape"] = function()
active_view:exit() core.active_view:exit()
end, end,
["command:select-previous"] = function(active_view) ["command:select-previous"] = function()
active_view:move_suggestion_idx(1) core.active_view:move_suggestion_idx(1)
end, end,
["command:select-next"] = function(active_view) ["command:select-next"] = function()
active_view:move_suggestion_idx(-1) core.active_view:move_suggestion_idx(-1)
end, end,
}) })

View File

@ -6,22 +6,10 @@ local LogView = require "core.logview"
local fullscreen = false local fullscreen = false
local restore_title_view = false
local function suggest_directory(text) local function suggest_directory(text)
text = common.home_expand(text) text = common.home_expand(text)
local basedir = common.dirname(core.project_dir) return common.home_encode_list(text == "" and core.recent_projects or common.dir_path_suggest(text))
return common.home_encode_list((basedir and text == basedir .. PATHSEP or text == "") and
core.recent_projects or common.dir_path_suggest(text))
end
local function check_directory_path(path)
local abs_path = system.absolute_path(path)
local info = abs_path and system.get_file_info(abs_path)
if not info or info.type ~= 'dir' then
return nil
end
return abs_path
end end
command.add(nil, { command.add(nil, {
@ -39,56 +27,47 @@ command.add(nil, {
["core:toggle-fullscreen"] = function() ["core:toggle-fullscreen"] = function()
fullscreen = not fullscreen fullscreen = not fullscreen
if fullscreen then
restore_title_view = core.title_view.visible
end
system.set_window_mode(fullscreen and "fullscreen" or "normal") system.set_window_mode(fullscreen and "fullscreen" or "normal")
core.show_title_bar(not fullscreen and restore_title_view) core.show_title_bar(not fullscreen)
core.title_view:configure_hit_test(not fullscreen and restore_title_view) core.title_view:configure_hit_test(not fullscreen)
end, end,
["core:reload-module"] = function() ["core:reload-module"] = function()
core.command_view:enter("Reload Module", { core.command_view:enter("Reload Module", function(text, item)
submit = function(text, item) local text = item and item.text or text
local text = item and item.text or text core.reload_module(text)
core.reload_module(text) core.log("Reloaded module %q", text)
core.log("Reloaded module %q", text) end, function(text)
end, local items = {}
suggest = function(text) for name in pairs(package.loaded) do
local items = {} table.insert(items, name)
for name in pairs(package.loaded) do
table.insert(items, name)
end
return common.fuzzy_match(items, text)
end end
}) return common.fuzzy_match(items, text)
end)
end, end,
["core:find-command"] = function() ["core:find-command"] = function()
local commands = command.get_all_valid() local commands = command.get_all_valid()
core.command_view:enter("Do Command", { core.command_view:enter("Do Command", function(text, item)
submit = function(text, item) if item then
if item then command.perform(item.command)
command.perform(item.command)
end
end,
suggest = function(text)
local res = common.fuzzy_match(commands, text)
for i, name in ipairs(res) do
res[i] = {
text = command.prettify_name(name),
info = keymap.get_binding(name),
command = name,
}
end
return res
end end
}) end, function(text)
local res = common.fuzzy_match(commands, text)
for i, name in ipairs(res) do
res[i] = {
text = command.prettify_name(name),
info = keymap.get_binding(name),
command = name,
}
end
return res
end)
end, end,
["core:find-file"] = function() ["core:find-file"] = function()
if not core.project_files_number() then if core.project_files_limit then
return command.perform "core:open-file" return command.perform "core:open-file"
end end
local files = {} local files = {}
for dir, item in core.get_project_files() do for dir, item in core.get_project_files() do
@ -97,72 +76,56 @@ command.add(nil, {
table.insert(files, common.home_encode(path .. item.filename)) table.insert(files, common.home_encode(path .. item.filename))
end end
end end
core.command_view:enter("Open File From Project", { core.command_view:enter("Open File From Project", function(text, item)
submit = function(text, item) text = item and item.text or text
text = item and item.text or text core.root_view:open_doc(core.open_doc(common.home_expand(text)))
core.root_view:open_doc(core.open_doc(common.home_expand(text))) end, function(text)
end, return common.fuzzy_match_with_recents(files, core.visited_files, text)
suggest = function(text) end)
return common.fuzzy_match_with_recents(files, core.visited_files, text)
end
})
end, end,
["core:new-doc"] = function() ["core:new-doc"] = function()
core.root_view:open_doc(core.open_doc()) core.root_view:open_doc(core.open_doc())
end, end,
["core:new-named-doc"] = function()
core.command_view:enter("File name", {
submit = function(text)
core.root_view:open_doc(core.open_doc(text))
end
})
end,
["core:open-file"] = function() ["core:open-file"] = function()
local view = core.active_view local view = core.active_view
local text
if view.doc and view.doc.abs_filename then if view.doc and view.doc.abs_filename then
local dirname, filename = view.doc.abs_filename:match("(.*)[/\\](.+)$") local dirname, filename = view.doc.abs_filename:match("(.*)[/\\](.+)$")
if dirname then if dirname then
dirname = core.normalize_to_project_dir(dirname) dirname = core.normalize_to_project_dir(dirname)
text = dirname == core.project_dir and "" or common.home_encode(dirname) .. PATHSEP local text = dirname == core.project_dir and "" or common.home_encode(dirname) .. PATHSEP
core.command_view:set_text(text)
end end
end end
core.command_view:enter("Open File", { core.command_view:enter("Open File", function(text)
text = text, local filename = system.absolute_path(common.home_expand(text))
submit = function(text) core.root_view:open_doc(core.open_doc(filename))
local filename = system.absolute_path(common.home_expand(text)) end, function (text)
core.root_view:open_doc(core.open_doc(filename)) return common.home_encode_list(common.path_suggest(common.home_expand(text)))
end, end, nil, function(text)
suggest = function (text) local filename = common.home_expand(text)
return common.home_encode_list(common.path_suggest(common.home_expand(text))) local path_stat, err = system.get_file_info(filename)
end, if err then
validate = function(text) if err:find("No such file", 1, true) then
local filename = common.home_expand(text) -- check if the containing directory exists
local path_stat, err = system.get_file_info(filename) local dirname = common.dirname(filename)
if err then local dir_stat = dirname and system.get_file_info(dirname)
if err:find("No such file", 1, true) then if not dirname or (dir_stat and dir_stat.type == 'dir') then
-- check if the containing directory exists
local dirname = common.dirname(filename)
local dir_stat = dirname and system.get_file_info(dirname)
if not dirname or (dir_stat and dir_stat.type == 'dir') then
return true
end
end
core.error("Cannot open file %s: %s", text, err)
elseif path_stat.type == 'dir' then
core.error("Cannot open %s, is a folder", text)
else
return true return true
end end
end, end
}) core.error("Cannot open file %s: %s", text, err)
elseif path_stat.type == 'dir' then
core.error("Cannot open %s, is a folder", text)
else
return true
end
end)
end, end,
["core:open-log"] = function() ["core:open-log"] = function()
local node = core.root_view:get_active_node_default() local node = core.root_view:get_active_node()
node:add_view(LogView()) node:add_view(LogView())
end, end,
@ -173,79 +136,64 @@ command.add(nil, {
end, end,
["core:open-project-module"] = function() ["core:open-project-module"] = function()
if not system.get_file_info(".lite_project.lua") then local filename = ".lite_project.lua"
core.try(core.write_init_project_module, ".lite_project.lua") if system.get_file_info(filename) then
core.root_view:open_doc(core.open_doc(filename))
else
local doc = core.open_doc()
core.root_view:open_doc(doc)
doc:save(filename)
end end
local doc = core.open_doc(".lite_project.lua")
core.root_view:open_doc(doc)
doc:save()
end, end,
["core:change-project-folder"] = function() ["core:change-project-folder"] = function()
local dirname = common.dirname(core.project_dir) local dirname = common.dirname(core.project_dir)
local text
if dirname then if dirname then
text = common.basepath(common.home_encode(dirname)) core.command_view:set_text(common.home_encode(dirname) .. PATHSEP)
end end
core.command_view:enter("Change Project Folder", { core.command_view:enter("Change Project Folder", function(text, item)
text = text, text = system.absolute_path(common.home_expand(item and item.text or text))
submit = function(text) if text == core.project_dir then return end
local path = common.home_expand(text) local path_stat = system.get_file_info(text)
local abs_path = check_directory_path(path) if not path_stat or path_stat.type ~= 'dir' then
if not abs_path then core.error("Cannot open folder %q", text)
core.error("Cannot open directory %q", path) return
return end
end core.confirm_close_docs(core.docs, core.open_folder_project, text)
if abs_path == core.project_dir then return end end, suggest_directory)
core.confirm_close_docs(core.docs, function(dirpath)
core.open_folder_project(dirpath)
end, abs_path)
end,
suggest = suggest_directory
})
end, end,
["core:open-project-folder"] = function() ["core:open-project-folder"] = function()
local dirname = common.dirname(core.project_dir) local dirname = common.dirname(core.project_dir)
local text
if dirname then if dirname then
text = common.home_encode(dirname) .. PATHSEP core.command_view:set_text(common.home_encode(dirname) .. PATHSEP)
end end
core.command_view:enter("Open Project", { core.command_view:enter("Open Project", function(text, item)
text = text, text = common.home_expand(item and item.text or text)
submit = function(text) local path_stat = system.get_file_info(text)
local path = common.home_expand(text) if not path_stat or path_stat.type ~= 'dir' then
local abs_path = check_directory_path(path) core.error("Cannot open folder %q", text)
if not abs_path then return
core.error("Cannot open directory %q", path) end
return system.exec(string.format("%q %q", EXEFILE, text))
end end, suggest_directory)
if abs_path == core.project_dir then
core.error("Directory %q is currently opened", abs_path)
return
end
system.exec(string.format("%q %q", EXEFILE, abs_path))
end,
suggest = suggest_directory
})
end, end,
["core:add-directory"] = function() ["core:add-directory"] = function()
core.command_view:enter("Add Directory", { core.command_view:enter("Add Directory", function(text)
submit = function(text) text = common.home_expand(text)
text = common.home_expand(text) local path_stat, err = system.get_file_info(text)
local path_stat, err = system.get_file_info(text) if not path_stat then
if not path_stat then core.error("cannot open %q: %s", text, err)
core.error("cannot open %q: %s", text, err) return
return elseif path_stat.type ~= 'dir' then
elseif path_stat.type ~= 'dir' then core.error("%q is not a directory", text)
core.error("%q is not a directory", text) return
return end
end core.add_project_directory(system.absolute_path(text))
core.add_project_directory(system.absolute_path(text)) -- TODO: add the name of directory to prioritize
end, core.reschedule_project_scan()
suggest = suggest_directory end, suggest_directory)
})
end, end,
["core:remove-directory"] = function() ["core:remove-directory"] = function()
@ -254,17 +202,14 @@ command.add(nil, {
for i = n, 2, -1 do for i = n, 2, -1 do
dir_list[n - i + 1] = core.project_directories[i].name dir_list[n - i + 1] = core.project_directories[i].name
end end
core.command_view:enter("Remove Directory", { core.command_view:enter("Remove Directory", function(text, item)
submit = function(text, item) text = common.home_expand(item and item.text or text)
text = common.home_expand(item and item.text or text) if not core.remove_project_directory(text) then
if not core.remove_project_directory(text) then core.error("No directory %q to be removed", text)
core.error("No directory %q to be removed", text)
end
end,
suggest = function(text)
text = common.home_expand(text)
return common.home_encode_list(common.dir_list_suggest(text, dir_list))
end end
}) end, function(text)
text = common.home_expand(text)
return common.home_encode_list(common.dir_list_suggest(text, dir_list))
end)
end, end,
}) })

View File

@ -3,25 +3,30 @@ local command = require "core.command"
local common = require "core.common" local common = require "core.common"
command.add("core.nagview", { command.add("core.nagview", {
["dialog:previous-entry"] = function(v) ["dialog:previous-entry"] = function()
local v = core.active_view
local hover = v.hovered_item or 1 local hover = v.hovered_item or 1
v:change_hovered(hover == 1 and #v.options or hover - 1) v:change_hovered(hover == 1 and #v.options or hover - 1)
end, end,
["dialog:next-entry"] = function(v) ["dialog:next-entry"] = function()
local v = core.active_view
local hover = v.hovered_item or 1 local hover = v.hovered_item or 1
v:change_hovered(hover == #v.options and 1 or hover + 1) v:change_hovered(hover == #v.options and 1 or hover + 1)
end, end,
["dialog:select-yes"] = function(v) ["dialog:select-yes"] = function()
local v = core.active_view
if v ~= core.nag_view then return end if v ~= core.nag_view then return end
v:change_hovered(common.find_index(v.options, "default_yes")) v:change_hovered(common.find_index(v.options, "default_yes"))
command.perform "dialog:select" command.perform "dialog:select"
end, end,
["dialog:select-no"] = function(v) ["dialog:select-no"] = function()
local v = core.active_view
if v ~= core.nag_view then return end if v ~= core.nag_view then return end
v:change_hovered(common.find_index(v.options, "default_no")) v:change_hovered(common.find_index(v.options, "default_no"))
command.perform "dialog:select" command.perform "dialog:select"
end, end,
["dialog:select"] = function(v) ["dialog:select"] = function()
local v = core.active_view
if v.hovered_item then if v.hovered_item then
v.on_selected(v.options[v.hovered_item]) v.on_selected(v.options[v.hovered_item])
v:next() v:next()

View File

@ -3,9 +3,12 @@ local command = require "core.command"
local common = require "core.common" local common = require "core.common"
local config = require "core.config" local config = require "core.config"
local translate = require "core.doc.translate" local translate = require "core.doc.translate"
local style = require "core.style"
local DocView = require "core.docview" local DocView = require "core.docview"
local tokenizer = require "core.tokenizer"
local function dv()
return core.active_view
end
local function doc() local function doc()
@ -13,6 +16,14 @@ local function doc()
end end
local function get_indent_string()
if config.tab_type == "hard" then
return "\t"
end
return string.rep(" ", config.indent_size)
end
local function doc_multiline_selections(sort) local function doc_multiline_selections(sort)
local iter, state, idx, line1, col1, line2, col2 = doc():get_selections(sort) local iter, state, idx, line1, col1, line2, col2 = doc():get_selections(sort)
return function() return function()
@ -37,196 +48,47 @@ local function save(filename)
filename = core.normalize_to_project_dir(filename) filename = core.normalize_to_project_dir(filename)
abs_filename = core.project_absolute_path(filename) abs_filename = core.project_absolute_path(filename)
end end
local ok, err = pcall(doc().save, doc(), filename, abs_filename) doc():save(filename, abs_filename)
if ok then local saved_filename = doc().filename
local saved_filename = doc().filename core.log("Saved \"%s\"", saved_filename)
core.log("Saved \"%s\"", saved_filename)
else
core.error(err)
core.nag_view:show("Saving failed", string.format("Couldn't save file \"%s\". Do you want to save to another location?", doc().filename), {
{ text = "Yes", default_yes = true },
{ text = "No", default_no = true }
}, function(item)
if item.text == "Yes" then
core.add_thread(function()
-- we need to run this in a thread because of the odd way the nagview is.
command.perform("doc:save-as")
end)
end
end)
end
end end
local function cut_or_copy(delete) local function cut_or_copy(delete)
local full_text = "" local full_text = ""
local text = "" for idx, line1, col1, line2, col2 in doc():get_selections() do
core.cursor_clipboard = {}
core.cursor_clipboard_whole_line = {}
for idx, line1, col1, line2, col2 in doc():get_selections(true, true) do
if line1 ~= line2 or col1 ~= col2 then if line1 ~= line2 or col1 ~= col2 then
text = doc():get_text(line1, col1, line2, col2) local text = doc():get_text(line1, col1, line2, col2)
full_text = full_text == "" and text or (text .. " " .. full_text)
core.cursor_clipboard_whole_line[idx] = false
if delete then if delete then
doc():delete_to_cursor(idx, 0) doc():delete_to_cursor(idx, 0)
end end
else -- Cut/copy whole line full_text = full_text == "" and text or (full_text .. "\n" .. text)
-- Remove newline from the text. It will be added as needed on paste. doc().cursor_clipboard[idx] = text
text = string.sub(doc().lines[line1], 1, -2) else
full_text = full_text == "" and text .. "\n" or (text .. "\n" .. full_text) doc().cursor_clipboard[idx] = ""
core.cursor_clipboard_whole_line[idx] = true
if delete then
if line1 < #doc().lines then
doc():remove(line1, 1, line1 + 1, 1)
elseif #doc().lines == 1 then
doc():remove(line1, 1, line1, math.huge)
else
doc():remove(line1 - 1, math.huge, line1, math.huge)
end
doc():set_selections(idx, line1, col1, line2, col2)
end
end end
core.cursor_clipboard[idx] = text
end end
if delete then doc():merge_cursors() end doc().cursor_clipboard["full"] = full_text
core.cursor_clipboard["full"] = full_text
system.set_clipboard(full_text) system.set_clipboard(full_text)
end end
local function split_cursor(dv, direction) local function split_cursor(direction)
local new_cursors = {} local new_cursors = {}
local dv_translate = direction < 0 for _, line1, col1 in doc():get_selections() do
and DocView.translate.previous_line if line1 + direction >= 1 and line1 + direction <= #doc().lines then
or DocView.translate.next_line table.insert(new_cursors, { line1 + direction, col1 })
for _, line1, col1 in dv.doc:get_selections() do
if line1 + direction >= 1 and line1 + direction <= #dv.doc.lines then
table.insert(new_cursors, { dv_translate(dv.doc, line1, col1, dv) })
end end
end end
-- add selections in the order that will leave the "last" added one as doc.last_selection for i,v in ipairs(new_cursors) do doc():add_selection(v[1], v[2]) end
local start, stop = 1, #new_cursors
if direction < 0 then
start, stop = #new_cursors, 1
end
for i = start, stop, direction do
local v = new_cursors[i]
dv.doc:add_selection(v[1], v[2])
end
core.blink_reset() core.blink_reset()
end end
local function set_cursor(dv, x, y, snap_type)
local line, col = dv:resolve_screen_position(x, y)
dv.doc:set_selection(line, col, line, col)
if snap_type == "word" or snap_type == "lines" then
command.perform("doc:select-" .. snap_type)
end
dv.mouse_selecting = { line, col, snap_type }
core.blink_reset()
end
local function line_comment(comment, line1, col1, line2, col2)
local start_comment = (type(comment) == 'table' and comment[1] or comment) .. " "
local end_comment = (type(comment) == 'table' and " " .. comment[2])
local uncomment = true
local start_offset = math.huge
for line = line1, line2 do
local text = doc().lines[line]
local s = text:find("%S")
if s then
local cs, ce = text:find(start_comment, s, true)
if cs ~= s then
uncomment = false
end
start_offset = math.min(start_offset, s)
end
end
local end_line = col2 == #doc().lines[line2]
for line = line1, line2 do
local text = doc().lines[line]
local s = text:find("%S")
if s and uncomment then
if end_comment and text:sub(#text - #end_comment, #text - 1) == end_comment then
doc():remove(line, #text - #end_comment, line, #text)
end
local cs, ce = text:find(start_comment, s, true)
if ce then
doc():remove(line, cs, line, ce + 1)
end
elseif s then
doc():insert(line, start_offset, start_comment)
if end_comment then
doc():insert(line, #doc().lines[line], " " .. comment[2])
end
end
end
col1 = col1 + (col1 > start_offset and #start_comment or 0) * (uncomment and -1 or 1)
col2 = col2 + (col2 > start_offset and #start_comment or 0) * (uncomment and -1 or 1)
if end_comment and end_line then
col2 = col2 + #end_comment * (uncomment and -1 or 1)
end
return line1, col1, line2, col2
end
local function block_comment(comment, line1, col1, line2, col2)
-- automatically skip spaces
local word_start = doc():get_text(line1, col1, line1, math.huge):find("%S")
local word_end = doc():get_text(line2, 1, line2, col2):find("%s*$")
col1 = col1 + (word_start and (word_start - 1) or 0)
col2 = word_end and word_end or col2
local block_start = doc():get_text(line1, col1, line1, col1 + #comment[1])
local block_end = doc():get_text(line2, col2 - #comment[2], line2, col2)
if block_start == comment[1] and block_end == comment[2] then
-- remove up to 1 whitespace after the comment
local start_len, stop_len = #comment[1], #comment[2]
if doc():get_text(line1, col1 + #comment[1], line1, col1 + #comment[1] + 1):find("%s$") then
start_len = start_len + 1
end
if doc():get_text(line2, col2 - #comment[2] - 1, line2, col2):find("^%s") then
stop_len = stop_len + 1
end
doc():remove(line1, col1, line1, col1 + start_len)
col2 = col2 - (line1 == line2 and start_len or 0)
doc():remove(line2, col2 - stop_len, line2, col2)
return line1, col1, line2, col2 - stop_len
else
doc():insert(line1, col1, comment[1] .. " ")
col2 = col2 + (line1 == line2 and (#comment[1] + 1) or 0)
doc():insert(line2, col2, " " .. comment[2])
return line1, col1, line2, col2 + #comment[2] + 1
end
end
local function insert_paste(doc, value, whole_line, idx)
if whole_line then
local line1, col1 = doc:get_selection_idx(idx)
doc:insert(line1, 1, value:gsub("\r", "").."\n")
-- Because we're inserting at the start of the line,
-- if the cursor is in the middle of the line
-- it gets carried to the next line along with the old text.
-- If it's at the start of the line it doesn't get carried,
-- so we move it of as many characters as we're adding.
if col1 == 1 then
doc:move_to_cursor(idx, #value+1)
end
else
doc:text_input(value:gsub("\r", ""), idx)
end
end
local commands = { local commands = {
["doc:select-none"] = function(dv) ["doc:undo"] = function()
local l1, c1 = dv.doc:get_selection_idx(dv.doc.last_selection) doc():undo()
if not l1 then end,
l1, c1 = dv.doc:get_selection_idx(1)
end ["doc:redo"] = function()
dv.doc:set_selection(l1, c1) doc():redo()
end, end,
["doc:cut"] = function() ["doc:cut"] = function()
@ -237,283 +99,208 @@ local commands = {
cut_or_copy(false) cut_or_copy(false)
end, end,
["doc:undo"] = function(dv) ["doc:paste"] = function()
dv.doc:undo()
end,
["doc:redo"] = function(dv)
dv.doc:redo()
end,
["doc:paste"] = function(dv)
local clipboard = system.get_clipboard() local clipboard = system.get_clipboard()
-- If the clipboard has changed since our last look, use that instead -- If the clipboard has changed since our last look, use that instead
if core.cursor_clipboard["full"] ~= clipboard then if doc().cursor_clipboard["full"] ~= clipboard then
core.cursor_clipboard = {} doc().cursor_clipboard = {}
core.cursor_clipboard_whole_line = {}
for idx in dv.doc:get_selections() do
insert_paste(dv.doc, clipboard, false, idx)
end
return
end end
-- Use internal clipboard(s) for idx, line1, col1, line2, col2 in doc():get_selections() do
-- If there are mixed whole lines and normal lines, consider them all as normal local value = doc().cursor_clipboard[idx] or clipboard
local only_whole_lines = true doc():text_input(value:gsub("\r", ""), idx)
for _,whole_line in pairs(core.cursor_clipboard_whole_line) do
if not whole_line then
only_whole_lines = false
break
end
end
if #core.cursor_clipboard_whole_line == (#dv.doc.selections/4) then
-- If we have the same number of clipboards and selections,
-- paste each clipboard into its corresponding selection
for idx in dv.doc:get_selections() do
insert_paste(dv.doc, core.cursor_clipboard[idx], only_whole_lines, idx)
end
else
-- Paste every clipboard and add a selection at the end of each one
local new_selections = {}
for idx in dv.doc:get_selections() do
for cb_idx in ipairs(core.cursor_clipboard_whole_line) do
insert_paste(dv.doc, core.cursor_clipboard[cb_idx], only_whole_lines, idx)
if not only_whole_lines then
table.insert(new_selections, {dv.doc:get_selection_idx(idx)})
end
end
if only_whole_lines then
table.insert(new_selections, {dv.doc:get_selection_idx(idx)})
end
end
local first = true
for _,selection in pairs(new_selections) do
if first then
dv.doc:set_selection(table.unpack(selection))
first = false
else
dv.doc:add_selection(table.unpack(selection))
end
end
end end
end, end,
["doc:newline"] = function(dv) ["doc:newline"] = function()
for idx, line, col in dv.doc:get_selections(false, true) do for idx, line, col in doc():get_selections(false, true) do
local indent = dv.doc.lines[line]:match("^[\t ]*") local indent = doc().lines[line]:match("^[\t ]*")
if col <= #indent then if col <= #indent then
indent = indent:sub(#indent + 2 - col) indent = indent:sub(#indent + 2 - col)
end end
-- Remove current line if it contains only whitespace doc():text_input("\n" .. indent, idx)
if not config.keep_newline_whitespace and dv.doc.lines[line]:match("^%s+$") then end
dv.doc:remove(line, 1, line, math.huge) end,
["doc:newline-below"] = function()
for idx, line in doc():get_selections(false, true) do
local indent = doc().lines[line]:match("^[\t ]*")
doc():insert(line, math.huge, "\n" .. indent)
doc():set_selections(idx, line + 1, math.huge)
end
end,
["doc:newline-above"] = function()
for idx, line in doc():get_selections(false, true) do
local indent = doc().lines[line]:match("^[\t ]*")
doc():insert(line, 1, indent .. "\n")
doc():set_selections(idx, line, math.huge)
end
end,
["doc:delete"] = function()
for idx, line1, col1, line2, col2 in doc():get_selections() do
if line1 == line2 and col1 == col2 and doc().lines[line1]:find("^%s*$", col1) then
doc():remove(line1, col1, line1, math.huge)
end end
dv.doc:text_input("\n" .. indent, idx) doc():delete_to_cursor(idx, translate.next_char)
end end
end, end,
["doc:newline-below"] = function(dv) ["doc:backspace"] = function()
for idx, line in dv.doc:get_selections(false, true) do for idx, line1, col1, line2, col2 in doc():get_selections() do
local indent = dv.doc.lines[line]:match("^[\t ]*")
dv.doc:insert(line, math.huge, "\n" .. indent)
dv.doc:set_selections(idx, line + 1, math.huge)
end
end,
["doc:newline-above"] = function(dv)
for idx, line in dv.doc:get_selections(false, true) do
local indent = dv.doc.lines[line]:match("^[\t ]*")
dv.doc:insert(line, 1, indent .. "\n")
dv.doc:set_selections(idx, line, math.huge)
end
end,
["doc:delete"] = function(dv)
for idx, line1, col1, line2, col2 in dv.doc:get_selections(true, true) do
if line1 == line2 and col1 == col2 and dv.doc.lines[line1]:find("^%s*$", col1) then
dv.doc:remove(line1, col1, line1, math.huge)
end
dv.doc:delete_to_cursor(idx, translate.next_char)
end
end,
["doc:backspace"] = function(dv)
local _, indent_size = dv.doc:get_indent_info()
for idx, line1, col1, line2, col2 in dv.doc:get_selections(true, true) do
if line1 == line2 and col1 == col2 then if line1 == line2 and col1 == col2 then
local text = dv.doc:get_text(line1, 1, line1, col1) local text = doc():get_text(line1, 1, line1, col1)
if #text >= indent_size and text:find("^ *$") then if #text >= config.indent_size and text:find("^ *$") then
dv.doc:delete_to_cursor(idx, 0, -indent_size) doc():delete_to_cursor(idx, 0, -config.indent_size)
goto continue return
end end
end end
dv.doc:delete_to_cursor(idx, translate.previous_char) doc():delete_to_cursor(idx, translate.previous_char)
::continue::
end end
end, end,
["doc:select-all"] = function(dv) ["doc:select-all"] = function()
dv.doc:set_selection(1, 1, math.huge, math.huge) doc():set_selection(1, 1, math.huge, math.huge)
-- avoid triggering DocView:scroll_to_make_visible
dv.last_line1 = 1
dv.last_col1 = 1
dv.last_line2 = #dv.doc.lines
dv.last_col2 = #dv.doc.lines[#dv.doc.lines]
end, end,
["doc:select-lines"] = function(dv) ["doc:select-none"] = function()
for idx, line1, _, line2 in dv.doc:get_selections(true) do local line, col = doc():get_selection()
doc():set_selection(line, col)
end,
["doc:select-lines"] = function()
for idx, line1, _, line2 in doc():get_selections(true) do
append_line_if_last_line(line2) append_line_if_last_line(line2)
dv.doc:set_selections(idx, line1, 1, line2 + 1, 1) doc():set_selections(idx, line1, 1, line2 + 1, 1)
end end
end, end,
["doc:select-word"] = function(dv) ["doc:select-word"] = function()
for idx, line1, col1 in dv.doc:get_selections(true) do for idx, line1, col1 in doc():get_selections(true) do
local line1, col1 = translate.start_of_word(dv.doc, line1, col1) local line1, col1 = translate.start_of_word(doc(), line1, col1)
local line2, col2 = translate.end_of_word(dv.doc, line1, col1) local line2, col2 = translate.end_of_word(doc(), line1, col1)
dv.doc:set_selections(idx, line2, col2, line1, col1) doc():set_selections(idx, line2, col2, line1, col1)
end end
end, end,
["doc:join-lines"] = function(dv) ["doc:join-lines"] = function()
for idx, line1, col1, line2, col2 in dv.doc:get_selections(true) do for idx, line1, col1, line2, col2 in doc():get_selections(true) do
if line1 == line2 then line2 = line2 + 1 end if line1 == line2 then line2 = line2 + 1 end
local text = dv.doc:get_text(line1, 1, line2, math.huge) local text = doc():get_text(line1, 1, line2, math.huge)
text = text:gsub("(.-)\n[\t ]*", function(x) text = text:gsub("(.-)\n[\t ]*", function(x)
return x:find("^%s*$") and x or x .. " " return x:find("^%s*$") and x or x .. " "
end) end)
dv.doc:insert(line1, 1, text) doc():insert(line1, 1, text)
dv.doc:remove(line1, #text + 1, line2, math.huge) doc():remove(line1, #text + 1, line2, math.huge)
if line1 ~= line2 or col1 ~= col2 then if line1 ~= line2 or col1 ~= col2 then
dv.doc:set_selections(idx, line1, math.huge) doc():set_selections(idx, line1, math.huge)
end end
end end
end, end,
["doc:indent"] = function(dv) ["doc:indent"] = function()
for idx, line1, col1, line2, col2 in doc_multiline_selections(true) do for idx, line1, col1, line2, col2 in doc_multiline_selections(true) do
local l1, c1, l2, c2 = dv.doc:indent_text(false, line1, col1, line2, col2) local l1, c1, l2, c2 = doc():indent_text(false, line1, col1, line2, col2)
if l1 then if l1 then
dv.doc:set_selections(idx, l1, c1, l2, c2) doc():set_selections(idx, l1, c1, l2, c2)
end end
end end
end, end,
["doc:unindent"] = function(dv) ["doc:unindent"] = function()
for idx, line1, col1, line2, col2 in doc_multiline_selections(true) do for idx, line1, col1, line2, col2 in doc_multiline_selections(true) do
local l1, c1, l2, c2 = dv.doc:indent_text(true, line1, col1, line2, col2) local l1, c1, l2, c2 = doc():indent_text(true, line1, col1, line2, col2)
if l1 then if l1 then
dv.doc:set_selections(idx, l1, c1, l2, c2) doc():set_selections(idx, l1, c1, l2, c2)
end end
end end
end, end,
["doc:duplicate-lines"] = function(dv) ["doc:duplicate-lines"] = function()
for idx, line1, col1, line2, col2 in doc_multiline_selections(true) do for idx, line1, col1, line2, col2 in doc_multiline_selections(true) do
append_line_if_last_line(line2) append_line_if_last_line(line2)
local text = doc():get_text(line1, 1, line2 + 1, 1) local text = doc():get_text(line1, 1, line2 + 1, 1)
dv.doc:insert(line2 + 1, 1, text) doc():insert(line2 + 1, 1, text)
local n = line2 - line1 + 1 local n = line2 - line1 + 1
dv.doc:set_selections(idx, line1 + n, col1, line2 + n, col2) doc():set_selections(idx, line1 + n, col1, line2 + n, col2)
end end
end, end,
["doc:delete-lines"] = function(dv) ["doc:delete-lines"] = function()
for idx, line1, col1, line2, col2 in doc_multiline_selections(true) do for idx, line1, col1, line2, col2 in doc_multiline_selections(true) do
append_line_if_last_line(line2) append_line_if_last_line(line2)
dv.doc:remove(line1, 1, line2 + 1, 1) doc():remove(line1, 1, line2 + 1, 1)
dv.doc:set_selections(idx, line1, col1) doc():set_selections(idx, line1, col1)
end end
end, end,
["doc:move-lines-up"] = function(dv) ["doc:move-lines-up"] = function()
for idx, line1, col1, line2, col2 in doc_multiline_selections(true) do for idx, line1, col1, line2, col2 in doc_multiline_selections(true) do
append_line_if_last_line(line2) append_line_if_last_line(line2)
if line1 > 1 then if line1 > 1 then
local text = doc().lines[line1 - 1] local text = doc().lines[line1 - 1]
dv.doc:insert(line2 + 1, 1, text) doc():insert(line2 + 1, 1, text)
dv.doc:remove(line1 - 1, 1, line1, 1) doc():remove(line1 - 1, 1, line1, 1)
dv.doc:set_selections(idx, line1 - 1, col1, line2 - 1, col2) doc():set_selections(idx, line1 - 1, col1, line2 - 1, col2)
end end
end end
end, end,
["doc:move-lines-down"] = function(dv) ["doc:move-lines-down"] = function()
for idx, line1, col1, line2, col2 in doc_multiline_selections(true) do for idx, line1, col1, line2, col2 in doc_multiline_selections(true) do
append_line_if_last_line(line2 + 1) append_line_if_last_line(line2 + 1)
if line2 < #dv.doc.lines then if line2 < #doc().lines then
local text = dv.doc.lines[line2 + 1] local text = doc().lines[line2 + 1]
dv.doc:remove(line2 + 1, 1, line2 + 2, 1) doc():remove(line2 + 1, 1, line2 + 2, 1)
dv.doc:insert(line1, 1, text) doc():insert(line1, 1, text)
dv.doc:set_selections(idx, line1 + 1, col1, line2 + 1, col2) doc():set_selections(idx, line1 + 1, col1, line2 + 1, col2)
end end
end end
end, end,
["doc:toggle-block-comments"] = function(dv) ["doc:toggle-line-comments"] = function()
for idx, line1, col1, line2, col2 in doc_multiline_selections(true) do local comment = doc().syntax.comment
local current_syntax = dv.doc.syntax if not comment then return end
if line1 > 1 then local indentation = get_indent_string()
-- Use the previous line state, as it will be the state local comment_text = comment .. " "
-- of the beginning of the current line for idx, line1, _, line2 in doc_multiline_selections(true) do
local state = dv.doc.highlighter:get_line(line1 - 1).state local uncomment = true
local syntaxes = tokenizer.extract_subsyntaxes(dv.doc.syntax, state) local start_offset = math.huge
-- Go through all the syntaxes until the first with `block_comment` defined for line = line1, line2 do
for _, s in pairs(syntaxes) do local text = doc().lines[line]
if s.block_comment then local s = text:find("%S")
current_syntax = s local cs, ce = text:find(comment_text, s, true)
break if s and cs ~= s then
uncomment = false
start_offset = math.min(start_offset, s)
end
end
for line = line1, line2 do
local text = doc().lines[line]
local s = text:find("%S")
if uncomment then
local cs, ce = text:find(comment_text, s, true)
if ce then
doc():remove(line, cs, line, ce + 1)
end end
elseif s then
doc():insert(line, start_offset, comment_text)
end end
end end
local comment = current_syntax.block_comment
if not comment then
if dv.doc.syntax.comment then
command.perform "doc:toggle-line-comments"
end
return
end
-- if nothing is selected, toggle the whole line
if line1 == line2 and col1 == col2 then
col1 = 1
col2 = #dv.doc.lines[line2]
end
dv.doc:set_selections(idx, block_comment(comment, line1, col1, line2, col2))
end
end,
["doc:toggle-line-comments"] = function(dv)
for idx, line1, col1, line2, col2 in doc_multiline_selections(true) do
local current_syntax = dv.doc.syntax
if line1 > 1 then
-- Use the previous line state, as it will be the state
-- of the beginning of the current line
local state = dv.doc.highlighter:get_line(line1 - 1).state
local syntaxes = tokenizer.extract_subsyntaxes(dv.doc.syntax, state)
-- Go through all the syntaxes until the first with comments defined
for _, s in pairs(syntaxes) do
if s.comment or s.block_comment then
current_syntax = s
break
end
end
end
local comment = current_syntax.comment or current_syntax.block_comment
if comment then
dv.doc:set_selections(idx, line_comment(comment, line1, col1, line2, col2))
end
end end
end, end,
["doc:upper-case"] = function(dv) ["doc:upper-case"] = function()
dv.doc:replace(string.uupper) doc():replace(string.upper)
end, end,
["doc:lower-case"] = function(dv) ["doc:lower-case"] = function()
dv.doc:replace(string.ulower) doc():replace(string.lower)
end, end,
["doc:go-to-line"] = function(dv) ["doc:go-to-line"] = function()
local dv = dv()
local items local items
local function init_items() local function init_items()
if items then return end if items then return end
@ -525,197 +312,138 @@ local commands = {
end end
end end
core.command_view:enter("Go To Line", { core.command_view:enter("Go To Line", function(text, item)
submit = function(text, item) local line = item and item.line or tonumber(text)
local line = item and item.line or tonumber(text) if not line then
if not line then core.error("Invalid line number or unmatched string")
core.error("Invalid line number or unmatched string") return
return
end
dv.doc:set_selection(line, 1 )
dv:scroll_to_line(line, true)
end,
suggest = function(text)
if not text:find("^%d*$") then
init_items()
return common.fuzzy_match(items, text)
end
end end
}) dv.doc:set_selection(line, 1 )
dv:scroll_to_line(line, true)
end, function(text)
if not text:find("^%d*$") then
init_items()
return common.fuzzy_match(items, text)
end
end)
end, end,
["doc:toggle-line-ending"] = function(dv) ["doc:toggle-line-ending"] = function()
dv.doc.crlf = not dv.doc.crlf doc().crlf = not doc().crlf
end, end,
["doc:save-as"] = function(dv) ["doc:save-as"] = function()
local last_doc = core.last_active_view and core.last_active_view.doc local last_doc = core.last_active_view and core.last_active_view.doc
local text if doc().filename then
if dv.doc.filename then core.command_view:set_text(doc().filename)
text = dv.doc.filename
elseif last_doc and last_doc.filename then elseif last_doc and last_doc.filename then
local dirname, filename = core.last_active_view.doc.abs_filename:match("(.*)[/\\](.+)$") local dirname, filename = core.last_active_view.doc.abs_filename:match("(.*)[/\\](.+)$")
text = core.normalize_to_project_dir(dirname) .. PATHSEP core.command_view:set_text(core.normalize_to_project_dir(dirname) .. PATHSEP)
end end
core.command_view:enter("Save As", { core.command_view:enter("Save As", function(filename)
text = text, save(common.home_expand(filename))
submit = function(filename) end, function (text)
save(common.home_expand(filename)) return common.home_encode_list(common.path_suggest(common.home_expand(text)))
end, end)
suggest = function (text)
return common.home_encode_list(common.path_suggest(common.home_expand(text)))
end
})
end, end,
["doc:save"] = function(dv) ["doc:save"] = function()
if dv.doc.filename then if doc().filename then
save() save()
else else
command.perform("doc:save-as") command.perform("doc:save-as")
end end
end, end,
["doc:reload"] = function(dv) ["file:rename"] = function()
dv.doc:reload() local old_filename = doc().filename
end,
["file:rename"] = function(dv)
local old_filename = dv.doc.filename
if not old_filename then if not old_filename then
core.error("Cannot rename unsaved doc") core.error("Cannot rename unsaved doc")
return return
end end
core.command_view:enter("Rename", { core.command_view:set_text(old_filename)
text = old_filename, core.command_view:enter("Rename", function(filename)
submit = function(filename) save(common.home_expand(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,
suggest = function (text)
return common.home_encode_list(common.path_suggest(common.home_expand(text)))
end end
}) end, function (text)
return common.home_encode_list(common.path_suggest(common.home_expand(text)))
end)
end, end,
["file:delete"] = function(dv) ["file:delete"] = function()
local filename = dv.doc.abs_filename local filename = doc().abs_filename
if not filename then if not filename then
core.error("Cannot remove unsaved doc") core.error("Cannot remove unsaved doc")
return return
end end
for i,docview in ipairs(core.get_views_referencing_doc(dv.doc)) do for i,docview in ipairs(core.get_views_referencing_doc(doc())) do
local node = core.root_view.root_node:get_node_for_view(docview) local node = core.root_view.root_node:get_node_for_view(docview)
node:close_view(core.root_view.root_node, docview) node:close_view(core.root_view, docview)
end end
os.remove(filename) os.remove(filename)
core.log("Removed \"%s\"", filename) core.log("Removed \"%s\"", filename)
end, end,
["doc:select-to-cursor"] = function(dv, x, y, clicks) ["doc:create-cursor-previous-line"] = function()
local line1, col1 = select(3, doc():get_selection()) split_cursor(-1)
local line2, col2 = dv:resolve_screen_position(x, y) doc():merge_cursors()
dv.mouse_selecting = { line1, col1, nil }
dv.doc:set_selection(line2, col2, line1, col1)
end, end,
["doc:create-cursor-previous-line"] = function(dv) ["doc:create-cursor-next-line"] = function()
split_cursor(dv, -1) split_cursor(1)
dv.doc:merge_cursors() doc():merge_cursors()
end,
["doc:create-cursor-next-line"] = function(dv)
split_cursor(dv, 1)
dv.doc:merge_cursors()
end end
} }
command.add(function(x, y)
if x == nil or y == nil or not core.active_view:extends(DocView) then return false end
local dv = core.active_view
local x1,y1,x2,y2 = dv.position.x, dv.position.y, dv.position.x + dv.size.x, dv.position.y + dv.size.y
return x >= x1 + dv:get_gutter_width() and x < x2 and y >= y1 and y < y2, dv, x, y
end, {
["doc:set-cursor"] = function(dv, x, y)
set_cursor(dv, x, y, "set")
end,
["doc:set-cursor-word"] = function(dv, x, y)
set_cursor(dv, x, y, "word")
end,
["doc:set-cursor-line"] = function(dv, x, y, clicks)
set_cursor(dv, x, y, "lines")
end,
["doc:split-cursor"] = function(dv, x, y, clicks)
local line, col = dv:resolve_screen_position(x, y)
local removal_target = nil
for idx, line1, col1 in dv.doc:get_selections(true) do
if line1 == line and col1 == col and #doc().selections > 4 then
removal_target = idx
end
end
if removal_target then
dv.doc:remove_selection(removal_target)
else
dv.doc:add_selection(line, col, line, col)
end
dv.mouse_selecting = { line, col, "set" }
end
})
local translations = { local translations = {
["previous-char"] = translate, ["previous-char"] = translate.previous_char,
["next-char"] = translate, ["next-char"] = translate.next_char,
["previous-word-start"] = translate, ["previous-word-start"] = translate.previous_word_start,
["next-word-end"] = translate, ["next-word-end"] = translate.next_word_end,
["previous-block-start"] = translate, ["previous-block-start"] = translate.previous_block_start,
["next-block-end"] = translate, ["next-block-end"] = translate.next_block_end,
["start-of-doc"] = translate, ["start-of-doc"] = translate.start_of_doc,
["end-of-doc"] = translate, ["end-of-doc"] = translate.end_of_doc,
["start-of-line"] = translate, ["start-of-line"] = translate.start_of_line,
["end-of-line"] = translate, ["end-of-line"] = translate.end_of_line,
["start-of-word"] = translate, ["start-of-word"] = translate.start_of_word,
["start-of-indentation"] = translate, ["start-of-indentation"] = translate.start_of_indentation,
["end-of-word"] = translate, ["end-of-word"] = translate.end_of_word,
["previous-line"] = DocView.translate, ["previous-line"] = DocView.translate.previous_line,
["next-line"] = DocView.translate, ["next-line"] = DocView.translate.next_line,
["previous-page"] = DocView.translate, ["previous-page"] = DocView.translate.previous_page,
["next-page"] = DocView.translate, ["next-page"] = DocView.translate.next_page,
} }
for name, obj in pairs(translations) do for name, fn in pairs(translations) do
commands["doc:move-to-" .. name] = function(dv) dv.doc:move_to(obj[name:gsub("-", "_")], dv) end commands["doc:move-to-" .. name] = function() doc():move_to(fn, dv()) end
commands["doc:select-to-" .. name] = function(dv) dv.doc:select_to(obj[name:gsub("-", "_")], dv) end commands["doc:select-to-" .. name] = function() doc():select_to(fn, dv()) end
commands["doc:delete-to-" .. name] = function(dv) dv.doc:delete_to(obj[name:gsub("-", "_")], dv) end commands["doc:delete-to-" .. name] = function() doc():delete_to(fn, dv()) end
end end
commands["doc:move-to-previous-char"] = function(dv) commands["doc:move-to-previous-char"] = function()
for idx, line1, col1, line2, col2 in dv.doc:get_selections(true) do for idx, line1, col1, line2, col2 in doc():get_selections(true) do
if line1 ~= line2 or col1 ~= col2 then if line1 ~= line2 or col1 ~= col2 then
dv.doc:set_selections(idx, line1, col1) doc():set_selections(idx, line1, col1)
else
dv.doc:move_to_cursor(idx, translate.previous_char)
end end
end end
dv.doc:merge_cursors() doc():move_to(translate.previous_char)
end end
commands["doc:move-to-next-char"] = function(dv) commands["doc:move-to-next-char"] = function()
for idx, line1, col1, line2, col2 in dv.doc:get_selections(true) do for idx, line1, col1, line2, col2 in doc():get_selections(true) do
if line1 ~= line2 or col1 ~= col2 then if line1 ~= line2 or col1 ~= col2 then
dv.doc:set_selections(idx, line2, col2) doc():set_selections(idx, line2, col2)
else
dv.doc:move_to_cursor(idx, translate.next_char)
end end
end end
dv.doc:merge_cursors() doc():move_to(translate.next_char)
end end
command.add("core.docview", commands) command.add("core.docview", commands)

View File

@ -0,0 +1,16 @@
local command = require "core.command"
local config = require "core.config"
command.add(nil, {
["draw-whitespace:toggle"] = function()
config.draw_whitespace = not config.draw_whitespace
end,
["draw-whitespace:disable"] = function()
config.draw_whitespace = false
end,
["draw-whitespace:enable"] = function()
config.draw_whitespace = true
end,
})

View File

@ -4,13 +4,11 @@ local common = require "core.common"
command.add(nil, { command.add(nil, {
["files:create-directory"] = function() ["files:create-directory"] = function()
core.command_view:enter("New directory name", { core.command_view:enter("New directory name", function(text)
submit = function(text) local success, err, path = common.mkdirp(text)
local success, err, path = common.mkdirp(text) if not success then
if not success then core.error("cannot create directory %q: %s", path, err)
core.error("cannot create directory %q: %s", path, err)
end
end end
}) end)
end, end,
}) })

View File

@ -7,15 +7,15 @@ local DocView = require "core.docview"
local CommandView = require "core.commandview" local CommandView = require "core.commandview"
local StatusView = require "core.statusview" local StatusView = require "core.statusview"
local last_view, last_fn, last_text, last_sel local max_last_finds = 50
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 found_expression
local function doc() local function doc()
local is_DocView = core.active_view:is(DocView) and not core.active_view:is(CommandView) return core.active_view:is(DocView) and core.active_view.doc or last_view.doc
return is_DocView and core.active_view.doc or (last_view and last_view.doc)
end end
local function get_find_tooltip() local function get_find_tooltip()
@ -37,7 +37,7 @@ local function update_preview(sel, search_fn, text)
last_view:scroll_to_line(line2, true) last_view:scroll_to_line(line2, true)
found_expression = true found_expression = true
else else
last_view.doc:set_selection(table.unpack(sel)) last_view.doc:set_selection(unpack(sel))
found_expression = false found_expression = false
end end
end end
@ -46,91 +46,70 @@ end
local function insert_unique(t, v) local function insert_unique(t, v)
local n = #t local n = #t
for i = 1, n do for i = 1, n do
if t[i] == v then if t[i] == v then return end
table.remove(t, i)
break
end
end end
table.insert(t, 1, v) t[n + 1] = v
end end
local function find(label, search_fn) local function find(label, search_fn)
last_view, last_sel = 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 = last_view.doc:get_text(table.unpack(last_sel)) local text = last_view.doc:get_text(unpack(last_sel))
found_expression = false found_expression = false
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, { core.command_view:set_hidden_suggestions()
text = text, core.command_view:enter(label, function(text, item)
select_text = true, insert_unique(core.previous_find, text)
show_suggestions = false, core.status_view:remove_tooltip()
submit = function(text, item) if found_expression then
insert_unique(core.previous_find, text)
core.status_view:remove_tooltip()
if found_expression then
last_fn, last_text = search_fn, text
else
core.error("Couldn't find %q", text)
last_view.doc:set_selection(table.unpack(last_sel))
last_view:scroll_to_make_visible(table.unpack(last_sel))
end
end,
suggest = function(text)
update_preview(last_sel, search_fn, text)
last_fn, last_text = search_fn, text last_fn, last_text = search_fn, text
return core.previous_find else
end, core.error("Couldn't find %q", text)
cancel = function(explicit) last_view.doc:set_selection(unpack(last_sel))
core.status_view:remove_tooltip() last_view:scroll_to_make_visible(unpack(last_sel))
if explicit then
last_view.doc:set_selection(table.unpack(last_sel))
last_view:scroll_to_make_visible(table.unpack(last_sel))
end
end end
}) end, function(text)
update_preview(last_sel, search_fn, text)
last_fn, last_text = search_fn, text
return core.previous_find
end, function(explicit)
core.status_view:remove_tooltip()
if explicit then
last_view.doc:set_selection(unpack(last_sel))
last_view:scroll_to_make_visible(unpack(last_sel))
end
end)
end end
local function replace(kind, default, fn) local function replace(kind, default, fn)
core.status_view:show_tooltip(get_find_tooltip()) core.command_view:set_text(default, true)
core.command_view:enter("Find To Replace " .. kind, {
text = default,
select_text = true,
show_suggestions = false,
submit = function(old)
insert_unique(core.previous_find, old)
local s = string.format("Replace %s %q With", kind, old) core.status_view:show_tooltip(get_find_tooltip())
core.command_view:enter(s, { core.command_view:set_hidden_suggestions()
text = old, core.command_view:enter("Find To Replace " .. kind, function(old)
select_text = true, insert_unique(core.previous_find, old)
show_suggestions = false, core.command_view:set_text(old, true)
submit = function(new)
core.status_view:remove_tooltip() local s = string.format("Replace %s %q With", kind, old)
insert_unique(core.previous_replace, new) core.command_view:set_hidden_suggestions()
local results = doc():replace(function(text) core.command_view:enter(s, function(new)
return fn(text, old, new)
end)
local n = 0
for _,v in pairs(results) do
n = n + v
end
core.log("Replaced %d instance(s) of %s %q with %q", n, kind, old, new)
end,
suggest = function() return core.previous_replace end,
cancel = function()
core.status_view:remove_tooltip()
end
})
end,
suggest = function() return core.previous_find end,
cancel = function()
core.status_view:remove_tooltip() core.status_view:remove_tooltip()
end insert_unique(core.previous_replace, new)
}) local n = doc():replace(function(text)
return fn(text, old, new)
end)
core.log("Replaced %d instance(s) of %s %q with %q", n, kind, old, new)
end, function() return core.previous_replace end, function()
core.status_view:remove_tooltip()
end)
end, function() return core.previous_find end, function()
core.status_view:remove_tooltip()
end)
end end
local function has_selection() local function has_selection()
@ -138,7 +117,7 @@ local function has_selection()
end end
local function has_unique_selection() local function has_unique_selection()
if not doc() then return false end if not core.active_view:is(DocView) then return false end
local text = nil local text = nil
for idx, line1, col1, line2, col2 in doc():get_selections(true, true) do for idx, line1, col1, line2, col2 in doc():get_selections(true, true) do
if line1 == line2 and col1 == col2 then return false end if line1 == line2 and col1 == col2 then return false end
@ -163,17 +142,14 @@ local function is_in_any_selection(line, col)
return false return false
end end
local function select_add_next(all) local function select_next(all)
local il1, ic1 local il1, ic1 = doc():get_selection(true)
for _, l1, c1, l2, c2 in doc():get_selections(true, true) do for idx, l1, c1, l2, c2 in doc():get_selections(true, true) do
if not il1 then
il1, ic1 = l1, c1
end
local text = doc():get_text(l1, c1, l2, c2) local text = doc():get_text(l1, c1, l2, c2)
repeat repeat
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 l1 == il1 and c1 == ic1 then break end if l1 == il1 and c1 == ic1 then break end
if l2 and not is_in_any_selection(l2, c2) then if l2 and (all or not is_in_any_selection(l2, c2)) then
doc():add_selection(l2, c2, l1, c1) doc():add_selection(l2, c2, l1, c1)
if not all then if not all then
core.active_view:scroll_to_make_visible(l2, c2) core.active_view:scroll_to_make_visible(l2, c2)
@ -185,28 +161,21 @@ local function select_add_next(all)
end end
end end
local function select_next(reverse)
local l1, c1, l2, c2 = doc():get_selection(true)
local text = doc():get_text(l1, c1, l2, c2)
if reverse then
l1, c1, l2, c2 = search.find(doc(), l1, c1, text, { wrap = true, reverse = true })
else
l1, c1, l2, c2 = search.find(doc(), l2, c2, text, { wrap = true })
end
if l2 then doc():set_selection(l2, c2, l1, c1) end
end
command.add(has_unique_selection, { command.add(has_unique_selection, {
["find-replace:select-next"] = select_next, ["find-replace:select-next"] = function()
["find-replace:select-previous"] = function() select_next(true) end, local l1, c1, l2, c2 = doc():get_selection(true)
["find-replace:select-add-next"] = select_add_next, local text = doc():get_text(l1, c1, l2, c2)
["find-replace:select-add-all"] = function() select_add_next(true) end l1, c1, l2, c2 = search.find(doc(), l2, c2, text, { wrap = true })
if l2 then doc():set_selection(l2, c2, l1, c1) 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", {
["find-replace:find"] = function() ["find-replace:find"] = function()
find("Find Text", function(doc, line, col, text, case_sensitive, find_regex, find_reverse) find("Find Text", function(doc, line, col, text, case_sensitive, find_regex)
local opt = { wrap = true, no_case = not case_sensitive, regex = find_regex, reverse = find_reverse } local opt = { wrap = true, no_case = not case_sensitive, regex = find_regex }
return search.find(doc, line, col, text, opt) return search.find(doc, line, col, text, opt)
end) end)
end, end,
@ -219,7 +188,7 @@ command.add("core.docview!", {
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, "m"), text, new) local result, matches = regex.gsub(regex.compile(old, "m"), text, new)
return result, matches return result, #matches
end) end)
end, end,
@ -243,42 +212,38 @@ command.add("core.docview!", {
}) })
local function valid_for_finding() local function valid_for_finding()
-- Allow using this while in the CommandView return core.active_view:is(DocView) or core.active_view:is(CommandView)
if core.active_view:is(CommandView) and last_view then
return true, last_view
end
return core.active_view:is(DocView), core.active_view
end end
command.add(valid_for_finding, { command.add(valid_for_finding, {
["find-replace:repeat-find"] = function(dv) ["find-replace:repeat-find"] = function()
if not last_fn then if not last_fn then
core.error("No find to continue from") core.error("No find to continue from")
else else
local sl1, sc1, sl2, sc2 = dv.doc:get_selection(true) local sl1, sc1, sl2, sc2 = doc():get_selection(true)
local line1, col1, line2, col2 = last_fn(dv.doc, sl2, sc2, last_text, case_sensitive, find_regex, false) local line1, col1, line2, col2 = last_fn(doc(), sl1, sc2, last_text, case_sensitive, find_regex)
if line1 then if line1 then
dv.doc:set_selection(line2, col2, line1, col1) if last_view.doc ~= doc() then
dv:scroll_to_line(line2, true) last_finds = {}
else end
core.error("Couldn't find %q", last_text) if #last_finds >= max_last_finds then
table.remove(last_finds, 1)
end
table.insert(last_finds, { sl1, sc1, sl2, sc2 })
doc():set_selection(line2, col2, line1, col1)
last_view:scroll_to_line(line2, true)
end end
end end
end, end,
["find-replace:previous-find"] = function(dv) ["find-replace:previous-find"] = function()
if not last_fn then local sel = table.remove(last_finds)
core.error("No find to continue from") if not sel or doc() ~= last_view.doc then
else core.error("No previous finds")
local sl1, sc1, sl2, sc2 = dv.doc:get_selection(true) return
local line1, col1, line2, col2 = last_fn(dv.doc, sl1, sc1, last_text, case_sensitive, find_regex, true)
if line1 then
dv.doc:set_selection(line2, col2, line1, col1)
dv:scroll_to_line(line2, true)
else
core.error("Couldn't find %q", last_text)
end
end end
doc():set_selection(table.unpack(sel))
last_view:scroll_to_line(sel[3], true)
end, end,
}) })

View File

@ -1,16 +0,0 @@
local core = require "core"
local command = require "core.command"
command.add(nil, {
["log:open-as-doc"] = function()
local doc = core.open_doc("logs.txt")
core.root_view:open_doc(doc)
doc:insert(1, 1, core.get_log())
doc.new_file = false
doc:clean()
end,
["log:copy-to-clipboard"] = function()
system.set_clipboard(core.get_log())
end
})

View File

@ -3,15 +3,16 @@ 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 = {
["root:close"] = function(node) ["root:close"] = function()
local node = core.root_view:get_active_node()
node:close_active_view(core.root_view.root_node) node:close_active_view(core.root_view.root_node)
end, end,
["root:close-or-quit"] = function(node) ["root:close-or-quit"] = function()
local node = core.root_view:get_active_node()
if node and (not node:is_empty() or not node.is_primary_node) then if node and (not node:is_empty() or not node.is_primary_node) then
node:close_active_view(core.root_view.root_node) node:close_active_view(core.root_view.root_node)
else else
@ -28,22 +29,25 @@ local t = {
for i, v in ipairs(core.docs) do if v ~= active_doc then table.insert(docs, v) end end 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) core.confirm_close_docs(docs, core.root_view.close_all_docviews, core.root_view, true)
end, end,
["root:switch-to-previous-tab"] = function(node) ["root:switch-to-previous-tab"] = function()
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)
idx = idx - 1 idx = idx - 1
if idx < 1 then idx = #node.views end if idx < 1 then idx = #node.views end
node:set_active_view(node.views[idx]) node:set_active_view(node.views[idx])
end, end,
["root:switch-to-next-tab"] = function(node) ["root:switch-to-next-tab"] = function()
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)
idx = idx + 1 idx = idx + 1
if idx > #node.views then idx = 1 end if idx > #node.views then idx = 1 end
node:set_active_view(node.views[idx]) node:set_active_view(node.views[idx])
end, end,
["root:move-tab-left"] = function(node) ["root:move-tab-left"] = function()
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)
if idx > 1 then if idx > 1 then
table.remove(node.views, idx) table.remove(node.views, idx)
@ -51,7 +55,8 @@ local t = {
end end
end, end,
["root:move-tab-right"] = function(node) ["root:move-tab-right"] = function()
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)
if idx < #node.views then if idx < #node.views then
table.remove(node.views, idx) table.remove(node.views, idx)
@ -59,22 +64,25 @@ local t = {
end end
end, end,
["root:shrink"] = function(node) ["root:shrink"] = function()
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)
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,
["root:grow"] = function(node) ["root:grow"] = function()
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)
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,
} }
for i = 1, 9 do for i = 1, 9 do
t["root:switch-to-tab-" .. i] = function(node) t["root:switch-to-tab-" .. i] = function()
local node = core.root_view:get_active_node()
local view = node.views[i] local view = node.views[i]
if view then if view then
node:set_active_view(view) node:set_active_view(view)
@ -84,7 +92,8 @@ end
for _, dir in ipairs { "left", "right", "up", "down" } do for _, dir in ipairs { "left", "right", "up", "down" } do
t["root:split-" .. dir] = function(node) t["root:split-" .. dir] = function()
local node = core.root_view:get_active_node()
local av = node.active_view local av = node.active_view
node:split(dir) node:split(dir)
if av:is(DocView) then if av:is(DocView) then
@ -92,7 +101,8 @@ for _, dir in ipairs { "left", "right", "up", "down" } do
end end
end end
t["root:switch-to-" .. dir] = function(node) t["root:switch-to-" .. dir] = function()
local node = core.root_view:get_active_node()
local x, y local x, y
if dir == "left" or dir == "right" then if dir == "left" or dir == "right" then
y = node.position.y + node.size.y / 2 y = node.position.y + node.size.y / 2
@ -102,8 +112,7 @@ for _, dir in ipairs { "left", "right", "up", "down" } do
y = node.position.y + (dir == "up" and -1 or node.size.y + style.divider_size) y = node.position.y + (dir == "up" and -1 or node.size.y + style.divider_size)
end end
local node = core.root_view.root_node:get_child_overlapping_point(x, y) local node = core.root_view.root_node:get_child_overlapping_point(x, y)
local sx, sy = node:get_locked_size() if not node:get_locked_size() then
if not sx and not sy then
core.set_active_view(node.active_view) core.set_active_view(node.active_view)
end end
end end
@ -111,25 +120,5 @@ end
command.add(function() command.add(function()
local node = core.root_view:get_active_node() local node = core.root_view:get_active_node()
local sx, sy = node:get_locked_size() return not node:get_locked_size()
return not sx and not sy, node
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,
["root:horizontal-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.x = view.scroll.to.x + delta * -config.mouse_wheel_scroll
return true
end
return false
end
})

View File

@ -1,71 +0,0 @@
local core = require "core"
local command = require "core.command"
local common = require "core.common"
local style = require "core.style"
local StatusView = require "core.statusview"
local function status_view_item_names()
local items = core.status_view:get_items_list()
local names = {}
for _, item in ipairs(items) do
table.insert(names, item.name)
end
return names
end
local function status_view_items_data(names)
local data = {}
for _, name in ipairs(names) do
local item = core.status_view:get_item(name)
table.insert(data, {
text = command.prettify_name(item.name),
info = item.alignment == StatusView.Item.LEFT and "Left" or "Right",
name = item.name
})
end
return data
end
local function status_view_get_items(text)
local names = status_view_item_names()
local results = common.fuzzy_match(names, text)
results = status_view_items_data(results)
return results
end
command.add(nil, {
["status-bar:toggle"] = function()
core.status_view:toggle()
end,
["status-bar:show"] = function()
core.status_view:show()
end,
["status-bar:hide"] = function()
core.status_view:hide()
end,
["status-bar:disable-messages"] = function()
core.status_view:display_messages(false)
end,
["status-bar:enable-messages"] = function()
core.status_view:display_messages(true)
end,
["status-bar:hide-item"] = function()
core.command_view:enter("Status bar item to hide", {
submit = function(text, item)
core.status_view:hide_items(item.name)
end,
suggest = status_view_get_items
})
end,
["status-bar:show-item"] = function()
core.command_view:enter("Status bar item to show", {
submit = function(text, item)
core.status_view:show_items(item.name)
end,
suggest = status_view_get_items
})
end,
["status-bar:reset-items"] = function()
core.status_view:show_items()
end,
})

View File

@ -6,16 +6,13 @@ local DocView = require "core.docview"
local View = require "core.view" local View = require "core.view"
---@class core.commandview.input : core.doc
---@field super core.doc
local SingleLineDoc = Doc:extend() local SingleLineDoc = Doc:extend()
function SingleLineDoc:insert(line, col, text) function SingleLineDoc:insert(line, col, text)
SingleLineDoc.super.insert(self, line, col, text:gsub("\n", "")) SingleLineDoc.super.insert(self, line, col, text:gsub("\n", ""))
end end
---@class core.commandview : core.docview
---@field super core.docview
local CommandView = DocView:extend() local CommandView = DocView:extend()
CommandView.context = "application" CommandView.context = "application"
@ -24,37 +21,21 @@ local max_suggestions = 10
local noop = function() end local noop = function() end
---@class core.commandview.state
---@field submit function
---@field suggest function
---@field cancel function
---@field validate function
---@field text string
---@field select_text boolean
---@field show_suggestions boolean
---@field typeahead boolean
---@field wrap boolean
local default_state = { local default_state = {
submit = noop, submit = noop,
suggest = noop, suggest = noop,
cancel = noop, cancel = noop,
validate = function() return true end, validate = function() return true end
text = "",
select_text = false,
show_suggestions = true,
typeahead = true,
wrap = true,
} }
function CommandView:new() function CommandView:new()
CommandView.super.new(self, SingleLineDoc()) CommandView.super.new(self, SingleLineDoc())
self.suggestion_idx = 1 self.suggestion_idx = 1
self.suggestions_offset = 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.last_text = ""
self.gutter_width = 0 self.gutter_width = 0
self.gutter_text_brightness = 0 self.gutter_text_brightness = 0
self.selection_offset = 0 self.selection_offset = 0
@ -65,10 +46,8 @@ function CommandView:new()
end end
---@deprecated
function CommandView:set_hidden_suggestions() function CommandView:set_hidden_suggestions()
core.warn("Using deprecated function CommandView:set_hidden_suggestions") self.show_suggestions = false
self.state.show_suggestions = false
end end
@ -77,8 +56,8 @@ function CommandView:get_name()
end end
function CommandView:get_line_screen_position(line, col) function CommandView:get_line_screen_position()
local x = CommandView.super.get_line_screen_position(self, 1, col) local x = CommandView.super.get_line_screen_position(self, 1)
local _, y = self:get_content_offset() local _, y = self:get_content_offset()
local lh = self:get_line_height() local lh = self:get_line_height()
return x, y + (self.size.y - lh) / 2 return x, y + (self.size.y - lh) / 2
@ -89,10 +68,6 @@ function CommandView:get_scrollable_size()
return 0 return 0
end end
function CommandView:get_h_scrollable_size()
return 0
end
function CommandView:scroll_to_make_visible() function CommandView:scroll_to_make_visible()
-- no-op function to disable this functionality -- no-op function to disable this functionality
@ -105,7 +80,6 @@ end
function CommandView:set_text(text, select) function CommandView:set_text(text, select)
self.last_text = text
self.doc:remove(1, 1, math.huge, math.huge) self.doc:remove(1, 1, math.huge, math.huge)
self.doc:text_input(text) self.doc:text_input(text)
if select then if select then
@ -115,36 +89,9 @@ end
function CommandView:move_suggestion_idx(dir) function CommandView:move_suggestion_idx(dir)
local function overflow_suggestion_idx(n, count) if self.show_suggestions then
if count == 0 then return 0 end
if self.state.wrap then
return (n - 1) % count + 1
else
return common.clamp(n, 1, count)
end
end
local function get_suggestions_offset()
local max_visible = math.min(max_suggestions, #self.suggestions)
if dir > 0 then
if self.suggestions_offset + max_visible < self.suggestion_idx + 1 then
return self.suggestion_idx - max_visible + 1
elseif self.suggestions_offset > self.suggestion_idx then
return self.suggestion_idx
end
else
if self.suggestions_offset > self.suggestion_idx then
return self.suggestion_idx
elseif self.suggestions_offset + max_visible < self.suggestion_idx + 1 then
return self.suggestion_idx - max_visible + 1
end
end
return self.suggestions_offset
end
if self.state.show_suggestions then
local n = self.suggestion_idx + dir local n = self.suggestion_idx + dir
self.suggestion_idx = overflow_suggestion_idx(n, #self.suggestions) self.suggestion_idx = common.clamp(n, 1, #self.suggestions)
self:complete() self:complete()
self.last_change_id = self.doc:get_change_id() self.last_change_id = self.doc:get_change_id()
else else
@ -155,7 +102,7 @@ function CommandView:move_suggestion_idx(dir)
if n == 0 and self.save_suggestion then if n == 0 and self.save_suggestion then
self:set_text(self.save_suggestion) self:set_text(self.save_suggestion)
else else
self.suggestion_idx = overflow_suggestion_idx(n, #self.suggestions) self.suggestion_idx = common.clamp(n, 1, #self.suggestions)
self:complete() self:complete()
end end
else else
@ -165,8 +112,6 @@ function CommandView:move_suggestion_idx(dir)
self.last_change_id = self.doc:get_change_id() self.last_change_id = self.doc:get_change_id()
self.state.suggest(self:get_text()) self.state.suggest(self:get_text())
end end
self.suggestions_offset = get_suggestions_offset()
end end
@ -180,60 +125,28 @@ end
function CommandView:submit() function CommandView:submit()
local suggestion = self.suggestions[self.suggestion_idx] local suggestion = self.suggestions[self.suggestion_idx]
local text = self:get_text() local text = self:get_text()
if self.state.validate(text, suggestion) then if self.state.validate(text) then
local submit = self.state.submit local submit = self.state.submit
self:exit(true) self:exit(true)
submit(text, suggestion) submit(text, suggestion)
end end
end end
---@param label string
---@varargs any function CommandView:enter(text, submit, suggest, cancel, validate)
---@overload fun(label:string, options: core.commandview.state)
function CommandView:enter(label, ...)
if self.state ~= default_state then if self.state ~= default_state then
return return
end end
local options = select(1, ...) self.state = {
submit = submit or noop,
if type(options) ~= "table" then suggest = suggest or noop,
core.warn("Using CommandView:enter in a deprecated way") cancel = cancel or noop,
local submit, suggest, cancel, validate = ... validate = validate or function() return true end
options = { }
submit = submit,
suggest = suggest,
cancel = cancel,
validate = validate,
}
end
-- Support deprecated CommandView:set_hidden_suggestions
-- Remove this when set_hidden_suggestions is not supported anymore
if options.show_suggestions == nil then
options.show_suggestions = self.state.show_suggestions
end
self.state = common.merge(default_state, options)
-- We need to keep the text entered with CommandView:set_text to
-- maintain compatibility with deprecated usage, but still allow
-- overwriting with options.text
local old_text = self:get_text()
if old_text ~= "" then
core.warn("Using deprecated function CommandView:set_text")
end
if options.text or options.select_text then
local text = options.text or old_text
self:set_text(text, self.state.select_text)
end
-- Replace with a simple
-- self:set_text(self.state.text, self.state.select_text)
-- once old usage is removed
core.set_active_view(self) core.set_active_view(self)
self:update_suggestions() self:update_suggestions()
self.gutter_text_brightness = 100 self.gutter_text_brightness = 100
self.label = label .. ": " self.label = text .. ": "
end end
@ -246,13 +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 self.save_suggestion = nil
self.last_text = ""
end
function CommandView:get_line_height()
return math.floor(self:get_font():get_height() * 1.2)
end end
@ -277,7 +185,6 @@ function CommandView:update_suggestions()
end end
self.suggestions = res self.suggestions = res
self.suggestion_idx = 1 self.suggestion_idx = 1
self.suggestions_offset = 1
end end
@ -291,45 +198,35 @@ function CommandView:update()
-- update suggestions if text has changed -- update suggestions if text has changed
if self.last_change_id ~= self.doc:get_change_id() then if self.last_change_id ~= self.doc:get_change_id() then
self:update_suggestions() self:update_suggestions()
if self.state.typeahead and self.suggestions[self.suggestion_idx] then
local current_text = self:get_text()
local suggested_text = self.suggestions[self.suggestion_idx].text or ""
if #self.last_text < #current_text and
string.find(suggested_text, current_text, 1, true) == 1 then
self:set_text(suggested_text)
self.doc:set_selection(1, #current_text + 1, 1, math.huge)
end
self.last_text = current_text
end
self.last_change_id = self.doc:get_change_id() self.last_change_id = self.doc:get_change_id()
end end
-- update gutter text color brightness -- update gutter text color brightness
self:move_towards("gutter_text_brightness", 0, 0.1, "commandview") self:move_towards("gutter_text_brightness", 0, 0.1)
-- update gutter width -- update gutter width
local dest = self:get_font():get_width(self.label) + style.padding.x local dest = self:get_font():get_width(self.label) + style.padding.x
if self.size.y <= 0 then if self.size.y <= 0 then
self.gutter_width = dest self.gutter_width = dest
else else
self:move_towards("gutter_width", dest, nil, "commandview") self:move_towards("gutter_width", dest)
end end
-- update suggestions box height -- update suggestions box height
local lh = self:get_suggestion_line_height() local lh = self:get_suggestion_line_height()
local dest = self.state.show_suggestions and math.min(#self.suggestions, max_suggestions) * lh or 0 local dest = self.show_suggestions and math.min(#self.suggestions, max_suggestions) * lh or 0
self:move_towards("suggestions_height", dest, nil, "commandview") self:move_towards("suggestions_height", dest)
-- update suggestion cursor offset -- update suggestion cursor offset
local dest = (self.suggestion_idx - self.suggestions_offset + 1) * self:get_suggestion_line_height() local dest = math.min(self.suggestion_idx, max_suggestions) * self:get_suggestion_line_height()
self:move_towards("selection_offset", dest, nil, "commandview") self:move_towards("selection_offset", dest)
-- update size based on whether this is the active_view -- update size based on whether this is the active_view
local dest = 0 local dest = 0
if self == core.active_view then if self == core.active_view then
dest = style.font:get_height() + style.padding.y * 2 dest = style.font:get_height() + style.padding.y * 2
end end
self:move_towards(self.size, "y", dest, nil, "commandview") self:move_towards(self.size, "y", dest)
end end
@ -346,7 +243,6 @@ function CommandView:draw_line_gutter(idx, x, y)
x = x + style.padding.x x = x + style.padding.x
renderer.draw_text(self:get_font(), self.label, x, y + yoffset, color) renderer.draw_text(self:get_font(), self.label, x, y + yoffset, color)
core.pop_clip_rect() core.pop_clip_rect()
return self:get_line_height()
end end
@ -357,7 +253,6 @@ local function draw_suggestions_box(self)
local h = math.ceil(self.suggestions_height) local h = math.ceil(self.suggestions_height)
local rx, ry, rw, rh = self.position.x, self.position.y - h - dh, self.size.x, h local rx, ry, rw, rh = self.position.x, self.position.y - h - dh, self.size.x, h
core.push_clip_rect(rx, ry, rw, rh)
-- draw suggestions background -- draw suggestions background
if #self.suggestions > 0 then if #self.suggestions > 0 then
renderer.draw_rect(rx, ry, rw, rh, style.background3) renderer.draw_rect(rx, ry, rw, rh, style.background3)
@ -367,18 +262,20 @@ local function draw_suggestions_box(self)
end end
-- draw suggestion text -- draw suggestion text
local first = math.max(self.suggestions_offset, 1) local suggestion_offset = math.max(self.suggestion_idx - max_suggestions, 0)
local last = math.min(self.suggestions_offset + max_suggestions, #self.suggestions) core.push_clip_rect(rx, ry, rw, rh)
for i=first, last do local i = 1 + suggestion_offset
while i <= #self.suggestions do
local item = self.suggestions[i] local item = self.suggestions[i]
local color = (i == self.suggestion_idx) and style.accent or style.text local color = (i == self.suggestion_idx) and style.accent or style.text
local y = self.position.y - (i - first + 1) * lh - dh local y = self.position.y - (i - suggestion_offset) * lh - dh
common.draw_text(self:get_font(), color, item.text, nil, x, y, 0, lh) common.draw_text(self:get_font(), color, item.text, nil, x, y, 0, lh)
if item.info then if item.info then
local w = self.size.x - x - style.padding.x local w = self.size.x - x - style.padding.x
common.draw_text(self:get_font(), style.dim, item.info, "right", x, y, w, lh) common.draw_text(self:get_font(), style.dim, item.info, "right", x, y, w, lh)
end end
i = i + 1
end end
core.pop_clip_rect() core.pop_clip_rect()
end end
@ -386,7 +283,7 @@ end
function CommandView:draw() function CommandView:draw()
CommandView.super.draw(self) CommandView.super.draw(self)
if self.state.show_suggestions then if self.show_suggestions then
core.root_view:defer_draw(draw_suggestions_box, self) core.root_view:defer_draw(draw_suggestions_box, self)
end end
end end

View File

@ -1,69 +1,27 @@
local common = {} local common = {}
---Checks if the byte at offset is a UTF-8 continuation byte. function common.is_utf8_cont(char)
--- local byte = char:byte()
---UTF-8 encodes code points in 1 to 4 bytes.
---For a multi-byte sequence, each byte following the start byte is a continuation byte.
---@param s string
---@param offset? integer The offset of the string to start searching. Defaults to 1.
---@return boolean
function common.is_utf8_cont(s, offset)
local byte = s:byte(offset or 1)
return byte >= 0x80 and byte < 0xc0 return byte >= 0x80 and byte < 0xc0
end end
---Returns an iterator that yields a UTF-8 character on each iteration.
---@param text string
---@return fun(): string
function common.utf8_chars(text) function common.utf8_chars(text)
return text:gmatch("[\0-\x7f\xc2-\xf4][\x80-\xbf]*") return text:gmatch("[\0-\x7f\xc2-\xf4][\x80-\xbf]*")
end end
---Clamps the number n between lo and hi.
---@param n number
---@param lo number
---@param hi number
---@return number
function common.clamp(n, lo, hi) function common.clamp(n, lo, hi)
return math.max(math.min(n, hi), lo) return math.max(math.min(n, hi), lo)
end end
---Returns a new table containing the contents of b merged into a.
---@param a table|nil
---@param b table?
---@return table
function common.merge(a, b)
a = type(a) == "table" and a or {}
local t = {}
for k, v in pairs(a) do
t[k] = v
end
if b and type(b) == "table" then
for k, v in pairs(b) do
t[k] = v
end
end
return t
end
---Returns the value of a number rounded to the nearest integer.
---@param n number
---@return number
function common.round(n) function common.round(n)
return n >= 0 and math.floor(n + 0.5) or math.ceil(n - 0.5) return n >= 0 and math.floor(n + 0.5) or math.ceil(n - 0.5)
end end
---Returns the first index where a subtable in tbl has prop set.
---If none is found, nil is returned.
---@param tbl table
---@param prop any
---@return number|nil
function common.find_index(tbl, prop) function common.find_index(tbl, prop)
for i, o in ipairs(tbl) do for i, o in ipairs(tbl) do
if o[prop] then return i end if o[prop] then return i end
@ -71,16 +29,6 @@ function common.find_index(tbl, prop)
end end
---Returns a value between a and b on a linear scale, based on the
---interpolation point t.
---
---If a and b are tables, a table containing the result for all the
---elements in a and b is returned.
---@param a number
---@param b number
---@param t number
---@return number
---@overload fun(a: table, b: table, t: number): table
function common.lerp(a, b, t) function common.lerp(a, b, t)
if type(a) ~= "table" then if type(a) ~= "table" then
return a + (b - a) * t return a + (b - a) * t
@ -93,64 +41,51 @@ function common.lerp(a, b, t)
end end
---Returns the euclidean distance between two points.
---@param x1 number
---@param y1 number
---@param x2 number
---@param y2 number
---@return number
function common.distance(x1, y1, x2, y2) function common.distance(x1, y1, x2, y2)
return math.sqrt(((x2-x1) ^ 2)+((y2-y1) ^ 2)) return math.sqrt(math.pow(x2-x1, 2)+math.pow(y2-y1, 2))
end end
---Parses a CSS color string.
---
---Only these formats are supported:
---* `rgb(r, g, b)`
---* `rgba(r, g, b, a)`
---* `#rrggbbaa`
---* `#rrggbb`
---@param str string
---@return number r
---@return number g
---@return number b
---@return number a
function common.color(str) function common.color(str)
local r, g, b, a = str:match("^#(%x%x)(%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
r = tonumber(r, 16) r = tonumber(r, 16)
g = tonumber(g, 16) g = tonumber(g, 16)
b = tonumber(b, 16) b = tonumber(b, 16)
a = tonumber(a, 16) or 0xff a = 1
elseif str:match("rgba?%s*%([%d%s%.,]+%)") then elseif str:match("rgba?%s*%([%d%s%.,]+%)") then
local f = str:gmatch("[%d.]+") local f = str:gmatch("[%d.]+")
r = (f() or 0) r = (f() or 0)
g = (f() or 0) g = (f() or 0)
b = (f() or 0) b = (f() or 0)
a = (f() or 1) * 0xff a = f() or 1
else else
error(string.format("bad color string '%s'", str)) error(string.format("bad color string '%s'", str))
end end
return r, g, b, a return r, g, b, a * 0xff
end end
---Splices a numerically indexed table.
---This function mutates the original table.
---@param t any[]
---@param at number Index at which to start splicing.
---@param remove number Number of elements to remove.
---@param insert? any[] A table containing elements to insert after splicing.
function common.splice(t, at, remove, insert) function common.splice(t, at, remove, insert)
assert(remove >= 0, "bad argument #3 to 'splice' (non-negative value expected)")
insert = insert or {} insert = insert or {}
local len = #insert local offset = #insert - remove
if remove ~= len then table.move(t, at + remove, #t + remove, at + len) end local old_len = #t
table.move(insert, 1, len, at, t) if offset < 0 then
for i = at - offset, old_len - offset do
t[i + offset] = t[i]
end
elseif offset > 0 then
for i = old_len, at, -1 do
t[i + offset] = t[i]
end
end
for i, item in ipairs(insert) do
t[at + i - 1] = item
end
end end
local function compare_score(a, b) local function compare_score(a, b)
return a.score > b.score return a.score > b.score
end end
@ -171,16 +106,6 @@ local function fuzzy_match_items(items, needle, files)
end end
---Performs fuzzy matching.
---
---If the haystack is a string, a score ranging from 0 to 1 is returned. </br>
---If the haystack is a table, a table containing the haystack sorted in ascending
---order of similarity is returned.
---@param haystack string
---@param needle string
---@param files boolean If true, the matching process will be performed in reverse to better match paths.
---@return number
---@overload fun(haystack: string[], needle: string, files: boolean): string[]
function common.fuzzy_match(haystack, needle, files) function common.fuzzy_match(haystack, needle, files)
if type(haystack) == "table" then if type(haystack) == "table" then
return fuzzy_match_items(haystack, needle, files) return fuzzy_match_items(haystack, needle, files)
@ -189,14 +114,6 @@ function common.fuzzy_match(haystack, needle, files)
end end
---Performs fuzzy matching and returns recently used strings if needed.
---
---If the needle is empty, then a list of recently used strings
---are added to the result, followed by strings from the haystack.
---@param haystack string[]
---@param recents string[]
---@param needle string
---@return string[]
function common.fuzzy_match_with_recents(haystack, recents, needle) function common.fuzzy_match_with_recents(haystack, recents, needle)
if needle == "" then if needle == "" then
local recents_ext = {} local recents_ext = {}
@ -215,42 +132,9 @@ function common.fuzzy_match_with_recents(haystack, recents, needle)
end end
---Returns a list of paths that are relative to the input path. function common.path_suggest(text)
--- local path, name = text:match("^(.-)([^/\\]*)$")
---If a root directory is specified, the function returns paths local files = system.list_dir(path == "" and "." or path) or {}
---that are relative to the root directory.
---@param text string The input path.
---@param root? string The root directory.
---@return string[]
function common.path_suggest(text, root)
if root and root:sub(-1) ~= PATHSEP then
root = root .. PATHSEP
end
local path, name
if (PLATFORM == "AmigaOS 4" or PLATFORM == "MorphOS") then
path, name = text:match("^(.-)([^:"..PATHSEP.."]*)$")
else
path, name = text:match("^(.-)([^"..PATHSEP.."]*)$")
end
local clean_dotslash = false
-- ignore root if path is absolute
local is_absolute = common.is_absolute_path(text)
if not is_absolute then
if path == "" then
path = root or "."
clean_dotslash = not root
else
path = (root or "") .. path
end
end
-- Only in Windows allow using both styles of PATHSEP
if (PATHSEP == "\\" and not string.match(path:sub(-1), "[\\/]")) or
(PATHSEP ~= "\\" and path:sub(-1) ~= PATHSEP) then
path = path .. PATHSEP
end
local files = system.list_dir(path) or {}
local res = {} local res = {}
for _, file in ipairs(files) do for _, file in ipairs(files) do
file = path .. file file = path .. file
@ -259,19 +143,6 @@ function common.path_suggest(text, root)
if info.type == "dir" then if info.type == "dir" then
file = file .. PATHSEP file = file .. PATHSEP
end end
if root then
-- remove root part from file path
local s, e = file:find(root, nil, true)
if s == 1 then
file = file:sub(e + 1)
end
elseif clean_dotslash then
-- remove added dot slash
local s, e = file:find("." .. PATHSEP, nil, true)
if s == 1 then
file = file:sub(e + 1)
end
end
if file:lower():find(text:lower(), nil, true) == 1 then if file:lower():find(text:lower(), nil, true) == 1 then
table.insert(res, file) table.insert(res, file)
end end
@ -281,16 +152,8 @@ function common.path_suggest(text, root)
end end
---Returns a list of directories that are related to a path.
---@param text string The input path.
---@return string[]
function common.dir_path_suggest(text) function common.dir_path_suggest(text)
local path, name local path, name = text:match("^(.-)([^/\\]*)$")
if (PLATFORM == "AmigaOS 4" or PLATFORM == "MorphOS") then
path, name = text:match("^(.-)([^:"..PATHSEP.."]*)$")
else
path, name = text:match("^(.-)([^"..PATHSEP.."]*)$")
end
local files = system.list_dir(path == "" and "." or path) or {} local files = system.list_dir(path == "" and "." or path) or {}
local res = {} local res = {}
for _, file in ipairs(files) do for _, file in ipairs(files) do
@ -304,18 +167,8 @@ function common.dir_path_suggest(text)
end end
---Filters a list of paths to find those that are related to the input path.
---@param text string The input path.
---@param dir_list string[] A list of paths to filter.
---@return string[]
function common.dir_list_suggest(text, dir_list) function common.dir_list_suggest(text, dir_list)
local path, name local path, name = text:match("^(.-)([^/\\]*)$")
if (PLATFORM == "AmigaOS 4" or PLATFORM == "MorphOS") then
path, name = text:match("^(.-)([^:"..PATHSEP.."]*)$")
else
path, name = text:match("^(.-)([^"..PATHSEP.."]*)$")
end
local res = {} local res = {}
for _, dir_path in ipairs(dir_list) do for _, dir_path in ipairs(dir_list) do
if dir_path:lower():find(text:lower(), nil, true) == 1 then if dir_path:lower():find(text:lower(), nil, true) == 1 then
@ -326,15 +179,6 @@ function common.dir_list_suggest(text, dir_list)
end end
---Matches a string against a list of patterns.
---
---If a match was found, its start and end index is returned.
---Otherwise, false is returned.
---@param text string
---@param pattern string|string[]
---@param ... any Other options for string.find().
---@return number|boolean start_index
---@return number|nil end_index
function common.match_pattern(text, pattern, ...) function common.match_pattern(text, pattern, ...)
if type(pattern) == "string" then if type(pattern) == "string" then
return text:find(pattern, ...) return text:find(pattern, ...)
@ -347,24 +191,8 @@ function common.match_pattern(text, pattern, ...)
end end
---Draws text onto the window.
---The function returns the X and Y coordinates of the bottom-right
---corner of the text.
---@param font renderer.font
---@param color renderer.color
---@param text string
---@param align string
---| '"left"' # Align text to the left of the bounding box
---| '"right"' # Align text to the right of the bounding box
---| '"center"' # Center text in the bounding box
---@param x number
---@param y number
---@param w number
---@param h number
---@return number x_advance
---@return number y_advance
function common.draw_text(font, color, text, align, x,y,w,h) function common.draw_text(font, color, text, align, x,y,w,h)
local tw, th = font:get_width(text), font:get_height() local tw, th = font:get_width(text), font:get_height(text)
if align == "center" then if align == "center" then
x = x + (w - tw) / 2 x = x + (w - tw) / 2
elseif align == "right" then elseif align == "right" then
@ -375,16 +203,6 @@ function common.draw_text(font, color, text, align, x,y,w,h)
end end
---Prints the execution time of a function.
---
---The execution time and percentage of frame time
---for the function is printed to standard output. </br>
---The frame rate is always assumed to be 60 FPS, thus
---a value of 100% would mean that the benchmark took
---1/60 of a second to execute.
---@param name string
---@param fn fun(...: any): any
---@return any # The result returned by the function
function common.bench(name, fn, ...) function common.bench(name, fn, ...)
local start = system.get_time() local start = system.get_time()
local res = fn(...) local res = fn(...)
@ -395,129 +213,34 @@ function common.bench(name, fn, ...)
return res return res
end end
-- From gvx/Ser
local oddvals = {[tostring(1/0)] = "1/0", [tostring(-1/0)] = "-1/0", [tostring(-(0/0))] = "-(0/0)", [tostring(0/0)] = "0/0"}
local function serialize(val, pretty, indent_str, escape, sort, limit, level) function common.serialize(val)
local space = pretty and " " or "" if type(val) == "string" then
local indent = pretty and string.rep(indent_str, level) or "" return string.format("%q", val)
local newline = pretty and "\n" or "" elseif type(val) == "table" then
local ty = type(val)
if ty == "string" then
local out = string.format("%q", val)
if escape then
out = string.gsub(out, "\\\n", "\\n")
out = string.gsub(out, "\\7", "\\a")
out = string.gsub(out, "\\8", "\\b")
out = string.gsub(out, "\\9", "\\t")
out = string.gsub(out, "\\11", "\\v")
out = string.gsub(out, "\\12", "\\f")
out = string.gsub(out, "\\13", "\\r")
end
return out
elseif ty == "table" then
-- early exit
if level >= limit then return tostring(val) end
local next_indent = pretty and (indent .. indent_str) or ""
local t = {} local t = {}
for k, v in pairs(val) do for k, v in pairs(val) do
table.insert(t, table.insert(t, "[" .. common.serialize(k) .. "]=" .. common.serialize(v))
next_indent .. "[" ..
serialize(k, pretty, indent_str, escape, sort, limit, level + 1) ..
"]" .. space .. "=" .. space .. serialize(v, pretty, indent_str, escape, sort, limit, level + 1))
end end
if #t == 0 then return "{}" end return "{" .. table.concat(t, ",") .. "}"
if sort then table.sort(t) end
return "{" .. newline .. table.concat(t, "," .. newline) .. newline .. indent .. "}"
end
if ty == "number" then
-- tostring is locale-dependent, so we need to replace an eventual `,` with `.`
local res, _ = tostring(val):gsub(",", ".")
-- handle inf/nan
return oddvals[res] or res
end end
return tostring(val) return tostring(val)
end end
---@class common.serializeoptions
---@field pretty boolean Enables pretty printing.
---@field indent_str string The indentation character to use. Defaults to `" "`.
---@field escape boolean Uses normal escape characters ("\n") instead of decimal escape sequences ("\10").
---@field limit number Limits the depth when serializing nested tables. Defaults to `math.huge`.
---@field sort boolean Sorts the output if it is a sortable table.
---@field initial_indent number The initial indentation level. Defaults to 0.
---Serializes a value into a Lua string that is loadable with load().
---
---Only these basic types are supported:
---* nil
---* boolean
---* number (except very large numbers and special constants, e.g. `math.huge`, `inf` and `nan`)
---* integer
---* string
---* table
---
---@param val any
---@param opts? common.serializeoptions
---@return string
function common.serialize(val, opts)
opts = opts or {}
local indent_str = opts.indent_str or " "
local initial_indent = opts.initial_indent or 0
local indent = opts.pretty and string.rep(indent_str, initial_indent) or ""
local limit = (opts.limit or math.huge) + initial_indent
return indent .. serialize(val, opts.pretty, indent_str,
opts.escape, opts.sort, limit, initial_indent)
end
---Returns the last portion of a path.
---@param path string
---@return string
function common.basename(path) function common.basename(path)
-- a path should never end by / or \ except if it is '/' (unix root) or -- a path should never end by / or \ except if it is '/' (unix root) or
-- 'X:\' (windows drive) -- 'X:\' (windows drive)
return path:match("[^"..PATHSEP.."]+$") or path return path:match("[^\\/]+$") or path
end end
---Returns the base path with the pathsep, if needed. -- can return nil if there is no directory part in the path
---@param path string
---@return string
function common.basepath(path)
-- Check for AmigaOS 4 and MorphOS if the last character is semicolon
-- In these systems the volume name doesn't have a / or \ after the name
-- but it is like VOLUME:
if (PLATFORM == "AmigaOS 4" or PLATFORM == "MorphOS") and (string.sub(path, -1) == ":") then
return path
end
return path .. PATHSEP
end
---Returns the directory name of a path.
---If the path doesn't have a directory, this function may return nil.
---@param path string
---@return string|nil
function common.dirname(path) function common.dirname(path)
if (PLATFORM == "AmigaOS 4" or PLATFORM == "MorphOS") then return path:match("(.+)[\\/][^\\/]+$")
local drive, relpath = path:match('^([%w%s]*:)(.+)')
if drive and relpath then
local dir = relpath:match("(.+)["..PATHSEP.."][^"..PATHSEP.."]+$")
if dir then
return drive .. dir
end
end
return path
end
return path:match("(.+)["..PATHSEP.."][^"..PATHSEP.."]+$")
end end
---Returns a path where the user's home directory is replaced by `"~"`.
---@param text string
---@return string
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
@ -531,9 +254,6 @@ function common.home_encode(text)
end end
---Returns a list of paths where the user's home directory is replaced by `"~"`.
---@param paths string[] A list of paths to encode
---@return string[]
function common.home_encode_list(paths) function common.home_encode_list(paths)
local t = {} local t = {}
for i = 1, #paths do for i = 1, #paths do
@ -543,145 +263,56 @@ function common.home_encode_list(paths)
end end
---Expands the `"~"` prefix in a path into the user's home directory.
---This function is not guaranteed to return an absolute path.
---@param text string
---@return string
function common.home_expand(text) function common.home_expand(text)
if text == nil then
return HOME
end
return HOME and text:gsub("^~", HOME) or text return HOME and text:gsub("^~", HOME) or text
end end
local function split_on_slash(s, sep_pattern) local function split_on_slash(s, sep_pattern)
local t = {} local t = {}
if s:match("^["..PATHSEP.."]") then if s:match("^[/\\]") then
t[#t + 1] = "" t[#t + 1] = ""
end end
if (PLATFORM == "AmigaOS 4" or PLATFORM == "MorphOS") then for fragment in string.gmatch(s, "([^/\\]+)") do
local drive = s:match("^([%w%s]*:)")
if drive then
t[#t + 1] = ""
s = s:gsub("^" .. drive, "")
end
end
for fragment in string.gmatch(s, "([^"..PATHSEP.."]+)") do
t[#t + 1] = fragment t[#t + 1] = fragment
end end
return t return t
end end
---Normalizes the drive letter in a Windows path to uppercase.
---This function expects an absolute path, e.g. a path from `system.absolute_path`.
---
---This function is needed because the path returned by `system.absolute_path`
---may contain drive letters in upper or lowercase.
---@param filename string|nil The input path.
---@return string|nil
function common.normalize_volume(filename)
if not filename then return end
if PATHSEP == '\\' then
local drive, rem = filename:match('^([a-zA-Z]:\\)(.-)'..PATHSEP..'?$')
if drive then
return drive:upper() .. rem
end
end
if (PLATFORM == "AmigaOS 4" or PLATFORM == "MorphOS") then
local drive, rem = filename:match('^([%w%s]*:)(.-)' .. PATHSEP .. '?$')
if drive then
return drive .. rem
end
end
return filename
end
---Normalizes a path into the same format across platforms.
---
---On Windows, all drive letters are converted to uppercase.
---UNC paths with drive letters are converted back to ordinary Windows paths.
---All path separators (`"/"`, `"\\"`) are converted to platform-specific ones.
---@param filename string|nil
---@return string|nil
function common.normalize_path(filename) function common.normalize_path(filename)
if not filename then return end if not filename then return end
local volume
if PATHSEP == '\\' then if PATHSEP == '\\' then
filename = filename:gsub('[/\\]', '\\') filename = filename:gsub('[/\\]', '\\')
local drive, rem = filename:match('^([a-zA-Z]:\\)(.*)') local drive, rem = filename:match('^([a-zA-Z])(:.*)')
if drive then filename = drive and drive:upper() .. rem or filename
volume, filename = drive:upper(), rem
else
drive, rem = filename:match('^(\\\\[^\\]+\\[^\\]+\\)(.*)')
if drive then
volume, filename = drive, rem
end
end
elseif (PLATFORM == "AmigaOS 4" or PLATFORM == "MorphOS") then
local drive, relpath = filename:match('^([%w%s]*:)(.+)')
if relpath then
volume, filename = drive, relpath
end
else
local relpath = filename:match('^/(.+)')
if relpath then
volume, filename = "/", relpath
end
end end
local parts = split_on_slash(filename, PATHSEP) local parts = split_on_slash(filename, PATHSEP)
local accu = {} local accu = {}
for _, part in ipairs(parts) do for _, part in ipairs(parts) do
if part == '..' then if part == '..' and #accu > 0 and accu[#accu] ~= ".." then
if #accu > 0 and accu[#accu] ~= ".." then table.remove(accu)
table.remove(accu)
elseif volume then
error("invalid path " .. volume .. filename)
else
table.insert(accu, part)
end
elseif part ~= '.' then elseif part ~= '.' then
table.insert(accu, part) table.insert(accu, part)
end end
end end
local npath = table.concat(accu, PATHSEP) local npath = table.concat(accu, PATHSEP)
return (volume or "") .. (npath == "" and PATHSEP or npath) return npath == "" and PATHSEP or npath
end end
---Checks whether a path is absolute or relative.
---@param path string
---@return boolean
function common.is_absolute_path(path)
return path:sub(1, 1) == PATHSEP or path:match("^(%a):\\") or path:match('^([%w%s]*):')
end
---Checks whether a path belongs to a parent directory.
---@param filename string The path to check.
---@param path string The parent path.
---@return boolean
function common.path_belongs_to(filename, path) function common.path_belongs_to(filename, path)
return string.find(filename, common.basepath(path), 1, true) == 1 return string.find(filename, path .. PATHSEP, 1, true) == 1
end end
---Checks whether a path is relative to another path.
---@param ref_dir string The path to check against.
---@param dir string The input path.
---@return boolean
function common.relative_path(ref_dir, dir) function common.relative_path(ref_dir, dir)
local drive_pattern = "^(%a):\\" local drive_pattern = "^(%a):\\"
if (PLATFORM == "AmigaOS 4" or PLATFORM == "MorphOS") then
drive_pattern = "^([%w%s]*:)"
end
local drive, ref_drive = dir:match(drive_pattern), ref_dir:match(drive_pattern) local drive, ref_drive = dir:match(drive_pattern), ref_dir:match(drive_pattern)
if drive and ref_drive and drive ~= ref_drive then if drive and ref_drive and drive ~= ref_drive then
-- Windows, different drives, system.absolute_path fails for C:\..\D:\ -- Windows, different drives, system.absolute_path fails for C:\..\D:\
return dir return dir
end end
local ref_ls = split_on_slash(ref_dir) local ref_ls = split_on_slash(ref_dir)
local dir_ls = split_on_slash(dir) local dir_ls = split_on_slash(dir)
local i = 1 local i = 1
@ -700,11 +331,6 @@ function common.relative_path(ref_dir, dir)
end end
---Creates a directory recursively if necessary.
---@param path string
---@return boolean success
---@return string|nil error
---@return string|nil path The path where an error occured.
function common.mkdirp(path) function common.mkdirp(path)
local stat = system.get_file_info(path) local stat = system.get_file_info(path)
if stat and stat.type then if stat and stat.type then
@ -714,12 +340,12 @@ function common.mkdirp(path)
while path and path ~= "" do while path and path ~= "" do
local success_mkdir = system.mkdir(path) local success_mkdir = system.mkdir(path)
if success_mkdir then break end if success_mkdir then break end
local updir, basedir = path:match("(.*)["..PATHSEP.."](.+)$") local updir, basedir = path:match("(.*)[/\\](.+)$")
table.insert(subdirs, 1, basedir or path) table.insert(subdirs, 1, basedir or path)
path = updir path = updir
end end
for _, dirname in ipairs(subdirs) do for _, dirname in ipairs(subdirs) do
path = path and common.basepath(path) .. dirname or dirname path = path and path .. PATHSEP .. dirname or dirname
if not system.mkdir(path) then if not system.mkdir(path) then
return false, "cannot create directory", path return false, "cannot create directory", path
end end
@ -727,13 +353,6 @@ function common.mkdirp(path)
return true return true
end end
---Removes a path.
---@param path string
---@param recursively boolean If true, the function will attempt to remove everything in the specified path.
---@return boolean success
---@return string|nil error
---@return string|nil path The path where the error occured.
function common.rm(path, recursively) function common.rm(path, recursively)
local stat = system.get_file_info(path) local stat = system.get_file_info(path)
if not stat or (stat.type ~= "file" and stat.type ~= "dir") then if not stat or (stat.type ~= "file" and stat.type ~= "dir") then
@ -781,5 +400,4 @@ function common.rm(path, recursively)
return true return true
end end
return common return common

View File

@ -1,105 +1,39 @@
local common = require "core.common"
local config = {} local config = {}
config.project_scan_rate = 5
config.fps = 60 config.fps = 60
config.max_log_items = 800 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.animate_drag_scroll = false
config.scroll_past_end = true config.scroll_past_end = true
---@type "expanded" | "contracted" | false @Force the scrollbar status of the DocView
config.force_scrollbar_status = false
config.file_size_limit = 10 config.file_size_limit = 10
config.ignore_files = { config.ignore_files = "^%."
-- folders
"^%.svn/", "^%.git/", "^%.hg/", "^CVS/", "^%.Trash/", "^%.Trash%-.*/",
"^node_modules/", "^%.cache/", "^__pycache__/",
-- files
"%.pyc$", "%.pyo$", "%.exe$", "%.dll$", "%.obj$", "%.o$",
"%.a$", "%.lib$", "%.so$", "%.dylib$", "%.ncb$", "%.sdf$",
"%.suo$", "%.pdb$", "%.idb$", "%.class$", "%.psd$", "%.db$",
"^desktop%.ini$", "^%.DS_Store$", "^%.directory$",
}
config.symbol_pattern = "[%a_][%w_]*" config.symbol_pattern = "[%a_][%w_]*"
config.non_word_chars = " \t\n/\\()\"':,.;<>~!@#$%^&*|+=[]{}`?-" 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 = 8 config.max_tabs = 8
config.always_show_tabs = true config.always_show_tabs = true
-- Possible values: false, true, "no_selection"
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
config.tab_type = "soft" config.tab_type = "soft"
config.keep_newline_whitespace = false
config.line_limit = 80 config.line_limit = 80
config.max_symbols = 4000
config.max_project_files = 2000 config.max_project_files = 2000
config.transitions = true config.transitions = true
config.disabled_transitions = {
scroll = false,
commandview = false,
contextmenu = false,
logview = false,
nagbar = false,
tabs = false,
tab_drag = false,
statusbar = false,
}
config.animation_rate = 1.0 config.animation_rate = 1.0
config.blink_period = 0.8 config.blink_period = 0.8
config.disable_blink = false 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
-- set as true to be able to test non supported plugins -- Disable plugin loading setting to false the config entry
config.skip_plugins_version = false -- of the same name.
-- holds the plugins real config table
local plugins_config = {}
-- virtual representation of plugins config table
config.plugins = {} config.plugins = {}
-- allows virtual access to the plugins config table config.plugins.trimwhitespace = false
setmetatable(config.plugins, { config.plugins.lineguide = false
__index = function(_, k)
if not plugins_config[k] then
plugins_config[k] = { enabled = true, config = {} }
end
if plugins_config[k].enabled ~= false then
return plugins_config[k].config
end
return false
end,
__newindex = function(_, k, v)
if not plugins_config[k] then
plugins_config[k] = { enabled = nil, config = {} }
end
if v == false and package.loaded["plugins."..k] then
local core = require "core"
core.warn("[%s] is already enabled, restart the editor for the change to take effect", k)
return
elseif plugins_config[k].enabled == false and v ~= false then
plugins_config[k].enabled = true
end
if v == false then
plugins_config[k].enabled = false
elseif type(v) == "table" then
plugins_config[k].enabled = true
plugins_config[k].config = common.merge(plugins_config[k].config, v)
end
end,
__pairs = function()
return coroutine.wrap(function()
for name, status in pairs(plugins_config) do
coroutine.yield(name, status.config)
end
end)
end
})
return config return config

View File

@ -5,52 +5,28 @@ local config = require "core.config"
local keymap = require "core.keymap" local keymap = require "core.keymap"
local style = require "core.style" local style = require "core.style"
local Object = require "core.object" local Object = require "core.object"
local View = require "core.view"
local border_width = 1 local border_width = 1
local divider_width = 1 local divider_width = 1
local divider_padding = 5
local DIVIDER = {} local DIVIDER = {}
---An item in the context menu.
---@class core.contextmenu.item
---@field text string
---@field info string|nil If provided, this text is displayed on the right side of the menu.
---@field command string|fun()
---A list of items with the same predicate.
---@see core.command.predicate
---@class core.contextmenu.itemset
---@field predicate core.command.predicate
---@field items core.contextmenu.item[]
---A context menu.
---@class core.contextmenu : core.object
---@field itemset core.contextmenu.itemset[]
---@field show_context_menu boolean
---@field selected number
---@field position core.view.position
---@field current_scale number
local ContextMenu = Object:extend() local ContextMenu = Object:extend()
---A unique value representing the divider in a context menu.
ContextMenu.DIVIDER = DIVIDER ContextMenu.DIVIDER = DIVIDER
---Creates a new context menu.
function ContextMenu:new() function ContextMenu:new()
self.itemset = {} self.itemset = {}
self.show_context_menu = false self.show_context_menu = false
self.selected = -1 self.selected = -1
self.height = 0 self.height = 0
self.position = { x = 0, y = 0 } self.position = { x = 0, y = 0 }
self.current_scale = SCALE
end end
local function get_item_size(item) local function get_item_size(item)
local lw, lh local lw, lh
if item == DIVIDER then if item == DIVIDER then
lw = 0 lw = 0
lh = divider_width + divider_padding * SCALE * 2 lh = divider_width
else else
lw = style.font:get_width(item.text) lw = style.font:get_width(item.text)
if item.info then if item.info then
@ -61,11 +37,19 @@ local function get_item_size(item)
return lw, lh return lw, lh
end end
local function update_items_size(items, update_binding) function ContextMenu:register(predicate, items)
local width, height = 0, 0 if type(predicate) == "string" then
for _, item in ipairs(items) do predicate = require(predicate)
if update_binding and item ~= DIVIDER then end
item.info = keymap.get_binding(item.command) if type(predicate) == "table" then
local class = predicate
predicate = function() return core.active_view:is(class) end
end
local width, height = 0, 0 --precalculate the size of context menu
for i, item in ipairs(items) do
if item ~= DIVIDER then
item.info = keymap.reverse_map[item.command]
end 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)
@ -73,34 +57,18 @@ local function update_items_size(items, update_binding)
end end
width = width + style.padding.x * 2 width = width + style.padding.x * 2
items.width, items.height = width, height items.width, items.height = width, height
end
---Registers a list of items into the context menu with a predicate.
---@param predicate core.command.predicate
---@param items core.contextmenu.item[]
function ContextMenu:register(predicate, items)
predicate = command.generate_predicate(predicate)
update_items_size(items, true)
table.insert(self.itemset, { predicate = predicate, items = items }) table.insert(self.itemset, { predicate = predicate, items = items })
end end
---Shows the context menu.
---@param x number
---@param y number
---@return boolean # If true, the context menu is shown.
function ContextMenu:show(x, y) function ContextMenu:show(x, y)
self.items = nil self.items = nil
local items_list = { width = 0, height = 0 } local items_list = { width = 0, height = 0 }
for _, items in ipairs(self.itemset) do for _, items in ipairs(self.itemset) do
if items.predicate(x, y) then if items.predicate(x, y) then
items_list.width = math.max(items_list.width, items.items.width) items_list.width = math.max(items_list.width, items.items.width)
items_list.height = items_list.height items_list.height = items_list.height + items.items.height
for _, subitems in ipairs(items.items) do for _, subitems in ipairs(items.items) do
if not subitems.command or command.is_valid(subitems.command) then table.insert(items_list, subitems)
local lw, lh = get_item_size(subitems)
items_list.height = items_list.height + lh
table.insert(items_list, subitems)
end
end end
end end
end end
@ -110,28 +78,27 @@ function ContextMenu:show(x, y)
local w, h = self.items.width, self.items.height local w, h = self.items.width, self.items.height
-- by default the box is opened on the right and below -- by default the box is opened on the right and below
x = common.clamp(x, 0, core.root_view.size.x - w - style.padding.x) if x + w >= core.root_view.size.x then
y = common.clamp(y, 0, core.root_view.size.y - h) x = x - w
end
if y + h >= core.root_view.size.y then
y = y - h
end
self.position.x, self.position.y = x, y self.position.x, self.position.y = x, y
self.show_context_menu = true self.show_context_menu = true
core.request_cursor("arrow")
return true return true
end end
return false return false
end end
---Hides the context menu.
function ContextMenu:hide() function ContextMenu:hide()
self.show_context_menu = false self.show_context_menu = false
self.items = nil self.items = nil
self.selected = -1 self.selected = -1
self.height = 0 self.height = 0
core.request_cursor(core.active_view.cursor)
end end
---Returns an iterator that iterates over each context menu item and their dimensions.
---@return fun(): number, core.contextmenu.item, number, number, number, number
function ContextMenu:each_item() function ContextMenu:each_item()
local x, y, w = self.position.x, self.position.y, self.items.width local x, y, w = self.position.x, self.position.y, self.items.width
local oy = y local oy = y
@ -145,12 +112,8 @@ function ContextMenu:each_item()
end) end)
end end
---Event handler for mouse movements.
---@param px any
---@param py any
---@return boolean # true if the event is caught.
function ContextMenu:on_mouse_moved(px, py) function ContextMenu:on_mouse_moved(px, py)
if not self.show_context_menu then return false end if not self.show_context_menu then return end
self.selected = -1 self.selected = -1
for i, item, x, y, w, h in self:each_item() do for i, item, x, y, w, h in self:each_item() do
@ -159,11 +122,12 @@ function ContextMenu:on_mouse_moved(px, py)
break break
end end
end end
if self.selected >= 0 then
core.request_cursor("arrow")
end
return true return true
end end
---Event handler for when the selection is confirmed.
---@param item core.contextmenu.item
function ContextMenu:on_selected(item) function ContextMenu:on_selected(item)
if type(item.command) == "string" then if type(item.command) == "string" then
command.perform(item.command) command.perform(item.command)
@ -172,94 +136,56 @@ function ContextMenu:on_selected(item)
end end
end end
local function change_value(value, change) function ContextMenu:on_mouse_pressed(button, x, y, clicks)
return value + change local selected = (self.items or {})[self.selected]
end
---Selects the the previous item.
function ContextMenu:focus_previous()
self.selected = (self.selected == -1 or self.selected == 1) and #self.items or change_value(self.selected, -1)
if self:get_item_selected() == DIVIDER then
self.selected = change_value(self.selected, -1)
end
end
---Selects the next item.
function ContextMenu:focus_next()
self.selected = (self.selected == -1 or self.selected == #self.items) and 1 or change_value(self.selected, 1)
if self:get_item_selected() == DIVIDER then
self.selected = change_value(self.selected, 1)
end
end
---Gets the currently selected item.
---@return core.contextmenu.item|nil
function ContextMenu:get_item_selected()
return (self.items or {})[self.selected]
end
---Hides the context menu and performs the command if an item is selected.
function ContextMenu:call_selected_item()
local selected = self:get_item_selected()
self:hide()
if selected then
self:on_selected(selected)
end
end
---Event handler for mouse press.
---@param button core.view.mousebutton
---@param px number
---@param py number
---@param clicks number
---@return boolean # true if the event is caught.
function ContextMenu:on_mouse_pressed(button, px, py, clicks)
local caught = false local caught = false
if self.show_context_menu then self:hide()
if button == "left" then if button == "left" then
local selected = self:get_item_selected() if selected then
if selected then self:on_selected(selected)
self:on_selected(selected) caught = true
end
end
self:hide()
caught = true
else
if button == "right" then
caught = self:show(px, py)
end end
end end
if button == "right" then
caught = self:show(x, y)
end
return caught return caught
end end
---@type fun(self: table, k: string, dest: number, rate?: number, name?: string) -- copied from core.docview
ContextMenu.move_towards = View.move_towards function ContextMenu:move_towards(t, k, dest, rate)
if type(t) ~= "table" then
---Event handler for content update. return self:move_towards(self, t, k, dest, rate)
function ContextMenu:update() end
if self.show_context_menu then local val = t[k]
self:move_towards("height", self.items.height, nil, "contextmenu") if not config.transitions or math.abs(val - dest) < 0.5 then
t[k] = dest
else
rate = rate or 0.5
if config.fps ~= 60 or config.animation_rate ~= 1 then
local dt = 60 / config.fps
rate = 1 - common.clamp(1 - rate, 1e-8, 1 - 1e-8)^(config.animation_rate * dt)
end
t[k] = common.lerp(val, dest, rate)
end
if val ~= dest then
core.redraw = true
end
end
function ContextMenu:update()
if self.show_context_menu then
self:move_towards("height", self.items.height)
end end
end end
---Draws the context menu.
---
---This wraps `ContextMenu:draw_context_menu()`.
---@see core.contextmenu.draw_context_menu
function ContextMenu:draw() function ContextMenu:draw()
if not self.show_context_menu then return end if not self.show_context_menu then return end
if self.current_scale ~= SCALE then
update_items_size(self.items)
for _, set in ipairs(self.itemset) do
update_items_size(set.items)
end
self.current_scale = SCALE
end
core.root_view:defer_draw(self.draw_context_menu, self) core.root_view:defer_draw(self.draw_context_menu, self)
end end
---Draws the context menu.
function ContextMenu:draw_context_menu() function ContextMenu:draw_context_menu()
if not self.items then return end if not self.items then return end
local bx, by, bw, bh = self.position.x, self.position.y, self.items.width, self.height local bx, by, bw, bh = self.position.x, self.position.y, self.items.width, self.height
@ -275,7 +201,7 @@ function ContextMenu:draw_context_menu()
for i, item, x, y, w, h in self:each_item() do for i, item, x, y, w, h in self:each_item() do
if item == DIVIDER then if item == DIVIDER then
renderer.draw_rect(x, y + divider_padding * SCALE, w, divider_width, style.divider) renderer.draw_rect(x, y, w, h, style.caret)
else else
if i == self.selected then if i == self.selected then
renderer.draw_rect(x, y, w, h, style.selection) renderer.draw_rect(x, y, w, h, style.selection)

View File

@ -1,240 +0,0 @@
local common = require "core.common"
local config = require "core.config"
local dirwatch = {}
function dirwatch:__index(idx)
local value = rawget(self, idx)
if value ~= nil then return value end
return dirwatch[idx]
end
function dirwatch.new()
local t = {
scanned = {},
watched = {},
reverse_watched = {},
monitor = dirmonitor.new(),
single_watch_top = nil,
single_watch_count = 0
}
setmetatable(t, dirwatch)
return t
end
function dirwatch:scan(directory, bool)
if bool == false then return self:unwatch(directory) end
self.scanned[directory] = system.get_file_info(directory).modified
end
-- Should be called on every directory in a subdirectory.
-- In windows, this is a no-op for anything underneath a top-level directory,
-- but code should be called anyway, so we can ensure that we have a proper
-- experience across all platforms. Should be an absolute path.
-- Can also be called on individual files, though this should be used sparingly,
-- so as not to run into system limits (like in the autoreload plugin).
function dirwatch:watch(directory, bool)
if bool == false then return self:unwatch(directory) end
local info = system.get_file_info(directory)
if not info then return end
if not self.watched[directory] and not self.scanned[directory] then
if self.monitor:mode() == "single" then
if info.type ~= "dir" then return self:scan(directory) end
if not self.single_watch_top or directory:find(self.single_watch_top, 1, true) ~= 1 then
-- Get the highest level of directory that is common to this directory, and the original.
local target = directory
while self.single_watch_top and self.single_watch_top:find(target, 1, true) ~= 1 do
target = common.dirname(target)
end
if target ~= self.single_watch_top then
local value = self.monitor:watch(target)
if value and value < 0 then
return self:scan(directory)
end
self.single_watch_top = target
end
end
self.single_watch_count = self.single_watch_count + 1
self.watched[directory] = true
else
local value = self.monitor:watch(directory)
-- If for whatever reason, we can't watch this directory, revert back to scanning.
-- Don't bother trying to find out why, for now.
if value and value < 0 then
return self:scan(directory)
end
self.watched[directory] = value
self.reverse_watched[value] = directory
end
end
end
-- this should be an absolute path
function dirwatch:unwatch(directory)
if self.watched[directory] then
if self.monitor:mode() == "multiple" then
self.monitor:unwatch(self.watched[directory])
self.reverse_watched[directory] = nil
else
self.single_watch_count = self.single_watch_count - 1
if self.single_watch_count == 0 then
self.single_watch_top = nil
self.monitor:unwatch(directory)
end
end
self.watched[directory] = nil
elseif self.scanned[directory] then
self.scanned[directory] = nil
end
end
-- designed to be run inside a coroutine.
function dirwatch:check(change_callback, scan_time, wait_time)
local had_change = false
local last_error
self.monitor:check(function(id)
had_change = true
if self.monitor:mode() == "single" then
local path = common.dirname(id)
if not string.match(id, "^/") and not string.match(id, "^%a:[/\\]") then
path = common.dirname(self.single_watch_top .. PATHSEP .. id)
end
change_callback(path)
elseif self.reverse_watched[id] then
change_callback(self.reverse_watched[id])
end
end, function(err)
last_error = err
end)
if last_error ~= nil then error(last_error) end
local start_time = system.get_time()
for directory, old_modified in pairs(self.scanned) do
if old_modified then
local info = system.get_file_info(directory)
local new_modified = info and info.modified
if old_modified ~= new_modified then
change_callback(directory)
had_change = true
self.scanned[directory] = new_modified
end
end
if system.get_time() - start_time > (scan_time or 0.01) then
coroutine.yield(wait_time or 0.01)
start_time = system.get_time()
end
end
return had_change
end
-- inspect config.ignore_files patterns and prepare ready to use entries.
local function compile_ignore_files()
local ipatterns = config.ignore_files
local compiled = {}
-- config.ignore_files could be a simple string...
if type(ipatterns) ~= "table" then ipatterns = {ipatterns} end
for i, pattern in ipairs(ipatterns) do
-- we ignore malformed pattern that raise an error
if pcall(string.match, "a", pattern) then
table.insert(compiled, {
use_path = pattern:match("/[^/$]"), -- contains a slash but not at the end
-- An '/' or '/$' at the end means we want to match a directory.
match_dir = pattern:match(".+/%$?$"), -- to be used as a boolen value
pattern = pattern -- get the actual pattern
})
end
end
return compiled
end
local function fileinfo_pass_filter(info, ignore_compiled)
if info.size >= config.file_size_limit * 1e6 then return false end
local basename = common.basename(info.filename)
-- replace '\' with '/' for Windows where PATHSEP = '\'
local fullname = "/" .. info.filename:gsub("\\", "/")
for _, compiled in ipairs(ignore_compiled) do
local test = compiled.use_path and fullname or basename
if compiled.match_dir then
if info.type == "dir" and string.match(test .. "/", compiled.pattern) then
return false
end
else
if string.match(test, compiled.pattern) then
return false
end
end
end
return true
end
local function compare_file(a, b)
return system.path_compare(a.filename, a.type, b.filename, b.type)
end
-- compute a file's info entry completed with "filename" to be used
-- in project scan or falsy if it shouldn't appear in the list.
local function get_project_file_info(root, file, ignore_compiled)
local info = system.get_file_info(common.basepath(root) .. file)
-- info can be not nil but info.type may be nil if is neither a file neither
-- a directory, for example for /dev/* entries on linux.
if info and info.type then
info.filename = file
return fileinfo_pass_filter(info, ignore_compiled) and info
end
end
-- "root" will by an absolute path without trailing '/'
-- "path" will be a path starting without '/' and without trailing '/'
-- or the empty string.
-- It identifies a sub-path within "root".
-- The current path location will therefore always be: root .. path.
-- When recursing, "root" will always be the same, only "path" will change.
-- Returns a list of file "items". In each item the "filename" will be the
-- complete file path relative to "root" *without* the trailing '/', and without the starting '/'.
function dirwatch.get_directory_files(dir, root, path, entries_count, recurse_pred)
local t = {}
local t0 = system.get_time()
local ignore_compiled = compile_ignore_files()
local all = system.list_dir(common.basepath(root) .. path)
if not all then return nil end
local entries = { }
for _, file in ipairs(all) do
local info = get_project_file_info(root, (path ~= "" and (path .. PATHSEP) or "") .. file, ignore_compiled)
if info then
table.insert(entries, info)
end
end
table.sort(entries, compare_file)
local recurse_complete = true
for _, info in ipairs(entries) do
table.insert(t, info)
entries_count = entries_count + 1
if info.type == "dir" then
if recurse_pred(dir, info.filename, entries_count, system.get_time() - t0) then
local t_rec, complete, n = dirwatch.get_directory_files(dir, root, info.filename, entries_count, recurse_pred)
recurse_complete = recurse_complete and complete
if n ~= nil then
entries_count = n
for _, info_rec in ipairs(t_rec) do
table.insert(t, info_rec)
end
end
else
recurse_complete = false
end
end
end
return t, recurse_complete, entries_count
end
return dirwatch

View File

@ -1,5 +1,4 @@
local core = require "core" local core = require "core"
local common = require "core.common"
local config = require "core.config" local config = require "core.config"
local tokenizer = require "core.tokenizer" local tokenizer = require "core.tokenizer"
local Object = require "core.object" local Object = require "core.object"
@ -10,101 +9,53 @@ local Highlighter = Object:extend()
function Highlighter:new(doc) function Highlighter:new(doc)
self.doc = doc self.doc = doc
self.running = false
self:reset() self:reset()
end
-- init incremental syntax highlighting -- init incremental syntax highlighting
function Highlighter:start()
if self.running then return end
self.running = true
core.add_thread(function() core.add_thread(function()
while self.first_invalid_line <= self.max_wanted_line do while true do
local max = math.min(self.first_invalid_line + 40, self.max_wanted_line) if self.first_invalid_line > self.max_wanted_line then
local retokenized_from self.max_wanted_line = 0
for i = self.first_invalid_line, max do coroutine.yield(1 / config.fps)
local state = (i > 1) and self.lines[i - 1].state
local line = self.lines[i] else
if line and line.resume and (line.init_state ~= state or line.text ~= self.doc.lines[i]) then local max = math.min(self.first_invalid_line + 40, self.max_wanted_line)
-- Reset the progress if no longer valid
line.resume = nil for i = self.first_invalid_line, max do
end local state = (i > 1) and self.lines[i - 1].state
if not (line and line.init_state == state and line.text == self.doc.lines[i] and not line.resume) then local line = self.lines[i]
retokenized_from = retokenized_from or i if not (line and line.init_state == state) then
self.lines[i] = self:tokenize_line(i, state, line and line.resume) self.lines[i] = self:tokenize_line(i, state)
if self.lines[i].resume then
self.first_invalid_line = i
goto yield
end end
elseif retokenized_from then
self:update_notify(retokenized_from, i - retokenized_from - 1)
retokenized_from = nil
end end
end
self.first_invalid_line = max + 1 self.first_invalid_line = max + 1
::yield:: core.redraw = true
if retokenized_from then coroutine.yield()
self:update_notify(retokenized_from, max - retokenized_from)
end end
core.redraw = true
coroutine.yield()
end end
self.max_wanted_line = 0
self.running = false
end, self) end, self)
end end
local function set_max_wanted_lines(self, amount)
self.max_wanted_line = amount
if self.first_invalid_line <= self.max_wanted_line then
self:start()
end
end
function Highlighter:reset() function Highlighter:reset()
self.lines = {} self.lines = {}
self:soft_reset()
end
function Highlighter:soft_reset()
for i=1,#self.lines do
self.lines[i] = false
end
self.first_invalid_line = 1 self.first_invalid_line = 1
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)
set_max_wanted_lines(self, math.min(self.max_wanted_line, #self.doc.lines)) self.max_wanted_line = math.min(self.max_wanted_line, #self.doc.lines)
end
function Highlighter:insert_notify(line, n)
self:invalidate(line)
local blanks = { }
for i = 1, n do
blanks[i] = false
end
common.splice(self.lines, line, 0, blanks)
end
function Highlighter:remove_notify(line, n)
self:invalidate(line)
common.splice(self.lines, line, n)
end
function Highlighter:update_notify(line, n)
-- plugins can hook here to be notified that lines have been retokenized
end end
function Highlighter:tokenize_line(idx, state, resume) function Highlighter:tokenize_line(idx, state)
local res = {} local res = {}
res.init_state = state res.init_state = state
res.text = self.doc.lines[idx] res.text = self.doc.lines[idx]
res.tokens, res.state, res.resume = tokenizer.tokenize(self.doc.syntax, res.text, state, resume) res.tokens, res.state = tokenizer.tokenize(self.doc.syntax, res.text, state)
return res return res
end end
@ -115,9 +66,8 @@ function Highlighter:get_line(idx)
local prev = self.lines[idx - 1] local prev = self.lines[idx - 1]
line = self:tokenize_line(idx, prev and prev.state) line = self:tokenize_line(idx, prev and prev.state)
self.lines[idx] = line self.lines[idx] = line
self:update_notify(idx, 0)
end end
set_max_wanted_lines(self, math.max(self.max_wanted_line, idx)) self.max_wanted_line = math.max(self.max_wanted_line, idx)
return line return line
end end

View File

@ -5,7 +5,7 @@ local syntax = require "core.syntax"
local config = require "core.config" local config = require "core.config"
local common = require "core.common" local common = require "core.common"
---@class core.doc : core.object
local Doc = Object:extend() local Doc = Object:extend()
@ -33,7 +33,7 @@ end
function Doc:reset() function Doc:reset()
self.lines = { "\n" } self.lines = { "\n" }
self.selections = { 1, 1, 1, 1 } self.selections = { 1, 1, 1, 1 }
self.last_selection = 1 self.cursor_clipboard = {}
self.undo_stack = { idx = 1 } self.undo_stack = { idx = 1 }
self.redo_stack = { idx = 1 } self.redo_stack = { idx = 1 }
self.clean_change_id = 1 self.clean_change_id = 1
@ -44,15 +44,10 @@ end
function Doc:reset_syntax() function Doc:reset_syntax()
local header = self:get_text(1, 1, self:position_offset(1, 1, 128)) local header = self:get_text(1, 1, self:position_offset(1, 1, 128))
local path = self.abs_filename local syn = syntax.get(self.filename or "", header)
if not path and self.filename then
path = common.basepath(core.project_dir) .. self.filename
end
if path then path = common.normalize_path(path) end
local syn = syntax.get(path, header)
if self.syntax ~= syn then if self.syntax ~= syn then
self.syntax = syn self.syntax = syn
self.highlighter:soft_reset() self.highlighter:reset()
end end
end end
@ -60,7 +55,6 @@ end
function Doc:set_filename(filename, abs_filename) function Doc:set_filename(filename, abs_filename)
self.filename = filename self.filename = filename
self.abs_filename = abs_filename self.abs_filename = abs_filename
self:reset_syntax()
end end
@ -68,15 +62,12 @@ function Doc:load(filename)
local fp = assert( io.open(filename, "rb") ) local fp = assert( io.open(filename, "rb") )
self:reset() self:reset()
self.lines = {} self.lines = {}
local i = 1
for line in fp:lines() do for line in fp:lines() do
if line:byte(-1) == 13 then if line:byte(-1) == 13 then
line = line:sub(1, -2) line = line:sub(1, -2)
self.crlf = true self.crlf = true
end end
table.insert(self.lines, line .. "\n") table.insert(self.lines, line .. "\n")
self.highlighter.lines[i] = false
i = i + 1
end end
if #self.lines == 0 then if #self.lines == 0 then
table.insert(self.lines, "\n") table.insert(self.lines, "\n")
@ -86,23 +77,11 @@ function Doc:load(filename)
end end
function Doc:reload()
if self.filename then
local sel = { self:get_selection() }
self:load(self.filename)
self:clean()
self:set_selection(table.unpack(sel))
end
end
function Doc:save(filename, abs_filename) function Doc:save(filename, abs_filename)
if not filename then if not filename then
assert(self.filename, "no filename set to default to") assert(self.filename, "no filename set to default to")
filename = self.filename filename = self.filename
abs_filename = self.abs_filename abs_filename = self.abs_filename
else
assert(self.filename or abs_filename, "calling save on unnamed doc without absolute path")
end 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
@ -112,6 +91,7 @@ function Doc:save(filename, abs_filename)
fp:close() fp:close()
self:set_filename(filename, abs_filename) self:set_filename(filename, abs_filename)
self.new_file = false self.new_file = false
self:reset_syntax()
self:clean() self:clean()
end end
@ -122,12 +102,7 @@ end
function Doc:is_dirty() function Doc:is_dirty()
if self.new_file then return self.clean_change_id ~= self:get_change_id() or self.new_file
if self.filename then return true end
return #self.lines > 1 or #self.lines[1] > 1
else
return self.clean_change_id ~= self:get_change_id()
end
end end
@ -136,49 +111,17 @@ function Doc:clean()
end end
function Doc:get_indent_info()
if not self.indent_info then return config.tab_type, config.indent_size, false end
return self.indent_info.type or config.tab_type,
self.indent_info.size or config.indent_size,
self.indent_info.confirmed
end
function Doc:get_change_id() function Doc:get_change_id()
return self.undo_stack.idx return self.undo_stack.idx
end end
local function sort_positions(line1, col1, line2, col2)
if line1 > line2 or line1 == line2 and col1 > col2 then
return line2, col2, line1, col1, true
end
return line1, col1, line2, col2, false
end
-- Cursor section. Cursor indices are *only* valid during a get_selections() call. -- Cursor section. Cursor indices are *only* valid during a get_selections() call.
-- Cursors will always be iterated in order from top to bottom. Through normal operation -- Cursors will always be iterated in order from top to bottom. Through normal operation
-- curors can never swap positions; only merge or split, or change their position in cursor -- curors can never swap positions; only merge or split, or change their position in cursor
-- order. -- order.
function Doc:get_selection(sort) function Doc:get_selection(sort)
local line1, col1, line2, col2, swap = self:get_selection_idx(self.last_selection, sort) local idx, line1, col1, line2, col2 = self:get_selections(sort)({ self.selections, sort }, 0)
if not line1 then return line1, col1, line2, col2, sort
line1, col1, line2, col2, swap = self:get_selection_idx(1, sort)
end
return line1, col1, line2, col2, swap
end
---Get the selection specified by `idx`
---@param idx integer @the index of the selection to retrieve
---@param sort? boolean @whether to sort the selection returned
---@return integer,integer,integer,integer,boolean? @line1, col1, line2, col2, was the selection sorted
function Doc:get_selection_idx(idx, sort)
local line1, col1, line2, col2 = self.selections[idx*4-3], self.selections[idx*4-2], self.selections[idx*4-1], self.selections[idx*4]
if line1 and sort then
return sort_positions(line1, col1, line2, col2)
else
return line1, col1, line2, col2
end
end end
function Doc:get_selection_text(limit) function Doc:get_selection_text(limit)
@ -199,19 +142,19 @@ function Doc:has_selection()
return line1 ~= line2 or col1 ~= col2 return line1 ~= line2 or col1 ~= col2
end end
function Doc:has_any_selection()
for idx, line1, col1, line2, col2 in self:get_selections() do
if line1 ~= line2 or col1 ~= col2 then return true end
end
return false
end
function Doc:sanitize_selection() function Doc:sanitize_selection()
for idx, line1, col1, line2, col2 in self:get_selections() do for idx, line1, col1, line2, col2 in self:get_selections() do
self:set_selections(idx, line1, col1, line2, col2) self:set_selections(idx, line1, col1, line2, col2)
end end
end end
local function sort_positions(line1, col1, line2, col2)
if line1 > line2 or line1 == line2 and col1 > col2 then
return line2, col2, line1, col1
end
return line1, col1, line2, col2
end
function Doc:set_selections(idx, line1, col1, line2, col2, swap, rm) function Doc:set_selections(idx, line1, col1, line2, col2, swap, rm)
assert(not line2 == not col2, "expected 3 or 5 arguments") assert(not line2 == not col2, "expected 3 or 5 arguments")
if swap then line1, col1, line2, col2 = line2, col2, line1, col1 end if swap then line1, col1, line2, col2 = line2, col2, line1, col1 end
@ -230,22 +173,11 @@ function Doc:add_selection(line1, col1, line2, col2, swap)
end end
end end
self:set_selections(target, line1, col1, line2, col2, swap, 0) self:set_selections(target, line1, col1, line2, col2, swap, 0)
self.last_selection = target
end end
function Doc:remove_selection(idx)
if self.last_selection >= idx then
self.last_selection = self.last_selection - 1
end
common.splice(self.selections, (idx - 1) * 4 + 1, 4)
end
function Doc:set_selection(line1, col1, line2, col2, swap) function Doc:set_selection(line1, col1, line2, col2, swap)
self.selections = {} self.selections, self.cursor_clipboard = {}, {}
self:set_selections(1, line1, col1, line2, col2, swap) self:set_selections(1, line1, col1, line2, col2, swap)
self.last_selection = 1
end end
function Doc:merge_cursors(idx) function Doc:merge_cursors(idx)
@ -254,22 +186,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)
if self.last_selection >= (i+3)/4 then common.splice(self.cursor_clipboard, i, 1)
self.last_selection = self.last_selection - 1
end
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(table.unpack(invariant[1], target, target+4)) return idx+(invariant[3] and -1 or 1), sort_positions(unpack(invariant[1], target, target+4))
else else
return idx+(invariant[3] and -1 or 1), table.unpack(invariant[1], target, target+4) return idx+(invariant[3] and -1 or 1), unpack(invariant[1], target, target+4)
end end
end end
@ -282,13 +213,9 @@ end
-- End of cursor seciton. -- End of cursor seciton.
function Doc:sanitize_position(line, col) function Doc:sanitize_position(line, col)
local nlines = #self.lines line = common.clamp(line, 1, #self.lines)
if line > nlines then col = common.clamp(col, 1, #self.lines[line])
return nlines, #self.lines[nlines] return line, col
elseif line < 1 then
return 1, 1
end
return line, common.clamp(col, 1, #self.lines[line])
end end
@ -407,7 +334,7 @@ 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 -- keep cursors where they should be
for idx, cline1, ccol1, cline2, ccol2 in self:get_selections(true, true) do for idx, cline1, ccol1, cline2, ccol2 in self:get_selections(true, true) do
if cline1 < line then break end if cline1 < line then break end
@ -418,11 +345,11 @@ function Doc:raw_insert(line, col, text, undo_stack, time)
-- 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", table.unpack(self.selections)) push_undo(undo_stack, time, "selection", 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:insert_notify(line, #lines - 1) self.highlighter:invalidate(line)
self:sanitize_selection() self:sanitize_selection()
end end
@ -430,68 +357,32 @@ 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", table.unpack(self.selections)) push_undo(undo_stack, time, "selection", 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
local before = self.lines[line1]:sub(1, col1 - 1) local before = self.lines[line1]:sub(1, col1 - 1)
local after = self.lines[line2]:sub(col2) local after = self.lines[line2]:sub(col2)
local line_removal = line2 - line1
local col_removal = col2 - col1
-- splice line into line array -- splice line into line array
common.splice(self.lines, line1, line_removal + 1, { before .. after }) common.splice(self.lines, line1, line2 - line1 + 1, { before .. after })
local merge = false -- move all cursors back if they share a line with the removed text
-- keep selections in correct positions: each pair (line, col)
-- * remains unchanged if before the deleted text
-- * is set to (line1, col1) if in the deleted text
-- * is set to (line1, col - col_removal) if on line2 but out of the deleted text
-- * is set to (line - line_removal, col) if after line2
for idx, cline1, ccol1, cline2, ccol2 in self:get_selections(true, true) do for idx, cline1, ccol1, cline2, ccol2 in self:get_selections(true, true) do
if cline2 < line1 then break end if cline1 < line2 then break end
local l1, c1, l2, c2 = cline1, ccol1, cline2, ccol2 local line_removal = line2 - line1
local column_removal = line2 == cline2 and col2 < ccol1 and (line2 == line1 and col2 - col1 or col2) or 0
if cline1 > line1 or (cline1 == line1 and ccol1 > col1) then self:set_selections(idx, cline1 - line_removal, ccol1 - column_removal, cline2 - line_removal, ccol2 - column_removal)
if cline1 > line2 then
l1 = l1 - line_removal
else
l1 = line1
c1 = (cline1 == line2 and ccol1 > col2) and c1 - col_removal or col1
end
end
if cline2 > line1 or (cline2 == line1 and ccol2 > col1) then
if cline2 > line2 then
l2 = l2 - line_removal
else
l2 = line1
c2 = (cline2 == line2 and ccol2 > col2) and c2 - col_removal or col1
end
end
if l1 == line1 and c1 == col1 then merge = true end
self:set_selections(idx, l1, c1, l2, c2)
end
if merge then
self:merge_cursors()
end end
-- update highlighter and assure selection is in bounds -- update highlighter and assure selection is in bounds
self.highlighter:remove_notify(line1, line_removal) self.highlighter:invalidate(line1)
self:sanitize_selection() self:sanitize_selection()
end end
function Doc:insert(line, col, text) function Doc:insert(line, col, text)
self.redo_stack = { idx = 1 } self.redo_stack = { idx = 1 }
-- Reset the clean id when we're pushing something new before it
if self:get_change_id() < self.clean_change_id then
self.clean_change_id = -1
end
line, col = self:sanitize_position(line, col) line, col = self:sanitize_position(line, col)
self:raw_insert(line, col, text, self.undo_stack, system.get_time()) self:raw_insert(line, col, text, self.undo_stack, system.get_time())
self:on_text_change("insert") self:on_text_change("insert")
@ -528,21 +419,9 @@ function Doc:text_input(text, idx)
end end
end end
function Doc:ime_text_editing(text, start, length, idx)
for sidx, line1, col1, line2, col2 in self:get_selections(true, idx or true) do
if line1 ~= line2 or col1 ~= col2 then
self:delete_to_cursor(sidx)
end
self:insert(line1, col1, text)
self:set_selections(sidx, line1, col1 + #text, line1, col1)
end
end
function Doc:replace_cursor(idx, line1, col1, line2, col2, fn) function Doc:replace_cursor(idx, line1, col1, line2, col2, fn)
local old_text = self:get_text(line1, col1, line2, col2) local old_text = self:get_text(line1, col1, line2, col2)
local new_text, res = fn(old_text) local new_text, n = fn(old_text)
if old_text ~= new_text then if old_text ~= new_text then
self:insert(line2, col2, new_text) self:insert(line2, col2, new_text)
self:remove(line1, col1, line2, col2) self:remove(line1, col1, line2, col2)
@ -551,22 +430,22 @@ function Doc:replace_cursor(idx, line1, col1, line2, col2, fn)
self:set_selections(idx, line1, col1, line2, col2) self:set_selections(idx, line1, col1, line2, col2)
end end
end end
return res return n
end end
function Doc:replace(fn) function Doc:replace(fn)
local has_selection, results = false, { } local has_selection, n = false, 0
for idx, line1, col1, line2, col2 in self:get_selections(true) do for idx, line1, col1, line2, col2 in self:get_selections(true) do
if line1 ~= line2 or col1 ~= col2 then if line1 ~= line2 or col1 ~= col2 then
results[idx] = self:replace_cursor(idx, line1, col1, line2, col2, fn) n = n + self:replace_cursor(idx, line1, col1, line2, col2, fn)
has_selection = true has_selection = true
end end
end end
if not has_selection then if not has_selection then
self:set_selection(table.unpack(self.selections)) self:set_selection(table.unpack(self.selections))
results[1] = self:replace_cursor(1, 1, 1, #self.lines, #self.lines[#self.lines], fn) n = n + self:replace_cursor(1, 1, 1, #self.lines, #self.lines[#self.lines], fn)
end end
return results return n
end end
@ -604,21 +483,19 @@ end
function Doc:select_to(...) return self:select_to_cursor(nil, ...) end function Doc:select_to(...) return self:select_to_cursor(nil, ...) end
function Doc:get_indent_string() local function get_indent_string()
local indent_type, indent_size = self:get_indent_info() if config.tab_type == "hard" then
if indent_type == "hard" then
return "\t" return "\t"
end end
return string.rep(" ", indent_size) return string.rep(" ", config.indent_size)
end end
-- returns the size of the original indent, and the indent -- returns the size of the original indent, and the indent
-- in your config format, rounded either up or down -- in your config format, rounded either up or down
function Doc:get_line_indent(line, rnd_up) local function get_line_indent(line, rnd_up)
local _, e = line:find("^[ \t]+") local _, e = line:find("^[ \t]+")
local indent_type, indent_size = self:get_indent_info() local soft_tab = string.rep(" ", config.indent_size)
local soft_tab = string.rep(" ", indent_size) if config.tab_type == "hard" then
if indent_type == "hard" then
local indent = e and line:sub(1, e):gsub(soft_tab, "\t") or "" local indent = e and line:sub(1, e):gsub(soft_tab, "\t") or ""
return e, indent:gsub(" +", rnd_up and "\t" or "") return e, indent:gsub(" +", rnd_up and "\t" or "")
else else
@ -640,19 +517,17 @@ end
-- * if you are unindenting, the cursor will jump to the start of the line, -- * if you are unindenting, the cursor will jump to the start of the line,
-- and remove the appropriate amount of spaces (or a tab). -- and remove the appropriate amount of spaces (or a tab).
function Doc:indent_text(unindent, line1, col1, line2, col2) function Doc:indent_text(unindent, line1, col1, line2, col2)
local text = self:get_indent_string() local text = get_indent_string()
local _, se = self.lines[line1]:find("^[ \t]+") local _, se = self.lines[line1]:find("^[ \t]+")
local in_beginning_whitespace = col1 == 1 or (se and col1 <= se + 1) local in_beginning_whitespace = col1 == 1 or (se and col1 <= se + 1)
local has_selection = line1 ~= line2 or col1 ~= col2 local has_selection = line1 ~= line2 or col1 ~= col2
if unindent or has_selection or in_beginning_whitespace then if unindent or has_selection or in_beginning_whitespace then
local l1d, l2d = #self.lines[line1], #self.lines[line2] local l1d, l2d = #self.lines[line1], #self.lines[line2]
for line = line1, line2 do for line = line1, line2 do
if not has_selection or #self.lines[line] > 1 then -- don't indent empty lines in a selection local e, rnded = get_line_indent(self.lines[line], unindent)
local e, rnded = self:get_line_indent(self.lines[line], unindent) self:remove(line, 1, line, (e or 0) + 1)
self:remove(line, 1, line, (e or 0) + 1) self:insert(line, 1,
self:insert(line, 1, unindent and rnded:sub(1, #rnded - #text) or rnded .. text)
unindent and rnded:sub(1, #rnded - #text) or rnded .. text)
end
end end
l1d, l2d = #self.lines[line1] - l1d, #self.lines[line2] - l2d l1d, l2d = #self.lines[line1] - l1d, #self.lines[line2] - l2d
if (unindent or in_beginning_whitespace) and not has_selection then if (unindent or in_beginning_whitespace) and not has_selection then

View File

@ -22,73 +22,37 @@ local function init_args(doc, line, col, text, opt)
return doc, line, col, text, opt return doc, line, col, text, opt
end end
-- This function is needed to uniform the behavior of
-- `regex:cmatch` and `string.find`.
local function regex_func(text, re, index, _)
local s, e = re:cmatch(text, index)
return s, e and e - 1
end
local function rfind(func, text, pattern, index, plain)
local s, e = func(text, pattern, 1, plain)
local last_s, last_e
if index < 0 then index = #text - index + 1 end
while e and e <= index do
last_s, last_e = s, e
s, e = func(text, pattern, s + 1, plain)
end
return last_s, last_e
end
function search.find(doc, line, col, text, opt) function search.find(doc, line, col, text, opt)
doc, line, col, text, opt = init_args(doc, line, col, text, opt) doc, line, col, text, opt = init_args(doc, line, col, text, opt)
local plain = not opt.pattern
local pattern = text local re
local search_func = string.find
if opt.regex then if opt.regex then
pattern = regex.compile(text, opt.no_case and "i" or "") re = regex.compile(text, opt.no_case and "i" or "")
search_func = regex_func
end end
local start, finish, step = line, #doc.lines, 1 for line = line, #doc.lines do
if opt.reverse then
start, finish, step = line, 1, -1
end
for line = start, finish, step do
local line_text = doc.lines[line] local line_text = doc.lines[line]
if opt.no_case and not opt.regex then if opt.regex then
line_text = line_text:lower() local s, e = re:cmatch(line_text, col)
end if s then
local s, e return line, s, line, e
if opt.reverse then end
s, e = rfind(search_func, line_text, pattern, col - 1, plain) col = 1
else else
s, e = search_func(line_text, pattern, col, plain) if opt.no_case then
end line_text = line_text:lower()
if s then
local line2 = line
-- If we've matched the newline too,
-- return until the initial character of the next line.
if e >= #doc.lines[line] then
line2 = line + 1
e = 0
end end
-- Avoid returning matches that go beyond the last line. local s, e = line_text:find(text, col, true)
-- This is needed to avoid selecting the "last" newline. if s then
if line2 <= #doc.lines then return line, s, line, e + 1
return line, s, line2, e + 1
end end
col = 1
end end
col = opt.reverse and -1 or 1
end end
if opt.wrap then if opt.wrap then
opt = { no_case = opt.no_case, regex = opt.regex, reverse = opt.reverse } opt = { no_case = opt.no_case, regex = opt.regex }
if opt.reverse then return search.find(doc, 1, 1, text, opt)
return search.find(doc, #doc.lines, #doc.lines[#doc.lines], text, opt)
else
return search.find(doc, 1, 1, text, opt)
end
end end
end end

View File

@ -4,11 +4,9 @@ local config = require "core.config"
local style = require "core.style" local style = require "core.style"
local keymap = require "core.keymap" local keymap = require "core.keymap"
local translate = require "core.doc.translate" local translate = require "core.doc.translate"
local ime = require "core.ime"
local View = require "core.view" local View = require "core.view"
---@class core.docview : core.view
---@field super core.view
local DocView = View:extend() local DocView = View:extend()
DocView.context = "session" DocView.context = "session"
@ -31,9 +29,6 @@ DocView.translate = {
end, end,
["next_page"] = function(doc, line, col, dv) ["next_page"] = function(doc, line, col, dv)
if line == #doc.lines then
return #doc.lines, #doc.lines[line]
end
local min, max = dv:get_visible_line_range() local min, max = dv:get_visible_line_range()
return line + (max - min), 1 return line + (max - min), 1
end, end,
@ -61,33 +56,25 @@ function DocView:new(doc)
self.doc = assert(doc) self.doc = assert(doc)
self.font = "code_font" self.font = "code_font"
self.last_x_offset = {} self.last_x_offset = {}
self.ime_selection = { from = 0, size = 0 }
self.ime_status = false
self.hovering_gutter = false
self.v_scrollbar:set_forced_status(config.force_scrollbar_status)
self.h_scrollbar:set_forced_status(config.force_scrollbar_status)
end end
function DocView:try_close(do_close) function DocView:try_close(do_close)
if self.doc:is_dirty() if self.doc:is_dirty()
and #core.get_views_referencing_doc(self.doc) == 1 then and #core.get_views_referencing_doc(self.doc) == 1 then
core.command_view:enter("Unsaved Changes; Confirm Close", { core.command_view:enter("Unsaved Changes; Confirm Close", function(_, item)
submit = function(_, item) if item.text:match("^[cC]") then
if item.text:match("^[cC]") then do_close()
do_close() elseif item.text:match("^[sS]") then
elseif item.text:match("^[sS]") then self.doc:save()
self.doc:save() do_close()
do_close()
end
end,
suggest = function(text)
local items = {}
if not text:find("^[^cC]") then table.insert(items, "Close Without Saving") end
if not text:find("^[^sS]") then table.insert(items, "Save And Close") end
return items
end end
}) end, function(text)
local items = {}
if not text:find("^[^cC]") then table.insert(items, "Close Without Saving") end
if not text:find("^[^sS]") then table.insert(items, "Save And Close") end
return items
end)
else else
do_close() do_close()
end end
@ -112,16 +99,11 @@ end
function DocView:get_scrollable_size() function DocView:get_scrollable_size()
if not config.scroll_past_end then if not config.scroll_past_end then
local _, _, _, h_scroll = self.h_scrollbar:get_track_rect() return self:get_line_height() * (#self.doc.lines) + style.padding.y * 2
return self:get_line_height() * (#self.doc.lines) + style.padding.y * 2 + h_scroll
end 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
function DocView:get_h_scrollable_size()
return math.huge
end
function DocView:get_font() function DocView:get_font()
return style[self.font] return style[self.font]
@ -139,18 +121,14 @@ function DocView:get_gutter_width()
end end
function DocView:get_line_screen_position(line, col) function DocView:get_line_screen_position(idx)
local x, y = self:get_content_offset() local x, y = self:get_content_offset()
local lh = self:get_line_height() local lh = self:get_line_height()
local gw = self:get_gutter_width() local gw = self:get_gutter_width()
y = y + (line-1) * lh + style.padding.y return x + gw, y + (idx-1) * lh + style.padding.y
if col then
return x + gw + self:get_col_x_offset(line, col), y
else
return x + gw, y
end
end end
function DocView:get_line_text_y_offset() function DocView:get_line_text_y_offset()
local lh = self:get_line_height() local lh = self:get_line_height()
local th = self:get_font():get_height() local th = self:get_font():get_height()
@ -161,40 +139,28 @@ end
function DocView:get_visible_line_range() function DocView:get_visible_line_range()
local x, y, x2, y2 = self:get_content_bounds() local x, y, x2, y2 = self:get_content_bounds()
local lh = self:get_line_height() local lh = self:get_line_height()
local minline = math.max(1, math.floor((y - style.padding.y) / lh) + 1) local minline = math.max(1, math.floor(y / lh))
local maxline = math.min(#self.doc.lines, math.floor((y2 - style.padding.y) / lh) + 1) local maxline = math.min(#self.doc.lines, math.floor(y2 / lh) + 1)
return minline, maxline return minline, maxline
end end
function DocView:get_col_x_offset(line, col) function DocView:get_col_x_offset(line, col)
local default_font = self:get_font() local default_font = self:get_font()
local _, indent_size = self.doc:get_indent_info()
default_font:set_tab_size(indent_size)
local column = 1 local column = 1
local xoffset = 0 local xoffset = 0
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
if font ~= default_font then font:set_tab_size(indent_size) end for char in common.utf8_chars(text) do
local length = #text if column == col then
if column + length <= col then return xoffset / font:subpixel_scale()
xoffset = xoffset + font:get_width(text)
column = column + length
if column >= col then
return xoffset
end
else
for char in common.utf8_chars(text) do
if column >= col then
return xoffset
end
xoffset = xoffset + font:get_width(char)
column = column + #char
end end
xoffset = xoffset + font:get_width_subpixel(char)
column = column + #char
end end
end end
return xoffset return xoffset / default_font:subpixel_scale()
end end
@ -203,27 +169,18 @@ 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 _, indent_size = self.doc:get_indent_info() local subpixel_scale = default_font:subpixel_scale()
default_font:set_tab_size(indent_size) 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
if font ~= default_font then font:set_tab_size(indent_size) end for char in common.utf8_chars(text) do
local width = font:get_width(text) local w = font:get_width_subpixel(char)
-- Don't take the shortcut if the width matches x, if xoffset >= subpixel_scale * x then
-- because we need last_i which should be calculated using utf-8. return (xoffset - x_subpixel > w / 2) and last_i or i
if xoffset + width < x then
xoffset = xoffset + width
i = i + #text
else
for char in common.utf8_chars(text) do
local w = font:get_width(char)
if xoffset >= x then
return (xoffset - x > w / 2) and last_i or i
end
xoffset = xoffset + w
last_i = i
i = i + #char
end end
xoffset = xoffset + w
last_i = i
i = i + #char
end end
end end
@ -243,10 +200,8 @@ end
function DocView:scroll_to_line(line, ignore_if_visible, instant) function DocView:scroll_to_line(line, ignore_if_visible, instant)
local min, max = self:get_visible_line_range() local min, max = self:get_visible_line_range()
if not (ignore_if_visible and line > min and line < max) then if not (ignore_if_visible and line > min and line < max) then
local x, y = self:get_line_screen_position(line) local lh = self:get_line_height()
local ox, oy = self:get_content_offset() self.scroll.to.y = math.max(0, lh * (line - 1) - self.size.y / 2)
local _, _, _, scroll_h = self.h_scrollbar:get_track_rect()
self.scroll.to.y = math.max(0, y - oy - (self.size.y - scroll_h) / 2)
if instant then if instant then
self.scroll.y = self.scroll.to.y self.scroll.y = self.scroll.to.y
end end
@ -255,69 +210,36 @@ end
function DocView:scroll_to_make_visible(line, col) function DocView:scroll_to_make_visible(line, col)
local _, oy = self:get_content_offset() local min = self:get_line_height() * (line - 1)
local _, ly = self:get_line_screen_position(line, col) local max = self:get_line_height() * (line + 2) - self.size.y
local lh = self:get_line_height() self.scroll.to.y = math.min(self.scroll.to.y, min)
local _, _, _, scroll_h = self.h_scrollbar:get_track_rect() self.scroll.to.y = math.max(self.scroll.to.y, max)
self.scroll.to.y = common.clamp(self.scroll.to.y, ly - oy - self.size.y + scroll_h + lh * 2, ly - oy - lh)
local gw = self:get_gutter_width() local gw = self:get_gutter_width()
local xoffset = self:get_col_x_offset(line, col) local xoffset = self:get_col_x_offset(line, col)
local xmargin = 3 * self:get_font():get_width(' ') local xmargin = 3 * self:get_font():get_width(' ')
local xsup = xoffset + gw + xmargin local xsup = xoffset + gw + xmargin
local xinf = xoffset - xmargin local xinf = xoffset - xmargin
local _, _, scroll_w = self.v_scrollbar:get_track_rect() if xsup > self.scroll.x + self.size.x then
local size_x = math.max(0, self.size.x - scroll_w) self.scroll.to.x = xsup - self.size.x
if xsup > self.scroll.x + size_x then
self.scroll.to.x = xsup - size_x
elseif xinf < self.scroll.x then elseif xinf < self.scroll.x then
self.scroll.to.x = math.max(0, xinf) self.scroll.to.x = math.max(0, xinf)
end end
end end
function DocView:on_mouse_moved(x, y, ...)
DocView.super.on_mouse_moved(self, x, y, ...)
self.hovering_gutter = false local function mouse_selection(doc, clicks, line1, col1, line2, col2)
local gw = self:get_gutter_width()
if self:scrollbar_hovering() or self:scrollbar_dragging() then
self.cursor = "arrow"
elseif gw > 0 and x >= self.position.x and x <= (self.position.x + gw) then
self.cursor = "arrow"
self.hovering_gutter = true
else
self.cursor = "ibeam"
end
if self.mouse_selecting then
local l1, c1 = self:resolve_screen_position(x, y)
local l2, c2, snap_type = table.unpack(self.mouse_selecting)
if keymap.modkeys["ctrl"] then
if l1 > l2 then l1, l2 = l2, l1 end
self.doc.selections = { }
for i = l1, l2 do
self.doc:set_selections(i - l1 + 1, i, math.min(c1, #self.doc.lines[i]), i, math.min(c2, #self.doc.lines[i]))
end
else
if snap_type then
l1, c1, l2, c2 = self:mouse_selection(self.doc, snap_type, l1, c1, l2, c2)
end
self.doc:set_selection(l1, c1, l2, c2)
end
end
end
function DocView:mouse_selection(doc, snap_type, line1, col1, line2, col2)
local swap = line2 < line1 or line2 == line1 and col2 <= col1 local swap = line2 < line1 or line2 == line1 and col2 <= col1
if swap then if swap then
line1, col1, line2, col2 = line2, col2, line1, col1 line1, col1, line2, col2 = line2, col2, line1, col1
end end
if snap_type == "word" then if clicks % 4 == 2 then
line1, col1 = translate.start_of_word(doc, line1, col1) line1, col1 = translate.start_of_word(doc, line1, col1)
line2, col2 = translate.end_of_word(doc, line2, col2) line2, col2 = translate.end_of_word(doc, line2, col2)
elseif snap_type == "lines" then elseif clicks % 4 == 3 then
col1, col2 = 1, math.huge 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 end
if swap then if swap then
return line2, col2, line1, col1 return line2, col2, line1, col1
@ -327,30 +249,57 @@ end
function DocView:on_mouse_pressed(button, x, y, clicks) function DocView:on_mouse_pressed(button, x, y, clicks)
if button ~= "left" or not self.hovering_gutter then local caught = DocView.super.on_mouse_pressed(self, button, x, y, clicks)
return DocView.super.on_mouse_pressed(self, button, x, y, clicks) if caught then
return
end end
local line = self:resolve_screen_position(x, y)
if keymap.modkeys["shift"] then if keymap.modkeys["shift"] then
local sline, scol, sline2, scol2 = self.doc:get_selection(true) if clicks % 2 == 1 then
if line > sline then local line1, col1 = select(3, self.doc:get_selection())
self.doc:set_selection(sline, 1, line, #self.doc.lines[line]) local line2, col2 = self:resolve_screen_position(x, y)
else self.doc:set_selection(line2, col2, line1, col1)
self.doc:set_selection(line, 1, sline2, #self.doc.lines[sline2])
end end
else else
if clicks == 1 then local line, col = self:resolve_screen_position(x, y)
self.doc:set_selection(line, 1, line, 1) if keymap.modkeys["ctrl"] then
elseif clicks == 2 then self.doc:add_selection(mouse_selection(self.doc, clicks, line, col, line, col))
self.doc:set_selection(line, 1, line, #self.doc.lines[line]) else
self.doc:set_selection(mouse_selection(self.doc, clicks, line, col, line, col))
end end
self.mouse_selecting = { line, col, clicks = clicks }
end end
return true core.blink_reset()
end end
function DocView:on_mouse_released(...) function DocView:on_mouse_moved(x, y, ...)
DocView.super.on_mouse_released(self, ...) DocView.super.on_mouse_moved(self, x, y, ...)
if self:scrollbar_overlaps_point(x, y) or self.dragging_scrollbar then
self.cursor = "arrow"
else
self.cursor = "ibeam"
end
if self.mouse_selecting then
local l1, c1 = self:resolve_screen_position(x, y)
local l2, c2 = table.unpack(self.mouse_selecting)
local clicks = self.mouse_selecting.clicks
if keymap.modkeys["ctrl"] then
if l1 > l2 then l1, l2 = l2, l1 end
self.doc.selections = { }
for i = l1, l2 do
self.doc:set_selections(i - l1 + 1, i, math.min(c1, #self.doc.lines[i]), i, math.min(c2, #self.doc.lines[i]))
end
else
self.doc:set_selection(mouse_selection(self.doc, clicks, l1, c1, l2, c2))
end
end
end
function DocView:on_mouse_released(button)
DocView.super.on_mouse_released(self, button)
self.mouse_selecting = nil self.mouse_selecting = nil
end end
@ -359,58 +308,39 @@ function DocView:on_text_input(text)
self.doc:text_input(text) self.doc:text_input(text)
end end
function DocView:on_ime_text_editing(text, start, length)
self.doc:ime_text_editing(text, start, length)
self.ime_status = #text > 0
self.ime_selection.from = start
self.ime_selection.size = length
-- Set the composition bounding box that the system IME function DocView:draw_ime_text_editing(text, start, len)
-- will consider when drawing its interface local line, col = self.doc:get_selection()
local line1, col1, line2, col2 = self.doc:get_selection(true) local x, y = self:get_line_screen_position(line)
local col = math.min(col1, col2) x = x + get_col_x_offset(line, col)
self:update_ime_location() local default_font = self:get_font()
self:scroll_to_make_visible(line1, col + start) local subpixel_scale = default_font:subpixel_scale()
local tx, ty = subpixel_scale * x, y + self:get_line_text_y_offset()
renderer.draw_text_subpixel(default_font, text, tx, ty, style.text)
end end
---Update the composition bounding box that the system IME
---will consider when drawing its interface
function DocView:update_ime_location()
if not self.ime_status then return end
local line1, col1, line2, col2 = self.doc:get_selection(true) function DocView:on_ime_text_editing(text, start, len)
local x, y = self:get_line_screen_position(line1) local line, col = self.doc:get_selection()
local x, y = self:get_line_screen_position(line)
x = x + get_col_x_offset(line, col)
local h = self:get_line_height() local h = self:get_line_height()
local col = math.min(col1, col2) local w = self:get_font():get_width(5)
system.set_ime_input_rect(x, y, w, h)
local x1, x2 = 0, 0 core.root_view:defer_draw(draw_ime_text_editing, self, text, start, len)
if self.ime_selection.size > 0 then
-- focus on a part of the text
local from = col + self.ime_selection.from
local to = from + self.ime_selection.size
x1 = self:get_col_x_offset(line1, from)
x2 = self:get_col_x_offset(line1, to)
else
-- focus the whole text
x1 = self:get_col_x_offset(line1, col1)
x2 = self:get_col_x_offset(line2, col2)
end
ime.set_location(x + x1, y, x2 - x1, h)
end end
function DocView:update() function DocView:update()
-- scroll to make caret visible and reset blink timer if it moved -- scroll to make caret visible and reset blink timer if it moved
local line1, col1, line2, col2 = self.doc:get_selection() local line, col = self.doc:get_selection()
if (line1 ~= self.last_line1 or col1 ~= self.last_col1 or if (line ~= self.last_line or col ~= self.last_col) and self.size.x > 0 then
line2 ~= self.last_line2 or col2 ~= self.last_col2) and self.size.x > 0 then if core.active_view == self then
if core.active_view == self and not ime.editing then self:scroll_to_make_visible(line, col)
self:scroll_to_make_visible(line1, col1)
end end
core.blink_reset() core.blink_reset()
self.last_line1, self.last_col1 = line1, col1 self.last_line, self.last_col = line, col
self.last_line2, self.last_col2 = line2, col2
end end
-- update blink timer -- update blink timer
@ -423,8 +353,6 @@ function DocView:update()
core.blink_timer = tb core.blink_timer = tb
end end
self:update_ime_location()
DocView.super.update(self) DocView.super.update(self)
end end
@ -435,24 +363,19 @@ function DocView:draw_line_highlight(x, y)
end end
function DocView:draw_line_text(line, x, y) function DocView:draw_line_text(idx, x, y)
local default_font = self:get_font() local default_font = self:get_font()
local tx, ty = x, y + self:get_line_text_y_offset() local subpixel_scale = default_font:subpixel_scale()
local last_token = nil local tx, ty = subpixel_scale * x, y + self:get_line_text_y_offset()
local tokens = self.doc.highlighter:get_line(line).tokens for _, type, text in self.doc.highlighter:each_token(idx) do
local tokens_count = #tokens
if tokens[tokens_count] ~= nil and string.sub(tokens[tokens_count], -1) == "\n" then
last_token = tokens_count - 1
end
for tidx, type, text in self.doc.highlighter:each_token(line) 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
-- do not render newline, fixes issue #1164 if config.draw_whitespace then
if tidx == last_token then text = text:sub(1, -2) end tx = renderer.draw_text_subpixel(font, text, tx, ty, color, core.replacements, style.syntax.comment)
tx = renderer.draw_text(font, text, tx, ty, color) else
if tx > self.position.x + self.size.x then break end tx = renderer.draw_text_subpixel(font, text, tx, ty, color)
end
end end
return self:get_line_height()
end end
function DocView:draw_caret(x, y) function DocView:draw_caret(x, y)
@ -460,84 +383,47 @@ function DocView:draw_caret(x, y)
renderer.draw_rect(x, y, style.caret_width, lh, style.caret) renderer.draw_rect(x, y, style.caret_width, lh, style.caret)
end end
function DocView:draw_line_body(line, x, y) function DocView:draw_line_body(idx, x, y)
-- draw highlight if any selection ends on this line
local draw_highlight = false
local hcl = config.highlight_current_line
if hcl ~= false then
for lidx, line1, col1, line2, col2 in self.doc:get_selections(false) do
if line1 == line then
if hcl == "no_selection" then
if (line1 ~= line2) or (col1 ~= col2) then
draw_highlight = false
break
end
end
draw_highlight = true
break
end
end
end
if draw_highlight and 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
local lh = self:get_line_height()
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 line >= line1 and line <= line2 then if idx >= line1 and idx <= line2 then
local text = self.doc.lines[line] local text = self.doc.lines[idx]
if line1 ~= line then col1 = 1 end if line1 ~= idx then col1 = 1 end
if line2 ~= line then col2 = #text + 1 end if line2 ~= idx then col2 = #text + 1 end
local x1 = x + self:get_col_x_offset(line, col1) local x1 = x + self:get_col_x_offset(idx, col1)
local x2 = x + self:get_col_x_offset(line, col2) local x2 = x + self:get_col_x_offset(idx, col2)
local lh = self:get_line_height()
if x1 ~= x2 then if x1 ~= x2 then
renderer.draw_rect(x1, y, x2 - x1, lh, style.selection) renderer.draw_rect(x1, y, x2 - x1, lh, style.selection)
end end
end end
end end
local draw_highlight = nil
for lidx, line1, col1, line2, col2 in self.doc:get_selections(true) do
-- draw line highlight if caret is on this line
if draw_highlight ~= false and config.highlight_current_line
and line1 == idx and core.active_view == self then
draw_highlight = (line1 == line2 and col1 == col2)
end
end
if draw_highlight then self:draw_line_highlight(x + self.scroll.x, y) end
-- draw line's text -- draw line's text
return self:draw_line_text(line, x, y) self:draw_line_text(idx, x, y)
end end
function DocView:draw_line_gutter(line, x, y, width) function DocView:draw_line_gutter(idx, x, y, width)
local color = style.line_number local color = style.line_number
for _, line1, _, line2 in self.doc:get_selections(true) do for _, line1, _, line2 in self.doc:get_selections(true) do
if line >= line1 and line <= line2 then if idx >= line1 and idx <= line2 then
color = style.line_number2 color = style.line_number2
break break
end end
end end
local yoffset = self:get_line_text_y_offset()
x = x + style.padding.x x = x + style.padding.x
local lh = self:get_line_height() common.draw_text(self:get_font(), color, idx, "right", x, y + yoffset, width, self:get_line_height())
common.draw_text(self:get_font(), color, line, "right", x, y, width, lh)
return lh
end
function DocView:draw_ime_decoration(line1, col1, line2, col2)
local x, y = self:get_line_screen_position(line1)
local line_size = math.max(1, SCALE)
local lh = self:get_line_height()
-- Draw IME underline
local x1 = self:get_col_x_offset(line1, col1)
local x2 = self:get_col_x_offset(line2, col2)
renderer.draw_rect(x + math.min(x1, x2), y + lh - line_size, math.abs(x1 - x2), line_size, style.text)
-- Draw IME selection
local col = math.min(col1, col2)
local from = col + self.ime_selection.from
local to = from + self.ime_selection.size
x1 = self:get_col_x_offset(line1, from)
if from ~= to then
x2 = self:get_col_x_offset(line1, to)
line_size = style.caret_width
renderer.draw_rect(x + math.min(x1, x2), y + lh - line_size, math.abs(x1 - x2), line_size, style.caret)
end
self:draw_caret(x + x1, y)
end end
@ -546,16 +432,13 @@ function DocView:draw_overlay()
local minline, maxline = self:get_visible_line_range() local minline, maxline = self:get_visible_line_range()
-- draw caret if it overlaps this line -- draw caret if it overlaps this line
local T = config.blink_period local T = config.blink_period
for _, line1, col1, line2, col2 in self.doc:get_selections() do for _, line, col in self.doc:get_selections() do
if line1 >= minline and line1 <= maxline if line >= minline and line <= maxline
and system.window_has_focus() then and system.window_has_focus() then
if ime.editing then if config.disable_blink
self:draw_ime_decoration(line1, col1, line2, col2) or (core.blink_timer - core.blink_start) % T < T / 2 then
else 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
self:draw_caret(self:get_line_screen_position(line1, col1))
end
end end
end end
end end
@ -564,8 +447,8 @@ end
function DocView:draw() function DocView:draw()
self:draw_background(style.background) self:draw_background(style.background)
local _, indent_size = self.doc:get_indent_info()
self:get_font():set_tab_size(indent_size) self:get_font():set_tab_size(config.indent_size)
local minline, maxline = self:get_visible_line_range() local minline, maxline = self:get_visible_line_range()
local lh = self:get_line_height() local lh = self:get_line_height()
@ -573,16 +456,16 @@ function DocView:draw()
local x, y = self:get_line_screen_position(minline) local x, y = self:get_line_screen_position(minline)
local gw, gpad = self:get_gutter_width() local gw, gpad = self:get_gutter_width()
for i = minline, maxline do for i = minline, maxline do
y = y + (self:draw_line_gutter(i, self.position.x, y, gpad and gw - gpad or gw) or lh) self:draw_line_gutter(i, self.position.x, y, gpad and gw - gpad or gw)
y = y + lh
end end
local pos = self.position local pos = self.position
x, y = self:get_line_screen_position(minline) x, y = self:get_line_screen_position(minline)
-- the clip below ensure we don't write on the gutter region. On the core.push_clip_rect(pos.x + gw, pos.y, self.size.x, self.size.y)
-- right side it is redundant with the Node's clip.
core.push_clip_rect(pos.x + gw, pos.y, self.size.x - gw, self.size.y)
for i = minline, maxline do for i = minline, maxline do
y = y + (self:draw_line_body(i, x, y) or lh) self:draw_line_body(i, x, y)
y = y + lh
end end
self:draw_overlay() self:draw_overlay()
core.pop_clip_rect() core.pop_clip_rect()

View File

@ -1,52 +0,0 @@
local style = require "core.style"
local keymap = require "core.keymap"
local View = require "core.view"
---@class core.emptyview : core.view
---@field super core.view
local EmptyView = View:extend()
local function draw_text(x, y, color)
local lines = {
{ fmt = "%s to run a command", cmd = "core:find-command" },
{ fmt = "%s to open a file from the project", cmd = "core:find-file" },
{ fmt = "%s to change project folder", cmd = "core:change-project-folder" },
{ fmt = "%s to open a project folder", cmd = "core:open-project-folder" },
}
local th = style.big_font:get_height()
local dh = 2 * th + style.padding.y * 2
local x1, y1 = x, y + ((dh - th) / #lines)
local xv = x1
local title = "Lite XL"
local version = "version " .. VERSION
local title_width = style.big_font:get_width(title)
local version_width = style.font:get_width(version)
if version_width > title_width then
version = VERSION
version_width = style.font:get_width(version)
xv = x1 - (version_width - title_width)
end
x = renderer.draw_text(style.big_font, title, x1, y1, color)
renderer.draw_text(style.font, version, xv, y1 + th, color)
x = x + style.padding.x
renderer.draw_rect(x, y, math.ceil(1 * SCALE), dh, color)
th = style.font:get_height()
y = y + (dh - (th + style.padding.y) * #lines) / 2
local w = 0
for _, line in ipairs(lines) do
local text = string.format(line.fmt, keymap.get_binding(line.cmd))
w = math.max(w, renderer.draw_text(style.font, text, x + style.padding.x, y, color))
y = y + th + style.padding.y
end
return w, dh
end
function EmptyView:draw()
self:draw_background(style.background)
local w, h = draw_text(0, 0, { 0, 0, 0, 0 })
local x = self.position.x + math.max(style.padding.x, (self.size.x - w) / 2)
local y = self.position.y + (self.size.y - h) / 2
draw_text(x, y, style.dim)
end
return EmptyView

View File

@ -1,92 +0,0 @@
local core = require "core"
local ime = { }
function ime.reset()
ime.editing = false
ime.last_location = { x = 0, y = 0, w = 0, h = 0 }
end
---Convert from utf-8 offset and length (from SDL) to byte offsets
---@param text string @Textediting string
---@param start integer @0-based utf-8 offset of the starting position of the selection
---@param length integer @Size of the utf-8 length of the selection
function ime.ingest(text, start, length)
if #text == 0 then
-- finished textediting
ime.reset()
return "", 0, 0
end
ime.editing = true
if start < 0 then
-- we assume no selection and caret at the end
return text, #text, 0
end
-- start is 0-based, so we use start + 1
local start_byte = utf8.offset(text, start + 1)
if not start_byte then
-- bad start offset
-- we assume it meant the last byte of the text
start_byte = #text
else
start_byte = math.min(start_byte - 1, #text)
end
if length < 0 then
-- caret only
return text, start_byte, 0
end
local end_byte = utf8.offset(text, start + length + 1)
if not end_byte or end_byte - 1 < start_byte then
-- bad length, assume caret only
return text, start_byte, 0
end
end_byte = math.min(end_byte - 1, #text)
return text, start_byte, end_byte - start_byte
end
---Forward the given textediting SDL event data to Views.
---@param text string @Textediting string
---@param start integer @0-based utf-8 offset of the starting position of the selection
---@param length integer @Size of the utf-8 length of the selection
function ime.on_text_editing(text, start, length, ...)
if ime.editing or #text > 0 then
core.root_view:on_ime_text_editing(ime.ingest(text, start, length, ...))
end
end
---Stop IME composition.
---Might not completely work on every platform.
function ime.stop()
if ime.editing then
-- SDL_ClearComposition for now doesn't work everywhere
system.clear_ime()
ime.on_text_editing("", 0, 0)
end
end
---Set the bounding box of the text pertaining the IME.
---The IME will draw its interface based on this info.
---@param x number
---@param y number
---@param w number
---@param h number
function ime.set_location(x, y, w, h)
if not ime.last_location or
ime.last_location.x ~= x or
ime.last_location.y ~= y or
ime.last_location.w ~= w or
ime.last_location.h ~= h
then
ime.last_location.x, ime.last_location.y, ime.last_location.w, ime.last_location.h = x, y, w, h
system.set_text_input_rect(x, y, w, h)
end
end
ime.reset()
return ime

File diff suppressed because it is too large Load Diff

View File

@ -6,7 +6,6 @@ local function keymap_macos(keymap)
["cmd+n"] = "core:new-doc", ["cmd+n"] = "core:new-doc",
["cmd+shift+c"] = "core:change-project-folder", ["cmd+shift+c"] = "core:change-project-folder",
["cmd+shift+o"] = "core:open-project-folder", ["cmd+shift+o"] = "core:open-project-folder",
["cmd+option+r"] = "core:restart",
["cmd+ctrl+return"] = "core:toggle-fullscreen", ["cmd+ctrl+return"] = "core:toggle-fullscreen",
["cmd+ctrl+shift+j"] = "root:split-left", ["cmd+ctrl+shift+j"] = "root:split-left",
@ -33,10 +32,6 @@ 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",
["hwheel"] = "root:horizontal-scroll",
["shift+hwheel"] = "root:horizontal-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",
@ -98,11 +93,6 @@ local function keymap_macos(keymap)
["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",

View File

@ -1,165 +1,49 @@
local core = require "core"
local command = require "core.command" local command = require "core.command"
local config = require "core.config"
local ime = require "core.ime"
local keymap = {} local keymap = {}
---@alias keymap.shortcut string
---@alias keymap.command string
---@alias keymap.modkey string
---@alias keymap.pressed boolean
---@alias keymap.map table<keymap.shortcut,keymap.command|keymap.command[]>
---@alias keymap.rmap table<keymap.command, keymap.shortcut|keymap.shortcut[]>
---Pressed status of mod keys.
---@type table<keymap.modkey, keymap.pressed>
keymap.modkeys = {} keymap.modkeys = {}
---List of commands assigned to a shortcut been the key of the map the shortcut.
---@type keymap.map
keymap.map = {} keymap.map = {}
---List of shortcuts assigned to a command been the key of the map the command.
---@type keymap.rmap
keymap.reverse_map = {} keymap.reverse_map = {}
local macos = PLATFORM == "Mac OS X" local macos = PLATFORM == "Mac OS X"
local os4 = PLATFORM == "AmigaOS 4"
local mos = PLATFORM == "MORPHOS"
-- 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 os4 and "os4" or mos and "mos" or "generic")) local modkeys_os = require("core.modkeys-" .. (macos and "macos" or "generic"))
---@type table<keymap.modkey, keymap.modkey>
local modkey_map = modkeys_os.map local modkey_map = modkeys_os.map
---@type keymap.modkey[]
local modkeys = modkeys_os.keys local modkeys = modkeys_os.keys
local function key_to_stroke(k)
---Normalizes a stroke sequence to follow the modkeys table local stroke = ""
---@param stroke string
---@return string
local function normalize_stroke(stroke)
local stroke_table = {}
for key in stroke:gmatch("[^+]+") do
table.insert(stroke_table, key)
end
table.sort(stroke_table, function(a, b)
if a == b then return false end
for _, mod in ipairs(modkeys) do
if a == mod or b == mod then
return a == mod
end
end
return a < b
end)
return table.concat(stroke_table, "+")
end
---Generates a stroke sequence including currently pressed mod keys.
---@param key string
---@return string
local function key_to_stroke(key)
local keys = { key }
for _, mk in ipairs(modkeys) do for _, mk in ipairs(modkeys) do
if keymap.modkeys[mk] then if keymap.modkeys[mk] then
table.insert(keys, mk) stroke = stroke .. mk .. "+"
end end
end end
return normalize_stroke(table.concat(keys, "+")) return stroke .. k
end end
---Remove the given value from an array associated to a key in a table.
---@param tbl table<string, string> The table containing the key
---@param k string The key containing the array
---@param v? string The value to remove from the array
local function remove_only(tbl, k, v)
if tbl[k] then
if v then
local j = 0
for i=1, #tbl[k] do
while tbl[k][i + j] == v do
j = j + 1
end
tbl[k][i] = tbl[k][i + j]
end
else
tbl[k] = nil
end
end
end
---Removes from a keymap.map the bindings that are already registered.
---@param map keymap.map
local function remove_duplicates(map)
for stroke, commands in pairs(map) do
local normalized_stroke = normalize_stroke(stroke)
if type(commands) == "string" or type(commands) == "function" then
commands = { commands }
end
if keymap.map[normalized_stroke] then
for _, registered_cmd in ipairs(keymap.map[normalized_stroke]) do
local j = 0
for i=1, #commands do
while commands[i + j] == registered_cmd do
j = j + 1
end
commands[i] = commands[i + j]
end
end
end
if #commands < 1 then
map[stroke] = nil
else
map[stroke] = commands
end
end
end
---Add bindings by replacing commands that were previously assigned to a shortcut.
---@param map keymap.map
function keymap.add_direct(map) function keymap.add_direct(map)
for stroke, commands in pairs(map) do for stroke, commands in pairs(map) do
stroke = normalize_stroke(stroke) if type(commands) == "string" then
if type(commands) == "string" or type(commands) == "function" then
commands = { commands } commands = { commands }
end end
if keymap.map[stroke] then
for _, cmd in ipairs(keymap.map[stroke]) do
remove_only(keymap.reverse_map, cmd, stroke)
end
end
keymap.map[stroke] = commands keymap.map[stroke] = commands
for _, cmd in ipairs(commands) do for _, cmd in ipairs(commands) do
keymap.reverse_map[cmd] = keymap.reverse_map[cmd] or {} keymap.reverse_map[cmd] = stroke
table.insert(keymap.reverse_map[cmd], stroke)
end end
end end
end end
---Adds bindings by appending commands to already registered shortcut or by
---replacing currently assigned commands if overwrite is specified.
---@param map keymap.map
---@param overwrite? boolean
function keymap.add(map, overwrite) function keymap.add(map, overwrite)
remove_duplicates(map)
for stroke, commands in pairs(map) do for stroke, commands in pairs(map) do
if macos then if macos then
stroke = stroke:gsub("%f[%a]ctrl%f[%A]", "cmd") stroke = stroke:gsub("%f[%a]ctrl%f[%A]", "cmd")
end end
stroke = normalize_stroke(stroke) if type(commands) == "string" then
commands = { commands }
end
if overwrite then if overwrite then
if keymap.map[stroke] then
for _, cmd in ipairs(keymap.map[stroke]) do
remove_only(keymap.reverse_map, cmd, stroke)
end
end
keymap.map[stroke] = commands keymap.map[stroke] = commands
else else
keymap.map[stroke] = keymap.map[stroke] or {} keymap.map[stroke] = keymap.map[stroke] or {}
@ -168,43 +52,18 @@ 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] = keymap.reverse_map[cmd] or {} keymap.reverse_map[cmd] = stroke
table.insert(keymap.reverse_map[cmd], stroke)
end end
end end
end end
---Unregisters the given shortcut and associated command.
---@param shortcut string
---@param cmd string
function keymap.unbind(shortcut, cmd)
shortcut = normalize_stroke(shortcut)
remove_only(keymap.map, shortcut, cmd)
remove_only(keymap.reverse_map, cmd, shortcut)
end
---Returns all the shortcuts associated to a command unpacked for easy assignment.
---@param cmd string
---@return ...
function keymap.get_binding(cmd) function keymap.get_binding(cmd)
return table.unpack(keymap.reverse_map[cmd] or {})
end
---Returns all the shortcuts associated to a command packed in a table.
---@param cmd string
---@return table<integer, string> | nil shortcuts
function keymap.get_bindings(cmd)
return keymap.reverse_map[cmd] return keymap.reverse_map[cmd]
end end
-------------------------------------------------------------------------------- function keymap.on_key_pressed(k)
-- Events listening
--------------------------------------------------------------------------------
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
@ -214,62 +73,18 @@ function keymap.on_key_pressed(k, ...)
end end
else else
local stroke = key_to_stroke(k) local stroke = key_to_stroke(k)
local commands, performed = keymap.map[stroke], false local commands = keymap.map[stroke]
if commands then if commands then
for _, cmd in ipairs(commands) do for _, cmd in ipairs(commands) do
if type(cmd) == "function" then local performed = command.perform(cmd)
local ok, res = core.try(cmd, ...)
if ok then
performed = not (res == false)
else
performed = true
end
else
performed = command.perform(cmd, ...)
end
if performed then break end if performed then break end
end end
return performed return true
end end
end end
return false return false
end end
function keymap.on_mouse_wheel(delta_y, delta_x, ...)
local y_direction = delta_y > 0 and "up" or "down"
local x_direction = delta_x > 0 and "left" or "right"
-- Try sending a "cumulative" event for both scroll directions
if delta_y ~= 0 and delta_x ~= 0 then
local result = keymap.on_key_pressed("wheel" .. y_direction .. x_direction, delta_y, delta_x, ...)
if not result then
result = keymap.on_key_pressed("wheelyx", delta_y, delta_x, ...)
end
if result then return true end
end
-- Otherwise send each direction as its own separate event
local y_result, x_result
if delta_y ~= 0 then
y_result = keymap.on_key_pressed("wheel" .. y_direction, delta_y, ...)
if not y_result then
y_result = keymap.on_key_pressed("wheel", delta_y, ...)
end
end
if delta_x ~= 0 then
x_result = keymap.on_key_pressed("wheel" .. x_direction, delta_x, ...)
if not x_result then
x_result = keymap.on_key_pressed("hwheel", delta_x, ...)
end
end
return y_result or x_result
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]
@ -279,9 +94,6 @@ function keymap.on_key_released(k)
end end
--------------------------------------------------------------------------------
-- Register default bindings
--------------------------------------------------------------------------------
if macos then if macos then
local keymap_macos = require("core.keymap-macos") local keymap_macos = require("core.keymap-macos")
keymap_macos(keymap) keymap_macos(keymap)
@ -295,7 +107,6 @@ keymap.add_direct {
["ctrl+n"] = "core:new-doc", ["ctrl+n"] = "core:new-doc",
["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",
["ctrl+alt+r"] = "core:restart",
["alt+return"] = "core:toggle-fullscreen", ["alt+return"] = "core:toggle-fullscreen",
["f11"] = "core:toggle-fullscreen", ["f11"] = "core:toggle-fullscreen",
@ -322,9 +133,6 @@ 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",
["hwheel"] = "root:horizontal-scroll",
["shift+wheel"] = "root:horizontal-scroll",
["ctrl+f"] = "find-replace:find", ["ctrl+f"] = "find-replace:find",
["ctrl+r"] = "find-replace:replace", ["ctrl+r"] = "find-replace:replace",
@ -362,11 +170,9 @@ keymap.add_direct {
["ctrl+a"] = "doc:select-all", ["ctrl+a"] = "doc:select-all",
["ctrl+d"] = { "find-replace:select-add-next", "doc:select-word" }, ["ctrl+d"] = { "find-replace:select-add-next", "doc:select-word" },
["ctrl+f3"] = "find-replace:select-next", ["ctrl+f3"] = "find-replace:select-next",
["ctrl+shift+f3"] = "find-replace:select-previous",
["ctrl+l"] = "doc:select-lines", ["ctrl+l"] = "doc:select-lines",
["ctrl+shift+l"] = { "find-replace:select-add-all", "doc:select-word" }, ["ctrl+shift+l"] = { "find-replace:select-add-all", "doc:select-word" },
["ctrl+/"] = "doc:toggle-line-comments", ["ctrl+/"] = "doc:toggle-line-comments",
["ctrl+shift+/"] = "doc:toggle-block-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",
["ctrl+shift+d"] = "doc:duplicate-lines", ["ctrl+shift+d"] = "doc:duplicate-lines",
@ -387,11 +193,6 @@ keymap.add_direct {
["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",

View File

@ -1,7 +1,5 @@
local core = require "core" local core = require "core"
local common = require "core.common" local common = require "core.common"
local config = require "core.config"
local keymap = require "core.keymap"
local style = require "core.style" local style = require "core.style"
local View = require "core.view" local View = require "core.view"
@ -38,15 +36,12 @@ local LogView = View:extend()
LogView.context = "session" 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.expanding = {}
self.scrollable = true self.scrollable = true
self.yoffset = 0 self.yoffset = 0
core.status_view:show_message("i", style.text, "ctrl+click to copy entry")
end end
@ -82,73 +77,45 @@ function LogView:each_item()
end end
function LogView:get_scrollable_size() function LogView:on_mouse_moved(px, py, ...)
local _, y_off = self:get_content_offset() LogView.super.on_mouse_moved(self, px, py, ...)
local last_y, last_h = 0, 0 local hovered = false
for i, item, x, y, w, h in self:each_item() do for _, item, x, y, w, h in self:each_item() do
last_y, last_h = y, h
end
if not config.scroll_past_end then
return last_y + last_h - y_off + style.padding.y
end
return last_y + self.size.y - y_off
end
function LogView:on_mouse_pressed(button, px, py, clicks)
if LogView.super.on_mouse_pressed(self, button, px, py, clicks) then
return true
end
local index, selected
for i, item, x, y, w, h in self:each_item() do
if px >= x and py >= y and px < x + w and py < y + h then if px >= x and py >= y and px < x + w and py < y + h then
index = i hovered = true
selected = item self.hovered_item = item
break break
end end
end end
if not hovered then self.hovered_item = nil end
end
if selected then
if keymap.modkeys["ctrl"] then function LogView:on_mouse_pressed(button, mx, my, clicks)
system.set_clipboard(core.get_log(selected)) if LogView.super.on_mouse_pressed(self, button, mx, my, clicks) then return end
core.status_view:show_message("i", style.text, "copied entry #"..index.." to clipboard.") if self.hovered_item then
else self:expand_item(self.hovered_item)
self:expand_item(selected)
end
end end
return true
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
local lh = style.font:get_height() + style.padding.y
if 0 < self.scroll.to.y then
local index = #core.log_items
while index > 1 and self.last_item ~= core.log_items[index] do
index = index - 1
end
local diff_index = #core.log_items - index
self.scroll.to.y = self.scroll.to.y + diff_index * lh
self.scroll.y = self.scroll.to.y
else
self.yoffset = -lh
end
self.last_item = item self.last_item = item
self.scroll.to.y = 0
self.yoffset = -(style.font:get_height() + style.padding.y)
end end
local expanding = self.expanding[1] local expanding = self.expanding[1]
if expanding then if expanding then
self:move_towards(expanding, "current", expanding.target, nil, "logview") self:move_towards(expanding, "current", expanding.target)
if expanding.current == expanding.target then if expanding.current == expanding.target then
table.remove(self.expanding, 1) table.remove(self.expanding, 1)
end end
end end
self:move_towards("yoffset", 0, nil, "logview") self:move_towards("yoffset", 0)
LogView.super.update(self) LogView.super.update(self)
end end
@ -164,62 +131,41 @@ local function draw_text_multiline(font, text, x, y, color)
return resx, y return resx, y
end end
-- this is just to get a date string that's consistent
local datestr = os.date()
function LogView:draw() function LogView:draw()
self:draw_background(style.background) self:draw_background(style.background)
local th = style.font:get_height() local th = style.font:get_height()
local lh = th + style.padding.y -- for one line local lh = th + style.padding.y -- for one line
local iw = math.max( for _, item, x, y, w in self:each_item() do
style.icon_font:get_width(style.log.ERROR.icon), x = x + style.padding.x
style.icon_font:get_width(style.log.INFO.icon)
)
local tw = style.font:get_width(datestr) local time = os.date(nil, item.time)
for _, item, x, y, w, h in self:each_item() do x = common.draw_text(style.font, style.dim, time, "left", x, y, w, lh)
if y + h >= self.position.y and y <= self.position.y + self.size.y then x = x + style.padding.x
core.push_clip_rect(x, y, w, h)
x = x + style.padding.x
x = common.draw_text( x = common.draw_text(style.code_font, style.dim, is_expanded(item) and "-" or "+", "left", x, y, w, lh)
style.icon_font, x = x + style.padding.x
style.log[item.level].color, w = w - (x - self:get_content_offset())
style.log[item.level].icon,
"center",
x, y, iw, lh
)
x = x + style.padding.x
-- timestamps are always 15% of the width if is_expanded(item) then
local time = os.date(nil, item.time) y = y + common.round(style.padding.y / 2)
common.draw_text(style.font, style.dim, time, "left", x, y, tw, lh) _, y = draw_text_multiline(style.font, item.text, x, y, style.text)
x = x + tw + style.padding.x
w = w - (x - self:get_content_offset()) local at = "at " .. common.home_encode(item.at)
_, y = common.draw_text(style.font, style.dim, at, "left", x, y, w, lh)
if is_expanded(item) then if item.info then
y = y + common.round(style.padding.y / 2) _, y = draw_text_multiline(style.font, item.info, x, y, style.dim)
_, 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
else
core.pop_clip_rect() 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
end end
LogView.super.draw_scrollbar(self)
end end

View File

@ -9,6 +9,6 @@ modkeys.map = {
["right alt"] = "altgr", ["right alt"] = "altgr",
} }
modkeys.keys = { "ctrl", "shift", "alt", "altgr" } modkeys.keys = { "ctrl", "alt", "altgr", "shift" }
return modkeys return modkeys

View File

@ -13,6 +13,6 @@ modkeys.map = {
["right alt"] = "altgr", ["right alt"] = "altgr",
} }
modkeys.keys = { "ctrl", "alt", "option", "altgr", "shift", "cmd" } modkeys.keys = { "cmd", "ctrl", "alt", "option", "altgr", "shift" }
return modkeys return modkeys

View File

@ -1,15 +0,0 @@
local modkeys = {}
modkeys.map = {
["left amiga"] = "cmd",
["right amiga"] = "cmd",
["control"] = "ctrl",
["left shift"] = "shift",
["right shift"] = "shift",
["left alt"] = "alt",
["right alt"] = "altgr",
}
modkeys.keys = { "cmd", "ctrl", "alt", "altgr", "shift" }
return modkeys

View File

@ -1,15 +0,0 @@
local modkeys = {}
modkeys.map = {
["left amiga"] = "cmd",
["right amiga"] = "cmd",
["control"] = "ctrl",
["left shift"] = "shift",
["right shift"] = "shift",
["left alt"] = "alt",
["right alt"] = "altgr",
}
modkeys.keys = { "cmd", "ctrl", "alt", "altgr", "shift" }
return modkeys

View File

@ -11,19 +11,13 @@ local UNDERLINE_MARGIN = common.round(1 * SCALE)
local noop = function() end local noop = function() end
---@class core.nagview : core.view
---@field super core.view
local NagView = View:extend() local NagView = View:extend()
function NagView:new() function NagView:new()
NagView.super.new(self) NagView.super.new(self)
self.size.y = 0 self.size.y = 0
self.show_height = 0
self.force_focus = false self.force_focus = false
self.queue = {} self.queue = {}
self.scrollable = true
self.target_height = 0
self.on_mouse_pressed_root = nil
end end
function NagView:get_title() function NagView:get_title()
@ -52,20 +46,20 @@ function NagView:get_target_height()
return self.target_height + 2 * style.padding.y return self.target_height + 2 * style.padding.y
end end
function NagView:get_scrollable_size() function NagView:update()
local w, h = system.get_window_size() NagView.super.update(self)
if self.visible and self:get_target_height() > h then
self.size.y = h if core.active_view == self and self.title then
return self:get_target_height() self:move_towards(self.size, "y", self:get_target_height())
self:move_towards(self, "underline_progress", 1)
else else
self.size.y = 0 self:move_towards(self.size, "y", 0)
end end
return 0
end end
function NagView:dim_window_content() function NagView:draw_overlay()
local ox, oy = self:get_content_offset() local ox, oy = self:get_content_offset()
oy = oy + self.show_height oy = oy + self.size.y
local w, h = core.root_view.size.x, core.root_view.size.y - oy local w, h = core.root_view.size.x, core.root_view.size.y - oy
core.root_view:defer_draw(function() core.root_view:defer_draw(function()
renderer.draw_rect(ox, oy, w, h, style.nagbar_dim) renderer.draw_rect(ox, oy, w, h, style.nagbar_dim)
@ -87,11 +81,11 @@ function NagView:each_option()
bh = self:get_buttons_height() bh = self:get_buttons_height()
ox,oy = self:get_content_offset() ox,oy = self:get_content_offset()
ox = ox + self.size.x ox = ox + self.size.x
oy = oy + self.show_height - bh - style.padding.y oy = oy + self.size.y - bh - style.padding.y
for i = #self.options, 1, -1 do for i = #self.options, 1, -1 do
opt = self.options[i] opt = self.options[i]
bw = style.font:get_width(opt.text) + 2 * BORDER_WIDTH + style.padding.x bw = opt.font:get_width(opt.text) + 2 * BORDER_WIDTH + style.padding.x
ox = ox - bw - style.padding.x ox = ox - bw - style.padding.x
coroutine.yield(i, opt, ox,oy,bw,bh) coroutine.yield(i, opt, ox,oy,bw,bh)
@ -100,8 +94,6 @@ function NagView:each_option()
end end
function NagView:on_mouse_moved(mx, my, ...) function NagView:on_mouse_moved(mx, my, ...)
if not self.visible then return end
core.set_active_view(self)
NagView.super.on_mouse_moved(self, mx, my, ...) NagView.super.on_mouse_moved(self, mx, my, ...)
for i, _, x,y,w,h in self:each_option() do for i, _, x,y,w,h in self:each_option() do
if mx >= x and my >= y and mx < x + w and my < y + h then if mx >= x and my >= y and mx < x + w and my < y + h then
@ -111,55 +103,18 @@ function NagView:on_mouse_moved(mx, my, ...)
end end
end end
local function register_mouse_pressed(self)
if self.on_mouse_pressed_root then return end
-- RootView is loaded locally to avoid NagView and RootView being
-- mutually recursive
local RootView = require "core.rootview"
self.on_mouse_pressed_root = RootView.on_mouse_pressed
local this = self
function RootView:on_mouse_pressed(button, x, y, clicks)
if
not this:on_mouse_pressed(button, x, y, clicks)
then
return this.on_mouse_pressed_root(self, button, x, y, clicks)
else
return true
end
end
self.new_on_mouse_pressed_root = RootView.on_mouse_pressed
end
local function unregister_mouse_pressed(self)
local RootView = require "core.rootview"
if
self.on_mouse_pressed_root
and
-- just in case prevent overwriting what something else may
-- have overwrote after us, but after testing with various
-- plugins this doesn't seems to happen, but just in case
self.new_on_mouse_pressed_root == RootView.on_mouse_pressed
then
RootView.on_mouse_pressed = self.on_mouse_pressed_root
self.on_mouse_pressed_root = nil
self.new_on_mouse_pressed_root = nil
end
end
function NagView:on_mouse_pressed(button, mx, my, clicks) function NagView:on_mouse_pressed(button, mx, my, clicks)
if not self.visible then return false end if NagView.super.on_mouse_pressed(self, button, mx, my, clicks) then return end
if NagView.super.on_mouse_pressed(self, button, mx, my, clicks) then return true end
for i, _, x,y,w,h in self:each_option() do for i, _, x,y,w,h in self:each_option() do
if mx >= x and my >= y and mx < x + w and my < y + h then if mx >= x and my >= y and mx < x + w and my < y + h then
self:change_hovered(i) self:change_hovered(i)
command.perform "dialog:select" command.perform "dialog:select"
break
end end
end end
return true
end end
function NagView:on_text_input(text) function NagView:on_text_input(text)
if not self.visible then return end
if text:lower() == "y" then if text:lower() == "y" then
command.perform "dialog:select-yes" command.perform "dialog:select-yes"
elseif text:lower() == "n" then elseif text:lower() == "n" then
@ -167,39 +122,20 @@ function NagView:on_text_input(text)
end end
end end
function NagView:update()
if not self.visible and self.show_height <= 0 then return end
NagView.super.update(self)
if self.visible and core.active_view == self and self.title then function NagView:draw()
self:move_towards(self, "show_height", self:get_target_height(), nil, "nagbar") if self.size.y <= 0 or not self.title then return end
self:move_towards(self, "underline_progress", 1, nil, "nagbar")
else
self:move_towards(self, "show_height", 0, nil, "nagbar")
if self.show_height <= 0 then
self.title = nil
self.message = nil
self.options = nil
self.on_selected = nil
end
end
end
local function draw_nagview_message(self) self:draw_overlay()
self:dim_window_content() self:draw_background(style.nagbar)
-- draw message's background
local ox, oy = self:get_content_offset() local ox, oy = self:get_content_offset()
renderer.draw_rect(ox, oy, self.size.x, self.show_height, style.nagbar)
ox = ox + style.padding.x ox = ox + style.padding.x
core.push_clip_rect(ox, oy, self.size.x, self.show_height)
-- if there are other items, show it -- if there are other items, show it
if #self.queue > 0 then if #self.queue > 0 then
local str = string.format("[%d]", #self.queue) local str = string.format("[%d]", #self.queue)
ox = common.draw_text(style.font, style.nagbar_text, str, "left", ox, oy, self.size.x, self.show_height) ox = common.draw_text(style.font, style.nagbar_text, str, "left", ox, oy, self.size.x, self.size.y)
ox = ox + style.padding.x ox = ox + style.padding.x
end end
@ -230,29 +166,8 @@ local function draw_nagview_message(self)
renderer.draw_rect(lx,ly,uw,UNDERLINE_WIDTH, style.nagbar_text) renderer.draw_rect(lx,ly,uw,UNDERLINE_WIDTH, style.nagbar_text)
end end
common.draw_text(style.font, style.nagbar_text, opt.text, "center", fx,fy,fw,fh) common.draw_text(opt.font, style.nagbar_text, opt.text, "center", fx,fy,fw,fh)
end end
self:draw_scrollbar()
core.pop_clip_rect()
end
function NagView:draw()
if (not self.visible and self.show_height <= 0) or not self.title then
return
end
core.root_view:defer_draw(draw_nagview_message, self)
end
function NagView:on_scale_change(new_scale, old_scale)
BORDER_WIDTH = common.round(1 * new_scale)
UNDERLINE_WIDTH = common.round(2 * new_scale)
UNDERLINE_MARGIN = common.round(1 * new_scale)
self.target_height = math.max(
self:get_message_height(),
self:get_buttons_height()
)
end end
function NagView:get_message_height() function NagView:get_message_height()
@ -263,31 +178,23 @@ function NagView:get_message_height()
return h return h
end end
function NagView:next() function NagView:next()
local opts = table.remove(self.queue, 1) or {} local opts = table.remove(self.queue, 1) or {}
if opts.title and opts.message and opts.options then self.title = opts.title
self.visible = true self.message = opts.message and opts.message .. "\n"
self.title = opts.title self.options = opts.options
self.message = opts.message and opts.message .. "\n" self.on_selected = opts.on_selected
self.options = opts.options if self.message and self.options then
self.on_selected = opts.on_selected
local message_height = self:get_message_height() local message_height = self:get_message_height()
-- self.target_height is the nagview height needed to display the message and -- self.target_height is the nagview height needed to display the message and
-- the buttons, excluding the top and bottom padding space. -- the buttons, excluding the top and bottom padding space.
self.target_height = math.max(message_height, self:get_buttons_height()) self.target_height = math.max(message_height, self:get_buttons_height())
self:change_hovered(common.find_index(self.options, "default_yes")) self:change_hovered(common.find_index(self.options, "default_yes"))
self.force_focus = true
core.set_active_view(self)
-- We add a hook to manage all the mouse_pressed events.
register_mouse_pressed(self)
else
self.force_focus = false
core.set_active_view(core.next_active_view or core.last_active_view)
self.visible = false
unregister_mouse_pressed(self)
end end
self.force_focus = self.message ~= nil
core.set_active_view(self.message ~= nil and self or
core.next_active_view or core.last_active_view)
end end
function NagView:show(title, message, options, on_select) function NagView:show(title, message, options, on_select)
@ -297,7 +204,7 @@ function NagView:show(title, message, options, on_select)
opts.options = assert(options, "No options") opts.options = assert(options, "No options")
opts.on_selected = on_select or noop opts.on_selected = on_select or noop
table.insert(self.queue, opts) table.insert(self.queue, opts)
self:next() if #self.queue > 0 and not self.title then self:next() end
end end
return NagView return NagView

View File

@ -1,763 +0,0 @@
local core = require "core"
local common = require "core.common"
local config = require "core.config"
local style = require "core.style"
local Object = require "core.object"
local EmptyView = require "core.emptyview"
local View = require "core.view"
---@class core.node : core.object
local Node = Object:extend()
function Node:new(type)
self.type = type or "leaf"
self.position = { x = 0, y = 0 }
self.size = { x = 0, y = 0 }
self.views = {}
self.divider = 0.5
if self.type == "leaf" then
self:add_view(EmptyView())
end
self.hovered = {x = -1, y = -1 }
self.hovered_close = 0
self.tab_shift = 0
self.tab_offset = 1
self.tab_width = style.tab_width
self.move_towards = View.move_towards
end
function Node:propagate(fn, ...)
self.a[fn](self.a, ...)
self.b[fn](self.b, ...)
end
function Node:on_mouse_moved(x, y, ...)
if self.type == "leaf" then
self.hovered.x, self.hovered.y = x, y
self.active_view:on_mouse_moved(x, y, ...)
else
self:propagate("on_mouse_moved", x, y, ...)
end
end
function Node:on_mouse_released(...)
if self.type == "leaf" then
self.active_view:on_mouse_released(...)
else
self:propagate("on_mouse_released", ...)
end
end
function Node:on_mouse_left()
if self.type == "leaf" then
self.active_view:on_mouse_left()
else
self:propagate("on_mouse_left")
end
end
function Node:consume(node)
for k, _ in pairs(self) do self[k] = nil end
for k, v in pairs(node) do self[k] = v end
end
local type_map = { up="vsplit", down="vsplit", left="hsplit", right="hsplit" }
-- The "locked" argument below should be in the form {x = <boolean>, y = <boolean>}
-- and it indicates if the node want to have a fixed size along the axis where the
-- boolean is true. If not it will be expanded to take all the available space.
-- The "resizable" flag indicates if, along the "locked" axis the node can be resized
-- by the user. If the node is marked as resizable their view should provide a
-- set_target_size method.
function Node:split(dir, view, locked, resizable)
assert(self.type == "leaf", "Tried to split non-leaf node")
local node_type = assert(type_map[dir], "Invalid direction")
local last_active = core.active_view
local child = Node()
child:consume(self)
self:consume(Node(node_type))
self.a = child
self.b = Node()
if view then self.b:add_view(view) end
if locked then
assert(type(locked) == 'table')
self.b.locked = locked
self.b.resizable = resizable or false
core.set_active_view(last_active)
end
if dir == "up" or dir == "left" then
self.a, self.b = self.b, self.a
return self.a
end
return self.b
end
function Node:remove_view(root, view)
if #self.views > 1 then
local idx = self:get_view_idx(view)
if idx < self.tab_offset then
self.tab_offset = self.tab_offset - 1
end
table.remove(self.views, idx)
if self.active_view == view then
self:set_active_view(self.views[idx] or self.views[#self.views])
end
else
local parent = self:get_parent_node(root)
local is_a = (parent.a == self)
local other = parent[is_a and "b" or "a"]
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
local next_primary
if self.is_primary_node then
next_primary = core.root_view:select_next_primary_node()
end
if locked_size or (self.is_primary_node and not next_primary) then
self.views = {}
self:add_view(EmptyView())
else
if other == next_primary then
next_primary = parent
end
parent:consume(other)
local p = parent
while p.type ~= "leaf" do
p = p[is_a and "a" or "b"]
end
p:set_active_view(p.active_view)
if self.is_primary_node then
next_primary.is_primary_node = true
end
end
end
core.last_active_view = nil
end
function Node:close_view(root, view)
local do_close = function()
self:remove_view(root, view)
end
view:try_close(do_close)
end
function Node:close_active_view(root)
self:close_view(root, self.active_view)
end
function Node:add_view(view, idx)
assert(self.type == "leaf", "Tried to add view to non-leaf node")
assert(not self.locked, "Tried to add view to locked node")
if self.views[1] and self.views[1]:is(EmptyView) then
table.remove(self.views)
end
table.insert(self.views, idx or (#self.views + 1), view)
self:set_active_view(view)
end
function Node:set_active_view(view)
assert(self.type == "leaf", "Tried to set active view on non-leaf node")
local last_active_view = self.active_view
self.active_view = view
core.set_active_view(view)
if last_active_view and last_active_view ~= view then
last_active_view:on_mouse_left()
end
end
function Node:get_view_idx(view)
for i, v in ipairs(self.views) do
if v == view then return i end
end
end
function Node:get_node_for_view(view)
for _, v in ipairs(self.views) do
if v == view then return self end
end
if self.type ~= "leaf" then
return self.a:get_node_for_view(view) or self.b:get_node_for_view(view)
end
end
function Node:get_parent_node(root)
if root.a == self or root.b == self then
return root
elseif root.type ~= "leaf" then
return self:get_parent_node(root.a) or self:get_parent_node(root.b)
end
end
function Node:get_children(t)
t = t or {}
for _, view in ipairs(self.views) do
table.insert(t, view)
end
if self.a then self.a:get_children(t) end
if self.b then self.b:get_children(t) end
return t
end
-- return the width including the padding space and separately
-- the padding space itself
local function get_scroll_button_width()
local w = style.icon_font:get_width(">")
local pad = w
return w + 2 * pad, pad
end
function Node:get_divider_overlapping_point(px, py)
if self.type ~= "leaf" then
local axis = self.type == "hsplit" and "x" or "y"
if self.a:is_resizable(axis) and self.b:is_resizable(axis) then
local p = 6
local x, y, w, h = self:get_divider_rect()
x, y = x - p, y - p
w, h = w + p * 2, h + p * 2
if px > x and py > y and px < x + w and py < y + h then
return self
end
end
return self.a:get_divider_overlapping_point(px, py)
or self.b:get_divider_overlapping_point(px, py)
end
end
function Node:get_visible_tabs_number()
return math.min(#self.views - self.tab_offset + 1, config.max_tabs)
end
function Node:get_tab_overlapping_point(px, py)
if not self:should_show_tabs() then return nil end
local tabs_number = self:get_visible_tabs_number()
local x1, y1, w, h = self:get_tab_rect(self.tab_offset)
local x2, y2 = self:get_tab_rect(self.tab_offset + tabs_number)
if px >= x1 and py >= y1 and px < x2 and py < y1 + h then
return math.floor((px - x1) / w) + self.tab_offset
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 cw = style.icon_font:get_width("C")
local pad = style.padding.x / 2
return x + w - cw - pad, cw, pad
end
function Node:get_scroll_button_index(px, py)
if #self.views == 1 then return end
for i = 1, 2 do
local x, y, w, h = self:get_scroll_button_rect(i)
if px >= x and px < x + w and py >= y and py < y + h then
return i
end
end
end
function Node:tab_hovered_update(px, py)
local tab_index = self:get_tab_overlapping_point(px, py)
self.hovered_tab = tab_index
self.hovered_close = 0
self.hovered_scroll_button = 0
if tab_index then
local x, y, w, h = self:get_tab_rect(tab_index)
local cx, cw = close_button_location(x, w)
if px >= cx and px < cx + cw and py >= y and py < y + h and config.tab_close_button then
self.hovered_close = tab_index
end
else
self.hovered_scroll_button = self:get_scroll_button_index(px, py) or 0
end
end
function Node:get_child_overlapping_point(x, y)
local child
if self.type == "leaf" then
return self
elseif self.type == "hsplit" then
child = (x < self.b.position.x) and self.a or self.b
elseif self.type == "vsplit" then
child = (y < self.b.position.y) and self.a or self.b
end
return child:get_child_overlapping_point(x, y)
end
function Node:get_scroll_button_rect(index)
local w, pad = get_scroll_button_width()
local h = style.font:get_height() + style.padding.y * 2
local x = self.position.x + (index == 1 and self.size.x - w * 2 or self.size.x - w)
return x, self.position.y, w, h, pad
end
function Node:get_tab_rect(idx)
local maxw = self.size.x
local x0 = self.position.x
local x1 = x0 + common.clamp(self.tab_width * (idx - 1) - self.tab_shift, 0, maxw)
local x2 = x0 + common.clamp(self.tab_width * idx - self.tab_shift, 0, maxw)
local h = style.font:get_height() + style.padding.y * 2
return x1, self.position.y, x2 - x1, h
end
function Node:get_divider_rect()
local x, y = self.position.x, self.position.y
if self.type == "hsplit" then
return x + self.a.size.x, y, style.divider_size, self.size.y
elseif self.type == "vsplit" then
return x, y + self.a.size.y, self.size.x, style.divider_size
end
end
-- Return two values for x and y axis and each of them is either falsy or a number.
-- A falsy value indicate no fixed size along the corresponding direction.
function Node:get_locked_size()
if self.type == "leaf" then
if self.locked then
local size = self.active_view.size
-- The values below should be either a falsy value or a number
local sx = (self.locked and self.locked.x) and size.x
local sy = (self.locked and self.locked.y) and size.y
return sx, sy
end
else
local x1, y1 = self.a:get_locked_size()
local x2, y2 = self.b:get_locked_size()
-- The values below should be either a falsy value or a number
local sx, sy
if self.type == 'hsplit' then
if x1 and x2 then
local dsx = (x1 < 1 or x2 < 1) and 0 or style.divider_size
sx = x1 + x2 + dsx
end
sy = y1 or y2
else
if y1 and y2 then
local dsy = (y1 < 1 or y2 < 1) and 0 or style.divider_size
sy = y1 + y2 + dsy
end
sx = x1 or x2
end
return sx, sy
end
end
function Node.copy_position_and_size(dst, src)
dst.position.x, dst.position.y = src.position.x, src.position.y
dst.size.x, dst.size.y = src.size.x, src.size.y
end
-- calculating the sizes is the same for hsplits and vsplits, except the x/y
-- axis are swapped; this function lets us use the same code for both
local function calc_split_sizes(self, x, y, x1, x2, y1, y2)
local ds = ((x1 and x1 < 1) or (x2 and x2 < 1)) and 0 or style.divider_size
local n = x1 and x1 + ds or (x2 and self.size[x] - x2 or math.floor(self.size[x] * self.divider))
self.a.position[x] = self.position[x]
self.a.position[y] = self.position[y]
self.a.size[x] = n - ds
self.a.size[y] = self.size[y]
self.b.position[x] = self.position[x] + n
self.b.position[y] = self.position[y]
self.b.size[x] = self.size[x] - n
self.b.size[y] = self.size[y]
end
function Node:update_layout()
if self.type == "leaf" then
local av = self.active_view
if self:should_show_tabs() then
local _, _, _, th = self:get_tab_rect(1)
av.position.x, av.position.y = self.position.x, self.position.y + th
av.size.x, av.size.y = self.size.x, self.size.y - th
else
Node.copy_position_and_size(av, self)
end
else
local x1, y1 = self.a:get_locked_size()
local x2, y2 = self.b:get_locked_size()
if self.type == "hsplit" then
calc_split_sizes(self, "x", "y", x1, x2)
elseif self.type == "vsplit" then
calc_split_sizes(self, "y", "x", y1, y2)
end
self.a:update_layout()
self.b:update_layout()
end
end
function Node:scroll_tabs_to_visible()
local index = self:get_view_idx(self.active_view)
if index then
local tabs_number = self:get_visible_tabs_number()
if self.tab_offset > index then
self.tab_offset = index
elseif self.tab_offset + tabs_number - 1 < index then
self.tab_offset = index - tabs_number + 1
elseif tabs_number < config.max_tabs and self.tab_offset > 1 then
self.tab_offset = #self.views - config.max_tabs + 1
end
end
end
function Node:scroll_tabs(dir)
local view_index = self:get_view_idx(self.active_view)
if dir == 1 then
if self.tab_offset > 1 then
self.tab_offset = self.tab_offset - 1
local last_index = self.tab_offset + self:get_visible_tabs_number() - 1
if view_index > last_index then
self:set_active_view(self.views[last_index])
end
end
elseif dir == 2 then
local tabs_number = self:get_visible_tabs_number()
if self.tab_offset + tabs_number - 1 < #self.views then
self.tab_offset = self.tab_offset + 1
local view_index = self:get_view_idx(self.active_view)
if view_index < self.tab_offset then
self:set_active_view(self.views[self.tab_offset])
end
end
end
end
function Node:target_tab_width()
local n = self:get_visible_tabs_number()
local w = self.size.x
if #self.views > n then
w = self.size.x - get_scroll_button_width() * 2
end
return common.clamp(style.tab_width, w / config.max_tabs, w / n)
end
function Node:update()
if self.type == "leaf" then
self:scroll_tabs_to_visible()
for _, view in ipairs(self.views) do
view:update()
end
self:tab_hovered_update(self.hovered.x, self.hovered.y)
local tab_width = self:target_tab_width()
self:move_towards("tab_shift", tab_width * (self.tab_offset - 1), nil, "tabs")
self:move_towards("tab_width", tab_width, nil, "tabs")
else
self.a:update()
self.b:update()
end
end
function Node:draw_tab_title(view, font, is_active, is_hovered, x, y, w, h)
local text = view and view:get_name() or ""
local dots_width = font:get_width("")
local align = "center"
if font:get_width(text) > w then
align = "left"
for i = 1, #text do
local reduced_text = text:sub(1, #text - i)
if font:get_width(reduced_text) + dots_width <= w then
text = reduced_text .. ""
break
end
end
end
local color = style.dim
if is_active then color = style.text end
if is_hovered then color = style.text end
common.draw_text(font, color, text, align, x, y, w, h)
end
function Node:draw_tab_borders(view, is_active, is_hovered, x, y, w, h, standalone)
-- Tabs deviders
local ds = style.divider_size
local 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
-- Full border
if is_active then
color = style.text
renderer.draw_rect(x, y, w, h, style.background)
renderer.draw_rect(x + w, y, ds, h, style.divider)
renderer.draw_rect(x - ds, y, ds, h, style.divider)
end
return x + ds, y, w - ds*2, h
end
function Node:draw_tab(view, is_active, is_hovered, is_close_hovered, x, y, w, h, standalone)
x, y, w, h = self:draw_tab_borders(view, is_active, is_hovered, x, y, w, h, standalone)
-- Close button
local cx, cw, cpad = close_button_location(x, w)
local show_close_button = ((is_active or is_hovered) and not standalone and config.tab_close_button)
if show_close_button then
local close_style = is_close_hovered and style.text or style.dim
common.draw_text(style.icon_font, close_style, "C", nil, cx, y, cw, h)
end
-- Title
x = x + cpad
w = cx - x
core.push_clip_rect(x, y, w, h)
self:draw_tab_title(view, style.font, is_active, is_hovered, x, y, w, h)
core.pop_clip_rect()
end
function Node:draw_tabs()
local _, y, w, h, scroll_padding = self:get_scroll_button_rect(1)
local x = self.position.x
local ds = style.divider_size
local dots_width = style.font:get_width("")
core.push_clip_rect(x, y, self.size.x, h)
renderer.draw_rect(x, y, self.size.x, h, style.background2)
renderer.draw_rect(x, y + h - ds, self.size.x, ds, style.divider)
local tabs_number = self:get_visible_tabs_number()
for i = self.tab_offset, self.tab_offset + tabs_number - 1 do
local view = self.views[i]
local x, y, w, h = self:get_tab_rect(i)
self:draw_tab(view, view == self.active_view,
i == self.hovered_tab, i == self.hovered_close,
x, y, w, h)
end
if #self.views > tabs_number then
local _, pad = get_scroll_button_width()
local xrb, yrb, wrb, hrb = self:get_scroll_button_rect(1)
renderer.draw_rect(xrb + pad, yrb, wrb * 2, hrb, style.background2)
local left_button_style = (self.hovered_scroll_button == 1 and self.tab_offset > 1) and style.text or style.dim
common.draw_text(style.icon_font, left_button_style, "<", nil, xrb + scroll_padding, yrb, 0, h)
xrb, yrb, wrb = self:get_scroll_button_rect(2)
local right_button_style = (self.hovered_scroll_button == 2 and #self.views > self.tab_offset + tabs_number - 1) and style.text or style.dim
common.draw_text(style.icon_font, right_button_style, ">", nil, xrb + scroll_padding, yrb, 0, h)
end
core.pop_clip_rect()
end
function Node:draw()
if self.type == "leaf" then
if self:should_show_tabs() then
self:draw_tabs()
end
local pos, size = self.active_view.position, self.active_view.size
core.push_clip_rect(pos.x, pos.y, size.x, size.y)
self.active_view:draw()
core.pop_clip_rect()
else
local x, y, w, h = self:get_divider_rect()
renderer.draw_rect(x, y, w, h, style.divider)
self:propagate("draw")
end
end
function Node:is_empty()
if self.type == "leaf" then
return #self.views == 0 or (#self.views == 1 and self.views[1]:is(EmptyView))
else
return self.a:is_empty() and self.b:is_empty()
end
end
function Node:close_all_docviews(keep_active)
local node_active_view = self.active_view
local lost_active_view = false
if self.type == "leaf" then
local i = 1
while i <= #self.views do
local view = self.views[i]
if view.context == "session" and (not keep_active or view ~= self.active_view) then
table.remove(self.views, i)
if view == node_active_view then
lost_active_view = true
end
else
i = i + 1
end
end
self.tab_offset = 1
if #self.views == 0 and self.is_primary_node then
-- if we are not the primary view and we had the active view it doesn't
-- matter to reattribute the active view because, within the close_all_docviews
-- top call, the primary node will take the active view anyway.
-- Set the empty view and takes the active view.
self:add_view(EmptyView())
elseif #self.views > 0 and lost_active_view then
-- In practice we never get there but if a view remain we need
-- to reset the Node's active view.
self:set_active_view(self.views[1])
end
else
self.a:close_all_docviews(keep_active)
self.b:close_all_docviews(keep_active)
if self.a:is_empty() and not self.a.is_primary_node then
self:consume(self.b)
elseif self.b:is_empty() and not self.b.is_primary_node then
self:consume(self.a)
end
end
end
-- Returns true for nodes that accept either "proportional" resizes (based on the
-- node.divider) or "locked" resizable nodes (along the resize axis).
function Node:is_resizable(axis)
if self.type == 'leaf' then
return not self.locked or not self.locked[axis] or self.resizable
else
local a_resizable = self.a:is_resizable(axis)
local b_resizable = self.b:is_resizable(axis)
return a_resizable and b_resizable
end
end
-- Return true iff it is a locked pane along the rezise axis and is
-- declared "resizable".
function Node:is_locked_resizable(axis)
return self.locked and self.locked[axis] and self.resizable
end
function Node:resize(axis, value)
-- the application works fine with non-integer values but to have pixel-perfect
-- placements of view elements, like the scrollbar, we round the value to be
-- an integer.
value = math.floor(value)
if self.type == 'leaf' then
-- If it is not locked we don't accept the
-- resize operation here because for proportional panes the resize is
-- done using the "divider" value of the parent node.
if self:is_locked_resizable(axis) then
return self.active_view:set_target_size(axis, value)
end
else
if self.type == (axis == "x" and "hsplit" or "vsplit") then
-- we are resizing a node that is splitted along the resize axis
if self.a:is_locked_resizable(axis) and self.b:is_locked_resizable(axis) then
local rem_value = value - self.a.size[axis]
if rem_value >= 0 then
return self.b.active_view:set_target_size(axis, rem_value)
else
self.b.active_view:set_target_size(axis, 0)
return self.a.active_view:set_target_size(axis, value)
end
end
else
-- we are resizing a node that is splitted along the axis perpendicular
-- to the resize axis
local a_resizable = self.a:is_resizable(axis)
local b_resizable = self.b:is_resizable(axis)
if a_resizable and b_resizable then
self.a:resize(axis, value)
self.b:resize(axis, value)
end
end
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
return Node

View File

@ -1,12 +1,11 @@
---@class core.object
---@field super core.object
local Object = {} local Object = {}
Object.__index = Object Object.__index = Object
---Can be overrided by child objects to implement a constructor.
function Object:new() end
---@return core.object function Object:new()
end
function Object:extend() function Object:extend()
local cls = {} local cls = {}
for k, v in pairs(self) do for k, v in pairs(self) do
@ -20,17 +19,8 @@ function Object:extend()
return cls return cls
end end
---Check if the object is strictly of the given type.
---@param T any
---@return boolean
function Object:is(T)
return getmetatable(self) == T
end
---Check if the object inherits from the given type. function Object:is(T)
---@param T any
---@return boolean
function Object:extends(T)
local mt = getmetatable(self) local mt = getmetatable(self)
while mt do while mt do
if mt == T then if mt == T then
@ -41,14 +31,12 @@ function Object:extends(T)
return false return false
end end
---Metamethod to get a string representation of an object.
---@return string
function Object:__tostring() function Object:__tostring()
return "Object" return "Object"
end end
---Metamethod to allow using the object call as a constructor.
---@return core.object
function Object:__call(...) function Object:__call(...)
local obj = setmetatable({}, self) local obj = setmetatable({}, self)
obj:new(...) obj:new(...)

View File

@ -1,82 +1,70 @@
-- 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
---Looks for the first match of `pattern` in the string `str`. regex.match = function(pattern_string, string, offset, options)
---If it finds a match, it returns the indices of `str` where this occurrence local pattern = type(pattern_string) == "table" and
---starts and ends; otherwise, it returns `nil`. pattern_string or regex.compile(pattern_string)
---If the pattern has captures, the captured start and end indexes are returned, return regex.cmatch(pattern, string, offset or 1, options or 0)
---after the two initial ones.
---
---@param pattern string|table The regex pattern to use, either as a simple string or precompiled.
---@param str string The string to search for valid matches.
---@param offset? integer The position on the subject to start searching.
---@param options? integer A bit field of matching options, eg: regex.NOTBOL | regex.NOTEMPTY
---
---@return integer? start Offset where the first match was found; `nil` if no match.
---@return integer? end Offset where the first match ends; `nil` if no match.
---@return integer? ... #Captured matches offsets.
regex.find_offsets = function(pattern, str, offset, options)
if type(pattern) ~= "table" then
pattern = regex.compile(pattern)
end
local res = { regex.cmatch(pattern, str, offset or 1, options or 0) }
-- Reduce every end delimiter by 1
for i = 2,#res,2 do
res[i] = res[i] - 1
end
return table.unpack(res)
end end
---Behaves like `string.match`. -- Will iterate back through any UTF-8 bytes so that we don't replace bits
---Looks for the first match of `pattern` in the string `str`. -- mid character.
---If it finds a match, it returns the matched string; otherwise, it returns `nil`. local function previous_character(str, index)
---If the pattern has captures, only the captured strings are returned. local byte
---If a capture is empty, its offset is returned instead. repeat
--- index = index - 1
---@param pattern string|table The regex pattern to use, either as a simple string or precompiled. byte = string.byte(str, index)
---@param str string The string to search for valid matches. until byte < 128 or byte >= 192
---@param offset? integer The position on the subject to start searching. return index
---@param options? integer A bit field of matching options, eg: regex.NOTBOL | regex.NOTEMPTY
---
---@return (string|integer)? ... #List of captured matches; the entire match if no matches were specified; if the match is empty, its offset is returned instead.
regex.match = function(pattern, str, offset, options)
local res = { regex.find(pattern, str, offset, options) }
if #res == 0 then return end
-- If available, only return captures
if #res > 2 then return table.unpack(res, 3) end
return string.sub(str, res[1], res[2])
end end
---Behaves like `string.find`. -- Moves to the end of the identified character.
---Looks for the first match of `pattern` in the string `str`. local function end_character(str, index)
---If it finds a match, it returns the indices of `str` where this occurrence local byte = string.byte(str, index + 1)
---starts and ends; otherwise, it returns `nil`. while byte and byte >= 128 and byte < 192 do
---If the pattern has captures, the captured strings are returned, index = index + 1
---after the two indexes ones. byte = string.byte(str, index + 1)
---If a capture is empty, its offset is returned instead. end
--- return index
---@param pattern string|table The regex pattern to use, either as a simple string or precompiled. end
---@param str string The string to search for valid matches.
---@param offset? integer The position on the subject to start searching. -- Build off matching. For now, only support basic replacements, but capture
---@param options? integer A bit field of matching options, eg: regex.NOTBOL | regex.NOTEMPTY -- groupings should be doable. We can even have custom group replacements and
--- -- transformations and stuff in lua. Currently, this takes group replacements
---@return integer? start Offset where the first match was found; `nil` if no match. -- as \1 - \9.
---@return integer? end Offset where the first match ends; `nil` if no match. -- Should work on UTF-8 text.
---@return (string|integer)? ... #List of captured matches; if the match is empty, its offset is returned instead. regex.gsub = function(pattern_string, str, replacement)
regex.find = function(pattern, str, offset, options) local pattern = type(pattern_string) == "table" and
local res = { regex.find_offsets(pattern, str, offset, options) } pattern_string or regex.compile(pattern_string)
local out = { } local result, indices = ""
if #res == 0 then return end local matches, replacements = {}, {}
out[1] = res[1] repeat
out[2] = res[2] indices = { regex.cmatch(pattern, str) }
for i = 3,#res,2 do if #indices > 0 then
if res[i] > res[i+1] then table.insert(matches, indices)
-- Like in string.find, if the group has size 0, return the index local currentReplacement = replacement
table.insert(out, res[i]) if #indices > 2 then
else for i = 1, (#indices/2 - 1) do
table.insert(out, string.sub(str, res[i], res[i+1])) currentReplacement = string.gsub(
currentReplacement,
"\\" .. i,
str:sub(indices[i*2+1], end_character(str,indices[i*2+2]-1))
)
end
end
currentReplacement = string.gsub(currentReplacement, "\\%d", "")
table.insert(replacements, { indices[1], #currentReplacement+indices[1] })
if indices[1] > 1 then
result = result ..
str:sub(1, previous_character(str, indices[1])) .. currentReplacement
else
result = result .. currentReplacement
end
str = str:sub(indices[2])
end end
end until #indices == 0 or indices[1] == indices[2]
return table.unpack(out) return result .. str, matches, replacements
end end

File diff suppressed because it is too large Load Diff

View File

@ -1,343 +0,0 @@
local core = require "core"
local common = require "core.common"
local config = require "core.config"
local style = require "core.style"
local Object = require "core.object"
---Scrollbar
---Use Scrollbar:set_size to set the bounding box of the view the scrollbar belongs to.
---Use Scrollbar:update to update the scrollbar animations.
---Use Scrollbar:draw to draw the scrollbar.
---Use Scrollbar:on_mouse_pressed, Scrollbar:on_mouse_released,
---Scrollbar:on_mouse_moved and Scrollbar:on_mouse_left to react to mouse movements;
---the scrollbar won't update automatically.
---Use Scrollbar:set_percent to set the scrollbar location externally.
---
---To manage all the orientations, the scrollbar changes the coordinates system
---accordingly. The "normal" coordinate system adapts the scrollbar coordinates
---as if it's always a vertical scrollbar, positioned at the end of the bounding box.
---@class core.scrollbar : core.object
local Scrollbar = Object:extend()
---@class ScrollbarOptions
---@field direction "v" | "h" @Vertical or Horizontal
---@field alignment "s" | "e" @Start or End (left to right, top to bottom)
---@field force_status "expanded" | "contracted" | false @Force the scrollbar status
---@field expanded_size number? @Override the default value specified by `style.expanded_scrollbar_size`
---@field contracted_size number? @Override the default value specified by `style.scrollbar_size`
---@param options ScrollbarOptions
function Scrollbar:new(options)
---Position information of the owner
self.rect = {
x = 0, y = 0, w = 0, h = 0,
---Scrollable size
scrollable = 0
}
self.normal_rect = {
across = 0,
along = 0,
across_size = 0,
along_size = 0,
scrollable = 0
}
---@type integer @Position in percent [0-1]
self.percent = 0
---@type boolean @Scrollbar dragging status
self.dragging = false
---@type integer @Private. Used to offset the start of the drag from the top of the thumb
self.drag_start_offset = 0
---What is currently being hovered. `thumb` implies` track`
self.hovering = { track = false, thumb = false }
---@type "v" | "h"@Vertical or Horizontal
self.direction = options.direction or "v"
---@type "s" | "e" @Start or End (left to right, top to bottom)
self.alignment = options.alignment or "e"
---@type number @Private. Used to keep track of animations
self.expand_percent = 0
---@type "expanded" | "contracted" | false @Force the scrollbar status
self.force_status = options.force_status
self:set_forced_status(options.force_status)
---@type number? @Override the default value specified by `style.expanded_scrollbar_size`
self.contracted_size = options.contracted_size
---@type number? @Override the default value specified by `style.scrollbar_size`
self.expanded_size = options.expanded_size
end
---Set the status the scrollbar is forced to keep
---@param status "expanded" | "contracted" | false @The status to force
function Scrollbar:set_forced_status(status)
self.force_status = status
if self.force_status == "expanded" then
self.expand_percent = 1
end
end
function Scrollbar:real_to_normal(x, y, w, h)
x, y, w, h = x or 0, y or 0, w or 0, h or 0
if self.direction == "v" then
if self.alignment == "s" then
x = (self.rect.x + self.rect.w) - x - w
end
return x, y, w, h
else
if self.alignment == "s" then
y = (self.rect.y + self.rect.h) - y - h
end
return y, x, h, w
end
end
function Scrollbar:normal_to_real(x, y, w, h)
x, y, w, h = x or 0, y or 0, w or 0, h or 0
if self.direction == "v" then
if self.alignment == "s" then
x = (self.rect.x + self.rect.w) - x - w
end
return x, y, w, h
else
if self.alignment == "s" then
x = (self.rect.y + self.rect.h) - x - w
end
return y, x, h, w
end
end
function Scrollbar:_get_thumb_rect_normal()
local nr = self.normal_rect
local sz = nr.scrollable
if sz == math.huge or sz <= nr.along_size
then
return 0, 0, 0, 0
end
local scrollbar_size = self.contracted_size or style.scrollbar_size
local expanded_scrollbar_size = self.expanded_size or style.expanded_scrollbar_size
local along_size = math.max(20, nr.along_size * nr.along_size / sz)
local across_size = scrollbar_size
across_size = across_size + (expanded_scrollbar_size - scrollbar_size) * self.expand_percent
return
nr.across + nr.across_size - across_size,
nr.along + self.percent * nr.scrollable * (nr.along_size - along_size) / (sz - nr.along_size),
across_size,
along_size
end
---Get the thumb rect (the part of the scrollbar that can be dragged)
---@return integer,integer,integer,integer @x, y, w, h
function Scrollbar:get_thumb_rect()
return self:normal_to_real(self:_get_thumb_rect_normal())
end
function Scrollbar:_get_track_rect_normal()
local nr = self.normal_rect
local sz = nr.scrollable
if sz <= nr.along_size or sz == math.huge then
return 0, 0, 0, 0
end
local scrollbar_size = self.contracted_size or style.scrollbar_size
local expanded_scrollbar_size = self.expanded_size or style.expanded_scrollbar_size
local across_size = scrollbar_size
across_size = across_size + (expanded_scrollbar_size - scrollbar_size) * self.expand_percent
return
nr.across + nr.across_size - across_size,
nr.along,
across_size,
nr.along_size
end
---Get the track rect (the "background" of the scrollbar)
---@return number,number,number,number @x, y, w, h
function Scrollbar:get_track_rect()
return self:normal_to_real(self:_get_track_rect_normal())
end
function Scrollbar:_overlaps_normal(x, y)
local sx, sy, sw, sh = self:_get_thumb_rect_normal()
local scrollbar_size = self.contracted_size or style.scrollbar_size
local result
if x >= sx - scrollbar_size * 3 and x <= sx + sw and y >= sy and y <= sy + sh then
result = "thumb"
else
sx, sy, sw, sh = self:_get_track_rect_normal()
if x >= sx - scrollbar_size * 3 and x <= sx + sw and y >= sy and y <= sy + sh then
result = "track"
end
end
return result
end
---Get what part of the scrollbar the coordinates overlap
---@return "thumb"|"track"|nil
function Scrollbar:overlaps(x, y)
x, y = self:real_to_normal(x, y)
return self:_overlaps_normal(x, y)
end
function Scrollbar:_on_mouse_pressed_normal(button, x, y, clicks)
local overlaps = self:_overlaps_normal(x, y)
if overlaps then
local _, along, _, along_size = self:_get_thumb_rect_normal()
self.dragging = true
if overlaps == "thumb" then
self.drag_start_offset = along - y
return true
elseif overlaps == "track" then
local nr = self.normal_rect
self.drag_start_offset = - along_size / 2
return common.clamp((y - nr.along - along_size / 2) / (nr.along_size - along_size), 0, 1)
end
end
end
---Updates the scrollbar with mouse pressed info.
---Won't update the scrollbar position automatically.
---Use Scrollbar:set_percent to update it.
---
---This sets the dragging status if needed.
---
---Returns a falsy value if the event happened outside the scrollbar.
---Returns `true` if the thumb was pressed.
---If the track was pressed this returns a value between 0 and 1
---representing the percent of the position.
---@return boolean|number
function Scrollbar:on_mouse_pressed(button, x, y, clicks)
if button ~= "left" then return end
x, y = self:real_to_normal(x, y)
return self:_on_mouse_pressed_normal(button, x, y, clicks)
end
---Updates the scrollbar hover status.
---This gets called by other functions and shouldn't be called manually
function Scrollbar:_update_hover_status_normal(x, y)
local overlaps = self:_overlaps_normal(x, y)
self.hovering.thumb = overlaps == "thumb"
self.hovering.track = self.hovering.thumb or overlaps == "track"
return self.hovering.track or self.hovering.thumb
end
function Scrollbar:_on_mouse_released_normal(button, x, y)
self.dragging = false
return self:_update_hover_status_normal(x, y)
end
---Updates the scrollbar dragging status
function Scrollbar:on_mouse_released(button, x, y)
if button ~= "left" then return end
x, y = self:real_to_normal(x, y)
return self:_on_mouse_released_normal(button, x, y)
end
function Scrollbar:_on_mouse_moved_normal(x, y, dx, dy)
if self.dragging then
local nr = self.normal_rect
return common.clamp((y - nr.along + self.drag_start_offset) / nr.along_size, 0, 1)
end
return self:_update_hover_status_normal(x, y)
end
---Updates the scrollbar with mouse moved info.
---Won't update the scrollbar position automatically.
---Use Scrollbar:set_percent to update it.
---
---This updates the hovering status.
---
---Returns a falsy value if the event happened outside the scrollbar.
---Returns `true` if the scrollbar is hovered.
---If the scrollbar was being dragged, this returns a value between 0 and 1
---representing the percent of the position.
---@return boolean|number
function Scrollbar:on_mouse_moved(x, y, dx, dy)
x, y = self:real_to_normal(x, y)
dx, dy = self:real_to_normal(dx, dy) -- TODO: do we need this? (is this even correct?)
return self:_on_mouse_moved_normal(x, y, dx, dy)
end
---Updates the scrollbar hovering status
function Scrollbar:on_mouse_left()
self.hovering.track, self.hovering.thumb = false, false
end
---Updates the bounding box of the view the scrollbar belongs to.
---@param x number
---@param y number
---@param w number
---@param h number
---@param scrollable number @size of the scrollable area
function Scrollbar:set_size(x, y, w, h, scrollable)
self.rect.x, self.rect.y, self.rect.w, self.rect.h = x, y, w, h
self.rect.scrollable = scrollable
local nr = self.normal_rect
nr.across, nr.along, nr.across_size, nr.along_size = self:real_to_normal(x, y, w, h)
nr.scrollable = scrollable
end
---Updates the scrollbar location
---@param percent number @number between 0 and 1 representing the position of the middle part of the thumb
function Scrollbar:set_percent(percent)
self.percent = percent
end
---Updates the scrollbar animations
function Scrollbar:update()
-- TODO: move the animation code to its own class
if not self.force_status then
local dest = (self.hovering.track or self.dragging) and 1 or 0
local diff = math.abs(self.expand_percent - dest)
if not config.transitions or diff < 0.05 or config.disabled_transitions["scroll"] then
self.expand_percent = dest
else
local rate = 0.3
if config.fps ~= 60 or config.animation_rate ~= 1 then
local dt = 60 / config.fps
rate = 1 - common.clamp(1 - rate, 1e-8, 1 - 1e-8)^(config.animation_rate * dt)
end
self.expand_percent = common.lerp(self.expand_percent, dest, rate)
end
if diff > 1e-8 then
core.redraw = true
end
elseif self.force_status == "expanded" then
self.expand_percent = 1
elseif self.force_status == "contracted" then
self.expand_percent = 0
end
end
---Draw the scrollbar track
function Scrollbar:draw_track()
if not (self.hovering.track or self.dragging)
and self.expand_percent == 0 then
return
end
local color = { table.unpack(style.scrollbar_track) }
color[4] = color[4] * self.expand_percent
local x, y, w, h = self:get_track_rect()
renderer.draw_rect(x, y, w, h, color)
end
---Draw the scrollbar thumb
function Scrollbar:draw_thumb()
local highlight = self.hovering.thumb or self.dragging
local color = highlight and style.scrollbar2 or style.scrollbar
local x, y, w, h = self:get_thumb_rect()
renderer.draw_rect(x, y, w, h, color)
end
---Draw both the scrollbar track and thumb
function Scrollbar:draw()
self:draw_track()
self:draw_thumb()
end
return Scrollbar

View File

@ -1,57 +1,22 @@
-- 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.1.4r1" VERSION = "@PROJECT_VERSION@"
MOD_VERSION = "3" MOD_VERSION = "2"
SCALE = tonumber(os.getenv("LITE_SCALE") or os.getenv("GDK_SCALE") or os.getenv("QT_SCALE_FACTOR")) or 1 SCALE = tonumber(os.getenv("LITE_SCALE")) or SCALE
PATHSEP = package.config:sub(1, 1) PATHSEP = package.config:sub(1, 1)
EXEDIR = EXEFILE:match("^(.+)[/\\][^/\\]+$") EXEDIR = EXEFILE:match("^(.+)[/\\][^/\\]+$")
if MACOS_RESOURCES then if MACOS_RESOURCES then
DATADIR = MACOS_RESOURCES DATADIR = MACOS_RESOURCES
else else
local prefix = os.getenv('LITE_PREFIX') or EXEDIR:match("^(.+)[/\\]bin$") local prefix = EXEDIR:match("^(.+)[/\\]bin$")
DATADIR = prefix and (prefix .. PATHSEP .. 'share' .. PATHSEP .. 'lite-xl') or (EXEDIR .. PATHSEP .. 'data') DATADIR = prefix and (prefix .. '/share/lite-xl') or (EXEDIR .. '/data')
end end
USERDIR = (system.get_file_info(EXEDIR .. PATHSEP .. 'user') and (EXEDIR .. PATHSEP .. 'user')) USERDIR = (os.getenv("XDG_CONFIG_HOME") and os.getenv("XDG_CONFIG_HOME") .. "/lite-xl")
or os.getenv("LITE_USERDIR") or (HOME and (HOME .. '/.config/lite-xl') or (EXEDIR .. '/user'))
or ((os.getenv("XDG_CONFIG_HOME") and os.getenv("XDG_CONFIG_HOME") .. PATHSEP .. "lite-xl"))
or (HOME and (HOME .. PATHSEP .. '.config' .. PATHSEP .. 'lite-xl'))
package.path = DATADIR .. '/?.lua;' package.path = DATADIR .. '/?.lua;' .. package.path
package.path = DATADIR .. '/?/init.lua;' .. package.path 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 suffix = PLATFORM == "Mac OS X" and 'lib' or (PLATFORM == "Windows" and 'dll' or 'so')
package.cpath =
USERDIR .. '/?.' .. ARCH .. "." .. suffix .. ";" ..
USERDIR .. '/?/init.' .. ARCH .. "." .. suffix .. ";" ..
USERDIR .. '/?.' .. suffix .. ";" ..
USERDIR .. '/?/init.' .. suffix .. ";" ..
DATADIR .. '/?.' .. ARCH .. "." .. suffix .. ";" ..
DATADIR .. '/?/init.' .. ARCH .. "." .. suffix .. ";" ..
DATADIR .. '/?.' .. suffix .. ";" ..
DATADIR .. '/?/init.' .. suffix .. ";"
package.native_plugins = {}
package.searchers = { package.searchers[1], package.searchers[2], function(modname)
local path, err = package.searchpath(modname, package.cpath)
if not path then return err end
return system.load_native_plugin, path
end }
table.pack = table.pack or pack or function(...) return {...} end
table.unpack = table.unpack or unpack
bit32 = bit32 or require "core.bit"
require "core.utf8string"
-- Because AppImages change the working directory before running the executable,
-- we need to change it back to the original one.
-- https://github.com/AppImage/AppImageKit/issues/172
-- https://github.com/AppImage/AppImageKit/pull/191
local appimage_owd = os.getenv("OWD")
if os.getenv("APPIMAGE") and appimage_owd then
system.chdir(appimage_owd)
end

File diff suppressed because it is too large Load Diff

View File

@ -4,7 +4,6 @@ local style = {}
style.padding = { x = common.round(14 * SCALE), y = common.round(7 * SCALE) } style.padding = { x = common.round(14 * SCALE), y = common.round(7 * SCALE) }
style.divider_size = common.round(1 * SCALE) style.divider_size = common.round(1 * SCALE)
style.scrollbar_size = common.round(4 * SCALE) style.scrollbar_size = common.round(4 * SCALE)
style.expanded_scrollbar_size = common.round(12 * SCALE)
style.caret_width = common.round(2 * SCALE) style.caret_width = common.round(2 * SCALE)
style.tab_width = common.round(170 * SCALE) style.tab_width = common.round(170 * SCALE)
@ -22,13 +21,43 @@ 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", 15 * SCALE) style.font = renderer.font.load(DATADIR .. "/fonts/FiraSans-Regular.ttf", 14 * SCALE)
style.big_font = style.font:copy(46 * SCALE) style.big_font = style.font:copy(40 * SCALE)
style.icon_font = renderer.font.load(DATADIR .. "/fonts/icons.ttf", 16 * 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(23 * SCALE) style.icon_big_font = style.icon_font:copy(24 * SCALE)
style.code_font = renderer.font.load(DATADIR .. "/fonts/JetBrainsMono-Regular.ttf", 15 * SCALE) style.code_font = renderer.font.load(DATADIR .. "/fonts/JetBrainsMono-Regular.ttf", 14 * SCALE)
style.background = { common.color "#2e2e32" }
style.background2 = { common.color "#252529" }
style.background3 = { common.color "#252529" }
style.text = { common.color "#97979c" }
style.caret = { common.color "#93DDFA" }
style.accent = { common.color "#e1e1e6" }
style.dim = { common.color "#525257" }
style.divider = { common.color "#202024" }
style.selection = { common.color "#48484f" }
style.line_number = { common.color "#525259" }
style.line_number2 = { common.color "#83838f" }
style.line_highlight = { common.color "#343438" }
style.scrollbar = { common.color "#414146" }
style.scrollbar2 = { common.color "#4b4b52" }
style.nagbar = { common.color "#FF0000" }
style.nagbar_text = { common.color "#FFFFFF" }
style.nagbar_dim = { common.color "rgba(0, 0, 0, 0.45)" }
style.drag_overlay = { common.color "rgba(255,255,255,0.1)" }
style.drag_overlay_tab = { common.color "#93DDFA" }
style.syntax = {} style.syntax = {}
style.syntax["normal"] = { common.color "#e1e1e6" }
style.syntax["symbol"] = { common.color "#e1e1e6" }
style.syntax["comment"] = { common.color "#676b6f" }
style.syntax["keyword"] = { common.color "#E58AC9" }
style.syntax["keyword2"] = { common.color "#F77483" }
style.syntax["number"] = { common.color "#FFA94D" }
style.syntax["literal"] = { common.color "#FFA94D" }
style.syntax["string"] = { common.color "#f7c95c" }
style.syntax["operator"] = { common.color "#93DDFA" }
style.syntax["function"] = { common.color "#93DDFA" }
-- This can be used to override fonts per syntax group. -- This can be used to override fonts per syntax group.
-- The syntax highlighter will take existing values from this table and -- The syntax highlighter will take existing values from this table and
@ -37,6 +66,4 @@ style.syntax = {}
style.syntax_fonts = {} style.syntax_fonts = {}
-- style.syntax_fonts["comment"] = renderer.font.load(path_to_font, size_of_font, rendering_options) -- style.syntax_fonts["comment"] = renderer.font.load(path_to_font, size_of_font, rendering_options)
style.log = {}
return style return style

View File

@ -3,49 +3,26 @@ local common = require "core.common"
local syntax = {} local syntax = {}
syntax.items = {} syntax.items = {}
local plain_text_syntax = { name = "Plain Text", patterns = {}, symbols = {} } local plain_text_syntax = { patterns = {}, symbols = {} }
function syntax.add(t) function syntax.add(t)
if type(t.space_handling) ~= "boolean" then t.space_handling = true end
if t.patterns then
-- the rule %s+ gives us a performance gain for the tokenizer in lines with
-- long amounts of consecutive spaces, can be disabled by plugins where it
-- causes conflicts by declaring the table property: space_handling = false
if t.space_handling then
table.insert(t.patterns, { pattern = "%s+", type = "normal" })
end
-- this rule gives us additional performance gain by matching every word
-- that was not matched by the syntax patterns as a single token, preventing
-- the tokenizer from iterating over each character individually which is a
-- lot slower since iteration occurs in lua instead of C and adding to that
-- it will also try to match every pattern to a single char (same as spaces)
table.insert(t.patterns, { pattern = "%w+%f[%s]", type = "normal" })
end
table.insert(syntax.items, t) table.insert(syntax.items, t)
end end
local function find(string, field) local function find(string, field)
local best_match = 0
local best_syntax
for i = #syntax.items, 1, -1 do for i = #syntax.items, 1, -1 do
local t = syntax.items[i] local t = syntax.items[i]
local s, e = common.match_pattern(string, t[field] or {}) if common.match_pattern(string, t[field] or {}) then
if s and e - s > best_match then return t
best_match = e - s
best_syntax = t
end end
end end
return best_syntax
end end
function syntax.get(filename, header) function syntax.get(filename, header)
return (filename and find(filename, "files")) return find(filename, "files")
or (header and find(header, "headers")) or find(header, "headers")
or plain_text_syntax or plain_text_syntax
end end

View File

@ -3,14 +3,6 @@ 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 icon_colors = {
bg = { common.color "#2e2e32ff" },
color6 = { common.color "#e1e1e6ff" },
color7 = { common.color "#ffa94dff" },
color8 = { common.color "#93ddfaff" },
color9 = { common.color "#f7c95cff" }
};
local restore_command = { local restore_command = {
symbol = "w", action = function() system.set_window_mode("normal") end symbol = "w", action = function() system.set_window_mode("normal") end
} }
@ -25,8 +17,6 @@ local title_commands = {
{symbol = "X", action = function() core.quit() end}, {symbol = "X", action = function() core.quit() end},
} }
---@class core.titleview : core.view
---@field super core.view
local TitleView = View:extend() local TitleView = View:extend()
local function title_view_height() local function title_view_height()
@ -51,10 +41,6 @@ function TitleView:configure_hit_test(borderless)
end end
end end
function TitleView:on_scale_change()
self:configure_hit_test(self.visible)
end
function TitleView:update() function TitleView:update()
self.size.y = self.visible and title_view_height() or 0 self.size.y = self.visible and title_view_height() or 0
title_commands[2] = core.window_mode == "maximized" and restore_command or maximize_command title_commands[2] = core.window_mode == "maximized" and restore_command or maximize_command
@ -67,11 +53,7 @@ function TitleView:draw_window_title()
local ox, oy = self:get_content_offset() local ox, oy = self:get_content_offset()
local color = style.text local color = style.text
local x, y = ox + style.padding.x, oy + style.padding.y local x, y = ox + style.padding.x, oy + style.padding.y
common.draw_text(style.icon_font, icon_colors.bg, "5", nil, x, y, 0, h) x = common.draw_text(style.icon_font, color, "M ", nil, x, y, 0, h)
common.draw_text(style.icon_font, icon_colors.color6, "6", nil, x, y, 0, h)
common.draw_text(style.icon_font, icon_colors.color7, "7", nil, x, y, 0, h)
common.draw_text(style.icon_font, icon_colors.color8, "8", nil, x, y, 0, h)
x = common.draw_text(style.icon_font, icon_colors.color9, "9 ", nil, x, y, 0, h)
local title = core.compose_window_title(core.window_title) local title = core.compose_window_title(core.window_title)
common.draw_text(style.font, color, title, nil, x, y, 0, h) common.draw_text(style.font, color, title, nil, x, y, 0, h)
end end

View File

@ -1,16 +1,11 @@
local core = require "core"
local syntax = require "core.syntax" local syntax = require "core.syntax"
local config = require "core.config"
local tokenizer = {} local tokenizer = {}
local bad_patterns = {}
local function push_token(t, type, text) local function push_token(t, type, text)
if not text or #text == 0 then return end
type = type or "normal"
local prev_type = t[#t-1] local prev_type = t[#t-1]
local prev_text = t[#t] local prev_text = t[#t]
if prev_type and (prev_type == type or (prev_text:ufind("^%s*$") and type ~= "incomplete")) then if prev_type and (prev_type == type or prev_text:find("^%s*$")) then
t[#t-1] = type t[#t-1] = type
t[#t] = prev_text .. text t[#t] = prev_text .. text
else else
@ -42,47 +37,41 @@ local function push_tokens(t, syn, pattern, full_text, find_results)
local fin = find_results[i + 1] - 1 local fin = find_results[i + 1] - 1
local type = pattern.type[i - 2] local type = pattern.type[i - 2]
-- ↑ (i - 2) to convert from [3; n] to [1; n] -- ↑ (i - 2) to convert from [3; n] to [1; n]
local text = full_text:usub(start, fin) local text = full_text:sub(start, fin)
push_token(t, syn.symbols[text] or type, text) push_token(t, syn.symbols[text] or type, text)
end end
else else
local start, fin = find_results[1], find_results[2] local start, fin = find_results[1], find_results[2]
local text = full_text:usub(start, fin) local text = full_text:sub(start, fin)
push_token(t, syn.symbols[text] or pattern.type, text) push_token(t, syn.symbols[text] or pattern.type, text)
end end
end end
-- State is a string of bytes, where the count of bytes represents the depth
-- of the subsyntax we are currently in. Each individual byte represents the
-- index of the pattern for the current subsyntax in relation to its parent
-- syntax. Using a string of bytes allows us to have as many subsyntaxes as
-- bytes can be stored on a string while keeping some level of performance in
-- comparison to a Lua table. The only limitation is that a syntax would not
-- be able to contain more than 255 patterns.
--
-- Lets say a state contains 2 bytes byte #1 with value `3` and byte #2 with
-- a value of `5`. This would mean that on the parent syntax at index `3` a
-- pattern subsyntax that matched current text was found, then inside that
-- subsyntax another subsyntax pattern at index `5` that matched current text
-- was also found.
-- Calling `push_subsyntax` appends the current subsyntax pattern index to the -- State is a 32-bit number that is four separate bytes, illustrating how many
-- state and increases the stack depth. Calling `pop_subsyntax` clears the -- differnet delimiters we have open, and which subsyntaxes we have active.
-- last appended subsyntax and decreases the stack. -- At most, there are 3 subsyntaxes active at the same time. Beyond that,
-- does not support further highlighting.
-- You can think of it as a maximum 4 integer (0-255) stack. It always has
-- 1 integer in it. Calling `push_subsyntax` increases the stack depth. Calling
-- `pop_subsyntax` decreases it. The integers represent the index of a pattern
-- that we're following in the syntax. The top of the stack can be any valid
-- pattern index, any integer lower in the stack must represent a pattern that
-- specifies a subsyntax.
-- If you do not have subsyntaxes in your syntax, the three most
-- singificant numbers will always be 0, the stack will only ever be length 1
-- and the state variable will only ever range from 0-255.
local function retrieve_syntax_state(incoming_syntax, state) local function retrieve_syntax_state(incoming_syntax, state)
local current_syntax, subsyntax_info, current_pattern_idx, current_level = local current_syntax, subsyntax_info, current_pattern_idx, current_level =
incoming_syntax, nil, state:byte(1) or 0, 1 incoming_syntax, nil, state, 0
if if state > 0 and (state > 255 or current_syntax.patterns[state].syntax) then
current_pattern_idx > 0 -- If we have higher bits, then decode them one at a time, and find which
and
current_syntax.patterns[current_pattern_idx]
then
-- If the state is not empty we iterate over each byte, and find which
-- syntax we're using. Rather than walking the bytes, and calling into -- syntax we're using. Rather than walking the bytes, and calling into
-- `syntax` each time, we could probably cache this in a single table. -- `syntax` each time, we could probably cache this in a single table.
for i = 1, #state do for i = 0, 2 do
local target = state:byte(i) local target = bit32.extract(state, i*8, 8)
if target ~= 0 then if target ~= 0 then
if current_syntax.patterns[target].syntax then if current_syntax.patterns[target].syntax then
subsyntax_info = current_syntax.patterns[target] subsyntax_info = current_syntax.patterns[target]
@ -102,87 +91,32 @@ local function retrieve_syntax_state(incoming_syntax, state)
return current_syntax, subsyntax_info, current_pattern_idx, current_level return current_syntax, subsyntax_info, current_pattern_idx, current_level
end end
---Return the list of syntaxes used in the specified state. function tokenizer.tokenize(incoming_syntax, text, state)
---@param base_syntax table @The initial base syntax (the syntax of the file) local res = {}
---@param state string @The state of the tokenizer to extract from
---@return table @Array of syntaxes starting from the innermost one
function tokenizer.extract_subsyntaxes(base_syntax, state)
local current_syntax
local t = {}
repeat
current_syntax = retrieve_syntax_state(base_syntax, state)
table.insert(t, current_syntax)
state = string.sub(state, 2)
until #state == 0
return t
end
local function report_bad_pattern(log_fn, syntax, pattern_idx, msg, ...)
if not bad_patterns[syntax] then
bad_patterns[syntax] = { }
end
if bad_patterns[syntax][pattern_idx] then return end
bad_patterns[syntax][pattern_idx] = true
log_fn("Malformed pattern #%d <%s> in %s language plugin.\n" .. msg,
pattern_idx,
syntax.patterns[pattern_idx].pattern or syntax.patterns[pattern_idx].regex,
syntax.name or "unnamed", ...)
end
---@param incoming_syntax table
---@param text string
---@param state string
function tokenizer.tokenize(incoming_syntax, text, state, resume)
local res
local i = 1 local i = 1
if #incoming_syntax.patterns == 0 then if #incoming_syntax.patterns == 0 then
return { "normal", text } return { "normal", text }
end end
state = state or string.char(0) state = state or 0
if resume then
res = resume.res
-- Remove "incomplete" tokens
while res[#res-1] == "incomplete" do
table.remove(res, #res)
table.remove(res, #res)
end
i = resume.i
state = resume.state
end
res = res or {}
-- incoming_syntax : the parent syntax of the file. -- incoming_syntax : the parent syntax of the file.
-- state : a string of bytes representing syntax state (see above) -- state : a 32-bit number representing syntax state (see above)
-- current_syntax : the syntax we're currently in. -- current_syntax : the syntax we're currently in.
-- subsyntax_info : info about the delimiters of this subsyntax. -- subsyntax_info : info about the delimiters of this subsyntax.
-- current_pattern_idx: the index of the pattern we're on for this syntax. -- current_pattern_idx: the index of the pattern we're on for this syntax.
-- current_level : how many subsyntaxes deep we are. -- current_level : how many subsyntaxes deep we are.
local current_syntax, subsyntax_info, current_pattern_idx, current_level = local current_syntax, subsyntax_info, current_pattern_idx, current_level =
retrieve_syntax_state(incoming_syntax, state) retrieve_syntax_state(incoming_syntax, state)
-- Should be used to set the state variable. Don't modify it directly. -- Should be used to set the state variable. Don't modify it directly.
local function set_subsyntax_pattern_idx(pattern_idx) local function set_subsyntax_pattern_idx(pattern_idx)
current_pattern_idx = pattern_idx current_pattern_idx = pattern_idx
local state_len = #state state = bit32.replace(state, pattern_idx, current_level*8, 8)
if current_level > state_len then
state = state .. string.char(pattern_idx)
elseif state_len == 1 then
state = string.char(pattern_idx)
else
state = ("%s%s%s"):format(
state:sub(1,current_level-1),
string.char(pattern_idx),
state:sub(current_level+1)
)
end
end end
local function push_subsyntax(entering_syntax, pattern_idx) local function push_subsyntax(entering_syntax, pattern_idx)
set_subsyntax_pattern_idx(pattern_idx) set_subsyntax_pattern_idx(pattern_idx)
current_level = current_level + 1 current_level = current_level + 1
@ -191,96 +125,40 @@ function tokenizer.tokenize(incoming_syntax, text, state, resume)
entering_syntax.syntax or syntax.get(entering_syntax.syntax) entering_syntax.syntax or syntax.get(entering_syntax.syntax)
current_pattern_idx = 0 current_pattern_idx = 0
end end
local function pop_subsyntax() local function pop_subsyntax()
current_level = current_level - 1
state = string.sub(state, 1, current_level)
set_subsyntax_pattern_idx(0) set_subsyntax_pattern_idx(0)
current_syntax, subsyntax_info, current_pattern_idx, current_level = current_level = current_level - 1
set_subsyntax_pattern_idx(0)
current_syntax, subsyntax_info, current_pattern_idx, current_level =
retrieve_syntax_state(incoming_syntax, state) retrieve_syntax_state(incoming_syntax, state)
end end
local function find_text(text, p, offset, at_start, close) local function find_text(text, p, offset, at_start, close)
local target, res = p.pattern or p.regex, { 1, offset - 1 } local target, res = p.pattern or p.regex, { 1, offset - 1 }, p.regex
local p_idx = close and 2 or 1 local code = type(target) == "table" and target[close and 2 or 1] or target
local code = type(target) == "table" and target[p_idx] or target
if p.whole_line == nil then p.whole_line = { } end
if p.whole_line[p_idx] == nil then
-- Match patterns that start with '^'
p.whole_line[p_idx] = code:umatch("^%^") and true or false
if p.whole_line[p_idx] then
-- Remove '^' from the beginning of the pattern
if type(target) == "table" then
target[p_idx] = code:usub(2)
code = target[p_idx]
else
p.pattern = p.pattern and code:usub(2)
p.regex = p.regex and code:usub(2)
code = p.pattern or p.regex
end
end
end
if p.regex and type(p.regex) ~= "table" then if p.regex and type(p.regex) ~= "table" then
p._regex = p._regex or regex.compile(p.regex) p._regex = p._regex or regex.compile(p.regex)
code = p._regex code = p._regex
end end
repeat repeat
local next = res[2] + 1 res = p.pattern and { text:find(at_start and "^" .. code or code, res[2]+1) }
-- If the pattern contained '^', allow matching only the whole line or { regex.match(code, text, res[2]+1, at_start and regex.ANCHORED or 0) }
if p.whole_line[p_idx] and next > 1 then if res[1] and close and target[3] then
return
end
res = p.pattern and { text:ufind((at_start or p.whole_line[p_idx]) and "^" .. code or code, next) }
or { regex.find(code, text, text:ucharpos(next), (at_start or p.whole_line[p_idx]) and regex.ANCHORED or 0) }
if p.regex and #res > 0 then -- set correct utf8 len for regex result
local char_pos_1 = res[1] > next and string.ulen(text:sub(1, res[1])) or next
local char_pos_2 = string.ulen(text:sub(1, res[2]))
for i=3,#res do
res[i] = string.ulen(text:sub(1, res[i] - 1)) + 1
end
res[1] = char_pos_1
res[2] = char_pos_2
end
if res[1] and target[3] then
-- Check to see if the escaped character is there,
-- and if it is not itself escaped.
local count = 0 local count = 0
for i = res[1] - 1, 1, -1 do for i = res[1] - 1, 1, -1 do
if text:ubyte(i) ~= target[3]:ubyte() then break end if text:byte(i) ~= target[3]:byte() then break end
count = count + 1 count = count + 1
end end
if count % 2 == 0 then -- Check to see if the escaped character is there,
-- The match is not escaped, so confirm it -- and if it is not itself escaped.
break if count % 2 == 0 then break end
elseif not close then
-- The *open* match is escaped, so avoid it
return
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 table.unpack(res) return unpack(res)
end end
local text_len = text:ulen() while i <= #text do
local start_time = system.get_time()
local starting_i = i
while text_len ~= nil and i <= text_len do
-- Every 200 chars, check if we're out of time
if i - starting_i > 200 then
starting_i = i
if system.get_time() - start_time > 0.5 / config.fps then
-- We're out of time
push_token(res, "incomplete", string.usub(text, i))
return res, string.char(0), {
res = res,
i = i,
state = state
}
end
end
-- continue trying to match the end pattern of a pair if we have a state set -- continue trying to match the end pattern of a pair if we have a state set
if current_pattern_idx > 0 then if current_pattern_idx > 0 then
local p = current_syntax.patterns[current_pattern_idx] local p = current_syntax.patterns[current_pattern_idx]
@ -292,12 +170,12 @@ function tokenizer.tokenize(incoming_syntax, text, state, resume)
-- precedence over ending the delimiter in the subsyntax. -- precedence over ending the delimiter in the subsyntax.
if subsyntax_info then if subsyntax_info then
local ss, se = find_text(text, subsyntax_info, i, false, true) local ss, se = find_text(text, subsyntax_info, i, false, true)
-- If we find that we end the subsyntax before the -- If we find that we end the subsyntax before the
-- delimiter, push the token, and signal we shouldn't -- delimiter, push the token, and signal we shouldn't
-- treat the bit after as a token to be normally parsed -- treat the bit after as a token to be normally parsed
-- (as it's the syntax delimiter). -- (as it's the syntax delimiter).
if ss and (s == nil or ss < s) then if ss and (s == nil or ss < s) then
push_token(res, p.type, text:usub(i, ss - 1)) push_token(res, p.type, text:sub(i, ss - 1))
i = ss i = ss
cont = false cont = false
end end
@ -306,11 +184,11 @@ function tokenizer.tokenize(incoming_syntax, text, state, resume)
-- continue on as normal. -- continue on as normal.
if cont then if cont then
if s then if s then
push_token(res, p.type, text:usub(i, e)) push_token(res, p.type, text:sub(i, e))
set_subsyntax_pattern_idx(0) set_subsyntax_pattern_idx(0)
i = e + 1 i = e + 1
else else
push_token(res, p.type, text:usub(i)) push_token(res, p.type, text:sub(i))
break break
end end
end end
@ -318,15 +196,13 @@ function tokenizer.tokenize(incoming_syntax, text, state, resume)
-- General end of syntax check. Applies in the case where -- General end of syntax check. Applies in the case where
-- we're ending early in the middle of a delimiter, or -- we're ending early in the middle of a delimiter, or
-- just normally, upon finding a token. -- just normally, upon finding a token.
while subsyntax_info do if subsyntax_info then
local s, e = find_text(text, subsyntax_info, i, true, true) local s, e = find_text(text, subsyntax_info, i, true, true)
if s then if s then
push_token(res, subsyntax_info.type, text:usub(i, e)) push_token(res, subsyntax_info.type, text:sub(i, e))
-- On finding unescaped delimiter, pop it. -- On finding unescaped delimiter, pop it.
pop_subsyntax() pop_subsyntax()
i = e + 1 i = e + 1
else
break
end end
end end
@ -335,28 +211,6 @@ function tokenizer.tokenize(incoming_syntax, text, state, resume)
for n, p in ipairs(current_syntax.patterns) do for n, p in ipairs(current_syntax.patterns) do
local find_results = { find_text(text, p, i, true, false) } local find_results = { find_text(text, p, i, true, false) }
if find_results[1] then if find_results[1] then
-- Check for patterns successfully matching nothing
if find_results[1] > find_results[2] then
report_bad_pattern(core.warn, current_syntax, n,
"Pattern successfully matched, but nothing was captured.")
goto continue
end
-- Check for patterns with mismatching number of `types`
local type_is_table = type(p.type) == "table"
local n_types = type_is_table and #p.type or 1
if #find_results == 2 and type_is_table then
report_bad_pattern(core.warn, current_syntax, n,
"Token type is a table, but a string was expected.")
p.type = p.type[1]
elseif #find_results - 1 > n_types then
report_bad_pattern(core.error, current_syntax, n,
"Not enough token types: got %d needed %d.", n_types, #find_results - 1)
elseif #find_results - 1 < n_types then
report_bad_pattern(core.warn, current_syntax, n,
"Too many token types: got %d needed %d.", n_types, #find_results - 1)
end
-- matched pattern; make and add tokens -- matched pattern; make and add tokens
push_tokens(res, current_syntax, p, text, find_results) push_tokens(res, current_syntax, p, text, find_results)
-- update state if this was a start|end pattern pair -- update state if this was a start|end pattern pair
@ -364,7 +218,7 @@ function tokenizer.tokenize(incoming_syntax, text, state, resume)
-- If we have a subsyntax, push that onto the subsyntax stack. -- If we have a subsyntax, push that onto the subsyntax stack.
if p.syntax then if p.syntax then
push_subsyntax(p, n) push_subsyntax(p, n)
else else
set_subsyntax_pattern_idx(n) set_subsyntax_pattern_idx(n)
end end
end end
@ -372,13 +226,12 @@ function tokenizer.tokenize(incoming_syntax, text, state, resume)
i = find_results[2] + 1 i = find_results[2] + 1
matched = true matched = true
break break
::continue::
end end
end end
-- consume character if we didn't match -- consume character if we didn't match
if not matched then if not matched then
push_token(res, "normal", text:usub(i, i)) push_token(res, "normal", text:sub(i, i))
i = i + 1 i = i + 1
end end
end end

View File

@ -1,32 +0,0 @@
--------------------------------------------------------------------------------
-- inject utf8 functions to strings
--------------------------------------------------------------------------------
local utf8 = require "utf8extra"
string.ubyte = utf8.byte
string.uchar = utf8.char
string.ufind = utf8.find
string.ugmatch = utf8.gmatch
string.ugsub = utf8.gsub
string.ulen = utf8.len
string.ulower = utf8.lower
string.umatch = utf8.match
string.ureverse = utf8.reverse
string.usub = utf8.sub
string.uupper = utf8.upper
string.uescape = utf8.escape
string.ucharpos = utf8.charpos
string.unext = utf8.next
string.uinsert = utf8.insert
string.uremove = utf8.remove
string.uwidth = utf8.width
string.uwidthindex = utf8.widthindex
string.utitle = utf8.title
string.ufold = utf8.fold
string.uncasecmp = utf8.ncasecmp
string.uoffset = utf8.offset
string.ucodepoint = utf8.codepoint
string.ucodes = utf8.codes

View File

@ -1,51 +1,10 @@
local core = require "core" local core = require "core"
local config = require "core.config" local config = require "core.config"
local style = require "core.style"
local common = require "core.common" local common = require "core.common"
local Object = require "core.object" local Object = require "core.object"
local Scrollbar = require "core.scrollbar"
---@class core.view.position
---@field x number
---@field y number
---@class core.view.scroll
---@field x number
---@field y number
---@field to core.view.position
---@class core.view.thumbtrack
---@field thumb number
---@field track number
---@class core.view.thumbtrackwidth
---@field thumb number
---@field track number
---@field to core.view.thumbtrack
---@class core.view.scrollbar
---@field x core.view.thumbtrack
---@field y core.view.thumbtrack
---@field w core.view.thumbtrackwidth
---@field h core.view.thumbtrack
---@alias core.view.cursor "'arrow'" | "'ibeam'" | "'sizeh'" | "'sizev'" | "'hand'"
---@alias core.view.mousebutton "'left'" | "'right'"
---@alias core.view.context "'application'" | "'session'"
---Base view.
---@class core.view : core.object
---@field context core.view.context
---@field super core.object
---@field position core.view.position
---@field size core.view.position
---@field scroll core.view.scroll
---@field cursor core.view.cursor
---@field scrollable boolean
---@field v_scrollbar core.scrollbar
---@field h_scrollbar core.scrollbar
---@field current_scale number
local View = Object:extend() local View = Object:extend()
-- context can be "application" or "session". The instance of objects -- context can be "application" or "session". The instance of objects
@ -59,18 +18,14 @@ function View:new()
self.scroll = { x = 0, y = 0, to = { x = 0, y = 0 } } self.scroll = { x = 0, y = 0, to = { x = 0, y = 0 } }
self.cursor = "arrow" self.cursor = "arrow"
self.scrollable = false self.scrollable = false
self.v_scrollbar = Scrollbar({direction = "v", alignment = "e"})
self.h_scrollbar = Scrollbar({direction = "h", alignment = "e"})
self.current_scale = SCALE
end end
function View:move_towards(t, k, dest, rate, name) function View:move_towards(t, k, dest, rate)
if type(t) ~= "table" then if type(t) ~= "table" then
return self:move_towards(self, t, k, dest, rate, name) return self:move_towards(self, t, k, dest, rate)
end end
local val = t[k] local val = t[k]
local diff = math.abs(val - dest) if not config.transitions or math.abs(val - dest) < 0.5 then
if not config.transitions or diff < 0.5 or config.disabled_transitions[name] then
t[k] = dest t[k] = dest
else else
rate = rate or 0.5 rate = rate or 0.5
@ -80,7 +35,7 @@ function View:move_towards(t, k, dest, rate, name)
end end
t[k] = common.lerp(val, dest, rate) t[k] = common.lerp(val, dest, rate)
end end
if diff > 1e-8 then if val ~= dest then
core.redraw = true core.redraw = true
end end
end end
@ -91,153 +46,73 @@ function View:try_close(do_close)
end end
---@return string
function View:get_name() function View:get_name()
return "---" return "---"
end end
---@return number
function View:get_scrollable_size() function View:get_scrollable_size()
return math.huge return math.huge
end end
---@return number
function View:get_h_scrollable_size() function View:get_scrollbar_rect()
return 0 local sz = self:get_scrollable_size()
if sz <= self.size.y or sz == math.huge then
return 0, 0, 0, 0
end
local h = math.max(20, self.size.y * self.size.y / sz)
return
self.position.x + self.size.x - style.scrollbar_size,
self.position.y + self.scroll.y * (self.size.y - h) / (sz - self.size.y),
style.scrollbar_size,
h
end end
---@param x number
---@param y number
---@return boolean
function View:scrollbar_overlaps_point(x, y) function View:scrollbar_overlaps_point(x, y)
return not (not (self.v_scrollbar:overlaps(x, y) or self.h_scrollbar:overlaps(x, y))) local sx, sy, sw, sh = self:get_scrollbar_rect()
return x >= sx - sw * 3 and x < sx + sw and y >= sy and y < sy + sh
end end
---@return boolean
function View:scrollbar_dragging()
return self.v_scrollbar.dragging or self.h_scrollbar.dragging
end
---@return boolean
function View:scrollbar_hovering()
return self.v_scrollbar.hovering.track or self.h_scrollbar.hovering.track
end
---@param button core.view.mousebutton
---@param x number
---@param y number
---@param clicks integer
---return boolean
function View:on_mouse_pressed(button, x, y, clicks) function View:on_mouse_pressed(button, x, y, clicks)
if not self.scrollable then return end if self:scrollbar_overlaps_point(x, y) then
local result = self.v_scrollbar:on_mouse_pressed(button, x, y, clicks) self.dragging_scrollbar = true
if result then
if result ~= true then
self.scroll.to.y = result * self:get_scrollable_size()
end
return true
end
result = self.h_scrollbar:on_mouse_pressed(button, x, y, clicks)
if result then
if result ~= true then
self.scroll.to.x = result * self:get_h_scrollable_size()
end
return true return true
end end
end end
---@param button core.view.mousebutton
---@param x number
---@param y number
function View:on_mouse_released(button, x, y) function View:on_mouse_released(button, x, y)
if not self.scrollable then return end self.dragging_scrollbar = false
self.v_scrollbar:on_mouse_released(button, x, y)
self.h_scrollbar:on_mouse_released(button, x, y)
end end
---@param x number
---@param y number
---@param dx number
---@param dy number
function View:on_mouse_moved(x, y, dx, dy) function View:on_mouse_moved(x, y, dx, dy)
if not self.scrollable then return end if self.dragging_scrollbar then
local result local delta = self:get_scrollable_size() / self.size.y * dy
if self.h_scrollbar.dragging then goto skip_v_scrollbar end self.scroll.to.y = self.scroll.to.y + delta
result = self.v_scrollbar:on_mouse_moved(x, y, dx, dy)
if result then
if result ~= true then
self.scroll.to.y = result * self:get_scrollable_size()
if not config.animate_drag_scroll then
self:clamp_scroll_position()
self.scroll.y = self.scroll.to.y
end
end
-- hide horizontal scrollbar
self.h_scrollbar:on_mouse_left()
return true
end
::skip_v_scrollbar::
result = self.h_scrollbar:on_mouse_moved(x, y, dx, dy)
if result then
if result ~= true then
self.scroll.to.x = result * self:get_h_scrollable_size()
if not config.animate_drag_scroll then
self:clamp_scroll_position()
self.scroll.x = self.scroll.to.x
end
end
return true
end end
self.hovered_scrollbar = self:scrollbar_overlaps_point(x, y)
end end
function View:on_mouse_left()
if not self.scrollable then return end
self.v_scrollbar:on_mouse_left()
self.h_scrollbar:on_mouse_left()
end
---@param filename string
---@param x number
---@param y number
---@return boolean
function View:on_file_dropped(filename, x, y)
return false
end
---@param text string
function View:on_text_input(text) function View:on_text_input(text)
-- no-op -- no-op
end end
-- no-op
View.on_ime_text_editing = View.on_text_input
function View:on_ime_text_editing(text, start, length)
-- no-op 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
---@param y number @Vertical scroll delta; positive is "up"
---@param x number @Horizontal scroll delta; positive is "left"
---@return boolean @Capture event
function View:on_mouse_wheel(y, x)
-- no-op
end
---Can be overriden to listen for scale change events to apply
---any neccesary changes in sizes, padding, etc...
---@param new_scale number
---@param prev_scale number
function View:on_scale_change(new_scale, prev_scale) end
function View:get_content_bounds() function View:get_content_bounds()
local x = self.scroll.x local x = self.scroll.x
local y = self.scroll.y local y = self.scroll.y
@ -245,8 +120,6 @@ function View:get_content_bounds()
end end
---@return number x
---@return number y
function View:get_content_offset() function View:get_content_offset()
local x = common.round(self.position.x - self.scroll.x) local x = common.round(self.position.x - self.scroll.x)
local y = common.round(self.position.y - self.scroll.y) local y = common.round(self.position.y - self.scroll.y)
@ -257,50 +130,28 @@ end
function View:clamp_scroll_position() function View:clamp_scroll_position()
local max = self:get_scrollable_size() - self.size.y local max = self:get_scrollable_size() - self.size.y
self.scroll.to.y = common.clamp(self.scroll.to.y, 0, max) self.scroll.to.y = common.clamp(self.scroll.to.y, 0, max)
max = self:get_h_scrollable_size() - self.size.x
self.scroll.to.x = common.clamp(self.scroll.to.x, 0, max)
end
function View:update_scrollbar()
local v_scrollable = self:get_scrollable_size()
self.v_scrollbar:set_size(self.position.x, self.position.y, self.size.x, self.size.y, v_scrollable)
self.v_scrollbar:set_percent(self.scroll.y/v_scrollable)
self.v_scrollbar:update()
local h_scrollable = self:get_h_scrollable_size()
self.h_scrollbar:set_size(self.position.x, self.position.y, self.size.x, self.size.y, h_scrollable)
self.h_scrollbar:set_percent(self.scroll.x/h_scrollable)
self.h_scrollbar:update()
end end
function View:update() function View:update()
if self.current_scale ~= SCALE then
self:on_scale_change(SCALE, self.current_scale)
self.current_scale = SCALE
end
self:clamp_scroll_position() self:clamp_scroll_position()
self:move_towards(self.scroll, "x", self.scroll.to.x, 0.3, "scroll") self:move_towards(self.scroll, "x", self.scroll.to.x, 0.3)
self:move_towards(self.scroll, "y", self.scroll.to.y, 0.3, "scroll") self:move_towards(self.scroll, "y", self.scroll.to.y, 0.3)
if not self.scrollable then return end
self:update_scrollbar()
end end
---@param color renderer.color
function View:draw_background(color) function View:draw_background(color)
local x, y = self.position.x, self.position.y local x, y = self.position.x, self.position.y
local w, h = self.size.x, self.size.y local w, h = self.size.x, self.size.y
renderer.draw_rect(x, y, w, h, color) renderer.draw_rect(x, y, w + x % 1, h + y % 1, color)
end end
function View:draw_scrollbar() function View:draw_scrollbar()
self.v_scrollbar:draw() local x, y, w, h = self:get_scrollbar_rect()
self.h_scrollbar:draw() local highlight = self.hovered_scrollbar or self.dragging_scrollbar
local color = highlight and style.scrollbar2 or style.scrollbar
renderer.draw_rect(x, y, w, h, color)
end end

Binary file not shown.

View File

@ -1,4 +1,4 @@
-- mod-version:3 -- 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"
@ -10,66 +10,14 @@ local RootView = require "core.rootview"
local DocView = require "core.docview" local DocView = require "core.docview"
local Doc = require "core.doc" local Doc = require "core.doc"
config.plugins.autocomplete = common.merge({ config.plugins.autocomplete = {
-- Amount of characters that need to be written for autocomplete -- Amount of characters that need to be written for autocomplete
min_len = 3, min_len = 3,
-- The max amount of visible items -- The max amount of visible items
max_height = 6, max_height = 6,
-- The max amount of scrollable items -- The max amount of scrollable items
max_suggestions = 100, max_suggestions = 100,
-- Maximum amount of symbols to cache per document }
max_symbols = 4000,
-- Font size of the description box
desc_font_size = 12,
-- The config specification used by gui generators
config_spec = {
name = "Autocomplete",
{
label = "Minimum Length",
description = "Amount of characters that need to be written for autocomplete to popup.",
path = "min_len",
type = "number",
default = 3,
min = 1,
max = 5
},
{
label = "Maximum Height",
description = "The maximum amount of visible items.",
path = "max_height",
type = "number",
default = 6,
min = 1,
max = 20
},
{
label = "Maximum Suggestions",
description = "The maximum amount of scrollable items.",
path = "max_suggestions",
type = "number",
default = 100,
min = 10,
max = 10000
},
{
label = "Maximum Symbols",
description = "Maximum amount of symbols to cache per document.",
path = "max_symbols",
type = "number",
default = 4000,
min = 1000,
max = 10000
},
{
label = "Description Font Size",
description = "Font size of the description box.",
path = "desc_font_size",
type = "number",
default = 12,
min = 8
}
}
}, config.plugins.autocomplete)
local autocomplete = {} local autocomplete = {}
@ -85,7 +33,7 @@ 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, manually_triggered) 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
if type(info) == "table" then if type(info) == "table" then
@ -95,10 +43,9 @@ function autocomplete.add(t, manually_triggered)
{ {
text = text, text = text,
info = info.info, info = info.info,
desc = info.desc, -- Description shown on item selected desc = info.desc, -- Description shown on item selected
onhover = info.onhover, -- A callback called once when item is hovered cb = info.cb, -- A callback called once when item is selected
onselect = info.onselect, -- A callback called when item is selected data = info.data -- Optional data that can be used on cb
data = info.data -- Optional data that can be used on cb
}, },
mt mt
) )
@ -109,7 +56,7 @@ function autocomplete.add(t, manually_triggered)
end end
end end
if not manually_triggered then if not triggered_manually then
autocomplete.map[t.name] = { files = t.files or ".*", items = items } autocomplete.map[t.name] = { files = t.files or ".*", items = items }
else else
autocomplete.map_manually[t.name] = { files = t.files or ".*", items = items } autocomplete.map_manually[t.name] = { files = t.files or ".*", items = items }
@ -119,43 +66,26 @@ end
-- --
-- Thread that scans open document symbols and cache them -- Thread that scans open document symbols and cache them
-- --
local max_symbols = config.plugins.autocomplete.max_symbols local max_symbols = config.max_symbols
core.add_thread(function() core.add_thread(function()
local cache = setmetatable({}, { __mode = "k" }) local cache = setmetatable({}, { __mode = "k" })
local function get_syntax_symbols(symbols, doc)
if doc.syntax then
for sym in pairs(doc.syntax.symbols) do
symbols[sym] = true
end
end
end
local function get_symbols(doc) local function get_symbols(doc)
local s = {} if doc.disable_symbols then return {} end
get_syntax_symbols(s, doc)
if doc.disable_symbols then return s end
local i = 1 local i = 1
local s = {}
local symbols_count = 0 local symbols_count = 0
while i <= #doc.lines do while i < #doc.lines do
for sym in doc.lines[i]:gmatch(config.symbol_pattern) do for sym in doc.lines[i]:gmatch(config.symbol_pattern) do
if not s[sym] then if not s[sym] then
symbols_count = symbols_count + 1 symbols_count = symbols_count + 1
if symbols_count > max_symbols then if symbols_count > max_symbols then
s = nil s = nil
doc.disable_symbols = true doc.disable_symbols = true
local filename_message
if doc.filename then
filename_message = "document " .. doc.filename
else
filename_message = "unnamed document"
end
core.status_view:show_message("!", style.accent, core.status_view:show_message("!", style.accent,
"Too many symbols in "..filename_message.. "Too many symbols in document "..doc.filename..
": stopping auto-complete for this document according to ".. ": stopping auto-complete for this document according to config.max_symbols.")
"config.plugins.autocomplete.max_symbols."
)
collectgarbage('collect') collectgarbage('collect')
return {} return {}
end end
@ -202,7 +132,6 @@ core.add_thread(function()
for _, doc in ipairs(core.docs) do for _, doc in ipairs(core.docs) do
if not cache_is_valid(doc) then if not cache_is_valid(doc) then
valid = false valid = false
break
end end
end end
end end
@ -212,14 +141,12 @@ end)
local partial = "" local partial = ""
local suggestions_offset = 1
local suggestions_idx = 1 local suggestions_idx = 1
local suggestions = {} local suggestions = {}
local last_line, last_col local last_line, last_col
local function reset_suggestions() local function reset_suggestions()
suggestions_offset = 1
suggestions_idx = 1 suggestions_idx = 1
suggestions = {} suggestions = {}
@ -232,6 +159,16 @@ local function reset_suggestions()
end 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 ""
@ -262,8 +199,6 @@ local function update_suggestions()
j = j + 1 j = j + 1
end end
end end
suggestions_idx = 1
suggestions_offset = 1
end end
local function get_partial_symbol() local function get_partial_symbol()
@ -274,161 +209,67 @@ local function get_partial_symbol()
end end
local function get_active_view() local function get_active_view()
if core.active_view:is(DocView) then if getmetatable(core.active_view) == DocView then
return core.active_view return core.active_view
end end
end end
local last_max_width = 0
local function get_suggestions_rect(av) local function get_suggestions_rect(av)
if #suggestions == 0 then if #suggestions == 0 then
last_max_width = 0
return 0, 0, 0, 0 return 0, 0, 0, 0
end end
local line, col = av.doc:get_selection() local line, col = av.doc:get_selection()
local x, y = av:get_line_screen_position(line, col - #partial) local x, y = av:get_line_screen_position(line)
x = x + av:get_col_x_offset(line, col - #partial)
y = y + av:get_line_height() + style.padding.y y = y + av:get_line_height() + style.padding.y
local font = av:get_font() local font = av:get_font()
local th = font:get_height() local th = font:get_height()
local ah = config.plugins.autocomplete.max_height
local max_items = math.min(ah, #suggestions)
local show_count = math.min(#suggestions, ah)
local start_index = math.max(suggestions_idx-(ah-1), 1)
local max_width = 0 local max_width = 0
for i = start_index, start_index + show_count - 1 do for _, s in ipairs(suggestions) do
local s = suggestions[i]
local w = font:get_width(s.text) local w = font:get_width(s.text)
if s.info then if s.info then
w = w + style.font:get_width(s.info) + style.padding.x w = w + style.font:get_width(s.info) + style.padding.x
end end
max_width = math.max(max_width, w) max_width = math.max(max_width, w)
end end
max_width = math.max(last_max_width, max_width)
last_max_width = max_width
max_width = max_width + style.padding.x * 2 local ah = config.plugins.autocomplete.max_height
x = x - style.padding.x
local max_items = #suggestions
if max_items > ah then
max_items = ah
end
-- additional line to display total items -- additional line to display total items
max_items = max_items + 1 max_items = max_items + 1
if max_width > core.root_view.size.x then if max_width < 150 then
max_width = core.root_view.size.x max_width = 150
end
if max_width < 150 * SCALE then
max_width = 150 * SCALE
end
-- if portion not visiable to right, reposition to DocView right margin
if x + max_width > core.root_view.size.x then
x = (av.size.x + av.position.x) - max_width
end end
return return
x, x - style.padding.x,
y - style.padding.y, y - style.padding.y,
max_width, max_width + style.padding.x * 2,
max_items * (th + style.padding.y) + style.padding.y max_items * (th + style.padding.y) + style.padding.y
end end
local function wrap_line(line, max_chars)
if #line > max_chars then
local lines = {}
local line_len = #line
local new_line = ""
local prev_char = ""
local position = 0
local indent = line:match("^%s+")
for char in line:gmatch(".") do
position = position + 1
if #new_line < max_chars then
new_line = new_line .. char
prev_char = char
if position >= line_len then
table.insert(lines, new_line)
end
else
if
not prev_char:match("%s")
and
not string.sub(line, position+1, 1):match("%s")
and
position < line_len
then
new_line = new_line .. "-"
end
table.insert(lines, new_line)
if indent then
new_line = indent .. char
else
new_line = char
end
end
end
return lines
end
return line
end
local previous_scale = SCALE
local desc_font = style.code_font:copy(
config.plugins.autocomplete.desc_font_size * SCALE
)
local function draw_description_box(text, av, sx, sy, sw, sh) local function draw_description_box(text, av, sx, sy, sw, sh)
if previous_scale ~= SCALE then
desc_font = style.code_font:copy(
config.plugins.autocomplete.desc_font_size * SCALE
)
previous_scale = SCALE
end
local font = desc_font
local lh = font:get_height()
local y = sy + style.padding.y
local x = sx + sw + style.padding.x / 4
local width = 0 local width = 0
local char_width = font:get_width(" ")
local draw_left = false;
local max_chars = 0
if sx - av.position.x < av.size.x - (sx - av.position.x) - sw then
max_chars = (((av.size.x+av.position.x) - x) / char_width) - 5
else
draw_left = true;
max_chars = (
(sx - av.position.x - (style.padding.x / 4) - style.scrollbar_size)
/ char_width
) - 5
end
local lines = {} local lines = {}
for line in string.gmatch(text.."\n", "(.-)\n") do for line in string.gmatch(text.."\n", "(.-)\n") do
local wrapper_lines = wrap_line(line, max_chars) width = math.max(width, style.font:get_width(line))
if type(wrapper_lines) == "table" then table.insert(lines, line)
for _, wrapped_line in pairs(wrapper_lines) do
width = math.max(width, font:get_width(wrapped_line))
table.insert(lines, wrapped_line)
end
else
width = math.max(width, font:get_width(line))
table.insert(lines, line)
end
end end
if draw_left then local height = #lines * style.font:get_height()
x = sx - (style.padding.x / 4) - width - (style.padding.x * 2)
end
local height = #lines * font:get_height()
-- draw background rect -- draw background rect
renderer.draw_rect( renderer.draw_rect(
x, sx + sw + style.padding.x / 4,
sy, sy,
width + style.padding.x * 2, width + style.padding.x * 2,
height + style.padding.y * 2, height + style.padding.y * 2,
@ -436,10 +277,13 @@ local function draw_description_box(text, av, sx, sy, sw, sh)
) )
-- draw text -- 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 for _, line in pairs(lines) do
common.draw_text( common.draw_text(
font, style.text, line, "left", style.font, style.text, line, "left", x + style.padding.x, y, width, lh
x + style.padding.x, y, width, lh
) )
y = y + lh y = y + lh
end end
@ -460,38 +304,26 @@ 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
local show_count = math.min(#suggestions, ah) local show_count = #suggestions <= ah and #suggestions or ah
local start_index = suggestions_offset local start_index = suggestions_idx > ah and (suggestions_idx-(ah-1)) or 1
for i=start_index, start_index+show_count-1, 1 do for i=start_index, start_index+show_count-1, 1 do
if not suggestions[i] then if not suggestions[i] then
break break
end end
local s = suggestions[i] local s = suggestions[i]
local info_size = s.info and (style.font:get_width(s.info) + style.padding.x) or style.padding.x
local color = (i == suggestions_idx) and style.accent or style.text local color = (i == suggestions_idx) and style.accent or style.text
-- Push clip to avoid that the suggestion text gets drawn over suggestion type/icon common.draw_text(font, color, s.text, "left", rx + style.padding.x, y, rw, lh)
core.push_clip_rect(rx + style.padding.x, y, rw - info_size - style.padding.x, lh)
local x_adv = common.draw_text(font, color, s.text, "left", rx + style.padding.x, y, rw, lh)
core.pop_clip_rect()
-- If the text wasn't fully visible, draw an ellipsis
if x_adv > rx + rw - info_size then
local ellipsis_size = font:get_width("")
local ell_x = rx + rw - info_size - ellipsis_size
renderer.draw_rect(ell_x, y, ellipsis_size, lh, style.background3)
common.draw_text(font, color, "", "left", ell_x, y, ellipsis_size, lh)
end
if s.info then if s.info then
color = (i == suggestions_idx) and style.text or style.dim color = (i == suggestions_idx) and style.text or style.dim
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 suggestions_idx == i then
if s.onhover then if s.cb then
s.onhover(suggestions_idx, s) s.cb(suggestions_idx, s)
s.onhover = nil s.cb = nil
s.data = nil
end end
if s.desc and #s.desc > 0 then if s.desc and #s.desc > 0 then
draw_description_box(s.desc, av, rx, ry, rw, rh) draw_description_box(s.desc, av, rx, ry, rw, rh)
@ -615,11 +447,8 @@ function autocomplete.open(on_close)
end end
local av = get_active_view() local av = get_active_view()
if av then last_line, last_col = av.doc:get_selection()
partial = get_partial_symbol() update_suggestions()
last_line, last_col = av.doc:get_selection()
update_suggestions()
end
end end
function autocomplete.close() function autocomplete.close()
@ -651,62 +480,26 @@ end
-- Commands -- Commands
-- --
local function predicate() local function predicate()
local active_docview = get_active_view() return get_active_view() and #suggestions > 0
return active_docview and #suggestions > 0, active_docview
end end
command.add(predicate, { command.add(predicate, {
["autocomplete:complete"] = function(dv) ["autocomplete:complete"] = function()
local doc = dv.doc local doc = core.active_view.doc
local item = suggestions[suggestions_idx] local line, col = doc:get_selection()
local text = item.text local text = suggestions[suggestions_idx].text
local inserted = false doc:insert(line, col, text)
if item.onselect then doc:remove(line, col, line, col - #partial)
inserted = item.onselect(suggestions_idx, item) doc:set_selection(line, col + #text - #partial)
end
if not inserted then
local current_partial = get_partial_symbol()
local sz = #current_partial
for _, line1, col1, line2, _ in doc:get_selections(true) do
local n = col1 - 1
local line = doc.lines[line1]
for i = 1, sz + 1 do
local j = sz - i
local subline = line:sub(n - j, n)
local subpartial = current_partial:sub(i, -1)
if subpartial == subline then
doc:remove(line1, col1, line2, n - j)
break
end
end
end
doc:text_input(item.text)
end
reset_suggestions() reset_suggestions()
end, end,
["autocomplete:previous"] = function() ["autocomplete:previous"] = function()
suggestions_idx = (suggestions_idx - 2) % #suggestions + 1 suggestions_idx = math.max(suggestions_idx - 1, 1)
local ah = math.min(config.plugins.autocomplete.max_height, #suggestions)
if suggestions_offset > suggestions_idx then
suggestions_offset = suggestions_idx
elseif suggestions_offset + ah < suggestions_idx + 1 then
suggestions_offset = suggestions_idx - ah + 1
end
end, end,
["autocomplete:next"] = function() ["autocomplete:next"] = function()
suggestions_idx = (suggestions_idx % #suggestions) + 1 suggestions_idx = math.min(suggestions_idx + 1, #suggestions)
local ah = math.min(config.plugins.autocomplete.max_height, #suggestions)
if suggestions_offset + ah < suggestions_idx + 1 then
suggestions_offset = suggestions_idx - ah + 1
elseif suggestions_offset > suggestions_idx then
suggestions_offset = suggestions_idx
end
end, end,
["autocomplete:cycle"] = function() ["autocomplete:cycle"] = function()

View File

@ -1,110 +1,50 @@
-- mod-version:3 -- 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 style = require "core.style"
local Doc = require "core.doc" local Doc = require "core.doc"
local Node = require "core.node"
local common = require "core.common"
local dirwatch = require "core.dirwatch"
config.plugins.autoreload = common.merge({
always_show_nagview = false,
config_spec = {
name = "Autoreload",
{
label = "Always Show Nagview",
description = "Alerts you if an opened file changes externally even if you haven't modified it.",
path = "always_show_nagview",
type = "toggle",
default = false
}
}
}, config.plugins.autoreload)
local watch = dirwatch.new()
local times = setmetatable({}, { __mode = "k" }) local times = setmetatable({}, { __mode = "k" })
local visible = setmetatable({}, { __mode = "k" })
local function get_project_doc_watch(doc)
for i, v in ipairs(core.project_directories) do
if doc.abs_filename:find(v.name, 1, true) == 1 then return v.watch end
end
return watch
end
local function update_time(doc) local function update_time(doc)
times[doc] = system.get_file_info(doc.filename).modified local info = system.get_file_info(doc.filename)
times[doc] = info.modified
end end
local function reload_doc(doc) local function reload_doc(doc)
doc:reload() local fp = io.open(doc.filename, "r")
local text = fp:read("*a")
fp:close()
local sel = { doc:get_selection() }
doc:remove(1, 1, math.huge, math.huge)
doc:insert(1, 1, text:gsub("\r", ""):gsub("\n$", ""))
doc:set_selection(table.unpack(sel))
update_time(doc) update_time(doc)
core.redraw = true doc:clean()
core.log_quiet("Auto-reloaded doc \"%s\"", doc.filename) core.log_quiet("Auto-reloaded doc \"%s\"", doc.filename)
end end
local function check_prompt_reload(doc)
if doc and doc.deferred_reload then
core.nag_view:show("File Changed", doc.filename .. " has changed. Reload this file?", {
{ text = "Yes", default_yes = true },
{ text = "No", default_no = true }
}, function(item)
if item.text == "Yes" then reload_doc(doc) end
doc.deferred_reload = false
end)
end
end
local function doc_changes_visiblity(doc, visibility)
if doc and visible[doc] ~= visibility and doc.abs_filename then
visible[doc] = visibility
if visibility then check_prompt_reload(doc) end
get_project_doc_watch(doc):watch(doc.abs_filename, visibility)
end
end
local on_check = dirwatch.check
function dirwatch:check(change_callback, ...)
on_check(self, function(dir)
for _, doc in ipairs(core.docs) do
if doc.abs_filename and (dir == common.dirname(doc.abs_filename) or dir == doc.abs_filename) then
local info = system.get_file_info(doc.filename or "")
if info and info.type == "file" and times[doc] ~= info.modified then
if not doc:is_dirty() and not config.plugins.autoreload.always_show_nagview then
reload_doc(doc)
else
doc.deferred_reload = true
if doc == core.active_view.doc then check_prompt_reload(doc) end
end
end
end
end
change_callback(dir)
end, ...)
end
local core_set_active_view = core.set_active_view
function core.set_active_view(view)
core_set_active_view(view)
doc_changes_visiblity(view.doc, true)
end
local node_set_active_view = Node.set_active_view
function Node:set_active_view(view)
if self.active_view then doc_changes_visiblity(self.active_view.doc, false) end
node_set_active_view(self, view)
doc_changes_visiblity(self.active_view.doc, true)
end
core.add_thread(function() core.add_thread(function()
while true do while true do
-- because we already hook this function above; we only -- check all doc modified times
-- need to check the file. for _, doc in ipairs(core.docs) do
watch:check(function() end) local info = system.get_file_info(doc.filename or "")
coroutine.yield(0.05) if info and times[doc] ~= info.modified then
reload_doc(doc)
end
coroutine.yield()
end
-- wait for next scan
coroutine.yield(config.project_scan_rate)
end end
end) end)
-- patch `Doc.save|load` to store modified time -- patch `Doc.save|load` to store modified time
local load = Doc.load local load = Doc.load
local save = Doc.save local save = Doc.save
@ -117,8 +57,6 @@ end
Doc.save = function(self, ...) Doc.save = function(self, ...)
local res = save(self, ...) local res = save(self, ...)
-- if starting with an unsaved document with a filename.
if not times[self] then get_project_doc_watch(self):watch(self.abs_filename, true) end
update_time(self) update_time(self)
return res return res
end end

View File

@ -1,10 +1,9 @@
-- mod-version:3 -- 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"
local ContextMenu = require "core.contextmenu" local ContextMenu = require "core.contextmenu"
local RootView = require "core.rootview" local RootView = require "core.rootview"
local config = require "core.config"
local menu = ContextMenu() local menu = ContextMenu()
local on_view_mouse_pressed = RootView.on_view_mouse_pressed local on_view_mouse_pressed = RootView.on_view_mouse_pressed
@ -33,9 +32,9 @@ function RootView:draw(...)
menu:draw() menu:draw()
end end
command.add("core.docview!", { command.add(nil, {
["context:show"] = function(dv) ["context:show"] = function()
menu:show(dv.position.x, dv.position.y) menu:show(core.active_view.position.x, core.active_view.position.y)
end end
}) })
@ -43,43 +42,36 @@ keymap.add {
["menu"] = "context:show" ["menu"] = "context:show"
} }
command.add(function() return menu.show_context_menu == true end, { local function copy_log()
["context:focus-previous"] = function() local item = core.active_view.hovered_item
menu:focus_previous() if item then
end, system.set_clipboard(core.get_log(item))
["context:focus-next"] = function() end
menu:focus_next()
end,
["context:hide"] = function()
menu:hide()
end,
["context:on-selected"] = function()
menu:call_selected_item()
end,
})
keymap.add { ["return"] = "context:on-selected" }
keymap.add { ["up"] = "context:focus-previous" }
keymap.add { ["down"] = "context:focus-next" }
keymap.add { ["escape"] = "context:hide" }
local cmds = {
{ text = "Cut", command = "doc:cut" },
{ text = "Copy", command = "doc:copy" },
{ text = "Paste", command = "doc:paste" },
ContextMenu.DIVIDER,
{ text = "Find", command = "find-replace:find" },
{ text = "Replace", command = "find-replace:replace" }
}
if config.plugins.scale ~= false and require("plugins.scale") then
table.move(cmds, 4, 6, 7)
cmds[4] = { text = "Font +", command = "scale:increase" }
cmds[5] = { text = "Font -", command = "scale:decrease" }
cmds[6] = { text = "Font Reset", command = "scale:reset" }
end end
menu:register("core.docview", cmds) local function open_as_doc()
local doc = core.open_doc("logs.txt")
core.root_view:open_doc(doc)
doc:insert(1, 1, core.get_log())
end
menu:register("core.logview", {
{ text = "Copy entry", command = copy_log },
{ text = "Open as file", command = open_as_doc }
})
if require("plugins.scale") then
menu:register("core.docview", {
{ text = "Font +", command = "scale:increase" },
{ text = "Font -", command = "scale:decrease" },
{ text = "Font Reset", command = "scale:reset" },
ContextMenu.DIVIDER,
{ text = "Find", command = "find-replace:find" },
{ text = "Replace", command = "find-replace:replace" },
ContextMenu.DIVIDER,
{ text = "Find Pattern", command = "find-replace:find-pattern" },
{ text = "Replace Pattern", command = "find-replace:replace-pattern" },
})
end
return menu return menu

View File

@ -1,258 +1,95 @@
-- mod-version:3 -- 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"
local config = require "core.config" local config = require "core.config"
local core_syntax = require "core.syntax"
local DocView = require "core.docview" local DocView = require "core.docview"
local Doc = require "core.doc" local Doc = require "core.doc"
local tokenizer = require "core.tokenizer"
local cache = setmetatable({}, { __mode = "k" }) local cache = setmetatable({}, { __mode = "k" })
local comments_cache = {}
local auto_detect_max_lines = 150
local function indent_occurrences_more_than_once(stat, idx) local function add_to_stat(stat, val)
if stat[idx-1] and stat[idx-1] == stat[idx] then for i = 1, #stat do
return true if val == stat[i][1] then
elseif stat[idx+1] and stat[idx+1] == stat[idx] then stat[i][2] = stat[i][2] + 1
return true return
end
end end
return false stat[#stat + 1] = {val, 1}
end end
local function optimal_indent_from_stat(stat) local function optimal_indent_from_stat(stat)
if #stat == 0 then return nil, 0 end if #stat == 0 then return nil, 0 end
table.sort(stat, function(a, b) return a > b end) local bins = {}
local best_indent = 0 for k = 1, #stat do
local best_score = 0 local indent = stat[k][1]
local count = #stat
for x=1, count do
local indent = stat[x]
local score = 0 local score = 0
for y=1, count do local mult_prev, lines_prev
if y ~= x and stat[y] % indent == 0 then for i = k, #stat do
score = score + 1 if stat[i][1] % indent == 0 then
elseif local mult = stat[i][1] / indent
indent > stat[y] if not mult_prev or (mult_prev + 1 == mult and lines_prev / stat[i][2] > 0.1) then
and -- we add the number of lines to the score only if the previous
indent_occurrences_more_than_once(stat, y) -- multiple of "indent" was populated with enough lines.
then score = score + stat[i][2]
score = 0 end
break mult_prev, lines_prev = mult, stat[i][2]
end end
end end
if score > best_score then bins[#bins + 1] = {indent, score}
best_indent = indent
best_score = score
end
if score > 0 then
break
end
end end
return best_score > 0 and best_indent or nil, best_score table.sort(bins, function(a, b) return a[2] > b[2] end)
return bins[1][1], bins[1][2]
end end
local function escape_comment_tokens(token) -- return nil if it is a comment or blank line or the initial part of the
local special_chars = "*-%[].()+?^$" -- line otherwise.
local escaped = "" -- we don't need to have the whole line to detect indentation.
for x=1, token:len() do local function get_first_line_part(tokens)
local found = false local i, n = 1, #tokens
for y=1, special_chars:len() do while i + 1 <= n do
if token:sub(x, x) == special_chars:sub(y, y) then local ttype, ttext = tokens[i], tokens[i + 1]
escaped = escaped .. "%" .. token:sub(x, x) if ttype ~= "comment" and ttext:gsub("%s+", "") ~= "" then
found = true return ttext
break
end
end
if not found then
escaped = escaped .. token:sub(x, x)
end end
i = i + 2
end end
return escaped
end end
local function get_comment_patterns(syntax, _loop)
_loop = _loop or 1
if _loop > 5 then return end
if comments_cache[syntax] then
if #comments_cache[syntax] > 0 then
return comments_cache[syntax]
else
return nil
end
end
local comments = {}
for idx=1, #syntax.patterns do
local pattern = syntax.patterns[idx]
local startp = ""
if
type(pattern.type) == "string"
and
(pattern.type == "comment" or pattern.type == "string")
then
local not_is_string = pattern.type ~= "string"
if pattern.pattern then
startp = type(pattern.pattern) == "table"
and pattern.pattern[1] or pattern.pattern
if not_is_string and startp:sub(1, 1) ~= "^" then
startp = "^%s*" .. startp
elseif not_is_string then
startp = "^%s*" .. startp:sub(2, startp:len())
end
if type(pattern.pattern) == "table" then
table.insert(comments, {"p", startp, pattern.pattern[2]})
elseif not_is_string then
table.insert(comments, {"p", startp})
end
elseif pattern.regex then
startp = type(pattern.regex) == "table"
and pattern.regex[1] or pattern.regex
if not_is_string and startp:sub(1, 1) ~= "^" then
startp = "^\\s*" .. startp
elseif not_is_string then
startp = "^\\s*" .. startp:sub(2, startp:len())
end
if type(pattern.regex) == "table" then
table.insert(comments, {
"r", regex.compile(startp), regex.compile(pattern.regex[2])
})
elseif not_is_string then
table.insert(comments, {"r", regex.compile(startp)})
end
end
elseif pattern.syntax then
local subsyntax = type(pattern.syntax) == 'table' and pattern.syntax
or core_syntax.get("file"..pattern.syntax, "")
local sub_comments = get_comment_patterns(subsyntax, _loop + 1)
if sub_comments then
for s=1, #sub_comments do
table.insert(comments, sub_comments[s])
end
end
end
end
if #comments == 0 then
local single_line_comment = syntax.comment
and escape_comment_tokens(syntax.comment) or nil
local block_comment = nil
if syntax.block_comment then
block_comment = {
escape_comment_tokens(syntax.block_comment[1]),
escape_comment_tokens(syntax.block_comment[2])
}
end
if single_line_comment then
table.insert(comments, {"p", "^%s*" .. single_line_comment})
end
if block_comment then
table.insert(comments, {"p", "^%s*" .. block_comment[1], block_comment[2]})
end
end
comments_cache[syntax] = comments
if #comments > 0 then
return comments
end
return nil
end
local function get_non_empty_lines(syntax, lines) local function get_non_empty_lines(syntax, lines)
return coroutine.wrap(function() return coroutine.wrap(function()
local comments = get_comment_patterns(syntax) local tokens, state
local i = 0 local i = 0
local end_regex = nil
local end_pattern = nil
local inside_comment = false
for _, line in ipairs(lines) do for _, line in ipairs(lines) do
if line:gsub("^%s+", "") ~= "" then tokens, state = tokenizer.tokenize(syntax, line, state)
local is_comment = false local line_start = get_first_line_part(tokens)
if comments then if line_start then
if not inside_comment then i = i + 1
for c=1, #comments do coroutine.yield(i, line_start)
local comment = comments[c]
if comment[1] == "p" then
if comment[3] then
local start, ending = line:find(comment[2])
if start then
if not line:find(comment[3], ending+1) then
is_comment = true
inside_comment = true
end_pattern = comment[3]
end
break
end
elseif line:find(comment[2]) then
is_comment = true
break
end
else
if comment[3] then
local start, ending = regex.find_offsets(
comment[2], line, 1, regex.ANCHORED
)
if start then
if not regex.find_offsets(
comment[3], line, ending+1, regex.ANCHORED
)
then
is_comment = true
inside_comment = true
end_regex = comment[3]
end
break
end
elseif regex.find_offsets(comment[2], line, 1, regex.ANCHORED) then
is_comment = true
break
end
end
end
elseif end_pattern and line:find(end_pattern) then
is_comment = true
inside_comment = false
end_pattern = nil
elseif end_regex and regex.find_offsets(end_regex, line) then
is_comment = true
inside_comment = false
end_regex = nil
end
end
if
not is_comment
and
not inside_comment
then
i = i + 1
coroutine.yield(i, line)
end
end end
end end
end) end)
end end
local auto_detect_max_lines = 100
local function detect_indent_stat(doc) local function detect_indent_stat(doc)
local stat = {} local stat = {}
local tab_count = 0 local tab_count = 0
local runs = 1
local max_lines = auto_detect_max_lines
for i, text in get_non_empty_lines(doc.syntax, doc.lines) do for i, text in get_non_empty_lines(doc.syntax, doc.lines) do
local spaces = text:match("^ +") local str = text:match("^ %s+%S")
if spaces and #spaces > 1 then table.insert(stat, #spaces) end if str then add_to_stat(stat, #str - 1) end
local tabs = text:match("^\t+") local str = text:match("^\t+")
if tabs then tab_count = tab_count + 1 end if str then tab_count = tab_count + 1 end
-- if nothing found for first lines try at least 4 more times
if i == max_lines and runs < 5 and #stat == 0 and tab_count == 0 then
max_lines = max_lines + auto_detect_max_lines
runs = runs + 1
-- Stop parsing when files is very long. Not needed for euristic determination. -- Stop parsing when files is very long. Not needed for euristic determination.
elseif i > max_lines then break end if i > auto_detect_max_lines then break end
end end
table.sort(stat, function(a, b) return a[1] < b[1] end)
local indent, score = optimal_indent_from_stat(stat) local indent, score = optimal_indent_from_stat(stat)
if tab_count > score then if tab_count > score then
return "hard", config.indent_size, tab_count return "hard", config.indent_size, tab_count
@ -264,7 +101,7 @@ 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 = 2 local score_threshold = 4
if score < score_threshold then if score < score_threshold then
-- use default values -- use default values
type = config.tab_type type = config.tab_type
@ -284,63 +121,86 @@ end
local clean = Doc.clean local clean = Doc.clean
function Doc:clean(...) function Doc:clean(...)
clean(self, ...) clean(self, ...)
local _, _, confirmed = self:get_indent_info() if not cache[self].confirmed then
if not confirmed then
update_cache(self) update_cache(self)
end end
end end
local function with_indent_override(doc, fn, ...)
local c = cache[doc]
if not c then
return fn(...)
end
local type, size = config.tab_type, config.indent_size
config.tab_type, config.indent_size = c.type, c.size or config.indent_size
local r1, r2, r3 = fn(...)
config.tab_type, config.indent_size = type, size
return r1, r2, r3
end
local perform = command.perform
function command.perform(...)
return with_indent_override(core.active_view.doc, perform, ...)
end
local draw = DocView.draw
function DocView:draw(...)
return with_indent_override(self.doc, draw, self, ...)
end
local function set_indent_type(doc, type) local function set_indent_type(doc, type)
local _, indent_size = doc:get_indent_info() cache[doc] = {type = type,
cache[doc] = { size = cache[doc].value or config.indent_size,
type = type, confirmed = true}
size = indent_size,
confirmed = true
}
doc.indent_info = cache[doc] doc.indent_info = cache[doc]
end end
local function set_indent_type_command(dv) local function set_indent_type_command()
core.command_view:enter("Specify indent style for this file", { core.command_view:enter(
submit = function(value) "Specify indent style for this file",
local doc = dv.doc function(value) -- submit
local doc = core.active_view.doc
value = value:lower() value = value:lower()
set_indent_type(doc, value == "tabs" and "hard" or "soft") set_indent_type(doc, value == "tabs" and "hard" or "soft")
end, end,
suggest = function(text) function(text) -- suggest
return common.fuzzy_match({"tabs", "spaces"}, text) return common.fuzzy_match({"tabs", "spaces"}, text)
end, end,
validate = function(text) nil, -- cancel
function(text) -- validate
local t = text:lower() local t = text:lower()
return t == "tabs" or t == "spaces" return t == "tabs" or t == "spaces"
end end
}) )
end end
local function set_indent_size(doc, size) local function set_indent_size(doc, size)
local indent_type = doc:get_indent_info() cache[doc] = {type = cache[doc].type or config.tab_type,
cache[doc] = { size = size,
type = indent_type, confirmed = true}
size = size,
confirmed = true
}
doc.indent_info = cache[doc] doc.indent_info = cache[doc]
end end
local function set_indent_size_command(dv) local function set_indent_size_command()
core.command_view:enter("Specify indent size for current file", { core.command_view:enter(
submit = function(value) "Specify indent size for current file",
value = math.floor(tonumber(value)) function(value) -- submit
local doc = dv.doc local value = math.floor(tonumber(value))
local doc = core.active_view.doc
set_indent_size(doc, value) set_indent_size(doc, value)
end, end,
validate = function(value) nil, -- suggest
value = tonumber(value) nil, -- cancel
function(value) -- validate
local value = tonumber(value)
return value ~= nil and value >= 1 return value ~= nil and value >= 1
end end
}) )
end end
@ -349,24 +209,20 @@ command.add("core.docview", {
["indent:set-file-indent-size"] = set_indent_size_command ["indent:set-file-indent-size"] = set_indent_size_command
}) })
command.add(
function() command.add(function()
return core.active_view:is(DocView) return core.active_view:is(DocView)
and cache[core.active_view.doc] and cache[core.active_view.doc]
and cache[core.active_view.doc].type == "soft" and cache[core.active_view.doc].type == "soft"
end, { end, {
["indent:switch-file-to-tabs-indentation"] = function() ["indent:switch-file-to-tabs-indentation"] = function() set_indent_type(core.active_view.doc, "hard") end
set_indent_type(core.active_view.doc, "hard")
end
}) })
command.add(
function() command.add(function()
return core.active_view:is(DocView) return core.active_view:is(DocView)
and cache[core.active_view.doc] and cache[core.active_view.doc]
and cache[core.active_view.doc].type == "hard" and cache[core.active_view.doc].type == "hard"
end, { end, {
["indent:switch-file-to-spaces-indentation"] = function() ["indent:switch-file-to-spaces-indentation"] = function() set_indent_type(core.active_view.doc, "soft") end
set_indent_type(core.active_view.doc, "soft")
end
}) })

View File

@ -1,321 +0,0 @@
-- mod-version:3
local style = require "core.style"
local DocView = require "core.docview"
local common = require "core.common"
local command = require "core.command"
local config = require "core.config"
local Highlighter = require "core.doc.highlighter"
config.plugins.drawwhitespace = common.merge({
enabled = false,
show_leading = true,
show_trailing = true,
show_middle = true,
show_middle_min = 1,
color = style.syntax.whitespace or style.syntax.comment,
leading_color = nil,
middle_color = nil,
trailing_color = nil,
substitutions = {
{
char = " ",
sub = "·",
-- You can put any of the previous options here too.
-- For example:
-- show_middle_min = 2,
-- show_leading = false,
},
{
char = "\t",
sub = "»",
},
},
config_spec = {
name = "Draw Whitespace",
{
label = "Enabled",
description = "Disable or enable the drawing of white spaces.",
path = "enabled",
type = "toggle",
default = false
},
{
label = "Show Leading",
description = "Draw whitespaces starting at the beginning of a line.",
path = "show_leading",
type = "toggle",
default = true,
},
{
label = "Show Middle",
description = "Draw whitespaces on the middle of a line.",
path = "show_middle",
type = "toggle",
default = true,
},
{
label = "Show Trailing",
description = "Draw whitespaces on the end of a line.",
path = "show_trailing",
type = "toggle",
default = true,
},
{
label = "Show Trailing as Error",
description = "Uses an error square to spot them easily, requires 'Show Trailing' enabled.",
path = "show_trailing_error",
type = "toggle",
default = false,
on_apply = function(enabled)
local found = nil
local substitutions = config.plugins.drawwhitespace.substitutions
for i, sub in ipairs(substitutions) do
if sub.trailing_error then
found = i
end
end
if found == nil and enabled then
table.insert(substitutions, {
char = " ",
sub = "",
show_leading = false,
show_middle = false,
show_trailing = true,
trailing_color = style.error,
trailing_error = true
})
elseif found ~= nil and not enabled then
table.remove(substitutions, found)
end
end
}
}
}, config.plugins.drawwhitespace)
local ws_cache
local cached_settings
local function reset_cache()
ws_cache = setmetatable({}, { __mode = "k" })
local settings = config.plugins.drawwhitespace
cached_settings = {
show_leading = settings.show_leading,
show_trailing = settings.show_trailing,
show_middle = settings.show_middle,
show_middle_min = settings.show_middle_min,
color = settings.color,
leading_color = settings.leading_color,
middle_color = settings.middle_color,
trailing_color = settings.trailing_color,
substitutions = settings.substitutions,
}
end
reset_cache()
local function reset_cache_if_needed()
local settings = config.plugins.drawwhitespace
if
not ws_cache or
cached_settings.show_leading ~= settings.show_leading
or cached_settings.show_trailing ~= settings.show_trailing
or cached_settings.show_middle ~= settings.show_middle
or cached_settings.show_middle_min ~= settings.show_middle_min
or cached_settings.color ~= settings.color
or cached_settings.leading_color ~= settings.leading_color
or cached_settings.middle_color ~= settings.middle_color
or cached_settings.trailing_color ~= settings.trailing_color
-- we assume that the entire table changes
or cached_settings.substitutions ~= settings.substitutions
then
reset_cache()
end
end
-- Move cache to make space for new lines
local prev_insert_notify = Highlighter.insert_notify
function Highlighter:insert_notify(line, n, ...)
prev_insert_notify(self, line, n, ...)
if not ws_cache[self] then
ws_cache[self] = {}
end
local to = math.min(line + n, #self.doc.lines)
for i=#self.doc.lines+n,to,-1 do
ws_cache[self][i] = ws_cache[self][i - n]
end
for i=line,to do
ws_cache[self][i] = nil
end
end
-- Close the cache gap created by removed lines
local prev_remove_notify = Highlighter.remove_notify
function Highlighter:remove_notify(line, n, ...)
prev_remove_notify(self, line, n, ...)
if not ws_cache[self] then
ws_cache[self] = {}
end
local to = math.max(line + n, #self.doc.lines)
for i=line,to do
ws_cache[self][i] = ws_cache[self][i + n]
end
end
-- Remove changed lines from the cache
local prev_update_notify = Highlighter.update_notify
function Highlighter:update_notify(line, n, ...)
prev_update_notify(self, line, n, ...)
if not ws_cache[self] then
ws_cache[self] = {}
end
for i=line,line+n do
ws_cache[self][i] = nil
end
end
local function get_option(substitution, option)
if substitution[option] == nil then
return config.plugins.drawwhitespace[option]
end
return substitution[option]
end
local draw_line_text = DocView.draw_line_text
function DocView:draw_line_text(idx, x, y)
if
not config.plugins.drawwhitespace.enabled
or
getmetatable(self) ~= DocView
then
return draw_line_text(self, idx, x, y)
end
local font = (self:get_font() or style.syntax_fonts["whitespace"] or style.syntax_fonts["comment"])
local font_size = font:get_size()
local _, indent_size = self.doc:get_indent_info()
reset_cache_if_needed()
if
not ws_cache[self.doc.highlighter]
or ws_cache[self.doc.highlighter].font ~= font
or ws_cache[self.doc.highlighter].font_size ~= font_size
or ws_cache[self.doc.highlighter].indent_size ~= indent_size
then
ws_cache[self.doc.highlighter] =
setmetatable(
{ font = font, font_size = font_size, indent_size = indent_size },
{ __mode = "k" }
)
end
if not ws_cache[self.doc.highlighter][idx] then -- need to cache line
local cache = {}
local tx
local text = self.doc.lines[idx]
for _, substitution in pairs(config.plugins.drawwhitespace.substitutions) do
local char = substitution.char
local sub = substitution.sub
local offset = 1
local show_leading = get_option(substitution, "show_leading")
local show_middle = get_option(substitution, "show_middle")
local show_trailing = get_option(substitution, "show_trailing")
local show_middle_min = get_option(substitution, "show_middle_min")
local base_color = get_option(substitution, "color")
local leading_color = get_option(substitution, "leading_color") or base_color
local middle_color = get_option(substitution, "middle_color") or base_color
local trailing_color = get_option(substitution, "trailing_color") or base_color
local pattern = char.."+"
while true do
local s, e = text:find(pattern, offset)
if not s then break end
tx = self:get_col_x_offset(idx, s)
local color = base_color
local draw = false
if e >= #text - 1 then
draw = show_trailing
color = trailing_color
elseif s == 1 then
draw = show_leading
color = leading_color
else
draw = show_middle and (e - s + 1 >= show_middle_min)
color = middle_color
end
if draw then
local last_cache_idx = #cache
-- We need to draw tabs one at a time because they might have a
-- different size than the substituting character.
-- This also applies to any other char if we use non-monospace fonts
-- but we ignore this case for now.
if char == "\t" then
for i = s,e do
tx = self:get_col_x_offset(idx, i)
cache[last_cache_idx + 1] = sub
cache[last_cache_idx + 2] = tx
cache[last_cache_idx + 3] = font:get_width(sub)
cache[last_cache_idx + 4] = color
last_cache_idx = last_cache_idx + 4
end
else
cache[last_cache_idx + 1] = string.rep(sub, e - s + 1)
cache[last_cache_idx + 2] = tx
cache[last_cache_idx + 3] = font:get_width(cache[last_cache_idx + 1])
cache[last_cache_idx + 4] = color
end
end
offset = e + 1
end
end
ws_cache[self.doc.highlighter][idx] = cache
end
-- draw from cache
local x1, _, x2, _ = self:get_content_bounds()
x1 = x1 + x
x2 = x2 + x
local ty = y + self:get_line_text_y_offset()
local cache = ws_cache[self.doc.highlighter][idx]
for i=1,#cache,4 do
local tx = cache[i + 1] + x
local tw = cache[i + 2]
if tx <= x2 then
local sub = cache[i]
local color = cache[i + 3]
if tx + tw >= x1 then
tx = renderer.draw_text(font, sub, tx, ty, color)
end
end
end
return draw_line_text(self, idx, x, y)
end
command.add(nil, {
["draw-whitespace:toggle"] = function()
config.plugins.drawwhitespace.enabled = not config.plugins.drawwhitespace.enabled
end,
["draw-whitespace:disable"] = function()
config.plugins.drawwhitespace.enabled = false
end,
["draw-whitespace:enable"] = function()
config.plugins.drawwhitespace.enabled = true
end,
})

View File

@ -1,13 +1,11 @@
-- mod-version:3 -- mod-version:2 -- lite-xl 2.0
local syntax = require "core.syntax" local syntax = require "core.syntax"
syntax.add { syntax.add {
name = "C", files = { "%.c$", "%.h$", "%.inl$" },
files = { "%.c$" },
comment = "//", comment = "//",
block_comment = { "/*", "*/" },
patterns = { patterns = {
{ pattern = "//.*", type = "comment" }, { pattern = "//.-\n", type = "comment" },
{ pattern = { "/%*", "%*/" }, type = "comment" }, { pattern = { "/%*", "%*/" }, type = "comment" },
{ pattern = { '"', '"', '\\' }, type = "string" }, { pattern = { '"', '"', '\\' }, type = "string" },
{ pattern = { "'", "'", '\\' }, type = "string" }, { pattern = { "'", "'", '\\' }, type = "string" },
@ -15,64 +13,12 @@ syntax.add {
{ pattern = "%d+[%d%.eE]*f?", type = "number" }, { pattern = "%d+[%d%.eE]*f?", type = "number" },
{ pattern = "%.?%d+f?", type = "number" }, { pattern = "%.?%d+f?", type = "number" },
{ pattern = "[%+%-=/%*%^%%<>!~|&]", type = "operator" }, { pattern = "[%+%-=/%*%^%%<>!~|&]", type = "operator" },
{ pattern = "##", type = "operator" },
{ pattern = "struct%s()[%a_][%w_]*", type = {"keyword", "keyword2"} }, { pattern = "struct%s()[%a_][%w_]*", type = {"keyword", "keyword2"} },
{ pattern = "union%s()[%a_][%w_]*", type = {"keyword", "keyword2"} }, { pattern = "union%s()[%a_][%w_]*", type = {"keyword", "keyword2"} },
-- static declarations
{ pattern = "static()%s+()inline",
type = { "keyword", "normal", "keyword" }
},
{ pattern = "static()%s+()const",
type = { "keyword", "normal", "keyword" }
},
{ pattern = "static()%s+()[%a_][%w_]*",
type = { "keyword", "normal", "literal" }
},
-- match function type declarations
{ pattern = "[%a_][%w_]*()%*+()%s+()[%a_][%w_]*%f[%(]",
type = { "literal", "operator", "normal", "function" }
},
{ pattern = "[%a_][%w_]*()%s+()%*+()[%a_][%w_]*%f[%(]",
type = { "literal", "normal", "operator", "function" }
},
{ pattern = "[%a_][%w_]*()%s+()[%a_][%w_]*%f[%(]",
type = { "literal", "normal", "function" }
},
-- match variable type declarations
{ pattern = "[%a_][%w_]*()%*+()%s+()[%a_][%w_]*",
type = { "literal", "operator", "normal", "normal" }
},
{ pattern = "[%a_][%w_]*()%s+()%*+()[%a_][%w_]*",
type = { "literal", "normal", "operator", "normal" }
},
{ pattern = "[%a_][%w_]*()%s+()[%a_][%w_]*()%s*()[;,%[%)]",
type = { "literal", "normal", "normal", "normal", "normal" }
},
{ pattern = "[%a_][%w_]*()%s+()[%a_][%w_]*()%s*()=",
type = { "literal", "normal", "normal", "normal", "operator" }
},
{ pattern = "[%a_][%w_]*()&()%s+()[%a_][%w_]*",
type = { "literal", "operator", "normal", "normal" }
},
{ pattern = "[%a_][%w_]*()%s+()&()[%a_][%w_]*",
type = { "literal", "normal", "operator", "normal" }
},
-- Uppercase constants of at least 2 chars in len
{ pattern = "_?%u[%u_][%u%d_]*%f[%s%+%*%-%.%)%]}%?%^%%=/<>~|&;:,!]",
type = "number"
},
-- Magic constants
{ pattern = "__[%u%l]+__", type = "number" },
-- all other functions
{ pattern = "[%a_][%w_]*%f[(]", type = "function" }, { pattern = "[%a_][%w_]*%f[(]", type = "function" },
-- Macros
{ pattern = "^%s*#%s*define%s+()[%a_][%a%d_]*",
type = { "keyword", "symbol" }
},
{ pattern = "#%s*include%s()<.->", type = {"keyword", "string"} },
{ pattern = "%f[#]#%s*[%a_][%w_]*", type = "keyword" },
-- Everything else to make the tokenizer work properly
{ pattern = "[%a_][%w_]*", type = "symbol" }, { pattern = "[%a_][%w_]*", type = "symbol" },
{ pattern = "#include%s()<.->", type = {"keyword", "string"} },
{ pattern = "#[%a_][%w_]*", type = "keyword" },
}, },
symbols = { symbols = {
["if"] = "keyword", ["if"] = "keyword",
@ -97,9 +43,7 @@ syntax.add {
["case"] = "keyword", ["case"] = "keyword",
["default"] = "keyword", ["default"] = "keyword",
["auto"] = "keyword", ["auto"] = "keyword",
["struct"] = "keyword", ["void"] = "keyword",
["union"] = "keyword",
["void"] = "keyword2",
["int"] = "keyword2", ["int"] = "keyword2",
["short"] = "keyword2", ["short"] = "keyword2",
["long"] = "keyword2", ["long"] = "keyword2",
@ -115,7 +59,6 @@ syntax.add {
["#if"] = "keyword", ["#if"] = "keyword",
["#ifdef"] = "keyword", ["#ifdef"] = "keyword",
["#ifndef"] = "keyword", ["#ifndef"] = "keyword",
["#elif"] = "keyword",
["#else"] = "keyword", ["#else"] = "keyword",
["#elseif"] = "keyword", ["#elseif"] = "keyword",
["#endif"] = "keyword", ["#endif"] = "keyword",

View File

@ -1,109 +1,36 @@
-- mod-version:3 -- mod-version:2 -- lite-xl 2.0
pcall(require, "plugins.language_c")
local syntax = require "core.syntax" local syntax = require "core.syntax"
syntax.add { syntax.add {
name = "C++",
files = { files = {
"%.h$", "%.inl$", "%.cpp$", "%.cc$", "%.C$", "%.cxx$", "%.h$", "%.inl$", "%.cpp$", "%.cc$", "%.C$", "%.cxx$",
"%.c++$", "%.hh$", "%.H$", "%.hxx$", "%.hpp$", "%.h++$", "%.c++$", "%.hh$", "%.H$", "%.hxx$", "%.hpp$", "%.h++$"
"%.ino$"
}, },
comment = "//", comment = "//",
block_comment = { "/*", "*/" },
patterns = { patterns = {
{ pattern = "//.*", type = "comment" }, { pattern = "//.-\n", type = "comment" },
{ pattern = { "/%*", "%*/" }, type = "comment" }, { pattern = { "/%*", "%*/" }, type = "comment" },
{ pattern = { '"', '"', '\\' }, type = "string" }, { pattern = { '"', '"', '\\' }, type = "string" },
{ pattern = { "'", "'", '\\' }, type = "string" }, { pattern = { "'", "'", '\\' }, type = "string" },
{ pattern = "0x%x+[%x']*", type = "number" }, { pattern = "0x%x+", type = "number" },
{ pattern = "%d+[%d%.'eE]*f?", type = "number" }, { pattern = "%d+[%d%.eE]*f?", type = "number" },
{ pattern = "%.?%d+[%d']*f?", type = "number" }, { pattern = "%.?%d+f?", type = "number" },
{ pattern = "[%+%-=/%*%^%%<>!~|:&]", type = "operator" }, { pattern = "[%+%-=/%*%^%%<>!~|&]", type = "operator" },
{ pattern = "##", type = "operator" },
{ pattern = "struct%s()[%a_][%w_]*", type = {"keyword", "keyword2"} }, { pattern = "struct%s()[%a_][%w_]*", type = {"keyword", "keyword2"} },
{ pattern = "class%s()[%a_][%w_]*", type = {"keyword", "keyword2"} }, { pattern = "class%s()[%a_][%w_]*", type = {"keyword", "keyword2"} },
{ pattern = "union%s()[%a_][%w_]*", type = {"keyword", "keyword2"} }, { pattern = "union%s()[%a_][%w_]*", type = {"keyword", "keyword2"} },
{ pattern = "namespace%s()[%a_][%w_]*", type = {"keyword", "keyword2"} }, { pattern = "namespace%s()[%a_][%w_]*", type = {"keyword", "keyword2"} },
-- static declarations { pattern = "[%a_][%w_]*::", type = "symbol" },
{ pattern = "static()%s+()inline", { pattern = "::", type = "symbol" },
type = { "keyword", "normal", "keyword" } { pattern = "[%a_][%w_]*", type = "symbol" },
}, { pattern = "#include%s()<.->", type = {"keyword", "string"} },
{ pattern = "static()%s+()const", { pattern = "#[%a_][%w_]*", type = "keyword" },
type = { "keyword", "normal", "keyword" }
},
{ pattern = "static()%s+()[%a_][%w_]*",
type = { "keyword", "normal", "literal" }
},
-- match method type declarations
{ pattern = "[%a_][%w_]*()%s*()%**()%s*()[%a_][%w_]*()%s*()::",
type = {
"literal", "normal", "operator", "normal",
"literal", "normal", "operator"
}
},
-- match function type declarations
{ pattern = "[%a_][%w_]*()%*+()%s+()[%a_][%w_]*%f[%(]",
type = { "literal", "operator", "normal", "function" }
},
{ pattern = "[%a_][%w_]*()%s+()%*+()[%a_][%w_]*%f[%(]",
type = { "literal", "normal", "operator", "function" }
},
{ pattern = "[%a_][%w_]*()%s+()[%a_][%w_]*%f[%(]",
type = { "literal", "normal", "function" }
},
-- match variable type declarations
{ pattern = "[%a_][%w_]*()%*+()%s+()[%a_][%w_]*",
type = { "literal", "operator", "normal", "normal" }
},
{ pattern = "[%a_][%w_]*()%s+()%*+()[%a_][%w_]*",
type = { "literal", "normal", "operator", "normal" }
},
{ pattern = "[%a_][%w_]*()%s+()[%a_][%w_]*()%s*()[;,%[%)]",
type = { "literal", "normal", "normal", "normal", "normal" }
},
{ pattern = "[%a_][%w_]*()%s+()[%a_][%w_]*()%s*()=",
type = { "literal", "normal", "normal", "normal", "operator" }
},
{ pattern = "[%a_][%w_]*()&()%s+()[%a_][%w_]*",
type = { "literal", "operator", "normal", "normal" }
},
{ pattern = "[%a_][%w_]*()%s+()&()[%a_][%w_]*",
type = { "literal", "normal", "operator", "normal" }
},
-- Match scope operator element access
{ pattern = "[%a_][%w_]*()%s*()::",
type = { "literal", "normal", "operator" }
},
-- Uppercase constants of at least 2 chars in len
{ pattern = "_?%u[%u_][%u%d_]*%f[%s%+%*%-%.%)%]}%?%^%%=/<>~|&;:,!]",
type = "number"
},
-- Magic constants
{ pattern = "__[%u%l]+__", type = "number" },
-- all other functions
{ pattern = "[%a_][%w_]*%f[(]", type = "function" },
-- Macros
{ pattern = "^%s*#%s*define%s+()[%a_][%a%d_]*",
type = { "keyword", "symbol" }
},
{ pattern = "#%s*include%s+()<.->",
type = { "keyword", "string" }
},
{ pattern = "%f[#]#%s*[%a_][%w_]*", type = "keyword" },
-- Everything else to make the tokenizer work properly
{ pattern = "[%a_][%w_]*", type = "symbol" },
}, },
symbols = { symbols = {
["alignof"] = "keyword", ["alignof"] = "keyword",
["alignas"] = "keyword", ["alignas"] = "keyword",
["and"] = "keyword",
["and_eq"] = "keyword",
["not"] = "keyword",
["not_eq"] = "keyword",
["or"] = "keyword",
["or_eq"] = "keyword",
["xor"] = "keyword",
["xor_eq"] = "keyword",
["private"] = "keyword", ["private"] = "keyword",
["protected"] = "keyword", ["protected"] = "keyword",
["public"] = "keyword", ["public"] = "keyword",
@ -111,12 +38,9 @@ syntax.add {
["nullptr"] = "keyword", ["nullptr"] = "keyword",
["operator"] = "keyword", ["operator"] = "keyword",
["asm"] = "keyword", ["asm"] = "keyword",
["bitand"] = "keyword",
["bitor"] = "keyword",
["catch"] = "keyword", ["catch"] = "keyword",
["throw"] = "keyword", ["throw"] = "keyword",
["try"] = "keyword", ["try"] = "keyword",
["class"] = "keyword",
["compl"] = "keyword", ["compl"] = "keyword",
["explicit"] = "keyword", ["explicit"] = "keyword",
["export"] = "keyword", ["export"] = "keyword",
@ -126,8 +50,8 @@ syntax.add {
["constinit"] = "keyword", ["constinit"] = "keyword",
["const_cast"] = "keyword", ["const_cast"] = "keyword",
["dynamic_cast"] = "keyword", ["dynamic_cast"] = "keyword",
["reinterpret_cast"] = "keyword", ["reinterpret_cast"] = "keyword",
["static_cast"] = "keyword", ["static_cast"] = "keyword",
["static_assert"] = "keyword", ["static_assert"] = "keyword",
["template"] = "keyword", ["template"] = "keyword",
["this"] = "keyword", ["this"] = "keyword",
@ -138,6 +62,7 @@ syntax.add {
["co_yield"] = "keyword", ["co_yield"] = "keyword",
["decltype"] = "keyword", ["decltype"] = "keyword",
["delete"] = "keyword", ["delete"] = "keyword",
["export"] = "keyword",
["friend"] = "keyword", ["friend"] = "keyword",
["typeid"] = "keyword", ["typeid"] = "keyword",
["typename"] = "keyword", ["typename"] = "keyword",
@ -145,7 +70,6 @@ syntax.add {
["override"] = "keyword", ["override"] = "keyword",
["virtual"] = "keyword", ["virtual"] = "keyword",
["using"] = "keyword", ["using"] = "keyword",
["namespace"] = "keyword",
["new"] = "keyword", ["new"] = "keyword",
["noexcept"] = "keyword", ["noexcept"] = "keyword",
["if"] = "keyword", ["if"] = "keyword",
@ -159,8 +83,6 @@ syntax.add {
["continue"] = "keyword", ["continue"] = "keyword",
["return"] = "keyword", ["return"] = "keyword",
["goto"] = "keyword", ["goto"] = "keyword",
["struct"] = "keyword",
["union"] = "keyword",
["typedef"] = "keyword", ["typedef"] = "keyword",
["enum"] = "keyword", ["enum"] = "keyword",
["extern"] = "keyword", ["extern"] = "keyword",
@ -172,7 +94,8 @@ syntax.add {
["case"] = "keyword", ["case"] = "keyword",
["default"] = "keyword", ["default"] = "keyword",
["auto"] = "keyword", ["auto"] = "keyword",
["void"] = "keyword2", ["const"] = "keyword",
["void"] = "keyword",
["int"] = "keyword2", ["int"] = "keyword2",
["short"] = "keyword2", ["short"] = "keyword2",
["long"] = "keyword2", ["long"] = "keyword2",
@ -181,18 +104,12 @@ syntax.add {
["char"] = "keyword2", ["char"] = "keyword2",
["unsigned"] = "keyword2", ["unsigned"] = "keyword2",
["bool"] = "keyword2", ["bool"] = "keyword2",
["true"] = "literal", ["true"] = "keyword2",
["false"] = "literal", ["false"] = "keyword2",
["NULL"] = "literal",
["wchar_t"] = "keyword2",
["char8_t"] = "keyword2",
["char16_t"] = "keyword2",
["char32_t"] = "keyword2",
["#include"] = "keyword", ["#include"] = "keyword",
["#if"] = "keyword", ["#if"] = "keyword",
["#ifdef"] = "keyword", ["#ifdef"] = "keyword",
["#ifndef"] = "keyword", ["#ifndef"] = "keyword",
["#elif"] = "keyword",
["#else"] = "keyword", ["#else"] = "keyword",
["#elseif"] = "keyword", ["#elseif"] = "keyword",
["#endif"] = "keyword", ["#endif"] = "keyword",
@ -200,5 +117,6 @@ syntax.add {
["#warning"] = "keyword", ["#warning"] = "keyword",
["#error"] = "keyword", ["#error"] = "keyword",
["#pragma"] = "keyword", ["#pragma"] = "keyword",
}, },
} }

View File

@ -1,13 +1,11 @@
-- mod-version:3 -- mod-version:2 -- lite-xl 2.0
local syntax = require "core.syntax" local syntax = require "core.syntax"
syntax.add { syntax.add {
name = "CSS",
files = { "%.css$" }, files = { "%.css$" },
block_comment = { "/*", "*/" },
patterns = { patterns = {
{ pattern = "\\.", type = "normal" }, { pattern = "\\.", type = "normal" },
{ pattern = "//.*", type = "comment" }, { pattern = "//.-\n", type = "comment" },
{ pattern = { "/%*", "%*/" }, type = "comment" }, { pattern = { "/%*", "%*/" }, type = "comment" },
{ pattern = { '"', '"', '\\' }, type = "string" }, { pattern = { '"', '"', '\\' }, type = "string" },
{ pattern = { "'", "'", '\\' }, type = "string" }, { pattern = { "'", "'", '\\' }, type = "string" },

View File

@ -1,23 +1,30 @@
-- mod-version:3 -- mod-version:2 -- lite-xl 2.0
local syntax = require "core.syntax" local syntax = require "core.syntax"
syntax.add { syntax.add {
name = "HTML",
files = { "%.html?$" }, files = { "%.html?$" },
block_comment = { "<!--", "-->" },
patterns = { patterns = {
{ {
pattern = { pattern = {
"<%s*[sS][cC][rR][iI][pP][tT]%f[%s>].->", "<%s*[sS][cC][rR][iI][pP][tT]%s+[tT][yY][pP][eE]%s*=%s*" ..
"<%s*/%s*[sS][cC][rR][iI][pP][tT]%s*>" "['\"]%a+/[jJ][aA][vV][aA][sS][cC][rR][iI][pP][tT]['\"]%s*>",
"<%s*/[sS][cC][rR][iI][pP][tT]>"
},
syntax = ".js",
type = "function"
},
{
pattern = {
"<%s*[sS][cC][rR][iI][pP][tT]%s*>",
"<%s*/%s*[sS][cC][rR][iI][pP][tT]>"
}, },
syntax = ".js", syntax = ".js",
type = "function" type = "function"
}, },
{ {
pattern = { pattern = {
"<%s*[sS][tT][yY][lL][eE]%f[%s>].->", "<%s*[sS][tT][yY][lL][eE][^>]*>",
"<%s*/%s*[sS][tT][yY][lL][eE]%s*>" "<%s*/%s*[sS][tT][yY][lL][eE]%s*>"
}, },
syntax = ".css", syntax = ".css",
type = "function" type = "function"

View File

@ -1,75 +1,22 @@
-- mod-version:3 -- mod-version:2 -- lite-xl 2.0
local syntax = require "core.syntax" local syntax = require "core.syntax"
-- Regex pattern explanation:
-- This will match / and will look ahead for something that looks like a regex.
--
-- (?!/) Don't match empty regexes.
--
-- (?>...) this is using an atomic group to minimize backtracking, as that'd
-- cause "Catastrophic Backtracking" in some cases.
--
-- [^\\[\/]++ will match anything that's isn't an escape, a start of character
-- class or an end of pattern, without backtracking (the second +).
--
-- \\. will match anything that's escaped.
--
-- \[(?:[^\\\]++]|\\.)*+\] will match character classes.
--
-- /[gmiyuvsd]*\s*[\n,;\)\]\}\.]) will match the end of pattern delimiter, optionally
-- followed by pattern options, and anything that can
-- be after a pattern.
--
-- Demo with some unit tests (click on the Unit Tests entry): https://regex101.com/r/Vx5L5V/1
-- Note that it has a couple of changes to make it work on that platform.
local regex_pattern = {
[=[\/(?=(?!\/)(?:(?>[^\\[\/]++|\\.|\[(?:[^\\\]]++|\\.)*+\])*+)++\/[gmiyuvsd]*\s*(?:[\n,;\)\]\}\.]|\/[\/*]))()]=],
"/()[gmiyuvsd]*", "\\"
}
-- For the moment let's not actually differentiate the insides of the regex,
-- as this will need new token types...
local inner_regex_syntax = {
patterns = {
{ pattern = "%(()%?[:!=><]", type = { "string", "string" } },
{ pattern = "[.?+*%(%)|]", type = "string" },
{ pattern = "{%d*,?%d*}", type = "string" },
{ regex = { [=[\[()\^?]=], [=[(?:\]|(?=\n))()]=], "\\" },
type = { "string", "string" },
syntax = { -- Inside character class
patterns = {
{ pattern = "\\\\", type = "string" },
{ pattern = "\\%]", type = "string" },
{ pattern = "[^%]\n]", type = "string" }
},
symbols = {}
}
},
{ regex = "\\/", type = "string" },
{ regex = "[^/\n]", type = "string" },
},
symbols = {}
}
syntax.add { syntax.add {
name = "JavaScript", files = { "%.js$", "%.json$", "%.cson$" },
files = { "%.js$", "%.json$", "%.cson$", "%.mjs$", "%.cjs$" },
comment = "//", comment = "//",
block_comment = { "/*", "*/" },
patterns = { patterns = {
{ pattern = "//.*", type = "comment" }, { pattern = "//.-\n", type = "comment" },
{ pattern = { "/%*", "%*/" }, type = "comment" }, { pattern = { "/%*", "%*/" }, type = "comment" },
{ regex = regex_pattern, syntax = inner_regex_syntax, type = {"string", "string"} }, { pattern = { '/%g', '/', '\\' }, type = "string" },
{ pattern = { '"', '"', '\\' }, type = "string" }, { pattern = { '"', '"', '\\' }, type = "string" },
{ pattern = { "'", "'", '\\' }, type = "string" }, { pattern = { "'", "'", '\\' }, type = "string" },
{ pattern = { "`", "`", '\\' }, type = "string" }, { pattern = { "`", "`", '\\' }, type = "string" },
-- Use (?:\/(?!\/|\*))? to avoid that a regex can start after a number, while also allowing // and /* comments { pattern = "0x[%da-fA-F]+", type = "number" },
{ regex = [[-?0[xXbBoO][\da-fA-F_]+n?()\s*()(?:\/(?!\/|\*))?]], type = {"number", "normal", "operator"} }, { pattern = "-?%d+[%d%.eE]*", type = "number" },
{ regex = [[-?\d+[0-9.eE_n]*()\s*()(?:\/(?!\/|\*))?]], type = {"number", "normal", "operator"} }, { pattern = "-?%.?%d+", type = "number" },
{ regex = [[-?\.?\d+()\s*()(?:\/(?!\/|\*))?]], type = {"number", "normal", "operator"} }, { pattern = "[%+%-=/%*%^%%<>!~|&]", type = "operator" },
{ pattern = "[%+%-=/%*%^%%<>!~|&]", type = "operator" }, { pattern = "[%a_][%w_]*%f[(]", type = "function" },
{ pattern = "[%a_][%w_]*%f[(]", type = "function" }, { pattern = "[%a_][%w_]*", type = "symbol" },
{ pattern = "[%a_][%w_]*", type = "symbol" },
}, },
symbols = { symbols = {
["async"] = "keyword", ["async"] = "keyword",
@ -93,7 +40,6 @@ syntax.add {
["get"] = "keyword", ["get"] = "keyword",
["if"] = "keyword", ["if"] = "keyword",
["import"] = "keyword", ["import"] = "keyword",
["from"] = "keyword",
["in"] = "keyword", ["in"] = "keyword",
["of"] = "keyword", ["of"] = "keyword",
["instanceof"] = "keyword", ["instanceof"] = "keyword",

View File

@ -1,18 +1,16 @@
-- mod-version:3 -- mod-version:2 -- lite-xl 2.0
local syntax = require "core.syntax" local syntax = require "core.syntax"
syntax.add { syntax.add {
name = "Lua",
files = "%.lua$", files = "%.lua$",
headers = "^#!.*[ /]lua", headers = "^#!.*[ /]lua",
comment = "--", comment = "--",
block_comment = { "--[[", "]]" },
patterns = { patterns = {
{ pattern = { '"', '"', '\\' }, type = "string" }, { pattern = { '"', '"', '\\' }, type = "string" },
{ pattern = { "'", "'", '\\' }, type = "string" }, { pattern = { "'", "'", '\\' }, type = "string" },
{ pattern = { "%[%[", "%]%]" }, type = "string" }, { pattern = { "%[%[", "%]%]" }, type = "string" },
{ pattern = { "%-%-%[%[", "%]%]"}, type = "comment" }, { pattern = { "%-%-%[%[", "%]%]"}, type = "comment" },
{ pattern = "%-%-.*", type = "comment" }, { pattern = "%-%-.-\n", type = "comment" },
{ pattern = "0x%x+%.%x*[pP][-+]?%d+", type = "number" }, { pattern = "0x%x+%.%x*[pP][-+]?%d+", type = "number" },
{ pattern = "0x%x+%.%x*", type = "number" }, { pattern = "0x%x+%.%x*", type = "number" },
{ pattern = "0x%.%x+[pP][-+]?%d+", type = "number" }, { pattern = "0x%.%x+[pP][-+]?%d+", type = "number" },

View File

@ -1,268 +1,41 @@
-- mod-version:3 -- mod-version:2 -- lite-xl 2.0
local syntax = require "core.syntax" local syntax = require "core.syntax"
local style = require "core.style"
local core = require "core"
local in_squares_match = "^%[%]"
local in_parenthesis_match = "^%(%)"
syntax.add { syntax.add {
name = "Markdown",
files = { "%.md$", "%.markdown$" }, files = { "%.md$", "%.markdown$" },
block_comment = { "<!--", "-->" },
space_handling = false, -- turn off this feature to handle it our selfs
patterns = { patterns = {
---- Place patterns that require spaces at start to optimize matching speed { pattern = "\\.", type = "normal" },
---- and apply the %s+ optimization immediately afterwards { pattern = { "<!%-%-", "%-%->" }, type = "comment" },
-- bullets { pattern = { "```c", "```" }, type = "string", syntax = ".c" },
{ pattern = "^%s*%*%s", type = "number" },
{ pattern = "^%s*%-%s", type = "number" },
{ pattern = "^%s*%+%s", type = "number" },
-- numbered bullet
{ pattern = "^%s*[0-9]+[%.%)]%s", type = "number" },
-- blockquote
{ pattern = "^%s*>+%s", type = "string" },
-- alternative bold italic formats
{ pattern = { "%s___", "___" }, type = "markdown_bold_italic" },
{ pattern = { "%s__", "__" }, type = "markdown_bold" },
{ pattern = { "%s_[%S]", "_" }, type = "markdown_italic" },
-- reference links
{
pattern = "^%s*%[%^()["..in_squares_match.."]+()%]: ",
type = { "function", "number", "function" }
},
{
pattern = "^%s*%[%^?()["..in_squares_match.."]+()%]:%s+.*",
type = { "function", "number", "function" }
},
-- optimization
{ pattern = "%s+", type = "normal" },
---- HTML rules imported and adapted from language_html
---- to not conflict with markdown rules
-- Inline JS and CSS
{
pattern = {
"<%s*[sS][cC][rR][iI][pP][tT]%s+[tT][yY][pP][eE]%s*=%s*" ..
"['\"]%a+/[jJ][aA][vV][aA][sS][cC][rR][iI][pP][tT]['\"]%s*>",
"<%s*/[sS][cC][rR][iI][pP][tT]>"
},
syntax = ".js",
type = "function"
},
{
pattern = {
"<%s*[sS][cC][rR][iI][pP][tT]%s*>",
"<%s*/%s*[sS][cC][rR][iI][pP][tT]>"
},
syntax = ".js",
type = "function"
},
{
pattern = {
"<%s*[sS][tT][yY][lL][eE][^>]*>",
"<%s*/%s*[sS][tT][yY][lL][eE]%s*>"
},
syntax = ".css",
type = "function"
},
-- Comments
{ pattern = { "<!%-%-", "%-%->" }, type = "comment" },
-- Tags
{ pattern = "%f[^<]![%a_][%w_]*", type = "keyword2" },
{ pattern = "%f[^<][%a_][%w_]*", type = "function" },
{ pattern = "%f[^<]/[%a_][%w_]*", type = "function" },
-- Attributes
{
pattern = "[a-z%-]+%s*()=%s*()\".-\"",
type = { "keyword", "operator", "string" }
},
{
pattern = "[a-z%-]+%s*()=%s*()'.-'",
type = { "keyword", "operator", "string" }
},
{
pattern = "[a-z%-]+%s*()=%s*()%-?%d[%d%.]*",
type = { "keyword", "operator", "number" }
},
-- Entities
{ pattern = "&#?[a-zA-Z0-9]+;", type = "keyword2" },
---- Markdown rules
-- math
{ pattern = { "%$%$", "%$%$", "\\" }, type = "string", syntax = ".tex"},
{ regex = { "\\$", [[\$|(?=\\*\n)]], "\\" }, type = "string", syntax = ".tex"},
-- code blocks
{ pattern = { "```caddyfile", "```" }, type = "string", syntax = "Caddyfile" },
{ pattern = { "```c++", "```" }, type = "string", syntax = ".cpp" }, { pattern = { "```c++", "```" }, type = "string", syntax = ".cpp" },
{ pattern = { "```cpp", "```" }, type = "string", syntax = ".cpp" },
{ pattern = { "```python", "```" }, type = "string", syntax = ".py" }, { pattern = { "```python", "```" }, type = "string", syntax = ".py" },
{ pattern = { "```ruby", "```" }, type = "string", syntax = ".rb" }, { pattern = { "```ruby", "```" }, type = "string", syntax = ".rb" },
{ pattern = { "```perl", "```" }, type = "string", syntax = ".pl" }, { pattern = { "```perl", "```" }, type = "string", syntax = ".pl" },
{ pattern = { "```php", "```" }, type = "string", syntax = ".php" }, { pattern = { "```php", "```" }, type = "string", syntax = ".php" },
{ pattern = { "```javascript", "```" }, type = "string", syntax = ".js" }, { pattern = { "```javascript", "```" }, type = "string", syntax = ".js" },
{ pattern = { "```json", "```" }, type = "string", syntax = ".js" },
{ pattern = { "```html", "```" }, type = "string", syntax = ".html" }, { pattern = { "```html", "```" }, type = "string", syntax = ".html" },
{ pattern = { "```ini", "```" }, type = "string", syntax = ".ini" },
{ pattern = { "```xml", "```" }, type = "string", syntax = ".xml" }, { pattern = { "```xml", "```" }, type = "string", syntax = ".xml" },
{ pattern = { "```css", "```" }, type = "string", syntax = ".css" }, { pattern = { "```css", "```" }, type = "string", syntax = ".css" },
{ pattern = { "```lua", "```" }, type = "string", syntax = ".lua" }, { pattern = { "```lua", "```" }, type = "string", syntax = ".lua" },
{ pattern = { "```bash", "```" }, type = "string", syntax = ".sh" }, { pattern = { "```bash", "```" }, type = "string", syntax = ".sh" },
{ pattern = { "```sh", "```" }, type = "string", syntax = ".sh" },
{ pattern = { "```java", "```" }, type = "string", syntax = ".java" }, { pattern = { "```java", "```" }, type = "string", syntax = ".java" },
{ pattern = { "```c#", "```" }, type = "string", syntax = ".cs" }, { pattern = { "```c#", "```" }, type = "string", syntax = ".cs" },
{ pattern = { "```cmake", "```" }, type = "string", syntax = ".cmake" }, { pattern = { "```cmake", "```" }, type = "string", syntax = ".cmake" },
{ pattern = { "```d", "```" }, type = "string", syntax = ".d" }, { pattern = { "```d", "```" }, type = "string", syntax = ".d" },
{ pattern = { "```glsl", "```" }, type = "string", syntax = ".glsl" }, { pattern = { "```glsl", "```" }, type = "string", syntax = ".glsl" },
{ pattern = { "```c", "```" }, type = "string", syntax = ".c" }, { pattern = { "```", "```" }, type = "string" },
{ pattern = { "```julia", "```" }, type = "string", syntax = ".jl" }, { pattern = { "``", "``", "\\" }, type = "string" },
{ pattern = { "```rust", "```" }, type = "string", syntax = ".rs" }, { pattern = { "`", "`", "\\" }, type = "string" },
{ pattern = { "```dart", "```" }, type = "string", syntax = ".dart" }, { pattern = { "~~", "~~", "\\" }, type = "keyword2" },
{ pattern = { "```v", "```" }, type = "string", syntax = ".v" }, { pattern = "%-%-%-+", type = "comment" },
{ pattern = { "```toml", "```" }, type = "string", syntax = ".toml" }, { pattern = "%*%s+", type = "operator" },
{ pattern = { "```yaml", "```" }, type = "string", syntax = ".yaml" }, { pattern = { "%*", "[%*\n]", "\\" }, type = "operator" },
{ pattern = { "```nim", "```" }, type = "string", syntax = ".nim" }, { pattern = { "%_", "[%_\n]", "\\" }, type = "keyword2" },
{ pattern = { "```typescript", "```" }, type = "string", syntax = ".ts" }, { pattern = "#.-\n", type = "keyword" },
{ pattern = { "```rescript", "```" }, type = "string", syntax = ".res" }, { pattern = "!?%[.-%]%(.-%)", type = "function" },
{ pattern = { "```moon", "```" }, type = "string", syntax = ".moon" },
{ pattern = { "```go", "```" }, type = "string", syntax = ".go" },
{ pattern = { "```lobster", "```" }, type = "string", syntax = ".lobster" },
{ pattern = { "```liquid", "```" }, type = "string", syntax = ".liquid" },
{ pattern = { "```", "```" }, type = "string" },
{ pattern = { "``", "``" }, type = "string" },
{ pattern = { "%f[\\`]%`[%S]", "`" }, type = "string" },
-- lines
{ pattern = "^%-%-%-+\n" , type = "comment" },
{ pattern = "^%*%*%*+\n", type = "comment" },
{ pattern = "^___+\n", type = "comment" },
{ pattern = "^===+\n", type = "comment" },
-- strike
{ pattern = { "~~", "~~" }, type = "keyword2" },
-- highlight
{ pattern = { "==", "==" }, type = "literal" },
-- bold and italic
{ pattern = { "%*%*%*%S", "%*%*%*" }, type = "markdown_bold_italic" },
{ pattern = { "%*%*%S", "%*%*" }, type = "markdown_bold" },
-- handle edge case where asterisk can be at end of line and not close
{
pattern = { "%f[\\%*]%*[%S]", "%*%f[^%*]" },
type = "markdown_italic"
},
-- alternative bold italic formats
{ pattern = "^___[%s%p%w]+___" , type = "markdown_bold_italic" },
{ pattern = "^__[%s%p%w]+__" , type = "markdown_bold" },
{ pattern = "^_[%s%p%w]+_" , type = "markdown_italic" },
-- heading with custom id
{
pattern = "^#+%s[%w%s%p]+(){()#[%w%-]+()}",
type = { "keyword", "function", "string", "function" }
},
-- headings
{ pattern = "^#+%s.+\n", type = "keyword" },
-- superscript and subscript
{
pattern = "%^()%d+()%^",
type = { "function", "number", "function" }
},
{
pattern = "%~()%d+()%~",
type = { "function", "number", "function" }
},
-- definitions
{ pattern = "^:%s.+", type = "function" },
-- emoji
{ pattern = ":[a-zA-Z0-9_%-]+:", type = "literal" },
-- images and link
{
pattern = "!?%[!?%[()["..in_squares_match.."]+()%]%(()["..in_parenthesis_match.."]+()%)%]%(()["..in_parenthesis_match.."]+()%)",
type = { "function", "string", "function", "number", "function", "number", "function" }
},
{
pattern = "!?%[!?%[?()["..in_squares_match.."]+()%]?%]%(()["..in_parenthesis_match.."]+()%)",
type = { "function", "string", "function", "number", "function" }
},
-- reference links
{
pattern = "%[()["..in_squares_match.."]+()%] *()%[()["..in_squares_match.."]+()%]",
type = { "function", "string", "function", "function", "number", "function" }
},
{
pattern = "!?%[%^?()["..in_squares_match.."]+()%]",
type = { "function", "number", "function" }
},
-- url's and email
{
pattern = "<[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+%.[a-zA-Z0-9-.]+>",
type = "function"
},
{ pattern = "<https?://%S+>", type = "function" },
{ pattern = "https?://%S+", type = "function" }, { pattern = "https?://%S+", type = "function" },
-- optimize consecutive dashes used in tables
{ pattern = "%-+", type = "normal" },
}, },
symbols = { }, symbols = { },
} }
-- Adjust the color on theme changes
core.add_thread(function()
local custom_fonts = { bold = {font = nil, color = nil}, italic = {}, bold_italic = {} }
local initial_color
local last_code_font
local function set_font(attr)
local attributes = {}
if attr ~= "bold_italic" then
attributes[attr] = true
else
attributes["bold"] = true
attributes["italic"] = true
end
local font = style.code_font:copy(
style.code_font:get_size(),
attributes
)
custom_fonts[attr].font = font
style.syntax_fonts["markdown_"..attr] = font
end
local function set_color(attr)
custom_fonts[attr].color = style.syntax["keyword2"]
style.syntax["markdown_"..attr] = style.syntax["keyword2"]
end
-- Add 3 type of font styles for use on markdown files
for attr, _ in pairs(custom_fonts) do
-- Only set it if the font wasn't manually customized
if not style.syntax_fonts["markdown_"..attr] then
set_font(attr)
end
-- Only set it if the color wasn't manually customized
if not style.syntax["markdown_"..attr] then
set_color(attr)
end
end
while true do
if last_code_font ~= style.code_font then
last_code_font = style.code_font
for attr, _ in pairs(custom_fonts) do
-- Only set it if the font wasn't manually customized
if style.syntax_fonts["markdown_"..attr] == custom_fonts[attr].font then
set_font(attr)
end
end
end
if initial_color ~= style.syntax["keyword2"] then
initial_color = style.syntax["keyword2"]
for attr, _ in pairs(custom_fonts) do
-- Only set it if the color wasn't manually customized
if style.syntax["markdown_"..attr] == custom_fonts[attr].color then
set_color(attr)
end
end
end
coroutine.yield(1)
end
end)

View File

@ -1,27 +1,21 @@
-- mod-version:3 -- mod-version:2 -- lite-xl 2.0
local syntax = require "core.syntax" local syntax = require "core.syntax"
syntax.add { syntax.add {
name = "Python", files = { "%.py$", "%.pyw$" },
files = { "%.py$", "%.pyw$", "%.rpy$", "%.pyi$" },
headers = "^#!.*[ /]python", headers = "^#!.*[ /]python",
comment = "#", comment = "#",
block_comment = { '"""', '"""' },
patterns = { patterns = {
{ pattern = "#.*", type = "comment" }, { pattern = { "#", "\n" }, type = "comment" },
{ pattern = { '^%s*"""', '"""' }, type = "comment" }, { pattern = { '[ruU]?"', '"', '\\' }, type = "string" },
{ pattern = '[uUrR]%f["]', type = "keyword" }, { pattern = { "[ruU]?'", "'", '\\' }, type = "string" },
{ pattern = "class%s+()[%a_][%w_]*", type = {"keyword", "keyword2"} }, { pattern = { '"""', '"""' }, type = "string" },
{ pattern = { '[ruU]?"""', '"""'; '\\' }, type = "string" }, { pattern = "0x[%da-fA-F]+", type = "number" },
{ pattern = { "[ruU]?'''", "'''", '\\' }, type = "string" }, { pattern = "-?%d+[%d%.eE]*", type = "number" },
{ pattern = { '[ruU]?"', '"', '\\' }, type = "string" }, { pattern = "-?%.?%d+", type = "number" },
{ pattern = { "[ruU]?'", "'", '\\' }, type = "string" }, { pattern = "[%+%-=/%*%^%%<>!~|&]", type = "operator" },
{ pattern = "-?0[xboXBO][%da-fA-F_]+",type = "number" }, { pattern = "[%a_][%w_]*%f[(]", type = "function" },
{ pattern = "-?%d+[%d%.eE_]*", type = "number" }, { pattern = "[%a_][%w_]*", type = "symbol" },
{ pattern = "-?%.?%d+", type = "number" },
{ pattern = "[%+%-=/%*%^%%<>!~|&]", type = "operator" },
{ pattern = "[%a_][%w_]*%f[(]", type = "function" },
{ pattern = "[%a_][%w_]*", type = "symbol" },
}, },
symbols = { symbols = {
["class"] = "keyword", ["class"] = "keyword",
@ -33,8 +27,6 @@ syntax.add {
["lambda"] = "keyword", ["lambda"] = "keyword",
["try"] = "keyword", ["try"] = "keyword",
["def"] = "keyword", ["def"] = "keyword",
["async"] = "keyword",
["await"] = "keyword",
["from"] = "keyword", ["from"] = "keyword",
["nonlocal"] = "keyword", ["nonlocal"] = "keyword",
["while"] = "keyword", ["while"] = "keyword",
@ -47,8 +39,6 @@ syntax.add {
["if"] = "keyword", ["if"] = "keyword",
["or"] = "keyword", ["or"] = "keyword",
["else"] = "keyword", ["else"] = "keyword",
["match"] = "keyword",
["case"] = "keyword",
["import"] = "keyword", ["import"] = "keyword",
["pass"] = "keyword", ["pass"] = "keyword",
["break"] = "keyword", ["break"] = "keyword",

View File

@ -1,11 +1,9 @@
-- mod-version:3 -- mod-version:2 -- lite-xl 2.0
local syntax = require "core.syntax" local syntax = require "core.syntax"
syntax.add { syntax.add {
name = "XML",
files = { "%.xml$" }, files = { "%.xml$" },
headers = "<%?xml", headers = "<%?xml",
block_comment = { "<!--", "-->" },
patterns = { patterns = {
{ pattern = { "<!%-%-", "%-%->" }, type = "comment" }, { pattern = { "<!%-%-", "%-%->" }, type = "comment" },
{ pattern = { '%f[^>][^<]', '%f[<]' }, type = "normal" }, { pattern = { '%f[^>][^<]', '%f[<]' }, type = "normal" },

View File

@ -1,115 +1,21 @@
-- mod-version:3 -- mod-version:2 -- lite-xl 2.0
local common = require "core.common"
local command = require "core.command"
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"
local CommandView = require "core.commandview"
config.plugins.lineguide = common.merge({
enabled = false,
width = 2,
rulers = {
-- 80,
-- 100,
-- 120,
config.line_limit
},
-- The config specification used by gui generators
config_spec = {
name = "Line Guide",
{
label = "Enabled",
description = "Disable or enable drawing of the line guide.",
path = "enabled",
type = "toggle",
default = true
},
{
label = "Width",
description = "Width in pixels of the line guide.",
path = "width",
type = "number",
default = 2,
min = 1
},
{
label = "Ruler Positions",
description = "The different column numbers for the line guides to draw.",
path = "rulers",
type = "list_strings",
default = { tostring(config.line_limit) or "80" },
get_value = function(rulers)
if type(rulers) == "table" then
local new_rulers = {}
for _, ruler in ipairs(rulers) do
table.insert(new_rulers, tostring(ruler))
end
return new_rulers
else
return { tostring(config.line_limit) }
end
end,
set_value = function(rulers)
local new_rulers = {}
for _, ruler in ipairs(rulers) do
local number = tonumber(ruler)
if number then
table.insert(new_rulers, number)
end
end
if #new_rulers == 0 then
table.insert(new_rulers, config.line_limit)
end
return new_rulers
end
}
}
}, config.plugins.lineguide)
local function get_ruler(v)
local result = nil
if type(v) == 'number' then
result = { columns = v }
elseif type(v) == 'table' then
result = v
end
return result
end
local draw_overlay = DocView.draw_overlay local draw_overlay = DocView.draw_overlay
function DocView:draw_overlay(...) function DocView:draw_overlay(...)
if local ns = ("n"):rep(config.line_limit)
type(config.plugins.lineguide) == "table" local ss = self:get_font():subpixel_scale()
and local offset = self:get_font():get_width_subpixel(ns) / ss
config.plugins.lineguide.enabled local x = self:get_line_screen_position(1) + offset
and local y = self.position.y
self:is(DocView) local w = math.ceil(SCALE * 1)
then local h = self.size.y
local line_x = self:get_line_screen_position(1)
local character_width = self:get_font():get_width("n")
local ruler_width = config.plugins.lineguide.width
local ruler_color = style.guide or style.selection
for k,v in ipairs(config.plugins.lineguide.rulers) do local color = style.guide or style.selection
local ruler = get_ruler(v) renderer.draw_rect(x, y, w, h, color)
if ruler then
local x = line_x + (character_width * ruler.columns)
local y = self.position.y
local w = ruler_width
local h = self.size.y
renderer.draw_rect(x, y, w, h, ruler.color or ruler_color)
end
end
end
-- everything else like the cursor above the line guides
draw_overlay(self, ...) draw_overlay(self, ...)
end end
command.add(nil, {
["lineguide:toggle"] = function()
config.plugins.lineguide.enabled = not config.plugins.lineguide.enabled
end
})

View File

@ -1,600 +0,0 @@
-- mod-version:3 --priority:10
local core = require "core"
local common = require "core.common"
local DocView = require "core.docview"
local Doc = require "core.doc"
local style = require "core.style"
local config = require "core.config"
local command = require "core.command"
local keymap = require "core.keymap"
local translate = require "core.doc.translate"
config.plugins.linewrapping = common.merge({
-- The type of wrapping to perform. Can be "letter" or "word".
mode = "letter",
-- If nil, uses the DocView's size, otherwise, uses this exact width. Can be a function.
width_override = nil,
-- Whether or not to draw a guide
guide = true,
-- Whether or not we should indent ourselves like the first line of a wrapped block.
indent = true,
-- Whether or not to enable wrapping by default when opening files.
enable_by_default = false,
-- Requires tokenization
require_tokenization = false,
-- The config specification used by gui generators
config_spec = {
name = "Line Wrapping",
{
label = "Mode",
description = "The type of wrapping to perform.",
path = "mode",
type = "selection",
default = "letter",
values = {
{"Letters", "letter"},
{"Words", "word"}
}
},
{
label = "Guide",
description = "Whether or not to draw a guide.",
path = "guide",
type = "toggle",
default = true
},
{
label = "Indent",
description = "Whether or not to follow the indentation of wrapped line.",
path = "indent",
type = "toggle",
default = true
},
{
label = "Enable by Default",
description = "Whether or not to enable wrapping by default when opening files.",
path = "enable_by_default",
type = "toggle",
default = false
},
{
label = "Require Tokenization",
description = "Use tokenization when applying wrapping.",
path = "require_tokenization",
type = "toggle",
default = false
}
}
}, config.plugins.linewrapping)
local LineWrapping = {}
-- Optimzation function. The tokenizer is relatively slow (at present), and
-- so if we don't need to run it, should be run sparingly.
local function spew_tokens(doc, line) if line < math.huge then return math.huge, "normal", doc.lines[line] end end
local function get_tokens(doc, line)
if config.plugins.linewrapping.require_tokenization then
return doc.highlighter:each_token(line)
end
return spew_tokens, doc, line
end
-- Computes the breaks for a given line, width and mode. Returns a list of columns
-- at which the line should be broken.
function LineWrapping.compute_line_breaks(doc, default_font, line, width, mode)
local xoffset, last_i, i, last_space, last_width, begin_width = 0, 1, 1, nil, 0, 0
local splits = { 1 }
for idx, type, text in get_tokens(doc, line) do
local font = style.syntax_fonts[type] or default_font
if idx == 1 or idx == math.huge and config.plugins.linewrapping.indent then
local _, indent_end = text:find("^%s+")
if indent_end then begin_width = font:get_width(text:sub(1, indent_end)) end
end
local w = font:get_width(text)
if xoffset + w > width then
for char in common.utf8_chars(text) do
w = font:get_width(char)
xoffset = xoffset + w
if xoffset > width then
if mode == "word" and last_space then
table.insert(splits, last_space + 1)
xoffset = w + begin_width + (xoffset - last_width)
else
table.insert(splits, i)
xoffset = w + begin_width
end
last_space = nil
elseif char == ' ' then
last_space = i
last_width = xoffset
end
i = i + #char
end
else
xoffset = xoffset + w
i = i + #text
end
end
return splits, begin_width
end
-- breaks are held in a single table that contains n*2 elements, where n is the amount of line breaks.
-- each element represents line and column of the break. line_offset will check from the specified line
-- if the first line has not changed breaks, it will stop there.
function LineWrapping.reconstruct_breaks(docview, default_font, width, line_offset)
if width ~= math.huge then
local doc = docview.doc
-- two elements per wrapped line; first maps to original line number, second to column number.
docview.wrapped_lines = { }
-- one element per actual line; maps to the first index of in wrapped_lines for this line
docview.wrapped_line_to_idx = { }
-- one element per actual line; gives the indent width for the acutal line
docview.wrapped_line_offsets = { }
docview.wrapped_settings = { ["width"] = width, ["font"] = default_font }
for i = line_offset or 1, #doc.lines do
local breaks, offset = LineWrapping.compute_line_breaks(doc, default_font, i, width, config.plugins.linewrapping.mode)
table.insert(docview.wrapped_line_offsets, offset)
for k, col in ipairs(breaks) do
table.insert(docview.wrapped_lines, i)
table.insert(docview.wrapped_lines, col)
end
end
-- list of indices for wrapped_lines, that are based on original line number
-- holds the index to the first in the wrapped_lines list.
local last_wrap = nil
for i = 1, #docview.wrapped_lines, 2 do
if not last_wrap or last_wrap ~= docview.wrapped_lines[i] then
table.insert(docview.wrapped_line_to_idx, (i + 1) / 2)
last_wrap = docview.wrapped_lines[i]
end
end
else
docview.wrapped_lines = nil
docview.wrapped_line_to_idx = nil
docview.wrapped_line_offsets = nil
docview.wrapped_settings = nil
end
end
-- When we have an insertion or deletion, we have four sections of text.
-- 1. The unaffected section, located prior to the cursor. This is completely ignored.
-- 2. The beginning of the affected line prior to the insertion or deletion. Begins on column 1 of the selection.
-- 3. The removed/pasted lines.
-- 4. Every line after the modification, begins one line after the selection in the initial document.
function LineWrapping.update_breaks(docview, old_line1, old_line2, net_lines)
-- Step 1: Determine the index for the line for #2.
local old_idx1 = docview.wrapped_line_to_idx[old_line1] or 1
-- Step 2: Determine the index of the line for #4.
local old_idx2 = (docview.wrapped_line_to_idx[old_line2 + 1] or ((#docview.wrapped_lines / 2) + 1)) - 1
-- Step 3: Remove all old breaks for the old lines from the table, and all old widths from wrapped_line_offsets.
local offset = (old_idx1 - 1) * 2 + 1
for i = old_idx1, old_idx2 do
table.remove(docview.wrapped_lines, offset)
table.remove(docview.wrapped_lines, offset)
end
for i = old_line1, old_line2 do
table.remove(docview.wrapped_line_offsets, old_line1)
end
-- Step 4: Shift the line number of wrapped_lines past #4 by the amount of inserted/deleted lines.
if net_lines ~= 0 then
for i = offset, #docview.wrapped_lines, 2 do
docview.wrapped_lines[i] = docview.wrapped_lines[i] + net_lines
end
end
-- Step 5: Compute the breaks and offsets for the lines for #2 and #3. Insert them into the table.
local new_line1 = old_line1
local new_line2 = old_line2 + net_lines
for line = new_line1, new_line2 do
local breaks, begin_width = LineWrapping.compute_line_breaks(docview.doc, docview.wrapped_settings.font, line, docview.wrapped_settings.width, config.plugins.linewrapping.mode)
table.insert(docview.wrapped_line_offsets, line, begin_width)
for i,b in ipairs(breaks) do
table.insert(docview.wrapped_lines, offset, b)
table.insert(docview.wrapped_lines, offset, line)
offset = offset + 2
end
end
-- Step 6: Recompute the wrapped_line_to_idx cache from #2.
local line = old_line1
offset = (old_idx1 - 1) * 2 + 1
while offset < #docview.wrapped_lines do
if docview.wrapped_lines[offset + 1] == 1 then
docview.wrapped_line_to_idx[line] = ((offset - 1) / 2) + 1
line = line + 1
end
offset = offset + 2
end
while line <= #docview.wrapped_line_to_idx do
table.remove(docview.wrapped_line_to_idx)
end
end
-- Draws a guide if applicable to show where wrapping is occurring.
function LineWrapping.draw_guide(docview)
if config.plugins.linewrapping.guide and docview.wrapped_settings.width ~= math.huge then
local x, y = docview:get_content_offset()
local gw = docview:get_gutter_width()
renderer.draw_rect(x + gw + docview.wrapped_settings.width, y, 1, core.root_view.size.y, style.selection)
end
end
function LineWrapping.update_docview_breaks(docview)
local w = docview.v_scrollbar.expanded_size or style.expanded_scrollbar_size
local width = (type(config.plugins.linewrapping.width_override) == "function" and config.plugins.linewrapping.width_override(docview))
or config.plugins.linewrapping.width_override or (docview.size.x - docview:get_gutter_width() - w)
if (not docview.wrapped_settings or docview.wrapped_settings.width == nil or width ~= docview.wrapped_settings.width) then
docview.scroll.to.x = 0
LineWrapping.reconstruct_breaks(docview, docview:get_font(), width)
end
end
local function get_idx_line_col(docview, idx)
local doc = docview.doc
if not docview.wrapped_settings then
if idx > #doc.lines then return #doc.lines, #doc.lines[#doc.lines] + 1 end
return idx, 1
end
if idx < 1 then return 1, 1 end
local offset = (idx - 1) * 2 + 1
if offset > #docview.wrapped_lines then return #doc.lines, #doc.lines[#doc.lines] + 1 end
return docview.wrapped_lines[offset], docview.wrapped_lines[offset + 1]
end
local function get_idx_line_length(docview, idx)
local doc = docview.doc
if not docview.wrapped_settings then
if idx > #doc.lines then return #doc.lines[#doc.lines] + 1 end
return #doc.lines[idx]
end
local offset = (idx - 1) * 2 + 1
local start = docview.wrapped_lines[offset + 1]
if docview.wrapped_lines[offset + 2] and docview.wrapped_lines[offset + 2] == docview.wrapped_lines[offset] then
return docview.wrapped_lines[offset + 3] - docview.wrapped_lines[offset + 1]
else
return #doc.lines[docview.wrapped_lines[offset]] - docview.wrapped_lines[offset + 1] + 1
end
end
local function get_total_wrapped_lines(docview)
if not docview.wrapped_settings then return docview.doc and #docview.doc.lines end
return #docview.wrapped_lines / 2
end
-- If line end, gives the end of an index line, rather than the first character of the next line.
local function get_line_idx_col_count(docview, line, col, line_end, ndoc)
local doc = docview.doc
if not docview.wrapped_settings then return common.clamp(line, 1, #doc.lines), col, 1, 1 end
if line > #doc.lines then return get_line_idx_col_count(docview, #doc.lines, #doc.lines[#doc.lines] + 1) end
line = math.max(line, 1)
local idx = docview.wrapped_line_to_idx[line] or 1
local ncol, scol = 1, 1
if col then
local i = idx + 1
while line == docview.wrapped_lines[(i - 1) * 2 + 1] and col >= docview.wrapped_lines[(i - 1) * 2 + 2] do
local nscol = docview.wrapped_lines[(i - 1) * 2 + 2]
if line_end and col == nscol then
break
end
scol = nscol
i = i + 1
idx = idx + 1
end
ncol = (col - scol) + 1
end
local count = (docview.wrapped_line_to_idx[line + 1] or (get_total_wrapped_lines(docview) + 1)) - (docview.wrapped_line_to_idx[line] or get_total_wrapped_lines(docview))
return idx, ncol, count, scol
end
local function get_line_col_from_index_and_x(docview, idx, x)
local doc = docview.doc
local line, col = get_idx_line_col(docview, idx)
if idx < 1 then return 1, 1 end
local xoffset, last_i, i = (col ~= 1 and docview.wrapped_line_offsets[line] or 0), col, 1
if x < xoffset then return line, col end
local default_font = docview:get_font()
for _, type, text in doc.highlighter:each_token(line) do
local font, w = style.syntax_fonts[type] or default_font, 0
for char in common.utf8_chars(text) do
if i >= col then
if xoffset >= x then
return line, (xoffset - x > (w / 2) and last_i or i)
end
w = font:get_width(char)
xoffset = xoffset + w
end
last_i = i
i = i + #char
end
end
return line, #doc.lines[line]
end
local open_files = setmetatable({ }, { __mode = "k" })
local old_doc_insert = Doc.raw_insert
function Doc:raw_insert(line, col, text, undo_stack, time)
local old_lines = #self.lines
old_doc_insert(self, line, col, text, undo_stack, time)
if open_files[self] then
for i,docview in ipairs(open_files[self]) do
if docview.wrapped_settings then
local lines = #self.lines - old_lines
LineWrapping.update_breaks(docview, line, line, lines)
end
end
end
end
local old_doc_remove = Doc.raw_remove
function Doc:raw_remove(line1, col1, line2, col2, undo_stack, time)
local old_lines = #self.lines
old_doc_remove(self, line1, col1, line2, col2, undo_stack, time)
if open_files[self] then
for i,docview in ipairs(open_files[self]) do
if docview.wrapped_settings then
local lines = #self.lines - old_lines
LineWrapping.update_breaks(docview, line1, line2, lines)
end
end
end
end
local old_doc_update = DocView.update
function DocView:update()
old_doc_update(self)
if self.wrapped_settings and self.size.x > 0 then
LineWrapping.update_docview_breaks(self)
end
end
function DocView:get_scrollable_size()
if not config.scroll_past_end then
return self:get_line_height() * get_total_wrapped_lines(self) + style.padding.y * 2
end
return self:get_line_height() * (get_total_wrapped_lines(self) - 1) + self.size.y
end
local old_get_h_scrollable_size = DocView.get_h_scrollable_size
function DocView:get_h_scrollable_size(...)
if self.wrapping_enabled then return 0 end
return old_get_h_scrollable_size(self, ...)
end
local old_new = DocView.new
function DocView:new(doc)
old_new(self, doc)
if not open_files[doc] then open_files[doc] = {} end
table.insert(open_files[doc], self)
if config.plugins.linewrapping.enable_by_default then
self.wrapping_enabled = true
LineWrapping.update_docview_breaks(self)
else
self.wrapping_enabled = false
end
end
local old_scroll_to_line = DocView.scroll_to_line
function DocView:scroll_to_line(...)
if self.wrapping_enabled then LineWrapping.update_docview_breaks(self) end
old_scroll_to_line(self, ...)
end
local old_scroll_to_make_visible = DocView.scroll_to_make_visible
function DocView:scroll_to_make_visible(line, col)
if self.wrapping_enabled then LineWrapping.update_docview_breaks(self) end
old_scroll_to_make_visible(self, line, col)
if self.wrapped_settings then self.scroll.to.x = 0 end
end
local old_get_visible_line_range = DocView.get_visible_line_range
function DocView:get_visible_line_range()
if not self.wrapped_settings then return old_get_visible_line_range(self) end
local x, y, x2, y2 = self:get_content_bounds()
local lh = self:get_line_height()
local minline = get_idx_line_col(self, math.max(1, math.floor(y / lh)))
local maxline = get_idx_line_col(self, math.min(get_total_wrapped_lines(self), math.floor(y2 / lh) + 1))
return minline, maxline
end
local old_get_x_offset_col = DocView.get_x_offset_col
function DocView:get_x_offset_col(line, x)
if not self.wrapped_settings then return old_get_x_offset_col(self, line, x) end
local idx = get_line_idx_col_count(self, line)
return get_line_col_from_index_and_x(self, idx, x)
end
-- If line end is true, returns the end of the previous line, in a multi-line break.
local old_get_col_x_offset = DocView.get_col_x_offset
function DocView:get_col_x_offset(line, col, line_end)
if not self.wrapped_settings then return old_get_col_x_offset(self, line, col) end
local idx, ncol, count, scol = get_line_idx_col_count(self, line, col, line_end)
local xoffset, i = (scol ~= 1 and self.wrapped_line_offsets[line] or 0), 1
local default_font = self:get_font()
for _, type, text in self.doc.highlighter:each_token(line) do
if i + #text >= scol then
if i < scol then
text = text:sub(scol - i + 1)
i = scol
end
local font = style.syntax_fonts[type] or default_font
for char in common.utf8_chars(text) do
if i >= col then
return xoffset
end
xoffset = xoffset + font:get_width(char)
i = i + #char
end
else
i = i + #text
end
end
return xoffset
end
local old_get_line_screen_position = DocView.get_line_screen_position
function DocView:get_line_screen_position(line, col)
if not self.wrapped_settings then return old_get_line_screen_position(self, line, col) end
local idx, ncol, count = get_line_idx_col_count(self, line, col)
local x, y = self:get_content_offset()
local lh = self:get_line_height()
local gw = self:get_gutter_width()
return x + gw + (col and self:get_col_x_offset(line, col) or 0), y + (idx-1) * lh + style.padding.y
end
local old_resolve_screen_position = DocView.resolve_screen_position
function DocView:resolve_screen_position(x, y)
if not self.wrapped_settings then return old_resolve_screen_position(self, x, y) end
local ox, oy = self:get_line_screen_position(1)
local idx = common.clamp(math.floor((y - oy) / self:get_line_height()) + 1, 1, get_total_wrapped_lines(self))
return get_line_col_from_index_and_x(self, idx, x - ox)
end
local old_draw_line_text = DocView.draw_line_text
function DocView:draw_line_text(line, x, y)
if not self.wrapped_settings then return old_draw_line_text(self, line, x, y) end
local default_font = self:get_font()
local tx, ty, begin_width = x, y + self:get_line_text_y_offset(), self.wrapped_line_offsets[line]
local lh = self:get_line_height()
local idx, _, count = get_line_idx_col_count(self, line)
local total_offset = 1
for _, type, text in self.doc.highlighter:each_token(line) do
local color = style.syntax[type]
local font = style.syntax_fonts[type] or default_font
local token_offset = 1
-- Split tokens if we're at the end of the document.
while text ~= nil and token_offset <= #text do
local next_line, next_line_start_col = get_idx_line_col(self, idx + 1)
if next_line ~= line then
next_line_start_col = #self.doc.lines[line]
end
local max_length = next_line_start_col - total_offset
local rendered_text = text:sub(token_offset, token_offset + max_length - 1)
tx = renderer.draw_text(font, rendered_text, tx, ty, color)
total_offset = total_offset + #rendered_text
if total_offset ~= next_line_start_col or max_length == 0 then break end
token_offset = token_offset + #rendered_text
idx = idx + 1
tx, ty = x + begin_width, ty + lh
end
end
return lh * count
end
local old_draw_line_body = DocView.draw_line_body
function DocView:draw_line_body(line, x, y)
if not self.wrapped_settings then return old_draw_line_body(self, line, x, y) end
local lh = self:get_line_height()
local idx0, _, count = get_line_idx_col_count(self, line)
for lidx, line1, col1, line2, col2 in self.doc:get_selections(true) do
if line >= line1 and line <= line2 then
if line1 ~= line then col1 = 1 end
if line2 ~= line then col2 = #self.doc.lines[line] + 1 end
if col1 ~= col2 then
local idx1, ncol1 = get_line_idx_col_count(self, line, col1)
local idx2, ncol2 = get_line_idx_col_count(self, line, col2)
local start = 0
for i = idx1, idx2 do
local x1, x2 = x + (idx1 == i and self:get_col_x_offset(line1, col1) or 0)
if idx2 == i then
x2 = x + self:get_col_x_offset(line, col2)
else
start = start + get_idx_line_length(self, i, line)
x2 = x + self:get_col_x_offset(line, start + 1, true)
end
renderer.draw_rect(x1, y + (i - idx0) * lh, x2 - x1, lh, style.selection)
end
end
end
end
local draw_highlight = nil
for lidx, line1, col1, line2, col2 in self.doc:get_selections(true) do
-- draw line highlight if caret is on this line
if draw_highlight ~= false and config.highlight_current_line
and line1 == line and core.active_view == self then
draw_highlight = (line1 == line2 and col1 == col2)
end
end
if draw_highlight then
for i=1,count do
self:draw_line_highlight(x + self.scroll.x, y + lh * (i - 1))
end
end
-- draw line's text
return self:draw_line_text(line, x, y)
end
local old_draw = DocView.draw
function DocView:draw()
old_draw(self)
if self.wrapped_settings then
LineWrapping.draw_guide(self)
end
end
local old_draw_line_gutter = DocView.draw_line_gutter
function DocView:draw_line_gutter(line, x, y, width)
local lh = self:get_line_height()
local _, _, count = get_line_idx_col_count(self, line)
return (old_draw_line_gutter(self, line, x, y, width) or lh) * count
end
local old_translate_end_of_line = translate.end_of_line
function translate.end_of_line(doc, line, col)
if not core.active_view or core.active_view.doc ~= doc or not core.active_view.wrapped_settings then old_translate_end_of_line(doc, line, col) end
local idx, ncol = get_line_idx_col_count(core.active_view, line, col)
local nline, ncol2 = get_idx_line_col(core.active_view, idx + 1)
if nline ~= line then return line, math.huge end
return line, ncol2 - 1
end
local old_translate_start_of_line = translate.start_of_line
function translate.start_of_line(doc, line, col)
if not core.active_view or core.active_view.doc ~= doc or not core.active_view.wrapped_settings then old_translate_start_of_line(doc, line, col) end
local idx, ncol = get_line_idx_col_count(core.active_view, line, col)
local nline, ncol2 = get_idx_line_col(core.active_view, idx - 1)
if nline ~= line then return line, 1 end
return line, ncol2 + 1
end
local old_previous_line = DocView.translate.previous_line
function DocView.translate.previous_line(doc, line, col, dv)
if not dv.wrapped_settings then return old_previous_line(doc, line, col, dv) end
local idx, ncol = get_line_idx_col_count(dv, line, col)
return get_line_col_from_index_and_x(dv, idx - 1, dv:get_col_x_offset(line, col))
end
local old_next_line = DocView.translate.next_line
function DocView.translate.next_line(doc, line, col, dv)
if not dv.wrapped_settings then return old_next_line(doc, line, col, dv) end
local idx, ncol = get_line_idx_col_count(dv, line, col)
return get_line_col_from_index_and_x(dv, idx + 1, dv:get_col_x_offset(line, col))
end
command.add(nil, {
["line-wrapping:enable"] = function()
if core.active_view and core.active_view.doc then
core.active_view.wrapping_enabled = true
LineWrapping.update_docview_breaks(core.active_view)
end
end,
["line-wrapping:disable"] = function()
if core.active_view and core.active_view.doc then
core.active_view.wrapping_enabled = false
LineWrapping.reconstruct_breaks(core.active_view, core.active_view:get_font(), math.huge)
end
end,
["line-wrapping:toggle"] = function()
if core.active_view and core.active_view.doc and core.active_view.wrapped_settings then
command.perform("line-wrapping:disable")
else
command.perform("line-wrapping:enable")
end
end
})
keymap.add {
["f10"] = "line-wrapping:toggle",
}
return LineWrapping

View File

@ -1,4 +1,4 @@
-- mod-version:3 -- 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:3 -- 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"
@ -6,16 +6,16 @@ local command = require "core.command"
local style = require "core.style" local style = require "core.style"
local View = require "core.view" local View = require "core.view"
---@class plugins.projectsearch.resultsview : core.view
local ResultsView = View:extend() local ResultsView = View:extend()
ResultsView.context = "session" ResultsView.context = "session"
function ResultsView:new(path, text, fn) function ResultsView:new(text, fn)
ResultsView.super.new(self) ResultsView.super.new(self)
self.scrollable = true self.scrollable = true
self.brightness = 0 self.brightness = 0
self:begin_search(path, text, fn) self:begin_search(text, fn)
end end
@ -45,8 +45,8 @@ local function find_all_matches_in_file(t, filename, fn)
end end
function ResultsView:begin_search(path, text, fn) function ResultsView:begin_search(text, fn)
self.search_args = { path, text, fn } self.search_args = { text, fn }
self.results = {} self.results = {}
self.last_file_idx = 1 self.last_file_idx = 1
self.query = text self.query = text
@ -56,9 +56,9 @@ function ResultsView:begin_search(path, text, fn)
core.add_thread(function() core.add_thread(function()
local i = 1 local i = 1
for dir_name, file in core.get_project_files() do for dir_name, file in core.get_project_files() do
if file.type == "file" and (not path or (dir_name .. "/" .. file.filename):find(path, 1, true) == 1) then if file.type == "file" then
local truncated_path = (dir_name == core.project_dir and "" or (dir_name .. PATHSEP)) local path = (dir_name == core.project_dir and "" or (dir_name .. PATHSEP))
find_all_matches_in_file(self.results, truncated_path .. file.filename, fn) find_all_matches_in_file(self.results, path .. file.filename, fn)
end end
self.last_file_idx = i self.last_file_idx = i
i = i + 1 i = i + 1
@ -92,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
return self:open_selected_result() self:open_selected_result()
end end
end end
@ -108,7 +108,6 @@ 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
@ -176,7 +175,7 @@ function ResultsView:draw()
local text local text
if self.searching then if self.searching then
if files_number then if files_number then
text = string.format("Searching %.f%% (%d of %d files, %d matches) for %q...", text = string.format("Searching %d%% (%d of %d files, %d matches) for %q...",
per * 100, self.last_file_idx, files_number, per * 100, self.last_file_idx, files_number,
#self.results, self.query) #self.results, self.query)
else else
@ -219,122 +218,41 @@ function ResultsView:draw()
end end
---@param path string local function begin_search(text, fn)
---@param text string
---@param fn fun(line_text:string):...
---@return plugins.projectsearch.resultsview?
local function begin_search(path, text, fn)
if text == "" then if text == "" then
core.error("Expected non-empty string") core.error("Expected non-empty string")
return return
end end
local rv = ResultsView(path, text, fn) local rv = ResultsView(text, fn)
core.root_view:get_active_node_default():add_view(rv) core.root_view:get_active_node_default():add_view(rv)
return rv
end
local function get_selected_text()
local view = core.active_view
local doc = (view and view.doc) and view.doc or nil
if doc then
return doc:get_text(table.unpack({ doc:get_selection() }))
end
end
local function normalize_path(path)
if not path then return nil end
path = common.normalize_path(path)
for i, project_dir in ipairs(core.project_directories) do
if common.path_belongs_to(path, project_dir.name) then
return project_dir.item.filename .. PATHSEP .. common.relative_path(project_dir.name, path)
end
end
return path
end
---@class plugins.projectsearch
local projectsearch = {}
---@type plugins.projectsearch.resultsview
projectsearch.ResultsView = ResultsView
---@param text string
---@param path string
---@param insensitive? boolean
---@return plugins.projectsearch.resultsview?
function projectsearch.search_plain(text, path, insensitive)
if insensitive then text = text:lower() end
return begin_search(path, text, function(line_text)
if insensitive then
return line_text:lower():find(text, nil, true)
else
return line_text:find(text, nil, true)
end
end)
end
---@param text string
---@param path string
---@param insensitive? boolean
---@return plugins.projectsearch.resultsview?
function projectsearch.search_regex(text, path, insensitive)
local re, errmsg
if insensitive then
re, errmsg = regex.compile(text, "i")
else
re, errmsg = regex.compile(text)
end
if not re then core.log("%s", errmsg) return end
return begin_search(path, text, function(line_text)
return regex.cmatch(re, line_text)
end)
end
---@param text string
---@param path string
---@param insensitive? boolean
---@return plugins.projectsearch.resultsview?
function projectsearch.search_fuzzy(text, path, insensitive)
if insensitive then text = text:lower() end
return begin_search(path, text, function(line_text)
if insensitive then
return common.fuzzy_match(line_text:lower(), text) and 1
else
return common.fuzzy_match(line_text, text) and 1
end
end)
end end
command.add(nil, { command.add(nil, {
["project-search:find"] = function(path) ["project-search:find"] = function()
core.command_view:enter("Find Text In " .. (normalize_path(path) or "Project"), { core.command_view:enter("Find Text In Project", function(text)
text = get_selected_text(), text = text:lower()
select_text = true, begin_search(text, function(line_text)
submit = function(text) return line_text:lower():find(text, nil, true)
projectsearch.search_plain(text, path, true) end)
end end)
})
end, end,
["project-search:find-regex"] = function(path) ["project-search:find-regex"] = function()
core.command_view:enter("Find Regex In " .. (normalize_path(path) or "Project"), { core.command_view:enter("Find Regex In Project", function(text)
submit = function(text) local re = regex.compile(text, "i")
projectsearch.search_regex(text, path, true) begin_search(text, function(line_text)
end return regex.cmatch(re, line_text)
}) end)
end)
end, end,
["project-search:fuzzy-find"] = function(path) ["project-search:fuzzy-find"] = function()
core.command_view:enter("Fuzzy Find Text In " .. (normalize_path(path) or "Project"), { core.command_view:enter("Fuzzy Find Text In Project", function(text)
text = get_selected_text(), begin_search(text, function(line_text)
select_text = true, return common.fuzzy_match(line_text, text) and 1
submit = function(text) end)
projectsearch.search_fuzzy(text, path, true) end)
end
})
end, end,
}) })
@ -359,22 +277,22 @@ command.add(ResultsView, {
["project-search:refresh"] = function() ["project-search:refresh"] = function()
core.active_view:refresh() core.active_view:refresh()
end, end,
["project-search:move-to-previous-page"] = function() ["project-search:move-to-previous-page"] = function()
local view = core.active_view local view = core.active_view
view.scroll.to.y = view.scroll.to.y - view.size.y view.scroll.to.y = view.scroll.to.y - view.size.y
end, end,
["project-search:move-to-next-page"] = function() ["project-search:move-to-next-page"] = function()
local view = core.active_view local view = core.active_view
view.scroll.to.y = view.scroll.to.y + view.size.y view.scroll.to.y = view.scroll.to.y + view.size.y
end, end,
["project-search:move-to-start-of-doc"] = function() ["project-search:move-to-start-of-doc"] = function()
local view = core.active_view local view = core.active_view
view.scroll.to.y = 0 view.scroll.to.y = 0
end, end,
["project-search:move-to-end-of-doc"] = function() ["project-search:move-to-end-of-doc"] = function()
local view = core.active_view local view = core.active_view
view.scroll.to.y = view:get_scrollable_size() view.scroll.to.y = view:get_scrollable_size()
@ -394,6 +312,3 @@ keymap.add {
["home"] = "project-search:move-to-start-of-doc", ["home"] = "project-search:move-to-start-of-doc",
["end"] = "project-search:move-to-end-of-doc" ["end"] = "project-search:move-to-end-of-doc"
} }
return projectsearch

View File

@ -1,4 +1,4 @@
-- mod-version:3 -- 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"
@ -19,8 +19,8 @@ end
command.add("core.docview", { command.add("core.docview", {
["quote:quote"] = function(dv) ["quote:quote"] = function()
dv.doc:replace(function(text) core.active_view.doc:replace(function(text)
return '"' .. text:gsub("[\0-\31\\\"]", replace) .. '"' return '"' .. text:gsub("[\0-\31\\\"]", replace) .. '"'
end) end)
end, end,

View File

@ -1,4 +1,4 @@
-- mod-version:3 -- 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"
@ -25,8 +25,8 @@ end
command.add("core.docview", { command.add("core.docview", {
["reflow:reflow"] = function(dv) ["reflow:reflow"] = function()
local doc = dv.doc local doc = core.active_view.doc
doc:replace(function(text) doc:replace(function(text)
local prefix_set = "[^%w\n%[%](){}`'\"]*" local prefix_set = "[^%w\n%[%](){}`'\"]*"

View File

@ -1,20 +1,17 @@
-- mod-version:3 -- 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"
local config = require "core.config" local config = require "core.config"
local keymap = require "core.keymap" local keymap = require "core.keymap"
local style = require "core.style" local style = require "core.style"
local RootView = require "core.rootview"
local CommandView = require "core.commandview" local CommandView = require "core.commandview"
config.plugins.scale = common.merge({ config.plugins.scale = {
-- The method used to apply the scaling: "code", "ui"
mode = "code", mode = "code",
-- Default scale applied at startup.
default_scale = "autodetect",
-- Allow using CTRL + MouseWheel for changing the scale.
use_mousewheel = true use_mousewheel = true
}, config.plugins.scale) }
local scale_steps = 0.05 local scale_steps = 0.05
@ -25,16 +22,11 @@ local function set_scale(scale)
scale = common.clamp(scale, 0.2, 6) scale = common.clamp(scale, 0.2, 6)
-- save scroll positions -- save scroll positions
local v_scrolls = {} local scrolls = {}
local h_scrolls = {}
for _, view in ipairs(core.root_view.root_node:get_children()) do for _, view in ipairs(core.root_view.root_node:get_children()) do
local n = view:get_scrollable_size() local n = view:get_scrollable_size()
if n ~= math.huge and n > view.size.y then if n ~= math.huge and not view:is(CommandView) and n > view.size.y then
v_scrolls[view] = view.scroll.y / (n - view.size.y) scrolls[view] = view.scroll.y / (n - view.size.y)
end
local hn = view:get_h_scrollable_size()
if hn ~= math.huge and hn > view.size.x then
h_scrolls[view] = view.scroll.x / (hn - view.size.x)
end end
end end
@ -44,34 +36,29 @@ local function set_scale(scale)
if config.plugins.scale.mode == "ui" then if config.plugins.scale.mode == "ui" then
SCALE = scale SCALE = scale
style.padding.x = style.padding.x * s style.padding.x = style.padding.x * s
style.padding.y = style.padding.y * s style.padding.y = style.padding.y * s
style.divider_size = style.divider_size * s style.divider_size = style.divider_size * s
style.scrollbar_size = style.scrollbar_size * s style.scrollbar_size = style.scrollbar_size * s
style.expanded_scrollbar_size = style.expanded_scrollbar_size * s style.caret_width = style.caret_width * s
style.caret_width = style.caret_width * s 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
style[name]:set_size(s * style[name]:get_size()) renderer.font.set_size(style[name], s * style[name]:get_size())
end end
else else
style.code_font:set_size(s * style.code_font:get_size()) renderer.font.set_size(style.code_font, s * style.code_font:get_size())
end end
for name, font in pairs(style.syntax_fonts) do for _, font in pairs(style.syntax_fonts) do
style.syntax_fonts[name]:set_size(s * font:get_size()) renderer.font.set_size(font, s * font:get_size())
end end
-- restore scroll positions -- restore scroll positions
for view, n in pairs(v_scrolls) do for view, n in pairs(scrolls) do
view.scroll.y = n * (view:get_scrollable_size() - view.size.y) view.scroll.y = n * (view:get_scrollable_size() - view.size.y)
view.scroll.to.y = view.scroll.y view.scroll.to.y = view.scroll.y
end end
for view, hn in pairs(h_scrolls) do
view.scroll.x = hn * (view:get_h_scrollable_size() - view.size.x)
view.scroll.to.x = view.scroll.x
end
core.redraw = true core.redraw = true
end end
@ -80,6 +67,17 @@ 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()
set_scale(default_scale) set_scale(default_scale)
end end
@ -92,75 +90,6 @@ local function dec_scale()
set_scale(current_scale - scale_steps) set_scale(current_scale - scale_steps)
end end
if default_scale ~= config.plugins.scale.default_scale then
if type(config.plugins.scale.default_scale) == "number" then
set_scale(config.plugins.scale.default_scale)
end
end
-- The config specification used by gui generators
config.plugins.scale.config_spec = {
name = "Scale",
{
label = "Mode",
description = "The method used to apply the scaling.",
path = "mode",
type = "selection",
default = "code",
values = {
{"Everything", "ui"},
{"Code Only", "code"}
}
},
{
label = "Default Scale",
description = "The scaling factor applied to lite-xl.",
path = "default_scale",
type = "selection",
default = "autodetect",
values = {
{"Autodetect", "autodetect"},
{"80%", 0.80},
{"90%", 0.90},
{"100%", 1.00},
{"110%", 1.10},
{"120%", 1.20},
{"125%", 1.25},
{"130%", 1.30},
{"140%", 1.40},
{"150%", 1.50},
{"175%", 1.75},
{"200%", 2.00},
{"250%", 2.50},
{"300%", 3.00}
},
on_apply = function(value)
if type(value) == "string" then value = default_scale end
if value ~= current_scale then
set_scale(value)
end
end
},
{
label = "Use MouseWheel",
description = "Allow using CTRL + MouseWheel for changing the scale.",
path = "use_mousewheel",
type = "toggle",
default = true,
on_apply = function(enabled)
if enabled then
keymap.add {
["ctrl+wheelup"] = "scale:increase",
["ctrl+wheeldown"] = "scale:decrease"
}
else
keymap.unbind("ctrl+wheelup", "scale:increase")
keymap.unbind("ctrl+wheeldown", "scale:decrease")
end
end
}
}
command.add(nil, { command.add(nil, {
["scale:reset" ] = function() res_scale() end, ["scale:reset" ] = function() res_scale() end,
@ -171,16 +100,9 @@ command.add(nil, {
keymap.add { keymap.add {
["ctrl+0"] = "scale:reset", ["ctrl+0"] = "scale:reset",
["ctrl+-"] = "scale:decrease", ["ctrl+-"] = "scale:decrease",
["ctrl+="] = "scale:increase" ["ctrl+="] = "scale:increase",
} }
if config.plugins.scale.use_mousewheel then
keymap.add {
["ctrl+wheelup"] = "scale:increase",
["ctrl+wheeldown"] = "scale:decrease"
}
end
return { return {
["set"] = set_scale, ["set"] = set_scale,
["get"] = get_scale, ["get"] = get_scale,

View File

@ -1,4 +1,4 @@
-- mod-version:3 -- 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"
@ -41,23 +41,21 @@ end
command.add("core.docview", { command.add("core.docview", {
["tabularize:tabularize"] = function(dv) ["tabularize:tabularize"] = function()
core.command_view:enter("Tabularize On Delimiter", { core.command_view:enter("Tabularize On Delimiter", function(delim)
submit = function(delim) if delim == "" then delim = " " end
if delim == "" then delim = " " end
local doc = dv.doc local doc = core.active_view.doc
local line1, col1, line2, col2, swap = doc:get_selection(true) local line1, col1, line2, col2, swap = doc:get_selection(true)
line1, col1 = doc:position_offset(line1, col1, translate.start_of_line) line1, col1 = doc:position_offset(line1, col1, translate.start_of_line)
line2, col2 = doc:position_offset(line2, col2, translate.end_of_line) line2, col2 = doc:position_offset(line2, col2, translate.end_of_line)
doc:set_selection(line1, col1, line2, col2, swap) doc:set_selection(line1, col1, line2, col2, swap)
doc:replace(function(text) doc:replace(function(text)
local lines = gmatch_to_array(text, "[^\n]*\n?") local lines = gmatch_to_array(text, "[^\n]*\n?")
tabularize_lines(lines, delim) tabularize_lines(lines, delim)
return table.concat(lines) return table.concat(lines)
end) end)
end end)
})
end, end,
}) })

View File

@ -1,4 +1,4 @@
-- mod-version:3 -- 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"
@ -7,26 +7,31 @@ local View = require "core.view"
local ToolbarView = View:extend() local ToolbarView = View:extend()
local toolbar_commands = {
{symbol = "f", command = "core:new-doc"},
{symbol = "D", command = "core:open-file"},
{symbol = "S", command = "doc:save"},
{symbol = "L", command = "core:find-file"},
{symbol = "B", command = "core:find-command"},
{symbol = "P", command = "core:open-user-module"},
}
local function toolbar_height()
return style.icon_big_font:get_height() + style.padding.y * 2
end
function ToolbarView:new() function ToolbarView:new()
ToolbarView.super.new(self) ToolbarView.super.new(self)
self.visible = true self.visible = true
self.init_size = true self.init_size = true
self.tooltip = false self.tooltip = false
self.toolbar_font = style.icon_big_font
self.toolbar_commands = {
{symbol = "f", command = "core:new-doc"},
{symbol = "D", command = "core:open-file"},
{symbol = "S", command = "doc:save"},
{symbol = "L", command = "core:find-file"},
{symbol = "B", command = "core:find-command"},
{symbol = "P", command = "core:open-user-module"},
}
end end
function ToolbarView:update() function ToolbarView:update()
local dest_size = self.visible and (self.toolbar_font:get_height() + style.padding.y * 2) or 0 local dest_size = self.visible and toolbar_height() or 0
if self.init_size then if self.init_size then
self.size.y = dest_size self.size.y = dest_size
self.init_size = nil self.init_size = nil
@ -39,31 +44,21 @@ end
function ToolbarView:toggle_visible() function ToolbarView:toggle_visible()
self.visible = not self.visible self.visible = not self.visible
if self.tooltip then
core.status_view:remove_tooltip()
self.tooltip = false
end
self.hovered_item = nil
end end
function ToolbarView:get_icon_width()
local max_width = 0
for i,v in ipairs(self.toolbar_commands) do max_width = math.max(max_width, self.toolbar_font:get_width(v.symbol)) end
return max_width
end
function ToolbarView:each_item() function ToolbarView:each_item()
local icon_h, icon_w = self.toolbar_font:get_height(), self:get_icon_width() local icon_h, icon_w = style.icon_big_font:get_height(), style.icon_big_font:get_width("D")
local toolbar_spacing = icon_w / 2 local toolbar_spacing = icon_w / 2
local ox, oy = self:get_content_offset() local ox, oy = self:get_content_offset()
local index = 0 local index = 0
local iter = function() local iter = function()
index = index + 1 index = index + 1
if index <= #self.toolbar_commands then if index <= #toolbar_commands then
local dx = style.padding.x + (icon_w + toolbar_spacing) * (index - 1) local dx = style.padding.x + (icon_w + toolbar_spacing) * (index - 1)
local dy = style.padding.y local dy = style.padding.y
if dx + icon_w > self.size.x then return end if dx + icon_w > self.size.x then return end
return self.toolbar_commands[index], ox + dx, oy + dy, icon_w, icon_h return toolbar_commands[index], ox + dx, oy + dy, icon_w, icon_h
end end
end end
return iter return iter
@ -71,37 +66,33 @@ end
function ToolbarView:get_min_width() function ToolbarView:get_min_width()
local icon_w = self:get_icon_width() local icon_w = style.icon_big_font:get_width("D")
local space = icon_w / 2 local space = icon_w / 2
return 2 * style.padding.x + (icon_w + space) * #self.toolbar_commands - space return 2 * style.padding.x + (icon_w + space) * #toolbar_commands - space
end end
function ToolbarView:draw() function ToolbarView:draw()
if not self.visible then return end
self:draw_background(style.background2) self:draw_background(style.background2)
for item, x, y, w, h in self:each_item() do for item, x, y, w, h in self:each_item() do
local color = item == self.hovered_item and command.is_valid(item.command) and style.text or style.dim local color = item == self.hovered_item and style.text or style.dim
common.draw_text(self.toolbar_font, color, item.symbol, nil, x, y, 0, h) common.draw_text(style.icon_big_font, color, item.symbol, nil, x, y, 0, h)
end end
end end
function ToolbarView:on_mouse_pressed(button, x, y, clicks) function ToolbarView:on_mouse_pressed(button, x, y, clicks)
if not self.visible then return end
local caught = ToolbarView.super.on_mouse_pressed(self, button, x, y, clicks) local caught = ToolbarView.super.on_mouse_pressed(self, button, x, y, clicks)
if caught then return caught end if caught then return end
core.set_active_view(core.last_active_view) core.set_active_view(core.last_active_view)
if self.hovered_item and command.is_valid(self.hovered_item.command) then if self.hovered_item then
command.perform(self.hovered_item.command) command.perform(self.hovered_item.command)
end end
return true
end end
function ToolbarView:on_mouse_moved(px, py, ...) function ToolbarView:on_mouse_moved(px, py, ...)
if not self.visible then return end
ToolbarView.super.on_mouse_moved(self, px, py, ...) ToolbarView.super.on_mouse_moved(self, px, py, ...)
self.hovered_item = nil self.hovered_item = nil
local x_min, x_max, y_min, y_max = self.size.x, 0, self.size.y, 0 local x_min, x_max, y_min, y_max = self.size.x, 0, self.size.y, 0

View File

@ -1,4 +1,4 @@
-- mod-version:3 -- 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"
@ -8,13 +8,9 @@ local style = require "core.style"
local View = require "core.view" local View = require "core.view"
local ContextMenu = require "core.contextmenu" local ContextMenu = require "core.contextmenu"
local RootView = require "core.rootview" local RootView = require "core.rootview"
local CommandView = require "core.commandview"
config.plugins.treeview = common.merge({
-- Default treeview width
size = 200 * SCALE
}, config.plugins.treeview)
local default_treeview_size = 200 * SCALE
local tooltip_offset = style.font:get_height() local tooltip_offset = style.font:get_height()
local tooltip_border = 1 local tooltip_border = 1
local tooltip_delay = 0.5 local tooltip_delay = 0.5
@ -24,7 +20,7 @@ local tooltip_alpha_rate = 1
local function get_depth(filename) local function get_depth(filename)
local n = 1 local n = 1
for _ in filename:gmatch(PATHSEP) do for sep in filename:gmatch("[\\/]") do
n = n + 1 n = n + 1
end end
return n return n
@ -43,13 +39,10 @@ function TreeView:new()
self.scrollable = true self.scrollable = true
self.visible = true self.visible = true
self.init_size = true self.init_size = true
self.target_size = config.plugins.treeview.size self.target_size = default_treeview_size
self.cache = {} self.cache = {}
self.last = {}
self.tooltip = { x = 0, y = 0, begin = 0, alpha = 0 } self.tooltip = { x = 0, y = 0, begin = 0, alpha = 0 }
self.cursor_pos = { x = 0, y = 0 }
self.item_icon_width = 0
self.item_text_spacing = 0
end end
@ -61,7 +54,7 @@ function TreeView:set_target_size(axis, value)
end end
function TreeView:get_cached(dir, item, dirname) function TreeView:get_cached(item, dirname)
local dir_cache = self.cache[dirname] local dir_cache = self.cache[dirname]
if not dir_cache then if not dir_cache then
dir_cache = {} dir_cache = {}
@ -72,7 +65,7 @@ function TreeView:get_cached(dir, item, dirname)
-- used only to identify the entry into the cache. -- used only to identify the entry into the cache.
local cache_name = item.filename .. (item.topdir and ":" or "") local cache_name = item.filename .. (item.topdir and ":" or "")
local t = dir_cache[cache_name] local t = dir_cache[cache_name]
if not t or t.type ~= item.type then if not t then
t = {} t = {}
local basename = common.basename(item.filename) local basename = common.basename(item.filename)
if item.topdir then if item.topdir then
@ -83,11 +76,10 @@ function TreeView:get_cached(dir, item, dirname)
else else
t.filename = item.filename t.filename = item.filename
t.depth = get_depth(item.filename) t.depth = get_depth(item.filename)
t.abs_filename = common.basepath(dirname) .. item.filename t.abs_filename = dirname .. PATHSEP .. item.filename
end end
t.name = basename t.name = basename
t.type = item.type t.type = item.type
t.dir_name = dir.name -- points to top level "dir" item
dir_cache[cache_name] = t dir_cache[cache_name] = t
end end
return t return t
@ -95,7 +87,7 @@ end
function TreeView:get_name() function TreeView:get_name()
return nil return "Project"
end end
@ -112,13 +104,18 @@ end
function TreeView:check_cache() function TreeView:check_cache()
-- invalidate cache's skip values if project_files has changed
for i = 1, #core.project_directories do for i = 1, #core.project_directories do
local dir = core.project_directories[i] local dir = core.project_directories[i]
-- invalidate cache's skip values if directory is declared dirty local last_files = self.last[dir.name]
if dir.is_dirty and self.cache[dir.name] then if not last_files then
self:invalidate_cache(dir.name) self.last[dir.name] = dir.files
else
if dir.files ~= last_files then
self:invalidate_cache(dir.name)
self.last[dir.name] = dir.files
end
end end
dir.is_dirty = false
end end
end end
@ -134,56 +131,39 @@ function TreeView:each_item()
for k = 1, #core.project_directories do for k = 1, #core.project_directories do
local dir = core.project_directories[k] local dir = core.project_directories[k]
local dir_cached = self:get_cached(dir, dir.item, dir.name) local dir_cached = self:get_cached(dir.item, dir.name)
coroutine.yield(dir_cached, ox, y, w, h) coroutine.yield(dir_cached, ox, y, w, h)
count_lines = count_lines + 1 count_lines = count_lines + 1
y = y + h y = y + h
local i = 1 local i = 1
if dir.files then -- if consumed max sys file descriptors this can be nil while i <= #dir.files and dir_cached.expanded do
while i <= #dir.files and dir_cached.expanded do local item = dir.files[i]
local item = dir.files[i] local cached = self:get_cached(item, dir.name)
local cached = self:get_cached(dir, item, dir.name)
coroutine.yield(cached, ox, y, w, h) coroutine.yield(cached, ox, y, w, h)
count_lines = count_lines + 1 count_lines = count_lines + 1
y = y + h y = y + h
i = i + 1 i = i + 1
if not cached.expanded then if not cached.expanded then
if cached.skip then if cached.skip then
i = cached.skip i = cached.skip
else else
local depth = cached.depth local depth = cached.depth
while i <= #dir.files do while i <= #dir.files do
if get_depth(dir.files[i].filename) <= depth then break end if get_depth(dir.files[i].filename) <= depth then break end
i = i + 1 i = i + 1
end
cached.skip = i
end end
cached.skip = i
end end
end -- while files end
end end -- while files
end -- for directories end -- for directories
self.count_lines = count_lines self.count_lines = count_lines
end) end)
end end
function TreeView:set_selection(selection, selection_y)
self.selected_item = selection
if selection and selection_y
and (selection_y <= 0 or selection_y >= self.size.y) then
local lh = self:get_item_height()
if selection_y >= self.size.y - lh then
selection_y = selection_y - self.size.y + lh
end
local _, y = self:get_content_offset()
self.scroll.to.y = selection and (selection_y - y)
end
end
function TreeView:get_text_bounding_box(item, x, y, w, h) function TreeView:get_text_bounding_box(item, x, y, w, h)
local icon_width = style.icon_font:get_width("D") local icon_width = style.icon_font:get_width("D")
local xoffset = item.depth * style.padding.x + style.padding.x + icon_width local xoffset = item.depth * style.padding.x + style.padding.x + icon_width
@ -194,14 +174,8 @@ end
function TreeView:on_mouse_moved(px, py, ...) function TreeView:on_mouse_moved(px, py, ...)
if not self.visible then return end TreeView.super.on_mouse_moved(self, px, py, ...)
self.cursor_pos.x = px if self.dragging_scrollbar then return end
self.cursor_pos.y = py
if TreeView.super.on_mouse_moved(self, px, py, ...) then
-- mouse movement handled by the View (scrollbar)
self.hovered_item = nil
return
end
local item_changed, tooltip_changed local item_changed, tooltip_changed
for item, x,y,w,h in self:each_item() do for item, x,y,w,h in self:each_item() do
@ -223,6 +197,59 @@ function TreeView:on_mouse_moved(px, py, ...)
end end
local function create_directory_in(item)
local path = item.abs_filename
core.command_view:enter("Create directory in " .. path, function(text)
local dirname = path .. PATHSEP .. text
local success, err = system.mkdir(dirname)
if not success then
core.error("cannot create directory %q: %s", dirname, err)
end
item.expanded = true
core.reschedule_project_scan()
end)
end
function TreeView:on_mouse_pressed(button, x, y, clicks)
local caught = TreeView.super.on_mouse_pressed(self, button, x, y, clicks)
if caught or button ~= "left" then
return
end
local hovered_item = self.hovered_item
if not hovered_item then
return
elseif hovered_item.type == "dir" then
if keymap.modkeys["ctrl"] and button == "left" then
create_directory_in(hovered_item)
else
if core.project_files_limit and not hovered_item.expanded then
local filename, abs_filename = hovered_item.filename, hovered_item.abs_filename
local index = 0
-- The loop below is used to find the first match starting from the end
-- in case there are multiple matches.
while index and index + #filename < #abs_filename do
index = string.find(abs_filename, filename, index + 1, true)
end
-- we assume here index is not nil because the abs_filename must contain the
-- relative filename
local dirname = string.sub(abs_filename, 1, index - 2)
if core.is_project_folder(dirname) then
core.scan_project_folder(dirname, filename)
self:invalidate_cache(dirname)
end
end
hovered_item.expanded = not hovered_item.expanded
end
else
core.try(function()
local doc_filename = core.normalize_to_project_dir(hovered_item.abs_filename)
core.root_view:open_doc(core.open_doc(doc_filename))
end)
end
end
function TreeView:update() function TreeView:update()
-- update width -- update width
local dest = self.visible and self.target_size or 0 local dest = self.visible and self.target_size or 0
@ -230,28 +257,16 @@ function TreeView:update()
self.size.x = dest self.size.x = dest
self.init_size = false self.init_size = false
else else
self:move_towards(self.size, "x", dest, nil, "treeview") self:move_towards(self.size, "x", dest)
end end
if not self.visible then return end
local duration = system.get_time() - self.tooltip.begin local duration = system.get_time() - self.tooltip.begin
if self.hovered_item and self.tooltip.x and duration > tooltip_delay then if self.hovered_item and self.tooltip.x and duration > tooltip_delay then
self:move_towards(self.tooltip, "alpha", tooltip_alpha, tooltip_alpha_rate, "treeview") self:move_towards(self.tooltip, "alpha", tooltip_alpha, tooltip_alpha_rate)
else else
self.tooltip.alpha = 0 self.tooltip.alpha = 0
end end
self.item_icon_width = style.icon_font:get_width("D")
self.item_text_spacing = style.icon_font:get_width("f") / 2
-- this will make sure hovered_item is updated
-- we don't want events when the thing is scrolling fast
local dy = math.abs(self.scroll.to.y - self.scroll.y)
if self.scroll.to.y ~= 0 and dy < self:get_item_height() then
self:on_mouse_moved(self.cursor_pos.x, self.cursor_pos.y, 0, 0)
end
TreeView.super.update(self) TreeView.super.update(self)
end end
@ -280,171 +295,60 @@ function TreeView:draw_tooltip()
end end
function TreeView:get_item_icon(item, active, hovered)
local character = "f"
if item.type == "dir" then
character = item.expanded and "D" or "d"
end
local font = style.icon_font
local color = style.text
if active or hovered then
color = style.accent
end
return character, font, color
end
function TreeView:get_item_text(item, active, hovered)
local text = item.name
local font = style.font
local color = style.text
if active or hovered then
color = style.accent
end
return text, font, color
end
function TreeView:draw_item_text(item, active, hovered, x, y, w, h)
local item_text, item_font, item_color = self:get_item_text(item, active, hovered)
common.draw_text(item_font, item_color, item_text, nil, x, y, 0, h)
end
function TreeView:draw_item_icon(item, active, hovered, x, y, w, h)
local icon_char, icon_font, icon_color = self:get_item_icon(item, active, hovered)
common.draw_text(icon_font, icon_color, icon_char, nil, x, y, 0, h)
return self.item_icon_width + self.item_text_spacing
end
function TreeView:draw_item_body(item, active, hovered, x, y, w, h)
x = x + self:draw_item_icon(item, active, hovered, x, y, w, h)
self:draw_item_text(item, active, hovered, x, y, w, h)
end
function TreeView:draw_item_chevron(item, active, hovered, x, y, w, h)
if item.type == "dir" then
local chevron_icon = item.expanded and "-" or "+"
local chevron_color = hovered and style.accent or style.text
common.draw_text(style.icon_font, chevron_color, chevron_icon, nil, x, y, 0, h)
end
return style.padding.x
end
function TreeView:draw_item_background(item, active, hovered, x, y, w, h)
if hovered then
local hover_color = { table.unpack(style.line_highlight) }
hover_color[4] = 160
renderer.draw_rect(x, y, w, h, hover_color)
elseif active then
renderer.draw_rect(x, y, w, h, style.line_highlight)
end
end
function TreeView:draw_item(item, active, hovered, x, y, w, h)
self:draw_item_background(item, active, hovered, x, y, w, h)
x = x + item.depth * style.padding.x + style.padding.x
x = x + self:draw_item_chevron(item, active, hovered, x, y, w, h)
self:draw_item_body(item, active, hovered, x, y, w, h)
end
function TreeView:draw() function TreeView:draw()
if not self.visible then return end
self:draw_background(style.background2) self:draw_background(style.background2)
local _y, _h = self.position.y, self.size.y
local icon_width = style.icon_font:get_width("D")
local spacing = style.icon_font:get_width("f") / 2
local doc = core.active_view.doc local doc = core.active_view.doc
local active_filename = doc and system.absolute_path(doc.filename or "") local active_filename = doc and system.absolute_path(doc.filename or "")
for item, x,y,w,h in self:each_item() do for item, x,y,w,h in self:each_item() do
if y + h >= _y and y < _y + _h then local color = style.text
self:draw_item(item,
item == self.selected_item, -- highlight active_view doc
item == self.hovered_item, if item.abs_filename == active_filename then
x, y, w, h) color = style.accent
end end
-- hovered item background
if item == self.hovered_item then
renderer.draw_rect(x, y, w, h, style.line_highlight)
color = style.accent
end
-- icons
x = x + item.depth * style.padding.x + style.padding.x
if item.type == "dir" then
local icon1 = item.expanded and "-" or "+"
local icon2 = item.expanded and "D" or "d"
common.draw_text(style.icon_font, color, icon1, nil, x, y, 0, h)
x = x + style.padding.x
common.draw_text(style.icon_font, color, icon2, nil, x, y, 0, h)
x = x + icon_width
else
x = x + style.padding.x
common.draw_text(style.icon_font, color, "f", nil, x, y, 0, h)
x = x + icon_width
end
-- text
x = x + spacing
x = common.draw_text(style.font, color, item.name, nil, x, y, 0, h)
end end
self:draw_scrollbar() self:draw_scrollbar()
if self.hovered_item and self.tooltip.x and self.tooltip.alpha > 0 then if self.hovered_item and self.tooltip.alpha > 0 then
core.root_view:defer_draw(self.draw_tooltip, self) core.root_view:defer_draw(self.draw_tooltip, self)
end end
end end
function TreeView:get_parent(item)
local parent_path = common.dirname(item.abs_filename)
if not parent_path then return end
for it, _, y in self:each_item() do
if it.abs_filename == parent_path then
return it, y
end
end
end
function TreeView:get_item(item, where)
local last_item, last_x, last_y, last_w, last_h
local stop = false
for it, x, y, w, h in self:each_item() do
if not item and where >= 0 then
return it, x, y, w, h
end
if item == it then
if where < 0 and last_item then
break
elseif where == 0 or (where < 0 and not last_item) then
return it, x, y, w, h
end
stop = true
elseif stop then
item = it
return it, x, y, w, h
end
last_item, last_x, last_y, last_w, last_h = it, x, y, w, h
end
return last_item, last_x, last_y, last_w, last_h
end
function TreeView:get_next(item)
return self:get_item(item, 1)
end
function TreeView:get_previous(item)
return self:get_item(item, -1)
end
function TreeView:toggle_expand(toggle)
local item = self.selected_item
if not item then return end
if item.type == "dir" then
if type(toggle) == "boolean" then
item.expanded = toggle
else
item.expanded = not item.expanded
end
local hovered_dir = core.project_dir_by_name(item.dir_name)
if hovered_dir and hovered_dir.files_limit then
core.update_project_subdir(hovered_dir, item.depth == 0 and "" or item.filename, item.expanded)
end
end
end
-- init -- init
local view = TreeView() local view = TreeView()
local node = core.root_view:get_active_node() local node = core.root_view:get_active_node()
view.node = node:split("left", view, {x = true}, true) local treeview_node = node:split("left", view, {x = true}, true)
-- The toolbarview plugin is special because it is plugged inside -- The toolbarview plugin is special because it is plugged inside
-- a treeview pane which is itelf provided in a plugin. -- a treeview pane which is itelf provided in a plugin.
@ -453,12 +357,12 @@ view.node = node:split("left", view, {x = true}, true)
-- plugin module that plug itself in the active node but it is plugged here -- plugin module that plug itself in the active node but it is plugged here
-- in the treeview node. -- in the treeview node.
local toolbar_view = nil local toolbar_view = nil
local toolbar_plugin, ToolbarView = pcall(require, "plugins.toolbarview") local toolbar_plugin, ToolbarView = core.try(require, "plugins.toolbarview")
if config.plugins.toolbarview ~= false and toolbar_plugin then if config.plugins.toolbarview ~= false and toolbar_plugin then
toolbar_view = ToolbarView() toolbar_view = ToolbarView()
view.node:split("down", toolbar_view, {y = true}) treeview_node:split("down", toolbar_view, {y = true})
local min_toolbar_width = toolbar_view:get_min_width() local min_toolbar_width = toolbar_view:get_min_width()
view:set_target_size("x", math.max(config.plugins.treeview.size, min_toolbar_width)) view:set_target_size("x", math.max(default_treeview_size, min_toolbar_width))
command.add(nil, { command.add(nil, {
["toolbar:toggle"] = function() ["toolbar:toggle"] = function()
toolbar_view:toggle_visible() toolbar_view:toggle_visible()
@ -499,38 +403,19 @@ function RootView:draw(...)
menu:draw() menu:draw()
end end
local on_quit_project = core.on_quit_project
function core.on_quit_project()
view.cache = {}
on_quit_project()
end
local function is_project_folder(path) local function is_project_folder(path)
for _,dir in pairs(core.project_directories) do return common.basename(core.project_dir) == path
if dir.name == path then
return true
end
end
return false
end end
local function is_primary_project_folder(path) menu:register(function() return view.hovered_item end, {
return core.project_dir == path
end
local function treeitem() return view.hovered_item or view.selected_item end
menu:register(function() return core.active_view:is(TreeView) and treeitem() end, {
{ text = "Open in System", command = "treeview:open-in-system" }, { text = "Open in System", command = "treeview:open-in-system" },
ContextMenu.DIVIDER ContextMenu.DIVIDER
}) })
menu:register( menu:register(
function() function()
local item = treeitem() return view.hovered_item
return core.active_view:is(TreeView) and item and not is_project_folder(item.abs_filename) and not is_project_folder(view.hovered_item.filename)
end, end,
{ {
{ text = "Rename", command = "treeview:rename" }, { text = "Rename", command = "treeview:rename" },
@ -540,8 +425,7 @@ menu:register(
menu:register( menu:register(
function() function()
local item = treeitem() return view.hovered_item and view.hovered_item.type == "dir"
return core.active_view:is(TreeView) and item and item.type == "dir"
end, end,
{ {
{ text = "New File", command = "treeview:new-file" }, { text = "New File", command = "treeview:new-file" },
@ -549,144 +433,76 @@ menu:register(
} }
) )
menu:register(
function()
local item = treeitem()
return core.active_view:is(TreeView) and item
and not is_primary_project_folder(item.abs_filename)
and is_project_folder(item.abs_filename)
end,
{
{ text = "Remove directory", command = "treeview:remove-project-directory" },
}
)
local previous_view = nil
-- Register the TreeView commands and keymap -- Register the TreeView commands and keymap
command.add(nil, { command.add(nil, {
["treeview:toggle"] = function() ["treeview:toggle"] = function()
view.visible = not view.visible view.visible = not view.visible
end, end})
["treeview:toggle-focus"] = function()
if not core.active_view:is(TreeView) then command.add(function() return view.hovered_item ~= nil end, {
if core.active_view:is(CommandView) then ["treeview:rename"] = function()
previous_view = core.last_active_view local old_filename = view.hovered_item.filename
local old_abs_filename = view.hovered_item.abs_filename
core.command_view:set_text(old_filename)
core.command_view:enter("Rename", function(filename)
filename = core.normalize_to_project_dir(filename)
local abs_filename = core.project_absolute_path(filename)
local res, err = os.rename(old_abs_filename, abs_filename)
if res then -- successfully renamed
for _, doc in ipairs(core.docs) do
if doc.abs_filename and old_abs_filename == doc.abs_filename then
doc:set_filename(filename, abs_filename) -- make doc point to the new filename
doc:reset_syntax()
break -- only first needed
end
end
core.log("Renamed \"%s\" to \"%s\"", old_filename, filename)
else else
previous_view = core.active_view core.error("Error while renaming \"%s\" to \"%s\": %s", old_abs_filename, abs_filename, err)
end
if not previous_view then
previous_view = core.root_view:get_primary_node().active_view
end
core.set_active_view(view)
if not view.selected_item then
for it, _, y in view:each_item() do
view:set_selection(it, y)
break
end
end end
core.reschedule_project_scan()
end, common.path_suggest)
end,
else ["treeview:new-file"] = function()
core.set_active_view( local dir_name = view.hovered_item.filename
previous_view or core.root_view:get_primary_node().active_view if not is_project_folder(dir_name) then
) core.command_view:set_text(dir_name .. "/")
end end
end core.command_view:enter("Filename", function(filename)
}) local doc_filename = core.project_dir .. PATHSEP .. filename
local file = io.open(doc_filename, "a+")
command.add( file:write("")
function() file:close()
return not menu.show_context_menu and core.active_view:extends(TreeView), TreeView core.root_view:open_doc(core.open_doc(doc_filename))
end, { core.reschedule_project_scan()
["treeview:next"] = function() core.log("Created %s", doc_filename)
local item, _, item_y = view:get_next(view.selected_item) end, common.path_suggest)
view:set_selection(item, item_y)
end, end,
["treeview:previous"] = function() ["treeview:new-folder"] = function()
local item, _, item_y = view:get_previous(view.selected_item) local dir_name = view.hovered_item.filename
view:set_selection(item, item_y) if not is_project_folder(dir_name) then
end, core.command_view:set_text(dir_name .. "/")
["treeview:open"] = function()
local item = view.selected_item
if not item then return end
if item.type == "dir" then
view:toggle_expand()
else
core.try(function()
if core.last_active_view and core.active_view == view then
core.set_active_view(core.last_active_view)
end
local doc_filename = core.normalize_to_project_dir(item.abs_filename)
core.root_view:open_doc(core.open_doc(doc_filename))
end)
end end
core.command_view:enter("Folder Name", function(filename)
local dir_path = core.project_dir .. PATHSEP .. filename
common.mkdirp(dir_path)
core.reschedule_project_scan()
core.log("Created %s", dir_path)
end, common.path_suggest)
end, end,
["treeview:deselect"] = function() ["treeview:delete"] = function()
view.selected_item = nil local filename = view.hovered_item.abs_filename
end, local relfilename = view.hovered_item.filename
["treeview:select"] = function()
view:set_selection(view.hovered_item)
end,
["treeview:select-and-open"] = function()
if view.hovered_item then
view:set_selection(view.hovered_item)
command.perform "treeview:open"
end
end,
["treeview:collapse"] = function()
if view.selected_item then
if view.selected_item.type == "dir" and view.selected_item.expanded then
view:toggle_expand(false)
else
local parent_item, y = view:get_parent(view.selected_item)
if parent_item then
view:set_selection(parent_item, y)
end
end
end
end,
["treeview:expand"] = function()
local item = view.selected_item
if not item or item.type ~= "dir" then return end
if item.expanded then
local next_item, _, next_y = view:get_next(item)
if next_item.depth > item.depth then
view:set_selection(next_item, next_y)
end
else
view:toggle_expand(true)
end
end,
})
command.add(
function()
local item = treeitem()
return item ~= nil and (core.active_view == view or menu.show_context_menu), item
end, {
["treeview:delete"] = function(item)
local filename = item.abs_filename
local relfilename = item.filename
if item.dir_name ~= core.project_dir then
-- add secondary project dirs names to the file path to show
relfilename = common.basepath(common.basename(item.dir_name)) .. PATHSEP .. relfilename
end
local file_info = system.get_file_info(filename) local file_info = system.get_file_info(filename)
local file_type = file_info.type == "dir" and "Directory" or "File" local file_type = file_info.type == "dir" and "Directory" or "File"
-- Ask before deleting -- Ask before deleting
local opt = { local opt = {
{ text = "Yes", default_yes = true }, { font = style.font, text = "Yes", default_yes = true },
{ text = "No", default_no = true } { font = style.font, text = "No" , default_no = true }
} }
core.nag_view:show( core.nag_view:show(
string.format("Delete %s", file_type), string.format("Delete %s", file_type),
@ -710,174 +526,27 @@ command.add(
return return
end end
end end
core.reschedule_project_scan()
core.log("Deleted \"%s\"", filename) core.log("Deleted \"%s\"", filename)
end end
end end
) )
end, end,
["treeview:rename"] = function(item) ["treeview:open-in-system"] = function()
local old_filename = item.filename local hovered_item = view.hovered_item
local old_abs_filename = item.abs_filename
core.command_view:enter("Rename", {
text = old_filename,
submit = function(filename)
local abs_filename = filename
if not common.is_absolute_path(filename) then
abs_filename = common.basepath(item.dir_name) .. filename
end
local res, err = os.rename(old_abs_filename, abs_filename)
if res then -- successfully renamed
for _, doc in ipairs(core.docs) do
if doc.abs_filename and old_abs_filename == doc.abs_filename then
doc:set_filename(filename, abs_filename) -- make doc point to the new filename
doc:reset_syntax()
break -- only first needed
end
end
core.log("Renamed \"%s\" to \"%s\"", old_filename, filename)
else
core.error("Error while renaming \"%s\" to \"%s\": %s", old_abs_filename, abs_filename, err)
end
end,
suggest = function(text)
return common.path_suggest(text, item.dir_name)
end
})
end,
["treeview:new-file"] = function(item)
local text
if not is_project_folder(item.abs_filename) then
text = item.filename .. PATHSEP
end
core.command_view:enter("Filename", {
text = text,
submit = function(filename)
local doc_filename = common.basepath(item.dir_name) .. filename
core.log(doc_filename)
local file = io.open(doc_filename, "a+")
file:write("")
file:close()
core.root_view:open_doc(core.open_doc(doc_filename))
core.log("Created %s", doc_filename)
end,
suggest = function(text)
return common.path_suggest(text, item.dir_name)
end
})
end,
["treeview:new-folder"] = function(item)
local text
if not is_project_folder(item.abs_filename) then
text = item.filename .. PATHSEP
end
core.command_view:enter("Folder Name", {
text = text,
submit = function(filename)
local dir_path = common.basepath(item.dir_name) .. filename
common.mkdirp(dir_path)
core.log("Created %s", dir_path)
end,
suggest = function(text)
return common.path_suggest(text, item.dir_name)
end
})
end,
["treeview:open-in-system"] = function(item)
if PLATFORM == "Windows" then if PLATFORM == "Windows" then
system.exec(string.format("start \"\" %q", item.abs_filename)) system.exec(string.format("start \"\" %q", hovered_item.abs_filename))
elseif string.find(PLATFORM, "Mac") or PLATFORM == "MorphOS" then elseif string.find(PLATFORM, "Mac") then
system.exec(string.format("open %q", item.abs_filename)) system.exec(string.format("open %q", hovered_item.abs_filename))
elseif PLATFORM == "Linux" or string.find(PLATFORM, "BSD") then elseif PLATFORM == "Linux" then
system.exec(string.format("xdg-open %q", item.abs_filename)) system.exec(string.format("xdg-open %q", hovered_item.abs_filename))
elseif PLATFORM == "AmigaOS 4" then
system.exec(string.format("WBRUN %q SHOW=all VIEWBY=name", item.abs_filename))
end end
end
})
local projectsearch = pcall(require, "plugins.projectsearch")
if projectsearch then
menu:register(function()
local item = treeitem()
return item and item.type == "dir"
end, {
{ text = "Find in directory", command = "treeview:search-in-directory" }
})
command.add(function()
return view.hovered_item and view.hovered_item.type == "dir"
end, {
["treeview:search-in-directory"] = function(item)
command.perform("project-search:find", view.hovered_item.abs_filename)
end
})
end
command.add(function()
local item = treeitem()
return item
and not is_primary_project_folder(item.abs_filename)
and is_project_folder(item.abs_filename), item
end, {
["treeview:remove-project-directory"] = function(item)
core.remove_project_directory(item.dir_name)
end, end,
}) })
keymap.add { ["ctrl+\\"] = "treeview:toggle" }
keymap.add {
["ctrl+\\"] = "treeview:toggle",
["up"] = "treeview:previous",
["down"] = "treeview:next",
["left"] = "treeview:collapse",
["right"] = "treeview:expand",
["return"] = "treeview:open",
["escape"] = "treeview:deselect",
["delete"] = "treeview:delete",
["ctrl+return"] = "treeview:new-folder",
["lclick"] = "treeview:select-and-open",
["mclick"] = "treeview:select",
["ctrl+lclick"] = "treeview:new-folder"
}
-- The config specification used by gui generators
config.plugins.treeview.config_spec = {
name = "Treeview",
{
label = "Size",
description = "Default treeview width.",
path = "size",
type = "number",
default = toolbar_view and math.ceil(toolbar_view:get_min_width() / SCALE)
or 200 * SCALE,
min = toolbar_view and toolbar_view:get_min_width() / SCALE
or 200 * SCALE,
get_value = function(value)
return value / SCALE
end,
set_value = function(value)
return value * SCALE
end,
on_apply = function(value)
view:set_target_size("x", math.max(
value, toolbar_view and toolbar_view:get_min_width() or 200 * SCALE
))
end
},
{
label = "Hide on Startup",
description = "Show or hide the treeview on startup.",
path = "visible",
type = "toggle",
default = false,
on_apply = function(value)
view.visible = not value
end
}
}
-- Return the treeview with toolbar and contextmenu to allow -- Return the treeview with toolbar and contextmenu to allow
-- user or plugin modifications -- user or plugin modifications

View File

@ -1,53 +1,10 @@
-- mod-version:3 -- mod-version:2 -- lite-xl 2.0
local common = require "core.common" local core = require "core"
local config = require "core.config"
local command = require "core.command" local command = require "core.command"
local Doc = require "core.doc" local Doc = require "core.doc"
---@class config.plugins.trimwhitespace
---@field enabled boolean
---@field trim_empty_end_lines boolean
config.plugins.trimwhitespace = common.merge({
enabled = false,
trim_empty_end_lines = false,
config_spec = {
name = "Trim Whitespace",
{
label = "Enabled",
description = "Disable or enable the trimming of white spaces by default.",
path = "enabled",
type = "toggle",
default = false
},
{
label = "Trim Empty End Lines",
description = "Remove any empty new lines at the end of documents.",
path = "trim_empty_end_lines",
type = "toggle",
default = false
}
}
}, config.plugins.trimwhitespace)
---@class plugins.trimwhitespace local function trim_trailing_whitespace(doc)
local trimwhitespace = {}
---Disable whitespace trimming for a specific document.
---@param doc core.doc
function trimwhitespace.disable(doc)
doc.disable_trim_whitespace = true
end
---Re-enable whitespace trimming if previously disabled.
---@param doc core.doc
function trimwhitespace.enable(doc)
doc.disable_trim_whitespace = nil
end
---Perform whitespace trimming in all lines of a document except the
---line where the caret is currently positioned.
---@param doc core.doc
function trimwhitespace.trim(doc)
local cline, ccol = doc:get_selection() local cline, ccol = doc:get_selection()
for i = 1, #doc.lines do for i = 1, #doc.lines do
local old_text = doc:get_text(i, 1, i, math.huge) local old_text = doc:get_text(i, 1, i, math.huge)
@ -65,54 +22,16 @@ function trimwhitespace.trim(doc)
end end
end end
---Removes all empty new lines at the end of the document.
---@param doc core.doc
---@param raw_remove? boolean Perform the removal not registering to undo stack
function trimwhitespace.trim_empty_end_lines(doc, raw_remove)
for _=#doc.lines, 1, -1 do
local l = #doc.lines
if l > 1 and doc.lines[l] == "\n" then
local current_line = doc:get_selection()
if current_line == l then
doc:set_selection(l-1, math.huge, l-1, math.huge)
end
if not raw_remove then
doc:remove(l-1, math.huge, l, math.huge)
else
table.remove(doc.lines, l)
end
else
break
end
end
end
command.add("core.docview", { command.add("core.docview", {
["trim-whitespace:trim-trailing-whitespace"] = function(dv) ["trim-whitespace:trim-trailing-whitespace"] = function()
trimwhitespace.trim(dv.doc) trim_trailing_whitespace(core.active_view.doc)
end,
["trim-whitespace:trim-empty-end-lines"] = function(dv)
trimwhitespace.trim_empty_end_lines(dv.doc)
end, end,
}) })
local doc_save = Doc.save local save = Doc.save
Doc.save = function(self, ...) Doc.save = function(self, ...)
if trim_trailing_whitespace(self)
config.plugins.trimwhitespace.enabled save(self, ...)
and
not self.disable_trim_whitespace
then
trimwhitespace.trim(self)
if config.plugins.trimwhitespace.trim_empty_end_lines then
trimwhitespace.trim_empty_end_lines(self)
end
end
doc_save(self, ...)
end end
return trimwhitespace

View File

@ -1,4 +1,4 @@
-- mod-version:3 -- 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"
@ -7,7 +7,7 @@ local LogView = require "core.logview"
local function workspace_files_for(project_dir) local function workspace_files_for(project_dir)
local basename = common.basename(project_dir) local basename = common.basename(project_dir)
local workspace_dir = common.basepath(USERDIR) .. "ws" local workspace_dir = USERDIR .. PATHSEP .. "ws"
local info_wsdir = system.get_file_info(workspace_dir) local info_wsdir = system.get_file_info(workspace_dir)
if not info_wsdir then if not info_wsdir then
local ok, err = system.mkdir(workspace_dir) local ok, err = system.mkdir(workspace_dir)
@ -22,7 +22,7 @@ local function workspace_files_for(project_dir)
if file:sub(1, n) == basename then if file:sub(1, n) == basename then
local id = tonumber(file:sub(n + 1):match("^-(%d+)$")) local id = tonumber(file:sub(n + 1):match("^-(%d+)$"))
if id then if id then
coroutine.yield(common.basepath(workspace_dir) .. file, id) coroutine.yield(workspace_dir .. PATHSEP .. file, id)
end end
end end
end end
@ -52,7 +52,7 @@ local function get_workspace_filename(project_dir)
id = id + 1 id = id + 1
end end
local basename = common.basename(project_dir) local basename = common.basename(project_dir)
return common.basepath(USERDIR) .. "ws" .. PATHSEP .. basename .. "-" .. tostring(id) return USERDIR .. PATHSEP .. "ws" .. PATHSEP .. basename .. "-" .. tostring(id)
end end
@ -83,8 +83,7 @@ local function save_view(view)
filename = view.doc.filename, filename = view.doc.filename,
selection = { view.doc:get_selection() }, selection = { view.doc:get_selection() },
scroll = { x = view.scroll.to.x, y = view.scroll.to.y }, scroll = { x = view.scroll.to.x, y = view.scroll.to.y },
crlf = view.doc.crlf, text = not view.doc.filename and view.doc:get_text(1, 1, math.huge, math.huge)
text = view.doc.new_file and view.doc:get_text(1, 1, math.huge, math.huge)
} }
end end
if mt == LogView then return end if mt == LogView then return end
@ -93,8 +92,7 @@ local function save_view(view)
return { return {
type = "view", type = "view",
active = (core.active_view == view), active = (core.active_view == view),
module = name, module = name
scroll = { x = view.scroll.to.x, y = view.scroll.to.y, to = { x = view.scroll.to.x, y = view.scroll.to.y } },
} }
end end
end end
@ -107,6 +105,7 @@ local function load_view(t)
if not t.filename then if not t.filename then
-- document not associated to a file -- document not associated to a file
dv = DocView(core.open_doc()) dv = DocView(core.open_doc())
if t.text then dv.doc:insert(1, 1, t.text) end
else else
-- we have a filename, try to read the file -- we have a filename, try to read the file
local ok, doc = pcall(core.open_doc, t.filename) local ok, doc = pcall(core.open_doc, t.filename)
@ -114,13 +113,11 @@ local function load_view(t)
dv = DocView(doc) dv = DocView(doc)
end end
end end
-- doc view "dv" can be nil here if the filename associated to the document
-- cannot be read.
if dv and dv.doc then if dv and dv.doc then
if dv.doc.new_file and t.text then
dv.doc:insert(1, 1, t.text)
dv.doc.crlf = t.crlf
end
dv.doc:set_selection(table.unpack(t.selection)) dv.doc:set_selection(table.unpack(t.selection))
dv.last_line1, dv.last_col1, dv.last_line2, dv.last_col2 = dv.doc:get_selection() dv.last_line, dv.last_col = dv.doc:get_selection()
dv.scroll.x, dv.scroll.to.x = t.scroll.x, t.scroll.x dv.scroll.x, dv.scroll.to.x = t.scroll.x, t.scroll.x
dv.scroll.y, dv.scroll.to.y = t.scroll.y, t.scroll.y dv.scroll.y, dv.scroll.to.y = t.scroll.y, t.scroll.y
end end
@ -165,9 +162,6 @@ local function load_node(node, t)
if t.active_view == i then if t.active_view == i then
active_view = view active_view = view
end end
if not view:is(DocView) then
view.scroll = v.scroll
end
end end
end end
if active_view then if active_view then

View File

@ -1,70 +0,0 @@
---@meta
---
---Functionality that allows to monitor a directory or file for changes
---using the native facilities provided by the current operating system
---for better efficiency and performance.
---@class dirmonitor
dirmonitor = {}
---@alias dirmonitor.callback fun(fd_or_path:integer|string)
---
---Creates a new dirmonitor object.
---
---@return dirmonitor
function dirmonitor.new() end
---
---Monitors a directory or file for changes.
---
---In "multiple" mode you will need to call this method more than once to
---recursively monitor directories and files.
---
---In "single" mode you will only need to call this method for the parent
---directory and every sub directory and files will get automatically monitored.
---
---@param path string
---
---@return integer fd The file descriptor id assigned to the monitored path when
---the mode is "multiple", in "single" mode: 1 for success or -1 on failure.
function dirmonitor:watch(path) end
---
---Stops monitoring a file descriptor in "multiple" mode
---or in "single" mode a directory path.
---
---@param fd_or_path integer | string A file descriptor or path.
function dirmonitor:unwatch(fd_or_path) end
---
---Verify if the resources registered for monitoring have changed, should
---be called periodically to check for changes.
---
---The callback will be called for each file or directory that was:
---edited, removed or added. A file descriptor will be passed to the
---callback in "multiple" mode or a path in "single" mode.
---
---If an error occurred during the callback execution, the error callback will be called with the error object.
---This callback should not manipulate coroutines to avoid deadlocks.
---
---@param callback dirmonitor.callback
---@param error_callback fun(error: any): nil
---
---@return boolean? changes True when changes were detected.
function dirmonitor:check(callback, error_callback) end
---
---Get the working mode for the current file system monitoring backend.
---
---"multiple": various file descriptors are needed to recursively monitor a
---directory contents, backends: inotify and kqueue.
---
---"single": a single process takes care of monitoring a path recursively
---so no individual file descriptors are used, backends: win32 and fsevents.
---
---@return "single" | "multiple"
function dirmonitor:mode() end
return dirmonitor

View File

@ -4,13 +4,8 @@
---@type table<integer, string> ---@type table<integer, string>
ARGS = {} ARGS = {}
---The current platform tuple used for native modules loading,
---for example: "x86_64-linux", "x86_64-darwin", "x86_64-windows", etc...
---@type string
ARCH = "Architecture-OperatingSystem"
---The current operating system. ---The current operating system.
---@type string | "Windows" | "Mac OS X" | "Linux" | "iOS" | "Android" ---@type string | "'Windows'" | "'Mac OS X'" | "'Linux'" | "'iOS'" | "'Android'"
PLATFORM = "Operating System" PLATFORM = "Operating System"
---The current text or ui scale. ---The current text or ui scale.

View File

@ -57,51 +57,48 @@ process.WAIT_INFINITE = -1
---@type integer ---@type integer
process.WAIT_DEADLINE = -2 process.WAIT_DEADLINE = -2
---Default behavior for redirecting streams. ---Used for the process.options stdin, stdout and stderr fields.
---This flag is deprecated and for backwards compatibility with reproc only.
---The behavior of this flag may change in future versions of Lite XL.
---@type integer ---@type integer
process.REDIRECT_DEFAULT = 0 process.REDIRECT_DEFAULT = 0
---Allow Process API to read this stream via process:read functions. ---Used for the process.options stdin, stdout and stderr fields.
---@type integer ---@type integer
process.REDIRECT_PIPE = 1 process.REDIRECT_PIPE = 1
---Redirect this stream to the parent. ---Used for the process.options stdin, stdout and stderr fields.
---@type integer ---@type integer
process.REDIRECT_PARENT = 2 process.REDIRECT_PARENT = 2
---Discard this stream (piping it to /dev/null) ---Used for the process.options stdin, stdout and stderr fields.
---@type integer ---@type integer
process.REDIRECT_DISCARD = 3 process.REDIRECT_DISCARD = 3
---Redirect this stream to stdout. ---Used for the process.options stdin, stdout and stderr fields.
---This flag can only be used on process.options.stderr.
---@type integer ---@type integer
process.REDIRECT_STDOUT = 4 process.REDIRECT_STDOUT = 4
---@alias process.errortype ---@alias process.errortype
---| `process.ERROR_PIPE` ---|>'process.ERROR_PIPE'
---| `process.ERROR_WOULDBLOCK` ---| 'process.ERROR_WOULDBLOCK'
---| `process.ERROR_TIMEDOUT` ---| 'process.ERROR_TIMEDOUT'
---| `process.ERROR_INVAL` ---| 'process.ERROR_INVAL'
---| `process.ERROR_NOMEM` ---| 'process.ERROR_NOMEM'
---@alias process.streamtype ---@alias process.streamtype
---| `process.STREAM_STDIN` ---|>'process.STREAM_STDIN'
---| `process.STREAM_STDOUT` ---| 'process.STREAM_STDOUT'
---| `process.STREAM_STDERR` ---| 'process.STREAM_STDERR'
---@alias process.waittype ---@alias process.waittype
---| `process.WAIT_INFINITE` ---|>'process.WAIT_INFINITE'
---| `process.WAIT_DEADLINE` ---| 'process.WAIT_DEADLINE'
---@alias process.redirecttype ---@alias process.redirecttype
---| `process.REDIRECT_DEFAULT` ---|>'process.REDIRECT_DEFAULT'
---| `process.REDIRECT_PIPE` ---| 'process.REDIRECT_PIPE'
---| `process.REDIRECT_PARENT` ---| 'process.REDIRECT_PARENT'
---| `process.REDIRECT_DISCARD` ---| 'process.REDIRECT_DISCARD'
---| `process.REDIRECT_STDOUT` ---| 'process.REDIRECT_STDOUT'
--- ---
--- Options that can be passed to process.start() --- Options that can be passed to process.start()
@ -112,6 +109,7 @@ process.REDIRECT_STDOUT = 4
---@field public stdout process.redirecttype ---@field public stdout process.redirecttype
---@field public stderr process.redirecttype ---@field public stderr process.redirecttype
---@field public env table<string, string> ---@field public env table<string, string>
process.options = {}
--- ---
---Create and start a new process ---Create and start a new process
@ -123,7 +121,7 @@ process.REDIRECT_STDOUT = 4
---@return process | nil ---@return process | nil
---@return string errmsg ---@return string errmsg
---@return process.errortype | integer errcode ---@return process.errortype | integer errcode
function process.start(command_and_params, options) end function process:start(command_and_params, options) end
--- ---
---Translates an error code into a useful text message ---Translates an error code into a useful text message
@ -232,6 +230,3 @@ function process:returncode() end
--- ---
---@return boolean ---@return boolean
function process:running() end function process:running() end
return process

View File

@ -31,9 +31,9 @@ regex.NOTEMPTY = 0x00000004
regex.NOTEMPTY_ATSTART = 0x00000008 regex.NOTEMPTY_ATSTART = 0x00000008
---@alias regex.modifiers ---@alias regex.modifiers
---| "i" # Case insesitive matching ---|>'"i"' # Case insesitive matching
---| "m" # Multiline matching ---| '"m"' # Multiline matching
---| "s" # Match all characters with dot (.) metacharacter even new lines ---| '"s"' # Match all characters with dot (.) metacharacter even new lines
--- ---
---Compiles a regular expression pattern that can be used to search in strings. ---Compiles a regular expression pattern that can be used to search in strings.
@ -41,8 +41,8 @@ regex.NOTEMPTY_ATSTART = 0x00000008
---@param pattern string ---@param pattern string
---@param options? regex.modifiers A string of one or more pattern modifiers. ---@param options? regex.modifiers A string of one or more pattern modifiers.
--- ---
---@return regex? regex Ready to use regular expression object or nil on error. ---@return regex|string regex Ready to use regular expression object or error
---@return string? error The error message if compiling the pattern failed. ---message if compiling the pattern failed.
function regex.compile(pattern, options) end function regex.compile(pattern, options) end
--- ---
@ -53,42 +53,5 @@ function regex.compile(pattern, options) end
---@param options? integer A bit field of matching options, eg: ---@param options? integer A bit field of matching options, eg:
---regex.NOTBOL | regex.NOTEMPTY ---regex.NOTBOL | regex.NOTEMPTY
--- ---
---@return integer? ... List of offsets where a match was found. ---@return table<integer, integer> list List of offsets where a match was found.
function regex:cmatch(subject, offset, options) end function regex:cmatch(subject, offset, options) end
---
---Returns an iterator function that, each time it is called, returns the
---next captures from `pattern` over the string subject.
---
---Example:
---```lua
--- s = "hello world hello world"
--- for hello, world in regex.gmatch("(hello)\\s+(world)", s) do
--- print(hello .. " " .. world)
--- end
---```
---
---@param pattern string
---@param subject string
---@param offset? integer
---
---@return fun():string, ...
function regex.gmatch(pattern, subject, offset) end
---
---Replaces the matched pattern globally on the subject with the given
---replacement, supports named captures ((?'name'<pattern>), ${name}) and
---$[1-9][0-9]* substitutions. Raises an error when failing to compile the
---pattern or by a substitution mistake.
---
---@param pattern regex|string
---@param subject string
---@param replacement string
---@param limit? integer Limits the number of substitutions that will be done.
---
---@return string? replaced_subject
---@return integer? total_replacements
function regex.gsub(pattern, subject, replacement, limit) end
return regex

View File

@ -6,25 +6,20 @@
renderer = {} renderer = {}
--- ---
---Array of bytes that represents a color used by the rendering functions. ---Represents a color used by the rendering functions.
---Note: indexes for rgba are numerical 1 = r, 2 = g, 3 = b, 4 = a but for
---documentation purposes the letters r, g, b, a were used.
---@class renderer.color ---@class renderer.color
---@field public r number Red ---@field public r number Red
---@field public g number Green ---@field public g number Green
---@field public b number Blue ---@field public b number Blue
---@field public a number Alpha ---@field public a number Alpha
renderer.color = {}
--- ---
---Represent options that affect a font's rendering. ---Represent options that affect a font's rendering.
---@class renderer.fontoptions ---@class renderer.fontoptions
---@field public antialiasing "none" | "grayscale" | "subpixel" ---@field public antialiasing "'grayscale'" | "'subpixel'"
---@field public hinting "slight" | "none" | "full" ---@field public hinting "'slight'" | "'none'" | '"full"'
---@field public bold boolean renderer.fontoptions = {}
---@field public italic boolean
---@field public underline boolean
---@field public smoothing boolean
---@field public strikethrough boolean
--- ---
---@class renderer.font ---@class renderer.font
@ -35,29 +30,18 @@ renderer.font = {}
--- ---
---@param path string ---@param path string
---@param size number ---@param size number
---@param options? renderer.fontoptions ---@param options renderer.fontoptions
--- ---
---@return renderer.font ---@return renderer.font
function renderer.font.load(path, size, options) end function renderer.font.load(path, size, options) end
---
---Combines an array of fonts into a single one for broader charset support,
---the order of the list determines the fonts precedence when retrieving
---a symbol from it.
---
---@param fonts renderer.font[]
---
---@return renderer.font
function renderer.font.group(fonts) end
--- ---
---Clones a font object into a new one. ---Clones a font object into a new one.
--- ---
---@param size? number Optional new size for cloned font. ---@param size? number Optional new size for cloned font.
---@param options? renderer.fontoptions
--- ---
---@return renderer.font ---@return renderer.font
function renderer.font:copy(size, options) end function renderer.font:copy(size) end
--- ---
---Set the amount of characters that represent a tab. ---Set the amount of characters that represent a tab.
@ -74,6 +58,15 @@ function renderer.font:set_tab_size(chars) end
---@return number ---@return number
function renderer.font:get_width(text) end function renderer.font:get_width(text) end
---
---Get the width in subpixels of the given text when
---rendered with this font.
---
---@param text string
---
---@return number
function renderer.font:get_width_subpixel(text) end
--- ---
---Get the height in pixels that occupies a single character ---Get the height in pixels that occupies a single character
---when rendered with this font. ---when rendered with this font.
@ -81,6 +74,12 @@ function renderer.font:get_width(text) end
---@return number ---@return number
function renderer.font:get_height() end function renderer.font:get_height() end
---
---Gets the font subpixel scale.
---
---@return number
function renderer.font:subpixel_scale() end
--- ---
---Get the current size of the font. ---Get the current size of the font.
--- ---
@ -94,11 +93,23 @@ function renderer.font:get_size() end
function renderer.font:set_size(size) end function renderer.font:set_size(size) end
--- ---
---Get the current path of the font as a string if a single font or as an ---Assistive functionality to replace characters in a
---array of strings if a group font. ---rendered text with other characters.
---@class renderer.replacements
renderer.replacements = {}
--- ---
---@return string | table<integer, string> ---Create a new character replacements object.
function renderer.font:get_path() end ---
---@return renderer.replacements
function renderer.replacements.new() end
---
---Add to internal map a character to character replacement.
---
---@param original_char string Should be a single character like '\t'
---@param replacement_char string Should be a single character like '»'
function renderer.replacements:add(original_char, replacement_char) end
--- ---
---Toggles drawing debugging rectangles on the currently rendered sections ---Toggles drawing debugging rectangles on the currently rendered sections
@ -142,16 +153,29 @@ function renderer.set_clip_rect(x, y, width, height) end
function renderer.draw_rect(x, y, width, height, color) end function renderer.draw_rect(x, y, width, height, color) end
--- ---
---Draw text and return the x coordinate where the text finished drawing. ---Draw text.
--- ---
---@param font renderer.font ---@param font renderer.font
---@param text string ---@param text string
---@param x number ---@param x number
---@param y number ---@param y number
---@param color renderer.color ---@param color renderer.color
---@param replace renderer.replacements
---@param color_replace renderer.color
--- ---
---@return number x ---@return number x_subpixel
function renderer.draw_text(font, text, x, y, color) end function renderer.draw_text(font, text, x, y, color, replace, color_replace) end
---
return renderer ---Draw text at subpixel level.
---
---@param font renderer.font
---@param text string
---@param x number
---@param y number
---@param color renderer.color
---@param replace renderer.replacements
---@param color_replace renderer.color
---
---@return number x_subpixel
function renderer.draw_text_subpixel(font, text, x, y, color, replace, color_replace) end

View File

@ -1,165 +0,0 @@
---@meta
---UTF-8 equivalent of string.byte
---@param s string
---@param i? integer
---@param j? integer
---@return integer
---@return ...
function string.ubyte(s, i, j) end
---UTF-8 equivalent of string.char
---@param byte integer
---@param ... integer
---@return string
---@return ...
function string.uchar(byte, ...) end
---UTF-8 equivalent of string.find
---@param s string
---@param pattern string
---@param init? integer
---@param plain? boolean
---@return integer start
---@return integer end
---@return ... captured
function string.ufind(s, pattern, init, plain) end
---UTF-8 equivalent of string.gmatch
---@param s string
---@param pattern string
---@param init? integer
---@return fun():string, ...
function string.ugmatch(s, pattern, init) end
---UTF-8 equivalent of string.gsub
---@param s string
---@param pattern string
---@param repl string|table|function
---@param n integer
---@return string
---@return integer count
function string.ugsub(s, pattern, repl, n) end
---UTF-8 equivalent of string.len
---@param s string
---@return integer
function string.ulen(s) end
---UTF-8 equivalent of string.lower
---@param s string
---@return string
function string.ulower(s) end
---UTF-8 equivalent of string.match
---@param s string
---@param pattern string
---@param init? integer
---@return string | number captured
function string.umatch(s, pattern, init) end
---UTF-8 equivalent of string.reverse
---@param s string
---@return string
function string.ureverse(s) end
---UTF-8 equivalent of string.sub
---@param s string
---@param i integer
---@param j? integer
---@return string
function string.usub(s, i, j) end
---UTF-8 equivalent of string.upper
---@param s string
---@return string
function string.uupper(s) end
---Equivalent to utf8.escape()
---@param s string
---@return string utf8_string
function string.uescape(s) end
---Equivalent to utf8.charpos()
---@param s string
---@param charpos? integer
---@param index? integer
---@return integer charpos
---@return integer codepoint
function string.ucharpos(s, charpos, index) end
---Equivalent to utf8.next()
---@param s string
---@param charpos? integer
---@param index? integer
---@return integer charpos
---@return integer codepoint
function string.unext(s, charpos, index) end
---Equivalent to utf8.insert()
---@param s string
---@param idx? integer
---@param substring string
---@return string new_string
function string.uinsert(s, idx, substring) end
---Equivalent to utf8.remove()
---@param s string
---@param start? integer
---@param stop? integer
---@return string new_string
function string.uremove(s, start, stop) end
---Equivalent to utf8.width()
---@param s string
---@param ambi_is_double? boolean
---@param default_width? integer
---@return integer width
function string.uwidth(s, ambi_is_double, default_width) end
---Equivalent to utf8.widthindex()
---@param s string
---@param location integer
---@param ambi_is_double? boolean
---@param default_width? integer
---@return integer idx
---@return integer offset
---@return integer width
function string.uwidthindex(s, location, ambi_is_double, default_width) end
---Equivalent to utf8.title()
---@param s string
---@return string new_string
function string.utitle(s) end
---Equivalent to utf8.fold()
---@param s string
---@return string new_string
function string.ufold(s) end
---Equivalent to utf8.ncasecmp()
---@param a string
---@param b string
---@return integer result
function string.uncasecmp(a, b) end
---Equivalent to utf8.offset()
---@param s string
---@param n integer
---@param i? integer
---@return integer position_in_bytes
function string.uoffset(s, n, i) end
---Equivalent to utf8.codepoint()
---@param s string
---@param i? integer
---@param j? integer
---@return integer code
---@return ...
function string.ucodepoint(s, i, j) end
---Equivalent to utf8.codes()
---@param s string
---@return fun():integer, integer
function string.ucodes(s) end

View File

@ -6,15 +6,15 @@
system = {} system = {}
---@alias system.fileinfotype ---@alias system.fileinfotype
---| "file" # It is a file. ---|>'"file"' # It is a file.
---| "dir" # It is a directory. ---| '"dir"' # It is a directory.
--- ---
---@class system.fileinfo ---@class system.fileinfo
---@field public modified number A timestamp in seconds. ---@field public modified number A timestamp in seconds.
---@field public size number Size in bytes. ---@field public size number Size in bytes.
---@field public type system.fileinfotype Type of file ---@field public type system.fileinfotype Type of file
---@field public symlink boolean The directory is a symlink. This field is only set on Linux and on directories. system.fileinfo = {}
--- ---
---Core function used to retrieve the current event been triggered by SDL. ---Core function used to retrieve the current event been triggered by SDL.
@ -24,7 +24,7 @@ system = {}
--- ---
---Window events: ---Window events:
--- * "quit" --- * "quit"
--- * "resized" -> width, height (in points) --- * "resized" -> width, height
--- * "exposed" --- * "exposed"
--- * "minimized" --- * "minimized"
--- * "maximized" --- * "maximized"
@ -38,18 +38,12 @@ system = {}
--- * "keypressed" -> key_name --- * "keypressed" -> key_name
--- * "keyreleased" -> key_name --- * "keyreleased" -> key_name
--- * "textinput" -> text --- * "textinput" -> text
--- * "textediting" -> text, start, length
--- ---
---Mouse events: ---Mouse events:
--- * "mousepressed" -> button_name, x, y, amount_of_clicks --- * "mousepressed" -> button_name, x, y, amount_of_clicks
--- * "mousereleased" -> button_name, x, y --- * "mousereleased" -> button_name, x, y
--- * "mousemoved" -> x, y, relative_x, relative_y --- * "mousemoved" -> x, y, relative_x, relative_y
--- * "mousewheel" -> y, x --- * "mousewheel" -> y
---
---Touch events:
--- * "touchpressed" -> x, y, finger_id
--- * "touchreleased" -> x, y, finger_id
--- * "touchmoved" -> x, y, distance_x, distance_y, finger_id
--- ---
---@return string type ---@return string type
---@return any? arg1 ---@return any? arg1
@ -61,16 +55,16 @@ function system.poll_event() end
--- ---
---Wait until an event is triggered. ---Wait until an event is triggered.
--- ---
---@param timeout? number Amount of seconds, also supports fractions ---@param timeout number Amount of seconds, also supports fractions
---of a second, eg: 0.01. If not provided, waits forever. ---of a second, eg: 0.01
--- ---
---@return boolean status True on success or false if there was an error or if no event was received. ---@return boolean status True on success or false if there was an error.
function system.wait_event(timeout) end function system.wait_event(timeout) end
--- ---
---Change the cursor type displayed on screen. ---Change the cursor type displayed on screen.
--- ---
---@param type string | "arrow" | "ibeam" | "sizeh" | "sizev" | "hand" ---@param type string | "'arrow'" | "'ibeam'" | "'sizeh'" | "'sizev'" | "'hand'"
function system.set_cursor(type) end function system.set_cursor(type) end
--- ---
@ -80,10 +74,10 @@ function system.set_cursor(type) end
function system.set_window_title(title) end function system.set_window_title(title) end
---@alias system.windowmode ---@alias system.windowmode
---| "normal" ---|>'"normal"'
---| "minimized" ---| '"minimized"'
---| "maximized" ---| '"maximized"'
---| "fullscreen" ---| '"fullscreen"'
--- ---
---Change the window mode. ---Change the window mode.
@ -107,12 +101,10 @@ function system.set_window_bordered(bordered) end
---When then window is run borderless (without system decorations), this ---When then window is run borderless (without system decorations), this
---function allows to set the size of the different regions that allow ---function allows to set the size of the different regions that allow
---for custom window management. ---for custom window management.
---To disable custom window management, call this function without any
---arguments
--- ---
---@param title_height? number Height of the window decoration ---@param title_height number
---@param controls_width? number Width of window controls (maximize,minimize and close buttons, etc). ---@param controls_width number This is for minimize, maximize, close, etc...
---@param resize_border? number The amount of pixels reserved for resizing ---@param resize_border number The amount of pixels reserved for resizing
function system.set_window_hit_test(title_height, controls_width, resize_border) end function system.set_window_hit_test(title_height, controls_width, resize_border) end
--- ---
@ -139,30 +131,6 @@ function system.set_window_size(width, height, x, y) end
---@return boolean ---@return boolean
function system.window_has_focus() end function system.window_has_focus() end
---
---Gets the mode of the window.
---
---@return system.windowmode
function system.get_window_mode() end
---
---Sets the position of the IME composition window.
---
---@param x number
---@param y number
---@param width number
---@param height number
function system.set_text_input_rect(x, y, width, height) end
---
---Clears any ongoing composition on the IME
function system.clear_ime() end
---
---Raise the main window and give it input focus.
---Note: may not always be obeyed by the users window manager.
function system.raise_window() end
--- ---
---Opens a message box to display an error message. ---Opens a message box to display an error message.
--- ---
@ -170,14 +138,6 @@ function system.raise_window() end
---@param message string ---@param message string
function system.show_fatal_error(title, message) end function system.show_fatal_error(title, message) end
---
---Deletes an empty directory.
---
---@param path string
---@return boolean success True if the operation suceeded, false otherwise
---@return string? message An error message if the operation failed
function system.rmdir(path) end
--- ---
---Change the current directory path which affects relative file operations. ---Change the current directory path which affects relative file operations.
---This function raises an error if the path doesn't exists. ---This function raises an error if the path doesn't exists.
@ -192,7 +152,6 @@ function system.chdir(path) end
---@param directory_path string ---@param directory_path string
--- ---
---@return boolean created True on success or false on failure. ---@return boolean created True on success or false on failure.
---@return string? message The error message if the operation failed.
function system.mkdir(directory_path) end function system.mkdir(directory_path) end
--- ---
@ -209,7 +168,7 @@ function system.list_dir(path) end
--- ---
---@param path string ---@param path string
--- ---
---@return string? abspath ---@return string
function system.absolute_path(path) end function system.absolute_path(path) end
--- ---
@ -221,28 +180,6 @@ function system.absolute_path(path) end
---@return string? message Error message in case of error. ---@return string? message Error message in case of error.
function system.get_file_info(path) end function system.get_file_info(path) end
---@alias system.fstype
---| "ext2/ext3"
---| "nfs"
---| "fuse"
---| "smb"
---| "smb2"
---| "reiserfs"
---| "tmpfs"
---| "ramfs"
---| "ntfs"
---
---Gets the filesystem type of a path.
---Note: This only works on Linux.
---
---@param path string Can be path to a directory or a file
---
---@return system.fstype
function system.get_fs_type(path) end
--- ---
---Retrieve the text currently stored on the clipboard. ---Retrieve the text currently stored on the clipboard.
--- ---
@ -255,12 +192,6 @@ function system.get_clipboard() end
---@param text string ---@param text string
function system.set_clipboard(text) end function system.set_clipboard(text) end
---
---Get the process id of lite-xl itself.
---
---@return integer
function system.get_process_id() end
--- ---
---Get amount of iterations since the application was launched ---Get amount of iterations since the application was launched
---also known as SDL_GetPerformanceCounter() / SDL_GetPerformanceFrequency() ---also known as SDL_GetPerformanceCounter() / SDL_GetPerformanceFrequency()
@ -278,9 +209,7 @@ function system.sleep(seconds) end
---Similar to os.execute() but does not return the exit status of the ---Similar to os.execute() but does not return the exit status of the
---executed command and executes the process in a non blocking way by ---executed command and executes the process in a non blocking way by
---forking it to the background. ---forking it to the background.
---Note: Do not use this function, use the Process API instead.
--- ---
---@deprecated
---@param command string The command to execute. ---@param command string The command to execute.
function system.exec(command) end function system.exec(command) end
@ -302,27 +231,4 @@ function system.fuzzy_match(haystack, needle, file) end
--- ---
---@param opacity number A value from 0.0 to 1.0, the lower the value ---@param opacity number A value from 0.0 to 1.0, the lower the value
---the less visible the window will be. ---the less visible the window will be.
---@return boolean success True if the operation suceeded.
function system.set_window_opacity(opacity) end function system.set_window_opacity(opacity) end
---
---Loads a lua native module using the default Lua API or lite-xl native plugin API.
---Note: Never use this function directly.
---
---@param name string the name of the module
---@param path string the path to the shared library file
---@return number nargs the return value of the entrypoint
function system.load_native_plugin(name, path) end
---
---Compares two paths in the order used by TreeView.
---
---@param path1 string
---@param type1 system.fileinfotype
---@param path2 string
---@param type2 system.fileinfotype
---@return boolean compare_result True if path1 < path2
function system.path_compare(path1, type1, path2, type2) end
return system

View File

@ -1,194 +0,0 @@
---@meta
---Additional utf8 support not provided by lua.
---@class utf8extra
utf8extra = {}
---UTF-8 equivalent of string.byte
---@param s string
---@param i? integer
---@param j? integer
---@return integer
---@return ...
function utf8extra.byte(s, i, j) end
---UTF-8 equivalent of string.char
---@param byte integer
---@param ... integer
---@return string
---@return ...
function utf8extra.char(byte, ...) end
---UTF-8 equivalent of string.find
---@param s string
---@param pattern string
---@param init? integer
---@param plain? boolean
---@return integer start
---@return integer end
---@return ... captured
function utf8extra.find(s, pattern, init, plain) end
---UTF-8 equivalent of string.gmatch
---@param s string
---@param pattern string
---@param init? integer
---@return fun():string, ...
function utf8extra.gmatch(s, pattern, init) end
---UTF-8 equivalent of string.gsub
---@param s string
---@param pattern string
---@param repl string|table|function
---@param n integer
---@return string
---@return integer count
function utf8extra.gsub(s, pattern, repl, n) end
---UTF-8 equivalent of string.len
---@param s string
---@return integer
function utf8extra.len(s) end
---UTF-8 equivalent of string.lower
---@param s string
---@return string
function utf8extra.lower(s) end
---UTF-8 equivalent of string.match
---@param s string
---@param pattern string
---@param init? integer
---@return string | number captured
function utf8extra.match(s, pattern, init) end
---UTF-8 equivalent of string.reverse
---@param s string
---@return string
function utf8extra.reverse(s) end
---UTF-8 equivalent of string.sub
---@param s string
---@param i integer
---@param j? integer
---@return string
function utf8extra.sub(s, i, j) end
---UTF-8 equivalent of string.upper
---@param s string
---@return string
function utf8extra.upper(s) end
---Escape a str to UTF-8 format string. It support several escape format:
---* %ddd - which ddd is a decimal number at any length: change Unicode code point to UTF-8 format.
---* %{ddd} - same as %nnn but has bracket around.
---* %uddd - same as %ddd, u stands Unicode
---* %u{ddd} - same as %{ddd}
---* %xhhh - hexadigit version of %ddd
---* %x{hhh} same as %xhhh.
---* %? - '?' stands for any other character: escape this character.
---Example:
---```lua
---local u = utf8.escape
---print(u"%123%u123%{123}%u{123}%xABC%x{ABC}")
---print(u"%%123%?%d%%u")
---```
---@param s string
---@return string utf8_string
function utf8extra.escape(s) end
---Convert UTF-8 position to byte offset. if only index is given, return byte
---offset of this UTF-8 char index. if both charpos and index is given, a new
---charpos will be calculated, by add/subtract UTF-8 char index to current
---charpos. in all cases, it returns a new char position, and code point
---(a number) at this position.
---@param s string
---@param charpos? integer
---@param index? integer
---@return integer charpos
---@return integer codepoint
function utf8extra.charpos(s, charpos, index) end
---Iterate though the UTF-8 string s. If only s is given, it can used as a iterator:
---```lua
--- for pos, code in utf8.next, "utf8-string" do
--- -- ...
--- end
---````
---If only charpos is given, return the next byte offset of in string. if
---charpos and index is given, a new charpos will be calculated, by add/subtract
---UTF-8 char offset to current charpos. in all case, it return a new char
---position (in bytes), and code point (a number) at this position.
---@param s string
---@param charpos? integer
---@param index? integer
---@return integer charpos
---@return integer codepoint
function utf8extra.next(s, charpos, index) end
---Insert a substring to s. If idx is given, insert substring before char at
---this index, otherwise substring will concat to s. idx can be negative.
---@param s string
---@param idx? integer
---@param substring string
---@return string new_string
function utf8extra.insert(s, idx, substring) end
---Delete a substring in s. If neither start nor stop is given, delete the last
---UTF-8 char in s, otherwise delete char from start to end of s. if stop is
---given, delete char from start to stop (include start and stop). start and
---stop can be negative.
---@param s string
---@param start? integer
---@param stop? integer
---@return string new_string
function utf8extra.remove(s, start, stop) end
---Calculate the width of UTF-8 string s. if ambi_is_double is given, the
---ambiguous width character's width is 2, otherwise it's 1. fullwidth/doublewidth
---character's width is 2, and other character's width is 1. if default_width is
---given, it will be the width of unprintable character, used display a
---non-character mark for these characters. if s is a code point, return the
---width of this code point.
---@param s string
---@param ambi_is_double? boolean
---@param default_width? integer
---@return integer width
function utf8extra.width(s, ambi_is_double, default_width) end
---Return the character index at given location in string s. this is a reverse
---operation of utf8.width(). this function returns a index of location, and a
---offset in UTF-8 encoding. e.g. if cursor is at the second column (middle)
---of the wide char, offset will be 2. the width of character at idx is
---returned, also.
---@param s string
---@param location integer
---@param ambi_is_double? boolean
---@param default_width? integer
---@return integer idx
---@return integer offset
---@return integer width
function utf8extra.widthindex(s, location, ambi_is_double, default_width) end
---Convert UTF-8 string s to title-case, used to compare by ignore case. if s
---is a number, it's treat as a code point and return a convert code point
---(number). utf8.lower/utf8.pper has the same extension.
---@param s string
---@return string new_string
function utf8extra.title(s) end
---Convert UTF-8 string s to folded case, used to compare by ignore case. if s
---is a number, it's treat as a code point and return a convert code point
---(number). utf8.lower/utf8.pper has the same extension.
---@param s string
---@return string new_string
function utf8extra.fold(s) end
---Compare a and b without case, -1 means a < b, 0 means a == b and 1 means a > b.
---@param a string
---@param b string
---@return integer result
function utf8extra.ncasecmp(a, b) end
return utf8extra

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,198 @@
//----------------------------------------------------------------------------
// 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

@ -0,0 +1,73 @@
// 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

@ -0,0 +1,93 @@
#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

@ -0,0 +1,445 @@
#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 > 0 ? glyph_avg_width * 28 : 28;
// dry run simulating pixel position to estimate required image's height
int x = x_start, y = 0, y_bottom = y;
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);
}
}
}

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