Merge branch 'master' into amiga-2.0

This commit is contained in:
George Sokianos 2022-09-26 17:27:35 +01:00
commit 2bdfd5a694
123 changed files with 14566 additions and 4680 deletions

3
.github/labeler.yml vendored
View File

@ -33,3 +33,6 @@
"Category: C Core": "Category: C Core":
- src/**/* - src/**/*
"Category: Libraries":
- lib/**/*

View File

@ -1,45 +1,20 @@
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-18.04 runs-on: ubuntu-22.04
strategy: strategy:
matrix: matrix:
config: config:
@ -49,103 +24,71 @@ 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)" >> "$GITHUB_ENV" echo "INSTALL_NAME=lite-xl-${GITHUB_REF##*/}-linux-$(uname -m)-portable" >> "$GITHUB_ENV"
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Python Setup - name: Python Setup
uses: actions/setup-python@v2 uses: actions/setup-python@v2
with: with:
python-version: 3.6 python-version: 3.9
- name: Update Packages - name: Update Packages
run: sudo apt-get update run: sudo apt-get update
- name: Install Dependencies - name: Install Dependencies
if: ${{ !startsWith(github.ref, 'refs/tags/') }} run: bash scripts/install-dependencies.sh --debug
run: bash scripts/install-dependencies.sh --debug - name: Build
- name: Install Release Dependencies run: |
if: ${{ startsWith(github.ref, 'refs/tags/') }} bash --version
run: | bash scripts/build.sh --debug --forcefallback --portable
bash scripts/install-dependencies.sh --debug --lhelper - name: Package
bash scripts/lhelper.sh --debug if: ${{ matrix.config.cc == 'gcc' }}
- name: Build run: bash scripts/package.sh --version ${INSTALL_REF} --debug --binary
run: | - name: Upload Artifacts
bash --version uses: actions/upload-artifact@v2
bash scripts/build.sh --debug --forcefallback if: ${{ matrix.config.cc == 'gcc' }}
- name: Package with:
if: ${{ matrix.config.cc == 'gcc' }} name: Linux Artifacts
run: bash scripts/package.sh --version ${INSTALL_REF} --debug --addons --binary path: ${{ env.INSTALL_NAME }}.tar.gz
- 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 (x86_64) name: macOS (x86_64)
runs-on: macos-10.15 runs-on: macos-11
env: env:
CC: clang CC: clang
CXX: clang++ CXX: clang++
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-$(uname -m)" >> "$GITHUB_ENV" echo "INSTALL_NAME=lite-xl-${GITHUB_REF##*/}-macos-$(uname -m)" >> "$GITHUB_ENV"
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Python Setup - name: Python Setup
uses: actions/setup-python@v2 uses: actions/setup-python@v2
with: with:
python-version: 3.9 python-version: 3.9
- name: Install Dependencies - name: Install Dependencies
if: ${{ !startsWith(github.ref, 'refs/tags/') }} run: bash scripts/install-dependencies.sh --debug
run: bash scripts/install-dependencies.sh --debug - name: Build
- name: Install Release Dependencies run: |
if: ${{ startsWith(github.ref, 'refs/tags/') }} bash --version
run: | bash scripts/build.sh --bundle --debug --forcefallback
bash scripts/install-dependencies.sh --debug --lhelper - name: Create DMG Image
bash scripts/lhelper.sh --debug run: bash scripts/package.sh --version ${INSTALL_REF} --debug --dmg
- name: Build - name: Upload DMG Image
run: | uses: actions/upload-artifact@v2
bash --version with:
bash scripts/build.sh --bundle --debug --forcefallback name: macOS DMG Image
- name: Error Logs path: ${{ env.INSTALL_NAME }}.dmg
if: failure()
run: |
mkdir ${INSTALL_NAME}
cp /usr/var/lhenv/lite-xl/logs/* ${INSTALL_NAME}
tar czvf ${INSTALL_NAME}.tar.gz ${INSTALL_NAME}
# - name: Package
# if: ${{ !startsWith(github.ref, 'refs/tags/') }}
# run: bash scripts/package.sh --version ${INSTALL_REF} --debug --addons
- name: Create DMG Image
run: bash scripts/package.sh --version ${INSTALL_REF} --debug --addons --dmg
- name: Upload DMG Image
uses: actions/upload-artifact@v2
with:
name: macOS DMG Image
path: ${{ env.INSTALL_NAME }}.dmg
- name: Upload Error Logs
uses: actions/upload-artifact@v2
if: failure()
with:
name: Error Logs
path: ${{ env.INSTALL_NAME }}.tar.gz
build_windows_msys2: build_windows_msys2:
name: Windows name: Windows
@ -160,7 +103,6 @@ jobs:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: msys2/setup-msys2@v2 - uses: msys2/setup-msys2@v2
with: with:
#msystem: MINGW64
msystem: ${{ matrix.msystem }} msystem: ${{ matrix.msystem }}
update: true update: true
install: >- install: >-
@ -170,83 +112,22 @@ jobs:
- 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: ${{ !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 --debug --forcefallback bash scripts/build.sh -U --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 --addons --binary run: bash scripts/package.sh --version ${INSTALL_REF} --debug --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@v2 uses: actions/upload-artifact@v2
with: with:
name: Windows Artifacts name: Windows Artifacts
path: | path: ${{ env.INSTALL_NAME }}.zip
LiteXL*.exe
${{ env.INSTALL_NAME }}.zip
- name: Upload Error Logs
uses: actions/upload-artifact@v2
if: failure()
with:
name: Error Logs
path: ${{ env.INSTALL_NAME }}.tar.gz
deploy:
name: Deployment
runs-on: ubuntu-18.04
# if: startsWith(github.ref, 'refs/tags/')
if: false
needs:
- archive_source_code
- build_linux
- build_macos
- build_windows_msys2
steps:
- name: Set Environment Variables
run: echo "INSTALL_REF=${GITHUB_REF##*/}" >> "$GITHUB_ENV"
- uses: actions/download-artifact@v2
with:
name: Linux Artifacts
- uses: actions/download-artifact@v2
with:
name: macOS DMG Image
- uses: actions/download-artifact@v2
with:
name: Source Code Tarball
- uses: actions/download-artifact@v2
with:
name: Windows Artifacts
- name: Display File Information
shell: bash
run: ls -lR
# Note: not using `actions/create-release@v1`
# because it cannot update an existing release
# see https://github.com/actions/create-release/issues/29
- uses: softprops/action-gh-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ env.INSTALL_REF }}
name: Release ${{ env.INSTALL_REF }}
draft: false
prerelease: false
files: |
lite-xl-${{ env.INSTALL_REF }}-*
LiteXL*.AppImage
LiteXL*.exe

183
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,183 @@
name: Release
on:
workflow_dispatch:
inputs:
version:
description: Release Version
default: v2.1.0
required: true
jobs:
release:
name: Create Release
runs-on: ubuntu-20.04
outputs:
upload_url: ${{ steps.create_release.outputs.upload_url }}
version: ${{ steps.tag.outputs.version }}
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Fetch Version
id: tag
run: |
if [[ "${{ github.event.inputs.version }}" != "" ]]; then
echo ::set-output name=version::${{ github.event.inputs.version }}
else
echo ::set-output name=version::${GITHUB_REF/refs\/tags\//}
fi
- name: Create Release
id: create_release
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ steps.tag.outputs.version }}
name: Lite XL ${{ steps.tag.outputs.version }}
draft: true
prerelease: false
body_path: changelog.md
generate_release_notes: true
build_linux:
name: Linux
needs: release
runs-on: ubuntu-20.04
env:
CC: gcc
CXX: g++
steps:
- name: Set Environment Variables
run: |
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
echo "INSTALL_REF=${{ needs.release.outputs.version }}" >> "$GITHUB_ENV"
- uses: actions/checkout@v2
- name: Python Setup
uses: actions/setup-python@v2
with:
python-version: 3.9
- name: Update Packages
run: sudo apt-get update
- name: Install Dependencies
run: |
bash scripts/install-dependencies.sh --debug
sudo apt-get install -y ccache
- name: Build Portable
run: |
bash --version
bash scripts/build.sh --debug --forcefallback --portable --release
- name: Package Portables
run: |
bash scripts/package.sh --version ${INSTALL_REF} --debug --binary --release
bash scripts/package.sh --version ${INSTALL_REF} --debug --addons --binary --release
- name: Build AppImages
run: |
bash scripts/appimage.sh --debug --static --version ${INSTALL_REF} --release
bash scripts/appimage.sh --debug --nobuild --addons --version ${INSTALL_REF}
- name: Upload Files
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ needs.release.outputs.version }}
files: |
lite-xl-${{ env.INSTALL_REF }}-linux-x86_64-portable.tar.gz
lite-xl-${{ env.INSTALL_REF }}-addons-linux-x86_64-portable.tar.gz
LiteXL-${{ env.INSTALL_REF }}-x86_64.AppImage
LiteXL-${{ env.INSTALL_REF }}-addons-x86_64.AppImage
build_macos:
name: macOS (x86_64)
needs: release
runs-on: macos-11
env:
CC: clang
CXX: clang++
steps:
- name: System Information
run: |
system_profiler SPSoftwareDataType
bash --version
gcc -v
xcodebuild -version
- name: Set Environment Variables
run: |
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
echo "INSTALL_REF=${{ needs.release.outputs.version }}" >> "$GITHUB_ENV"
echo "INSTALL_NAME=lite-xl-${{ needs.release.outputs.version }}-macos-$(uname -m)" >> "$GITHUB_ENV"
echo "INSTALL_NAME_ADDONS=lite-xl-${{ needs.release.outputs.version }}-addons-macos-$(uname -m)" >> "$GITHUB_ENV"
- uses: actions/checkout@v2
- name: Python Setup
uses: actions/setup-python@v2
with:
python-version: 3.9
- name: Install Dependencies
run: bash scripts/install-dependencies.sh --debug
- name: Build
run: |
bash --version
bash scripts/build.sh --bundle --debug --forcefallback --release
- name: Create DMG Image
run: |
bash scripts/package.sh --version ${INSTALL_REF} --debug --dmg --release
bash scripts/package.sh --version ${INSTALL_REF} --debug --addons --dmg --release
- name: Upload Files
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ needs.release.outputs.version }}
files: |
${{ env.INSTALL_NAME }}.dmg
${{ env.INSTALL_NAME_ADDONS }}.dmg
build_windows_msys2:
name: Windows
needs: release
runs-on: windows-2019
strategy:
matrix:
msystem: [MINGW32, MINGW64]
defaults:
run:
shell: msys2 {0}
steps:
- uses: actions/checkout@v2
- uses: msys2/setup-msys2@v2
with:
msystem: ${{ matrix.msystem }}
update: true
install: >-
base-devel
git
zip
- name: Set Environment Variables
run: |
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
echo "INSTALL_REF=${{ needs.release.outputs.version }}" >> "$GITHUB_ENV"
if [[ "${MSYSTEM}" == "MINGW64" ]]; then
echo "BUILD_ARCH=x86_64" >> "$GITHUB_ENV"
echo "INSTALL_NAME=lite-xl-${{ needs.release.outputs.version }}-windows-x86_64" >> "$GITHUB_ENV"
echo "INSTALL_NAME_ADDONS=lite-xl-${{ needs.release.outputs.version }}-addons-windows-x86_64" >> "$GITHUB_ENV"
else
echo "BUILD_ARCH=i686" >> "$GITHUB_ENV"
echo "INSTALL_NAME=lite-xl-${{ needs.release.outputs.version }}-windows-i686" >> "$GITHUB_ENV"
echo "INSTALL_NAME_ADDONS=lite-xl-${{ needs.release.outputs.version }}-addons-windows-i686" >> "$GITHUB_ENV"
fi
- name: Install Dependencies
run: bash scripts/install-dependencies.sh --debug
- name: Build
run: |
bash --version
bash scripts/build.sh -U --debug --forcefallback --release
- name: Package
run: bash scripts/package.sh --version ${INSTALL_REF} --debug --binary --release
- name: Build Installer
run: bash scripts/innosetup/innosetup.sh --debug --version ${INSTALL_REF}
- name: Package With Addons
run: bash scripts/package.sh --version ${INSTALL_REF} --debug --addons --binary --release
- name: Build Installer With Addons
run: bash scripts/innosetup/innosetup.sh --debug --version ${INSTALL_REF} --addons
- name: Upload Files
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ needs.release.outputs.version }}
files: |
${{ env.INSTALL_NAME }}.zip
${{ env.INSTALL_NAME_ADDONS }}.zip
LiteXL-${{ env.INSTALL_REF }}-${{ env.BUILD_ARCH }}-setup.exe
LiteXL-${{ env.INSTALL_REF }}-addons-${{ env.BUILD_ARCH }}-setup.exe

7
.gitignore vendored
View File

@ -2,9 +2,10 @@ build*/
.build*/ .build*/
lhelper/ lhelper/
submodules/ submodules/
subprojects/lua/ subprojects/*/
subprojects/reproc/
/appimage* /appimage*
.vscode
.cache
.ccls-cache .ccls-cache
.lite-debug.log .lite-debug.log
.run* .run*
@ -25,3 +26,5 @@ release_files
*.o *.o
*.snalyzerinfo *.snalyzerinfo
!resources/windows/*.diff

View File

@ -1,4 +1,4 @@
Copyright (c) 2020-2021 Francesco Abbate Copyright (c) 2020-2021 Lite XL Team
Permission is hereby granted, free of charge, to any person obtaining a copy of 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

@ -27,7 +27,7 @@ The changes and differences between Lite XL and rxi/lite are listed in the
## 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.
@ -148,12 +148,13 @@ See the [licenses] file for details on licenses used by the required dependencie
[screenshot-dark]: https://user-images.githubusercontent.com/433545/111063905-66943980-84b1-11eb-9040-3876f1133b20.png [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.com
[build]: https://lite-xl.com/en/documentation/build/ [build]: https://lite-xl.com/en/documentation/build
[Get Lite XL]: https://github.com/lite-xl/lite-xl/releases/latest [Get Lite XL]: https://github.com/lite-xl/lite-xl/releases/latest
[Get plugins]: https://github.com/lite-xl/lite-xl-plugins [Get plugins]: https://github.com/lite-xl/lite-xl-plugins
[Get color themes]: https://github.com/lite-xl/lite-xl-colors [Get color themes]: https://github.com/lite-xl/lite-xl-colors
[changelog]: https://github.com/lite-xl/lite-xl/blob/master/changelog.md [changelog]: https://github.com/lite-xl/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/lite-xl/lite-xl-plugins
[plugins repository]: https://github.com/rxi/lite-plugins
[colors repository]: https://github.com/lite-xl/lite-xl-colors [colors repository]: https://github.com/lite-xl/lite-xl-colors
[LICENSE]: LICENSE [LICENSE]: LICENSE
[licenses]: licenses/licenses.md [licenses]: licenses/licenses.md

View File

@ -37,6 +37,7 @@ show_help() {
echo "-D --dmg Create a DMG disk image (macOS only)." echo "-D --dmg Create a DMG disk image (macOS only)."
echo " Requires NPM and AppDMG." 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
@ -58,6 +59,7 @@ main() {
local innosetup local innosetup
local portable local portable
local pgo local pgo
local release
for i in "$@"; do for i in "$@"; do
case $i in case $i in
@ -109,6 +111,10 @@ main() {
portable="--portable" portable="--portable"
shift shift
;; ;;
-r|--release)
release="--release"
shift
;;
-S|--source) -S|--source)
source="--source" source="--source"
shift shift
@ -145,6 +151,7 @@ main() {
$force_fallback \ $force_fallback \
$bundle \ $bundle \
$portable \ $portable \
$release \
$pgo $pgo
source scripts/package.sh \ source scripts/package.sh \
@ -158,6 +165,7 @@ main() {
$appimage \ $appimage \
$dmg \ $dmg \
$innosetup \ $innosetup \
$release \
$source $source
} }

8
build.lhelper Normal file
View File

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

View File

@ -1,20 +1,405 @@
This files document the changes done in Lite XL for each release. # Changes Log
### 2.0.3 ## [2.1.0] - 2022-09-25
Replace periodic rescan of project folder with a notification based system using the ### New Features
[dmon library](https://github.com/septag/dmon). Improves performance especially for * Make distinction between
large project folders since the application no longer needs to rescan. [line and block comments](https://github.com/lite-xl/lite-xl/pull/771),
The application also reports immediatly any change in the project directory even and added all appropriate functionality to the commenting/uncommenting lines.
when the application is unfocused.
* [Added in line paste mode](https://github.com/lite-xl/lite-xl/pull/713),
if you copy without a selection.
* Many [improvements to treeview](https://github.com/lite-xl/lite-xl/pull/732),
including keyboard navigation of treeview, and ability to specify single vs.
double-click behavior.
* Added in [soft line wrapping](https://github.com/lite-xl/lite-xl/pull/636)
as core plugin, under `linewrapping.lua`, use `F10` to activate.
* Revamped [StatusView](https://github.com/lite-xl/lite-xl/pull/852) API with
new features that include:
* Support for predicates, click actions, tooltips on item hover
and custom drawing of added items.
* Hide items that are too huge by rendering with clip_rect.
* Ability to drag or scroll the left or right if too many items to display.
* New status bar commands accessible from the command palette that
include: toggling status bar visibility, toggling specific item visibility,
enable/disable status messages, etc...
* Added `renderer.font.group` interface to set up
[font fallback groups](https://github.com/lite-xl/lite-xl/pull/616) in
the font renderer, if a token doesn't have a corresponding glyph.
**Example:**
```lua
local emoji_font = renderer.font.load(USERDIR .. "/fonts/NotoEmoji-Regular.ttf", 15 * SCALE)
local nonicons = renderer.font.load(USERDIR .. "/fonts/nonicons.ttf", 15 * SCALE)
style.code_font = renderer.font.group({style.code_font, nonicons, emoji_font})
```
* Added in the ability to specify
[mouse clicks](https://github.com/lite-xl/lite-xl/pull/589) in the
keymap, allowing for easy binds of `ctrl+lclick`, and the like.
**Example:**
```lua
keymap.add { ["ctrl+shift+3lclick"] = "core:open-log" }
```
* Improved ability for plugins to be loaded at a given time, by making the
convention of defining a config for the plugin using `common.merge` to merge
existing hashes together, rather than overwriting.
* Releases will now include all language plugins and the
[settings gui](https://github.com/lite-xl/lite-xl-plugins/pull/65) plugin.
* New [core.warn](https://github.com/lite-xl/lite-xl/pull/1005) was introduced.
* Added [suggestions warping](https://github.com/lite-xl/lite-xl/pull/1003)
for `CommandView`.
* Allow regexes in tokenizer to
[split tokens with group](https://github.com/lite-xl/lite-xl/pull/999).
* Added [settings gui support](https://github.com/lite-xl/lite-xl/pull/995)
to core plugins.
* Support for [stricter predicates](https://github.com/lite-xl/lite-xl/pull/990)
by appending a `!`, eg: `"core.docview!"`.
* [UTF8 support in tokenizer](https://github.com/lite-xl/lite-xl/pull/945)
and new utf8 counter parts of string functions,
eg: `string.ulen`, `string.ulower`, etc...
* Added [utf8 support](https://github.com/lite-xl/lite-xl/pull/986) on doc
lower and upper commands.
* Allow syntax patterns to match with the
[beginning of the line](https://github.com/lite-xl/lite-xl/pull/860).
**Example:**
```lua
{ pattern = "^my_pattern_starting_at_beginning", type="symbol" }
```
* [Add View:on_file_dropped](https://github.com/lite-xl/lite-xl/pull/845).
* Implemented new function to retrieve current process id of lite-xl
[system.get_process_id()](https://github.com/lite-xl/lite-xl/pull/833).
* [Allow functions in keymap](https://github.com/lite-xl/lite-xl/pull/948).
* [Add type ahead to CommandView](https://github.com/lite-xl/lite-xl/pull/963).
* Add syntax symbols to
[auto-complete](https://github.com/lite-xl/lite-xl/pull/913).
* Add [animation categories](https://github.com/lite-xl/lite-xl/pull/941)
to enable finer transitions control.
* Added in a [native plugin](https://github.com/lite-xl/lite-xl/pull/527)
interface that allows for C-level interfacing with a statically-linked
lite-xl. The implementation of this may change in future.
* Config: added new development option to prevent plugin version checking at
startup named [skip_plugins_version](https://github.com/lite-xl/lite-xl/pull/879)
* Added a smoothing and strikethrough option to font loading
([#1087](https://github.com/lite-xl/lite-xl/pull/1087))
* Allow command predicates to manage parameters, allow overwriting commands
([#1098](https://github.com/lite-xl/lite-xl/pull/1098))
* Added in simple directory search to treeview.
([#1110](https://github.com/lite-xl/lite-xl/pull/1110))
* Added in native modules suffixes.
([#1111](https://github.com/lite-xl/lite-xl/pull/1111))
* plugin scale: added option to set default scale
([#1115](https://github.com/lite-xl/lite-xl/pull/1115))
* Added in ability to have init.so as a require for cpath.
([#1126](https://github.com/lite-xl/lite-xl/pull/1126))
### Performance Improvements
* [Load space metrics only when creating font](https://github.com/lite-xl/lite-xl/pull/1032)
* [Performance improvement](https://github.com/lite-xl/lite-xl/pull/883)
of detect indent plugin.
* Improve performance of
[ren_draw_rect](https://github.com/lite-xl/lite-xl/pull/935).
* Improved [tokenizer performance](https://github.com/lite-xl/lite-xl/pull/896).
* drawwhitespace: [Cache whitespace location](https://github.com/lite-xl/lite-xl/pull/1030)
* CommandView: improve performance by
[only drawing visible](https://github.com/lite-xl/lite-xl/pull/1047)
### Backward Incompatible Changes
* [Upgraded Lua to 5.4](https://github.com/lite-xl/lite-xl/pull/781), which
should improve performance, and provide useful extra functionality. It should
also be more available out of the box with most modern
linux/unix-based package managers.
* Bumped plugin mod-version number as various interfaces like: `DocView`,
`StatusView` and `CommandView` have changed which should require a revision
from plugin developers to make sure their plugins work with this new release.
* Changed interface for key handling; now, all components should return true if
they've handled the event.
* For plugin developers, declaring config options by directly assigning
to the plugin table (eg: `config.plugins.plugin_name.myvalue = 10`) was
deprecated in favor of using `common.merge`.
**Example:**
```lua
config.plugins.autowrap = common.merge({
enabled = false,
files = { "%.md$", "%.txt$" }
}, config.plugins.autowrap)
```
* `DocView:draw_text_line` and related functions been used by plugin developers
require a revision, since some of this interfaces were updated to support
line wrapping.
* Removed `cp_replace`, and replaced this with a core plugin,
[drawwhitespace.lua](https://github.com/lite-xl/lite-xl/pull/908).
### Deprecated Features
* For plugins the usage of the `--lite-xl` version tag was dropped
in favor of `--mod-version`.
* Overriding `StatusView:get_items()` has been deprecated in favor of
the new dedicated interface to insert status bar items:
**New Interface:**
```lua
------@return StatusView.Item
function StatusView:add_item(
{ predicate, name, alignment, get_item, command, position, tooltip, separator }
) end
```
**Example:**
```lua
core.status_view:add_item({
predicate = nil,
name = "status:memory-usage",
alignment = StatusView.Item.RIGHT,
get_item = function()
return {
style.text,
string.format(
"%.2f MB",
(math.floor(collectgarbage("count") / 10.24) / 100)
)
}
end,
command = nil,
position = 1,
tooltip = "lua memory usage",
separator = core.status_view.separator2
})
```
* [CommandView:enter](https://github.com/lite-xl/lite-xl/pull/1004) now accepts
a single options table as a parameter, meaning that the old way of calling
this function will now show a deprecation message. Also `CommandView:set_text`
and `CommandView:set_hidden_suggestions` has been
[deprecated](https://github.com/lite-xl/lite-xl/pull/1014).
**Example:**
```lua
core.command_view:enter("Title", {
submit = function() end,
suggest = function() return end,
cancel = function() end,
validate = function() return true end,
text = "",
select_text = false,
show_suggestions = true,
typeahead = true,
wrap = true
})
```
### Other Changes
* Removed `dmon`, and implemented independent backends for dirmonitoring. Also
more cleanly split out dirmonitoring into its own class in lua, from core.init.
We should now support FreeBSD; and any other system that uses `kqueue` as
their dir monitoring library. We also have a dummy-backend, which reverts
transparently to scanning if there is some issue with applying OS-level
watches (such as system limits).
* Removed `libagg` and the font renderer; compacted all font rendering into a
single renderer.c file which uses `libfreetype` directly. Now allows for ad-hoc
bolding, italics, and underlining of fonts.
* Removed `reproc` and replaced this with a simple POSIX/Windows implementation
in `process.c`. This allows for greater tweakability (i.e. we can now `break`
for debugging purposes), performance (startup time of subprocesses is
noticeably shorter), and simplicity (we no longer have to link reproc, or
winsock, on windows).
* [Split out `Node` and `EmptyView`](https://github.com/lite-xl/lite-xl/pull/715)
into their own lua files, for plugin extensibility reasons.
* Improved fuzzy_matching to probably give you something closer to what you're
looking for.
* Improved handling of alternate keyboard layouts.
* Added in a default keymap for `core:restart`, `ctrl+shift+r`.
* Improvements to the [C and C++](https://github.com/lite-xl/lite-xl/pull/875)
syntax files.
* Improvements to [markdown](https://github.com/lite-xl/lite-xl/pull/862)
syntax file.
* [Improvements to borderless](https://github.com/lite-xl/lite-xl/pull/994)
mode on Windows.
* Fixed a bunch of problems relating to
[multi-cursor](https://github.com/lite-xl/lite-xl/pull/886).
* NagView: [support vscroll](https://github.com/lite-xl/lite-xl/pull/876) when
message is too long.
* Meson improvements which include:
* Added in meson wraps for freetype, pcre2, and SDL2 which target public,
rather than lite-xl maintained repos.
* [Seperate dirmonitor logic](https://github.com/lite-xl/lite-xl/pull/866),
add build time detection of features.
* Add [fallbacks](https://github.com/lite-xl/lite-xl/pull/798) to all
common dependencies.
* [Update SDL to 2.0.20](https://github.com/lite-xl/lite-xl/pull/884).
* install [docs/api](https://github.com/lite-xl/lite-xl/pull/979) to datadir
for lsp support.
* Always check if the beginning of the
[text needs to be clipped](https://github.com/lite-xl/lite-xl/pull/871).
* Added [git commit](https://github.com/lite-xl/lite-xl/pull/859)
on development builds.
* Update [autocomplete](https://github.com/lite-xl/lite-xl/pull/832)
with changes needed for latest LSP plugin.
* Use SDL to manage color format mapping in
[ren_draw_rect](https://github.com/lite-xl/lite-xl/pull/829).
* Various code [clean ups](https://github.com/lite-xl/lite-xl/pull/826).
* [Autoreload Nagview](https://github.com/lite-xl/lite-xl/pull/942).
* [Enhancements to scrollbar](https://github.com/lite-xl/lite-xl/pull/916).
* Set the correct working directory for the
[AppImage version](https://github.com/lite-xl/lite-xl/pull/937).
* Core: fixes and changes to
[temp file](https://github.com/lite-xl/lite-xl/pull/906) functions.
* [Added plugin load-time log](https://github.com/lite-xl/lite-xl/pull/966).
* TreeView improvements for
[multi-project](https://github.com/lite-xl/lite-xl/pull/1010).
* Open LogView on user/project
[module reload error](https://github.com/lite-xl/lite-xl/pull/1022).
* Check if ["open" pattern is escaped](https://github.com/lite-xl/lite-xl/pull/1034)
* Support [UTF-8 on Windows](https://github.com/lite-xl/lite-xl/pull/1041) (Lua)
* Make system.* functions support
[UTF8 filenames on windows](https://github.com/lite-xl/lite-xl/pull/1042)
* [Fix memory leak](https://github.com/lite-xl/lite-xl/pull/1039) and wrong
check in font_retrieve
* Many, many, many more changes that are too numerous to list.
* CommandView: do not change caret size with config.line_height
([#1080](https://github.com/lite-xl/lite-xl/pull/1080))
## [2.0.5] - 2022-01-29
Revamp the project's user module so that modifications are immediately applied.
Add a mechanism to ignore files or directory based on their project's path.
The new mechanism is backward compatible.*
Essentially there are two mechanisms:
- if a '/' or a '/$' appear at the end of the pattern it will match only
directories
- if a '/' appears anywhere in the pattern except at the end the pattern will
be applied to the path
In the first case, when the pattern corresponds to a directory, a '/' will be
appended to the name of each directory before checking the pattern.
In the second case, when the pattern corresponds to a path, the complete path of
the file or directory will be used with an initial '/' added to the path.
Fix several problems with the directory monitoring library.
Now the application should no longer assert when some related system call fails
and we fallback to rescan when an error happens.
On linux no longer use the recursive monitoring which was a source of problem.
Directory monitoring is now aware of symlinks and treat them appropriately.
Fix problem when encountering special files type on linux.
Improve directory monitoring so that the related thread actually waits without
using any CPU time when there are no events.
Improve the suggestion when changing project folder or opening a new one.
Now the previously used directory are suggested but if the path is changed the
actual existing directories that match the pattern are suggested.
In addition always use the text entered in the command view even if a suggested
entry is highlighted.
The NagView warning window now no longer moves the document content.
## [2.0.4] - 2021-12-20
Fix some bugs related to newly introduced directory monitoring using the
dmon library.
Fix a problem with plain text search using Lua patterns by error.
Fix a problem with visualization of UTF-8 characters that caused garbage
characters visualization.
Other fixes and improvements contributed by @Guldoman.
## [2.0.3] - 2021-10-23
Replace periodic rescan of project folder with a notification based system
using the [dmon library](https://github.com/septag/dmon). Improves performance
especially for large project folders since the application no longer needs to
rescan. The application also reports immediately any change in the project
directory even when the application is unfocused.
Improved find-replace reverse and forward search. Improved find-replace reverse and forward search.
Fixed a bug in incremental syntax highlighting affecting documents with multiple-lines Fixed a bug in incremental syntax highlighting affecting documents with
comments or strings. multiple-lines comments or strings.
The application now always shows the tabs in the documents' view even when a single The application now always shows the tabs in the documents' view even when
document is opened. Can be changed with the option `config.always_show_tabs`. a single document is opened. Can be changed with the option
`config.always_show_tabs`.
Fix problem with numeric keypad function keys not properly working. Fix problem with numeric keypad function keys not properly working.
@ -22,32 +407,36 @@ Fix problem with pixel not correctly drawn at the window's right edge.
Treat correctly and open network paths on Windows. Treat correctly and open network paths on Windows.
Add some improvements for very slow network filesystems. Add some improvements for very slow network file systems.
Fix problem with python syntax highliting, contributed by @dflock. Fix problem with python syntax highlighting, contributed by @dflock.
### 2.0.2 ## [2.0.2] - 2021-09-10
Fix problem project directory when starting the application from Launcher on macOS. Fix problem project directory when starting the application from Launcher on
macOS.
Improved LogView. Entries can now be expanded and there is a context menu to copy the item's content. Improved LogView. Entries can now be expanded and there is a context menu to
copy the item's content.
Change the behavior of `ctrl+d` to add a multi-cursor selection to the next occurrence. Change the behavior of `ctrl+d` to add a multi-cursor selection to the next
The old behavior to move the selection to the next occurrence is now done using the shortcut `ctrl+f3`. occurrence. The old behavior to move the selection to the next occurrence is
now done using the shortcut `ctrl+f3`.
Added a command to create a multi-cursor with all the occurrences of the current selection. Added a command to create a multi-cursor with all the occurrences of the
Activated with the shortcut `ctrl+shift+l`. current selection. Activated with the shortcut `ctrl+shift+l`.
Fix problem when trying to close an unsaved new document. Fix problem when trying to close an unsaved new document.
No longer shows an error for the `-psn` argument passed to the application on macOS. No longer shows an error for the `-psn` argument passed to the application on
macOS.
Fix `treeview:open-in-system` command on Windows. Fix `treeview:open-in-system` command on Windows.
Fix rename command to update name of document if opened. Fix rename command to update name of document if opened.
Improve the find and replace dialog so that previously used expressions can be recalled Improve the find and replace dialog so that previously used expressions can be
using "up" and "down" keys. recalled using "up" and "down" keys.
Build package script rewrite with many improvements. Build package script rewrite with many improvements.
@ -55,63 +444,76 @@ Use bigger fonts by default.
Other minor improvements and fixes. Other minor improvements and fixes.
With many thanks to the contributors: @adamharrison, @takase1121, @Guldoman, @redtide, @Timofffee, @boppyt, @Jan200101. With many thanks to the contributors: @adamharrison, @takase1121, @Guldoman,
@redtide, @Timofffee, @boppyt, @Jan200101.
### 2.0.1 ## [2.0.1] - 2021-08-28
Fix a few bugs and we mandate the mod-version 2 for plugins. Fix a few bugs and we mandate the mod-version 2 for plugins.
This means that users should ensure they have up-to-date plugins for Lite XL 2.0. This means that users should ensure they have up-to-date plugins for Lite XL 2.0.
Here some details about the bug fixes: Here some details about the bug fixes:
- fix a bug that created a fatal error when using the command to change project folder or when closing all the active documents - fix a bug that created a fatal error when using the command to change project
- add a limit to avoid scaling fonts too much and fix a related invalid memory access for very small fonts folder or when closing all the active documents
- add a limit to avoid scaling fonts too much and fix a related invalid memory
access for very small fonts
- fix focus problem with NagView when switching project directory - fix focus problem with NagView when switching project directory
- fix error that prevented the verification of plugins versions - fix error that prevented the verification of plugins versions
- fix error on X11 that caused a bug window event on exit - fix error on X11 that caused a bug window event on exit
### 2.0 ## [2.0] - 2021-08-16
The 2.0 version of lite contains *breaking changes* to lite, in terms of how plugin settings are structured; The 2.0 version of lite contains *breaking changes* to lite, in terms of how
any custom plugins may need to be adjusted accordingly (see note below about plugin namespacing). plugin settings are structured; any custom plugins may need to be adjusted
accordingly (see note below about plugin namespacing).
Contains the following new features: Contains the following new features:
Full PCRE (regex) support for find and replace, as well as in language syntax definitions. Can be accessed Full PCRE (regex) support for find and replace, as well as in language syntax
programatically via the lua `regex` module. definitions. Can be accessed programatically via the lua `regex` module.
A full, finalized subprocess API, using libreproc. Subprocess can be started and interacted with using A full, finalized subprocess API, using libreproc. Subprocess can be started
`Process.new`. and interacted with using `Process.new`.
Support for multi-cursor editing. Cursors can be created by either ctrl+clicking on the screen, or by using Support for multi-cursor editing. Cursors can be created by either ctrl+clicking
the keyboard shortcuts ctrl+shift+up/down to create an additional cursor on the previous/next line. on the screen, or by using the keyboard shortcuts ctrl+shift+up/down to create
an additional cursor on the previous/next line.
All build systems other than meson removed. All build systems other than meson removed.
A more organized directory structure has been implemented; in particular a docs folder which contains C api A more organized directory structure has been implemented; in particular a docs
documentation, and a resource folder which houses all build resources. folder which contains C api documentation, and a resource folder which houses
all build resources.
Plugin config namespacing has been implemented. This means that instead of using `config.myplugin.a`, Plugin config namespacing has been implemented. This means that instead of
to read settings, and `config.myplugin = false` to disable plugins, this has been changed to using `config.myplugin.a`, to read settings, and `config.myplugin = false` to
`config.plugins.myplugin.a`, and `config.plugins.myplugin = false` repsectively. This may require changes to disable plugins, this has been changed to `config.plugins.myplugin.a`, and
`config.plugins.myplugin = false` respectively. This may require changes to
your user plugin, or to any custom plugins you have. your user plugin, or to any custom plugins you have.
A context menu on right click has been added. A context menu on right click has been added.
Changes to how we deal with indentation have been implemented; in particular, hitting home no longer brings you Changes to how we deal with indentation have been implemented; in particular,
to the start of a line, it'll bring you to the start of indentation, which is more in line with other editors. hitting home no longer brings you to the start of a line, it'll bring you to
the start of indentation, which is more in line with other editors.
Lineguide, and scale plugins moved into the core, and removed from `lite-plugins`. This may also require you to Lineguide, and scale plugins moved into the core, and removed from
adjust your personal plugin folder to remove these if they're present. `lite-plugins`. This may also require you to adjust your personal plugin
folder to remove these if they're present.
In addition, there have been many other small fixes and improvements, too numerous to list here. In addition, there have been many other small fixes and improvements, too
numerous to list here.
### 1.16.11 ## [1.16.11] - 2021-05-28
When opening directories with too many files lite-xl now keep diplaying files and directories in the treeview. When opening directories with too many files lite-xl now keep displaying files
The application remains functional and the directories can be explored without using too much memory. and directories in the treeview. The application remains functional and the
In this operating mode the files of the project are not indexed so the command "Core: Find File" will act as the "Core: Open File" command. directories can be explored without using too much memory. In this operating
The "Project Search: Find" will work by searching all the files present in the project directory even if they are not indexed. mode the files of the project are not indexed so the command "Core: Find File"
will act as the "Core: Open File" command.The "Project Search: Find" will work
by searching all the files present in the project directory even if they are
not indexed.
Implemented changing fonts per syntax group by @liquidev. Implemented changing fonts per syntax group by @liquidev.
@ -131,30 +533,30 @@ Fix bug with close button not working in borderless window mode.
Fix problem with normalization of filename for opened documents. Fix problem with normalization of filename for opened documents.
### 1.16.10 ## [1.16.10] - 2021-05-22
Improved syntax highlight system thanks to @liquidev and @adamharrison. Improved syntax highlight system thanks to @liquidev and @adamharrison.
Thanks to the new system we provide more a accurate syntax highlighting for Lua, C and C++. Thanks to the new system we provide more a accurate syntax highlighting for
Other syntax improvements contributed by @vincens2005. Lua, C and C++. Other syntax improvements contributed by @vincens2005.
Move to JetBrains Mono and Fira Sans fonts for code and UI respectively. Move to JetBrains Mono and Fira Sans fonts for code and UI respectively.
Thet are provided under the SIL Open Font License, Version 1.1. They are provided under the SIL Open Font License, Version 1.1.
See `doc/licenses.md` for license details. See `doc/licenses.md` for license details.
Fixed bug with fonts and rencache module. Fixed bug with fonts and rencache module. Under very specific situations the
Under very specific situations the application was crashing due to invalid memory access. application was crashing due to invalid memory access.
Add documentation for keymap binding, thanks to @Janis-Leuenberger. Add documentation for keymap binding, thanks to @Janis-Leuenberger.
Added a contributors page in `doc/contributors.md`. Added a contributors page in `doc/contributors.md`.
### 1.16.9 ## [1.16.9] - 2021-05-06
Fix a bug related to nested panes resizing. Fix a bug related to nested panes resizing.
Fix problem preventing creating a new file. Fix problem preventing creating a new file.
### 1.16.8 ## [1.16.8] - 2021-05-06
Fix application crash when using the command `core:restart`. Fix application crash when using the command `core:restart`.
@ -176,27 +578,28 @@ Both kind of tags can appear in new plugins in the form:
where the old tag needs to appear at the end for compatibility. where the old tag needs to appear at the end for compatibility.
### 1.16.7 ## [1.16.7] - 2021-05-01
Add support for retina displays on Mac OS X. Add support for retina displays on Mac OS X.
Fix a few problems related to file paths. Fix a few problems related to file paths.
### 1.16.6 ## [1.16.6] - 2021-04-21
Implement a system to check the compatibility of plugins by checking a release tag. Implement a system to check the compatibility of plugins by checking a release
Plugins that don't have the release tag will not be loaded. tag. Plugins that don't have the release tag will not be loaded.
Improve and extend the NagView with keyboard commands. Improve and extend the NagView with keyboard commands.
Special thanks to @takase1121 for the implementation and @liquidev for proposing and Special thanks to @takase1121 for the implementation and @liquidev for proposing
discussing the enhancements. and discussing the enhancements.
Add support to build on Mac OS X and create an application bundle. Add support to build on Mac OS X and create an application bundle.
Special thanks to @mathewmariani for his lite-macos fork, the Mac OS specific Special thanks to @mathewmariani for his lite-macos fork, the Mac OS specific
resources and his support. resources and his support.
Add hook function `DocView.on_text_change` so that plugin can accurately react on document changes. Add hook function `DocView.on_text_change` so that plugin can accurately react
Thanks to @vincens2005 for the suggestion and testing the implementation. on document changes. Thanks to @vincens2005 for the suggestion and testing the
implementation.
Enable borderless window mode using the `config.borderless` variable. Enable borderless window mode using the `config.borderless` variable.
If enable the system window's bar will be replaced by a title bar provided If enable the system window's bar will be replaced by a title bar provided
@ -214,13 +617,14 @@ commands `draw-whitespace:toggle`, `draw-whitespace:enable`,
Improve the NagView to accept keyboard commands and introduce dialog commands. Improve the NagView to accept keyboard commands and introduce dialog commands.
Add hook function `Doc:on_text_change` called on document changes, to be used by plugins. Add hook function `Doc:on_text_change` called on document changes, to be
used by plugins.
### 1.16.5 ## [1.16.5] - 2021-03-20
Hotfix for Github's issue https://github.com/franko/lite-xl/issues/122 Hotfix for Github's issue https://github.com/franko/lite-xl/issues/122
### 1.16.4 ## [1.16.4] - 2021-03-20
Add tooltips to show full file names from the tree-view. Add tooltips to show full file names from the tree-view.
@ -235,7 +639,7 @@ Made borders between tabs look cleaner.
Fix problem with files using hard tabs. Fix problem with files using hard tabs.
### 1.16.2 ## [1.16.2] - 2021-03-05
Implement close button for tabs. Implement close button for tabs.
@ -243,12 +647,12 @@ Make the command view list of suggestion scrollable to see all the items.
Improve update/resize behavior of treeview and toolbar. Improve update/resize behavior of treeview and toolbar.
### 1.16.1 ## [1.16.1] - 2021-02-25
Improve behavior of commands to move, delete and duplicate multiple lines: Improve behavior of commands to move, delete and duplicate multiple lines:
no longer include the last line if it does not contain any selection. no longer include the last line if it does not contain any selection.
Fix graphical artefacts when rendering some fonts like FiraSans. Fix graphical artifacts when rendering some fonts like FiraSans.
Introduce the `config.transitions` boolean variable. Introduce the `config.transitions` boolean variable.
When false the transitions will be disabled and changes will be done immediately. When false the transitions will be disabled and changes will be done immediately.
@ -257,7 +661,7 @@ Very useful for remote sessions where visual transitions doesn't work well.
Fix many small problems related to the new toolbar and the tooptips. Fix many small problems related to the new toolbar and the tooptips.
Fix problem with spacing in treeview when using monospace fonts. Fix problem with spacing in treeview when using monospace fonts.
### 1.16 ## [1.16] - 2021-02-19
Implement a toolbar shown in the bottom part of the tree-view. Implement a toolbar shown in the bottom part of the tree-view.
The toolbar is especially meant for new users to give an easy, visual, access The toolbar is especially meant for new users to give an easy, visual, access
@ -269,8 +673,8 @@ are actually resizable.
Add config mechanism to disable a plugin by setting Add config mechanism to disable a plugin by setting
`config.<plugin-name> = false`. `config.<plugin-name> = false`.
Improve the "detect indent" plugin to take into account the syntax and exclude comments Improve the "detect indent" plugin to take into account the syntax and exclude
for much accurate results. comments for much accurate results.
Add command `root:close-all` to close all the documents currently opened. Add command `root:close-all` to close all the documents currently opened.
@ -278,21 +682,24 @@ Show the full path filename of the active document in the window's title.
Fix problem with user's module reload not always enabled. Fix problem with user's module reload not always enabled.
### 1.15 ## [1.15] - 2021-01-04
**Project directories** **Project directories**
Extend your project by adding more directories using the command `core:add-directory`. Extend your project by adding more directories using the command
To remove them use the corresponding command `core:remove-directory`. `core:add-directory`. To remove them use the corresponding command
`core:remove-directory`.
**Workspaces** **Workspaces**
The workspace plugin from rxi/lite-plugins is now part of Lite XL. The workspace plugin from rxi/lite-plugins is now part of Lite XL.
In addition to the functionalities of the original plugin the extended version will In addition to the functionalities of the original plugin the extended version
also remember the window size and position and the additonal project directories. will also remember the window size and position and the additional project
To not interfere with the project's files the workspace file is saved in the personal directories.
Lite's configuration folder.
On unix-like systems it will be in: `$HOME/.config/lite-xl/ws`. To not interfere with the project's files the workspace file is saved in the
personal Lite's configuration folder. On unix-like systems it will be in:
`$HOME/.config/lite-xl/ws`.
**Scrolling the Tree View** **Scrolling the Tree View**
@ -304,10 +711,11 @@ As in the unix shell `~` is now used to identify the home directory.
**Files and Directories** **Files and Directories**
Add command to create a new empty directory within the project using the command Add command to create a new empty directory within the project using the
`files:create-directory`. command `files:create-directory`.
In addition a control-click on a project directory will prompt the user to create
a new directory inside the directory pointed. In addition a control-click on a project directory will prompt the user to
create a new directory inside the directory pointed.
**New welcome screen** **New welcome screen**
@ -315,51 +723,56 @@ Show 'Lite XL' instead of 'lite' and the version number.
**Various fixes and improvements** **Various fixes and improvements**
A few quirks previously with some of the new features have been fixed for a better user experience. A few quirks previously with some of the new features have been fixed for a
better user experience.
### 1.14 ## [1.14] - 2020-12-13
**Project Management** **Project Management**
Add a new command, Core: Change Project Folder, to change project directory by staying on the same window. Add a new command, Core: Change Project Folder, to change project directory by
All the current opened documents will be closed. staying on the same window. All the current opened documents will be closed.
The new command is associated with the keyboard combination ctrl+shit+c. The new command is associated with the keyboard combination ctrl+shit+c.
A similar command is also added, Core: Open Project Folder, with key binding ctrl+shift+o. A similar command is also added, Core: Open Project Folder, with key binding
It will open the chosen folder in a new window. ctrl+shift+o. It will open the chosen folder in a new window.
In addition Lite XL will now remember the recently used projects across different sessions. In addition Lite XL will now remember the recently used projects across
When invoked without arguments it will now open the project more recently used. different sessions. When invoked without arguments it will now open the project
If a directory is specified it will behave like before and open the directory indicated as an argument. more recently used. If a directory is specified it will behave like before and
open the directory indicated as an argument.
**Restart command** **Restart command**
A Core: Restart command is added to restart the editor without leaving the current window. A Core: Restart command is added to restart the editor without leaving the
Very convenient when modifying the Lua code for the editor itself. current window. Very convenient when modifying the Lua code for the editor
itself.
**User's setting auto-reload** **User's setting auto-reload**
When saving the user configuration, the user's module, the changes will be automatically applied to the When saving the user configuration, the user's module, the changes will be
current instance. automatically applied to the current instance.
**Bundle community provided colors schemes** **Bundle community provided colors schemes**
Included now in the release files the colors schemes from github.com/rxi/lite-colors. Included now in the release files the colors schemes from
github.com/rxi/lite-colors.
**Usability improvements** **Usability improvements**
Improve left and right scrolling of text to behave like other editors and improves text selection with mouse. Improve left and right scrolling of text to behave like other editors and
improves text selection with mouse.
**Fixes** **Fixes**
Correct font's rendering for full hinting mode when using subpixel antialiasing. Correct font's rendering for full hinting mode when using subpixel antialiasing.
### 1.13 ## [1.13] - 2020-12-06
**Rendering options for fonts** **Rendering options for fonts**
When loading fonts with the function renderer.font.load some rendering options can When loading fonts with the function renderer.font.load some rendering options
be optionally specified: can be optionally specified:
- antialiasing: grayscale or subpixel - antialiasing: grayscale or subpixel
- hinting: none, slight or full - hinting: none, slight or full
@ -368,36 +781,39 @@ See data/core/style.lua for the details about its utilisation.
The default remains antialiasing subpixel and hinting slight to reproduce the The default remains antialiasing subpixel and hinting slight to reproduce the
behavior of previous versions. behavior of previous versions.
The option grayscale with full hinting is specially interesting for crisp font rendering The option grayscale with full hinting is specially interesting for crisp font
without color artifacts. rendering without color artifacts.
**Unix-like install directories** **Unix-like install directories**
Use unix-like install directories for the executable and for the data directory. Use unix-like install directories for the executable and for the data directory.
The executable will be placed under $prefix/bin and the data folder will be The executable will be placed under $prefix/bin and the data folder will be
$prefix/share/lite-xl. $prefix/share/lite-xl.
The folder $prefix is not hard-coded in the binary but is determined at runtime The folder $prefix is not hard-coded in the binary but is determined at runtime
as the directory such as the executable is inside $prefix/bin. as the directory such as the executable is inside $prefix/bin.
If no such $prefix exist it will fall back to the old behavior and use the "data"
folder from the executable directory.
In addtion to the `EXEDIR` global variable an additional variable is exposed, `DATADIR`, If no such $prefix exist it will fall back to the old behavior and use the
to point to the data directory. "data" folder from the executable directory.
The old behavior using the "data" directory can be still selected at compile time In addtion to the `EXEDIR` global variable an additional variable is exposed,
using the "portable" option. The released Windows package will use the "data" `DATADIR`, to point to the data directory.
directory as before.
The old behavior using the "data" directory can be still selected at compile
time using the "portable" option. The released Windows package will use the
"data" directory as before.
**Configuration stored into the user's home directory** **Configuration stored into the user's home directory**
Now the Lite XL user's configuration will be stored in the user's home directory under Now the Lite XL user's configuration will be stored in the user's home directory
".config/lite-xl". under .config/lite-xl".
The home directory is determined using the "HOME" environment variable except on Windows
wher "USERPROFILE" is used instead. The home directory is determined using the "HOME" environment variable except
on Windows wher "USERPROFILE" is used instead.
A new global variable `USERDIR` is exposed to point to the user's directory. A new global variable `USERDIR` is exposed to point to the user's directory.
### 1.11 ## [1.11] - 2020-07-05
- include changes from rxi's Lite 1.11 - include changes from rxi's Lite 1.11
- fix behavior of tab to indent multiple lines - fix behavior of tab to indent multiple lines
@ -405,11 +821,36 @@ A new global variable `USERDIR` is exposed to point to the user's directory.
- limit project scan to a maximum number of files to limit memory usage - limit project scan to a maximum number of files to limit memory usage
- list recently visited files when using "Find File" command - list recently visited files when using "Find File" command
### 1.08 ## [1.08] - 2020-06-14
- Subpixel font rendering, removed gamma correction - Subpixel font rendering, removed gamma correction
- Avoid using CPU when the editor is idle - Avoid using CPU when the editor is idle
### 1.06 ## [1.06] - 2020-05-31
- subpixel font rendering with gamma correction - subpixel font rendering with gamma correction
[2.1.0]: https://github.com/lite-xl/lite-xl/releases/tag/v2.1.0
[2.0.5]: https://github.com/lite-xl/lite-xl/releases/tag/v2.0.5
[2.0.4]: https://github.com/lite-xl/lite-xl/releases/tag/v2.0.4
[2.0.3]: https://github.com/lite-xl/lite-xl/releases/tag/v2.0.3
[2.0.2]: https://github.com/lite-xl/lite-xl/releases/tag/v2.0.2
[2.0.1]: https://github.com/lite-xl/lite-xl/releases/tag/v2.0.1
[2.0]: https://github.com/lite-xl/lite-xl/releases/tag/v2.0.0
[1.16.11]: https://github.com/lite-xl/lite-xl/releases/tag/v1.16.11
[1.16.10]: https://github.com/lite-xl/lite-xl/releases/tag/v1.16.10
[1.16.9]: https://github.com/lite-xl/lite-xl/releases/tag/v1.16.9
[1.16.8]: https://github.com/lite-xl/lite-xl/releases/tag/v1.16.8
[1.16.7]: https://github.com/lite-xl/lite-xl/releases/tag/v1.16.7
[1.16.6]: https://github.com/lite-xl/lite-xl/releases/tag/v1.16.6
[1.16.5]: https://github.com/lite-xl/lite-xl/releases/tag/v1.16.5
[1.16.4]: https://github.com/lite-xl/lite-xl/releases/tag/v1.16.4
[1.16.2]: https://github.com/lite-xl/lite-xl/releases/tag/v1.16.2-lite-xl
[1.16.1]: https://github.com/lite-xl/lite-xl/releases/tag/v1.16.1-lite-xl
[1.16]: https://github.com/lite-xl/lite-xl/releases/tag/v1.16.0-lite-xl
[1.15]: https://github.com/lite-xl/lite-xl/releases/tag/v1.15-lite-xl
[1.14]: https://github.com/lite-xl/lite-xl/releases/tag/v1.14-lite-xl
[1.13]: https://github.com/lite-xl/lite-xl/releases/tag/v1.13-lite-xl
[1.11]: https://github.com/lite-xl/lite-xl/releases/tag/v1.11-lite-xl
[1.08]: https://github.com/lite-xl/lite-xl/releases/tag/v1.08-subpixel
[1.06]: https://github.com/lite-xl/lite-xl/releases/tag/1.06-subpixel-rc1

46
data/colors/default.lua Normal file
View File

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

36
data/core/bit.lua Normal file
View File

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

View File

@ -6,17 +6,48 @@ command.map = {}
local always_true = function() return true end local always_true = function() return true end
function command.add(predicate, map) ---Used iternally by command.add, statusview, and contextmenu to generate a
---function with a condition to evaluate returning the boolean result of this
---evaluation.
---
---If a string predicate is given it is treated as a require import that should
---return a valid object which is checked against the current active view,
---eg: "core.docview" will match any view that inherits from DocView. Appending
---a `!` at the end of the string means we want to match the given object
---from the import strcitly eg: "core.docview!" only DocView is matched.
---A function that returns a boolean can be used instead to perform a custom
---evaluation, setting to nil means always evaluates to true.
---
---@param predicate string | table | function
---@return function
function command.generate_predicate(predicate)
predicate = predicate or always_true 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
predicate = function() return core.active_view:is(class) end if not strict then
predicate = function(...) return core.active_view:extends(class), core.active_view, ... end
else
predicate = function(...) return core.active_view:is(class), core.active_view, ... end
end
end end
return predicate
end
function command.add(predicate, map)
predicate = command.generate_predicate(predicate)
for name, fn in pairs(map) do for name, fn in pairs(map) do
assert(not command.map[name], "command already exists: " .. name) if command.map[name] then
core.log_quiet("Replacing existing command \"%s\"", name)
end
command.map[name] = { predicate = predicate, perform = fn } command.map[name] = { predicate = predicate, perform = fn }
end end
end end
@ -33,8 +64,12 @@ end
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 cmd.predicate() then if memoized_predicates[cmd.predicate] == nil then
memoized_predicates[cmd.predicate] = cmd.predicate()
end
if memoized_predicates[cmd.predicate] then
table.insert(res, name) table.insert(res, name)
end end
end end
@ -47,8 +82,16 @@ end
local function perform(name, ...) local function perform(name, ...)
local cmd = command.map[name] local cmd = command.map[name]
if cmd and cmd.predicate(...) then if not cmd then return false end
cmd.perform(...) local res = { cmd.predicate(...) }
if table.remove(res, 1) then
if #res > 0 then
-- send values returned from predicate
cmd.perform(table.unpack(res))
else
-- send original parameters
cmd.perform(...)
end
return true return true
end end
return false return false
@ -64,7 +107,7 @@ end
function command.add_defaults() function command.add_defaults()
local reg = { local reg = {
"core", "root", "command", "doc", "findreplace", "core", "root", "command", "doc", "findreplace",
"files", "drawwhitespace", "dialog" "files", "drawwhitespace", "dialog", "log", "statusbar"
} }
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() ["command:submit"] = function(active_view)
core.active_view:submit() active_view:submit()
end, end,
["command:complete"] = function() ["command:complete"] = function(active_view)
core.active_view:complete() active_view:complete()
end, end,
["command:escape"] = function() ["command:escape"] = function(active_view)
core.active_view:exit() active_view:exit()
end, end,
["command:select-previous"] = function() ["command:select-previous"] = function(active_view)
core.active_view:move_suggestion_idx(1) active_view:move_suggestion_idx(1)
end, end,
["command:select-next"] = function() ["command:select-next"] = function(active_view)
core.active_view:move_suggestion_idx(-1) active_view:move_suggestion_idx(-1)
end, end,
}) })

View File

@ -10,8 +10,18 @@ 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)
return common.home_encode_list((text == "" or text == common.home_expand(common.dirname(core.project_dir))) local basedir = common.dirname(core.project_dir)
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, {
@ -38,36 +48,42 @@ command.add(nil, {
end, end,
["core:reload-module"] = function() ["core:reload-module"] = function()
core.command_view:enter("Reload Module", function(text, item) core.command_view:enter("Reload Module", {
local text = item and item.text or text submit = function(text, item)
core.reload_module(text) local text = item and item.text or text
core.log("Reloaded module %q", text) core.reload_module(text)
end, function(text) core.log("Reloaded module %q", text)
local items = {} end,
for name in pairs(package.loaded) do suggest = function(text)
table.insert(items, name) local items = {}
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", function(text, item) core.command_view:enter("Do Command", {
if item then submit = function(text, item)
command.perform(item.command) if item then
command.perform(item.command)
end
end,
suggest = function(text)
local res = common.fuzzy_match(commands, text)
for i, name in ipairs(res) do
res[i] = {
text = command.prettify_name(name),
info = keymap.get_binding(name),
command = name,
}
end
return res
end end
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()
@ -81,56 +97,72 @@ 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", function(text, item) core.command_view:enter("Open File From Project", {
text = item and item.text or text submit = function(text, item)
core.root_view:open_doc(core.open_doc(common.home_expand(text))) text = item and item.text or text
end, function(text) core.root_view:open_doc(core.open_doc(common.home_expand(text)))
return common.fuzzy_match_with_recents(files, core.visited_files, text) end,
end) suggest = function(text)
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)
local text = dirname == core.project_dir and "" or common.home_encode(dirname) .. PATHSEP 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", function(text) core.command_view:enter("Open File", {
local filename = system.absolute_path(common.home_expand(text)) text = text,
core.root_view:open_doc(core.open_doc(filename)) submit = function(text)
end, function (text) local filename = system.absolute_path(common.home_expand(text))
return common.home_encode_list(common.path_suggest(common.home_expand(text))) core.root_view:open_doc(core.open_doc(filename))
end, nil, function(text) end,
local filename = common.home_expand(text) suggest = function (text)
local path_stat, err = system.get_file_info(filename) return common.home_encode_list(common.path_suggest(common.home_expand(text)))
if err then end,
if err:find("No such file", 1, true) then validate = function(text)
-- check if the containing directory exists local filename = common.home_expand(text)
local dirname = common.dirname(filename) local path_stat, err = system.get_file_info(filename)
local dir_stat = dirname and system.get_file_info(dirname) if err then
if not dirname or (dir_stat and dir_stat.type == 'dir') then if err:find("No such file", 1, true) then
-- check if the containing directory exists
local dirname = common.dirname(filename)
local dir_stat = dirname and system.get_file_info(dirname)
if not dirname or (dir_stat and dir_stat.type == 'dir') then
return true
end
end
core.error("Cannot open file %s: %s", text, err)
elseif path_stat.type == 'dir' then
core.error("Cannot open %s, is a folder", text)
else
return true 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() local node = core.root_view:get_active_node_default()
node:add_view(LogView()) node:add_view(LogView())
end, end,
@ -141,62 +173,79 @@ command.add(nil, {
end, end,
["core:open-project-module"] = function() ["core:open-project-module"] = function()
local filename = ".lite_project.lua" if not system.get_file_info(".lite_project.lua") then
if system.get_file_info(filename) then core.try(core.write_init_project_module, ".lite_project.lua")
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
core.command_view:set_text(common.home_encode(dirname)) text = common.home_encode(dirname) .. PATHSEP
end end
core.command_view:enter("Change Project Folder", function(text, item) core.command_view:enter("Change Project Folder", {
text = system.absolute_path(common.home_expand(item and item.text or text)) text = text,
if text == core.project_dir then return end 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
core.confirm_close_docs(core.docs, core.open_folder_project, text) end
end, suggest_directory) if abs_path == core.project_dir then return end
core.confirm_close_docs(core.docs, function(dirpath)
core.open_folder_project(dirpath)
end, abs_path)
end,
suggest = suggest_directory
})
end, 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
core.command_view:set_text(common.home_encode(dirname)) text = common.home_encode(dirname) .. PATHSEP
end end
core.command_view:enter("Open Project", function(text, item) core.command_view:enter("Open Project", {
text = common.home_expand(item and item.text or text) text = text,
local path_stat = system.get_file_info(text) submit = function(text)
if not path_stat or path_stat.type ~= 'dir' then local path = common.home_expand(text)
core.error("Cannot open folder %q", text) local abs_path = check_directory_path(path)
return if not abs_path then
end core.error("Cannot open directory %q", path)
system.exec(string.format("%q %q", EXEFILE, text)) return
end, suggest_directory) end
if abs_path == core.project_dir then
core.error("Directory %q is currently opened", abs_path)
return
end
system.exec(string.format("%q %q", EXEFILE, abs_path))
end,
suggest = suggest_directory
})
end, end,
["core:add-directory"] = function() ["core:add-directory"] = function()
core.command_view:enter("Add Directory", function(text) core.command_view:enter("Add Directory", {
text = common.home_expand(text) submit = function(text)
local path_stat, err = system.get_file_info(text) text = common.home_expand(text)
if not path_stat then local path_stat, err = system.get_file_info(text)
core.error("cannot open %q: %s", text, err) if not path_stat then
return core.error("cannot open %q: %s", text, err)
elseif path_stat.type ~= 'dir' then return
core.error("%q is not a directory", text) elseif path_stat.type ~= 'dir' then
return core.error("%q is not a directory", text)
end return
core.add_project_directory(system.absolute_path(text)) end
end, suggest_directory) core.add_project_directory(system.absolute_path(text))
end,
suggest = suggest_directory
})
end, end,
["core:remove-directory"] = function() ["core:remove-directory"] = function()
@ -205,14 +254,17 @@ 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", function(text, item) core.command_view:enter("Remove Directory", {
text = common.home_expand(item and item.text or text) submit = function(text, item)
if not core.remove_project_directory(text) then text = common.home_expand(item and item.text or text)
core.error("No directory %q to be removed", text) if not core.remove_project_directory(text) then
core.error("No directory %q to be removed", text)
end
end,
suggest = function(text)
text = common.home_expand(text)
return common.home_encode_list(common.dir_list_suggest(text, dir_list))
end end
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,30 +3,25 @@ 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() ["dialog:previous-entry"] = function(v)
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() ["dialog:next-entry"] = function(v)
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() ["dialog:select-yes"] = function(v)
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() ["dialog:select-no"] = function(v)
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() ["dialog:select"] = function(v)
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

@ -47,19 +47,34 @@ end
local function cut_or_copy(delete) local function cut_or_copy(delete)
local full_text = "" local full_text = ""
local text = ""
core.cursor_clipboard = {}
core.cursor_clipboard_whole_line = {}
for idx, line1, col1, line2, col2 in doc():get_selections() do for idx, line1, col1, line2, col2 in doc():get_selections() do
if line1 ~= line2 or col1 ~= col2 then if line1 ~= line2 or col1 ~= col2 then
local text = doc():get_text(line1, col1, line2, col2) text = doc():get_text(line1, col1, line2, col2)
full_text = full_text == "" and text or (full_text .. " " .. text)
core.cursor_clipboard_whole_line[idx] = false
if delete then if delete then
doc():delete_to_cursor(idx, 0) doc():delete_to_cursor(idx, 0)
end end
full_text = full_text == "" and text or (full_text .. "\n" .. text) else -- Cut/copy whole line
doc().cursor_clipboard[idx] = text text = doc().lines[line1]
else full_text = full_text == "" and text or (full_text .. 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
end
end end
core.cursor_clipboard[idx] = text
end 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
@ -74,17 +89,100 @@ local function split_cursor(direction)
core.blink_reset() core.blink_reset()
end end
local function set_cursor(x, y, snap_type) local function set_cursor(dv, x, y, snap_type)
local line, col = dv():resolve_screen_position(x, y) local line, col = dv:resolve_screen_position(x, y)
doc():set_selection(line, col, line, col) dv.doc:set_selection(line, col, line, col)
if snap_type == "word" or snap_type == "lines" then if snap_type == "word" or snap_type == "lines" then
command.perform("doc:select-" .. snap_type) command.perform("doc:select-" .. snap_type)
end end
dv().mouse_selecting = { line, col, snap_type } dv.mouse_selecting = { line, col, snap_type }
core.blink_reset() core.blink_reset()
end end
local selection_commands = { local function line_comment(comment, line1, col1, line2, col2)
local start_comment = (type(comment) == 'table' and comment[1] or comment) .. " "
local end_comment = (type(comment) == 'table' and " " .. comment[2])
local uncomment = true
local start_offset = math.huge
for line = line1, line2 do
local text = doc().lines[line]
local s = text:find("%S")
if s then
local cs, ce = text:find(start_comment, s, true)
if cs ~= s then
uncomment = false
end
start_offset = math.min(start_offset, s)
end
end
local end_line = col2 == #doc().lines[line2]
for line = line1, line2 do
local text = doc().lines[line]
local s = text:find("%S")
if s and uncomment then
if end_comment and text:sub(#text - #end_comment, #text - 1) == end_comment then
doc():remove(line, #text - #end_comment, line, #text)
end
local cs, ce = text:find(start_comment, s, true)
if ce then
doc():remove(line, cs, line, ce + 1)
end
elseif s then
doc():insert(line, start_offset, start_comment)
if end_comment then
doc():insert(line, #doc().lines[line], " " .. comment[2])
end
end
end
col1 = col1 + (col1 > start_offset and #start_comment or 0) * (uncomment and -1 or 1)
col2 = col2 + (col2 > start_offset and #start_comment or 0) * (uncomment and -1 or 1)
if end_comment and end_line then
col2 = col2 + #end_comment * (uncomment and -1 or 1)
end
return line1, col1, line2, col2
end
local function block_comment(comment, line1, col1, line2, col2)
-- automatically skip spaces
local word_start = doc():get_text(line1, col1, line1, math.huge):find("%S")
local word_end = doc():get_text(line2, 1, line2, col2):find("%s*$")
col1 = col1 + (word_start and (word_start - 1) or 0)
col2 = word_end and word_end or col2
local block_start = doc():get_text(line1, col1, line1, col1 + #comment[1])
local block_end = doc():get_text(line2, col2 - #comment[2], line2, col2)
if block_start == comment[1] and block_end == comment[2] then
-- remove up to 1 whitespace after the comment
local start_len, stop_len = #comment[1], #comment[2]
if doc():get_text(line1, col1 + #comment[1], line1, col1 + #comment[1] + 1):find("%s$") then
start_len = start_len + 1
end
if doc():get_text(line2, col2 - #comment[2] - 1, line2, col2):find("^%s") then
stop_len = stop_len + 1
end
doc():remove(line1, col1, line1, col1 + start_len)
col2 = col2 - (line1 == line2 and start_len or 0)
doc():remove(line2, col2 - stop_len, line2, col2)
return line1, col1, line2, col2 - stop_len
else
doc():insert(line1, col1, comment[1] .. " ")
col2 = col2 + (line1 == line2 and (#comment[1] + 1) or 0)
doc():insert(line2, col2, " " .. comment[2])
return line1, col1, line2, col2 + #comment[2] + 1
end
end
local commands = {
["doc:select-none"] = function(dv)
local line, col = dv.doc:get_selection()
dv.doc:set_selection(line, col)
end,
["doc:cut"] = function() ["doc:cut"] = function()
cut_or_copy(true) cut_or_copy(true)
end, end,
@ -93,219 +191,231 @@ local selection_commands = {
cut_or_copy(false) cut_or_copy(false)
end, end,
["doc:select-none"] = function() ["doc:undo"] = function(dv)
local line, col = doc():get_selection() dv.doc:undo()
doc():set_selection(line, col)
end
}
local commands = {
["doc:undo"] = function()
doc():undo()
end, end,
["doc:redo"] = function() ["doc:redo"] = function(dv)
doc():redo() dv.doc:redo()
end, end,
["doc:paste"] = function() ["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 doc().cursor_clipboard["full"] ~= clipboard then local external_paste = core.cursor_clipboard["full"] ~= clipboard
doc().cursor_clipboard = {} if external_paste then
core.cursor_clipboard = {}
core.cursor_clipboard_whole_line = {}
end end
for idx, line1, col1, line2, col2 in doc():get_selections() do local value, whole_line
local value = doc().cursor_clipboard[idx] or clipboard for idx, line1, col1, line2, col2 in dv.doc:get_selections() do
doc():text_input(value:gsub("\r", ""), idx) if #core.cursor_clipboard_whole_line == (#dv.doc.selections/4) then
value = core.cursor_clipboard[idx]
whole_line = core.cursor_clipboard_whole_line[idx] == true
else
value = clipboard
whole_line = not external_paste and clipboard:find("\n") ~= nil
end
if whole_line then
dv.doc:insert(line1, 1, value:gsub("\r", ""))
if col1 == 1 then
dv.doc:move_to_cursor(idx, #value)
end
else
dv.doc:text_input(value:gsub("\r", ""), idx)
end
end end
end, end,
["doc:newline"] = function() ["doc:newline"] = function(dv)
for idx, line, col in doc():get_selections(false, true) do for idx, line, col in dv.doc:get_selections(false, true) do
local indent = doc().lines[line]:match("^[\t ]*") local indent = dv.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
doc():text_input("\n" .. indent, idx) -- Remove current line if it contains only whitespace
end if dv.doc.lines[line]:match("^%s+$") then
end, dv.doc:remove(line, 1, line, math.huge)
["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
doc():delete_to_cursor(idx, translate.next_char) dv.doc:text_input("\n" .. indent, idx)
end end
end, end,
["doc:backspace"] = function() ["doc:newline-below"] = function(dv)
local _, indent_size = doc():get_indent_info() 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() do
if line1 == line2 and col1 == col2 and dv.doc.lines[line1]:find("^%s*$", col1) then
dv.doc:remove(line1, col1, line1, math.huge)
end
dv.doc:delete_to_cursor(idx, translate.next_char)
end
end,
["doc:backspace"] = function(dv)
local _, indent_size = dv.doc:get_indent_info()
for idx, line1, col1, line2, col2 in dv.doc:get_selections() do
if line1 == line2 and col1 == col2 then if line1 == line2 and col1 == col2 then
local text = doc():get_text(line1, 1, line1, col1) local text = dv.doc:get_text(line1, 1, line1, col1)
if #text >= indent_size and text:find("^ *$") then if #text >= indent_size and text:find("^ *$") then
doc():delete_to_cursor(idx, 0, -indent_size) dv.doc:delete_to_cursor(idx, 0, -indent_size)
return return
end end
end end
doc():delete_to_cursor(idx, translate.previous_char) dv.doc:delete_to_cursor(idx, translate.previous_char)
end end
end, end,
["doc:select-all"] = function() ["doc:select-all"] = function(dv)
doc():set_selection(1, 1, math.huge, math.huge) dv.doc:set_selection(1, 1, math.huge, math.huge)
-- avoid triggering DocView:scroll_to_make_visible
dv.last_line1 = 1
dv.last_col1 = 1
dv.last_line2 = #dv.doc.lines
dv.last_col2 = #dv.doc.lines[#dv.doc.lines]
end, end,
["doc:select-lines"] = function() ["doc:select-lines"] = function(dv)
for idx, line1, _, line2 in doc():get_selections(true) do for idx, line1, _, line2 in dv.doc:get_selections(true) do
append_line_if_last_line(line2) append_line_if_last_line(line2)
doc():set_selections(idx, line1, 1, line2 + 1, 1) dv.doc:set_selections(idx, line1, 1, line2 + 1, 1)
end end
end, end,
["doc:select-word"] = function() ["doc:select-word"] = function(dv)
for idx, line1, col1 in doc():get_selections(true) do for idx, line1, col1 in dv.doc:get_selections(true) do
local line1, col1 = translate.start_of_word(doc(), line1, col1) local line1, col1 = translate.start_of_word(dv.doc, line1, col1)
local line2, col2 = translate.end_of_word(doc(), line1, col1) local line2, col2 = translate.end_of_word(dv.doc, line1, col1)
doc():set_selections(idx, line2, col2, line1, col1) dv.doc:set_selections(idx, line2, col2, line1, col1)
end end
end, end,
["doc:join-lines"] = function() ["doc:join-lines"] = function(dv)
for idx, line1, col1, line2, col2 in doc():get_selections(true) do for idx, line1, col1, line2, col2 in dv.doc:get_selections(true) do
if line1 == line2 then line2 = line2 + 1 end if line1 == line2 then line2 = line2 + 1 end
local text = doc():get_text(line1, 1, line2, math.huge) local text = dv.doc:get_text(line1, 1, line2, math.huge)
text = text:gsub("(.-)\n[\t ]*", function(x) 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)
doc():insert(line1, 1, text) dv.doc:insert(line1, 1, text)
doc():remove(line1, #text + 1, line2, math.huge) dv.doc:remove(line1, #text + 1, line2, math.huge)
if line1 ~= line2 or col1 ~= col2 then if line1 ~= line2 or col1 ~= col2 then
doc():set_selections(idx, line1, math.huge) dv.doc:set_selections(idx, line1, math.huge)
end end
end end
end, end,
["doc:indent"] = function() ["doc:indent"] = function(dv)
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 = doc():indent_text(false, line1, col1, line2, col2) local l1, c1, l2, c2 = dv.doc:indent_text(false, line1, col1, line2, col2)
if l1 then if l1 then
doc():set_selections(idx, l1, c1, l2, c2) dv.doc:set_selections(idx, l1, c1, l2, c2)
end end
end end
end, end,
["doc:unindent"] = function() ["doc:unindent"] = function(dv)
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 = doc():indent_text(true, line1, col1, line2, col2) local l1, c1, l2, c2 = dv.doc:indent_text(true, line1, col1, line2, col2)
if l1 then if l1 then
doc():set_selections(idx, l1, c1, l2, c2) dv.doc:set_selections(idx, l1, c1, l2, c2)
end end
end end
end, end,
["doc:duplicate-lines"] = function() ["doc:duplicate-lines"] = function(dv)
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)
doc():insert(line2 + 1, 1, text) dv.doc:insert(line2 + 1, 1, text)
local n = line2 - line1 + 1 local n = line2 - line1 + 1
doc():set_selections(idx, line1 + n, col1, line2 + n, col2) dv.doc:set_selections(idx, line1 + n, col1, line2 + n, col2)
end end
end, end,
["doc:delete-lines"] = function() ["doc:delete-lines"] = function(dv)
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)
doc():remove(line1, 1, line2 + 1, 1) dv.doc:remove(line1, 1, line2 + 1, 1)
doc():set_selections(idx, line1, col1) dv.doc:set_selections(idx, line1, col1)
end end
end, end,
["doc:move-lines-up"] = function() ["doc:move-lines-up"] = function(dv)
for idx, line1, col1, line2, col2 in doc_multiline_selections(true) do 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]
doc():insert(line2 + 1, 1, text) dv.doc:insert(line2 + 1, 1, text)
doc():remove(line1 - 1, 1, line1, 1) dv.doc:remove(line1 - 1, 1, line1, 1)
doc():set_selections(idx, line1 - 1, col1, line2 - 1, col2) dv.doc:set_selections(idx, line1 - 1, col1, line2 - 1, col2)
end end
end end
end, end,
["doc:move-lines-down"] = function() ["doc:move-lines-down"] = function(dv)
for idx, line1, col1, line2, col2 in doc_multiline_selections(true) do 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 < #doc().lines then if line2 < #dv.doc.lines then
local text = doc().lines[line2 + 1] local text = dv.doc.lines[line2 + 1]
doc():remove(line2 + 1, 1, line2 + 2, 1) dv.doc:remove(line2 + 1, 1, line2 + 2, 1)
doc():insert(line1, 1, text) dv.doc:insert(line1, 1, text)
doc():set_selections(idx, line1 + 1, col1, line2 + 1, col2) dv.doc:set_selections(idx, line1 + 1, col1, line2 + 1, col2)
end end
end end
end, end,
["doc:toggle-line-comments"] = function() ["doc:toggle-block-comments"] = function(dv)
local comment = doc().syntax.comment local comment = dv.doc.syntax.block_comment
if not comment then return end if not comment then
local indentation = doc():get_indent_string() if dv.doc.syntax.comment then
local comment_text = comment .. " " command.perform "doc:toggle-line-comments"
for idx, line1, _, line2 in doc_multiline_selections(true) do
local uncomment = true
local start_offset = math.huge
for line = line1, line2 do
local text = doc().lines[line]
local s = text:find("%S")
local cs, ce = text:find(comment_text, s, true)
if s and cs ~= s then
uncomment = false
start_offset = math.min(start_offset, s)
end
end end
for line = line1, line2 do return
local text = doc().lines[line] end
local s = text:find("%S")
if uncomment then for idx, line1, col1, line2, col2 in doc_multiline_selections(true) do
local cs, ce = text:find(comment_text, s, true) -- if nothing is selected, toggle the whole line
if ce then if line1 == line2 and col1 == col2 then
doc():remove(line, cs, line, ce + 1) col1 = 1
end col2 = #dv.doc.lines[line2]
elseif s then end
doc():insert(line, start_offset, comment_text) dv.doc:set_selections(idx, block_comment(comment, line1, col1, line2, col2))
end end
end,
["doc:toggle-line-comments"] = function(dv)
local comment = dv.doc.syntax.comment or dv.doc.syntax.block_comment
if comment then
for idx, line1, col1, line2, col2 in doc_multiline_selections(true) do
dv.doc:set_selections(idx, line_comment(comment, line1, col1, line2, col2))
end end
end end
end, end,
["doc:upper-case"] = function() ["doc:upper-case"] = function(dv)
doc():replace(string.upper) dv.doc:replace(string.uupper)
end, end,
["doc:lower-case"] = function() ["doc:lower-case"] = function(dv)
doc():replace(string.lower) dv.doc:replace(string.ulower)
end, end,
["doc:go-to-line"] = function() ["doc:go-to-line"] = function(dv)
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
@ -317,165 +427,195 @@ local commands = {
end end
end end
core.command_view:enter("Go To Line", function(text, item) core.command_view:enter("Go To Line", {
local line = item and item.line or tonumber(text) submit = function(text, item)
if not line then local line = item and item.line or tonumber(text)
core.error("Invalid line number or unmatched string") if not line then
return core.error("Invalid line number or unmatched string")
return
end
dv.doc:set_selection(line, 1 )
dv:scroll_to_line(line, true)
end,
suggest = function(text)
if not text:find("^%d*$") then
init_items()
return common.fuzzy_match(items, text)
end
end 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() ["doc:toggle-line-ending"] = function(dv)
doc().crlf = not doc().crlf dv.doc.crlf = not dv.doc.crlf
end, end,
["doc:save-as"] = function() ["doc:save-as"] = function(dv)
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
if doc().filename then local text
core.command_view:set_text(doc().filename) if dv.doc.filename then
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("(.*)[/\\](.+)$")
core.command_view:set_text(core.normalize_to_project_dir(dirname) .. PATHSEP) text = core.normalize_to_project_dir(dirname) .. PATHSEP
end end
core.command_view:enter("Save As", function(filename) core.command_view:enter("Save As", {
save(common.home_expand(filename)) text = text,
end, function (text) submit = function(filename)
return common.home_encode_list(common.path_suggest(common.home_expand(text))) save(common.home_expand(filename))
end) end,
suggest = function (text)
return common.home_encode_list(common.path_suggest(common.home_expand(text)))
end
})
end, end,
["doc:save"] = function() ["doc:save"] = function(dv)
if doc().filename then if dv.doc.filename then
save() save()
else else
command.perform("doc:save-as") command.perform("doc:save-as")
end end
end, end,
["file:rename"] = function() ["doc:reload"] = function(dv)
local old_filename = doc().filename dv.doc:reload()
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:set_text(old_filename) core.command_view:enter("Rename", {
core.command_view:enter("Rename", function(filename) text = old_filename,
save(common.home_expand(filename)) submit = function(filename)
core.log("Renamed \"%s\" to \"%s\"", old_filename, filename) save(common.home_expand(filename))
if filename ~= old_filename then core.log("Renamed \"%s\" to \"%s\"", old_filename, filename)
os.remove(old_filename) if filename ~= old_filename then
os.remove(old_filename)
end
end,
suggest = function (text)
return common.home_encode_list(common.path_suggest(common.home_expand(text)))
end end
end, function (text) })
return common.home_encode_list(common.path_suggest(common.home_expand(text)))
end)
end, end,
["file:delete"] = function() ["file:delete"] = function(dv)
local filename = doc().abs_filename local filename = dv.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(doc())) do for i,docview in ipairs(core.get_views_referencing_doc(dv.doc)) do
local node = core.root_view.root_node:get_node_for_view(docview) local node = core.root_view.root_node:get_node_for_view(docview)
node:close_view(core.root_view, docview) node:close_view(core.root_view.root_node, docview)
end end
os.remove(filename) os.remove(filename)
core.log("Removed \"%s\"", filename) core.log("Removed \"%s\"", filename)
end, end,
["doc:select-to-cursor"] = function(x, y, clicks) ["doc:select-to-cursor"] = function(dv, x, y, clicks)
local line1, col1 = select(3, doc():get_selection()) local line1, col1 = select(3, doc():get_selection())
local line2, col2 = dv():resolve_screen_position(x, y) local line2, col2 = dv:resolve_screen_position(x, y)
dv().mouse_selecting = { line1, col1, nil } dv.mouse_selecting = { line1, col1, nil }
doc():set_selection(line2, col2, line1, col1) dv.doc:set_selection(line2, col2, line1, col1)
end,
["doc:set-cursor"] = function(x, y)
set_cursor(x, y, "set")
end,
["doc:set-cursor-word"] = function(x, y)
set_cursor(x, y, "word")
end,
["doc:set-cursor-line"] = function(x, y, clicks)
set_cursor(x, y, "lines")
end,
["doc:split-cursor"] = function(x, y, clicks)
local line, col = dv():resolve_screen_position(x, y)
doc():add_selection(line, col, line, col)
end, end,
["doc:create-cursor-previous-line"] = function() ["doc:create-cursor-previous-line"] = function(dv)
split_cursor(-1) split_cursor(-1)
doc():merge_cursors() dv.doc:merge_cursors()
end, end,
["doc:create-cursor-next-line"] = function() ["doc:create-cursor-next-line"] = function(dv)
split_cursor(1) split_cursor(1)
doc():merge_cursors() dv.doc:merge_cursors()
end end
} }
command.add(function(x, y)
if x == nil or y == nil or not core.active_view:is(DocView) then return false end
local dv = core.active_view
local x1,y1,x2,y2 = dv.position.x, dv.position.y, dv.position.x + dv.size.x, dv.position.y + dv.size.y
return x >= x1 + dv:get_gutter_width() and x < x2 and y >= y1 and y < y2, dv, x, y
end, {
["doc:set-cursor"] = function(dv, x, y)
set_cursor(dv, x, y, "set")
end,
["doc:set-cursor-word"] = function(dv, x, y)
set_cursor(dv, x, y, "word")
end,
["doc:set-cursor-line"] = function(dv, x, y, clicks)
set_cursor(dv, x, y, "lines")
end,
["doc:split-cursor"] = function(dv, x, y, clicks)
local line, col = dv:resolve_screen_position(x, y)
local removal_target = nil
for idx, line1, col1 in dv.doc:get_selections(true) do
if line1 == line and col1 == col and #doc().selections > 4 then
removal_target = idx
end
end
if removal_target then
dv.doc:remove_selection(removal_target)
else
dv.doc:add_selection(line, col, line, col)
end
dv.mouse_selecting = { line, col, "set" }
end
})
local translations = { local translations = {
["previous-char"] = translate.previous_char, ["previous-char"] = translate,
["next-char"] = translate.next_char, ["next-char"] = translate,
["previous-word-start"] = translate.previous_word_start, ["previous-word-start"] = translate,
["next-word-end"] = translate.next_word_end, ["next-word-end"] = translate,
["previous-block-start"] = translate.previous_block_start, ["previous-block-start"] = translate,
["next-block-end"] = translate.next_block_end, ["next-block-end"] = translate,
["start-of-doc"] = translate.start_of_doc, ["start-of-doc"] = translate,
["end-of-doc"] = translate.end_of_doc, ["end-of-doc"] = translate,
["start-of-line"] = translate.start_of_line, ["start-of-line"] = translate,
["end-of-line"] = translate.end_of_line, ["end-of-line"] = translate,
["start-of-word"] = translate.start_of_word, ["start-of-word"] = translate,
["start-of-indentation"] = translate.start_of_indentation, ["start-of-indentation"] = translate,
["end-of-word"] = translate.end_of_word, ["end-of-word"] = translate,
["previous-line"] = DocView.translate.previous_line, ["previous-line"] = DocView.translate,
["next-line"] = DocView.translate.next_line, ["next-line"] = DocView.translate,
["previous-page"] = DocView.translate.previous_page, ["previous-page"] = DocView.translate,
["next-page"] = DocView.translate.next_page, ["next-page"] = DocView.translate,
} }
for name, fn in pairs(translations) do for name, obj in pairs(translations) do
commands["doc:move-to-" .. name] = function() doc():move_to(fn, dv()) end commands["doc:move-to-" .. name] = function(dv) dv.doc:move_to(obj[name:gsub("-", "_")], dv) end
commands["doc:select-to-" .. name] = function() doc():select_to(fn, dv()) end commands["doc:select-to-" .. name] = function(dv) dv.doc:select_to(obj[name:gsub("-", "_")], dv) end
commands["doc:delete-to-" .. name] = function() doc():delete_to(fn, dv()) end commands["doc:delete-to-" .. name] = function(dv) dv.doc:delete_to(obj[name:gsub("-", "_")], dv) end
end end
commands["doc:move-to-previous-char"] = function() commands["doc:move-to-previous-char"] = function(dv)
for idx, line1, col1, line2, col2 in doc():get_selections(true) do for idx, line1, col1, line2, col2 in dv.doc:get_selections(true) do
if line1 ~= line2 or col1 ~= col2 then if line1 ~= line2 or col1 ~= col2 then
doc():set_selections(idx, line1, col1) dv.doc:set_selections(idx, line1, col1)
else
dv.doc:move_to_cursor(idx, translate.previous_char)
end end
end end
doc():move_to(translate.previous_char)
end end
commands["doc:move-to-next-char"] = function() commands["doc:move-to-next-char"] = function(dv)
for idx, line1, col1, line2, col2 in doc():get_selections(true) do for idx, line1, col1, line2, col2 in dv.doc:get_selections(true) do
if line1 ~= line2 or col1 ~= col2 then if line1 ~= line2 or col1 ~= col2 then
doc():set_selections(idx, line2, col2) dv.doc:set_selections(idx, line2, col2)
else
dv.doc:move_to_cursor(idx, translate.next_char)
end end
end end
doc():move_to(translate.next_char)
end end
command.add("core.docview", commands) command.add("core.docview", commands)
command.add(function()
return core.active_view:is(DocView) and core.active_view.doc:has_any_selection()
end ,selection_commands)

View File

@ -4,11 +4,13 @@ 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", function(text) core.command_view:enter("New directory name", {
local success, err, path = common.mkdirp(text) submit = function(text)
if not success then local success, err, path = common.mkdirp(text)
core.error("cannot create directory %q: %s", path, err) if not success then
core.error("cannot create directory %q: %s", path, err)
end
end end
end) })
end, end,
}) })

View File

@ -46,9 +46,12 @@ 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 return end if t[i] == v then
table.remove(t, i)
break
end
end end
t[n + 1] = v table.insert(t, 1, v)
end end
@ -58,58 +61,76 @@ local function find(label, search_fn)
local text = last_view.doc:get_text(table.unpack(last_sel)) local text = last_view.doc:get_text(table.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:set_hidden_suggestions() core.command_view:enter(label, {
core.command_view:enter(label, function(text, item) text = text,
insert_unique(core.previous_find, text) select_text = true,
core.status_view:remove_tooltip() show_suggestions = false,
if found_expression then submit = function(text, item)
insert_unique(core.previous_find, text)
core.status_view:remove_tooltip()
if found_expression then
last_fn, last_text = search_fn, text
else
core.error("Couldn't find %q", text)
last_view.doc:set_selection(table.unpack(last_sel))
last_view:scroll_to_make_visible(table.unpack(last_sel))
end
end,
suggest = function(text)
update_preview(last_sel, search_fn, text)
last_fn, last_text = search_fn, text last_fn, last_text = search_fn, text
else return core.previous_find
core.error("Couldn't find %q", text) end,
last_view.doc:set_selection(table.unpack(last_sel)) cancel = function(explicit)
last_view:scroll_to_make_visible(table.unpack(last_sel)) core.status_view:remove_tooltip()
if explicit then
last_view.doc:set_selection(table.unpack(last_sel))
last_view:scroll_to_make_visible(table.unpack(last_sel))
end
end end
end, function(text) })
update_preview(last_sel, search_fn, text)
last_fn, last_text = search_fn, text
return core.previous_find
end, function(explicit)
core.status_view:remove_tooltip()
if explicit then
last_view.doc:set_selection(table.unpack(last_sel))
last_view:scroll_to_make_visible(table.unpack(last_sel))
end
end)
end end
local function replace(kind, default, fn) local function replace(kind, default, fn)
core.command_view:set_text(default, true)
core.status_view:show_tooltip(get_find_tooltip()) core.status_view:show_tooltip(get_find_tooltip())
core.command_view:set_hidden_suggestions() core.command_view:enter("Find To Replace " .. kind, {
core.command_view:enter("Find To Replace " .. kind, function(old) text = default,
insert_unique(core.previous_find, old) select_text = true,
core.command_view:set_text(old, true) show_suggestions = false,
submit = function(old)
insert_unique(core.previous_find, old)
local s = string.format("Replace %s %q With", kind, old) local s = string.format("Replace %s %q With", kind, old)
core.command_view:set_hidden_suggestions() core.command_view:enter(s, {
core.command_view:enter(s, function(new) text = old,
select_text = true,
show_suggestions = false,
submit = function(new)
core.status_view:remove_tooltip()
insert_unique(core.previous_replace, new)
local results = doc():replace(function(text)
return fn(text, old, new)
end)
local n = 0
for _,v in pairs(results) do
n = n + v
end
core.log("Replaced %d instance(s) of %s %q with %q", n, kind, old, new)
end,
suggest = function() return core.previous_replace end,
cancel = function()
core.status_view:remove_tooltip()
end
})
end,
suggest = function() return core.previous_find end,
cancel = function()
core.status_view:remove_tooltip() core.status_view:remove_tooltip()
insert_unique(core.previous_replace, new) end
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()
@ -179,7 +200,7 @@ command.add(has_unique_selection, {
["find-replace:select-add-all"] = function() select_add_next(true) end ["find-replace:select-add-all"] = function() select_add_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, find_reverse)
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, reverse = find_reverse }

View File

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

View File

@ -7,13 +7,11 @@ local config = require "core.config"
local t = { local t = {
["root:close"] = function() ["root:close"] = function(node)
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() ["root:close-or-quit"] = function(node)
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
@ -30,25 +28,22 @@ local t = {
for i, v in ipairs(core.docs) do if v ~= active_doc then table.insert(docs, v) end end 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() ["root:switch-to-previous-tab"] = function(node)
local node = core.root_view:get_active_node()
local idx = node:get_view_idx(core.active_view) local idx = node:get_view_idx(core.active_view)
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() ["root:switch-to-next-tab"] = function(node)
local node = core.root_view:get_active_node()
local idx = node:get_view_idx(core.active_view) local idx = node:get_view_idx(core.active_view)
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() ["root:move-tab-left"] = function(node)
local node = core.root_view:get_active_node()
local idx = node:get_view_idx(core.active_view) local idx = node:get_view_idx(core.active_view)
if idx > 1 then if idx > 1 then
table.remove(node.views, idx) table.remove(node.views, idx)
@ -56,24 +51,21 @@ local t = {
end end
end, end,
["root:move-tab-right"] = function() ["root:move-tab-right"] = function(node)
local node = core.root_view:get_active_node()
local idx = node:get_view_idx(core.active_view) local idx = node:get_view_idx(core.active_view)
if idx < #node.views then if idx < #node.views then
table.remove(node.views, idx) table.remove(node.views, idx)
table.insert(node.views, idx + 1, core.active_view) table.insert(node.views, idx + 1, core.active_view)
end end
end, end,
["root:shrink"] = function() ["root:shrink"] = function(node)
local node = core.root_view:get_active_node()
local parent = node:get_parent_node(core.root_view.root_node) local parent = node:get_parent_node(core.root_view.root_node)
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() ["root:grow"] = function(node)
local node = core.root_view:get_active_node()
local parent = node:get_parent_node(core.root_view.root_node) local parent = node:get_parent_node(core.root_view.root_node)
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)
@ -82,8 +74,7 @@ local t = {
for i = 1, 9 do for i = 1, 9 do
t["root:switch-to-tab-" .. i] = function() t["root:switch-to-tab-" .. i] = function(node)
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)
@ -93,8 +84,7 @@ end
for _, dir in ipairs { "left", "right", "up", "down" } do for _, dir in ipairs { "left", "right", "up", "down" } do
t["root:split-" .. dir] = function() t["root:split-" .. dir] = function(node)
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
@ -102,8 +92,7 @@ for _, dir in ipairs { "left", "right", "up", "down" } do
end end
end end
t["root:switch-to-" .. dir] = function() t["root:switch-to-" .. dir] = function(node)
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
@ -123,7 +112,7 @@ 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() local sx, sy = node:get_locked_size()
return not sx and not sy return not sx and not sy, node
end, t) end, t)
command.add(nil, { command.add(nil, {

View File

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

View File

@ -6,13 +6,16 @@ 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"
@ -21,11 +24,26 @@ 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,
} }
@ -34,8 +52,8 @@ function CommandView:new()
self.suggestion_idx = 1 self.suggestion_idx = 1
self.suggestions = {} self.suggestions = {}
self.suggestions_height = 0 self.suggestions_height = 0
self.show_suggestions = true
self.last_change_id = 0 self.last_change_id = 0
self.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
@ -46,8 +64,10 @@ function CommandView:new()
end end
---@deprecated
function CommandView:set_hidden_suggestions() function CommandView:set_hidden_suggestions()
self.show_suggestions = false core.warn("Using deprecated function CommandView:set_hidden_suggestions")
self.state.show_suggestions = false
end end
@ -56,8 +76,8 @@ function CommandView:get_name()
end end
function CommandView:get_line_screen_position() function CommandView:get_line_screen_position(line, col)
local x = CommandView.super.get_line_screen_position(self, 1) local x = CommandView.super.get_line_screen_position(self, 1, col)
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
@ -80,6 +100,7 @@ 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
@ -89,9 +110,18 @@ end
function CommandView:move_suggestion_idx(dir) function CommandView:move_suggestion_idx(dir)
if self.show_suggestions then local function overflow_suggestion_idx(n, count)
if count == 0 then return 0 end
if self.state.wrap then
return (n - 1) % count + 1
else
return common.clamp(n, 1, count)
end
end
if self.state.show_suggestions then
local n = self.suggestion_idx + dir local n = self.suggestion_idx + dir
self.suggestion_idx = common.clamp(n, 1, #self.suggestions) self.suggestion_idx = overflow_suggestion_idx(n, #self.suggestions)
self:complete() self:complete()
self.last_change_id = self.doc:get_change_id() self.last_change_id = self.doc:get_change_id()
else else
@ -102,7 +132,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 = common.clamp(n, 1, #self.suggestions) self.suggestion_idx = overflow_suggestion_idx(n, #self.suggestions)
self:complete() self:complete()
end end
else else
@ -132,21 +162,53 @@ function CommandView:submit()
end end
end end
---@param label string
function CommandView:enter(text, submit, suggest, cancel, validate) ---@varargs any
---@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
self.state = { local options = select(1, ...)
submit = submit or noop,
suggest = suggest or noop, if type(options) ~= "table" then
cancel = cancel or noop, core.warn("Using CommandView:enter in a deprecated way")
validate = validate or function() return true end local submit, suggest, cancel, validate = ...
} options = {
submit = submit,
suggest = suggest,
cancel = cancel,
validate = validate,
}
end
-- Support deprecated CommandView:set_hidden_suggestions
-- Remove this when set_hidden_suggestions is not supported anymore
if options.show_suggestions == nil then
options.show_suggestions = self.state.show_suggestions
end
self.state = common.merge(default_state, options)
-- We need to keep the text entered with CommandView:set_text to
-- maintain compatibility with deprecated usage, but still allow
-- overwriting with options.text
local old_text = self:get_text()
if old_text ~= "" then
core.warn("Using deprecated function CommandView:set_text")
end
if options.text or options.select_text then
local text = options.text or old_text
self:set_text(text, self.state.select_text)
end
-- Replace with a simple
-- self:set_text(self.state.text, self.state.select_text)
-- once old usage is removed
core.set_active_view(self) core.set_active_view(self)
self:update_suggestions() self:update_suggestions()
self.gutter_text_brightness = 100 self.gutter_text_brightness = 100
self.label = text .. ": " self.label = label .. ": "
end end
@ -159,8 +221,13 @@ 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
@ -198,35 +265,45 @@ 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) self:move_towards("gutter_text_brightness", 0, 0.1, "commandview")
-- 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) self:move_towards("gutter_width", dest, nil, "commandview")
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.show_suggestions and math.min(#self.suggestions, max_suggestions) * lh or 0 local dest = self.state.show_suggestions and math.min(#self.suggestions, max_suggestions) * lh or 0
self:move_towards("suggestions_height", dest) self:move_towards("suggestions_height", dest, nil, "commandview")
-- update suggestion cursor offset -- update suggestion cursor offset
local dest = math.min(self.suggestion_idx, max_suggestions) * 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) self:move_towards("selection_offset", dest, nil, "commandview")
-- 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) self:move_towards(self.size, "y", dest, nil, "commandview")
end end
@ -243,6 +320,7 @@ 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
@ -262,20 +340,20 @@ local function draw_suggestions_box(self)
end end
-- draw suggestion text -- draw suggestion text
local suggestion_offset = math.max(self.suggestion_idx - max_suggestions, 0) local offset = math.max(self.suggestion_idx - max_suggestions, 0)
local last = math.min(offset + max_suggestions, #self.suggestions)
core.push_clip_rect(rx, ry, rw, rh) core.push_clip_rect(rx, ry, rw, rh)
local i = 1 + suggestion_offset local first = 1 + offset
while i <= #self.suggestions do for i=first, last 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 - suggestion_offset) * lh - dh local y = self.position.y - (i - offset) * lh - dh
common.draw_text(self:get_font(), color, item.text, nil, x, y, 0, lh) 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
@ -283,7 +361,7 @@ end
function CommandView:draw() function CommandView:draw()
CommandView.super.draw(self) CommandView.super.draw(self)
if self.show_suggestions then if self.state.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

@ -16,6 +16,21 @@ function common.clamp(n, lo, hi)
end end
function common.merge(a, b)
a = type(a) == "table" and a or {}
local t = {}
for k, v in pairs(a) do
t[k] = v
end
if b and type(b) == "table" then
for k, v in pairs(b) do
t[k] = v
end
end
return t
end
function common.round(n) 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
@ -41,7 +56,7 @@ end
function common.distance(x1, y1, x2, y2) function common.distance(x1, y1, x2, y2)
return math.sqrt(math.pow(x2-x1, 2)+math.pow(y2-y1, 2)) return math.sqrt(((x2-x1) ^ 2)+((y2-y1) ^ 2))
end end
@ -57,7 +72,7 @@ function common.color(str)
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) * 0xff
else else
error(string.format("bad color string '%s'", str)) error(string.format("bad color string '%s'", str))
end end
@ -131,9 +146,29 @@ function common.fuzzy_match_with_recents(haystack, recents, needle)
end end
function common.path_suggest(text) function common.path_suggest(text, root)
if root and root:sub(-1) ~= PATHSEP then
root = root .. PATHSEP
end
local path, name = text:match("^(.-)([^:/\\]*)$") local path, name = text:match("^(.-)([^:/\\]*)$")
local files = system.list_dir(path == "" and "." or path) or {} local clean_dotslash = false
-- ignore root if path is absolute
local is_absolute = common.is_absolute_path(text)
if not is_absolute then
if path == "" then
path = root or "."
clean_dotslash = not root
else
path = (root or "") .. path
end
end
-- Only in Windows allow using both styles of PATHSEP
if (PATHSEP == "\\" and not string.match(path:sub(-1), "[\\/]")) or
(PATHSEP ~= "\\" and path:sub(-1) ~= PATHSEP) then
path = path .. PATHSEP
end
local files = system.list_dir(path) or {}
local res = {} local res = {}
for _, file in ipairs(files) do for _, file in ipairs(files) do
file = path .. file file = path .. file
@ -142,6 +177,19 @@ function common.path_suggest(text)
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
@ -213,19 +261,58 @@ function common.bench(name, fn, ...)
end end
function common.serialize(val) local function serialize(val, pretty, indent_str, escape, sort, limit, level)
local space = pretty and " " or ""
local indent = pretty and string.rep(indent_str, level) or ""
local newline = pretty and "\n" or ""
if type(val) == "string" then if type(val) == "string" then
return string.format("%q", val) local out = string.format("%q", val)
if escape then
out = string.gsub(out, "\\\n", "\\n")
out = string.gsub(out, "\\7", "\\a")
out = string.gsub(out, "\\8", "\\b")
out = string.gsub(out, "\\9", "\\t")
out = string.gsub(out, "\\11", "\\v")
out = string.gsub(out, "\\12", "\\f")
out = string.gsub(out, "\\13", "\\r")
end
return out
elseif type(val) == "table" then elseif type(val) == "table" then
-- early exit
if level >= limit then return tostring(val) end
local next_indent = pretty and (indent .. indent_str) or ""
local t = {} local t = {}
for k, v in pairs(val) do for k, v in pairs(val) do
table.insert(t, "[" .. common.serialize(k) .. "]=" .. common.serialize(v)) table.insert(t,
next_indent .. "[" ..
serialize(k, pretty, indent_str, escape, sort, limit, level + 1) ..
"]" .. space .. "=" .. space .. serialize(v, pretty, indent_str, escape, sort, limit, level + 1))
end end
return "{" .. table.concat(t, ",") .. "}" if #t == 0 then return "{}" end
if sort then table.sort(t) end
return "{" .. newline .. table.concat(t, "," .. newline) .. newline .. indent .. "}"
end end
return tostring(val) return tostring(val)
end end
-- Serialize `val` into a parsable string.
-- Available options
-- * pretty: enable pretty printing
-- * indent_str: indent to use (" " by default)
-- * escape: use normal escape characters instead of the ones used by string.format("%q", ...)
-- * sort: sort the keys inside tables
-- * limit: limit how deep to serialize
-- * initial_indent: the initial indentation level
function common.serialize(val, opts)
opts = opts or {}
local indent_str = opts.indent_str or " "
local initial_indent = opts.initial_indent or 0
local indent = opts.pretty and string.rep(indent_str, initial_indent) or ""
local limit = (opts.limit or math.huge) + initial_indent
return indent .. serialize(val, opts.pretty, indent_str,
opts.escape, opts.sort, limit, initial_indent)
end
function common.basename(path) 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
@ -287,11 +374,11 @@ end
-- absolute path without . or .. elements. -- absolute path without . or .. elements.
-- This function exists because on Windows the drive letter returned -- This function exists because on Windows the drive letter returned
-- by system.absolute_path is sometimes with a lower case and sometimes -- by system.absolute_path is sometimes with a lower case and sometimes
-- with an upper case to we normalize to upper case. -- with an upper case so we normalize to upper case.
function common.normalize_volume(filename) function common.normalize_volume(filename)
if not filename then return end if not filename then return end
if PATHSEP == '\\' then if PATHSEP == '\\' then
local drive, rem = filename:match('^([a-zA-Z]:\\)(.*)') local drive, rem = filename:match('^([a-zA-Z]:\\)(.-)'..PATHSEP..'?$')
if drive then if drive then
return drive:upper() .. rem return drive:upper() .. rem
end end
@ -340,6 +427,11 @@ function common.normalize_path(filename)
end end
function common.is_absolute_path(path)
return path:sub(1, 1) == PATHSEP or path:match("^(%a):\\")
end
function common.path_belongs_to(filename, path) function common.path_belongs_to(filename, path)
return string.find(filename, path .. PATHSEP, 1, true) == 1 return string.find(filename, path .. PATHSEP, 1, true) == 1
end end
@ -439,5 +531,6 @@ function common.rm(path, recursively)
return true return true
end end
return common return common

View File

@ -1,26 +1,37 @@
local config = {} local config = {}
config.fps = 60 config.fps = 60
config.max_log_items = 80 config.max_log_items = 800
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
config.file_size_limit = 10 config.file_size_limit = 10
config.ignore_files = "^%." config.ignore_files = { "^%." }
config.symbol_pattern = "[%a_][%w_]*" config.symbol_pattern = "[%a_][%w_]*"
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.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
@ -29,12 +40,20 @@ config.borderless = false
config.tab_close_button = true config.tab_close_button = true
config.max_clicks = 3 config.max_clicks = 3
-- Disable plugin loading setting to false the config entry -- set as true to be able to test non supported plugins
-- of the same name. config.skip_plugins_version = false
config.plugins = {}
config.plugins = {}
-- Allow you to set plugin configs even if we haven't seen the plugin before.
setmetatable(config.plugins, {
__index = function(t, k)
if rawget(t, k) == nil then rawset(t, k, {}) end
return rawget(t, k)
end
})
-- Disable these plugins by default.
config.plugins.trimwhitespace = false config.plugins.trimwhitespace = false
config.plugins.lineguide = false
config.plugins.drawwhitespace = false config.plugins.drawwhitespace = false
return config return config

View File

@ -5,11 +5,13 @@ 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 = {} local DIVIDER = {}
---@class core.contextmenu : core.object
local ContextMenu = Object:extend() local ContextMenu = Object:extend()
ContextMenu.DIVIDER = DIVIDER ContextMenu.DIVIDER = DIVIDER
@ -20,6 +22,7 @@ function ContextMenu:new()
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)
@ -37,18 +40,10 @@ local function get_item_size(item)
return lw, lh return lw, lh
end end
function ContextMenu:register(predicate, items) local function update_items_size(items, update_binding)
if type(predicate) == "string" then local width, height = 0, 0
predicate = require(predicate) for _, item in ipairs(items) do
end if update_binding and item ~= DIVIDER then
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.get_binding(item.command) item.info = keymap.get_binding(item.command)
end end
local lw, lh = get_item_size(item) local lw, lh = get_item_size(item)
@ -57,6 +52,11 @@ function ContextMenu:register(predicate, items)
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
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
@ -91,6 +91,7 @@ function ContextMenu:show(x, y)
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
@ -101,6 +102,7 @@ function ContextMenu:hide()
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
function ContextMenu:each_item() function ContextMenu:each_item()
@ -126,9 +128,6 @@ 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
@ -140,53 +139,73 @@ function ContextMenu:on_selected(item)
end end
end end
function ContextMenu:on_mouse_pressed(button, x, y, clicks) local function change_value(value, change)
local selected = (self.items or {})[self.selected] return value + change
local caught = false end
self:hide() function ContextMenu:focus_previous()
if button == "left" then self.selected = (self.selected == -1 or self.selected == 1) and #self.items or change_value(self.selected, -1)
if self:get_item_selected() == DIVIDER then
self.selected = change_value(self.selected, -1)
end
end
function ContextMenu:focus_next()
self.selected = (self.selected == -1 or self.selected == #self.items) and 1 or change_value(self.selected, 1)
if self:get_item_selected() == DIVIDER then
self.selected = change_value(self.selected, 1)
end
end
function ContextMenu:get_item_selected()
return (self.items or {})[self.selected]
end
function ContextMenu:call_selected_item()
local selected = self:get_item_selected()
self:hide()
if selected then if selected then
self:on_selected(selected) self:on_selected(selected)
caught = true
end end
end end
if button == "right" then function ContextMenu:on_mouse_pressed(button, px, py, clicks)
caught = self:show(x, y) local caught = false
if self.show_context_menu then
if button == "left" then
local selected = self:get_item_selected()
if selected then
self:on_selected(selected)
end
end
self:hide()
caught = true
else
if button == "right" then
caught = self:show(px, py)
end
end end
return caught return caught
end end
-- copied from core.docview ContextMenu.move_towards = View.move_towards
function ContextMenu:move_towards(t, k, dest, rate)
if type(t) ~= "table" then
return self:move_towards(self, t, k, dest, rate)
end
local val = t[k]
if not config.transitions or math.abs(val - dest) < 0.5 then
t[k] = dest
else
rate = rate or 0.5
if config.fps ~= 60 or config.animation_rate ~= 1 then
local dt = 60 / config.fps
rate = 1 - common.clamp(1 - rate, 1e-8, 1 - 1e-8)^(config.animation_rate * dt)
end
t[k] = common.lerp(val, dest, rate)
end
if val ~= dest then
core.redraw = true
end
end
function ContextMenu:update() function ContextMenu:update()
if self.show_context_menu then if self.show_context_menu then
self:move_towards("height", self.items.height) self:move_towards("height", self.items.height, nil, "contextmenu")
end end
end end
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

232
data/core/dirwatch.lua Normal file
View File

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

View File

@ -22,13 +22,21 @@ function Highlighter:new(doc)
else else
local max = math.min(self.first_invalid_line + 40, self.max_wanted_line) local max = math.min(self.first_invalid_line + 40, self.max_wanted_line)
local retokenized_from
for i = self.first_invalid_line, max do for i = self.first_invalid_line, max do
local state = (i > 1) and self.lines[i - 1].state local state = (i > 1) and self.lines[i - 1].state
local line = self.lines[i] local line = self.lines[i]
if not (line and line.init_state == state and line.text == self.doc.lines[i]) then if not (line and line.init_state == state and line.text == self.doc.lines[i]) then
retokenized_from = retokenized_from or i
self.lines[i] = self:tokenize_line(i, state) self.lines[i] = self:tokenize_line(i, state)
elseif retokenized_from then
self:update_notify(retokenized_from, i - retokenized_from - 1)
retokenized_from = nil
end end
end end
if retokenized_from then
self:update_notify(retokenized_from, max - retokenized_from)
end
self.first_invalid_line = max + 1 self.first_invalid_line = max + 1
core.redraw = true core.redraw = true
@ -71,6 +79,10 @@ function Highlighter:remove_notify(line, n)
common.splice(self.lines, line, n) common.splice(self.lines, line, n)
end end
function Highlighter:update_notify(line, n)
-- plugins can hook here to be notified that lines have been retokenized
end
function Highlighter:tokenize_line(idx, state) function Highlighter:tokenize_line(idx, state)
local res = {} local res = {}
@ -87,6 +99,7 @@ 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
self.max_wanted_line = math.max(self.max_wanted_line, idx) self.max_wanted_line = math.max(self.max_wanted_line, idx)
return line return line

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,6 @@ 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.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
@ -55,6 +54,7 @@ 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
@ -80,11 +80,23 @@ 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
@ -94,7 +106,6 @@ 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
@ -135,8 +146,8 @@ end
-- 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 idx, line1, col1, line2, col2 = self:get_selections(sort)({ self.selections, sort }, 0) local idx, line1, col1, line2, col2, swap = self:get_selections(sort)({ self.selections, sort }, 0)
return line1, col1, line2, col2, sort return line1, col1, line2, col2, swap
end end
function Doc:get_selection_text(limit) function Doc:get_selection_text(limit)
@ -172,9 +183,9 @@ end
local function sort_positions(line1, col1, line2, col2) local function sort_positions(line1, col1, line2, col2)
if line1 > line2 or line1 == line2 and col1 > col2 then if line1 > line2 or line1 == line2 and col1 > col2 then
return line2, col2, line1, col1 return line2, col2, line1, col1, true
end end
return line1, col1, line2, col2 return line1, col1, line2, col2, false
end end
function Doc:set_selections(idx, line1, col1, line2, col2, swap, rm) function Doc:set_selections(idx, line1, col1, line2, col2, swap, rm)
@ -197,8 +208,14 @@ function Doc:add_selection(line1, col1, line2, col2, swap)
self:set_selections(target, line1, col1, line2, col2, swap, 0) self:set_selections(target, line1, col1, line2, col2, swap, 0)
end end
function Doc:remove_selection(idx)
common.splice(self.selections, (idx - 1) * 4 + 1, 4)
end
function Doc:set_selection(line1, col1, line2, col2, swap) function Doc:set_selection(line1, col1, line2, col2, swap)
self.selections, self.cursor_clipboard = {}, {} self.selections = {}
self:set_selections(1, line1, col1, line2, col2, swap) self:set_selections(1, line1, col1, line2, col2, swap)
end end
@ -208,12 +225,10 @@ function Doc:merge_cursors(idx)
if self.selections[i] == self.selections[j] and if self.selections[i] == self.selections[j] and
self.selections[i+1] == self.selections[j+1] then self.selections[i+1] == self.selections[j+1] then
common.splice(self.selections, i, 4) common.splice(self.selections, i, 4)
common.splice(self.cursor_clipboard, i, 1)
break break
end end
end end
end end
if #self.selections <= 4 then self.cursor_clipboard = {} end
end end
local function selection_iterator(invariant, idx) local function selection_iterator(invariant, idx)
@ -356,7 +371,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
@ -388,7 +403,7 @@ function Doc:raw_remove(line1, col1, line2, col2, undo_stack, time)
-- splice line into line array -- splice line into line array
common.splice(self.lines, line1, line2 - line1 + 1, { before .. after }) common.splice(self.lines, line1, line2 - line1 + 1, { before .. after })
-- move all cursors back if they share a line with the removed text -- move all cursors back if they share a line with the removed text
for idx, cline1, ccol1, cline2, ccol2 in self:get_selections(true, true) do for idx, cline1, ccol1, cline2, ccol2 in self:get_selections(true, true) do
if cline1 < line2 then break end if cline1 < line2 then break end
@ -443,7 +458,7 @@ 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, n = fn(old_text) local new_text, res = 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)
@ -452,22 +467,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 n return res
end end
function Doc:replace(fn) function Doc:replace(fn)
local has_selection, n = false, 0 local has_selection, results = false, { }
for idx, line1, col1, line2, col2 in self:get_selections(true) do 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
n = n + self:replace_cursor(idx, line1, col1, line2, col2, fn) results[idx] = 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))
n = n + self:replace_cursor(1, 1, 1, #self.lines, #self.lines[#self.lines], fn) results[1] = self:replace_cursor(1, 1, 1, #self.lines, #self.lines[#self.lines], fn)
end end
return n return results
end end
@ -548,10 +563,12 @@ function Doc:indent_text(unindent, line1, col1, line2, 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
local e, rnded = self:get_line_indent(self.lines[line], unindent) if not has_selection or #self.lines[line] > 1 then -- don't indent empty lines in a selection
self:remove(line, 1, line, (e or 0) + 1) local e, rnded = self:get_line_indent(self.lines[line], unindent)
self:insert(line, 1, self:remove(line, 1, line, (e or 0) + 1)
unindent and rnded:sub(1, #rnded - #text) or rnded .. text) self:insert(line, 1,
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

@ -6,7 +6,8 @@ local keymap = require "core.keymap"
local translate = require "core.doc.translate" local translate = require "core.doc.translate"
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"
@ -29,6 +30,9 @@ 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,
@ -62,19 +66,22 @@ 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", function(_, item) core.command_view:enter("Unsaved Changes; Confirm Close", {
if item.text:match("^[cC]") then submit = function(_, item)
do_close() if item.text:match("^[cC]") then
elseif item.text:match("^[sS]") then do_close()
self.doc:save() elseif item.text:match("^[sS]") then
do_close() self.doc:save()
do_close()
end
end,
suggest = function(text)
local items = {}
if not text:find("^[^cC]") then table.insert(items, "Close Without Saving") end
if not text:find("^[^sS]") then table.insert(items, "Save And Close") end
return items
end end
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
@ -121,14 +128,18 @@ function DocView:get_gutter_width()
end end
function DocView:get_line_screen_position(idx) function DocView:get_line_screen_position(line, col)
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()
return x + gw, y + (idx-1) * lh + style.padding.y y = y + (line-1) * lh + style.padding.y
if col then
return x + gw + self:get_col_x_offset(line, col), y
else
return x + gw, y
end
end 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()
@ -198,8 +209,9 @@ 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 lh = self:get_line_height() local x, y = self:get_line_screen_position(line)
self.scroll.to.y = math.max(0, lh * (line - 1) - self.size.y / 2) local ox, oy = self:get_content_offset()
self.scroll.to.y = math.max(0, y - oy - self.size.y / 2)
if instant then if instant then
self.scroll.y = self.scroll.to.y self.scroll.y = self.scroll.to.y
end end
@ -208,10 +220,10 @@ end
function DocView:scroll_to_make_visible(line, col) function DocView:scroll_to_make_visible(line, col)
local min = self:get_line_height() * (line - 1) local ox, oy = self:get_content_offset()
local max = self:get_line_height() * (line + 2) - self.size.y local _, ly = self:get_line_screen_position(line, col)
self.scroll.to.y = math.min(self.scroll.to.y, min) local lh = self:get_line_height()
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 + 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(' ')
@ -224,11 +236,10 @@ function DocView:scroll_to_make_visible(line, col)
end end
end end
function DocView:on_mouse_moved(x, y, ...) function DocView:on_mouse_moved(x, y, ...)
DocView.super.on_mouse_moved(self, x, y, ...) DocView.super.on_mouse_moved(self, x, y, ...)
if self:scrollbar_overlaps_point(x, y) or self.dragging_scrollbar then if self.hovered_scrollbar_track or self.dragging_scrollbar then
self.cursor = "arrow" self.cursor = "arrow"
else else
self.cursor = "ibeam" self.cursor = "ibeam"
@ -271,8 +282,8 @@ function DocView:mouse_selection(doc, snap_type, line1, col1, line2, col2)
end end
function DocView:on_mouse_released(button) function DocView:on_mouse_released(...)
DocView.super.on_mouse_released(self, button) DocView.super.on_mouse_released(self, ...)
self.mouse_selecting = nil self.mouse_selecting = nil
end end
@ -284,13 +295,15 @@ 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 line, col = self.doc:get_selection() local line1, col1, line2, col2 = self.doc:get_selection()
if (line ~= self.last_line or col ~= self.last_col) and self.size.x > 0 then if (line1 ~= self.last_line1 or col1 ~= self.last_col1 or
line2 ~= self.last_line2 or col2 ~= self.last_col2) and self.size.x > 0 then
if core.active_view == self then if core.active_view == self 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_line, self.last_col = line, col self.last_line1, self.last_col1 = line1, col1
self.last_line2, self.last_col2 = line2, col2
end end
-- update blink timer -- update blink timer
@ -313,14 +326,15 @@ function DocView:draw_line_highlight(x, y)
end end
function DocView:draw_line_text(idx, x, y) function DocView:draw_line_text(line, 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 tx, ty = x, y + self:get_line_text_y_offset()
for _, type, text in self.doc.highlighter:each_token(idx) do for _, type, text in self.doc.highlighter:each_token(line) do
local color = style.syntax[type] local color = style.syntax[type]
local font = style.syntax_fonts[type] or default_font local font = style.syntax_fonts[type] or default_font
tx = renderer.draw_text(font, text, tx, ty, color) tx = renderer.draw_text(font, text, tx, ty, color)
end end
return self:get_line_height()
end end
function DocView:draw_caret(x, y) function DocView:draw_caret(x, y)
@ -328,28 +342,37 @@ function DocView:draw_caret(x, y)
renderer.draw_rect(x, y, style.caret_width, lh, style.caret) renderer.draw_rect(x, y, style.caret_width, lh, style.caret)
end end
function DocView:draw_line_body(idx, x, y) function DocView:draw_line_body(line, x, y)
-- draw highlight if any selection ends on this line -- draw highlight if any selection ends on this line
local draw_highlight = false local draw_highlight = false
for lidx, line1, col1, line2, col2 in self.doc:get_selections(false) do local hcl = config.highlight_current_line
if line1 == idx then if hcl ~= false then
draw_highlight = true for lidx, line1, col1, line2, col2 in self.doc:get_selections(false) do
break 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
end end
if draw_highlight and config.highlight_current_line and core.active_view == self then if draw_highlight and core.active_view == self then
self:draw_line_highlight(x + self.scroll.x, y) self:draw_line_highlight(x + self.scroll.x, y)
end 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 idx >= line1 and idx <= line2 then if line >= line1 and line <= line2 then
local text = self.doc.lines[idx] local text = self.doc.lines[line]
if line1 ~= idx then col1 = 1 end if line1 ~= line then col1 = 1 end
if line2 ~= idx then col2 = #text + 1 end if line2 ~= line then col2 = #text + 1 end
local x1 = x + self:get_col_x_offset(idx, col1) local x1 = x + self:get_col_x_offset(line, col1)
local x2 = x + self:get_col_x_offset(idx, col2) local x2 = x + self:get_col_x_offset(line, 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
@ -357,21 +380,22 @@ function DocView:draw_line_body(idx, x, y)
end end
-- draw line's text -- draw line's text
self:draw_line_text(idx, x, y) return self:draw_line_text(line, x, y)
end end
function DocView:draw_line_gutter(idx, x, y, width) function DocView:draw_line_gutter(line, 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 idx >= line1 and idx <= line2 then if line >= line1 and line <= 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
common.draw_text(self:get_font(), color, idx, "right", x, y + yoffset, width, self:get_line_height()) local lh = self:get_line_height()
common.draw_text(self:get_font(), color, line, "right", x, y, width, lh)
return lh
end end
@ -385,8 +409,7 @@ function DocView:draw_overlay()
and system.window_has_focus() then and system.window_has_focus() then
if config.disable_blink if config.disable_blink
or (core.blink_timer - core.blink_start) % T < T / 2 then or (core.blink_timer - core.blink_start) % T < T / 2 then
local x, y = self:get_line_screen_position(line) self:draw_caret(self:get_line_screen_position(line, col))
self:draw_caret(x + self:get_col_x_offset(line, col), y)
end end
end end
end end
@ -404,8 +427,7 @@ 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
self:draw_line_gutter(i, self.position.x, y, gpad and gw - gpad or gw) y = y + (self:draw_line_gutter(i, self.position.x, y, gpad and gw - gpad or gw) or lh)
y = y + lh
end end
local pos = self.position local pos = self.position
@ -414,8 +436,7 @@ function DocView:draw()
-- right side it is redundant with the Node's clip. -- 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) 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
self:draw_line_body(i, x, y) y = y + (self:draw_line_body(i, x, y) or lh)
y = y + lh
end end
self:draw_overlay() self:draw_overlay()
core.pop_clip_rect() core.pop_clip_rect()

View File

@ -2,14 +2,26 @@ local style = require "core.style"
local keymap = require "core.keymap" local keymap = require "core.keymap"
local View = require "core.view" local View = require "core.view"
---@class core.emptyview : core.view
---@field super core.view
local EmptyView = View:extend() local EmptyView = View:extend()
local function draw_text(x, y, color) local function draw_text(x, y, color)
local th = style.big_font:get_height() local th = style.big_font:get_height()
local dh = 2 * th + style.padding.y * 2 local dh = 2 * th + style.padding.y * 2
local x1, y1 = x, y + (dh - th) / 2 local x1, y1 = x, y + (dh - th) / 2
x = renderer.draw_text(style.big_font, "Lite XL", x1, y1, color) local xv = x1
renderer.draw_text(style.font, "version " .. VERSION, x1, y1 + th, color) 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 x = x + style.padding.x
renderer.draw_rect(x, y, math.ceil(1 * SCALE), dh, color) renderer.draw_rect(x, y, math.ceil(1 * SCALE), dh, color)
local lines = { local lines = {

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,25 @@
local core = require "core"
local command = require "core.command" local command = require "core.command"
local config = require "core.config" local config = require "core.config"
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"
@ -12,25 +28,88 @@ 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 os4 and "os4" or mos and "mos" 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)
---Generates a stroke sequence including currently pressed mod keys.
---@param key string
---@return string
local function key_to_stroke(key)
local stroke = "" local stroke = ""
for _, mk in ipairs(modkeys) do for _, mk in ipairs(modkeys) do
if keymap.modkeys[mk] then if keymap.modkeys[mk] then
stroke = stroke .. mk .. "+" stroke = stroke .. mk .. "+"
end end
end end
return stroke .. k return stroke .. key
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
if type(commands) == "string" or type(commands) == "function" then
commands = { commands }
end
if keymap.map[stroke] then
for _, registered_cmd in ipairs(keymap.map[stroke]) do
local j = 0
for i=1, #commands do
while commands[i + j] == registered_cmd do
j = j + 1
end
commands[i] = commands[i + j]
end
end
end
if #commands < 1 then
map[stroke] = nil
else
map[stroke] = commands
end
end
end
---Add bindings by replacing commands that were previously assigned to a shortcut.
---@param map keymap.map
function keymap.add_direct(map) function keymap.add_direct(map)
for stroke, commands in pairs(map) do for stroke, commands in pairs(map) do
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] = keymap.reverse_map[cmd] or {}
@ -39,15 +118,23 @@ function keymap.add_direct(map)
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
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 {}
@ -63,35 +150,34 @@ function keymap.add(map, overwrite)
end end
local function remove_only(tbl, k, v) ---Unregisters the given shortcut and associated command.
for key, values in pairs(tbl) do ---@param shortcut string
if key == k then ---@param cmd string
if v then function keymap.unbind(shortcut, cmd)
for i, value in ipairs(values) do remove_only(keymap.map, shortcut, cmd)
if value == v then remove_only(keymap.reverse_map, cmd, shortcut)
table.remove(values, i)
end
end
else
tbl[key] = nil
end
break
end
end
end
function keymap.unbind(key, cmd)
remove_only(keymap.map, key, cmd)
remove_only(keymap.reverse_map, cmd, key)
end 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 {}) return table.unpack(keymap.reverse_map[cmd] or {})
end 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]
end
--------------------------------------------------------------------------------
-- Events listening
--------------------------------------------------------------------------------
function keymap.on_key_pressed(k, ...) function keymap.on_key_pressed(k, ...)
local mk = modkey_map[k] local mk = modkey_map[k]
if mk then if mk then
@ -102,10 +188,19 @@ 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] local commands, performed = keymap.map[stroke], false
if commands then if commands then
for _, cmd in ipairs(commands) do for _, cmd in ipairs(commands) do
performed = command.perform(cmd, ...) if type(cmd) == "function" then
local ok, res = core.try(cmd, ...)
if ok then
performed = not (res == false)
else
performed = true
end
else
performed = command.perform(cmd, ...)
end
if performed then break end if performed then break end
end end
return performed return performed
@ -135,6 +230,9 @@ 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)
@ -148,7 +246,7 @@ 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+shift+r"] = "core:restart", ["ctrl+alt+r"] = "core:restart",
["alt+return"] = "core:toggle-fullscreen", ["alt+return"] = "core:toggle-fullscreen",
["f11"] = "core:toggle-fullscreen", ["f11"] = "core:toggle-fullscreen",
@ -217,6 +315,7 @@ keymap.add_direct {
["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",

View File

@ -1,5 +1,7 @@
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"
@ -36,12 +38,15 @@ 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
@ -77,25 +82,43 @@ function LogView:each_item()
end end
function LogView:on_mouse_moved(px, py, ...) function LogView:get_scrollable_size()
LogView.super.on_mouse_moved(self, px, py, ...) local _, y_off = self:get_content_offset()
local hovered = false local last_y, last_h = 0, 0
for _, item, x, y, w, h in self:each_item() do 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 last_y, last_h = y, h
hovered = true
self.hovered_item = item
break
end
end end
if not hovered then self.hovered_item = nil end if not config.scroll_past_end then
return last_y + last_h - y_off + style.padding.y
end
return last_y + self.size.y - y_off
end end
function LogView:on_mouse_pressed(button, mx, my, clicks) function LogView:on_mouse_pressed(button, px, py, clicks)
if LogView.super.on_mouse_pressed(self, button, mx, my, clicks) then return end if LogView.super.on_mouse_pressed(self, button, px, py, clicks) then
if self.hovered_item then return true
self:expand_item(self.hovered_item)
end end
local index, selected
for i, item, x, y, w, h in self:each_item() do
if px >= x and py >= y and px < x + w and py < y + h then
index = i
selected = item
break
end
end
if selected then
if keymap.modkeys["ctrl"] then
system.set_clipboard(core.get_log(selected))
core.status_view:show_message("i", style.text, "copied entry #"..index.." to clipboard.")
else
self:expand_item(selected)
end
end
return true
end end
@ -109,13 +132,13 @@ function LogView:update()
local expanding = self.expanding[1] local expanding = self.expanding[1]
if expanding then if expanding then
self:move_towards(expanding, "current", expanding.target) self:move_towards(expanding, "current", expanding.target, nil, "logview")
if expanding.current == expanding.target then 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) self:move_towards("yoffset", 0, nil, "logview")
LogView.super.update(self) LogView.super.update(self)
end end
@ -131,41 +154,62 @@ 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
for _, item, x, y, w in self:each_item() do local iw = math.max(
x = x + style.padding.x style.icon_font:get_width(style.log.ERROR.icon),
style.icon_font:get_width(style.log.INFO.icon)
)
local time = os.date(nil, item.time) local tw = style.font:get_width(datestr)
x = common.draw_text(style.font, style.dim, time, "left", x, y, w, lh) for _, item, x, y, w, h in self:each_item() do
x = x + style.padding.x if y + h >= self.position.y and y <= self.position.y + self.size.y then
core.push_clip_rect(x, y, w, h)
x = x + style.padding.x
x = common.draw_text(style.code_font, style.dim, is_expanded(item) and "-" or "+", "left", x, y, w, lh) x = common.draw_text(
x = x + style.padding.x style.icon_font,
w = w - (x - self:get_content_offset()) style.log[item.level].color,
style.log[item.level].icon,
"center",
x, y, iw, lh
)
x = x + style.padding.x
if is_expanded(item) then -- timestamps are always 15% of the width
y = y + common.round(style.padding.y / 2) local time = os.date(nil, item.time)
_, y = draw_text_multiline(style.font, item.text, x, y, style.text) common.draw_text(style.font, style.dim, time, "left", x, y, tw, lh)
x = x + tw + style.padding.x
local at = "at " .. common.home_encode(item.at) w = w - (x - self:get_content_offset())
_, y = common.draw_text(style.font, style.dim, at, "left", x, y, w, lh)
if item.info then if is_expanded(item) then
_, y = draw_text_multiline(style.font, item.info, x, y, style.dim) y = y + common.round(style.padding.y / 2)
_, y = draw_text_multiline(style.font, item.text, x, y, style.text)
local at = "at " .. common.home_encode(item.at)
_, y = common.draw_text(style.font, style.dim, at, "left", x, y, w, lh)
if item.info then
_, y = draw_text_multiline(style.font, item.info, x, y, style.dim)
end
else
local line, has_newline = string.match(item.text, "([^\n]+)(\n?)")
if has_newline ~= "" then
line = line .. " ..."
end
_, y = common.draw_text(style.font, style.text, line, "left", x, y, w, lh)
end end
else
local line, has_newline = string.match(item.text, "([^\n]+)(\n?)") core.pop_clip_rect()
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

@ -11,13 +11,19 @@ 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()
@ -46,20 +52,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:update() function NagView:get_scrollable_size()
NagView.super.update(self) local w, h = system.get_window_size()
if self.visible and self:get_target_height() > h then
if core.active_view == self and self.title then self.size.y = h
self:move_towards(self.size, "y", self:get_target_height()) return self:get_target_height()
self:move_towards(self, "underline_progress", 1)
else else
self:move_towards(self.size, "y", 0) self.size.y = 0
end end
return 0
end end
function NagView:draw_overlay() function NagView:dim_window_content()
local ox, oy = self:get_content_offset() local ox, oy = self:get_content_offset()
oy = oy + self.size.y oy = oy + self.show_height
local w, h = core.root_view.size.x, core.root_view.size.y - oy 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)
@ -81,7 +87,7 @@ 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.size.y - bh - style.padding.y oy = oy + self.show_height - 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]
@ -94,6 +100,8 @@ 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
@ -103,18 +111,55 @@ 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 NagView.super.on_mouse_pressed(self, button, mx, my, clicks) then return end if not self.visible then return false end
if NagView.super.on_mouse_pressed(self, button, mx, my, clicks) then return true end
for i, _, x,y,w,h in self:each_option() do 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
@ -122,20 +167,39 @@ 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)
function NagView:draw() if self.visible and core.active_view == self and self.title then
if self.size.y <= 0 or not self.title then return end self:move_towards(self, "show_height", self:get_target_height(), nil, "nagbar")
self:move_towards(self, "underline_progress", 1, nil, "nagbar")
else
self:move_towards(self, "show_height", 0, nil, "nagbar")
if self.show_height <= 0 then
self.title = nil
self.message = nil
self.options = nil
self.on_selected = nil
end
end
end
self:draw_overlay() local function draw_nagview_message(self)
self:draw_background(style.nagbar) self:dim_window_content()
-- 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.size.y) ox = common.draw_text(style.font, style.nagbar_text, str, "left", ox, oy, self.size.x, self.show_height)
ox = ox + style.padding.x ox = ox + style.padding.x
end end
@ -168,6 +232,17 @@ function NagView:draw()
common.draw_text(opt.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 end
function NagView:get_message_height() function NagView:get_message_height()
@ -178,23 +253,31 @@ 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 {}
self.title = opts.title if opts.title and opts.message and opts.options then
self.message = opts.message and opts.message .. "\n" self.visible = true
self.options = opts.options self.title = opts.title
self.on_selected = opts.on_selected self.message = opts.message and opts.message .. "\n"
if self.message and self.options then self.options = opts.options
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)
@ -204,7 +287,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)
if #self.queue > 0 and not self.title then self:next() end self:next()
end end
return NagView return NagView

View File

@ -6,6 +6,7 @@ local Object = require "core.object"
local EmptyView = require "core.emptyview" local EmptyView = require "core.emptyview"
local View = require "core.view" local View = require "core.view"
---@class core.node : core.object
local Node = Object:extend() local Node = Object:extend()
function Node:new(type) function Node:new(type)
@ -51,6 +52,15 @@ function Node: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) function Node:consume(node)
for k, _ in pairs(self) do self[k] = nil end for k, _ in pairs(self) do self[k] = nil end
for k, v in pairs(node) do self[k] = v end for k, v in pairs(node) do self[k] = v end
@ -160,8 +170,12 @@ end
function Node:set_active_view(view) function Node:set_active_view(view)
assert(self.type == "leaf", "Tried to set active view on non-leaf node") assert(self.type == "leaf", "Tried to set active view on non-leaf node")
local last_active_view = self.active_view
self.active_view = view self.active_view = view
core.set_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 end
@ -260,8 +274,8 @@ end
local function close_button_location(x, w) local function close_button_location(x, w)
local cw = style.icon_font:get_width("C") local cw = style.icon_font:get_width("C")
local pad = style.padding.y local pad = style.padding.x / 2
return x + w - pad - cw, cw, pad return x + w - cw - pad, cw, pad
end end
@ -468,59 +482,67 @@ function Node:update()
end end
self:tab_hovered_update(self.hovered.x, self.hovered.y) self:tab_hovered_update(self.hovered.x, self.hovered.y)
local tab_width = self:target_tab_width() local tab_width = self:target_tab_width()
self:move_towards("tab_shift", tab_width * (self.tab_offset - 1)) self:move_towards("tab_shift", tab_width * (self.tab_offset - 1), nil, "tabs")
self:move_towards("tab_width", tab_width) self:move_towards("tab_width", tab_width, nil, "tabs")
else else
self.a:update() self.a:update()
self.b:update() self.b:update()
end end
end end
function Node:draw_tab(text, is_active, is_hovered, is_close_hovered, x, y, w, h, standalone) function Node:draw_tab_title(view, font, is_active, is_hovered, x, y, w, h)
local text = view and view:get_name() or ""
local dots_width = font:get_width("")
local align = "center"
if font:get_width(text) > w then
align = "left"
for i = 1, #text do
local reduced_text = text:sub(1, #text - i)
if font:get_width(reduced_text) + dots_width <= w then
text = reduced_text .. ""
break
end
end
end
local color = style.dim
if is_active then color = style.text end
if is_hovered then color = style.text end
common.draw_text(font, color, text, align, x, y, w, h)
end
function Node:draw_tab_borders(view, is_active, is_hovered, x, y, w, h, standalone)
-- Tabs deviders
local ds = style.divider_size local ds = style.divider_size
local dots_width = style.font:get_width("")
local color = style.dim local color = style.dim
local padding_y = style.padding.y local padding_y = style.padding.y
renderer.draw_rect(x + w, y + padding_y, ds, h - padding_y * 2, style.dim) renderer.draw_rect(x + w, y + padding_y, ds, h - padding_y*2, style.dim)
if standalone then if standalone then
renderer.draw_rect(x-1, y-1, w+2, h+2, style.background2) renderer.draw_rect(x-1, y-1, w+2, h+2, style.background2)
end end
-- Full border
if is_active then if is_active then
color = style.text color = style.text
renderer.draw_rect(x, y, w, h, style.background) renderer.draw_rect(x, y, w, h, style.background)
renderer.draw_rect(x + w, y, ds, h, style.divider) renderer.draw_rect(x + w, y, ds, h, style.divider)
renderer.draw_rect(x - ds, y, ds, h, style.divider) renderer.draw_rect(x - ds, y, ds, h, style.divider)
end end
local cx, cw, cspace = close_button_location(x, w) return x + ds, y, w - ds*2, h
end
function Node:draw_tab(view, is_active, is_hovered, is_close_hovered, x, y, w, h, standalone)
x, y, w, h = self:draw_tab_borders(view, is_active, is_hovered, x, y, w, h, standalone)
-- Close button
local cx, cw, cpad = close_button_location(x, w)
local show_close_button = ((is_active or is_hovered) and not standalone and config.tab_close_button) local show_close_button = ((is_active or is_hovered) and not standalone and config.tab_close_button)
if show_close_button then if show_close_button then
local close_style = is_close_hovered and style.text or style.dim local close_style = is_close_hovered and style.text or style.dim
common.draw_text(style.icon_font, close_style, "C", nil, cx, y, 0, h) common.draw_text(style.icon_font, close_style, "C", nil, cx, y, cw, h)
end end
if is_hovered then -- Title
color = style.text x = x + cpad
end w = cx - x
local padx = style.padding.x core.push_clip_rect(x, y, w, h)
-- Normally we should substract "cspace" from text_avail_width and from the self:draw_tab_title(view, style.font, is_active, is_hovered, x, y, w, h)
-- clipping width. It is the padding space we give to the left and right of the
-- close button. However, since we are using dots to terminate filenames, we
-- choose to ignore "cspace" accepting that the text can possibly "touch" the
-- close button.
local text_avail_width = cx - x - padx
core.push_clip_rect(x, y, cx - x, h)
x, w = x + padx, w - padx * 2
local align = "center"
if style.font:get_width(text) > text_avail_width then
align = "left"
for i = 1, #text do
local reduced_text = text:sub(1, #text - i)
if style.font:get_width(reduced_text) + dots_width <= text_avail_width then
text = reduced_text .. ""
break
end
end
end
common.draw_text(style.font, color, text, align, x, y, w, h)
core.pop_clip_rect() core.pop_clip_rect()
end end
@ -547,7 +569,7 @@ function Node:draw_tabs()
for i = self.tab_offset, self.tab_offset + tabs_number - 1 do for i = self.tab_offset, self.tab_offset + tabs_number - 1 do
local view = self.views[i] local view = self.views[i]
local x, y, w, h = self:get_tab_rect(i) local x, y, w, h = self:get_tab_rect(i)
self:draw_tab(view:get_name(), view == self.active_view, self:draw_tab(view, view == self.active_view,
i == self.hovered_tab, i == self.hovered_close, i == self.hovered_tab, i == self.hovered_close,
x, y, w, h) x, y, w, h)
end end
@ -688,7 +710,7 @@ function Node:get_split_type(mouse_x, mouse_y)
local local_mouse_x = mouse_x - x local local_mouse_x = mouse_x - x
local local_mouse_y = mouse_y - y local local_mouse_y = mouse_y - y
if local_mouse_y < 0 then if local_mouse_y < 0 then
return "tab" return "tab"
else else

View File

@ -1,11 +1,12 @@
---@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
function Object:new() ---@return core.object
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
@ -19,8 +20,17 @@ 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) function Object:is(T)
return getmetatable(self) == T
end
---Check if the object inherits from the given type.
---@param T any
---@return boolean
function Object:extends(T)
local mt = getmetatable(self) local mt = getmetatable(self)
while mt do while mt do
if mt == T then if mt == T then
@ -31,12 +41,14 @@ function Object:is(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
---Methamethod 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

@ -5,8 +5,9 @@ regex.__index = function(table, key) return regex[key]; end
regex.match = function(pattern_string, string, offset, options) regex.match = function(pattern_string, string, offset, options)
local pattern = type(pattern_string) == "table" and local pattern = type(pattern_string) == "table" and
pattern_string or regex.compile(pattern_string) pattern_string or regex.compile(pattern_string)
local s, e = regex.cmatch(pattern, string, offset or 1, options or 0) local res = { regex.cmatch(pattern, string, offset or 1, options or 0) }
return s, e and e - 1 res[2] = res[2] and res[2] - 1
return table.unpack(res)
end end
-- Will iterate back through any UTF-8 bytes so that we don't replace bits -- Will iterate back through any UTF-8 bytes so that we don't replace bits

View File

@ -5,7 +5,10 @@ local Node = require "core.node"
local View = require "core.view" local View = require "core.view"
local DocView = require "core.docview" local DocView = require "core.docview"
---@class core.rootview : core.view
---@field super core.view
---@field root_node core.node
---@field mouse core.view.position
local RootView = View:extend() local RootView = View:extend()
function RootView:new() function RootView:new()
@ -29,11 +32,15 @@ function RootView:defer_draw(fn, ...)
end end
---@return core.node
function RootView:get_active_node() function RootView:get_active_node()
return self.root_node:get_node_for_view(core.active_view) local node = self.root_node:get_node_for_view(core.active_view)
if not node then node = self:get_primary_node() end
return node
end end
---@return core.node
local function get_primary_node(node) local function get_primary_node(node)
if node.is_primary_node then if node.is_primary_node then
return node return node
@ -44,8 +51,10 @@ local function get_primary_node(node)
end end
---@return core.node
function RootView:get_active_node_default() function RootView:get_active_node_default()
local node = self.root_node:get_node_for_view(core.active_view) local node = self.root_node:get_node_for_view(core.active_view)
if not node then node = self:get_primary_node() end
if node.locked then if node.locked then
local default_view = self:get_primary_node().views[1] local default_view = self:get_primary_node().views[1]
assert(default_view, "internal error: cannot find original document node.") assert(default_view, "internal error: cannot find original document node.")
@ -56,11 +65,14 @@ function RootView:get_active_node_default()
end end
---@return core.node
function RootView:get_primary_node() function RootView:get_primary_node()
return get_primary_node(self.root_node) return get_primary_node(self.root_node)
end end
---@param node core.node
---@return core.node
local function select_next_primary_node(node) local function select_next_primary_node(node)
if node.is_primary_node then return end if node.is_primary_node then return end
if node.type ~= "leaf" then if node.type ~= "leaf" then
@ -74,11 +86,14 @@ local function select_next_primary_node(node)
end end
---@return core.node
function RootView:select_next_primary_node() function RootView:select_next_primary_node()
return select_next_primary_node(self.root_node) return select_next_primary_node(self.root_node)
end end
---@param doc core.doc
---@return core.docview
function RootView:open_doc(doc) function RootView:open_doc(doc)
local node = self:get_active_node_default() local node = self:get_active_node_default()
for i, view in ipairs(node.views) do for i, view in ipairs(node.views) do
@ -95,17 +110,27 @@ function RootView:open_doc(doc)
end end
---@param keep_active boolean
function RootView:close_all_docviews(keep_active) function RootView:close_all_docviews(keep_active)
self.root_node:close_all_docviews(keep_active) self.root_node:close_all_docviews(keep_active)
end end
-- Function to intercept mouse pressed events on the active view. ---Function to intercept mouse pressed events on the active view.
-- Do nothing by default. ---Do nothing by default.
---@param button core.view.mousebutton
---@param x number
---@param y number
---@param clicks integer
function RootView.on_view_mouse_pressed(button, x, y, clicks) function RootView.on_view_mouse_pressed(button, x, y, clicks)
end end
---@param button core.view.mousebutton
---@param x number
---@param y number
---@param clicks integer
---@return boolean
function RootView:on_mouse_pressed(button, x, y, clicks) function RootView:on_mouse_pressed(button, x, y, clicks)
local div = self.root_node:get_divider_overlapping_point(x, y) local div = self.root_node:get_divider_overlapping_point(x, y)
local node = self.root_node:get_child_overlapping_point(x, y) local node = self.root_node:get_child_overlapping_point(x, y)
@ -159,6 +184,9 @@ function RootView:set_show_overlay(overlay, status)
end end
---@param button core.view.mousebutton
---@param x number
---@param y number
function RootView:on_mouse_released(button, x, y, ...) function RootView:on_mouse_released(button, x, y, ...)
if self.dragged_divider then if self.dragged_divider then
self.dragged_divider = nil self.dragged_divider = nil
@ -217,6 +245,10 @@ local function resize_child_node(node, axis, value, delta)
end end
---@param x number
---@param y number
---@param dx number
---@param dy number
function RootView:on_mouse_moved(x, y, dx, dy) function RootView:on_mouse_moved(x, y, dx, dy)
if core.active_view == core.nag_view then if core.active_view == core.nag_view then
core.request_cursor("arrow") core.request_cursor("arrow")
@ -253,8 +285,13 @@ function RootView:on_mouse_moved(x, y, dx, dy)
self.root_node:on_mouse_moved(x, y, dx, dy) self.root_node:on_mouse_moved(x, y, dx, dy)
local last_overlapping_node = self.overlapping_node
self.overlapping_node = self.root_node:get_child_overlapping_point(x, y) self.overlapping_node = self.root_node:get_child_overlapping_point(x, y)
if last_overlapping_node and last_overlapping_node ~= self.overlapping_node then
last_overlapping_node:on_mouse_left()
end
local div = self.root_node:get_divider_overlapping_point(x, y) local div = self.root_node:get_divider_overlapping_point(x, y)
local tab_index = self.overlapping_node and self.overlapping_node:get_tab_overlapping_point(x, y) local tab_index = self.overlapping_node and self.overlapping_node:get_tab_overlapping_point(x, y)
if self.overlapping_node and self.overlapping_node:get_scroll_button_index(x, y) then if self.overlapping_node and self.overlapping_node:get_scroll_button_index(x, y) then
@ -269,6 +306,23 @@ function RootView:on_mouse_moved(x, y, dx, dy)
end end
function RootView:on_mouse_left()
if self.overlapping_node then
self.overlapping_node:on_mouse_left()
end
end
---@param filename string
---@param x number
---@param y number
---@return boolean
function RootView:on_file_dropped(filename, x, y)
local node = self.root_node:get_child_overlapping_point(x, y)
return node and node.active_view:on_file_dropped(filename, x, y)
end
function RootView:on_mouse_wheel(...) function RootView:on_mouse_wheel(...)
local x, y = self.mouse.x, self.mouse.y local x, y = self.mouse.x, self.mouse.y
local node = self.root_node:get_child_overlapping_point(x, y) local node = self.root_node:get_child_overlapping_point(x, y)
@ -288,12 +342,12 @@ end
function RootView:interpolate_drag_overlay(overlay) function RootView:interpolate_drag_overlay(overlay)
self:move_towards(overlay, "x", overlay.to.x) self:move_towards(overlay, "x", overlay.to.x, nil, "tab_drag")
self:move_towards(overlay, "y", overlay.to.y) self:move_towards(overlay, "y", overlay.to.y, nil, "tab_drag")
self:move_towards(overlay, "w", overlay.to.w) self:move_towards(overlay, "w", overlay.to.w, nil, "tab_drag")
self:move_towards(overlay, "h", overlay.to.h) self:move_towards(overlay, "h", overlay.to.h, nil, "tab_drag")
self:move_towards(overlay, "opacity", overlay.visible and 100 or 0) self:move_towards(overlay, "opacity", overlay.visible and 100 or 0, nil, "tab_drag")
overlay.color[4] = overlay.base_color[4] * overlay.opacity / 100 overlay.color[4] = overlay.base_color[4] * overlay.opacity / 100
end end
@ -381,8 +435,8 @@ function RootView:draw_grabbed_tab()
local _,_, w, h = dn.node:get_tab_rect(dn.idx) local _,_, w, h = dn.node:get_tab_rect(dn.idx)
local x = self.mouse.x - w / 2 local x = self.mouse.x - w / 2
local y = self.mouse.y - h / 2 local y = self.mouse.y - h / 2
local text = dn.node.views[dn.idx] and dn.node.views[dn.idx]:get_name() or "" local view = dn.node.views[dn.idx]
self.root_node:draw_tab(text, true, true, false, x, y, w, h, true) self.root_node:draw_tab(view, true, true, false, x, y, w, h, true)
end end

View File

@ -1,6 +1,6 @@
-- this file is used by lite-xl to setup the Lua environment when starting -- this file is used by lite-xl to setup the Lua environment when starting
VERSION = "2.0.3r3" VERSION = "2.0.5r1"
MOD_VERSION = "2" MOD_VERSION = "3"
SCALE = tonumber(os.getenv("LITE_SCALE") or os.getenv("GDK_SCALE") or os.getenv("QT_SCALE_FACTOR")) or SCALE SCALE = tonumber(os.getenv("LITE_SCALE") or os.getenv("GDK_SCALE") or os.getenv("QT_SCALE_FACTOR")) or SCALE
PATHSEP = package.config:sub(1, 1) PATHSEP = package.config:sub(1, 1)
@ -12,16 +12,26 @@ else
local prefix = EXEDIR:match("^(.+)[/\\]bin$") local prefix = EXEDIR:match("^(.+)[/\\]bin$")
DATADIR = prefix and (prefix .. '/share/lite-xl') or (EXEDIR .. '/data') DATADIR = prefix and (prefix .. '/share/lite-xl') or (EXEDIR .. '/data')
end end
USERDIR = (os.getenv("XDG_CONFIG_HOME") and os.getenv("XDG_CONFIG_HOME") .. "/lite-xl") USERDIR = (system.get_file_info(EXEDIR .. '/user') and (EXEDIR .. '/user'))
or (HOME and (HOME .. '/.config/lite-xl') or (EXEDIR .. '/user')) or ((os.getenv("XDG_CONFIG_HOME") and os.getenv("XDG_CONFIG_HOME") .. "/lite-xl"))
or (HOME and (HOME .. '/.config/lite-xl'))
package.path = DATADIR .. '/?.lua;' .. package.path package.path = DATADIR .. '/?.lua;'
package.path = DATADIR .. '/?/init.lua;' .. package.path package.path = DATADIR .. '/?/init.lua;' .. package.path
package.path = USERDIR .. '/?.lua;' .. package.path package.path = USERDIR .. '/?.lua;' .. package.path
package.path = USERDIR .. '/?/init.lua;' .. package.path package.path = USERDIR .. '/?/init.lua;' .. package.path
local dynamic_suffix = PLATFORM == "Mac OS X" and 'lib' or (PLATFORM == "Windows" and 'dll' or 'so') local suffix = PLATFORM == "Mac OS X" and 'lib' or (PLATFORM == "Windows" and 'dll' or 'so')
package.cpath = DATADIR .. '/?.' .. dynamic_suffix .. ";" .. USERDIR .. '/?.' .. dynamic_suffix 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.native_plugins = {}
package.searchers = { package.searchers[1], package.searchers[2], function(modname) package.searchers = { package.searchers[1], package.searchers[2], function(modname)
local path = package.searchpath(modname, package.cpath) local path = package.searchpath(modname, package.cpath)
@ -32,3 +42,15 @@ end }
table.pack = table.pack or pack or function(...) return {...} end table.pack = table.pack or pack or function(...) return {...} end
table.unpack = table.unpack or unpack 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,6 +4,7 @@ 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)
@ -27,43 +28,7 @@ style.icon_font = renderer.font.load(DATADIR .. "/fonts/icons.ttf", 16 * SCALE,
style.icon_big_font = style.icon_font:copy(23 * SCALE) style.icon_big_font = style.icon_font:copy(23 * 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", 15 * SCALE)
style.background = { common.color "#2e2e32" } -- Docview
style.background2 = { common.color "#252529" } -- Treeview
style.background3 = { common.color "#252529" } -- Command view
style.text = { common.color "#97979c" }
style.caret = { common.color "#93DDFA" }
style.accent = { common.color "#e1e1e6" }
-- style.dim - text color for nonactive tabs, tabs divider, prefix in log and
-- search result, hotkeys for context menu and command view
style.dim = { common.color "#525257" }
style.divider = { common.color "#202024" } -- Line between nodes
style.selection = { common.color "#48484f" }
style.line_number = { common.color "#525259" }
style.line_number2 = { common.color "#83838f" } -- With cursor
style.line_highlight = { common.color "#343438" }
style.scrollbar = { common.color "#414146" }
style.scrollbar2 = { common.color "#4b4b52" } -- Hovered
style.nagbar = { common.color "#FF0000" }
style.nagbar_text = { common.color "#FFFFFF" }
style.nagbar_dim = { common.color "rgba(0, 0, 0, 0.45)" }
style.drag_overlay = { common.color "rgba(255,255,255,0.1)" }
style.drag_overlay_tab = { common.color "#93DDFA" }
style.good = { common.color "#72b886" }
style.warn = { common.color "#FFA94D" }
style.error = { common.color "#FF3333" }
style.modified = { common.color "#1c7c9c" }
style.syntax = {} style.syntax = {}
style.syntax["normal"] = { common.color "#e1e1e6" }
style.syntax["symbol"] = { common.color "#e1e1e6" }
style.syntax["comment"] = { common.color "#676b6f" }
style.syntax["keyword"] = { common.color "#E58AC9" } -- local function end if case
style.syntax["keyword2"] = { common.color "#F77483" } -- self int float
style.syntax["number"] = { common.color "#FFA94D" }
style.syntax["literal"] = { common.color "#FFA94D" } -- true false nil
style.syntax["string"] = { common.color "#f7c95c" }
style.syntax["operator"] = { common.color "#93DDFA" } -- = + - / < >
style.syntax["function"] = { common.color "#93DDFA" }
-- This can be used to override fonts per syntax group. -- 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
@ -72,5 +37,7 @@ style.syntax["function"] = { common.color "#93DDFA" }
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

@ -7,6 +7,24 @@ local plain_text_syntax = { name = "Plain Text", 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

View File

@ -17,6 +17,8 @@ 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()

View File

@ -1,12 +1,15 @@
local core = require "core"
local syntax = require "core.syntax" local syntax = require "core.syntax"
local common = require "core.common" local common = require "core.common"
local tokenizer = {} local tokenizer = {}
local bad_patterns = {}
local function push_token(t, type, text) local function push_token(t, type, text)
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:find("^%s*$")) then if prev_type and (prev_type == type or prev_text:ufind("^%s*$")) then
t[#t-1] = type t[#t-1] = type
t[#t] = prev_text .. text t[#t] = prev_text .. text
else else
@ -38,12 +41,12 @@ 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:sub(start, fin) local text = full_text:usub(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:sub(start, fin) local text = full_text:usub(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
@ -52,12 +55,12 @@ end
-- State is a 32-bit number that is four separate bytes, illustrating how many -- State is a 32-bit number that is four separate bytes, illustrating how many
-- differnet delimiters we have open, and which subsyntaxes we have active. -- differnet delimiters we have open, and which subsyntaxes we have active.
-- At most, there are 3 subsyntaxes active at the same time. Beyond that, -- At most, there are 3 subsyntaxes active at the same time. Beyond that,
-- does not support further highlighting. -- does not support further highlighting.
-- You can think of it as a maximum 4 integer (0-255) stack. It always has -- 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 -- 1 integer in it. Calling `push_subsyntax` increases the stack depth. Calling
-- `pop_subsyntax` decreases it. The integers represent the index of a pattern -- `pop_subsyntax` decreases it. The integers represent the index of a pattern
-- that we're following in the syntax. The top of the stack can be any valid -- that we're following in the syntax. The top of the stack can be any valid
-- pattern index, any integer lower in the stack must represent a pattern that -- pattern index, any integer lower in the stack must represent a pattern that
-- specifies a subsyntax. -- specifies a subsyntax.
@ -92,6 +95,19 @@ local function retrieve_syntax_state(incoming_syntax, state)
return current_syntax, subsyntax_info, current_pattern_idx, current_level return current_syntax, subsyntax_info, current_pattern_idx, current_level
end end
local function report_bad_pattern(log_fn, syntax, pattern_idx, msg, ...)
if not bad_patterns[syntax] then
bad_patterns[syntax] = { }
end
if bad_patterns[syntax][pattern_idx] then return end
bad_patterns[syntax][pattern_idx] = true
log_fn("Malformed pattern #%d in %s language plugin. " .. msg,
pattern_idx, syntax.name or "unnamed", ...)
end
---@param incoming_syntax table
---@param text string
---@param state integer
function tokenizer.tokenize(incoming_syntax, text, state) function tokenizer.tokenize(incoming_syntax, text, state)
local res = {} local res = {}
local i = 1 local i = 1
@ -102,22 +118,22 @@ function tokenizer.tokenize(incoming_syntax, text, state)
state = state or 0 state = state or 0
-- incoming_syntax : the parent syntax of the file. -- incoming_syntax : the parent syntax of the file.
-- state : a 32-bit number representing syntax state (see above) -- state : a 32-bit number representing syntax state (see above)
-- current_syntax : the syntax we're currently in. -- 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
state = bit32.replace(state, pattern_idx, current_level*8, 8) state = bit32.replace(state, pattern_idx, current_level*8, 8)
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
@ -126,45 +142,90 @@ function tokenizer.tokenize(incoming_syntax, text, state)
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()
set_subsyntax_pattern_idx(0) set_subsyntax_pattern_idx(0)
current_level = current_level - 1 current_level = current_level - 1
set_subsyntax_pattern_idx(0) set_subsyntax_pattern_idx(0)
current_syntax, subsyntax_info, current_pattern_idx, current_level = current_syntax, subsyntax_info, current_pattern_idx, current_level =
retrieve_syntax_state(incoming_syntax, state) 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 }, p.regex local target, res = p.pattern or p.regex, { 1, offset - 1 }
local code = type(target) == "table" and target[close and 2 or 1] or target local p_idx = close and 2 or 1
local code = type(target) == "table" and target[p_idx] or target
if p.whole_line == nil then p.whole_line = { } end
if p.whole_line[p_idx] == nil then
-- Match patterns that start with '^'
p.whole_line[p_idx] = code:umatch("^%^") and true or false
if p.whole_line[p_idx] then
-- Remove '^' from the beginning of the pattern
if type(target) == "table" then
target[p_idx] = code:usub(2)
else
p.pattern = p.pattern and code:usub(2)
p.regex = p.regex and code:usub(2)
end
end
end
if p.regex and type(p.regex) ~= "table" then 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 local next = res[2] + 1
-- go to the start of the next utf-8 character -- If the pattern contained '^', allow matching only the whole line
while text:byte(next) and common.is_utf8_cont(text, next) do if p.whole_line[p_idx] and next > 1 then
next = next + 1 return
end end
res = p.pattern and { text:find(at_start and "^" .. code or code, next) } res = p.pattern and { text:ufind((at_start or p.whole_line[p_idx]) and "^" .. code or code, next) }
or { regex.match(code, text, next, at_start and regex.ANCHORED or 0) } or { regex.match(code, text, text:ucharpos(next), (at_start or p.whole_line[p_idx]) and regex.ANCHORED or 0) }
if res[1] and close and target[3] then if p.regex and #res > 0 then -- set correct utf8 len for regex result
local count = 0 local char_pos_1 = string.ulen(text:sub(1, res[1]))
for i = res[1] - 1, 1, -1 do local char_pos_2 = char_pos_1 + string.ulen(text:sub(res[1], res[2])) - 1
if text:byte(i) ~= target[3]:byte() then break end -- `regex.match` returns group results as a series of `begin, end`
count = count + 1 -- we only want `begin`s
if #res >= 3 then
res[3] = char_pos_1 + string.ulen(text:sub(res[1], res[3])) - 1
end end
for i=1,(#res-3) do
local curr = i + 3
local from = i * 2 + 3
if from < #res then
res[curr] = char_pos_1 + string.ulen(text:sub(res[1], res[from])) - 1
else
res[curr] = nil
end
end
res[1] = char_pos_1
res[2] = char_pos_2
end
if res[1] and target[3] then
-- Check to see if the escaped character is there, -- Check to see if the escaped character is there,
-- and if it is not itself escaped. -- and if it is not itself escaped.
if count % 2 == 0 then break end local count = 0
for i = res[1] - 1, 1, -1 do
if text:ubyte(i) ~= target[3]:ubyte() then break end
count = count + 1
end
if count % 2 == 0 then
-- The match is not escaped, so confirm it
break
elseif not close then
-- The *open* match is escaped, so avoid it
return
end
end 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 table.unpack(res)
end end
while i <= #text do local text_len = text:ulen()
while i <= text_len do
-- continue trying to match the end pattern of a pair if we have a state set -- 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]
@ -176,12 +237,12 @@ function tokenizer.tokenize(incoming_syntax, text, state)
-- 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:sub(i, ss - 1)) push_token(res, p.type, text:usub(i, ss - 1))
i = ss i = ss
cont = false cont = false
end end
@ -190,11 +251,11 @@ function tokenizer.tokenize(incoming_syntax, text, state)
-- 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:sub(i, e)) push_token(res, p.type, text:usub(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:sub(i)) push_token(res, p.type, text:usub(i))
break break
end end
end end
@ -205,7 +266,7 @@ function tokenizer.tokenize(incoming_syntax, text, state)
if subsyntax_info then 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:sub(i, e)) push_token(res, subsyntax_info.type, text:usub(i, e))
-- On finding unescaped delimiter, pop it. -- On finding unescaped delimiter, pop it.
pop_subsyntax() pop_subsyntax()
i = e + 1 i = e + 1
@ -217,6 +278,19 @@ function tokenizer.tokenize(incoming_syntax, text, state)
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
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
@ -224,7 +298,7 @@ function tokenizer.tokenize(incoming_syntax, text, state)
-- 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
@ -237,13 +311,8 @@ function tokenizer.tokenize(incoming_syntax, text, state)
-- consume character if we didn't match -- consume character if we didn't match
if not matched then if not matched then
local n = 0 push_token(res, "normal", text:usub(i, i))
-- reach the next character i = i + 1
while text:byte(i + n + 1) and common.is_utf8_cont(text, i + n + 1) do
n = n + 1
end
push_token(res, "normal", text:sub(i, i + n))
i = i + n + 1
end end
end end

32
data/core/utf8string.lua Normal file
View File

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

View File

@ -4,7 +4,51 @@ 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"
---@class core.view.position
---@field x number
---@field y number
---@class core.view.scroll
---@field x number
---@field y number
---@field to core.view.position
---@class core.view.thumbtrack
---@field thumb number
---@field track number
---@class core.view.thumbtrackwidth
---@field thumb number
---@field track number
---@field to core.view.thumbtrack
---@class core.view.scrollbar
---@field x core.view.thumbtrack
---@field y core.view.thumbtrack
---@field w core.view.thumbtrackwidth
---@field h core.view.thumbtrack
---@class core.view.increment
---@field value number
---@field to number
---@alias core.view.cursor "'arrow'" | "'ibeam'" | "'sizeh'" | "'sizev'" | "'hand'"
---@alias core.view.mousebutton "'left'" | "'right'"
---@alias core.view.context "'application'" | "'session'"
---Base view.
---@class core.view : core.object
---@field context core.view.context
---@field super core.object
---@field position core.view.position
---@field size core.view.position
---@field scroll core.view.scroll
---@field cursor core.view.cursor
---@field scrollable boolean
---@field scrollbar core.view.scrollbar
---@field scrollbar_alpha core.view.increment
local View = Object:extend() local View = Object:extend()
-- context can be "application" or "session". The instance of objects -- context can be "application" or "session". The instance of objects
@ -18,14 +62,22 @@ function View:new()
self.scroll = { x = 0, y = 0, to = { x = 0, y = 0 } } self.scroll = { x = 0, y = 0, to = { x = 0, y = 0 } }
self.cursor = "arrow" self.cursor = "arrow"
self.scrollable = false self.scrollable = false
self.scrollbar = {
x = { thumb = 0, track = 0 },
y = { thumb = 0, track = 0 },
w = { thumb = 0, track = 0, to = { thumb = 0, track = 0 } },
h = { thumb = 0, track = 0 },
}
self.scrollbar_alpha = { value = 0, to = 0 }
end end
function View:move_towards(t, k, dest, rate) function View:move_towards(t, k, dest, rate, name)
if type(t) ~= "table" then if type(t) ~= "table" then
return self:move_towards(self, t, k, dest, rate) return self:move_towards(self, t, k, dest, rate, name)
end end
local val = t[k] local val = t[k]
if not config.transitions or math.abs(val - dest) < 0.5 then local diff = math.abs(val - dest)
if not config.transitions or diff < 0.5 or config.disabled_transitions[name] then
t[k] = dest t[k] = dest
else else
rate = rate or 0.5 rate = rate or 0.5
@ -35,7 +87,7 @@ function View:move_towards(t, k, dest, rate)
end end
t[k] = common.lerp(val, dest, rate) t[k] = common.lerp(val, dest, rate)
end end
if val ~= dest then if diff > 1e-8 then
core.redraw = true core.redraw = true
end end
end end
@ -46,62 +98,146 @@ 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 x
---@return number y
---@return number width
---@return number height
function View:get_scrollbar_track_rect()
local sz = self:get_scrollable_size()
if sz <= self.size.y or sz == math.huge then
return 0, 0, 0, 0
end
local width = style.scrollbar_size
if self.hovered_scrollbar_track or self.dragging_scrollbar then
width = style.expanded_scrollbar_size
end
return
self.position.x + self.size.x - width,
self.position.y,
width,
self.size.y
end
---@return number x
---@return number y
---@return number width
---@return number height
function View:get_scrollbar_rect() function View:get_scrollbar_rect()
local sz = self:get_scrollable_size() local sz = self:get_scrollable_size()
if sz <= self.size.y or sz == math.huge then if sz <= self.size.y or sz == math.huge then
return 0, 0, 0, 0 return 0, 0, 0, 0
end end
local h = math.max(20, self.size.y * self.size.y / sz) local h = math.max(20, self.size.y * self.size.y / sz)
local width = style.scrollbar_size
if self.hovered_scrollbar_track or self.dragging_scrollbar then
width = style.expanded_scrollbar_size
end
return return
self.position.x + self.size.x - style.scrollbar_size, self.position.x + self.size.x - width,
self.position.y + self.scroll.y * (self.size.y - h) / (sz - self.size.y), self.position.y + self.scroll.y * (self.size.y - h) / (sz - self.size.y),
style.scrollbar_size, width,
h 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)
local sx, sy, sw, sh = self:get_scrollbar_rect() local sx, sy, sw, sh = self:get_scrollbar_rect()
return x >= sx - sw * 3 and x < sx + sw and y >= sy and y < sy + sh return x >= sx - style.scrollbar_size * 3 and x < sx + sw and y > sy and y <= sy + sh
end
---@param x number
---@param y number
---@return boolean
function View:scrollbar_track_overlaps_point(x, y)
local sx, sy, sw, sh = self:get_scrollbar_track_rect()
return x >= sx - style.scrollbar_size * 3 and x < sx + sw and y > sy and y <= sy + sh
end 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 self:scrollbar_overlaps_point(x, y) then if self:scrollbar_track_overlaps_point(x, y) then
self.dragging_scrollbar = true if self:scrollbar_overlaps_point(x, y) then
self.dragging_scrollbar = true
else
local _, _, _, sh = self:get_scrollbar_rect()
local ly = (y - self.position.y) - sh / 2
local pct = common.clamp(ly / self.size.y, 0, 100)
self.scroll.to.y = self:get_scrollable_size() * pct
end
return true 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)
self.dragging_scrollbar = false self.dragging_scrollbar = false
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 self.dragging_scrollbar then if self.dragging_scrollbar then
local delta = self:get_scrollable_size() / self.size.y * dy local delta = self:get_scrollable_size() / self.size.y * dy
self.scroll.to.y = self.scroll.to.y + delta self.scroll.to.y = self.scroll.to.y + delta
if not config.animate_drag_scroll then
self:clamp_scroll_position()
self.scroll.y = self.scroll.to.y
end
end end
self.hovered_scrollbar = self:scrollbar_overlaps_point(x, y) self.hovered_scrollbar = self:scrollbar_overlaps_point(x, y)
self.hovered_scrollbar_track = self.hovered_scrollbar or self:scrollbar_track_overlaps_point(x, y)
end end
function View:on_mouse_left()
self.hovered_scrollbar = false
self.hovered_scrollbar_track = false
end
---@param filename string
---@param x number
---@param y number
---@return boolean
function View:on_file_dropped(filename, x, y)
return false
end
---@param text string
function View:on_text_input(text) function View:on_text_input(text)
-- no-op -- no-op
end end
---@param y number
---@return boolean
function View:on_mouse_wheel(y) function View:on_mouse_wheel(y)
end end
@ -113,6 +249,8 @@ 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)
@ -126,13 +264,37 @@ function View:clamp_scroll_position()
end end
function View:update() function View:update_scrollbar()
self:clamp_scroll_position() local x, y, w, h = self:get_scrollbar_rect()
self:move_towards(self.scroll, "x", self.scroll.to.x, 0.3) self.scrollbar.w.to.thumb = w
self:move_towards(self.scroll, "y", self.scroll.to.y, 0.3) self:move_towards(self.scrollbar.w, "thumb", self.scrollbar.w.to.thumb, 0.3, "scroll")
self.scrollbar.x.thumb = x + w - self.scrollbar.w.thumb
self.scrollbar.y.thumb = y
self.scrollbar.h.thumb = h
local x, y, w, h = self:get_scrollbar_track_rect()
self.scrollbar.w.to.track = w
self:move_towards(self.scrollbar.w, "track", self.scrollbar.w.to.track, 0.3, "scroll")
self.scrollbar.x.track = x + w - self.scrollbar.w.track
self.scrollbar.y.track = y
self.scrollbar.h.track = h
-- we use 100 for a smoother transition
self.scrollbar_alpha.to = (self.hovered_scrollbar_track or self.dragging_scrollbar) and 100 or 0
self:move_towards(self.scrollbar_alpha, "value", self.scrollbar_alpha.to, 0.3, "scroll")
end end
function View:update()
self:clamp_scroll_position()
self:move_towards(self.scroll, "x", self.scroll.to.x, 0.3, "scroll")
self:move_towards(self.scroll, "y", self.scroll.to.y, 0.3, "scroll")
self:update_scrollbar()
end
---@param color renderer.color
function View:draw_background(color) 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
@ -140,11 +302,29 @@ function View:draw_background(color)
end end
function View:draw_scrollbar() function View:draw_scrollbar_track()
local x, y, w, h = self:get_scrollbar_rect() if not (self.hovered_scrollbar_track or self.dragging_scrollbar)
and self.scrollbar_alpha.value == 0 then
return
end
local color = { table.unpack(style.scrollbar_track) }
color[4] = color[4] * self.scrollbar_alpha.value / 100
renderer.draw_rect(self.scrollbar.x.track, self.scrollbar.y.track,
self.scrollbar.w.track, self.scrollbar.h.track, color)
end
function View:draw_scrollbar_thumb()
local highlight = self.hovered_scrollbar or self.dragging_scrollbar local highlight = self.hovered_scrollbar or self.dragging_scrollbar
local color = highlight and style.scrollbar2 or style.scrollbar local color = highlight and style.scrollbar2 or style.scrollbar
renderer.draw_rect(x, y, w, h, color) renderer.draw_rect(self.scrollbar.x.thumb, self.scrollbar.y.thumb,
self.scrollbar.w.thumb, self.scrollbar.h.thumb, color)
end
function View:draw_scrollbar()
self:draw_scrollbar_track()
self:draw_scrollbar_thumb()
end end

View File

@ -1,4 +1,4 @@
-- mod-version:2 -- lite-xl 2.0 -- mod-version:3
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,14 +10,66 @@ 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 = { config.plugins.autocomplete = common.merge({
-- 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 = {}
@ -33,7 +85,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, triggered_manually) function autocomplete.add(t, manually_triggered)
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
@ -43,9 +95,10 @@ function autocomplete.add(t, triggered_manually)
{ {
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
cb = info.cb, -- A callback called once when item is selected onhover = info.onhover, -- A callback called once when item is hovered
data = info.data -- Optional data that can be used on cb onselect = info.onselect, -- A callback called when item is selected
data = info.data -- Optional data that can be used on cb
}, },
mt mt
) )
@ -56,7 +109,7 @@ function autocomplete.add(t, triggered_manually)
end end
end end
if not triggered_manually then if not manually_triggered 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 }
@ -66,26 +119,43 @@ end
-- --
-- Thread that scans open document symbols and cache them -- Thread that scans open document symbols and cache them
-- --
local max_symbols = config.max_symbols local max_symbols = config.plugins.autocomplete.max_symbols
core.add_thread(function() 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)
if doc.disable_symbols then return {} end
local i = 1
local s = {} local s = {}
get_syntax_symbols(s, doc)
if doc.disable_symbols then return s end
local i = 1
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 document "..doc.filename.. "Too many symbols in "..filename_message..
": stopping auto-complete for this document according to config.max_symbols.") ": stopping auto-complete for this document according to "..
"config.plugins.autocomplete.max_symbols."
)
collectgarbage('collect') collectgarbage('collect')
return {} return {}
end end
@ -132,6 +202,7 @@ 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
@ -159,16 +230,6 @@ 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 ""
@ -199,6 +260,7 @@ local function update_suggestions()
j = j + 1 j = j + 1
end end
end end
suggestions_idx = 1
end end
local function get_partial_symbol() local function get_partial_symbol()
@ -209,7 +271,7 @@ local function get_partial_symbol()
end end
local function get_active_view() local function get_active_view()
if getmetatable(core.active_view) == DocView then if core.active_view:is(DocView) then
return core.active_view return core.active_view
end end
end end
@ -220,8 +282,7 @@ local function get_suggestions_rect(av)
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) local x, y = av:get_line_screen_position(line, col - #partial)
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()
@ -249,6 +310,11 @@ local function get_suggestions_rect(av)
max_width = 150 max_width = 150
end end
-- if portion not visiable to right, reposition to DocView right margin
if (x - av.position.x) + max_width > av.size.x then
x = (av.size.x + av.position.x) - max_width - (style.padding.x * 2)
end
return return
x - style.padding.x, x - style.padding.x,
y - style.padding.y, y - style.padding.y,
@ -256,20 +322,99 @@ local function get_suggestions_rect(av)
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
width = math.max(width, style.font:get_width(line)) local wrapper_lines = wrap_line(line, max_chars)
table.insert(lines, line) if type(wrapper_lines) == "table" then
for _, wrapped_line in pairs(wrapper_lines) do
width = math.max(width, font:get_width(wrapped_line))
table.insert(lines, wrapped_line)
end
else
width = math.max(width, font:get_width(line))
table.insert(lines, line)
end
end end
local height = #lines * style.font:get_height() if draw_left then
x = sx - (style.padding.x / 4) - width - (style.padding.x * 2)
end
local height = #lines * font:get_height()
-- draw background rect -- draw background rect
renderer.draw_rect( renderer.draw_rect(
sx + sw + style.padding.x / 4, x,
sy, sy,
width + style.padding.x * 2, width + style.padding.x * 2,
height + style.padding.y * 2, height + style.padding.y * 2,
@ -277,13 +422,10 @@ 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(
style.font, style.text, line, "left", x + style.padding.x, y, width, lh font, style.text, line, "left",
x + style.padding.x, y, width, lh
) )
y = y + lh y = y + lh
end end
@ -320,10 +462,9 @@ local function draw_suggestions_box(av)
end end
y = y + lh y = y + lh
if suggestions_idx == i then if suggestions_idx == i then
if s.cb then if s.onhover then
s.cb(suggestions_idx, s) s.onhover(suggestions_idx, s)
s.cb = nil s.onhover = 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)
@ -480,17 +621,26 @@ end
-- Commands -- Commands
-- --
local function predicate() local function predicate()
return get_active_view() and #suggestions > 0 local active_docview = get_active_view()
return active_docview and #suggestions > 0, active_docview
end end
command.add(predicate, { command.add(predicate, {
["autocomplete:complete"] = function() ["autocomplete:complete"] = function(dv)
local doc = core.active_view.doc local doc = dv.doc
local line, col = doc:get_selection() local line, col = doc:get_selection()
local text = suggestions[suggestions_idx].text local item = suggestions[suggestions_idx]
doc:insert(line, col, text) local text = item.text
doc:remove(line, col, line, col - #partial) local inserted = false
doc:set_selection(line, col + #text - #partial) if item.onselect then
inserted = item.onselect(suggestions_idx, item)
end
if not inserted then
local current_partial = get_partial_symbol()
doc:insert(line, col, text)
doc:remove(line, col, line, col - #current_partial)
doc:set_selection(line, col + #text - #current_partial)
end
reset_suggestions() reset_suggestions()
end, end,

View File

@ -1,44 +1,110 @@
-- mod-version:2 -- lite-xl 2.0 -- mod-version:3
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)
local info = system.get_file_info(doc.filename) times[doc] = system.get_file_info(doc.filename).modified
times[doc] = info.modified
end end
local function reload_doc(doc) local function reload_doc(doc)
local fp = io.open(doc.filename, "r") doc:reload()
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)
doc:clean() core.redraw = true
core.log_quiet("Auto-reloaded doc \"%s\"", doc.filename) core.log_quiet("Auto-reloaded doc \"%s\"", doc.filename)
end end
local on_modify = core.on_dirmonitor_modify local function check_prompt_reload(doc)
if doc and doc.deferred_reload then
core.on_dirmonitor_modify = function(dir, filepath) core.nag_view:show("File Changed", doc.filename .. " has changed. Reload this file?", {
local abs_filename = dir.name .. PATHSEP .. filepath { font = style.font, text = "Yes", default_yes = true },
for _, doc in ipairs(core.docs) do { font = style.font, text = "No" , default_no = true }
local info = system.get_file_info(doc.filename or "") }, function(item)
if doc.abs_filename == abs_filename and info and times[doc] ~= info.modified then if item.text == "Yes" then reload_doc(doc) end
reload_doc(doc) doc.deferred_reload = false
break end)
end
end end
on_modify(dir, filepath)
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 times[doc] ~= info.modified then
if not doc:is_dirty() and not config.plugins.autoreload.always_show_nagview then
reload_doc(doc)
else
doc.deferred_reload = true
if doc == core.active_view.doc then check_prompt_reload(doc) end
end
end
end
end
change_callback(dir)
end, ...)
end
local core_set_active_view = core.set_active_view
function core.set_active_view(view)
core_set_active_view(view)
doc_changes_visiblity(view.doc, true)
end
local node_set_active_view = Node.set_active_view
function Node:set_active_view(view)
if self.active_view then doc_changes_visiblity(self.active_view.doc, false) end
node_set_active_view(self, view)
doc_changes_visiblity(self.active_view.doc, true)
end
core.add_thread(function()
while true do
-- because we already hook this function above; we only
-- need to check the file.
watch:check(function() end)
coroutine.yield(0.05)
end
end)
-- patch `Doc.save|load` to store modified time -- patch `Doc.save|load` to store modified time
local load = Doc.load local load = Doc.load
local save = Doc.save local save = Doc.save
@ -51,6 +117,8 @@ 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,4 +1,4 @@
-- mod-version:2 -- lite-xl 2.0 -- mod-version:3
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"
@ -32,9 +32,9 @@ function RootView:draw(...)
menu:draw() menu:draw()
end end
command.add(nil, { command.add("core.docview!", {
["context:show"] = function() ["context:show"] = function(dv)
menu:show(core.active_view.position.x, core.active_view.position.y) menu:show(dv.position.x, dv.position.y)
end end
}) })
@ -42,23 +42,24 @@ keymap.add {
["menu"] = "context:show" ["menu"] = "context:show"
} }
local function copy_log() command.add(function() return menu.show_context_menu == true end, {
local item = core.active_view.hovered_item ["context:focus-previous"] = function()
if item then menu:focus_previous()
system.set_clipboard(core.get_log(item)) end,
end ["context:focus-next"] = function()
end menu:focus_next()
end,
local function open_as_doc() ["context:hide"] = function()
local doc = core.open_doc("logs.txt") menu:hide()
core.root_view:open_doc(doc) end,
doc:insert(1, 1, core.get_log()) ["context:on-selected"] = function()
end menu:call_selected_item()
end,
menu:register("core.logview", {
{ text = "Copy entry", command = copy_log },
{ text = "Open as file", command = open_as_doc }
}) })
keymap.add { ["return"] = "context:on-selected" }
keymap.add { ["up"] = "context:focus-previous" }
keymap.add { ["down"] = "context:focus-next" }
keymap.add { ["escape"] = "context:hide" }
if require("plugins.scale") then if require("plugins.scale") then
menu:register("core.docview", { menu:register("core.docview", {

View File

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

View File

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

View File

@ -1,12 +1,13 @@
-- mod-version:2 -- lite-xl 2.0 -- mod-version:3
local syntax = require "core.syntax" local syntax = require "core.syntax"
syntax.add { syntax.add {
name = "C", name = "C",
files = { "%.c$", "%.h$", "%.inl$" }, files = { "%.c$" },
comment = "//", comment = "//",
block_comment = { "/*", "*/" },
patterns = { patterns = {
{ pattern = "//.-\n", type = "comment" }, { pattern = "//.*", type = "comment" },
{ pattern = { "/%*", "%*/" }, type = "comment" }, { pattern = { "/%*", "%*/" }, type = "comment" },
{ pattern = { '"', '"', '\\' }, type = "string" }, { pattern = { '"', '"', '\\' }, type = "string" },
{ pattern = { "'", "'", '\\' }, type = "string" }, { pattern = { "'", "'", '\\' }, type = "string" },
@ -14,12 +15,64 @@ 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",
@ -44,6 +97,8 @@ syntax.add {
["case"] = "keyword", ["case"] = "keyword",
["default"] = "keyword", ["default"] = "keyword",
["auto"] = "keyword", ["auto"] = "keyword",
["struct"] = "keyword",
["union"] = "keyword",
["void"] = "keyword2", ["void"] = "keyword2",
["int"] = "keyword2", ["int"] = "keyword2",
["short"] = "keyword2", ["short"] = "keyword2",
@ -60,6 +115,7 @@ 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,6 +1,4 @@
-- mod-version:2 -- lite-xl 2.0 -- mod-version:3
pcall(require, "plugins.language_c")
local syntax = require "core.syntax" local syntax = require "core.syntax"
syntax.add { syntax.add {
@ -10,28 +8,101 @@ syntax.add {
"%.c++$", "%.hh$", "%.H$", "%.hxx$", "%.hpp$", "%.h++$" "%.c++$", "%.hh$", "%.H$", "%.hxx$", "%.hpp$", "%.h++$"
}, },
comment = "//", comment = "//",
block_comment = { "/*", "*/" },
patterns = { patterns = {
{ pattern = "//.-\n", type = "comment" }, { pattern = "//.*", type = "comment" },
{ pattern = { "/%*", "%*/" }, type = "comment" }, { pattern = { "/%*", "%*/" }, type = "comment" },
{ pattern = { '"', '"', '\\' }, type = "string" }, { pattern = { '"', '"', '\\' }, type = "string" },
{ pattern = { "'", "'", '\\' }, type = "string" }, { pattern = { "'", "'", '\\' }, type = "string" },
{ pattern = "0x%x+", type = "number" }, { pattern = "0x%x+", type = "number" },
{ 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 = "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"} },
{ pattern = "[%a_][%w_]*::", type = "symbol" }, -- static declarations
{ pattern = "::", type = "symbol" }, { pattern = "static()%s+()inline",
{ pattern = "[%a_][%w_]*", type = "symbol" }, type = { "keyword", "normal", "keyword" }
{ pattern = "#include%s()<.->", type = {"keyword", "string"} }, },
{ pattern = "#[%a_][%w_]*", type = "keyword" }, { pattern = "static()%s+()const",
type = { "keyword", "normal", "keyword" }
},
{ pattern = "static()%s+()[%a_][%w_]*",
type = { "keyword", "normal", "literal" }
},
-- match method type declarations
{ pattern = "[%a_][%w_]*()%s*()%**()%s*()[%a_][%w_]*()%s*()::",
type = {
"literal", "normal", "operator", "normal",
"literal", "normal", "operator"
}
},
-- match function type declarations
{ pattern = "[%a_][%w_]*()%*+()%s+()[%a_][%w_]*%f[%(]",
type = { "literal", "operator", "normal", "function" }
},
{ pattern = "[%a_][%w_]*()%s+()%*+()[%a_][%w_]*%f[%(]",
type = { "literal", "normal", "operator", "function" }
},
{ pattern = "[%a_][%w_]*()%s+()[%a_][%w_]*%f[%(]",
type = { "literal", "normal", "function" }
},
-- match variable type declarations
{ pattern = "[%a_][%w_]*()%*+()%s+()[%a_][%w_]*",
type = { "literal", "operator", "normal", "normal" }
},
{ pattern = "[%a_][%w_]*()%s+()%*+()[%a_][%w_]*",
type = { "literal", "normal", "operator", "normal" }
},
{ pattern = "[%a_][%w_]*()%s+()[%a_][%w_]*()%s*()[;,%[%)]",
type = { "literal", "normal", "normal", "normal", "normal" }
},
{ pattern = "[%a_][%w_]*()%s+()[%a_][%w_]*()%s*()=",
type = { "literal", "normal", "normal", "normal", "operator" }
},
{ pattern = "[%a_][%w_]*()&()%s+()[%a_][%w_]*",
type = { "literal", "operator", "normal", "normal" }
},
{ pattern = "[%a_][%w_]*()%s+()&()[%a_][%w_]*",
type = { "literal", "normal", "operator", "normal" }
},
-- Match scope operator element access
{ pattern = "[%a_][%w_]*()%s*()::",
type = { "literal", "normal", "operator" }
},
-- Uppercase constants of at least 2 chars in len
{ pattern = "_?%u[%u_][%u%d_]*%f[%s%+%*%-%.%)%]}%?%^%%=/<>~|&;:,!]",
type = "number"
},
-- Magic constants
{ pattern = "__[%u%l]+__", type = "number" },
-- all other functions
{ pattern = "[%a_][%w_]*%f[(]", type = "function" },
-- Macros
{ pattern = "^%s*#%s*define%s+()[%a_][%a%d_]*",
type = { "keyword", "symbol" }
},
{ pattern = "#%s*include%s+()<.->",
type = { "keyword", "string" }
},
{ pattern = "%f[#]#%s*[%a_][%w_]*", type = "keyword" },
-- Everything else to make the tokenizer work properly
{ pattern = "[%a_][%w_]*", type = "symbol" },
}, },
symbols = { 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",
@ -39,9 +110,12 @@ 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",
@ -51,8 +125,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",
@ -63,7 +137,6 @@ 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",
@ -71,6 +144,7 @@ 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",
@ -84,6 +158,8 @@ 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",
@ -95,7 +171,6 @@ syntax.add {
["case"] = "keyword", ["case"] = "keyword",
["default"] = "keyword", ["default"] = "keyword",
["auto"] = "keyword", ["auto"] = "keyword",
["const"] = "keyword",
["void"] = "keyword2", ["void"] = "keyword2",
["int"] = "keyword2", ["int"] = "keyword2",
["short"] = "keyword2", ["short"] = "keyword2",
@ -105,12 +180,18 @@ syntax.add {
["char"] = "keyword2", ["char"] = "keyword2",
["unsigned"] = "keyword2", ["unsigned"] = "keyword2",
["bool"] = "keyword2", ["bool"] = "keyword2",
["true"] = "keyword2", ["true"] = "literal",
["false"] = "keyword2", ["false"] = "literal",
["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",
@ -118,6 +199,5 @@ syntax.add {
["#warning"] = "keyword", ["#warning"] = "keyword",
["#error"] = "keyword", ["#error"] = "keyword",
["#pragma"] = "keyword", ["#pragma"] = "keyword",
}, },
} }

View File

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

View File

@ -1,31 +1,23 @@
-- mod-version:2 -- lite-xl 2.0 -- mod-version:3
local syntax = require "core.syntax" local syntax = require "core.syntax"
syntax.add { syntax.add {
name = "HTML", name = "HTML",
files = { "%.html?$" }, files = { "%.html?$" },
block_comment = { "<!--", "-->" },
patterns = { patterns = {
{ {
pattern = { pattern = {
"<%s*[sS][cC][rR][iI][pP][tT]%s+[tT][yY][pP][eE]%s*=%s*" .. "<%s*[sS][cC][rR][iI][pP][tT]%f[%s>].->",
"['\"]%a+/[jJ][aA][vV][aA][sS][cC][rR][iI][pP][tT]['\"]%s*>", "<%s*/%s*[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][^>]*>", "<%s*[sS][tT][yY][lL][eE]%f[%s>].->",
"<%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,12 +1,13 @@
-- mod-version:2 -- lite-xl 2.0 -- mod-version:3
local syntax = require "core.syntax" local syntax = require "core.syntax"
syntax.add { syntax.add {
name = "JavaScript", name = "JavaScript",
files = { "%.js$", "%.json$", "%.cson$" }, files = { "%.js$", "%.json$", "%.cson$", "%.mjs$", "%.cjs$" },
comment = "//", comment = "//",
block_comment = { "/*", "*/" },
patterns = { patterns = {
{ pattern = "//.-\n", type = "comment" }, { pattern = "//.*", type = "comment" },
{ pattern = { "/%*", "%*/" }, type = "comment" }, { pattern = { "/%*", "%*/" }, type = "comment" },
{ pattern = { '/[^= ]', '/', '\\' },type = "string" }, { pattern = { '/[^= ]', '/', '\\' },type = "string" },
{ pattern = { '"', '"', '\\' }, type = "string" }, { pattern = { '"', '"', '\\' }, type = "string" },

View File

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

View File

@ -1,4 +1,4 @@
-- mod-version:2 -- lite-xl 2.0 -- mod-version:3
local syntax = require "core.syntax" local syntax = require "core.syntax"
syntax.add { syntax.add {
@ -6,9 +6,14 @@ syntax.add {
files = { "%.py$", "%.pyw$", "%.rpy$" }, files = { "%.py$", "%.pyw$", "%.rpy$" },
headers = "^#!.*[ /]python", headers = "^#!.*[ /]python",
comment = "#", comment = "#",
block_comment = { '"""', '"""' },
patterns = { patterns = {
{ pattern = { "#", "\n" }, type = "comment" }, { pattern = "#.*", type = "comment" },
{ pattern = { '^%s*"""', '"""' }, type = "comment" },
{ pattern = '[uUrR]%f["]', type = "keyword" },
{ pattern = "class%s+()[%a_][%w_]*", type = {"keyword", "keyword2"} },
{ pattern = { '[ruU]?"""', '"""'; '\\' }, type = "string" }, { pattern = { '[ruU]?"""', '"""'; '\\' }, type = "string" },
{ pattern = { "[ruU]?'''", "'''", '\\' }, type = "string" },
{ pattern = { '[ruU]?"', '"', '\\' }, type = "string" }, { pattern = { '[ruU]?"', '"', '\\' }, type = "string" },
{ pattern = { "[ruU]?'", "'", '\\' }, type = "string" }, { pattern = { "[ruU]?'", "'", '\\' }, type = "string" },
{ pattern = "0x[%da-fA-F]+", type = "number" }, { pattern = "0x[%da-fA-F]+", type = "number" },
@ -28,6 +33,8 @@ 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",
@ -40,6 +47,8 @@ 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,10 +1,11 @@
-- mod-version:2 -- lite-xl 2.0 -- mod-version:3
local syntax = require "core.syntax" local syntax = require "core.syntax"
syntax.add { syntax.add {
name = "XML", 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,21 +1,115 @@
-- mod-version:2 -- lite-xl 2.0 -- mod-version:3
local common = require "core.common"
local command = require "core.command"
local config = require "core.config" local 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" local CommandView = require "core.commandview"
local draw_overlay = DocView.draw_overlay config.plugins.lineguide = common.merge({
enabled = false,
width = 2,
rulers = {
-- 80,
-- 100,
-- 120,
config.line_limit
},
-- The config specification used by gui generators
config_spec = {
name = "Line Guide",
{
label = "Enabled",
description = "Disable or enable drawing of the line guide.",
path = "enabled",
type = "toggle",
default = true
},
{
label = "Width",
description = "Width in pixels of the line guide.",
path = "width",
type = "number",
default = 2,
min = 1
},
{
label = "Ruler Positions",
description = "The different column numbers for the line guides to draw.",
path = "rulers",
type = "list_strings",
default = { tostring(config.line_limit) or "80" },
get_value = function(rulers)
if type(rulers) == "table" then
local new_rulers = {}
for _, ruler in ipairs(rulers) do
table.insert(new_rulers, tostring(ruler))
end
return new_rulers
else
return { tostring(config.line_limit) }
end
end,
set_value = function(rulers)
local new_rulers = {}
for _, ruler in ipairs(rulers) do
local number = tonumber(ruler)
if number then
table.insert(new_rulers, number)
end
end
if #new_rulers == 0 then
table.insert(new_rulers, config.line_limit)
end
return new_rulers
end
}
}
}, config.plugins.lineguide)
function DocView:draw_overlay(...) local function get_ruler(v)
if not self:is(CommandView) then local result = nil
local offset = self:get_font():get_width("n") * config.line_limit if type(v) == 'number' then
local x = self:get_line_screen_position(1) + offset result = { columns = v }
local y = self.position.y elseif type(v) == 'table' then
local w = math.ceil(SCALE * 1) result = v
local h = self.size.y
local color = style.guide or style.selection
renderer.draw_rect(x, y, w, h, color)
end end
draw_overlay(self, ...) return result
end end
local draw_overlay = DocView.draw_overlay
function DocView:draw_overlay(...)
draw_overlay(self, ...)
if
type(config.plugins.lineguide) == "table"
and
config.plugins.lineguide.enabled
and
not self:is(CommandView)
then
local line_x = self:get_line_screen_position(1)
local character_width = self:get_font():get_width("n")
local ruler_width = config.plugins.lineguide.width
local ruler_color = style.guide or style.selection
for k,v in ipairs(config.plugins.lineguide.rulers) do
local ruler = get_ruler(v)
if ruler then
local x = line_x + (character_width * ruler.columns)
local y = self.position.y
local w = ruler_width
local h = self.size.y
renderer.draw_rect(x, y, w, h, ruler.color or ruler_color)
end
end
end
end
command.add(nil, {
["lineguide:toggle"] = function()
config.plugins.lineguide.enabled = not config.plugins.lineguide.enabled
end
})

View File

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

View File

@ -1,4 +1,4 @@
-- mod-version:2 -- lite-xl 2.0 -- mod-version:3
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:2 -- lite-xl 2.0 -- mod-version:3
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"
@ -11,11 +11,11 @@ local ResultsView = View:extend()
ResultsView.context = "session" ResultsView.context = "session"
function ResultsView:new(text, fn) function ResultsView:new(path, 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(text, fn) self:begin_search(path, text, fn)
end end
@ -45,8 +45,8 @@ local function find_all_matches_in_file(t, filename, fn)
end end
function ResultsView:begin_search(text, fn) function ResultsView:begin_search(path, text, fn)
self.search_args = { text, fn } self.search_args = { path, 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(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" then if file.type == "file" and (not path or (dir_name .. "/" .. file.filename):find(path, 1, true) == 1) then
local path = (dir_name == core.project_dir and "" or (dir_name .. PATHSEP)) local truncated_path = (dir_name == core.project_dir and "" or (dir_name .. PATHSEP))
find_all_matches_in_file(self.results, path .. file.filename, fn) find_all_matches_in_file(self.results, truncated_path .. file.filename, fn)
end end
self.last_file_idx = i self.last_file_idx = i
i = i + 1 i = i + 1
@ -176,7 +176,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 %d%% (%d of %d files, %d matches) for %q...", text = string.format("Searching %.f%% (%d of %d files, %d matches) for %q...",
per * 100, self.last_file_idx, files_number, per * 100, self.last_file_idx, files_number,
#self.results, self.query) #self.results, self.query)
else else
@ -219,41 +219,72 @@ function ResultsView:draw()
end end
local function begin_search(text, fn) 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(text, fn) local rv = ResultsView(path, text, fn)
core.root_view:get_active_node_default():add_view(rv) core.root_view:get_active_node_default():add_view(rv)
end end
local function get_selected_text()
local view = core.active_view
local doc = (view and view.doc) and view.doc or nil
if doc then
return doc:get_text(table.unpack({ doc:get_selection() }))
end
end
local function normalize_path(path)
if not path then return nil end
path = common.normalize_path(path)
for i, project_dir in ipairs(core.project_directories) do
if common.path_belongs_to(path, project_dir.name) then
return project_dir.item.filename .. PATHSEP .. common.relative_path(project_dir.name, path)
end
end
return path
end
command.add(nil, { command.add(nil, {
["project-search:find"] = function() ["project-search:find"] = function(path)
core.command_view:enter("Find Text In Project", function(text) core.command_view:enter("Find Text In " .. (normalize_path(path) or "Project"), {
text = text:lower() text = get_selected_text(),
begin_search(text, function(line_text) select_text = true,
return line_text:lower():find(text, nil, true) submit = function(text)
end) text = text:lower()
end) begin_search(path, text, function(line_text)
return line_text:lower():find(text, nil, true)
end)
end
})
end, end,
["project-search:find-regex"] = function() ["project-search:find-regex"] = function(path)
core.command_view:enter("Find Regex In Project", function(text) core.command_view:enter("Find Regex In " .. (normalize_path(path) or "Project"), {
local re = regex.compile(text, "i") submit = function(text)
begin_search(text, function(line_text) local re = regex.compile(text, "i")
return regex.cmatch(re, line_text) begin_search(path, text, function(line_text)
end) return regex.cmatch(re, line_text)
end) end)
end
})
end, end,
["project-search:fuzzy-find"] = function() ["project-search:fuzzy-find"] = function(path)
core.command_view:enter("Fuzzy Find Text In Project", function(text) core.command_view:enter("Fuzzy Find Text In " .. (normalize_path(path) or "Project"), {
begin_search(text, function(line_text) text = get_selected_text(),
return common.fuzzy_match(line_text, text) and 1 select_text = true,
end) submit = function(text)
end) begin_search(path, text, function(line_text)
return common.fuzzy_match(line_text, text) and 1
end)
end
})
end, end,
}) })
@ -278,22 +309,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()

View File

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

View File

@ -1,17 +1,20 @@
-- mod-version:2 -- lite-xl 2.0 -- mod-version:3
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 = { config.plugins.scale = common.merge({
-- 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
@ -44,18 +47,14 @@ local function set_scale(scale)
style.tab_width = style.tab_width * s style.tab_width = style.tab_width * s
for _, name in ipairs {"font", "big_font", "icon_font", "icon_big_font", "code_font"} do for _, name in ipairs {"font", "big_font", "icon_font", "icon_big_font", "code_font"} do
style[name] = renderer.font.copy(style[name], s * style[name]:get_size()) style[name]:set_size(s * style[name]:get_size())
end end
else else
style.code_font = renderer.font.copy(style.code_font, s * style.code_font:get_size()) style.code_font:set_size(s * style.code_font:get_size())
end end
for _, font in pairs(style.syntax_fonts) do for name, font in pairs(style.syntax_fonts) do
renderer.font.set_size(font, s * font:get_size()) style.syntax_fonts[name]:set_size(s * font:get_size())
end
for _, font in pairs(style.syntax_fonts) do
renderer.font.set_size(font, s * font:get_size())
end end
-- restore scroll positions -- restore scroll positions
@ -83,6 +82,75 @@ 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,
@ -93,11 +161,16 @@ 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"
["ctrl+wheelup"] = "scale:increase",
["ctrl+wheeldown"] = "scale:decrease"
} }
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:2 -- lite-xl 2.0 -- mod-version:3
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,21 +41,23 @@ end
command.add("core.docview", { command.add("core.docview", {
["tabularize:tabularize"] = function() ["tabularize:tabularize"] = function(dv)
core.command_view:enter("Tabularize On Delimiter", function(delim) core.command_view:enter("Tabularize On Delimiter", {
if delim == "" then delim = " " end submit = function(delim)
if delim == "" then delim = " " end
local doc = core.active_view.doc local doc = dv.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:2 -- lite-xl 2.0 -- mod-version:3
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,31 +7,26 @@ 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 toolbar_height() or 0 local dest_size = self.visible and (self.toolbar_font:get_height() + style.padding.y * 2) or 0
if self.init_size then if self.init_size then
self.size.y = dest_size self.size.y = dest_size
self.init_size = nil self.init_size = nil
@ -46,19 +41,24 @@ function ToolbarView:toggle_visible()
self.visible = not self.visible self.visible = not self.visible
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 = style.icon_big_font:get_height(), style.icon_big_font:get_width("D") local icon_h, icon_w = self.toolbar_font:get_height(), self:get_icon_width()
local toolbar_spacing = icon_w / 2 local 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 <= #toolbar_commands then if index <= #self.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 toolbar_commands[index], ox + dx, oy + dy, icon_w, icon_h return self.toolbar_commands[index], ox + dx, oy + dy, icon_w, icon_h
end end
end end
return iter return iter
@ -66,9 +66,9 @@ end
function ToolbarView:get_min_width() function ToolbarView:get_min_width()
local icon_w = style.icon_big_font:get_width("D") local icon_w = self:get_icon_width()
local space = icon_w / 2 local space = icon_w / 2
return 2 * style.padding.x + (icon_w + space) * #toolbar_commands - space return 2 * style.padding.x + (icon_w + space) * #self.toolbar_commands - space
end end
@ -76,19 +76,20 @@ function ToolbarView:draw()
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 style.text or style.dim local color = item == self.hovered_item and command.is_valid(item.command) and style.text or style.dim
common.draw_text(style.icon_big_font, color, item.symbol, nil, x, y, 0, h) common.draw_text(self.toolbar_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)
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 end if caught then return caught end
core.set_active_view(core.last_active_view) core.set_active_view(core.last_active_view)
if self.hovered_item then if self.hovered_item and command.is_valid(self.hovered_item.command) then
command.perform(self.hovered_item.command) command.perform(self.hovered_item.command)
end end
return true
end end

View File

@ -1,4 +1,4 @@
-- mod-version:2 -- lite-xl 2.0 -- mod-version:3
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,9 +8,13 @@ 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
@ -39,19 +43,26 @@ 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 = default_treeview_size self.target_size = config.plugins.treeview.size
self.cache = {} self.cache = {}
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_icon_width = 0
self.item_text_spacing = 0 self.item_text_spacing = 0
local on_dirmonitor_modify = core.on_dirmonitor_modify self:add_core_hooks()
function core.on_dirmonitor_modify(dir, filepath) end
if self.cache[dir.name] then
self.cache[dir.name][filepath] = nil
end function TreeView:add_core_hooks()
on_dirmonitor_modify(dir, filepath) -- When a file or directory is deleted we delete the corresponding cache entry
-- because if the entry is recreated we may use wrong information from cache.
local on_delete = core.on_dirmonitor_delete
core.on_dirmonitor_delete = function(dir, filepath)
local cache = self.cache[dir.name]
if cache then cache[filepath] = nil end
on_delete(dir, filepath)
end end
end end
@ -90,7 +101,7 @@ function TreeView:get_cached(dir, item, dirname)
end end
t.name = basename t.name = basename
t.type = item.type t.type = item.type
t.dir = dir -- points to top level "dir" item t.dir_name = dir.name -- points to top level "dir" item
dir_cache[cache_name] = t dir_cache[cache_name] = t
end end
return t return t
@ -98,7 +109,7 @@ end
function TreeView:get_name() function TreeView:get_name()
return "Project" return nil
end end
@ -142,34 +153,51 @@ function TreeView:each_item()
count_lines = count_lines + 1 count_lines = count_lines + 1
y = y + h y = y + h
local i = 1 local i = 1
while i <= #dir.files and dir_cached.expanded do if dir.files then -- if consumed max sys file descriptors this can be nil
local item = dir.files[i] while i <= #dir.files and dir_cached.expanded do
local cached = self:get_cached(dir, item, dir.name) local item = dir.files[i]
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 end -- while files
end -- while files end
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
@ -180,8 +208,14 @@ 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, ...) TreeView.super.on_mouse_moved(self, px, py, ...)
if self.dragging_scrollbar then return end self.cursor_pos.x = px
self.cursor_pos.y = py
if self.dragging_scrollbar then
self.hovered_item = nil
return
end
local item_changed, tooltip_changed 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
@ -203,50 +237,6 @@ 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
end)
end
function TreeView:on_mouse_pressed(button, x, y, clicks)
local caught = TreeView.super.on_mouse_pressed(self, button, x, y, clicks)
if caught or button ~= "left" then
return true
end
local hovered_item = self.hovered_item
if not hovered_item then
return false
elseif hovered_item.type == "dir" then
if keymap.modkeys["ctrl"] and button == "left" then
create_directory_in(hovered_item)
else
hovered_item.expanded = not hovered_item.expanded
if hovered_item.dir.files_limit then
core.update_project_subdir(hovered_item.dir, hovered_item.filename, hovered_item.expanded)
core.project_subdir_set_show(hovered_item.dir, hovered_item.filename, hovered_item.expanded)
end
end
else
core.try(function()
if core.last_active_view and core.active_view == self then
core.set_active_view(core.last_active_view)
end
local doc_filename = core.normalize_to_project_dir(hovered_item.abs_filename)
core.root_view:open_doc(core.open_doc(doc_filename))
end)
end
return true
end
function TreeView:update() 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
@ -254,12 +244,14 @@ 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) self:move_towards(self.size, "x", dest, nil, "treeview")
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) self:move_towards(self.tooltip, "alpha", tooltip_alpha, tooltip_alpha_rate, "treeview")
else else
self.tooltip.alpha = 0 self.tooltip.alpha = 0
end end
@ -267,6 +259,13 @@ function TreeView:update()
self.item_icon_width = style.icon_font:get_width("D") self.item_icon_width = style.icon_font:get_width("D")
self.item_text_spacing = style.icon_font:get_width("f") / 2 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
@ -350,6 +349,10 @@ end
function TreeView:draw_item_background(item, active, hovered, x, y, w, h) function TreeView:draw_item_background(item, active, hovered, x, y, w, h)
if hovered then 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) renderer.draw_rect(x, y, w, h, style.line_highlight)
end end
end end
@ -366,6 +369,7 @@ 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 _y, _h = self.position.y, self.size.y
@ -375,23 +379,86 @@ function TreeView:draw()
for item, x,y,w,h in self:each_item() do for item, x,y,w,h in self:each_item() do
if y + h >= _y and y < _y + _h then if y + h >= _y and y < _y + _h then
self:draw_item(item, self:draw_item(item,
item.abs_filename == active_filename, item == self.selected_item,
item == self.hovered_item, item == self.hovered_item,
x, y, w, h) x, y, w, h)
end end
end end
self:draw_scrollbar() self:draw_scrollbar()
if self.hovered_item and self.tooltip.alpha > 0 then if self.hovered_item and self.tooltip.x and self.tooltip.alpha > 0 then
core.root_view:defer_draw(self.draw_tooltip, self) 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()
local treeview_node = node:split("left", view, {x = true}, true) view.node = node:split("left", view, {x = true}, true)
-- The toolbarview plugin is special because it is plugged inside -- 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.
@ -400,12 +467,12 @@ local treeview_node = node:split("left", view, {x = true}, true)
-- plugin module that plug itself in the active node but it is plugged here -- 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 = core.try(require, "plugins.toolbarview") local toolbar_plugin, ToolbarView = pcall(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()
treeview_node:split("down", toolbar_view, {y = true}) view.node:split("down", toolbar_view, {y = true})
local min_toolbar_width = toolbar_view:get_min_width() local min_toolbar_width = toolbar_view:get_min_width()
view:set_target_size("x", math.max(default_treeview_size, min_toolbar_width)) view:set_target_size("x", math.max(config.plugins.treeview.size, min_toolbar_width))
command.add(nil, { command.add(nil, {
["toolbar:toggle"] = function() ["toolbar:toggle"] = function()
toolbar_view:toggle_visible() toolbar_view:toggle_visible()
@ -446,7 +513,22 @@ 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
if dir.name == path then
return true
end
end
return false
end
local function is_primary_project_folder(path)
return core.project_dir == path return core.project_dir == path
end end
@ -476,65 +558,143 @@ menu:register(
} }
) )
menu:register(
function()
return view.hovered_item
and not is_primary_project_folder(view.hovered_item.abs_filename)
and is_project_folder(view.hovered_item.abs_filename)
end,
{
{ text = "Remove directory", command = "treeview:remove-project-directory" },
}
)
local previous_view = nil
-- Register the TreeView commands and keymap -- 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()
command.add(function() return view.hovered_item ~= nil end, { if not core.active_view:is(TreeView) then
["treeview:rename"] = function() if core.active_view:is(CommandView) then
local old_filename = view.hovered_item.filename previous_view = core.last_active_view
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
core.error("Error while renaming \"%s\" to \"%s\": %s", old_abs_filename, abs_filename, err) previous_view = core.active_view
end
if not previous_view then
previous_view = core.root_view:get_primary_node().active_view
end
core.set_active_view(view)
if not view.selected_item then
for it, _, y in view:each_item() do
view:set_selection(it, y)
break
end
end end
end, common.path_suggest)
end,
["treeview:new-file"] = function() else
if not is_project_folder(view.hovered_item.abs_filename) then core.set_active_view(
core.command_view:set_text(view.hovered_item.filename .. "/") previous_view or core.root_view:get_primary_node().active_view
)
end end
core.command_view:enter("Filename", function(filename) end
local doc_filename = core.project_dir .. PATHSEP .. filename })
local file = io.open(doc_filename, "a+")
file:write("") command.add(TreeView, {
file:close() ["treeview:next"] = function()
core.root_view:open_doc(core.open_doc(doc_filename)) local item, _, item_y = view:get_next(view.selected_item)
core.log("Created %s", doc_filename) view:set_selection(item, item_y)
end, common.path_suggest)
end, end,
["treeview:new-folder"] = function() ["treeview:previous"] = function()
if not is_project_folder(view.hovered_item.abs_filename) then local item, _, item_y = view:get_previous(view.selected_item)
core.command_view:set_text(view.hovered_item.filename .. "/") view:set_selection(item, item_y)
end,
["treeview:open"] = function()
local item = view.selected_item
if not item then return end
if item.type == "dir" then
view:toggle_expand()
else
core.try(function()
if core.last_active_view and core.active_view == view then
core.set_active_view(core.last_active_view)
end
local doc_filename = core.normalize_to_project_dir(item.abs_filename)
core.root_view:open_doc(core.open_doc(doc_filename))
end)
end end
core.command_view:enter("Folder Name", function(filename)
local dir_path = core.project_dir .. PATHSEP .. filename
common.mkdirp(dir_path)
core.log("Created %s", dir_path)
end, common.path_suggest)
end, end,
["treeview:delete"] = function() ["treeview:deselect"] = function()
local filename = view.hovered_item.abs_filename view.selected_item = nil
local relfilename = view.hovered_item.filename end,
["treeview:select"] = function()
view:set_selection(view.hovered_item)
end,
["treeview:select-and-open"] = function()
if view.hovered_item then
view:set_selection(view.hovered_item)
command.perform "treeview:open"
end
end,
["treeview:collapse"] = function()
if view.selected_item then
if view.selected_item.type == "dir" and view.selected_item.expanded then
view:toggle_expand(false)
else
local parent_item, y = view:get_parent(view.selected_item)
if parent_item then
view:set_selection(parent_item, y)
end
end
end
end,
["treeview:expand"] = function()
local item = view.selected_item
if not item or item.type ~= "dir" then return end
if item.expanded then
local next_item, _, next_y = view:get_next(item)
if next_item.depth > item.depth then
view:set_selection(next_item, next_y)
end
else
view:toggle_expand(true)
end
end,
})
local function treeitem() return view.hovered_item or view.selected_item end
command.add(
function()
local item = treeitem()
return item ~= nil
and (
core.active_view == view or core.active_view == menu
or (view.toolbar and core.active_view == view.toolbar)
-- sometimes the context menu is shown on top of statusbar
or core.active_view == core.status_view
), item
end, {
["treeview:delete"] = function(item)
local filename = item.abs_filename
local relfilename = item.filename
if item.dir_name ~= core.project_dir then
-- add secondary project dirs names to the file path to show
relfilename = common.basename(item.dir_name) .. PATHSEP .. relfilename
end
local file_info = system.get_file_info(filename) local file_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
@ -568,22 +728,175 @@ command.add(function() return view.hovered_item ~= nil end, {
end end
end end
) )
end
})
command.add(function()
if not (core.root_view.overlapping_node and core.root_view.overlapping_node.active_view) then return end
if core.root_view.overlapping_node.active_view ~= view then return end
local item = treeitem()
return item ~= nil, item
end, {
["treeview:rename"] = function(item)
local old_filename = item.filename
local old_abs_filename = item.abs_filename
core.command_view:enter("Rename", {
text = old_filename,
submit = function(filename)
local abs_filename = filename
if not common.is_absolute_path(filename) then
abs_filename = item.dir_name .. PATHSEP .. filename
end
local res, err = os.rename(old_abs_filename, abs_filename)
if res then -- successfully renamed
for _, doc in ipairs(core.docs) do
if doc.abs_filename and old_abs_filename == doc.abs_filename then
doc:set_filename(filename, abs_filename) -- make doc point to the new filename
doc:reset_syntax()
break -- only first needed
end
end
core.log("Renamed \"%s\" to \"%s\"", old_filename, filename)
else
core.error("Error while renaming \"%s\" to \"%s\": %s", old_abs_filename, abs_filename, err)
end
end,
suggest = function(text)
return common.path_suggest(text, item.dir_name)
end
})
end, end,
["treeview:open-in-system"] = function() ["treeview:new-file"] = function(item)
local hovered_item = view.hovered_item local text
if not is_project_folder(item.abs_filename) then
if PLATFORM == "Windows" then text = item.filename .. PATHSEP
system.exec(string.format("start \"\" %q", hovered_item.abs_filename))
elseif string.find(PLATFORM, "Mac") then
system.exec(string.format("open %q", hovered_item.abs_filename))
elseif PLATFORM == "Linux" then
system.exec(string.format("xdg-open %q", hovered_item.abs_filename))
end end
core.command_view:enter("Filename", {
text = text,
submit = function(filename)
local doc_filename = item.dir_name .. PATHSEP .. filename
core.log(doc_filename)
local file = io.open(doc_filename, "a+")
file:write("")
file:close()
core.root_view:open_doc(core.open_doc(doc_filename))
core.log("Created %s", doc_filename)
end,
suggest = function(text)
return common.path_suggest(text, item.dir_name)
end
})
end,
["treeview:new-folder"] = function(item)
local text
if not is_project_folder(item.abs_filename) then
text = item.filename .. PATHSEP
end
core.command_view:enter("Folder Name", {
text = text,
submit = function(filename)
local dir_path = item.dir_name .. PATHSEP .. filename
common.mkdirp(dir_path)
core.log("Created %s", dir_path)
end,
suggest = function(text)
return common.path_suggest(text, item.dir_name)
end
})
end,
["treeview:open-in-system"] = function(item)
if PLATFORM == "Windows" then
system.exec(string.format("start \"\" %q", item.abs_filename))
elseif string.find(PLATFORM, "Mac") then
system.exec(string.format("open %q", item.abs_filename))
elseif PLATFORM == "Linux" or string.find(PLATFORM, "BSD") then
system.exec(string.format("xdg-open %q", item.abs_filename))
end
end
})
local projectsearch = pcall(require, "plugins.projectsearch")
if projectsearch then
menu:register(function()
return view.hovered_item and view.hovered_item.type == "dir"
end, {
{ text = "Find in directory", command = "treeview:search-in-directory" }
})
command.add(function()
return view.hovered_item and view.hovered_item.type == "dir"
end, {
["treeview:search-in-directory"] = function(item)
command.perform("project-search:find", view.hovered_item.abs_filename)
end
})
end
command.add(function()
local item = treeitem()
return item
and not is_primary_project_folder(item.abs_filename)
and is_project_folder(item.abs_filename), item
end, {
["treeview:remove-project-directory"] = function(item)
core.remove_project_directory(item.dir_name)
end, 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,4 +1,4 @@
-- mod-version:2 -- lite-xl 2.0 -- mod-version:3
local core = require "core" local core = require "core"
local command = require "core.command" local command = require "core.command"
local Doc = require "core.doc" local Doc = require "core.doc"
@ -24,8 +24,8 @@ end
command.add("core.docview", { command.add("core.docview", {
["trim-whitespace:trim-trailing-whitespace"] = function() ["trim-whitespace:trim-trailing-whitespace"] = function(dv)
trim_trailing_whitespace(core.active_view.doc) trim_trailing_whitespace(dv.doc)
end, end,
}) })

View File

@ -1,4 +1,4 @@
-- mod-version:2 -- lite-xl 2.0 -- mod-version:3
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"
@ -117,7 +117,7 @@ local function load_view(t)
-- cannot be read. -- cannot be read.
if dv and dv.doc then if dv and dv.doc then
dv.doc:set_selection(table.unpack(t.selection)) dv.doc:set_selection(table.unpack(t.selection))
dv.last_line, dv.last_col = dv.doc:get_selection() dv.last_line1, dv.last_col1, dv.last_line2, dv.last_col2 = dv.doc:get_selection()
dv.scroll.x, dv.scroll.to.x = t.scroll.x, t.scroll.x dv.scroll.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

View File

@ -4,6 +4,11 @@
---@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"

View File

@ -124,7 +124,7 @@ process.options = {}
---@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

View File

@ -6,7 +6,9 @@
renderer = {} renderer = {}
--- ---
---Represents a color used by the rendering functions. ---Array of bytes that represents a color used by the rendering functions.
---Note: indexes for rgba are numerical 1 = r, 2 = g, 3 = b, 4 = a but for
---documentation purposes the letters r, g, b, a were used.
---@class renderer.color ---@class renderer.color
---@field public r number Red ---@field public r number Red
---@field public g number Green ---@field public g number Green
@ -17,11 +19,13 @@ 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 "'grayscale'" | "'subpixel'" ---@field public antialiasing "'none'" | "'grayscale'" | "'subpixel'"
---@field public hinting "'slight'" | "'none'" | '"full"' ---@field public hinting "'slight'" | "'none'" | '"full"'
-- @field public bold boolean -- @field public bold boolean
-- @field public italic boolean -- @field public italic boolean
-- @field public underline boolean -- @field public underline boolean
-- @field public smoothing boolean
-- @field public strikethrough boolean
renderer.fontoptions = {} renderer.fontoptions = {}
--- ---
@ -33,18 +37,29 @@ 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) end function renderer.font:copy(size, options) end
--- ---
---Set the amount of characters that represent a tab. ---Set the amount of characters that represent a tab.
@ -81,23 +96,11 @@ function renderer.font:get_size() end
function renderer.font:set_size(size) end function renderer.font:set_size(size) end
--- ---
---Assistive functionality to replace characters in a ---Get the current path of the font as a string if a single font or as an
---rendered text with other characters. ---array of strings if a group font.
---@class renderer.replacements
renderer.replacements = {}
--- ---
---Create a new character replacements object. ---@return string | table<integer, string>
--- 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
@ -141,29 +144,13 @@ 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. ---Draw text and return the x coordinate where the text finished drawing.
--- ---
---@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_subpixel ---@return number x
function renderer.draw_text(font, text, x, y, color, replace, color_replace) end function renderer.draw_text(font, text, x, y, color) end
---
---Draw text at subpixel level.
---
---@param font renderer.font
---@param text string
---@param x number
---@param y number
---@param color renderer.color
---@param replace renderer.replacements
---@param color_replace renderer.color
---
---@return number x_subpixel
function renderer.draw_text_subpixel(font, text, x, y, color, replace, color_replace) end

165
docs/api/string.lua Normal file
View File

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

View File

@ -192,6 +192,12 @@ 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 it self.
---
---@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()

191
docs/api/utf8extra.lua Normal file
View File

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

File diff suppressed because it is too large Load Diff

View File

@ -1,162 +0,0 @@
#ifndef __DMON_EXTRA_H__
#define __DMON_EXTRA_H__
//
// Copyright 2021 Sepehr Taghdisian (septag@github). All rights reserved.
// License: https://github.com/septag/dmon#license-bsd-2-clause
//
// Extra header functionality for dmon.h for the backend based on inotify
//
// Add/Remove directory functions:
// dmon_watch_add: Adds a sub-directory to already valid watch_id. sub-directories are assumed to be relative to watch root_dir
// dmon_watch_add: Removes a sub-directory from already valid watch_id. sub-directories are assumed to be relative to watch root_dir
// Reason: The inotify backend does not work well when watching in recursive mode a root directory of a large tree, it may take
// quite a while until all inotify watches are established, and events will not be received in this time. Also, since one
// inotify watch will be established per subdirectory, it is possible that the maximum amount of inotify watches per user
// will be reached. The default maximum is 8192.
// When using inotify backend users may turn off the DMON_WATCHFLAGS_RECURSIVE flag and add/remove selectively the
// sub-directories to be watched based on application-specific logic about which sub-directory actually needs to be watched.
// The function dmon_watch_add and dmon_watch_rm are used to this purpose.
//
#ifndef __DMON_H__
#error "Include 'dmon.h' before including this file"
#endif
#ifdef __cplusplus
extern "C" {
#endif
DMON_API_DECL bool dmon_watch_add(dmon_watch_id id, const char* subdir);
DMON_API_DECL bool dmon_watch_rm(dmon_watch_id id, const char* watchdir);
#ifdef __cplusplus
}
#endif
#ifdef DMON_IMPL
#if DMON_OS_LINUX
DMON_API_IMPL bool dmon_watch_add(dmon_watch_id id, const char* watchdir)
{
DMON_ASSERT(id.id > 0 && id.id <= DMON_MAX_WATCHES);
bool skip_lock = pthread_self() == _dmon.thread_handle;
if (!skip_lock)
pthread_mutex_lock(&_dmon.mutex);
dmon__watch_state* watch = &_dmon.watches[id.id - 1];
// check if the directory exists
// if watchdir contains absolute/root-included path, try to strip the rootdir from it
// else, we assume that watchdir is correct, so save it as it is
struct stat st;
dmon__watch_subdir subdir;
if (stat(watchdir, &st) == 0 && (st.st_mode & S_IFDIR)) {
dmon__strcpy(subdir.rootdir, sizeof(subdir.rootdir), watchdir);
if (strstr(subdir.rootdir, watch->rootdir) == subdir.rootdir) {
dmon__strcpy(subdir.rootdir, sizeof(subdir.rootdir), watchdir + strlen(watch->rootdir));
}
} else {
char fullpath[DMON_MAX_PATH];
dmon__strcpy(fullpath, sizeof(fullpath), watch->rootdir);
dmon__strcat(fullpath, sizeof(fullpath), watchdir);
if (stat(fullpath, &st) != 0 || (st.st_mode & S_IFDIR) == 0) {
_DMON_LOG_ERRORF("Watch directory '%s' is not valid", watchdir);
if (!skip_lock)
pthread_mutex_unlock(&_dmon.mutex);
return false;
}
dmon__strcpy(subdir.rootdir, sizeof(subdir.rootdir), watchdir);
}
int dirlen = (int)strlen(subdir.rootdir);
if (subdir.rootdir[dirlen - 1] != '/') {
subdir.rootdir[dirlen] = '/';
subdir.rootdir[dirlen + 1] = '\0';
}
// check that the directory is not already added
for (int i = 0, c = stb_sb_count(watch->subdirs); i < c; i++) {
if (strcmp(subdir.rootdir, watch->subdirs[i].rootdir) == 0) {
_DMON_LOG_ERRORF("Error watching directory '%s', because it is already added.", watchdir);
if (!skip_lock)
pthread_mutex_unlock(&_dmon.mutex);
return false;
}
}
const uint32_t inotify_mask = IN_MOVED_TO | IN_CREATE | IN_MOVED_FROM | IN_DELETE | IN_MODIFY;
char fullpath[DMON_MAX_PATH];
dmon__strcpy(fullpath, sizeof(fullpath), watch->rootdir);
dmon__strcat(fullpath, sizeof(fullpath), subdir.rootdir);
int wd = inotify_add_watch(watch->fd, fullpath, inotify_mask);
if (wd == -1) {
_DMON_LOG_ERRORF("Error watching directory '%s'. (inotify_add_watch:err=%d)", watchdir, errno);
if (!skip_lock)
pthread_mutex_unlock(&_dmon.mutex);
return false;
}
stb_sb_push(watch->subdirs, subdir);
stb_sb_push(watch->wds, wd);
if (!skip_lock)
pthread_mutex_unlock(&_dmon.mutex);
return true;
}
DMON_API_IMPL bool dmon_watch_rm(dmon_watch_id id, const char* watchdir)
{
DMON_ASSERT(id.id > 0 && id.id <= DMON_MAX_WATCHES);
bool skip_lock = pthread_self() == _dmon.thread_handle;
if (!skip_lock)
pthread_mutex_lock(&_dmon.mutex);
dmon__watch_state* watch = &_dmon.watches[id.id - 1];
char subdir[DMON_MAX_PATH];
dmon__strcpy(subdir, sizeof(subdir), watchdir);
if (strstr(subdir, watch->rootdir) == subdir) {
dmon__strcpy(subdir, sizeof(subdir), watchdir + strlen(watch->rootdir));
}
int dirlen = (int)strlen(subdir);
if (subdir[dirlen - 1] != '/') {
subdir[dirlen] = '/';
subdir[dirlen + 1] = '\0';
}
int i, c = stb_sb_count(watch->subdirs);
for (i = 0; i < c; i++) {
if (strcmp(watch->subdirs[i].rootdir, subdir) == 0) {
break;
}
}
if (i >= c) {
_DMON_LOG_ERRORF("Watch directory '%s' is not valid", watchdir);
if (!skip_lock)
pthread_mutex_unlock(&_dmon.mutex);
return false;
}
inotify_rm_watch(watch->fd, watch->wds[i]);
/* Remove entry from subdirs and wds by swapping position with the last entry */
watch->subdirs[i] = stb_sb_last(watch->subdirs);
stb_sb_pop(watch->subdirs);
watch->wds[i] = stb_sb_last(watch->wds);
stb_sb_pop(watch->wds);
if (!skip_lock)
pthread_mutex_unlock(&_dmon.mutex);
return true;
}
#endif // DMON_OS_LINUX
#endif // DMON_IMPL
#endif // __DMON_EXTRA_H__

View File

@ -1 +0,0 @@
lite_includes += include_directories('.')

View File

@ -22,33 +22,6 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. SOFTWARE.
## septag/dmon
Copyright 2019 Sepehr Taghdisian. All rights reserved.
https://github.com/septag/dmon
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY COPYRIGHT HOLDER "AS IS" AND ANY EXPRESS OR
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
EVENT SHALL COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
## Fira Sans ## Fira Sans
Digitized data copyright (c) 2012-2015, The Mozilla Foundation and Telefonica S.A. Digitized data copyright (c) 2012-2015, The Mozilla Foundation and Telefonica S.A.

View File

@ -1,18 +1,41 @@
project('lite-xl', project('lite-xl',
['c'], ['c'],
version : '2.0.3', version : '2.1.0',
license : 'MIT', license : 'MIT',
meson_version : '>= 0.54', meson_version : '>= 0.47',
default_options : ['c_std=gnu11'] default_options : [
'c_std=gnu11',
'wrap_mode=nofallback'
]
) )
#===============================================================================
# Project version including git commit if possible
#===============================================================================
version = meson.project_version()
if get_option('buildtype') != 'release'
git_command = find_program('git', required : false)
if git_command.found()
git_commit = run_command(
[git_command, 'rev-parse', 'HEAD'],
check : false
).stdout().strip()
if git_commit != ''
version += ' (git-' + git_commit.substring(0, 8) + ')'
endif
endif
endif
#=============================================================================== #===============================================================================
# Configuration # Configuration
#=============================================================================== #===============================================================================
conf_data = configuration_data() conf_data = configuration_data()
conf_data.set('PROJECT_BUILD_DIR', meson.current_build_dir()) conf_data.set('PROJECT_BUILD_DIR', meson.current_build_dir())
conf_data.set('PROJECT_SOURCE_DIR', meson.current_source_dir()) conf_data.set('PROJECT_SOURCE_DIR', meson.current_source_dir())
conf_data.set('PROJECT_VERSION', meson.project_version()) conf_data.set('PROJECT_VERSION', version)
#=============================================================================== #===============================================================================
# Compiler Settings # Compiler Settings
@ -24,7 +47,7 @@ endif
cc = meson.get_compiler('c') cc = meson.get_compiler('c')
lite_includes = [] lite_includes = []
lite_cargs = [] lite_cargs = ['-DSDL_MAIN_HANDLED', '-DPCRE2_STATIC']
# On macos we need to use the SDL renderer to support retina displays # On macos we need to use the SDL renderer to support retina displays
if get_option('renderer') or host_machine.system() == 'darwin' if get_option('renderer') or host_machine.system() == 'darwin'
lite_cargs += '-DLITE_USE_SDL_RENDERER' lite_cargs += '-DLITE_USE_SDL_RENDERER'
@ -46,14 +69,84 @@ endif
if not get_option('source-only') if not get_option('source-only')
libm = cc.find_library('m', required : false) libm = cc.find_library('m', required : false)
libdl = cc.find_library('dl', required : false) libdl = cc.find_library('dl', required : false)
threads_dep = dependency('threads')
lua_dep = dependency('lua5.2', fallback: ['lua', 'lua_dep'], default_fallback_options = ['warning_level=0', 'werror=false']
default_options: ['shared=false', 'use_readline=false', 'app=false']
# Lua has no official .pc file
# so distros come up with their own names
lua_names = [
'lua5.4', # Debian
'lua-5.4', # FreeBSD
'lua', # Fedora
]
foreach lua : lua_names
last_lua = (lua == lua_names[-1] or get_option('wrap_mode') == 'forcefallback')
lua_dep = dependency(lua, fallback: last_lua ? ['lua', 'lua_dep'] : [], required : last_lua,
version: '>= 5.4',
default_options: default_fallback_options + ['default_library=static', 'line_editing=false', 'interpreter=false']
)
if lua_dep.found()
break
endif
endforeach
pcre2_dep = dependency('libpcre2-8', fallback: ['pcre2', 'libpcre2_8'],
default_options: default_fallback_options + ['default_library=static', 'grep=false', 'test=false']
) )
pcre2_dep = dependency('libpcre2-8')
freetype_dep = dependency('freetype2') freetype_dep = dependency('freetype2', fallback: ['freetype2', 'freetype_dep'],
sdl_dep = dependency('sdl2', method: 'config-tool') default_options: default_fallback_options + ['default_library=static', 'zlib=disabled', 'bzip2=disabled', 'png=disabled', 'harfbuzz=disabled', 'brotli=disabled']
lite_deps = [lua_dep, sdl_dep, freetype_dep, pcre2_dep, libm, libdl, threads_dep] )
sdl_options = ['default_library=static']
# we explicitly need these
sdl_options += 'use_loadso=enabled'
sdl_options += 'prefer_dlopen=true'
sdl_options += 'use_video=enabled'
sdl_options += 'use_atomic=enabled'
sdl_options += 'use_threads=enabled'
sdl_options += 'use_timers=enabled'
# investigate if this is truly needed
# Do not remove before https://github.com/libsdl-org/SDL/issues/5413 is released
sdl_options += 'use_events=enabled'
if host_machine.system() == 'darwin' or host_machine.system() == 'windows'
sdl_options += 'use_video_x11=disabled'
sdl_options += 'use_video_wayland=disabled'
else
sdl_options += 'use_render=enabled'
sdl_options += 'use_video_x11=auto'
sdl_options += 'use_video_wayland=auto'
endif
# we leave this up to what the host system has except on windows
if host_machine.system() != 'windows'
sdl_options += 'use_video_opengl=auto'
sdl_options += 'use_video_openglesv2=auto'
else
sdl_options += 'use_video_opengl=disabled'
sdl_options += 'use_video_openglesv2=disabled'
endif
# we don't need these
sdl_options += 'test=false'
sdl_options += 'use_sensor=disabled'
sdl_options += 'use_haptic=disabled'
sdl_options += 'use_audio=disabled'
sdl_options += 'use_cpuinfo=disabled'
sdl_options += 'use_joystick=disabled'
sdl_options += 'use_video_vulkan=disabled'
sdl_options += 'use_video_offscreen=disabled'
sdl_options += 'use_power=disabled'
sdl_dep = dependency('sdl2', fallback: ['sdl2', 'sdl2_dep'],
default_options: default_fallback_options + sdl_options
)
lite_deps = [lua_dep, sdl_dep, freetype_dep, pcre2_dep, libm, libdl]
endif endif
#=============================================================================== #===============================================================================
# Install Configuration # Install Configuration
@ -94,21 +187,20 @@ endif
install_data('licenses/licenses.md', install_dir : lite_docdir) install_data('licenses/licenses.md', install_dir : lite_docdir)
install_subdir('data' / 'core' , install_dir : lite_datadir, exclude_files : 'start.lua') install_subdir('docs/api' , install_dir : lite_datadir, strip_directory: true)
install_subdir('data/core' , install_dir : lite_datadir, exclude_files : 'start.lua')
foreach data_module : ['fonts', 'plugins', 'colors'] foreach data_module : ['fonts', 'plugins', 'colors']
install_subdir('data' / data_module , install_dir : lite_datadir) install_subdir(join_paths('data', data_module), install_dir : lite_datadir)
endforeach endforeach
configure_file( configure_file(
input : 'data/core/start.lua', input : 'data/core/start.lua',
output : 'start.lua', output : 'start.lua',
configuration : conf_data, configuration : conf_data,
install : true, install_dir : join_paths(lite_datadir, 'core'),
install_dir : lite_datadir / 'core',
) )
if not get_option('source-only') if not get_option('source-only')
subdir('lib/dmon')
subdir('src') subdir('src')
subdir('scripts') subdir('scripts')
endif endif

View File

@ -2,3 +2,4 @@ option('bundle', type : 'boolean', value : false, description: 'Build a macOS bu
option('source-only', type : 'boolean', value : false, description: 'Configure source files only, doesn\'t checks for dependencies') option('source-only', type : 'boolean', value : false, description: 'Configure source files only, doesn\'t checks for dependencies')
option('portable', type : 'boolean', value : false, description: 'Portable install') option('portable', type : 'boolean', value : false, description: 'Portable install')
option('renderer', type : 'boolean', value : false, description: 'Use SDL renderer') option('renderer', type : 'boolean', value : false, description: 'Use SDL renderer')
option('dirmonitor_backend', type : 'combo', value : '', choices : ['', 'inotify', 'kqueue', 'win32', 'dummy'], description: 'define what dirmonitor backend to use')

View File

@ -6,6 +6,7 @@
<name>Lite XL</name> <name>Lite XL</name>
<summary>A lightweight text editor written in Lua</summary> <summary>A lightweight text editor written in Lua</summary>
<content_rating type="oars-1.0" /> <content_rating type="oars-1.0" />
<launchable type="desktop-id">org.lite_xl.lite_xl.desktop</launchable>
<description> <description>
<p> <p>
@ -17,11 +18,11 @@
<screenshots> <screenshots>
<screenshot type="default"> <screenshot type="default">
<caption>The editor window</caption> <caption>The editor window</caption>
<image>https://lite-xl.github.io/assets/img/screenshots/editor.png</image> <image>https://lite-xl.com/assets/img/editor.png</image>
</screenshot> </screenshot>
</screenshots> </screenshots>
<url type="homepage">https://lite-xl.github.io</url> <url type="homepage">https://lite-xl.com</url>
<provides> <provides>
<binary>lite-xl</binary> <binary>lite-xl</binary>

View File

@ -8,6 +8,8 @@
<string>lite-xl</string> <string>lite-xl</string>
<key>CFBundleIconFile</key> <key>CFBundleIconFile</key>
<string>icon.icns</string> <string>icon.icns</string>
<key>CFBundleIdentifier</key>
<string>com.lite-xl</string>
<key>CFBundleName</key> <key>CFBundleName</key>
<string>Lite XL</string> <string>Lite XL</string>
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>

View File

@ -1,54 +0,0 @@
`core.set_project_dir`:
Reset project directories and set its directory.
It chdir into the directory, empty the `core.project_directories` and add
the given directory.
`core.add_project_directory`:
Add a new top-level directory to the project.
Also called from modules and commands outside core.init.
local function `scan_project_folder`:
Scan all files for a given top-level project directory.
Can emit a warning about file limit.
Called only from within core.init module.
`core.scan_project_subdir`: (before was named `core.scan_project_folder`)
scan a single folder, without recursion. Used when too many files.
Local function `scan_project_folder`:
Populate the project folder top directory. Done only once when the directory
is added to the project.
`core.add_project_directory`:
Add a new top-level folder to the project.
`core.set_project_dir`:
Set the initial project directory.
`core.dir_rescan_add_job`:
Add a job to rescan after an elapsed time a project's subdirectory to fix for any
changes.
Local function `rescan_project_subdir`:
Rescan a project's subdirectory, compare to the current version and patch the list if
a difference is found.
`core.project_scan_thread`:
Should disappear now that we use dmon.
`core.project_scan_topdir`:
New function to scan a top level project folder.
`config.project_scan_rate`:
`core.project_scan_thread_id`:
`core.reschedule_project_scan`:
`core.project_files_limit`:
A eliminer.
`core.get_project_files`:
To be fixed. Use `find_project_files_co` for a single directory
In TreeView remove usage of self.last to detect new scan that changed the files list.

View File

@ -0,0 +1,195 @@
diff -ruN lua-5.4.3/meson.build newlua/meson.build
--- lua-5.4.3/meson.build 2022-05-29 21:04:17.850449500 +0800
+++ newlua/meson.build 2022-06-10 19:23:55.685139800 +0800
@@ -82,6 +82,7 @@
'src/lutf8lib.c',
'src/lvm.c',
'src/lzio.c',
+ 'src/utf8_wrappers.c',
dependencies: lua_lib_deps,
override_options: project_options,
implicit_include_directories: false,
Binary files lua-5.4.3/src/lua54.dll and newlua/src/lua54.dll differ
diff -ruN lua-5.4.3/src/luaconf.h newlua/src/luaconf.h
--- lua-5.4.3/src/luaconf.h 2021-03-15 21:32:52.000000000 +0800
+++ newlua/src/luaconf.h 2022-06-10 19:15:03.014745300 +0800
@@ -786,5 +786,15 @@
+#if defined(lua_c) || defined(luac_c) || (defined(LUA_LIB) && \
+ (defined(lauxlib_c) || defined(liolib_c) || \
+ defined(loadlib_c) || defined(loslib_c)))
+#include "utf8_wrappers.h"
+#endif
+
+
+
+
+
#endif
diff -ruN lua-5.4.3/src/Makefile newlua/src/Makefile
--- lua-5.4.3/src/Makefile 2021-02-10 02:47:17.000000000 +0800
+++ newlua/src/Makefile 2022-06-10 19:22:45.267931400 +0800
@@ -33,7 +33,7 @@
PLATS= guess aix bsd c89 freebsd generic linux linux-readline macosx mingw posix solaris
LUA_A= liblua.a
-CORE_O= lapi.o lcode.o lctype.o ldebug.o ldo.o ldump.o lfunc.o lgc.o llex.o lmem.o lobject.o lopcodes.o lparser.o lstate.o lstring.o ltable.o ltm.o lundump.o lvm.o lzio.o
+CORE_O= lapi.o lcode.o lctype.o ldebug.o ldo.o ldump.o lfunc.o lgc.o llex.o lmem.o lobject.o lopcodes.o lparser.o lstate.o lstring.o ltable.o ltm.o lundump.o lvm.o lzio.o utf8_wrappers.o
LIB_O= lauxlib.o lbaselib.o lcorolib.o ldblib.o liolib.o lmathlib.o loadlib.o loslib.o lstrlib.o ltablib.o lutf8lib.o linit.o
BASE_O= $(CORE_O) $(LIB_O) $(MYOBJS)
diff -ruN lua-5.4.3/src/utf8_wrappers.c newlua/src/utf8_wrappers.c
--- lua-5.4.3/src/utf8_wrappers.c 1970-01-01 07:30:00.000000000 +0730
+++ newlua/src/utf8_wrappers.c 2022-06-10 19:13:11.904613300 +0800
@@ -0,0 +1,101 @@
+/**
+ * Wrappers to provide Unicode (UTF-8) support on Windows.
+ *
+ * Copyright (c) 2018 Peter Wu <peter@lekensteyn.nl>
+ * SPDX-License-Identifier: (GPL-2.0-or-later OR MIT)
+ */
+
+#ifdef _WIN32
+#include <windows.h> /* for MultiByteToWideChar */
+#include <wchar.h> /* for _wrename */
+#include <stdio.h>
+#include <stdlib.h>
+#include <errno.h>
+
+// Set a high limit in case long paths are enabled.
+#define MAX_PATH_SIZE 4096
+#define MAX_MODE_SIZE 128
+// cmd.exe argument length is reportedly limited to 8192.
+#define MAX_CMD_SIZE 8192
+
+FILE *fopen_utf8(const char *pathname, const char *mode) {
+ wchar_t pathname_w[MAX_PATH_SIZE];
+ wchar_t mode_w[MAX_MODE_SIZE];
+ if (!MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, pathname, -1, pathname_w, MAX_PATH_SIZE) ||
+ !MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, mode, -1, mode_w, MAX_MODE_SIZE)) {
+ errno = EINVAL;
+ return NULL;
+ }
+ return _wfopen(pathname_w, mode_w);
+}
+
+FILE *freopen_utf8(const char *pathname, const char *mode, FILE *stream) {
+ wchar_t pathname_w[MAX_PATH_SIZE];
+ wchar_t mode_w[MAX_MODE_SIZE];
+ if (!MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, pathname, -1, pathname_w, MAX_PATH_SIZE) ||
+ !MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, mode, -1, mode_w, MAX_MODE_SIZE)) {
+ // Close stream as documented for the error case.
+ fclose(stream);
+ errno = EINVAL;
+ return NULL;
+ }
+ return _wfreopen(pathname_w, mode_w, stream);
+}
+
+int remove_utf8(const char *pathname) {
+ wchar_t pathname_w[MAX_PATH_SIZE];
+ if (!MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, pathname, -1, pathname_w, MAX_PATH_SIZE)) {
+ errno = EINVAL;
+ return -1;
+ }
+ return _wremove(pathname_w);
+}
+
+int rename_utf8(const char *oldpath, const char *newpath) {
+ wchar_t oldpath_w[MAX_PATH_SIZE];
+ wchar_t newpath_w[MAX_PATH_SIZE];
+ if (!MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, oldpath, -1, oldpath_w, MAX_PATH_SIZE) ||
+ !MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, newpath, -1, newpath_w, MAX_PATH_SIZE)) {
+ errno = EINVAL;
+ return -1;
+ }
+ return _wrename(oldpath_w, newpath_w);
+}
+
+FILE *popen_utf8(const char *command, const char *mode) {
+ wchar_t command_w[MAX_CMD_SIZE];
+ wchar_t mode_w[MAX_MODE_SIZE];
+ if (!MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, command, -1, command_w, MAX_CMD_SIZE) ||
+ !MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, mode, -1, mode_w, MAX_MODE_SIZE)) {
+ errno = EINVAL;
+ return NULL;
+ }
+ return _wpopen(command_w, mode_w);
+}
+
+int system_utf8(const char *command) {
+ wchar_t command_w[MAX_CMD_SIZE];
+ if (!MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, command, -1, command_w, MAX_CMD_SIZE)) {
+ errno = EINVAL;
+ return -1;
+ }
+ return _wsystem(command_w);
+}
+
+DWORD GetModuleFileNameA_utf8(HMODULE hModule, LPSTR lpFilename, DWORD nSize) {
+ wchar_t filename_w[MAX_PATH + 1];
+ if (!GetModuleFileNameW(hModule, filename_w, MAX_PATH + 1)) {
+ return 0;
+ }
+ return WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, filename_w, -1, lpFilename, nSize, NULL, NULL);
+}
+
+HMODULE LoadLibraryExA_utf8(LPCSTR lpLibFileName, HANDLE hFile, DWORD dwFlags) {
+ wchar_t pathname_w[MAX_PATH_SIZE];
+ if (!MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, lpLibFileName, -1, pathname_w, MAX_PATH_SIZE)) {
+ SetLastError(ERROR_INVALID_NAME);
+ return NULL;
+ }
+ return LoadLibraryExW(pathname_w, hFile, dwFlags);
+}
+#endif
diff -ruN lua-5.4.3/src/utf8_wrappers.h newlua/src/utf8_wrappers.h
--- lua-5.4.3/src/utf8_wrappers.h 1970-01-01 07:30:00.000000000 +0730
+++ newlua/src/utf8_wrappers.h 2022-06-10 19:22:53.554879400 +0800
@@ -0,0 +1,42 @@
+/**
+ * Wrappers to provide Unicode (UTF-8) support on Windows.
+ *
+ * Copyright (c) 2018 Peter Wu <peter@lekensteyn.nl>
+ * SPDX-License-Identifier: (GPL-2.0-or-later OR MIT)
+ */
+
+#ifdef _WIN32
+
+#if defined(loadlib_c) || defined(lauxlib_c) || defined(liolib_c) || defined(luac_c)
+#include <stdio.h> /* for loadlib_c */
+FILE *fopen_utf8(const char *pathname, const char *mode);
+#define fopen fopen_utf8
+#endif
+
+#ifdef lauxlib_c
+FILE *freopen_utf8(const char *pathname, const char *mode, FILE *stream);
+#define freopen freopen_utf8
+#endif
+
+#ifdef liolib_c
+FILE *popen_utf8(const char *command, const char *mode);
+#define _popen popen_utf8
+#endif
+
+#ifdef loslib_c
+int remove_utf8(const char *pathname);
+int rename_utf8(const char *oldpath, const char *newpath);
+int system_utf8(const char *command);
+#define remove remove_utf8
+#define rename rename_utf8
+#define system system_utf8
+#endif
+
+#ifdef loadlib_c
+#include <windows.h>
+DWORD GetModuleFileNameA_utf8(HMODULE hModule, LPSTR lpFilename, DWORD nSize);
+HMODULE LoadLibraryExA_utf8(LPCSTR lpLibFileName, HANDLE hFile, DWORD dwFlags);
+#define GetModuleFileNameA GetModuleFileNameA_utf8
+#define LoadLibraryExA LoadLibraryExA_utf8
+#endif
+#endif

View File

@ -1,5 +1,5 @@
#!/bin/env bash #!/bin/env bash
set -ex set -e
if [ ! -e "src/api/api.h" ]; then if [ ! -e "src/api/api.h" ]; then
echo "Please run this script from the root directory of Lite XL." echo "Please run this script from the root directory of Lite XL."
@ -8,6 +8,13 @@ fi
source scripts/common.sh source scripts/common.sh
ARCH="$(uname -m)"
BUILD_DIR="$(get_default_build_dir)"
RUN_BUILD=true
STATIC_BUILD=false
ADDONS=false
BUILD_TYPE="debug"
show_help(){ show_help(){
echo echo
echo "Usage: $0 <OPTIONS>" echo "Usage: $0 <OPTIONS>"
@ -16,22 +23,21 @@ show_help(){
echo echo
echo "-h --help Show this help and exits." echo "-h --help Show this help and exits."
echo "-b --builddir DIRNAME Sets the name of the build dir (no path)." echo "-b --builddir DIRNAME Sets the name of the build dir (no path)."
echo " Default: 'build'." echo " Default: '${BUILD_DIR}'."
echo " --debug Debug this script."
echo "-n --nobuild Skips the build step, use existing files." echo "-n --nobuild Skips the build step, use existing files."
echo "-s --static Specify if building using static libraries" echo "-s --static Specify if building using static libraries."
echo " by using lhelper tool."
echo "-v --version VERSION Specify a version, non whitespace separated string." echo "-v --version VERSION Specify a version, non whitespace separated string."
echo "-a --addons Install 3rd party addons."
echo "-r --release Compile in release mode."
echo echo
} }
ARCH="$(uname -m)" initial_arg_count=$#
BUILD_DIR="$(get_default_build_dir)"
RUN_BUILD=true
STATIC_BUILD=false
for i in "$@"; do for i in "$@"; do
case $i in case $i in
-h|--belp) -h|--help)
show_help show_help
exit 0 exit 0
;; ;;
@ -40,10 +46,22 @@ for i in "$@"; do
shift shift
shift shift
;; ;;
-a|--addons)
ADDONS=true
shift
;;
--debug)
set -x
shift
;;
-n|--nobuild) -n|--nobuild)
RUN_BUILD=false RUN_BUILD=false
shift shift
;; ;;
-r|--release)
BUILD_TYPE="release"
shift
;;
-s|--static) -s|--static)
STATIC_BUILD=true STATIC_BUILD=true
shift shift
@ -59,25 +77,19 @@ for i in "$@"; do
esac esac
done done
# TODO: Versioning using git # show help if no valid argument was found
#if [[ -z $VERSION && -d .git ]]; then if [ $initial_arg_count -eq $# ]; then
# VERSION=$(git describe --tags --long | sed 's/^v//; s/\([^-]*-g\)/r\1/; s/-/./g')
#fi
if [[ -n $1 ]]; then
show_help show_help
exit 1 exit 1
fi fi
setup_appimagetool() { setup_appimagetool() {
if ! which appimagetool > /dev/null ; then if [ ! -e appimagetool ]; then
if [ ! -e appimagetool ]; then if ! wget -O appimagetool "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-${ARCH}.AppImage" ; then
if ! wget -O appimagetool "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-${ARCH}.AppImage" ; then echo "Could not download the appimagetool for the arch '${ARCH}'."
echo "Could not download the appimagetool for the arch '${ARCH}'." exit 1
exit 1 else
else chmod 0755 appimagetool
chmod 0755 appimagetool
fi
fi fi
fi fi
} }
@ -104,7 +116,14 @@ build_litexl() {
echo "Build lite-xl..." echo "Build lite-xl..."
sleep 1 sleep 1
meson setup --buildtype=release --prefix /usr ${BUILD_DIR} if [[ $STATIC_BUILD == false ]]; then
meson setup --buildtype=$BUILD_TYPE --prefix=/usr ${BUILD_DIR}
else
meson setup --wrap-mode=forcefallback \
--buildtype=$BUILD_TYPE \
--prefix=/usr \
${BUILD_DIR}
fi
meson compile -C ${BUILD_DIR} meson compile -C ${BUILD_DIR}
} }
@ -121,6 +140,11 @@ generate_appimage() {
cp resources/icons/lite-xl.svg LiteXL.AppDir/ cp resources/icons/lite-xl.svg LiteXL.AppDir/
cp resources/linux/org.lite_xl.lite_xl.desktop LiteXL.AppDir/ cp resources/linux/org.lite_xl.lite_xl.desktop LiteXL.AppDir/
if [[ $ADDONS == true ]]; then
addons_download "${BUILD_DIR}"
addons_install "${BUILD_DIR}" "LiteXL.AppDir/usr/share/lite-xl"
fi
if [[ $STATIC_BUILD == false ]]; then if [[ $STATIC_BUILD == false ]]; then
echo "Copying libraries..." echo "Copying libraries..."
@ -153,6 +177,10 @@ generate_appimage() {
version="-$VERSION" version="-$VERSION"
fi fi
if [[ $ADDONS == true ]]; then
version="${version}-addons"
fi
./appimagetool LiteXL.AppDir LiteXL${version}-${ARCH}.AppImage ./appimagetool LiteXL.AppDir LiteXL${version}-${ARCH}.AppImage
} }

View File

@ -22,19 +22,25 @@ show_help() {
echo "-B --bundle Create an App bundle (macOS only)" echo "-B --bundle Create an App bundle (macOS only)"
echo "-P --portable Create a portable binary package." echo "-P --portable Create a portable binary package."
echo "-O --pgo Use profile guided optimizations (pgo)." echo "-O --pgo Use profile guided optimizations (pgo)."
echo "-U --windows-lua-utf Use the UTF8 patch for Lua."
echo " macOS: disabled when used with --bundle," echo " macOS: disabled when used with --bundle,"
echo " Windows: Implicit being the only option." echo " Windows: Implicit being the only option."
echo "-r --release Compile in release mode."
echo echo
} }
main() { main() {
local platform="$(get_platform_name)" local platform="$(get_platform_name)"
local build_dir="$(get_default_build_dir)" local build_dir="$(get_default_build_dir)"
local build_type="debug"
local prefix=/ local prefix=/
local force_fallback local force_fallback
local bundle local bundle
local portable local portable
local pgo local pgo
local patch_lua
local lua_subproject_path
for i in "$@"; do for i in "$@"; do
case $i in case $i in
@ -76,6 +82,14 @@ main() {
pgo="-Db_pgo=generate" pgo="-Db_pgo=generate"
shift shift
;; ;;
-U|--windows-lua-utf)
patch_lua="true"
shift
;;
-r|--release)
build_type="release"
shift
;;
*) *)
# unknown option # unknown option
;; ;;
@ -95,7 +109,7 @@ main() {
rm -rf "${build_dir}" rm -rf "${build_dir}"
CFLAGS=$CFLAGS LDFLAGS=$LDFLAGS meson setup \ CFLAGS=$CFLAGS LDFLAGS=$LDFLAGS meson setup \
--buildtype=release \ --buildtype=$build_type \
--prefix "$prefix" \ --prefix "$prefix" \
$force_fallback \ $force_fallback \
$bundle \ $bundle \
@ -103,6 +117,11 @@ main() {
$pgo \ $pgo \
"${build_dir}" "${build_dir}"
lua_subproject_path=$(echo subprojects/lua-*/)
if [[ $patch_lua == "true" ]] && [[ ! -z $force_fallback ]] && [[ -d $lua_subproject_path ]]; then
patch -d $lua_subproject_path -p1 --forward < resources/windows/001-lua-unicode.diff
fi
meson compile -C "${build_dir}" meson compile -C "${build_dir}"
if [ ! -z ${pgo+x} ]; then if [ ! -z ${pgo+x} ]; then

View File

@ -2,6 +2,64 @@
set -e set -e
addons_download() {
local build_dir="$1"
if [[ -d "${build_dir}/third/data/colors" ]]; then
echo "Warning: found previous addons installation, skipping."
echo " addons path: ${build_dir}/third/data/colors"
return 0
fi
# Download third party color themes
curl --insecure \
-L "https://github.com/lite-xl/lite-xl-colors/archive/master.zip" \
-o "${build_dir}/lite-xl-colors.zip"
mkdir -p "${build_dir}/third/data/colors"
unzip "${build_dir}/lite-xl-colors.zip" -d "${build_dir}"
mv "${build_dir}/lite-xl-colors-master/colors" "${build_dir}/third/data"
rm -rf "${build_dir}/lite-xl-colors-master"
# Download widgets library
curl --insecure \
-L "https://github.com/lite-xl/lite-xl-widgets/archive/master.zip" \
-o "${build_dir}/lite-xl-widgets.zip"
unzip "${build_dir}/lite-xl-widgets.zip" -d "${build_dir}"
mv "${build_dir}/lite-xl-widgets-master" "${build_dir}/third/data/widget"
# Downlaod thirdparty plugins
curl --insecure \
-L "https://github.com/lite-xl/lite-xl-plugins/archive/2.1.zip" \
-o "${build_dir}/lite-xl-plugins.zip"
unzip "${build_dir}/lite-xl-plugins.zip" -d "${build_dir}"
mv "${build_dir}/lite-xl-plugins-2.1/plugins" "${build_dir}/third/data"
rm -rf "${build_dir}/lite-xl-plugins-2.1"
}
# Addons installation: some distributions forbid external downloads
# so make it as optional module.
addons_install() {
local build_dir="$1"
local data_dir="$2"
for module_name in colors widget; do
cp -r "${build_dir}/third/data/$module_name" "${data_dir}"
done
mkdir -p "${data_dir}/plugins"
for plugin_name in settings open_ext; do
cp -r "${build_dir}/third/data/plugins/${plugin_name}.lua" \
"${data_dir}/plugins/"
done
cp "${build_dir}/third/data/plugins/"language_* \
"${data_dir}/plugins/"
}
get_platform_name() { get_platform_name() {
if [[ "$OSTYPE" == "msys" ]]; then if [[ "$OSTYPE" == "msys" ]]; then
echo "windows" echo "windows"
@ -14,9 +72,23 @@ get_platform_name() {
fi fi
} }
get_platform_arch() {
platform=$(get_platform_name)
arch=$(uname -m)
if [[ $MSYSTEM != "" ]]; then
if [[ $MSYSTEM == "MINGW64" ]]; then
arch=x86_64
else
arch=i686
fi
fi
echo "$arch"
}
get_default_build_dir() { get_default_build_dir() {
platform=$(get_platform_name) platform=$(get_platform_name)
echo "build-$platform-$(uname -m)" arch=$(get_platform_arch)
echo "build-$platform-$arch"
} }
if [[ $(get_platform_name) == "UNSUPPORTED-OS" ]]; then if [[ $(get_platform_name) == "UNSUPPORTED-OS" ]]; then

View File

@ -15,15 +15,29 @@ show_help() {
echo echo
echo "-b --builddir DIRNAME Sets the name of the build directory (not path)." echo "-b --builddir DIRNAME Sets the name of the build directory (not path)."
echo " Default: '$(get_default_build_dir)'." echo " Default: '$(get_default_build_dir)'."
echo "-v --version VERSION Sets the version on the package name."
echo "-a --addons Tell the script we are packaging an install with addons."
echo " --debug Debug this script." echo " --debug Debug this script."
echo echo
} }
main() { main() {
local build_dir=$(get_default_build_dir) local build_dir=$(get_default_build_dir)
local addons=false
local arch local arch
local arch_file
local version
local output
if [[ $MSYSTEM == "MINGW64" ]]; then arch=x64; else arch=Win32; fi if [[ $MSYSTEM == "MINGW64" ]]; then
arch=x64
arch_file=x86_64
else
arch=i686;
arch_file=i686
fi
initial_arg_count=$#
for i in "$@"; do for i in "$@"; do
case $i in case $i in
@ -31,11 +45,20 @@ main() {
show_help show_help
exit 0 exit 0
;; ;;
-a|--addons)
addons=true
shift
;;
-b|--builddir) -b|--builddir)
build_dir="$2" build_dir="$2"
shift shift
shift shift
;; ;;
-v|--version)
if [[ -n $2 ]]; then version="-$2"; fi
shift
shift
;;
--debug) --debug)
set -x set -x
shift shift
@ -46,19 +69,19 @@ main() {
esac esac
done done
if [[ -n $1 ]]; then # show help if no valid argument was found
if [ $initial_arg_count -eq $# ]; then
show_help show_help
exit 1 exit 1
fi fi
# Copy MinGW libraries dependencies. if [[ $addons == true ]]; then
# MSYS2 ldd command seems to be only 64bit, so use ntldd version="${version}-addons"
# see https://github.com/msys2/MINGW-packages/issues/4164 fi
local mingwLibsDir="${build_dir}/mingwLibs$arch"
mkdir -p "$mingwLibsDir"
ntldd -R "${build_dir}/src/lite-xl.exe" | grep mingw | awk '{print $3}' | sed 's#\\#/#g' | xargs -I '{}' cp -v '{}' $mingwLibsDir
"/c/Program Files (x86)/Inno Setup 6/ISCC.exe" -dARCH=$arch "${build_dir}/scripts/innosetup.iss" output="LiteXL${version}-${arch_file}-setup"
"/c/Program Files (x86)/Inno Setup 6/ISCC.exe" -dARCH=$arch //F"${output}" "${build_dir}/scripts/innosetup.iss"
pushd "${build_dir}/scripts"; mv LiteXL*.exe "./../../"; popd pushd "${build_dir}/scripts"; mv LiteXL*.exe "./../../"; popd
} }

View File

@ -48,7 +48,7 @@ main() {
if [[ $lhelper == true ]]; then if [[ $lhelper == true ]]; then
sudo apt-get install -qq ninja-build sudo apt-get install -qq ninja-build
else else
sudo apt-get install -qq ninja-build libsdl2-dev libfreetype6 sudo apt-get install -qq libfuse2 ninja-build wayland-protocols libsdl2-dev libfreetype6
fi fi
pip3 install meson pip3 install meson
elif [[ "$OSTYPE" == "darwin"* ]]; then elif [[ "$OSTYPE" == "darwin"* ]]; then
@ -63,10 +63,10 @@ main() {
elif [[ "$OSTYPE" == "msys" ]]; then elif [[ "$OSTYPE" == "msys" ]]; then
if [[ $lhelper == true ]]; then if [[ $lhelper == true ]]; then
pacman --noconfirm -S \ pacman --noconfirm -S \
${MINGW_PACKAGE_PREFIX}-{gcc,meson,ninja,ntldd,pkg-config} unzip ${MINGW_PACKAGE_PREFIX}-{gcc,meson,ninja,ntldd,pkg-config,mesa} unzip
else else
pacman --noconfirm -S \ pacman --noconfirm -S \
${MINGW_PACKAGE_PREFIX}-{gcc,meson,ninja,ntldd,pkg-config,freetype,pcre2,SDL2} unzip ${MINGW_PACKAGE_PREFIX}-{gcc,meson,ninja,ntldd,pkg-config,mesa,freetype,pcre2,SDL2} unzip
fi fi
fi fi
} }

View File

@ -51,25 +51,23 @@ main() {
pushd lhelper; bash install "${lhelper_prefix}"; popd pushd lhelper; bash install "${lhelper_prefix}"; popd
if [[ "$OSTYPE" == "darwin"* ]]; then if [[ "$OSTYPE" == "darwin"* ]]; then
CC=clang CXX=clang++ lhelper create lite-xl -n CC=clang CXX=clang++ lhelper create build
else else
lhelper create lite-xl -n lhelper create lite-xl build
fi fi
fi fi
# Not using $(lhelper activate lite-xl) to support CI # Not using $(lhelper activate lite-xl) to support CI
source "$(lhelper env-source lite-xl)" source "$(lhelper env-source build)"
lhelper install freetype2
lhelper install sdl2 2.0.14-wait-event-timeout-1
lhelper install pcre2
# Help MSYS2 to find the SDL2 include and lib directories to avoid errors # Help MSYS2 to find the SDL2 include and lib directories to avoid errors
# during build and linking when using lhelper. # during build and linking when using lhelper.
if [[ "$OSTYPE" == "msys" ]]; then # Francesco: not sure why this is needed. I have never observed the problem when
CFLAGS=-I${LHELPER_ENV_PREFIX}/include/SDL2 # building on window.
LDFLAGS=-L${LHELPER_ENV_PREFIX}/lib # if [[ "$OSTYPE" == "msys" ]]; then
fi # CFLAGS=-I${LHELPER_ENV_PREFIX}/include/SDL2
# LDFLAGS=-L${LHELPER_ENV_PREFIX}/lib
# fi
} }
main main "$@"

View File

@ -20,44 +20,19 @@ show_help() {
echo "-h --help Show this help and exit." echo "-h --help Show this help and exit."
echo "-p --prefix PREFIX Install directory prefix. Default: '/'." echo "-p --prefix PREFIX Install directory prefix. Default: '/'."
echo "-v --version VERSION Sets the version on the package name." echo "-v --version VERSION Sets the version on the package name."
echo " --addons Install 3rd party addons (currently Lite XL colors)." echo "-a --addons Install 3rd party addons."
echo " --debug Debug this script." echo " --debug Debug this script."
echo "-A --appimage Create an AppImage (Linux only)." echo "-A --appimage Create an AppImage (Linux only)."
echo "-B --binary Create a normal / portable package or macOS bundle," echo "-B --binary Create a normal / portable package or macOS bundle,"
echo " depending on how the build was configured. (Default.)" echo " depending on how the build was configured. (Default.)"
echo "-D --dmg Create a DMG disk image with AppDMG (macOS only)." echo "-D --dmg Create a DMG disk image with AppDMG (macOS only)."
echo "-I --innosetup Create a InnoSetup package (Windows only)." echo "-I --innosetup Create a InnoSetup package (Windows only)."
echo "-r --release Strip debugging symbols."
echo "-S --source Create a source code package," echo "-S --source Create a source code package,"
echo " including subprojects dependencies." echo " including subprojects dependencies."
echo echo
} }
# Addons installation: some distributions forbid external downloads
# so make it as optional module.
install_addons() {
local build_dir="$1"
local data_dir="$2"
if [[ -d "${build_dir}/third/data/colors" ]]; then
echo "Warning: found previous colors addons installation, skipping."
return 0
fi
# Copy third party color themes
curl --insecure \
-L "https://github.com/lite-xl/lite-xl-colors/archive/master.zip" \
-o "${build_dir}/lite-xl-colors.zip"
mkdir -p "${build_dir}/third/data/colors"
unzip "${build_dir}/lite-xl-colors.zip" -d "${build_dir}"
mv "${build_dir}/lite-xl-colors-master/colors" "${build_dir}/third/data"
rm -rf "${build_dir}/lite-xl-colors-master"
for module_name in colors; do
cp -r "${build_dir}/third/data/$module_name" "${data_dir}"
done
}
source_package() { source_package() {
local build_dir=build-src local build_dir=build-src
local package_name=$1 local package_name=$1
@ -85,7 +60,7 @@ source_package() {
} }
main() { main() {
local arch="$(uname -m)" local arch="$(get_platform_arch)"
local platform="$(get_platform_name)" local platform="$(get_platform_name)"
local build_dir="$(get_default_build_dir)" local build_dir="$(get_default_build_dir)"
local dest_dir=lite-xl local dest_dir=lite-xl
@ -96,8 +71,12 @@ main() {
local binary=false local binary=false
local dmg=false local dmg=false
local innosetup=false local innosetup=false
local release=false
local source=false local source=false
# store the current flags to easily pass them to appimage script
local flags="$@"
for i in "$@"; do for i in "$@"; do
case $i in case $i in
-b|--builddir) -b|--builddir)
@ -152,11 +131,15 @@ main() {
fi fi
shift shift
;; ;;
-r|--release)
release=true
shift
;;
-S|--source) -S|--source)
source=true source=true
shift shift
;; ;;
--addons) -a|--addons)
addons=true addons=true
shift shift
;; ;;
@ -170,6 +153,10 @@ main() {
esac esac
done done
if [[ $addons == true ]]; then
version="$version-addons"
fi
if [[ -n $1 ]]; then show_help; exit 1; fi if [[ -n $1 ]]; then show_help; exit 1; fi
# The source package doesn't require a previous build, # The source package doesn't require a previous build,
@ -190,6 +177,7 @@ main() {
local data_dir="$(pwd)/${dest_dir}/data" local data_dir="$(pwd)/${dest_dir}/data"
local exe_file="$(pwd)/${dest_dir}/lite-xl" local exe_file="$(pwd)/${dest_dir}/lite-xl"
local package_name=lite-xl$version-$platform-$arch local package_name=lite-xl$version-$platform-$arch
local bundle=false local bundle=false
local portable=false local portable=false
@ -202,6 +190,14 @@ main() {
if [[ $platform == "windows" ]]; then if [[ $platform == "windows" ]]; then
exe_file="${exe_file}.exe" exe_file="${exe_file}.exe"
stripcmd="strip --strip-all" stripcmd="strip --strip-all"
# Copy MinGW libraries dependencies.
# MSYS2 ldd command seems to be only 64bit, so use ntldd
# see https://github.com/msys2/MINGW-packages/issues/4164
ntldd -R "${exe_file}" \
| grep mingw \
| awk '{print $3}' \
| sed 's#\\#/#g' \
| xargs -I '{}' cp -v '{}' "$(pwd)/${dest_dir}/"
else else
# Windows archive is always portable # Windows archive is always portable
package_name+="-portable" package_name+="-portable"
@ -216,18 +212,21 @@ main() {
rm -rf "Lite XL.app"; mv "${dest_dir}" "Lite XL.app" rm -rf "Lite XL.app"; mv "${dest_dir}" "Lite XL.app"
dest_dir="Lite XL.app" dest_dir="Lite XL.app"
exe_file="$(pwd)/${dest_dir}/Contents/MacOS/lite-xl" exe_file="$(pwd)/${dest_dir}/Contents/MacOS/lite-xl"
data_dir="$(pwd)/${dest_dir}/Contents/Resources"
fi fi
fi fi
if [[ $bundle == false && $portable == false ]]; then if [[ $bundle == false && $portable == false ]]; then
echo "Creating a compressed archive..."
data_dir="$(pwd)/${dest_dir}/$prefix/share/lite-xl" data_dir="$(pwd)/${dest_dir}/$prefix/share/lite-xl"
exe_file="$(pwd)/${dest_dir}/$prefix/bin/lite-xl" exe_file="$(pwd)/${dest_dir}/$prefix/bin/lite-xl"
fi fi
mkdir -p "${data_dir}" mkdir -p "${data_dir}"
if [[ $addons == true ]]; then install_addons "${build_dir}" "${data_dir}"; fi if [[ $addons == true ]]; then
addons_download "${build_dir}"
addons_install "${build_dir}" "${data_dir}"
fi
# TODO: use --skip-subprojects when 0.58.0 will be available on supported # TODO: use --skip-subprojects when 0.58.0 will be available on supported
# distributions to avoid subprojects' include and lib directories to be copied. # distributions to avoid subprojects' include and lib directories to be copied.
@ -238,8 +237,11 @@ main() {
find . -type d -empty -delete find . -type d -empty -delete
popd popd
$stripcmd "${exe_file}" if [[ $release == true ]]; then
$stripcmd "${exe_file}"
fi
echo "Creating a compressed archive ${package_name}"
if [[ $binary == true ]]; then if [[ $binary == true ]]; then
rm -f "${package_name}".tar.gz rm -f "${package_name}".tar.gz
rm -f "${package_name}".zip rm -f "${package_name}".zip
@ -251,9 +253,15 @@ main() {
fi fi
fi fi
if [[ $appimage == true ]]; then source scripts/appimage.sh; fi if [[ $appimage == true ]]; then
if [[ $bundle == true && $dmg == true ]]; then source scripts/appdmg.sh "${package_name}"; fi source scripts/appimage.sh $flags --static
if [[ $innosetup == true ]]; then source scripts/innosetup/innosetup.sh -b "${build_dir}"; fi fi
if [[ $bundle == true && $dmg == true ]]; then
source scripts/appdmg.sh "${package_name}"
fi
if [[ $innosetup == true ]]; then
source scripts/innosetup/innosetup.sh $flags
fi
} }
main "$@" main "$@"

View File

@ -1,19 +1,23 @@
#include "api.h" #include "api.h"
int luaopen_system(lua_State *L); int luaopen_system(lua_State *L);
int luaopen_renderer(lua_State *L); int luaopen_renderer(lua_State *L);
int luaopen_regex(lua_State *L); int luaopen_regex(lua_State *L);
// int luaopen_process(lua_State *L); int luaopen_process(lua_State *L);
int luaopen_dirmonitor(lua_State* L);
int luaopen_utf8extra(lua_State* L);
static const luaL_Reg libs[] = { static const luaL_Reg libs[] = {
{ "system", luaopen_system }, { "system", luaopen_system },
{ "renderer", luaopen_renderer }, { "renderer", luaopen_renderer },
{ "regex", luaopen_regex }, { "regex", luaopen_regex },
// { "process", luaopen_process }, { "process", luaopen_process },
{ "dirmonitor", luaopen_dirmonitor },
{ "utf8extra", luaopen_utf8extra },
{ NULL, NULL } { NULL, NULL }
}; };
void api_load_libs(lua_State *L) { void api_load_libs(lua_State *L) {
for (int i = 0; libs[i].name; i++) for (int i = 0; libs[i].name; i++)
luaL_requiref(L, libs[i].name, libs[i].func, 1); luaL_requiref(L, libs[i].name, libs[i].func, 1);

View File

@ -7,6 +7,7 @@
#define API_TYPE_FONT "Font" #define API_TYPE_FONT "Font"
#define API_TYPE_PROCESS "Process" #define API_TYPE_PROCESS "Process"
#define API_TYPE_DIRMONITOR "Dirmonitor"
#define API_CONSTANT_DEFINE(L, idx, key, n) (lua_pushnumber(L, n), lua_setfield(L, idx - 1, key)) #define API_CONSTANT_DEFINE(L, idx, key, n) (lua_pushnumber(L, n), lua_setfield(L, idx - 1, key))

130
src/api/dirmonitor.c Normal file
View File

@ -0,0 +1,130 @@
#include "api.h"
#include <SDL.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <stdbool.h>
static unsigned int DIR_EVENT_TYPE = 0;
struct dirmonitor {
SDL_Thread* thread;
SDL_mutex* mutex;
char buffer[64512];
volatile int length;
struct dirmonitor_internal* internal;
};
struct dirmonitor_internal* init_dirmonitor();
void deinit_dirmonitor(struct dirmonitor_internal*);
int get_changes_dirmonitor(struct dirmonitor_internal*, char*, int);
int translate_changes_dirmonitor(struct dirmonitor_internal*, char*, int, int (*)(int, const char*, void*), void*);
int add_dirmonitor(struct dirmonitor_internal*, const char*);
void remove_dirmonitor(struct dirmonitor_internal*, int);
static int f_check_dir_callback(int watch_id, const char* path, void* L) {
lua_pushvalue(L, -1);
if (path)
lua_pushlstring(L, path, watch_id);
else
lua_pushnumber(L, watch_id);
lua_call(L, 1, 1);
int result = lua_toboolean(L, -1);
lua_pop(L, 1);
return !result;
}
static int dirmonitor_check_thread(void* data) {
struct dirmonitor* monitor = data;
while (monitor->length >= 0) {
if (monitor->length == 0) {
int result = get_changes_dirmonitor(monitor->internal, monitor->buffer, sizeof(monitor->buffer));
SDL_LockMutex(monitor->mutex);
if (monitor->length == 0)
monitor->length = result;
SDL_UnlockMutex(monitor->mutex);
}
SDL_Delay(1);
SDL_Event event = { .type = DIR_EVENT_TYPE };
SDL_PushEvent(&event);
}
return 0;
}
static int f_dirmonitor_new(lua_State* L) {
if (DIR_EVENT_TYPE == 0)
DIR_EVENT_TYPE = SDL_RegisterEvents(1);
struct dirmonitor* monitor = lua_newuserdata(L, sizeof(struct dirmonitor));
luaL_setmetatable(L, API_TYPE_DIRMONITOR);
memset(monitor, 0, sizeof(struct dirmonitor));
monitor->internal = init_dirmonitor();
return 1;
}
static int f_dirmonitor_gc(lua_State* L) {
struct dirmonitor* monitor = luaL_checkudata(L, 1, API_TYPE_DIRMONITOR);
SDL_LockMutex(monitor->mutex);
monitor->length = -1;
deinit_dirmonitor(monitor->internal);
SDL_UnlockMutex(monitor->mutex);
SDL_WaitThread(monitor->thread, NULL);
free(monitor->internal);
SDL_DestroyMutex(monitor->mutex);
return 0;
}
static int f_dirmonitor_watch(lua_State *L) {
struct dirmonitor* monitor = luaL_checkudata(L, 1, API_TYPE_DIRMONITOR);
lua_pushnumber(L, add_dirmonitor(monitor->internal, luaL_checkstring(L, 2)));
if (!monitor->thread)
monitor->thread = SDL_CreateThread(dirmonitor_check_thread, "dirmonitor_check_thread", monitor);
return 1;
}
static int f_dirmonitor_unwatch(lua_State *L) {
remove_dirmonitor(((struct dirmonitor*)luaL_checkudata(L, 1, API_TYPE_DIRMONITOR))->internal, lua_tonumber(L, 2));
return 0;
}
static int f_dirmonitor_check(lua_State* L) {
struct dirmonitor* monitor = luaL_checkudata(L, 1, API_TYPE_DIRMONITOR);
SDL_LockMutex(monitor->mutex);
if (monitor->length < 0)
lua_pushnil(L);
else if (monitor->length > 0) {
if (translate_changes_dirmonitor(monitor->internal, monitor->buffer, monitor->length, f_check_dir_callback, L) == 0)
monitor->length = 0;
lua_pushboolean(L, 1);
} else
lua_pushboolean(L, 0);
SDL_UnlockMutex(monitor->mutex);
return 1;
}
static const luaL_Reg dirmonitor_lib[] = {
{ "new", f_dirmonitor_new },
{ "__gc", f_dirmonitor_gc },
{ "watch", f_dirmonitor_watch },
{ "unwatch", f_dirmonitor_unwatch },
{ "check", f_dirmonitor_check },
{NULL, NULL}
};
int luaopen_dirmonitor(lua_State* L) {
luaL_newmetatable(L, API_TYPE_DIRMONITOR);
luaL_setfuncs(L, dirmonitor_lib, 0);
lua_pushvalue(L, -1);
lua_setfield(L, -2, "__index");
return 1;
}

View File

@ -0,0 +1,8 @@
#include <stdlib.h>
struct dirmonitor_internal* init_dirmonitor() { return NULL; }
void deinit_dirmonitor(struct dirmonitor_internal* monitor) { }
int get_changes_dirmonitor(struct dirmonitor_internal* monitor, char* buffer, size_t len) { return -1; }
int translate_changes_dirmonitor(struct dirmonitor_internal* monitor, char* buffer, int size, int (*callback)(int, const char*, void*), void* data) { return -1; }
int add_dirmonitor(struct dirmonitor_internal* monitor, const char* path) { return -1; }
void remove_dirmonitor(struct dirmonitor_internal* monitor, int fd) { }

View File

@ -0,0 +1,53 @@
#include <sys/inotify.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <poll.h>
struct dirmonitor_internal {
int fd;
// a pipe is used to wake the thread in case of exit
int sig[2];
};
struct dirmonitor_internal* init_dirmonitor() {
struct dirmonitor_internal* monitor = calloc(sizeof(struct dirmonitor_internal), 1);
monitor->fd = inotify_init();
pipe(monitor->sig);
fcntl(monitor->sig[0], F_SETFD, FD_CLOEXEC);
fcntl(monitor->sig[1], F_SETFD, FD_CLOEXEC);
return monitor;
}
void deinit_dirmonitor(struct dirmonitor_internal* monitor) {
close(monitor->fd);
close(monitor->sig[0]);
close(monitor->sig[1]);
}
int get_changes_dirmonitor(struct dirmonitor_internal* monitor, char* buffer, int length) {
struct pollfd fds[2] = { { .fd = monitor->fd, .events = POLLIN | POLLERR, .revents = 0 }, { .fd = monitor->sig[0], .events = POLLIN | POLLERR, .revents = 0 } };
poll(fds, 2, -1);
return read(monitor->fd, buffer, length);
}
int translate_changes_dirmonitor(struct dirmonitor_internal* monitor, char* buffer, int length, int (*change_callback)(int, const char*, void*), void* data) {
for (struct inotify_event* info = (struct inotify_event*)buffer; (char*)info < buffer + length; info = (struct inotify_event*)((char*)info + sizeof(struct inotify_event)))
change_callback(info->wd, NULL, data);
return 0;
}
int add_dirmonitor(struct dirmonitor_internal* monitor, const char* path) {
return inotify_add_watch(monitor->fd, path, IN_CREATE | IN_DELETE | IN_MOVED_FROM | IN_MODIFY | IN_MOVED_TO);
}
void remove_dirmonitor(struct dirmonitor_internal* monitor, int fd) {
inotify_rm_watch(monitor->fd, fd);
}

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