Compare commits

..

32 Commits

Author SHA1 Message Date
George Sokianos 1279fde93b Cleaned up and fixed os4build.sh 2022-01-11 22:49:14 +00:00
George Sokianos 7a347ca436 Some extra changes before the release 2022-01-11 20:04:48 +00:00
George Sokianos 7930499e6e Prepare 1.16.12.7 release 2022-01-11 20:00:14 +00:00
George Sokianos 9908e129a6 Added addons folder with plugins and color schemes 2022-01-11 19:59:12 +00:00
George Sokianos fb3d36da43 Some fixes on the README_OS4.md file 2022-01-06 20:22:35 +00:00
George Sokianos 94807d505c Added 'SDL_RENDERER_ACCELERATED' and 'SDL_RENDERER_PRESENTVSYNC' on SDL_CreateRenderer() 2022-01-05 22:13:35 +00:00
George Sokianos 44cd036b7a Added "SDL_RENDERER_ACCELERATED 2022-01-05 22:12:18 +00:00
George Sokianos 3c7da82ad2 Added the latest update version file in repo 2022-01-05 20:05:56 +00:00
George Sokianos d30a9622a5 Added config.scroll_past_end 2022-01-05 20:03:20 +00:00
George Sokianos 2fdf19ec49 Prepare 1.16.12.6 release 2022-01-04 12:47:51 +00:00
George Sokianos 9f656f8ab4 Fixed the issue with the current path introduced in 1.16.12.5 release #13 2022-01-03 21:25:25 +00:00
George Sokianos 32a3d4b933 Prepare 1.16.12.5 release 2022-01-03 00:53:13 +00:00
George Sokianos c5309e04d6 Fixed loading the current folder from terminal using the dot character 2022-01-03 00:43:04 +00:00
George Sokianos 1c3f766e6b Fixed the resolution on fullscreen toggle to be like the workbench (#4) 2022-01-02 23:47:50 +00:00
George Sokianos de6c0fd575 Removed the extra title bar when return from fullscreen 2022-01-02 23:47:19 +00:00
George Sokianos 7bd164b17e Added a workaround to fix the window resize assertion crash #2 2022-01-02 21:04:41 +00:00
George Sokianos de3e4815ee Removed clearQuit() and applied commit 456f6ed changes to free the resources 2022-01-02 19:04:18 +00:00
George Sokianos b5d4f3f0f8 Prepare 1.16.12.4 release 2021-12-31 12:15:39 +00:00
George Sokianos a788ac871b Fixed the gfx memory leak 2021-12-31 12:15:15 +00:00
George Sokianos 1f27d6f923 Removed some commented code 2021-12-31 12:14:48 +00:00
George Sokianos 5af782b884 prepare 1.16.12.3 release 2021-12-29 13:15:35 +00:00
George Sokianos 95ece12d74 Added Makefile and releases files 2021-12-27 15:11:10 +00:00
George Sokianos 03f2818657 Added more information at README_OS4 file 2021-12-26 17:26:15 +00:00
George Sokianos 689901daca Added Amiga version and cookie stack 2021-12-26 17:15:22 +00:00
George Sokianos 4499f1f111 Some code cleanup and added .config folder to gitignore 2021-12-26 17:08:09 +00:00
George Sokianos 14d813cea1 Made HOME optional and added some caching at the _fullpath() 2021-12-26 17:04:00 +00:00
George Sokianos 2c711138d7 Removed the AMIGAOS4 global variable and now I am using the PLATFORM one to determine the running platform 2021-12-22 19:10:33 +00:00
George Sokianos ff535843e8 Fixed keyboard shortcuts 2021-12-21 20:26:28 +00:00
George Sokianos fdd2f3af33 Fixed a forgotten hardcoded path 2021-12-21 18:27:25 +00:00
George Sokianos 7c85530e92 Added icon and OS4 readme file 2021-12-21 17:56:49 +00:00
George Sokianos e13efe91b4 Added the build script for font renderer 2021-12-21 17:55:37 +00:00
George Sokianos c155f797cf First commit of compilable code for OS4 2021-12-21 17:52:44 +00:00
245 changed files with 10703 additions and 24260 deletions

View File

@ -1,11 +0,0 @@
root = true
[*]
charset = utf-8
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[meson.build]
indent_size = 4

6
.gitattributes vendored
View File

@ -1,7 +1,3 @@
# Set the default behavior, in case people don't have core.autocrlf set.
# See https://help.github.com/en/articles/dealing-with-line-endings
* text=auto
winlib/* linguist-vendored
src/lib/* linguist-vendored
resources/icons/icon.inl linguist-vendored
icon.inl linguist-vendored

38
.github/labeler.yml vendored
View File

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

View File

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

View File

@ -1,20 +1,12 @@
name: CI
on:
push:
branches:
- '*'
pull_request:
branches:
- '*'
workflow_dispatch:
jobs:
build_linux:
name: Linux
runs-on: ubuntu-22.04
build-linux:
name: Build Linux
runs-on: ubuntu-18.04
strategy:
matrix:
config:
@ -24,110 +16,48 @@ jobs:
CC: ${{ matrix.config.cc }}
CXX: ${{ matrix.config.cxx }}
steps:
- name: Set Environment Variables
if: ${{ matrix.config.cc == 'gcc' }}
run: |
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
echo "INSTALL_REF=${GITHUB_REF##*/}" >> "$GITHUB_ENV"
echo "INSTALL_NAME=lite-xl-${GITHUB_REF##*/}-linux-$(uname -m)-portable" >> "$GITHUB_ENV"
- uses: actions/checkout@v2
- name: Python Setup
- name: Set up Python
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
- name: Build
python-version: 3.6
- name: Install dependencies
run: |
bash --version
bash scripts/build.sh --debug --forcefallback --portable
- name: Package
if: ${{ matrix.config.cc == 'gcc' }}
run: bash scripts/package.sh --version ${INSTALL_REF} --debug --binary
- name: Upload Artifacts
uses: actions/upload-artifact@v2
if: ${{ matrix.config.cc == 'gcc' }}
with:
name: Linux Artifacts
path: ${{ env.INSTALL_NAME }}.tar.gz
build_macos:
name: macOS (x86_64)
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=${GITHUB_REF##*/}" >> "$GITHUB_ENV"
echo "INSTALL_NAME=lite-xl-${GITHUB_REF##*/}-macos-$(uname -m)" >> "$GITHUB_ENV"
- uses: actions/checkout@v2
- name: Python Setup
uses: actions/setup-python@v2
with:
python-version: 3.9
- name: Install Dependencies
run: bash scripts/install-dependencies.sh --debug
- name: Build
run: |
bash --version
bash scripts/build.sh --bundle --debug --forcefallback
- name: Create DMG Image
run: bash scripts/package.sh --version ${INSTALL_REF} --debug --dmg
- name: Upload DMG Image
sudo apt-get install -qq libsdl2-dev libfreetype6 ninja-build
pip3 install meson
- name: Build package
run: bash build-packages.sh x86-64
- name: upload packages
uses: actions/upload-artifact@v2
with:
name: macOS DMG Image
path: ${{ env.INSTALL_NAME }}.dmg
name: Ubuntu Package
path: lite-xl-linux-*.tar.gz
build_windows_msys2:
name: Windows
runs-on: windows-2019
build-macox:
name: Build Mac OS X
runs-on: macos-10.15
strategy:
matrix:
msystem: [MINGW32, MINGW64]
defaults:
run:
shell: msys2 {0}
config:
# - { name: "GCC", cc: gcc-10, cxx: g++-10 }
- { name: "clang", cc: clang, cxx: clang++ }
env:
CC: ${{ matrix.config.cc }}
CXX: ${{ matrix.config.cxx }}
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=${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
run: bash scripts/install-dependencies.sh --debug
- name: Build
run: |
bash --version
bash scripts/build.sh -U --debug --forcefallback
- name: Package
run: bash scripts/package.sh --version ${INSTALL_REF} --debug --binary
- name: Upload Artifacts
uses: actions/upload-artifact@v2
with:
name: Windows Artifacts
path: ${{ env.INSTALL_NAME }}.zip
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: 3.9
- name: Install dependencies
run: |
pip3 install meson
brew install ninja sdl2
- name: Build package
run: bash build-packages.sh x86-64
- name: upload packages
uses: actions/upload-artifact@v2
with:
name: Mac OS X Package
path: lite-xl-macosx-*.zip

View File

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

33
.gitignore vendored
View File

@ -1,30 +1,11 @@
build*/
.build*/
lhelper/
submodules/
subprojects/*/
/appimage*
.vscode
.cache
.ccls-cache
.lite-debug.log
build*
.build*
.run*
*.diff
*.exe
*.tar.gz
*.zip
*.DS_Store
*App*
compile_commands.json
error.txt
lite-xl*
LiteXL*
*.tar.gz
.lite-debug.log
subprojects/lua
subprojects/libagg
sybprojects/lua
lite
.config/
*.lha
release_files
*.o
*.snalyzerinfo
!resources/windows/*.diff

View File

@ -1,4 +1,4 @@
Copyright (c) 2020-2021 Lite XL Team
Copyright (c) 2020-2021 Francesco Abbate
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

View File

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

View File

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

190
README.md
View File

@ -1,160 +1,126 @@
# Lite XL
[![CI]](https://github.com/lite-xl/lite-xl/actions/workflows/build.yml)
[![Discord Badge Image]](https://discord.gg/RWzqC3nx7K)
![screenshot-dark](https://user-images.githubusercontent.com/433545/111063905-66943980-84b1-11eb-9040-3876f1133b20.png)
![screenshot-dark]
A lightweight text editor written in Lua, adapted from [lite](https://github.com/rxi/lite)
A lightweight text editor written in Lua, adapted from [lite].
* **[Get Lite XL](https://github.com/franko/lite-xl/releases/latest)** — Download
for Windows, Linux and Mac OS (notarized app).
* **[Get started](doc/usage.md)** — A quick overview on how to get started
* **[Get plugins](https://github.com/franko/lite-plugins)** — Add additional
functionality, adapted for Lite XL
* **[Get color themes](https://github.com/rxi/lite-colors)** — Add additional colors
themes
* **[Get Lite XL]** — Download for Windows, Linux and Mac OS.
* **[Get plugins]** — Add additional functionality, adapted for Lite XL.
* **[Get color themes]** — Add additional colors themes.
Please refer to our [website] for the user and developer documentation,
including [build] instructions details. A quick build guide is described below.
Lite XL has support for high DPI display on Windows and Linux and,
since 1.16.7 release, it supports **retina displays** on macOS.
Lite XL has support for high DPI display on Windows and Linux and, since 1.16.7 release, it supports **retina displays** on Mac OS.
Please note that Lite XL is compatible with lite for most plugins and all color themes.
We provide a separate lite-xl-plugins repository for Lite XL, because in some cases
some adaptations may be needed to make them work better with Lite XL.
The repository with modified plugins is https://github.com/lite-xl/lite-xl-plugins.
We provide a separate lite-plugins repository for Lite XL, because in some cases some adaptations may be needed to make them work better with Lite XL.
The repository with modified plugins is http://github.com/franko/lite-plugins.
The changes and differences between Lite XL and rxi/lite are listed in the
[changelog].
The changes and differences between Lite XL and rxi/lite are listed in the [changelog](https://github.com/franko/lite-xl/blob/master/changelog.md).
## Overview
Lite XL is derived from [lite].
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,
or to use without doing either.
Lite XL is derived from lite. 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, or to use without doing either.
The aim of Lite XL compared to lite is to be more user friendly,
improve the quality of font rendering, and reduce CPU usage.
The aim of Lite XL compared to lite is to be more user friendly, improve the quality of font rendering, and reduce CPU usage.
## Customization
Additional functionality can be added through plugins which are available in
the [plugins repository] or in the [Lite XL plugins repository].
the [plugins repository](https://github.com/rxi/lite-plugins) or in the [Lite XL-specific plugins repository](https://github.com/franko/lite-plugins).
Additional color themes can be found in the [colors repository].
Additional color themes can be found in the [colors repository](https://github.com/rxi/lite-colors).
These color themes are bundled with all releases of Lite XL by default.
## Quick Build Guide
The editor can be customized by making changes to the [user module](data/user/init.lua).
If you compile Lite XL yourself, it is recommended to use the script
`build-packages.sh`:
## Building
You can build Lite XL yourself using Meson.
In addition, the `build-packages.sh` script can be used to compile Lite XL and create an OS-specific package for Linux, Windows or Mac OS.
The following libraries are required:
- freetype2
- SDL2
The following libraries are **optional**:
- libagg
- Lua 5.2
If they are not found, they will be downloaded and compiled by Meson.
Otherwise, if they are present, they will be used to compile Lite XL.
On Debian-based systems the required libraries and Meson can be installed using the following commands:
```sh
bash build-packages.sh -h
# To install the required libraries:
sudo apt install libfreetype6-dev libsdl2-dev
# To install Meson:
sudo apt install meson
# or pip3 install --user meson
```
The script will run Meson and create a tar compressed archive with the application or,
for Windows, a zip file. Lite XL can be easily installed
by unpacking the archive in any directory of your choice.
Otherwise the following is an example of basic commands if you want to customize
the build:
To build Lite XL with Meson the commands below can be used:
```sh
meson setup --buildtype=release --prefix <prefix> build
meson setup --buildtype=release build
meson compile -C build
DESTDIR="$(pwd)/lite-xl" meson install --skip-subprojects -C build
meson install -C build
```
where `<prefix>` might be one of `/`, `/usr` or `/opt`, the default is `/`.
To build a bundle application on macOS:
If you are using a version of Meson below 0.54 you need to use diffent commands to compile and install:
```sh
meson setup --buildtype=release --Dbundle=true --prefix / build
meson compile -C build
DESTDIR="$(pwd)/Lite XL.app" meson install --skip-subprojects -C build
meson setup --buildtype=release build
ninja -C build
ninja -C build install
```
Please note that the package is relocatable to any prefix and the option prefix
affects only the place where the application is actually installed.
When performing the `meson setup` command you may enable the `-Dportable=true` option to specify whether files should be installed as in a portable application.
## Installing Prebuilt
If `portable` is enabled, Lite XL is built to use a `data` directory placed next to the executable.
Otherwise, Lite XL will use unix-like directory locations.
In this case, the `data` directory will be `$prefix/share/lite-xl` and the executable will be located in `$prefix/bin`.
`$prefix` is determined when the application starts as a directory such that `$prefix/bin` corresponds to the location of the executable.
Head over to [releases](https://github.com/lite-xl/lite-xl/releases) and download the version for your operating system.
The `user` directory does not depend on the `portable` option and will always be `$HOME/.config/lite-xl`.
`$HOME` is determined from the corresponding environment variable.
As a special case on Windows the variable `$USERPROFILE` will be used instead.
### Linux
Unzip the file and `cd` into the `lite-xl` directory:
If you compile Lite XL yourself, it is recommended to use the script `build-packages.sh`:
```sh
tar -xzf <file>
cd lite-xl
bash build-packages.sh <arch>
```
To run lite-xl without installing:
```sh
cd bin
./lite-xl
```
The script will run Meson and create a zip file with the application or, for linux, a tar compressed archive.
Lite XL can be easily installed by unpacking the archive in any directory of your choice.
To install lite-xl copy files over into appropriate directories:
On Windows two packages will be created, one called "portable" using the "data" folder next to the executable and
the other one using a unix-like file layout. Both packages works correctly. The one with unix-like file layout
is meant for people using a unix-like shell and the command line.
```sh
mkdir -p $HOME/.local/bin && cp bin/lite-xl $HOME/.local/bin
cp -r share $HOME/.local
```
If `$HOME/.local/bin` is not in PATH:
```sh
echo -e 'export PATH=$PATH:$HOME/.local/bin' >> $HOME/.bashrc
```
To get the icon to show up in app launcher:
```sh
xdg-desktop-menu forceupdate
```
You may need to logout and login again to see icon in app launcher.
To uninstall just run:
```sh
rm -f $HOME/.local/bin/lite-xl
rm -rf $HOME/.local/share/icons/hicolor/scalable/apps/lite-xl.svg \
$HOME/.local/share/applications/org.lite_xl.lite_xl.desktop \
$HOME/.local/share/metainfo/org.lite_xl.lite_xl.appdata.xml \
$HOME/.local/share/lite-xl
```
Please note that there aren't any hard-coded directories in the executable, so that the
package can be extracted and used in any directory.
Mac OS X is fully supported and a notarized app disk image is provided in the [release page](https://github.com/franko/lite-xl/releases).
In addition the application can be compiled using the generic instructions given above.
## Contributing
Any additional functionality that can be added through a plugin should be done
as a plugin, after which a pull request to the [Lite XL plugins repository] can be made.
as a plugin, after which a pull request to the
[plugins repository](https://github.com/rxi/lite-plugins) can be made.
If the plugin uses any Lite XL-specific functionality, please open a pull request to the
[Lite XL plugins repository](https://github.com/franko/lite-plugins).
Pull requests to improve or modify the editor itself are welcome.
## Licenses
## License
This project is free software; you can redistribute it and/or modify it under
the terms of the MIT license. See [LICENSE] for details.
See the [licenses] file for details on licenses used by the required dependencies.
[CI]: https://github.com/lite-xl/lite-xl/actions/workflows/build.yml/badge.svg
[Discord Badge Image]: https://img.shields.io/discord/847122429742809208?label=discord&logo=discord
[screenshot-dark]: https://user-images.githubusercontent.com/433545/111063905-66943980-84b1-11eb-9040-3876f1133b20.png
[lite]: https://github.com/rxi/lite
[website]: https://lite-xl.com
[build]: https://lite-xl.com/en/documentation/build
[Get Lite XL]: https://github.com/lite-xl/lite-xl/releases/latest
[Get plugins]: https://github.com/lite-xl/lite-xl-plugins
[Get color themes]: https://github.com/lite-xl/lite-xl-colors
[changelog]: https://github.com/lite-xl/lite-xl/blob/master/changelog.md
[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
[LICENSE]: LICENSE
[licenses]: licenses/licenses.md
the terms of the MIT license. See [LICENSE](LICENSE) for details.

View File

@ -1,280 +0,0 @@
# Lite XL v2 for AmigaOS 4.1 FE & MorphOS 3
Lite XL is a lightweight text editor written in Lua.
The port is not perfect and it might have issues here and there. For example
the filesystem notifications are not working yet. So when you make changes
at a project folder those will not be reflected in Lite XL automatically.
It might crash from time to time, if there is a path problem, but overall
it works pretty well. This is my daily editor for any kind of development.
If it crashes on your system, try to delete to `.config` folder.
## Installation
You can extract the Lite XL archive wherever you want and run the *lite*
editor.
## Configuration folder
This editor creates a `.config` folder where the configuration is saved, as
well as plugins, themes etc.. By default this version uses the
executable folder, but if you want to override it, you can create an ENV
variable named `HOME` and set there your prefferable path.
You can check if there is one already set by executing the following command
in a shell
```
GetEnv HOME
```
If there is one set, then you will see the path at the output.
Otherwise, you can set your home path be executing the following command.
Change the path to the one of your preference.
```
SetEnv SAVE HOME "Sys:home/"
```
## Addons
### Colors
Colors are lua files that set the color scheme of the editor. There are
light and dark themes for you to choose.
To install and use them you have to copy the ones you would like from
`addons/colors/light` or `addons/colors/dark` into the folder
`.config/lite-xl/colors/`. Don't add light or dark folders. Just copy the
.lua files in there.
Then you have to start Lite XL and open your configuration by clicking
at the cog icon at the toolbar (bottom left sixth icon). Go at the line
that looks like below
```
-- core.reload_module("colors.summer")
```
and change the `summer` with the name of your color theme. Also, remove
the two dashes `--` at the start of the line and save the file. If you
did everything right, the color schema should change instantly.
The themes can also be found at
https://github.com/lite-xl/lite-xl-colors
### Plugins
This Lite XL release is based on version 2.0.3 of the application as
released on other systems, by the original development team.
This not the latest version. This means that some of the latest
plugins might not working at all or need modifications to work.
To make it easier for you, I gathered some of the plugins that are working
well, and I included them under `addons/plugins`. For you to install the
ones you would like to use, you have to copy the `.lua` files into the
folder `.config/lite-xl/plugins/` and restart the editor.
Please, choose wisely, because adding all the plugins might make the editor
slower on your system. I would recommend you add only those that you really
need.
The included plugins are the following:
**autoinsert**
Automatically inserts closing brackets and quotes. Also allows selected
text to be wrapped with brackets or quotes.
**autosaveonfocuslost**
Automatically saves files that were changed when the main window loses
focus by switching to another application
**autowrap**
Automatically hardwraps lines when typing
**bigclock**
Shows the current time and date in a view with large text
**bracketmatch**
Underlines matching pair for bracket under the caret
**colorpreview**
Underlays color values (eg. `#ff00ff` or `rgb(255, 0, 255)`) with their
resultant color.
**ephemeral_tabs**
Preview tabs. Opening a doc will replace the contents of the preview tab.
Marks tabs as non-preview on any change or tab double clicking.
**ghmarkdown**
Opens a preview of the current markdown file in a browser window.
On AmigaOS 4 it uses *urlopen* and on MorphOS it uses *openurl* to load
the generated html in the browser.
**indentguide**
Adds indent guides
**language_guide**
Syntax for the AmigaGuide scripting language
**language_hws**
Syntax for the Hollywood language
**language_make**
Syntax for the Make build system language
**language_sh**
Syntax for shell scripting language
**lfautoinsert**
Automatically inserts indentation and closing bracket/text after newline
**markers**
Add markers to docs and jump between them quickly
**minimap**
Shows a minimap on the right-hand side of the docview. Please note that
this plugin will make the editor slower on file loading and scrolling.
**navigate**
Allows moving back and forward between document positions, reducing the
amount of scrolling
**nonicons**
File icons set for TreeView. Download TTF font to your config/fonts
folder from https://github.com/yamatsum/nonicons/tree/master/dist
**opacity**
Change the opaqueness/transparency of lite-xl using shift+mousewheel
or a command.
**openfilelocation**
Opens the parent directory of the current file in the file manager
**rainbowparen**
Show nesting of parentheses with rainbow colours
**restoretabs**
Keep a list of recently closed tabs, and restore the tab in order on
cntrl+shift+t.
**select_colorscheme**
Select a color theme, like VScode, Sublime Text.
(plugin saves changes)
**selectionhighlight**
Highlights regions of code that match the current selection
**smallclock**
It adds a small clock at the bottom right corner.
## Tips and tricks
### Transitions
If you want to disable the transitions and make the editor faster,
open your configuration file by clicking at the cog icon at the toolbar
(bottom left, 6th icon) and add the following line at the end of the file,
and then save it. You might need to restart your editor (CTRL+SHIFT+R)
```
config.transitions = false
```
### Hide files from the file list
If you would like to hide files or whole folder from the left side bar list,
open your configuration by clicking at the cog icon at the toolbar
(bottom left sixth icon) and add the followline at the end of the file and
save it. This hides all the files that start with a dot, and all the `.info`
files. You might need to restart your editor (CTRL+SHIFT+R)
```
config.ignore_files = {"^%.", "%.info$"}
```
You can add as many rules as you want in there, to hide files or
folders, as you like.
## I would like to thank
- IconDesigner for the proper glow icons that are included in the release
- Capehill for his tireless work on SDL port for AmigaOS 4.1 FE
- Michael Trebilcock for his port on liblua
- Bruno "BeWorld" Peloille for his great work on porting SDL to MorphOS
and for his valuable help
- Lite XL original team for being helpful and providing info
Without all the above Lite XL would not be possible
## Support
If you enjoy what I am doing and would like to keep me up during the night,
please consider to buy me a coffee at:
https://ko-fi.com/walkero
## Known issues
You can find the known issues at
https://git.walkero.gr/walkero/lite-xl/issues
# Changelog
## [2.1.0r1] - 2022-10-10
### Added
- This version of LiteXL recognises changes that are done outside the editor
in files and folders, and updates the items when it gets focus again.
### Changed
- Synced the code with the latest upstream master branch, which means that
this version is based on the latest available source
- Now the plugins need to be version 3. The older versions will not work.
All the included plugins are updated to the latest available version.
- Compiled with SDL 2.24
- Compiled with Lua 5.4
### Fixed
- Fixed regex issues with some plugins
- Fixed "Open in System" on AmigaOS 4 and MorphOS. When you right click
at a file or a folder at the treeview at the left side, then depending
the type of the item opens on Workbench. A folder opens in a list view
and a file opens with its default tool
- Fixed markdown preview on MorphOS. Now, it calls openurl with the html
file (#20)
- Fixed locale issues on MorphOS (again), since the previous fix didn't
actually fixed the problem
## [2.0.3r3] - 2022-09-26
### Added
- Added plugin for AmigaGuide files
- Added plugin for Hollywood files
### Fixed
- Fixed non existing path crashes on OS4 and MorphOS
- Fixed editor refresh whenever init.lua is changed, no matter the working
folder
- Fixed an issue when the user added a directory in the project that
already existed
- Fixed locale issue on start for MorphOS. Now it should start just fine
no matter what locale the user has on his system.
- Fixed "Find" on MorphOS that was not working (shortcut CTRL+F)
- If the user selects to change the project folder and inserts Sys: or any
partition name, the included folders will be listed as suggestions
### Changed
- Removed linking with unix on OS4 build
- Makefiles updated
## [2.0.3r2] - 2022-06-18
### Added
- First public MorphOS version released
### Changed
- Merged source code for both AmigaOS 4 and MorphOS
- Moved the declaration of the $VER and $STACK for the AmigaOS 4 version,
so to happen only once (reported by capehill)
### Fixed
- Fixed the usage of NumPad (reported by root)
## [2.0.3r1] - 2022-03-30
### Changed
- Applied all the necessary changes to make it run under AmigaOS 4.1 FE
- Fixes and changes
# Disclaimer
YOU MAY USE IT AT YOUR OWN RISK!
I will not be held responsible for any data loss or problems you might get
by using this software.

231
README_OS4.md Normal file
View File

@ -0,0 +1,231 @@
# Lite XL for AmigaOS 4.1 FE
Lite XL is a lightweight text editor written in Lua.
## Installation
You can extract the Lite XL archive wherever you want and run the *lite*
editor.
## Configuration folder
This editor creates a `.config` folder where the configuration is saved, as
well as plugins, themes etc.. By default this AmigaOS 4.1 FE version uses the
executable folder, but if you want to ovveride it, create an ENV variable
named `HOME` and set there your path.
You can check if there is one already set by executing the following command
in a shell
```
GetEnv HOME
```
If there is one set, then you will see the path at the output.
Otherwise, you can set your home path be executing the following command.
Change the path to the one of your preference.
```
SetEnv SAVE HOME "Sys:home/"
```
## Addons
### Colors
Colors are lua files that set the color scheme of the editor. There are
light and dark themes for you to choose.
To install and use them you have to copy the ones you would like from
`addons/colors/light` or `addons/colors/dark` into the folder
`.config/lite-xl/colors/`. Don't add light or dark folders. Just copy the
.lua files in there.
Then you have to start Lite XL and open your configuration by clicking
at the cog icon at the toolbar (bottom left sixth icon). Go at the line
that looks like below
```
-- core.reload_module("colors.summer")
```
and change the `summer` with the name of your color theme. Also, remove
the two dashes `--` at the start of the line and save the file. If you
did everything right, the color schema should change instantly.
The themes can also be found at
https://github.com/lite-xl/lite-xl-colors
### Plugins
The Lite XL that you are using on AmigaOS 4 is based on version 1.16.12
and not the latest version that is available by the development team.
This means that the latest plugins are not working at all or need some
modifications to work.
To make it easier for you, I gathered some of the plugins that are working
well, and I included them at the `addons/plugins`. For you to install the
ones you would like to use, you have to copy the `.lua` files into the
folder `.config/lite-xl/plugins/` and restart the editor.
The included plugins are the following:
**autoinsert**
Automatically inserts closing brackets and quotes. Also allows selected
text to be wrapped with brackets or quotes.
**autowrap**
Automatically hardwraps lines when typing
**bigclock**
Shows the current time and date in a view with large text
**bracketmatch**
Underlines matching pair for bracket under the caret
**colorpreview**
Underlays color values (eg. `#ff00ff` or `rgb(255, 0, 255)`) with their
resultant color.
**eofnewline**
Make sure the file ends with one blank line.
**ephemeraldocviews**
Preview tabs. Opening a doc will replace the contents of the preview tab.
Marks tabs as non-preview on any change or tab double clicking.
**ghmarkdown**
Opens a preview of the current markdown file in a browser window
**hidelinenumbers**
Hides the line numbers on the left of documents
**indentguide**
Adds indent guides
**language_make**
Syntax for the Make build system language
**language_sh**
Syntax for shell scripting language
**lfautoinsert**
Automatically inserts indentation and closing bracket/text after newline
**markers**
Add markers to docs and jump between them quickly
**memoryusage**
Show memory usage in the status view
**minimap**
Shows a minimap on the right-hand side of the docview.
**motiontrail**
Adds a motion-trail to the caret
**navigate**
Allows moving back and forward between document positions, reducing the
amount of scrolling
**rainbowparen**
Show nesting of parentheses with rainbow colours
**restoretabs**
Keep a list of recently closed tabs, and restore the tab in order on
cntrl+shift+t.
**selectionhighlight**
Highlights regions of code that match the current selection
**todotreeview**
Todo tree viewer for annotations in code like `TODO`, `BUG`, `FIX`,
`IMPROVEMENT`
## Tips and tricks
### Transitions
If you want to disable the transitions and make the scrolling a little faster,
open your configuration by clicking at the cog icon at the toolbar
(bottom left sixth icon) and add the followline at the end of the file and
save it.
```
config.transitions = false
```
### Hide files from the file list
If you would like to hide files or whole folder from the left side bar list,
open your configuration by clicking at the cog icon at the toolbar
(bottom left sixth icon) and add the followline at the end of the file and
save it. This hides all the files that start with a dot, and all the `.info`
files.
```
config.ignore_files = {"^%.", "%.info$"}
```
You can add as many rules as you want in there, to hide fore files or
folders, as you like.
## Know issues
You can find the known issues at
https://git.walkero.gr/walkero/lite-xl/issues
# Changelog
## [1.16.12.7] - 2022-01-11
## Added
- Added config.scroll_past_end that when its true lets the user scroll
further than the end of the file. By default is set to true.
- Added "SDL_RENDERER_ACCELERATED" and "SDL_RENDERER_PRESENTVSYNC" on
SDL_CreateRenderer() since this reduces the CPU usage when the user
scrolls and seems to work pretty good on my systems (X5000, A1222 and
microAmigaOne). This is exeprimental. If this brings problems on your
system, you can disable them using SDL ENV variable, like below:
setenv SDL_RENDER_VSYNC 0
setenv SDL_RENDER_DRIVER "software"
- Added plugins and color schemas in addons folder and information at
the README_OS4 file (#10)
## Changed
- Compiled Lite XL with gcc 8.4.0
## [1.16.12.6] - 2022-01-04
### Fixed
- Fixed a problem introduced in previous version when LiteXL was executed
from the root path of a partition or from ram disk (#13)
## [1.16.12.5] - 2022-01-03
### Changed
- Changed the Gfx memory leak solution to a fix that was applied by the
editor development team on Lua scripts at a later version. Less custom
code for AmigaOS 4 port.
- Now, when return from fullscreen, there is no extra header visible
at the top of the window content
### Fixed
- Fixed the assertion error and crash when the window is resized (#2)
- Fixed the resolution on fullscreen toggle to be like the workbench (#4)
- Fixed loading the current folder from terminal using the dot, like
`lite .` or without it (#3)
## [1.16.12.4] - 2021-12-31
### Fixed
- Fixed the Gfx memory leak. Now LiteXL frees the reserved memory from the
gfx card.
## [1.16.12.3] - 2021-12-29
### Changed
- Compiled with an experimental version of the latest Anti-Grain Geometry
library. This is might have issues and crash LiteXL
## [1.16.12.2] - 2021-12-26
### Added
- Added Amiga version. This version of LiteXL is based on v1.16.12 source code
which will not change. I will use the fourth digit to distinguish different
AmigaOS 4 releases, until a new port of the latest available source (v2.x)
is made.
### Fixed
- The keyboard shortcuts are now working
### Changed
- Now the `HOME` ENV variable is optional. If this is not set, the LiteXL
folder will be used to create user's `.config` folder

View File

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

View File

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

39
build.sh Executable file
View File

@ -0,0 +1,39 @@
#!/bin/bash
cflags+="-Wall -O3 -g -std=gnu11 -fno-strict-aliasing -Isrc -Ilib/font_renderer"
cflags+=" $(pkg-config --cflags lua5.2) $(sdl2-config --cflags)"
lflags="-static-libgcc -static-libstdc++"
for package in libagg freetype2 lua5.2 x11; do
lflags+=" $(pkg-config --libs $package)"
done
lflags+=" $(sdl2-config --libs) -lm"
if [[ $* == *windows* ]]; then
echo "cross compiling for windows is not yet supported"
exit 1
else
outfile="lite"
compiler="gcc"
cxxcompiler="g++"
fi
lib/font_renderer/build.sh || exit 1
libs=libfontrenderer.a
echo "compiling lite..."
for f in `find src -name "*.c"`; do
$compiler -c $cflags $f -o "${f//\//_}.o"
if [[ $? -ne 0 ]]; then
got_error=true
fi
done
if [[ ! $got_error ]]; then
echo "linking..."
$cxxcompiler -o $outfile *.o $libs $lflags
fi
echo "cleaning up..."
rm *.o *.a
echo "done"

View File

@ -1,519 +1,25 @@
# Changes Log
This files document the changes done in Lite XL for each release.
## [2.1.0] - 2022-09-25
### 1.16.12
### New Features
* Make distinction between
[line and block comments](https://github.com/lite-xl/lite-xl/pull/771),
and added all appropriate functionality to the commenting/uncommenting lines.
Add syntax support for C++.
* [Added in line paste mode](https://github.com/lite-xl/lite-xl/pull/713),
if you copy without a selection.
Respect the `XDG_CONFIG_HOME` variable if set to define the USERDIR.
* 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.
Fix an error that prevented navigating large repositories in some rare situations.
* Added in [soft line wrapping](https://github.com/lite-xl/lite-xl/pull/636)
as core plugin, under `linewrapping.lua`, use `F10` to activate.
Minor preformance improvements for drawing operations and events handling.
* Revamped [StatusView](https://github.com/lite-xl/lite-xl/pull/852) API with
new features that include:
Improve macOS keybindings thanks to @bjornbm and @prantlf.
* 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...
Improve behavior of applications when restoring workspaces to avoid displaying empty documents.
* 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.
### 1.16.11
**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.
Fixed a bug in incremental syntax highlighting affecting documents with
multiple-lines comments or strings.
The application now always shows the tabs in the documents' view even when
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 pixel not correctly drawn at the window's right edge.
Treat correctly and open network paths on Windows.
Add some improvements for very slow network file systems.
Fix problem with python syntax highlighting, contributed by @dflock.
## [2.0.2] - 2021-09-10
Fix problem project directory when starting the application from Launcher on
macOS.
Improved LogView. Entries can now be expanded and there is a context menu to
copy the item's content.
Change the behavior of `ctrl+d` to add a multi-cursor selection to the next
occurrence. The old behavior to move the selection to the next occurrence is
now done using the shortcut `ctrl+f3`.
Added a command to create a multi-cursor with all the occurrences of the
current selection. Activated with the shortcut `ctrl+shift+l`.
Fix problem when trying to close an unsaved new document.
No longer shows an error for the `-psn` argument passed to the application on
macOS.
Fix `treeview:open-in-system` command on Windows.
Fix rename command to update name of document if opened.
Improve the find and replace dialog so that previously used expressions can be
recalled using "up" and "down" keys.
Build package script rewrite with many improvements.
Use bigger fonts by default.
Other minor improvements and fixes.
With many thanks to the contributors: @adamharrison, @takase1121, @Guldoman,
@redtide, @Timofffee, @boppyt, @Jan200101.
## [2.0.1] - 2021-08-28
Fix a few bugs and we mandate the mod-version 2 for plugins.
This means that users should ensure they have up-to-date plugins for Lite XL 2.0.
Here some details about the bug fixes:
- fix a bug that created a fatal error when using the command to change project
folder or when closing all the active documents
- add a limit to avoid scaling fonts too much and fix a related invalid memory
access for very small fonts
- fix focus problem with NagView when switching project directory
- fix error that prevented the verification of plugins versions
- fix error on X11 that caused a bug window event on exit
## [2.0] - 2021-08-16
The 2.0 version of lite contains *breaking changes* to lite, in terms of how
plugin settings are structured; any custom plugins may need to be adjusted
accordingly (see note below about plugin namespacing).
Contains the following new features:
Full PCRE (regex) support for find and replace, as well as in language syntax
definitions. Can be accessed programatically via the lua `regex` module.
A full, finalized subprocess API, using libreproc. Subprocess can be started
and interacted with using `Process.new`.
Support for multi-cursor editing. Cursors can be created by either ctrl+clicking
on the screen, or by using the keyboard shortcuts ctrl+shift+up/down to create
an additional cursor on the previous/next line.
All build systems other than meson removed.
A more organized directory structure has been implemented; in particular a docs
folder which contains C api documentation, and a resource folder which houses
all build resources.
Plugin config namespacing has been implemented. This means that instead of
using `config.myplugin.a`, to read settings, and `config.myplugin = false` to
disable plugins, this has been changed to `config.plugins.myplugin.a`, and
`config.plugins.myplugin = false` respectively. This may require changes to
your user plugin, or to any custom plugins you have.
A context menu on right click has been added.
Changes to how we deal with indentation have been implemented; in particular,
hitting home no longer brings you to the start of a line, it'll bring you to
the start of indentation, which is more in line with other editors.
Lineguide, and scale plugins moved into the core, and removed from
`lite-plugins`. This may also require you to adjust your personal plugin
folder to remove these if they're present.
In addition, there have been many other small fixes and improvements, too
numerous to list here.
## [1.16.11] - 2021-05-28
When opening directories with too many files lite-xl now keep displaying files
and directories in the treeview. The application remains functional and the
directories can be explored without using too much memory. 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.The "Project Search: Find" will work
by searching all the files present in the project directory even if they are
not indexed.
When opening directories with too many files lite-xl now keep diplaying files and directories in the treeview.
The application remains functional and the directories can be explored without using too much memory.
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.
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.
@ -533,30 +39,30 @@ Fix bug with close button not working in borderless window mode.
Fix problem with normalization of filename for opened documents.
## [1.16.10] - 2021-05-22
### 1.16.10
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++. Other syntax improvements contributed by @vincens2005.
Thanks to the new system we provide more a accurate syntax highlighting for Lua, C and C++.
Other syntax improvements contributed by @vincens2005.
Move to JetBrains Mono and Fira Sans fonts for code and UI respectively.
They are provided under the SIL Open Font License, Version 1.1.
Thet are provided under the SIL Open Font License, Version 1.1.
See `doc/licenses.md` for license details.
Fixed bug with fonts and rencache module. Under very specific situations the
application was crashing due to invalid memory access.
Fixed bug with fonts and rencache module.
Under very specific situations the application was crashing due to invalid memory access.
Add documentation for keymap binding, thanks to @Janis-Leuenberger.
Added a contributors page in `doc/contributors.md`.
## [1.16.9] - 2021-05-06
### 1.16.9
Fix a bug related to nested panes resizing.
Fix problem preventing creating a new file.
## [1.16.8] - 2021-05-06
### 1.16.8
Fix application crash when using the command `core:restart`.
@ -578,28 +84,27 @@ Both kind of tags can appear in new plugins in the form:
where the old tag needs to appear at the end for compatibility.
## [1.16.7] - 2021-05-01
### 1.16.7
Add support for retina displays on Mac OS X.
Fix a few problems related to file paths.
## [1.16.6] - 2021-04-21
### 1.16.6
Implement a system to check the compatibility of plugins by checking a release
tag. Plugins that don't have the release tag will not be loaded.
Implement a system to check the compatibility of plugins by checking a release tag.
Plugins that don't have the release tag will not be loaded.
Improve and extend the NagView with keyboard commands.
Special thanks to @takase1121 for the implementation and @liquidev for proposing
and discussing the enhancements.
Special thanks to @takase1121 for the implementation and @liquidev for proposing and
discussing the enhancements.
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
resources and his support.
Add hook function `DocView.on_text_change` so that plugin can accurately react
on document changes. Thanks to @vincens2005 for the suggestion and testing the
implementation.
Add hook function `DocView.on_text_change` so that plugin can accurately react on document changes.
Thanks to @vincens2005 for the suggestion and testing the implementation.
Enable borderless window mode using the `config.borderless` variable.
If enable the system window's bar will be replaced by a title bar provided
@ -617,14 +122,13 @@ commands `draw-whitespace:toggle`, `draw-whitespace:enable`,
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] - 2021-03-20
### 1.16.5
Hotfix for Github's issue https://github.com/franko/lite-xl/issues/122
## [1.16.4] - 2021-03-20
### 1.16.4
Add tooltips to show full file names from the tree-view.
@ -639,7 +143,7 @@ Made borders between tabs look cleaner.
Fix problem with files using hard tabs.
## [1.16.2] - 2021-03-05
### 1.16.2
Implement close button for tabs.
@ -647,12 +151,12 @@ Make the command view list of suggestion scrollable to see all the items.
Improve update/resize behavior of treeview and toolbar.
## [1.16.1] - 2021-02-25
### 1.16.1
Improve behavior of commands to move, delete and duplicate multiple lines:
no longer include the last line if it does not contain any selection.
Fix graphical artifacts when rendering some fonts like FiraSans.
Fix graphical artefacts when rendering some fonts like FiraSans.
Introduce the `config.transitions` boolean variable.
When false the transitions will be disabled and changes will be done immediately.
@ -661,7 +165,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 problem with spacing in treeview when using monospace fonts.
## [1.16] - 2021-02-19
### 1.16
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
@ -673,8 +177,8 @@ are actually resizable.
Add config mechanism to disable a plugin by setting
`config.<plugin-name> = false`.
Improve the "detect indent" plugin to take into account the syntax and exclude
comments for much accurate results.
Improve the "detect indent" plugin to take into account the syntax and exclude comments
for much accurate results.
Add command `root:close-all` to close all the documents currently opened.
@ -682,24 +186,21 @@ Show the full path filename of the active document in the window's title.
Fix problem with user's module reload not always enabled.
## [1.15] - 2021-01-04
### 1.15
**Project directories**
Extend your project by adding more directories using the command
`core:add-directory`. To remove them use the corresponding command
`core:remove-directory`.
Extend your project by adding more directories using the command `core:add-directory`.
To remove them use the corresponding command `core:remove-directory`.
**Workspaces**
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 also remember the window size and position and the additional project
directories.
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`.
In addition to the functionalities of the original plugin the extended version will
also remember the window size and position and the additonal project directories.
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**
@ -711,11 +212,10 @@ As in the unix shell `~` is now used to identify the home directory.
**Files and Directories**
Add command to create a new empty directory within the project using the
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.
Add command to create a new empty directory within the project using the 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.
**New welcome screen**
@ -723,56 +223,51 @@ Show 'Lite XL' instead of 'lite' and the version number.
**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] - 2020-12-13
### 1.14
**Project Management**
Add a new command, Core: Change Project Folder, to change project directory by
staying on the same window. All the current opened documents will be closed.
Add a new command, Core: Change Project Folder, to change project directory by 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.
A similar command is also added, Core: Open Project Folder, with key binding
ctrl+shift+o. It will open the chosen folder in a new window.
A similar command is also added, Core: Open Project Folder, with key binding 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. When invoked without arguments it will now open the project
more recently used. If a directory is specified it will behave like before and
open the directory indicated as an argument.
In addition Lite XL will now remember the recently used projects across different sessions.
When invoked without arguments it will now open the project more recently used.
If a directory is specified it will behave like before and open the directory indicated as an argument.
**Restart command**
A Core: Restart command is added to restart the editor without leaving the
current window. Very convenient when modifying the Lua code for the editor
itself.
A Core: Restart command is added to restart the editor without leaving the current window.
Very convenient when modifying the Lua code for the editor itself.
**User's setting auto-reload**
When saving the user configuration, the user's module, the changes will be
automatically applied to the current instance.
When saving the user configuration, the user's module, the changes will be automatically applied to the
current instance.
**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**
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**
Correct font's rendering for full hinting mode when using subpixel antialiasing.
## [1.13] - 2020-12-06
### 1.13
**Rendering options for fonts**
When loading fonts with the function renderer.font.load some rendering options
can be optionally specified:
When loading fonts with the function renderer.font.load some rendering options can
be optionally specified:
- antialiasing: grayscale or subpixel
- hinting: none, slight or full
@ -781,39 +276,36 @@ See data/core/style.lua for the details about its utilisation.
The default remains antialiasing subpixel and hinting slight to reproduce the
behavior of previous versions.
The option grayscale with full hinting is specially interesting for crisp font
rendering without color artifacts.
The option grayscale with full hinting is specially interesting for crisp font rendering
without color artifacts.
**Unix-like install directories**
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
$prefix/share/lite-xl.
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.
If no such $prefix exist it will fall back to the old behavior and use the "data"
folder from the executable directory.
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`,
to point to the data directory.
In addtion to the `EXEDIR` global variable an additional variable is exposed,
`DATADIR`, to point to the data directory.
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.
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**
Now the Lite XL user's configuration will be stored in the user's home directory
under .config/lite-xl".
The home directory is determined using the "HOME" environment variable except
on Windows wher "USERPROFILE" is used instead.
Now the Lite XL user's configuration will be stored in the user's home directory under
".config/lite-xl".
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.
## [1.11] - 2020-07-05
### 1.11
- include changes from rxi's Lite 1.11
- fix behavior of tab to indent multiple lines
@ -821,36 +313,11 @@ 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
- list recently visited files when using "Find File" command
## [1.08] - 2020-06-14
### 1.08
- Subpixel font rendering, removed gamma correction
- Avoid using CPU when the editor is idle
## [1.06] - 2020-05-31
### 1.06
- 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

View File

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

0
data/colors/fall.lua Normal file → Executable file
View File

0
data/colors/summer.lua Normal file → Executable file
View File

View File

@ -1,51 +0,0 @@
local b05 = 'rgba(0,0,0,0.5)' local red = '#994D4D'
local b80 = '#333333' local orange = '#B3661A'
local b60 = '#808080' local green = '#52994D'
local b40 = '#ADADAD' local teal = '#4D9999'
local b20 = '#CECECE' local blue = '#1A66B3'
local b00 = '#E6E6E6' local magenta = '#994D99'
--------------------------=--------------------------
local style = require 'core.style'
local common = require 'core.common'
--------------------------=--------------------------
style.line_highlight = { common.color(b20) }
style.background = { common.color(b00) }
style.background2 = { common.color(b20) }
style.background3 = { common.color(b20) }
style.text = { common.color(b60) }
style.caret = { common.color(b80) }
style.accent = { common.color(b80) }
style.dim = { common.color(b60) }
style.divider = { common.color(b40) }
style.selection = { common.color(b40) }
style.line_number = { common.color(b60) }
style.line_number2 = { common.color(b80) }
style.scrollbar = { common.color(b40) }
style.scrollbar2 = { common.color(b60) }
style.nagbar = { common.color(red) }
style.nagbar_text = { common.color(b00) }
style.nagbar_dim = { common.color(b05) }
--------------------------=--------------------------
style.syntax = {}
style.syntax['normal'] = { common.color(b80) }
style.syntax['symbol'] = { common.color(b80) }
style.syntax['comment'] = { common.color(b60) }
style.syntax['keyword'] = { common.color(blue) }
style.syntax['keyword2'] = { common.color(red) }
style.syntax['number'] = { common.color(teal) }
style.syntax['literal'] = { common.color(blue) }
style.syntax['string'] = { common.color(green) }
style.syntax['operator'] = { common.color(magenta) }
style.syntax['function'] = { common.color(blue) }
--------------------------=--------------------------
style.syntax.paren1 = { common.color(magenta) }
style.syntax.paren2 = { common.color(orange) }
style.syntax.paren3 = { common.color(teal) }
style.syntax.paren4 = { common.color(blue) }
style.syntax.paren5 = { common.color(red) }
--------------------------=--------------------------
style.lint = {}
style.lint.info = { common.color(blue) }
style.lint.hint = { common.color(green) }
style.lint.warning = { common.color(red) }
style.lint.error = { common.color(orange) }

View File

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

65
data/core/command.lua Normal file → Executable file
View File

@ -6,48 +6,17 @@ command.map = {}
local always_true = function() return true end
---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)
function command.add(predicate, map)
predicate = predicate or always_true
local strict = false
if type(predicate) == "string" then
if predicate:match("!$") then
strict = true
predicate = predicate:gsub("!$", "")
end
predicate = require(predicate)
end
if type(predicate) == "table" then
local class = predicate
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
predicate = function() return core.active_view:is(class) end
end
return predicate
end
function command.add(predicate, map)
predicate = command.generate_predicate(predicate)
for name, fn in pairs(map) do
if command.map[name] then
core.log_quiet("Replacing existing command \"%s\"", name)
end
assert(not command.map[name], "command already exists: " .. name)
command.map[name] = { predicate = predicate, perform = fn }
end
end
@ -64,34 +33,19 @@ end
function command.get_all_valid()
local res = {}
local memoized_predicates = {}
for name, cmd in pairs(command.map) do
if memoized_predicates[cmd.predicate] == nil then
memoized_predicates[cmd.predicate] = cmd.predicate()
end
if memoized_predicates[cmd.predicate] then
if cmd.predicate() then
table.insert(res, name)
end
end
return res
end
function command.is_valid(name, ...)
return command.map[name] and command.map[name].predicate(...)
end
local function perform(name, ...)
local function perform(name)
local cmd = command.map[name]
if not cmd then return false end
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
if cmd and cmd.predicate() then
cmd.perform()
return true
end
return false
@ -105,10 +59,7 @@ end
function command.add_defaults()
local reg = {
"core", "root", "command", "doc", "findreplace",
"files", "drawwhitespace", "dialog", "log", "statusbar"
}
local reg = { "core", "root", "command", "doc", "findreplace", "files", "drawwhitespace" }
for _, name in ipairs(reg) do
require("core.commands." .. name)
end

28
data/core/commands/command.lua Normal file → Executable file
View File

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

View File

@ -10,18 +10,7 @@ local restore_title_view = false
local function suggest_directory(text)
text = common.home_expand(text)
local basedir = common.dirname(core.project_dir)
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
return common.home_encode_list(text == "" and core.recent_projects or common.dir_path_suggest(text))
end
command.add(nil, {
@ -48,47 +37,41 @@ command.add(nil, {
end,
["core:reload-module"] = function()
core.command_view:enter("Reload Module", {
submit = function(text, item)
local text = item and item.text or text
core.reload_module(text)
core.log("Reloaded module %q", text)
end,
suggest = function(text)
local items = {}
for name in pairs(package.loaded) do
table.insert(items, name)
end
return common.fuzzy_match(items, text)
core.command_view:enter("Reload Module", function(text, item)
local text = item and item.text or text
core.reload_module(text)
core.log("Reloaded module %q", text)
end, function(text)
local items = {}
for name in pairs(package.loaded) do
table.insert(items, name)
end
})
return common.fuzzy_match(items, text)
end)
end,
["core:find-command"] = function()
local commands = command.get_all_valid()
core.command_view:enter("Do Command", {
submit = function(text, item)
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
core.command_view:enter("Do Command", function(text, item)
if item then
command.perform(item.command)
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,
["core:find-file"] = function()
if not core.project_files_number() then
return command.perform "core:open-file"
if core.project_files_limit then
return command.perform "core:open-file"
end
local files = {}
for dir, item in core.get_project_files() do
@ -97,72 +80,47 @@ command.add(nil, {
table.insert(files, common.home_encode(path .. item.filename))
end
end
core.command_view:enter("Open File From Project", {
submit = function(text, item)
text = item and item.text or text
core.root_view:open_doc(core.open_doc(common.home_expand(text)))
end,
suggest = function(text)
return common.fuzzy_match_with_recents(files, core.visited_files, text)
end
})
core.command_view:enter("Open File From Project", function(text, item)
text = item and item.text or text
core.root_view:open_doc(core.open_doc(common.home_expand(text)))
end, function(text)
return common.fuzzy_match_with_recents(files, core.visited_files, text)
end)
end,
["core:new-doc"] = function()
core.root_view:open_doc(core.open_doc())
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()
local view = core.active_view
local text
if view.doc and view.doc.abs_filename then
local dirname, filename = view.doc.abs_filename:match("(.*)[/\\](.+)$")
if dirname then
dirname = core.normalize_to_project_dir(dirname)
text = dirname == core.project_dir and "" or common.home_encode(dirname) .. PATHSEP
local text = dirname == core.project_dir and "" or common.home_encode(dirname) .. PATHSEP
core.command_view:set_text(text)
end
end
core.command_view:enter("Open File", {
text = text,
submit = function(text)
local filename = system.absolute_path(common.home_expand(text))
core.root_view:open_doc(core.open_doc(filename))
end,
suggest = function (text)
return common.home_encode_list(common.path_suggest(common.home_expand(text)))
end,
validate = function(text)
local filename = common.home_expand(text)
local path_stat, err = system.get_file_info(filename)
if err 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
end
end,
})
core.command_view:enter("Open File", function(text)
local filename = system.absolute_path(common.home_expand(text))
core.root_view:open_doc(core.open_doc(filename))
end, function (text)
return common.home_encode_list(common.path_suggest(common.home_expand(text)))
end, nil, function(text)
local path_stat, err = system.get_file_info(common.home_expand(text))
if err then
core.error("Cannot open file %q: %q", text, err)
elseif path_stat.type == 'dir' then
core.error("Cannot open %q, is a folder", text)
else
return true
end
end)
end,
["core:open-log"] = function()
local node = core.root_view:get_active_node_default()
local node = core.root_view:get_active_node()
node:add_view(LogView())
end,
@ -173,79 +131,56 @@ command.add(nil, {
end,
["core:open-project-module"] = function()
if not system.get_file_info(".lite_project.lua") then
core.try(core.write_init_project_module, ".lite_project.lua")
local filename = ".lite_project.lua"
if system.get_file_info(filename) then
core.root_view:open_doc(core.open_doc(filename))
else
local doc = core.open_doc()
core.root_view:open_doc(doc)
doc:save(filename)
end
local doc = core.open_doc(".lite_project.lua")
core.root_view:open_doc(doc)
doc:save()
end,
["core:change-project-folder"] = function()
local dirname = common.dirname(core.project_dir)
local text
if dirname then
text = common.home_encode(dirname) .. PATHSEP
end
core.command_view:enter("Change Project Folder", {
text = text,
submit = function(text)
local path = common.home_expand(text)
local abs_path = check_directory_path(path)
if not abs_path then
core.error("Cannot open directory %q", path)
return
end
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
})
core.command_view:enter("Change Project Folder", function(text, item)
text = system.absolute_path(common.home_expand(item and item.text or text))
if text == core.project_dir then return end
local path_stat = system.get_file_info(text)
if not path_stat or path_stat.type ~= 'dir' then
core.error("Cannot open folder %q", text)
return
end
core.confirm_close_all(core.open_folder_project, text)
end, suggest_directory)
end,
["core:open-project-folder"] = function()
local dirname = common.dirname(core.project_dir)
local text
if dirname then
text = common.home_encode(dirname) .. PATHSEP
end
core.command_view:enter("Open Project", {
text = text,
submit = function(text)
local path = common.home_expand(text)
local abs_path = check_directory_path(path)
if not abs_path then
core.error("Cannot open directory %q", path)
return
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
})
core.command_view:enter("Open Project", function(text, item)
text = common.home_expand(item and item.text or text)
local path_stat = system.get_file_info(text)
if not path_stat or path_stat.type ~= 'dir' then
core.error("Cannot open folder %q", text)
return
end
system.exec(string.format("%q %q", EXEFILE, text))
end, suggest_directory)
end,
["core:add-directory"] = function()
core.command_view:enter("Add Directory", {
submit = function(text)
text = common.home_expand(text)
local path_stat, err = system.get_file_info(text)
if not path_stat then
core.error("cannot open %q: %s", text, err)
return
elseif path_stat.type ~= 'dir' then
core.error("%q is not a directory", text)
return
end
core.add_project_directory(system.absolute_path(text))
end,
suggest = suggest_directory
})
core.command_view:enter("Add Directory", function(text)
text = common.home_expand(text)
local path_stat, err = system.get_file_info(text)
if not path_stat then
core.error("cannot open %q: %s", text, err)
return
elseif path_stat.type ~= 'dir' then
core.error("%q is not a directory", text)
return
end
core.add_project_directory(system.absolute_path(text))
-- TODO: add the name of directory to prioritize
core.reschedule_project_scan()
end, suggest_directory)
end,
["core:remove-directory"] = function()
@ -254,17 +189,14 @@ command.add(nil, {
for i = n, 2, -1 do
dir_list[n - i + 1] = core.project_directories[i].name
end
core.command_view:enter("Remove Directory", {
submit = function(text, item)
text = common.home_expand(item and item.text or 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))
core.command_view:enter("Remove Directory", function(text, item)
text = common.home_expand(item and item.text or text)
if not core.remove_project_directory(text) then
core.error("No directory %q to be removed", text)
end
})
end, function(text)
text = common.home_expand(text)
return common.home_encode_list(common.dir_list_suggest(text, dir_list))
end)
end,
})

View File

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

823
data/core/commands/doc.lua Normal file → Executable file
View File

@ -16,16 +16,21 @@ local function doc()
end
local function doc_multiline_selections(sort)
local iter, state, idx, line1, col1, line2, col2 = doc():get_selections(sort)
return function()
idx, line1, col1, line2, col2 = iter(state, idx)
if idx and line2 > line1 and col2 == 1 then
line2 = line2 - 1
col2 = #doc().lines[line2]
end
return idx, line1, col1, line2, col2
local function get_indent_string()
if config.tab_type == "hard" then
return "\t"
end
return string.rep(" ", config.indent_size)
end
local function doc_multiline_selection(sort)
local line1, col1, line2, col2, swap = doc():get_selection(sort)
if line2 > line1 and col2 == 1 then
line2 = line2 - 1
col2 = #doc().lines[line2]
end
return line1, col1, line2, col2, swap
end
local function append_line_if_last_line(line)
@ -34,388 +39,260 @@ local function append_line_if_last_line(line)
end
end
local function save(filename)
local abs_filename
if filename then
filename = core.normalize_to_project_dir(filename)
abs_filename = core.project_absolute_path(filename)
end
doc():save(filename, abs_filename)
doc():save(filename and core.normalize_to_project_dir(filename))
local saved_filename = doc().filename
core.on_doc_save(saved_filename)
core.log("Saved \"%s\"", saved_filename)
end
local function cut_or_copy(delete)
local full_text = ""
local text = ""
core.cursor_clipboard = {}
core.cursor_clipboard_whole_line = {}
for idx, line1, col1, line2, col2 in doc():get_selections() do
if line1 ~= line2 or col1 ~= col2 then
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
doc():delete_to_cursor(idx, 0)
end
else -- Cut/copy whole line
text = doc().lines[line1]
full_text = full_text == "" and text or (full_text .. text)
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
core.cursor_clipboard[idx] = text
end
core.cursor_clipboard["full"] = full_text
system.set_clipboard(full_text)
end
local function split_cursor(direction)
local new_cursors = {}
for _, line1, col1 in doc():get_selections() do
if line1 + direction >= 1 and line1 + direction <= #doc().lines then
table.insert(new_cursors, { line1 + direction, col1 })
end
end
for i,v in ipairs(new_cursors) do doc():add_selection(v[1], v[2]) end
core.blink_reset()
end
local function set_cursor(dv, x, y, snap_type)
local line, col = dv:resolve_screen_position(x, y)
dv.doc:set_selection(line, col, line, col)
if snap_type == "word" or snap_type == "lines" then
command.perform("doc:select-" .. snap_type)
end
dv.mouse_selecting = { line, col, snap_type }
core.blink_reset()
end
local function line_comment(comment, line1, col1, line2, col2)
local start_comment = (type(comment) == 'table' and comment[1] or comment) .. " "
local end_comment = (type(comment) == 'table' and " " .. comment[2])
local uncomment = true
local start_offset = math.huge
for line = line1, line2 do
local text = doc().lines[line]
local s = text:find("%S")
if s then
local cs, ce = text:find(start_comment, s, true)
if cs ~= s then
uncomment = false
end
start_offset = math.min(start_offset, s)
end
end
local end_line = col2 == #doc().lines[line2]
for line = line1, line2 do
local text = doc().lines[line]
local s = text:find("%S")
if s and uncomment then
if end_comment and text:sub(#text - #end_comment, #text - 1) == end_comment then
doc():remove(line, #text - #end_comment, line, #text)
end
local cs, ce = text:find(start_comment, s, true)
if ce then
doc():remove(line, cs, line, ce + 1)
end
elseif s then
doc():insert(line, start_offset, start_comment)
if end_comment then
doc():insert(line, #doc().lines[line], " " .. comment[2])
end
end
end
col1 = col1 + (col1 > start_offset and #start_comment or 0) * (uncomment and -1 or 1)
col2 = col2 + (col2 > start_offset and #start_comment or 0) * (uncomment and -1 or 1)
if end_comment and end_line then
col2 = col2 + #end_comment * (uncomment and -1 or 1)
end
return line1, col1, line2, col2
end
local function block_comment(comment, line1, col1, line2, col2)
-- automatically skip spaces
local word_start = doc():get_text(line1, col1, line1, math.huge):find("%S")
local word_end = doc():get_text(line2, 1, line2, col2):find("%s*$")
col1 = col1 + (word_start and (word_start - 1) or 0)
col2 = word_end and word_end or col2
local block_start = doc():get_text(line1, col1, line1, col1 + #comment[1])
local block_end = doc():get_text(line2, col2 - #comment[2], line2, col2)
if block_start == comment[1] and block_end == comment[2] then
-- remove up to 1 whitespace after the comment
local start_len, stop_len = #comment[1], #comment[2]
if doc():get_text(line1, col1 + #comment[1], line1, col1 + #comment[1] + 1):find("%s$") then
start_len = start_len + 1
end
if doc():get_text(line2, col2 - #comment[2] - 1, line2, col2):find("^%s") then
stop_len = stop_len + 1
end
doc():remove(line1, col1, line1, col1 + start_len)
col2 = col2 - (line1 == line2 and start_len or 0)
doc():remove(line2, col2 - stop_len, line2, col2)
return line1, col1, line2, col2 - stop_len
-- returns the size of the original indent, and the indent
-- in your config format, rounded either up or down
local function get_line_indent(line, rnd_up)
local _, e = line:find("^[ \t]+")
local soft_tab = string.rep(" ", config.indent_size)
if config.tab_type == "hard" then
local indent = e and line:sub(1, e):gsub(soft_tab, "\t") or ""
return e, indent:gsub(" +", rnd_up and "\t" or "")
else
doc():insert(line1, col1, comment[1] .. " ")
col2 = col2 + (line1 == line2 and (#comment[1] + 1) or 0)
doc():insert(line2, col2, " " .. comment[2])
local indent = e and line:sub(1, e):gsub("\t", soft_tab) or ""
local number = #indent / #soft_tab
return e, indent:sub(1,
(rnd_up and math.ceil(number) or math.floor(number))*#soft_tab)
end
end
return line1, col1, line2, col2 + #comment[2] + 1
-- un/indents text; behaviour varies based on selection and un/indent.
-- * if there's a selection, it will stay static around the
-- text for both indenting and unindenting.
-- * if you are in the beginning whitespace of a line, and are indenting, the
-- cursor will insert the exactly appropriate amount of spaces, and jump the
-- cursor to the beginning of first non whitespace characters
-- * if you are not in the beginning whitespace of a line, and you indent, it
-- inserts the appropriate whitespace, as if you typed them normally.
-- * if you are unindenting, the cursor will jump to the start of the line,
-- and remove the appropriate amount of spaces (or a tab).
local function indent_text(unindent)
local text = get_indent_string()
local line1, col1, line2, col2, swap = doc_multiline_selection(true)
local _, se = doc().lines[line1]:find("^[ \t]+")
local in_beginning_whitespace = col1 == 1 or (se and col1 <= se + 1)
if unindent or doc():has_selection() or in_beginning_whitespace then
local l1d, l2d = #doc().lines[line1], #doc().lines[line2]
for line = line1, line2 do
local e, rnded = get_line_indent(doc().lines[line], unindent)
doc():remove(line, 1, line, (e or 0) + 1)
doc():insert(line, 1,
unindent and rnded:sub(1, #rnded - #text) or rnded .. text)
end
l1d, l2d = #doc().lines[line1] - l1d, #doc().lines[line2] - l2d
if (unindent or in_beginning_whitespace) and not doc():has_selection() then
local start_cursor = (se and se + 1 or 1) + l1d or #(doc().lines[line1])
doc():set_selection(line1, start_cursor, line2, start_cursor, swap)
else
doc():set_selection(line1, col1 + l1d, line2, col2 + l2d, swap)
end
else
doc():text_input(text)
end
end
local commands = {
["doc:select-none"] = function(dv)
local line, col = dv.doc:get_selection()
dv.doc:set_selection(line, col)
["doc:undo"] = function()
doc():undo()
end,
["doc:redo"] = function()
doc():redo()
end,
["doc:cut"] = function()
cut_or_copy(true)
if doc():has_selection() then
local text = doc():get_text(doc():get_selection())
system.set_clipboard(text)
doc():delete_to(0)
end
end,
["doc:copy"] = function()
cut_or_copy(false)
end,
["doc:undo"] = function(dv)
dv.doc:undo()
end,
["doc:redo"] = function(dv)
dv.doc:redo()
end,
["doc:paste"] = function(dv)
local clipboard = system.get_clipboard()
-- If the clipboard has changed since our last look, use that instead
local external_paste = core.cursor_clipboard["full"] ~= clipboard
if external_paste then
core.cursor_clipboard = {}
core.cursor_clipboard_whole_line = {}
if doc():has_selection() then
local text = doc():get_text(doc():get_selection())
system.set_clipboard(text)
end
local value, whole_line
for idx, line1, col1, line2, col2 in dv.doc:get_selections() do
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,
["doc:paste"] = function()
doc():text_input(system.get_clipboard():gsub("\r", ""))
end,
["doc:newline"] = function()
local line, col = doc():get_selection()
local indent = doc().lines[line]:match("^[\t ]*")
if col <= #indent then
indent = indent:sub(#indent + 2 - col)
end
doc():text_input("\n" .. indent)
end,
["doc:newline-below"] = function()
local line = doc():get_selection()
local indent = doc().lines[line]:match("^[\t ]*")
doc():insert(line, math.huge, "\n" .. indent)
doc():set_selection(line + 1, math.huge)
end,
["doc:newline-above"] = function()
local line = doc():get_selection()
local indent = doc().lines[line]:match("^[\t ]*")
doc():insert(line, 1, indent .. "\n")
doc():set_selection(line, math.huge)
end,
["doc:delete"] = function()
local line, col = doc():get_selection()
if not doc():has_selection() and doc().lines[line]:find("^%s*$", col) then
doc():remove(line, col, line, math.huge)
end
doc():delete_to(translate.next_char)
end,
["doc:backspace"] = function()
local line, col = doc():get_selection()
if not doc():has_selection() then
local text = doc():get_text(line, 1, line, col)
if #text >= config.indent_size and text:find("^ *$") then
doc():delete_to(0, -config.indent_size)
return
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
doc():delete_to(translate.previous_char)
end,
["doc:select-all"] = function()
doc():set_selection(1, 1, math.huge, math.huge)
end,
["doc:select-none"] = function()
local line, col = doc():get_selection()
doc():set_selection(line, col)
end,
["doc:select-lines"] = function()
local line1, _, line2, _, swap = doc():get_selection(true)
append_line_if_last_line(line2)
doc():set_selection(line1, 1, line2 + 1, 1, swap)
end,
["doc:select-word"] = function()
local line1, col1 = doc():get_selection(true)
local line1, col1 = translate.start_of_word(doc(), line1, col1)
local line2, col2 = translate.end_of_word(doc(), line1, col1)
doc():set_selection(line2, col2, line1, col1)
end,
["doc:join-lines"] = function()
local line1, _, line2 = doc():get_selection(true)
if line1 == line2 then line2 = line2 + 1 end
local text = doc():get_text(line1, 1, line2, math.huge)
text = text:gsub("(.-)\n[\t ]*", function(x)
return x:find("^%s*$") and x or x .. " "
end)
doc():insert(line1, 1, text)
doc():remove(line1, #text + 1, line2, math.huge)
if doc():has_selection() then
doc():set_selection(line1, math.huge)
end
end,
["doc:indent"] = function()
indent_text()
end,
["doc:unindent"] = function()
indent_text(true)
end,
["doc:duplicate-lines"] = function()
local line1, col1, line2, col2, swap = doc_multiline_selection(true)
append_line_if_last_line(line2)
local text = doc():get_text(line1, 1, line2 + 1, 1)
doc():insert(line2 + 1, 1, text)
local n = line2 - line1 + 1
doc():set_selection(line1 + n, col1, line2 + n, col2, swap)
end,
["doc:delete-lines"] = function()
local line1, col1, line2 = doc_multiline_selection(true)
append_line_if_last_line(line2)
doc():remove(line1, 1, line2 + 1, 1)
doc():set_selection(line1, col1)
end,
["doc:move-lines-up"] = function()
local line1, col1, line2, col2, swap = doc_multiline_selection(true)
append_line_if_last_line(line2)
if line1 > 1 then
local text = doc().lines[line1 - 1]
doc():insert(line2 + 1, 1, text)
doc():remove(line1 - 1, 1, line1, 1)
doc():set_selection(line1 - 1, col1, line2 - 1, col2, swap)
end
end,
["doc:move-lines-down"] = function()
local line1, col1, line2, col2, swap = doc_multiline_selection(true)
append_line_if_last_line(line2 + 1)
if line2 < #doc().lines then
local text = doc().lines[line2 + 1]
doc():remove(line2 + 1, 1, line2 + 2, 1)
doc():insert(line1, 1, text)
doc():set_selection(line1 + 1, col1, line2 + 1, col2, swap)
end
end,
["doc:toggle-line-comments"] = function()
local comment = doc().syntax.comment
if not comment then return end
local indentation = get_indent_string()
local comment_text = comment .. " "
local line1, _, line2 = doc_multiline_selection(true)
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
for line = line1, line2 do
local text = doc().lines[line]
local s = text:find("%S")
if uncomment then
local cs, ce = text:find(comment_text, s, true)
if ce then
doc():remove(line, cs, line, ce + 1)
end
else
dv.doc:text_input(value:gsub("\r", ""), idx)
elseif s then
doc():insert(line, start_offset, comment_text)
end
end
end,
["doc:newline"] = function(dv)
for idx, line, col in dv.doc:get_selections(false, true) do
local indent = dv.doc.lines[line]:match("^[\t ]*")
if col <= #indent then
indent = indent:sub(#indent + 2 - col)
end
-- Remove current line if it contains only whitespace
if dv.doc.lines[line]:match("^%s+$") then
dv.doc:remove(line, 1, line, math.huge)
end
dv.doc:text_input("\n" .. indent, idx)
end
["doc:upper-case"] = function()
doc():replace(string.upper)
end,
["doc:newline-below"] = 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, math.huge, "\n" .. indent)
dv.doc:set_selections(idx, line + 1, math.huge)
end
["doc:lower-case"] = function()
doc():replace(string.lower)
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:go-to-line"] = function()
local dv = dv()
["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
local text = dv.doc:get_text(line1, 1, line1, col1)
if #text >= indent_size and text:find("^ *$") then
dv.doc:delete_to_cursor(idx, 0, -indent_size)
return
end
end
dv.doc:delete_to_cursor(idx, translate.previous_char)
end
end,
["doc:select-all"] = function(dv)
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,
["doc:select-lines"] = function(dv)
for idx, line1, _, line2 in dv.doc:get_selections(true) do
append_line_if_last_line(line2)
dv.doc:set_selections(idx, line1, 1, line2 + 1, 1)
end
end,
["doc:select-word"] = function(dv)
for idx, line1, col1 in dv.doc:get_selections(true) do
local line1, col1 = translate.start_of_word(dv.doc, line1, col1)
local line2, col2 = translate.end_of_word(dv.doc, line1, col1)
dv.doc:set_selections(idx, line2, col2, line1, col1)
end
end,
["doc:join-lines"] = function(dv)
for idx, line1, col1, line2, col2 in dv.doc:get_selections(true) do
if line1 == line2 then line2 = line2 + 1 end
local text = dv.doc:get_text(line1, 1, line2, math.huge)
text = text:gsub("(.-)\n[\t ]*", function(x)
return x:find("^%s*$") and x or x .. " "
end)
dv.doc:insert(line1, 1, text)
dv.doc:remove(line1, #text + 1, line2, math.huge)
if line1 ~= line2 or col1 ~= col2 then
dv.doc:set_selections(idx, line1, math.huge)
end
end
end,
["doc:indent"] = function(dv)
for idx, line1, col1, line2, col2 in doc_multiline_selections(true) do
local l1, c1, l2, c2 = dv.doc:indent_text(false, line1, col1, line2, col2)
if l1 then
dv.doc:set_selections(idx, l1, c1, l2, c2)
end
end
end,
["doc:unindent"] = function(dv)
for idx, line1, col1, line2, col2 in doc_multiline_selections(true) do
local l1, c1, l2, c2 = dv.doc:indent_text(true, line1, col1, line2, col2)
if l1 then
dv.doc:set_selections(idx, l1, c1, l2, c2)
end
end
end,
["doc:duplicate-lines"] = function(dv)
for idx, line1, col1, line2, col2 in doc_multiline_selections(true) do
append_line_if_last_line(line2)
local text = doc():get_text(line1, 1, line2 + 1, 1)
dv.doc:insert(line2 + 1, 1, text)
local n = line2 - line1 + 1
dv.doc:set_selections(idx, line1 + n, col1, line2 + n, col2)
end
end,
["doc:delete-lines"] = function(dv)
for idx, line1, col1, line2, col2 in doc_multiline_selections(true) do
append_line_if_last_line(line2)
dv.doc:remove(line1, 1, line2 + 1, 1)
dv.doc:set_selections(idx, line1, col1)
end
end,
["doc:move-lines-up"] = function(dv)
for idx, line1, col1, line2, col2 in doc_multiline_selections(true) do
append_line_if_last_line(line2)
if line1 > 1 then
local text = doc().lines[line1 - 1]
dv.doc:insert(line2 + 1, 1, text)
dv.doc:remove(line1 - 1, 1, line1, 1)
dv.doc:set_selections(idx, line1 - 1, col1, line2 - 1, col2)
end
end
end,
["doc:move-lines-down"] = function(dv)
for idx, line1, col1, line2, col2 in doc_multiline_selections(true) do
append_line_if_last_line(line2 + 1)
if line2 < #dv.doc.lines then
local text = dv.doc.lines[line2 + 1]
dv.doc:remove(line2 + 1, 1, line2 + 2, 1)
dv.doc:insert(line1, 1, text)
dv.doc:set_selections(idx, line1 + 1, col1, line2 + 1, col2)
end
end
end,
["doc:toggle-block-comments"] = function(dv)
local comment = dv.doc.syntax.block_comment
if not comment then
if dv.doc.syntax.comment then
command.perform "doc:toggle-line-comments"
end
return
end
for idx, line1, col1, line2, col2 in doc_multiline_selections(true) do
-- if nothing is selected, toggle the whole line
if line1 == line2 and col1 == col2 then
col1 = 1
col2 = #dv.doc.lines[line2]
end
dv.doc:set_selections(idx, block_comment(comment, line1, col1, line2, col2))
end
end,
["doc:toggle-line-comments"] = function(dv)
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,
["doc:upper-case"] = function(dv)
dv.doc:replace(string.uupper)
end,
["doc:lower-case"] = function(dv)
dv.doc:replace(string.ulower)
end,
["doc:go-to-line"] = function(dv)
local items
local function init_items()
if items then return end
@ -427,194 +304,108 @@ local commands = {
end
end
core.command_view:enter("Go To Line", {
submit = function(text, item)
local line = item and item.line or tonumber(text)
if not line then
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
core.command_view:enter("Go To Line", function(text, item)
local line = item and item.line or tonumber(text)
if not line then
core.error("Invalid line number or unmatched string")
return
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,
["doc:toggle-line-ending"] = function(dv)
dv.doc.crlf = not dv.doc.crlf
["doc:toggle-line-ending"] = function()
doc().crlf = not doc().crlf
end,
["doc:save-as"] = function(dv)
["doc:save-as"] = function()
local last_doc = core.last_active_view and core.last_active_view.doc
local text
if dv.doc.filename then
text = dv.doc.filename
if doc().filename then
core.command_view:set_text(doc().filename)
elseif last_doc and last_doc.filename then
local dirname, filename = core.last_active_view.doc.abs_filename:match("(.*)[/\\](.+)$")
text = core.normalize_to_project_dir(dirname) .. PATHSEP
core.command_view:set_text(core.normalize_to_project_dir(dirname) .. PATHSEP)
end
core.command_view:enter("Save As", {
text = text,
submit = function(filename)
save(common.home_expand(filename))
end,
suggest = function (text)
return common.home_encode_list(common.path_suggest(common.home_expand(text)))
end
})
core.command_view:enter("Save As", function(filename)
save(common.home_expand(filename))
end, function (text)
return common.home_encode_list(common.path_suggest(common.home_expand(text)))
end)
end,
["doc:save"] = function(dv)
if dv.doc.filename then
["doc:save"] = function()
if doc().filename then
save()
else
command.perform("doc:save-as")
end
end,
["doc:reload"] = function(dv)
dv.doc:reload()
end,
["file:rename"] = function(dv)
local old_filename = dv.doc.filename
["doc:rename"] = function()
local old_filename = doc().filename
if not old_filename then
core.error("Cannot rename unsaved doc")
return
end
core.command_view:enter("Rename", {
text = old_filename,
submit = function(filename)
save(common.home_expand(filename))
core.log("Renamed \"%s\" to \"%s\"", old_filename, filename)
if filename ~= old_filename then
os.remove(old_filename)
end
end,
suggest = function (text)
return common.home_encode_list(common.path_suggest(common.home_expand(text)))
core.command_view:set_text(old_filename)
core.command_view:enter("Rename", function(filename)
doc():save(filename)
core.log("Renamed \"%s\" to \"%s\"", old_filename, filename)
if filename ~= old_filename then
os.remove(old_filename)
end
})
end, common.path_suggest)
end,
["file:delete"] = function(dv)
local filename = dv.doc.abs_filename
if not filename then
core.error("Cannot remove unsaved doc")
return
end
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)
node:close_view(core.root_view.root_node, docview)
end
os.remove(filename)
core.log("Removed \"%s\"", filename)
end,
["doc:select-to-cursor"] = function(dv, x, y, clicks)
local line1, col1 = select(3, doc():get_selection())
local line2, col2 = dv:resolve_screen_position(x, y)
dv.mouse_selecting = { line1, col1, nil }
dv.doc:set_selection(line2, col2, line1, col1)
end,
["doc:create-cursor-previous-line"] = function(dv)
split_cursor(-1)
dv.doc:merge_cursors()
end,
["doc:create-cursor-next-line"] = function(dv)
split_cursor(1)
dv.doc:merge_cursors()
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 = {
["previous-char"] = translate,
["next-char"] = translate,
["previous-word-start"] = translate,
["next-word-end"] = translate,
["previous-block-start"] = translate,
["next-block-end"] = translate,
["start-of-doc"] = translate,
["end-of-doc"] = translate,
["start-of-line"] = translate,
["end-of-line"] = translate,
["start-of-word"] = translate,
["start-of-indentation"] = translate,
["end-of-word"] = translate,
["previous-line"] = DocView.translate,
["next-line"] = DocView.translate,
["previous-page"] = DocView.translate,
["next-page"] = DocView.translate,
["previous-char"] = translate.previous_char,
["next-char"] = translate.next_char,
["previous-word-start"] = translate.previous_word_start,
["next-word-end"] = translate.next_word_end,
["previous-block-start"] = translate.previous_block_start,
["next-block-end"] = translate.next_block_end,
["start-of-doc"] = translate.start_of_doc,
["end-of-doc"] = translate.end_of_doc,
["start-of-line"] = translate.start_of_line,
["end-of-line"] = translate.end_of_line,
["start-of-word"] = translate.start_of_word,
["end-of-word"] = translate.end_of_word,
["previous-line"] = DocView.translate.previous_line,
["next-line"] = DocView.translate.next_line,
["previous-page"] = DocView.translate.previous_page,
["next-page"] = DocView.translate.next_page,
}
for name, obj in pairs(translations) do
commands["doc:move-to-" .. name] = function(dv) dv.doc:move_to(obj[name:gsub("-", "_")], dv) end
commands["doc:select-to-" .. name] = function(dv) dv.doc:select_to(obj[name:gsub("-", "_")], dv) end
commands["doc:delete-to-" .. name] = function(dv) dv.doc:delete_to(obj[name:gsub("-", "_")], dv) end
for name, fn in pairs(translations) do
commands["doc:move-to-" .. name] = function() doc():move_to(fn, dv()) end
commands["doc:select-to-" .. name] = function() doc():select_to(fn, dv()) end
commands["doc:delete-to-" .. name] = function() doc():delete_to(fn, dv()) end
end
commands["doc:move-to-previous-char"] = function(dv)
for idx, line1, col1, line2, col2 in dv.doc:get_selections(true) do
if line1 ~= line2 or col1 ~= col2 then
dv.doc:set_selections(idx, line1, col1)
else
dv.doc:move_to_cursor(idx, translate.previous_char)
end
commands["doc:move-to-previous-char"] = function()
if doc():has_selection() then
local line, col = doc():get_selection(true)
doc():set_selection(line, col)
else
doc():move_to(translate.previous_char)
end
end
commands["doc:move-to-next-char"] = function(dv)
for idx, line1, col1, line2, col2 in dv.doc:get_selections(true) do
if line1 ~= line2 or col1 ~= col2 then
dv.doc:set_selections(idx, line2, col2)
else
dv.doc:move_to_cursor(idx, translate.next_char)
end
commands["doc:move-to-next-char"] = function()
if doc():has_selection() then
local _, _, line, col = doc():get_selection(true)
doc():set_selection(line, col)
else
doc():move_to(translate.next_char)
end
end

0
data/core/commands/drawwhitespace.lua Normal file → Executable file
View File

13
data/core/commands/files.lua Normal file → Executable file
View File

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

334
data/core/commands/findreplace.lua Normal file → Executable file
View File

@ -2,221 +2,151 @@ local core = require "core"
local command = require "core.command"
local config = require "core.config"
local search = require "core.doc.search"
local keymap = require "core.keymap"
local DocView = require "core.docview"
local CommandView = require "core.commandview"
local StatusView = require "core.statusview"
local last_view, last_fn, last_text, last_sel
local max_previous_finds = 50
local case_sensitive = config.find_case_sensitive or false
local find_regex = config.find_regex or false
local found_expression
local function doc()
local is_DocView = core.active_view:is(DocView) and not core.active_view:is(CommandView)
return is_DocView and core.active_view.doc or (last_view and last_view.doc)
return core.active_view.doc
end
local function get_find_tooltip()
local rf = keymap.get_binding("find-replace:repeat-find")
local ti = keymap.get_binding("find-replace:toggle-sensitivity")
local tr = keymap.get_binding("find-replace:toggle-regex")
return (find_regex and "[Regex] " or "") ..
(case_sensitive and "[Sensitive] " or "") ..
(rf and ("Press " .. rf .. " to select the next match.") or "") ..
(ti and (" " .. ti .. " toggles case sensitivity.") or "") ..
(tr and (" " .. tr .. " toggles regex find.") or "")
end
local function update_preview(sel, search_fn, text)
local ok, line1, col1, line2, col2 = pcall(search_fn, last_view.doc,
sel[1], sel[2], text, case_sensitive, find_regex)
if ok and line1 and text ~= "" then
last_view.doc:set_selection(line2, col2, line1, col1)
last_view:scroll_to_line(line2, true)
found_expression = true
else
last_view.doc:set_selection(table.unpack(sel))
found_expression = false
local previous_finds
local last_doc
local last_fn, last_text
local function push_previous_find(doc, sel)
if last_doc ~= doc then
last_doc = doc
previous_finds = {}
end
end
local function insert_unique(t, v)
local n = #t
for i = 1, n do
if t[i] == v then
table.remove(t, i)
break
end
if #previous_finds >= max_previous_finds then
table.remove(previous_finds, 1)
end
table.insert(t, 1, v)
table.insert(previous_finds, sel or { doc:get_selection() })
end
local function find(label, search_fn)
last_view, last_sel = core.active_view,
{ core.active_view.doc:get_selection() }
local text = last_view.doc:get_text(table.unpack(last_sel))
found_expression = false
local dv = core.active_view
local sel = { dv.doc:get_selection() }
local text = dv.doc:get_text(table.unpack(sel))
local found = false
core.status_view:show_tooltip(get_find_tooltip())
core.command_view:set_text(text, true)
core.command_view:enter(label, {
text = text,
select_text = true,
show_suggestions = false,
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)
core.command_view:enter(label, function(text)
if found then
last_fn, last_text = search_fn, text
return core.previous_find
end,
cancel = 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
previous_finds = {}
push_previous_find(dv.doc, sel)
else
core.error("Couldn't find %q", text)
dv.doc:set_selection(table.unpack(sel))
dv:scroll_to_make_visible(sel[1], sel[2])
end
})
end, function(text)
local ok, line1, col1, line2, col2 = pcall(search_fn, dv.doc, sel[1], sel[2], text)
if ok and line1 and text ~= "" then
dv.doc:set_selection(line2, col2, line1, col1)
dv:scroll_to_line(line2, true)
found = true
else
dv.doc:set_selection(table.unpack(sel))
found = false
end
end, function(explicit)
if explicit then
dv.doc:set_selection(table.unpack(sel))
dv:scroll_to_make_visible(sel[1], sel[2])
end
end)
end
local function replace(kind, default, fn)
core.status_view:show_tooltip(get_find_tooltip())
core.command_view:enter("Find To Replace " .. kind, {
text = default,
select_text = true,
show_suggestions = false,
submit = function(old)
insert_unique(core.previous_find, old)
core.command_view:set_text(default, true)
local s = string.format("Replace %s %q With", kind, old)
core.command_view:enter(s, {
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()
end
})
core.command_view:enter("Find To Replace " .. kind, function(old)
core.command_view:set_text(old, true)
local s = string.format("Replace %s %q With", kind, old)
core.command_view:enter(s, function(new)
local n = doc():replace(function(text)
return fn(text, old, new)
end)
core.log("Replaced %d instance(s) of %s %q with %q", n, kind, old, new)
end)
end)
end
local function has_selection()
return core.active_view:is(DocView) and core.active_view.doc:has_selection()
return core.active_view:is(DocView)
and core.active_view.doc:has_selection()
end
local function has_unique_selection()
if not doc() then return false end
local text = nil
for idx, line1, col1, line2, col2 in doc():get_selections(true, true) do
if line1 == line2 and col1 == col2 then return false end
local selection = doc():get_text(line1, col1, line2, col2)
if text ~= nil and text ~= selection then return false end
text = selection
end
return text ~= nil
end
local function is_in_selection(line, col, l1, c1, l2, c2)
if line < l1 or line > l2 then return false end
if line == l1 and col <= c1 then return false end
if line == l2 and col > c2 then return false end
return true
end
local function is_in_any_selection(line, col)
for idx, l1, c1, l2, c2 in doc():get_selections(true, false) do
if is_in_selection(line, col, l1, c1, l2, c2) then return true end
end
return false
end
local function select_add_next(all)
local il1, ic1 = doc():get_selection(true)
for idx, l1, c1, l2, c2 in doc():get_selections(true, true) do
command.add(has_selection, {
["find-replace:select-next"] = function()
local l1, c1, l2, c2 = doc():get_selection(true)
local text = doc():get_text(l1, c1, l2, c2)
repeat
l1, c1, l2, c2 = search.find(doc(), l2, c2, text, { wrap = true })
if l1 == il1 and c1 == ic1 then break end
if l2 and (all or not is_in_any_selection(l2, c2)) then
doc():add_selection(l2, c2, l1, c1)
if not all then
core.active_view:scroll_to_make_visible(l2, c2)
return
end
end
until not all or not l2
if all then break end
local l1, c1, l2, c2 = search.find(doc(), l2, c2, text, { wrap = true })
if l2 then doc():set_selection(l2, c2, l1, c1) end
end
end
local function select_next(reverse)
local l1, c1, l2, c2 = doc():get_selection(true)
local text = doc():get_text(l1, c1, l2, c2)
if reverse then
l1, c1, l2, c2 = search.find(doc(), l1, c1, text, { wrap = true, reverse = true })
else
l1, c1, l2, c2 = search.find(doc(), l2, c2, text, { wrap = true })
end
if l2 then doc():set_selection(l2, c2, l1, c1) end
end
command.add(has_unique_selection, {
["find-replace:select-next"] = select_next,
["find-replace:select-previous"] = function() select_next(true) end,
["find-replace:select-add-next"] = select_add_next,
["find-replace:select-add-all"] = function() select_add_next(true) end
})
command.add("core.docview!", {
command.add("core.docview", {
["find-replace:find"] = function()
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 }
find("Find Text", function(doc, line, col, text)
local opt = { wrap = true, no_case = true }
return search.find(doc, line, col, text, opt)
end)
end,
["find-replace:replace"] = function()
local l1, c1, l2, c2 = doc():get_selection()
local selected_text = doc():get_text(l1, c1, l2, c2)
replace("Text", l1 == l2 and selected_text or "", function(text, old, new)
if not find_regex then
return text:gsub(old:gsub("%W", "%%%1"), new:gsub("%%", "%%%%"), nil)
["find-replace:find-pattern"] = function()
find("Find Text Pattern", function(doc, line, col, text)
local opt = { wrap = true, no_case = true, pattern = true }
return search.find(doc, line, col, text, opt)
end)
end,
["find-replace:repeat-find"] = function()
if not last_fn then
core.error("No find to continue from")
else
local line, col = doc():get_selection()
local line1, col1, line2, col2 = last_fn(doc(), line, col, last_text)
if line1 then
push_previous_find(doc())
doc():set_selection(line2, col2, line1, col1)
core.active_view:scroll_to_line(line2, true)
end
local result, matches = regex.gsub(regex.compile(old, "m"), text, new)
return result, #matches
end
end,
["find-replace:previous-find"] = function()
local sel = table.remove(previous_finds)
if not sel or doc() ~= last_doc then
core.error("No previous finds")
return
end
doc():set_selection(table.unpack(sel))
core.active_view:scroll_to_line(sel[3], true)
end,
["find-replace:replace"] = function()
replace("Text", "", function(text, old, new)
return text:gsub(old:gsub("%W", "%%%1"), new:gsub("%%", "%%%%"), nil)
end)
end,
["find-replace:replace-pattern"] = function()
replace("Pattern", "", function(text, old, new)
return text:gsub(old, new)
end)
end,
@ -238,53 +168,3 @@ command.add("core.docview!", {
end)
end,
})
local function valid_for_finding()
return core.active_view:is(DocView) or core.active_view:is(CommandView)
end
command.add(valid_for_finding, {
["find-replace:repeat-find"] = function()
if not last_fn then
core.error("No find to continue from")
else
local sl1, sc1, sl2, sc2 = doc():get_selection(true)
local line1, col1, line2, col2 = last_fn(doc(), sl1, sc2, last_text, case_sensitive, find_regex, false)
if line1 then
doc():set_selection(line2, col2, line1, col1)
last_view:scroll_to_line(line2, true)
else
core.error("Couldn't find %q", last_text)
end
end
end,
["find-replace:previous-find"] = function()
if not last_fn then
core.error("No find to continue from")
else
local sl1, sc1, sl2, sc2 = doc():get_selection(true)
local line1, col1, line2, col2 = last_fn(doc(), sl1, sc1, last_text, case_sensitive, find_regex, true)
if line1 then
doc():set_selection(line2, col2, line1, col1)
last_view:scroll_to_line(line2, true)
else
core.error("Couldn't find %q", last_text)
end
end
end,
})
command.add("core.commandview", {
["find-replace:toggle-sensitivity"] = function()
case_sensitive = not case_sensitive
core.status_view:show_tooltip(get_find_tooltip())
if last_sel then update_preview(last_sel, last_fn, last_text) end
end,
["find-replace:toggle-regex"] = function()
find_regex = not find_regex
core.status_view:show_tooltip(get_find_tooltip())
if last_sel then update_preview(last_sel, last_fn, last_text) end
end
})

View File

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

66
data/core/commands/root.lua Normal file → Executable file
View File

@ -3,47 +3,36 @@ local style = require "core.style"
local DocView = require "core.docview"
local command = require "core.command"
local common = require "core.common"
local config = require "core.config"
local t = {
["root:close"] = function(node)
["root:close"] = function()
local node = core.root_view:get_active_node()
node:close_active_view(core.root_view.root_node)
end,
["root:close-or-quit"] = function(node)
if node and (not node:is_empty() or not node.is_primary_node) then
node:close_active_view(core.root_view.root_node)
else
core.quit()
end
end,
["root:close-all"] = function()
core.confirm_close_docs(core.docs, core.root_view.close_all_docviews, core.root_view)
core.confirm_close_all(core.root_view.close_all_docviews, core.root_view)
end,
["root:close-all-others"] = function()
local active_doc, docs = core.active_view and core.active_view.doc, {}
for i, v in ipairs(core.docs) do if v ~= active_doc then table.insert(docs, v) end end
core.confirm_close_docs(docs, core.root_view.close_all_docviews, core.root_view, true)
end,
["root:switch-to-previous-tab"] = function(node)
["root:switch-to-previous-tab"] = function()
local node = core.root_view:get_active_node()
local idx = node:get_view_idx(core.active_view)
idx = idx - 1
if idx < 1 then idx = #node.views end
node:set_active_view(node.views[idx])
end,
["root:switch-to-next-tab"] = function(node)
["root:switch-to-next-tab"] = function()
local node = core.root_view:get_active_node()
local idx = node:get_view_idx(core.active_view)
idx = idx + 1
if idx > #node.views then idx = 1 end
node:set_active_view(node.views[idx])
end,
["root:move-tab-left"] = function(node)
["root:move-tab-left"] = function()
local node = core.root_view:get_active_node()
local idx = node:get_view_idx(core.active_view)
if idx > 1 then
table.remove(node.views, idx)
@ -51,7 +40,8 @@ local t = {
end
end,
["root:move-tab-right"] = function(node)
["root:move-tab-right"] = function()
local node = core.root_view:get_active_node()
local idx = node:get_view_idx(core.active_view)
if idx < #node.views then
table.remove(node.views, idx)
@ -59,22 +49,25 @@ local t = {
end
end,
["root:shrink"] = function(node)
["root:shrink"] = function()
local node = core.root_view:get_active_node()
local parent = node:get_parent_node(core.root_view.root_node)
local n = (parent.a == node) and -0.1 or 0.1
parent.divider = common.clamp(parent.divider + n, 0.1, 0.9)
end,
["root:grow"] = function(node)
["root:grow"] = function()
local node = core.root_view:get_active_node()
local parent = node:get_parent_node(core.root_view.root_node)
local n = (parent.a == node) and 0.1 or -0.1
parent.divider = common.clamp(parent.divider + n, 0.1, 0.9)
end
end,
}
for i = 1, 9 do
t["root:switch-to-tab-" .. i] = function(node)
t["root:switch-to-tab-" .. i] = function()
local node = core.root_view:get_active_node()
local view = node.views[i]
if view then
node:set_active_view(view)
@ -84,7 +77,8 @@ end
for _, dir in ipairs { "left", "right", "up", "down" } do
t["root:split-" .. dir] = function(node)
t["root:split-" .. dir] = function()
local node = core.root_view:get_active_node()
local av = node.active_view
node:split(dir)
if av:is(DocView) then
@ -92,7 +86,8 @@ for _, dir in ipairs { "left", "right", "up", "down" } do
end
end
t["root:switch-to-" .. dir] = function(node)
t["root:switch-to-" .. dir] = function()
local node = core.root_view:get_active_node()
local x, y
if dir == "left" or dir == "right" then
y = node.position.y + node.size.y / 2
@ -102,8 +97,7 @@ for _, dir in ipairs { "left", "right", "up", "down" } do
y = node.position.y + (dir == "up" and -1 or node.size.y + style.divider_size)
end
local node = core.root_view.root_node:get_child_overlapping_point(x, y)
local sx, sy = node:get_locked_size()
if not sx and not sy then
if not node:get_locked_size() then
core.set_active_view(node.active_view)
end
end
@ -111,17 +105,5 @@ end
command.add(function()
local node = core.root_view:get_active_node()
local sx, sy = node:get_locked_size()
return not sx and not sy, node
return not node:get_locked_size()
end, t)
command.add(nil, {
["root:scroll"] = function(delta)
local view = (core.root_view.overlapping_node and core.root_view.overlapping_node.active_view) or core.active_view
if view and view.scrollable then
view.scroll.to.y = view.scroll.to.y + delta * -config.mouse_wheel_scroll
return true
end
return false
end
})

View File

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

167
data/core/commandview.lua Normal file → Executable file
View File

@ -6,44 +6,24 @@ local DocView = require "core.docview"
local View = require "core.view"
---@class core.commandview.input : core.doc
---@field super core.doc
local SingleLineDoc = Doc:extend()
function SingleLineDoc:insert(line, col, text)
SingleLineDoc.super.insert(self, line, col, text:gsub("\n", ""))
end
---@class core.commandview : core.docview
---@field super core.docview
local CommandView = DocView:extend()
CommandView.context = "application"
local CommandView = DocView:extend()
local max_suggestions = 10
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 = {
submit = noop,
suggest = noop,
cancel = noop,
validate = function() return true end,
text = "",
select_text = false,
show_suggestions = true,
typeahead = true,
wrap = true,
validate = function() return true end
}
@ -53,7 +33,6 @@ function CommandView:new()
self.suggestions = {}
self.suggestions_height = 0
self.last_change_id = 0
self.last_text = ""
self.gutter_width = 0
self.gutter_text_brightness = 0
self.selection_offset = 0
@ -64,20 +43,13 @@ function CommandView:new()
end
---@deprecated
function CommandView:set_hidden_suggestions()
core.warn("Using deprecated function CommandView:set_hidden_suggestions")
self.state.show_suggestions = false
end
function CommandView:get_name()
return View.get_name(self)
end
function CommandView:get_line_screen_position(line, col)
local x = CommandView.super.get_line_screen_position(self, 1, col)
function CommandView:get_line_screen_position()
local x = CommandView.super.get_line_screen_position(self, 1)
local _, y = self:get_content_offset()
local lh = self:get_line_height()
return x, y + (self.size.y - lh) / 2
@ -100,7 +72,6 @@ end
function CommandView:set_text(text, select)
self.last_text = text
self.doc:remove(1, 1, math.huge, math.huge)
self.doc:text_input(text)
if select then
@ -110,38 +81,10 @@ end
function CommandView:move_suggestion_idx(dir)
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
self.suggestion_idx = overflow_suggestion_idx(n, #self.suggestions)
self:complete()
self.last_change_id = self.doc:get_change_id()
else
local current_suggestion = #self.suggestions > 0 and self.suggestions[self.suggestion_idx].text
local text = self:get_text()
if text == current_suggestion then
local n = self.suggestion_idx + dir
if n == 0 and self.save_suggestion then
self:set_text(self.save_suggestion)
else
self.suggestion_idx = overflow_suggestion_idx(n, #self.suggestions)
self:complete()
end
else
self.save_suggestion = text
self:complete()
end
self.last_change_id = self.doc:get_change_id()
self.state.suggest(self:get_text())
end
local n = self.suggestion_idx + dir
self.suggestion_idx = common.clamp(n, 1, #self.suggestions)
self:complete()
self.last_change_id = self.doc:get_change_id()
end
@ -162,53 +105,21 @@ function CommandView:submit()
end
end
---@param label string
---@varargs any
---@overload fun(label:string, options: core.commandview.state)
function CommandView:enter(label, ...)
function CommandView:enter(text, submit, suggest, cancel, validate)
if self.state ~= default_state then
return
end
local options = select(1, ...)
if type(options) ~= "table" then
core.warn("Using CommandView:enter in a deprecated way")
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
self.state = {
submit = submit or noop,
suggest = suggest or noop,
cancel = cancel or noop,
validate = validate or function() return true end
}
core.set_active_view(self)
self:update_suggestions()
self.gutter_text_brightness = 100
self.label = label .. ": "
self.label = text .. ": "
end
@ -221,13 +132,6 @@ function CommandView:exit(submitted, inexplicit)
self.doc:reset()
self.suggestions = {}
if not submitted then cancel(not inexplicit) end
self.save_suggestion = nil
self.last_text = ""
end
function CommandView:get_line_height()
return math.floor(self:get_font():get_height() * 1.2)
end
@ -265,45 +169,35 @@ function CommandView:update()
-- update suggestions if text has changed
if self.last_change_id ~= self.doc:get_change_id() then
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()
end
-- update gutter text color brightness
self:move_towards("gutter_text_brightness", 0, 0.1, "commandview")
self:move_towards("gutter_text_brightness", 0, 0.1)
-- update gutter width
local dest = self:get_font():get_width(self.label) + style.padding.x
if self.size.y <= 0 then
self.gutter_width = dest
else
self:move_towards("gutter_width", dest, nil, "commandview")
self:move_towards("gutter_width", dest)
end
-- update suggestions box height
local lh = self:get_suggestion_line_height()
local dest = self.state.show_suggestions and math.min(#self.suggestions, max_suggestions) * lh or 0
self:move_towards("suggestions_height", dest, nil, "commandview")
local dest = math.min(#self.suggestions, max_suggestions) * lh
self:move_towards("suggestions_height", dest)
-- update suggestion cursor offset
local dest = math.min(self.suggestion_idx, max_suggestions) * self:get_suggestion_line_height()
self:move_towards("selection_offset", dest, nil, "commandview")
self:move_towards("selection_offset", dest)
-- update size based on whether this is the active_view
local dest = 0
if self == core.active_view then
dest = style.font:get_height() + style.padding.y * 2
end
self:move_towards(self.size, "y", dest, nil, "commandview")
self:move_towards(self.size, "y", dest)
end
@ -320,7 +214,6 @@ function CommandView:draw_line_gutter(idx, x, y)
x = x + style.padding.x
renderer.draw_text(self:get_font(), self.label, x, y + yoffset, color)
core.pop_clip_rect()
return self:get_line_height()
end
@ -340,20 +233,20 @@ local function draw_suggestions_box(self)
end
-- draw suggestion text
local offset = math.max(self.suggestion_idx - max_suggestions, 0)
local last = math.min(offset + max_suggestions, #self.suggestions)
local suggestion_offset = math.max(self.suggestion_idx - max_suggestions, 0)
core.push_clip_rect(rx, ry, rw, rh)
local first = 1 + offset
for i=first, last do
local i = 1 + suggestion_offset
while i <= #self.suggestions do
local item = self.suggestions[i]
local color = (i == self.suggestion_idx) and style.accent or style.text
local y = self.position.y - (i - offset) * lh - dh
local y = self.position.y - (i - suggestion_offset) * lh - dh
common.draw_text(self:get_font(), color, item.text, nil, x, y, 0, lh)
if item.info then
local w = self.size.x - x - style.padding.x
common.draw_text(self:get_font(), style.dim, item.info, "right", x, y, w, lh)
end
i = i + 1
end
core.pop_clip_rect()
end
@ -361,9 +254,7 @@ end
function CommandView:draw()
CommandView.super.draw(self)
if self.state.show_suggestions then
core.root_view:defer_draw(draw_suggestions_box, self)
end
core.root_view:defer_draw(draw_suggestions_box, self)
end

314
data/core/common.lua Normal file → Executable file
View File

@ -1,7 +1,8 @@
local common = {}
function common.is_utf8_cont(s, offset)
local byte = s:byte(offset or 1)
function common.is_utf8_cont(char)
local byte = char:byte()
return byte >= 0x80 and byte < 0xc0
end
@ -16,33 +17,11 @@ function common.clamp(n, lo, hi)
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)
return n >= 0 and math.floor(n + 0.5) or math.ceil(n - 0.5)
end
function common.find_index(tbl, prop)
for i, o in ipairs(tbl) do
if o[prop] then return i end
end
end
function common.lerp(a, b, t)
if type(a) ~= "table" then
return a + (b - a) * t
@ -55,51 +34,26 @@ function common.lerp(a, b, t)
end
function common.distance(x1, y1, x2, y2)
return math.sqrt(((x2-x1) ^ 2)+((y2-y1) ^ 2))
end
function common.color(str)
local r, g, b, a = str:match("^#(%x%x)(%x%x)(%x%x)(%x?%x?)$")
local r, g, b, a = str:match("#(%x%x)(%x%x)(%x%x)")
if r then
r = tonumber(r, 16)
g = tonumber(g, 16)
b = tonumber(b, 16)
a = tonumber(a, 16) or 0xff
a = 1
elseif str:match("rgba?%s*%([%d%s%.,]+%)") then
local f = str:gmatch("[%d.]+")
r = (f() or 0)
g = (f() or 0)
b = (f() or 0)
a = (f() or 1) * 0xff
a = f() or 1
else
error(string.format("bad color string '%s'", str))
end
return r, g, b, a
return r, g, b, a * 0xff
end
function common.splice(t, at, remove, insert)
insert = insert or {}
local offset = #insert - remove
local old_len = #t
if offset < 0 then
for i = at - offset, old_len - offset do
t[i + offset] = t[i]
end
elseif offset > 0 then
for i = old_len, at, -1 do
t[i + offset] = t[i]
end
end
for i, item in ipairs(insert) do
t[at + i - 1] = item
end
end
local function compare_score(a, b)
return a.score > b.score
end
@ -146,29 +100,9 @@ function common.fuzzy_match_with_recents(haystack, recents, needle)
end
function common.path_suggest(text, root)
if root and root:sub(-1) ~= PATHSEP then
root = root .. PATHSEP
end
local path, name = text:match("^(.-)([^:/\\]*)$")
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 {}
function common.path_suggest(text)
local path, name = text:match("^(.-)([^/\\]*)$")
local files = system.list_dir(path == "" and "." or path) or {}
local res = {}
for _, file in ipairs(files) do
file = path .. file
@ -177,19 +111,6 @@ function common.path_suggest(text, root)
if info.type == "dir" then
file = file .. PATHSEP
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
table.insert(res, file)
end
@ -200,7 +121,7 @@ end
function common.dir_path_suggest(text)
local path, name = text:match("^(.-)([^:/\\]*)$")
local path, name = text:match("^(.-)([^/\\]*)$")
local files = system.list_dir(path == "" and "." or path) or {}
local res = {}
for _, file in ipairs(files) do
@ -215,7 +136,7 @@ end
function common.dir_list_suggest(text, dir_list)
local path, name = text:match("^(.-)([^:/\\]*)$")
local path, name = text:match("^(.-)([^/\\]*)$")
local res = {}
for _, dir_path in ipairs(dir_list) do
if dir_path:lower():find(text:lower(), nil, true) == 1 then
@ -261,58 +182,19 @@ function common.bench(name, fn, ...)
end
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 ""
function common.serialize(val)
if type(val) == "string" then
local out = string.format("%q", val)
if escape then
out = string.gsub(out, "\\\n", "\\n")
out = string.gsub(out, "\\7", "\\a")
out = string.gsub(out, "\\8", "\\b")
out = string.gsub(out, "\\9", "\\t")
out = string.gsub(out, "\\11", "\\v")
out = string.gsub(out, "\\12", "\\f")
out = string.gsub(out, "\\13", "\\r")
end
return out
return string.format("%q", val)
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 = {}
for k, v in pairs(val) do
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))
table.insert(t, "[" .. common.serialize(k) .. "]=" .. common.serialize(v))
end
if #t == 0 then return "{}" end
if sort then table.sort(t) end
return "{" .. newline .. table.concat(t, "," .. newline) .. newline .. indent .. "}"
return "{" .. table.concat(t, ",") .. "}"
end
return tostring(val)
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)
-- a path should never end by / or \ except if it is '/' (unix root) or
@ -321,12 +203,6 @@ function common.basename(path)
end
-- can return nil if there is no directory part in the path
function common.dirname(path)
return path:match("(.+)[:\\/][^\\/]+$")
end
function common.home_encode(text)
if HOME and string.find(text, HOME, 1, true) == 1 then
local dir_pos = #HOME + 1
@ -350,18 +226,22 @@ end
function common.home_expand(text)
if text == nil then
return HOME
end
return HOME and text:gsub("^~", HOME) or text
end
function common.normalize_path(filename)
if filename and PATHSEP == '\\' then
filename = filename:gsub('[/\\]', '\\')
local drive, rem = filename:match('^([a-zA-Z])(:.*)')
return drive and drive:upper() .. rem or filename
end
return filename
end
local function split_on_slash(s, sep_pattern)
local t = {}
if s:match("^[/\\]") then
t[#t + 1] = ""
end
for fragment in string.gmatch(s, "([^/\\]+)") do
t[#t + 1] = fragment
end
@ -369,81 +249,12 @@ local function split_on_slash(s, sep_pattern)
end
-- The filename argument given to the function is supposed to
-- come from system.absolute_path and as such should be an
-- absolute path without . or .. elements.
-- This function exists because on Windows the drive letter returned
-- by system.absolute_path is sometimes with a lower case and sometimes
-- with an upper case so we normalize to upper case.
function common.normalize_volume(filename)
if not filename then return end
if PATHSEP == '\\' then
local drive, rem = filename:match('^([a-zA-Z]:\\)(.-)'..PATHSEP..'?$')
if drive then
return drive:upper() .. rem
end
end
return filename
end
function common.normalize_path(filename)
if not filename then return end
local volume
if PATHSEP == '\\' then
filename = filename:gsub('[/\\]', '\\')
local drive, rem = filename:match('^([a-zA-Z]:\\)(.*)')
if drive then
volume, filename = drive:upper(), rem
else
drive, rem = filename:match('^(\\\\[^\\]+\\[^\\]+\\)(.*)')
if drive then
volume, filename = drive, rem
end
end
else
local relpath = filename:match('^/(.+)')
if relpath then
volume, filename = "/", relpath
end
end
local parts = split_on_slash(filename, PATHSEP)
local accu = {}
for _, part in ipairs(parts) do
if part == '..' then
if #accu > 0 and accu[#accu] ~= ".." then
table.remove(accu)
elseif volume then
error("invalid path " .. volume .. filename)
else
table.insert(accu, part)
end
elseif part ~= '.' then
table.insert(accu, part)
end
end
local npath = table.concat(accu, PATHSEP)
return (volume or "") .. (npath == "" and PATHSEP or npath)
end
function common.is_absolute_path(path)
return path:sub(1, 1) == PATHSEP or path:match("^(%a):\\") or path:match('^(%w*):')
end
function common.path_belongs_to(filename, path)
return string.find(filename, path .. PATHSEP, 1, true) == 1
return filename and string.find(filename, path .. PATHSEP, 1, true) == 1
end
function common.relative_path(ref_dir, dir)
local drive_pattern = "^(%a):\\"
local drive, ref_drive = dir:match(drive_pattern), ref_dir:match(drive_pattern)
if drive and ref_drive and drive ~= ref_drive then
-- Windows, different drives, system.absolute_path fails for C:\..\D:\
return dir
end
local ref_ls = split_on_slash(ref_dir)
local dir_ls = split_on_slash(dir)
local i = 1
@ -462,75 +273,4 @@ function common.relative_path(ref_dir, dir)
end
function common.mkdirp(path)
local stat = system.get_file_info(path)
if stat and stat.type then
return false, "path exists", path
end
local subdirs = {}
while path and path ~= "" do
local success_mkdir = system.mkdir(path)
if success_mkdir then break end
local updir, basedir = path:match("(.*)[/\\](.+)$")
table.insert(subdirs, 1, basedir or path)
path = updir
end
for _, dirname in ipairs(subdirs) do
path = path and path .. PATHSEP .. dirname or dirname
if not system.mkdir(path) then
return false, "cannot create directory", path
end
end
return true
end
function common.rm(path, recursively)
local stat = system.get_file_info(path)
if not stat or (stat.type ~= "file" and stat.type ~= "dir") then
return false, "invalid path given", path
end
if stat.type == "file" then
local removed, error = os.remove(path)
if not removed then
return false, error, path
end
else
local contents = system.list_dir(path)
if #contents > 0 and not recursively then
return false, "directory is not empty", path
end
for _, item in pairs(contents) do
local item_path = path .. PATHSEP .. item
local item_stat = system.get_file_info(item_path)
if not item_stat then
return false, "invalid file encountered", item_path
end
if item_stat.type == "dir" then
local deleted, error, ipath = common.rm(item_path, recursively)
if not deleted then
return false, error, ipath
end
elseif item_stat.type == "file" then
local removed, error = os.remove(item_path)
if not removed then
return false, error, item_path
end
end
end
local removed, error = system.rmdir(path)
if not removed then
return false, error, path
end
end
return true
end
return common

View File

@ -1,59 +1,33 @@
local config = {}
config.project_scan_rate = 5
config.fps = 60
config.max_log_items = 800
config.max_log_items = 80
config.message_timeout = 5
config.mouse_wheel_scroll = 50 * SCALE
config.animate_drag_scroll = false
config.scroll_past_end = true
config.file_size_limit = 10
config.ignore_files = { "^%." }
config.scroll_past_end = true
config.ignore_files = "^%."
config.symbol_pattern = "[%a_][%w_]*"
config.non_word_chars = " \t\n/\\()\"':,.;<>~!@#$%^&*|+=[]{}`?-"
config.undo_merge_timeout = 0.3
config.max_undos = 10000
config.max_tabs = 8
config.always_show_tabs = true
-- Possible values: false, true, "no_selection"
config.max_tabs = 10
config.highlight_current_line = true
config.line_height = 1.2
config.indent_size = 2
config.tab_type = "soft"
config.line_limit = 80
config.max_symbols = 4000
config.max_project_files = 2000
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.blink_period = 0.8
config.disable_blink = false
config.draw_whitespace = false
config.borderless = false
config.tab_close_button = true
config.max_clicks = 3
-- set as true to be able to test non supported plugins
config.skip_plugins_version = false
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.drawwhitespace = false
-- Disable plugin loading setting to false the config entry
-- of the same name.
config.trimwhitespace = false
return config

View File

@ -1,241 +0,0 @@
local core = require "core"
local common = require "core.common"
local command = require "core.command"
local config = require "core.config"
local keymap = require "core.keymap"
local style = require "core.style"
local Object = require "core.object"
local View = require "core.view"
local border_width = 1
local divider_width = 1
local DIVIDER = {}
---@class core.contextmenu : core.object
local ContextMenu = Object:extend()
ContextMenu.DIVIDER = DIVIDER
function ContextMenu:new()
self.itemset = {}
self.show_context_menu = false
self.selected = -1
self.height = 0
self.position = { x = 0, y = 0 }
self.current_scale = SCALE
end
local function get_item_size(item)
local lw, lh
if item == DIVIDER then
lw = 0
lh = divider_width
else
lw = style.font:get_width(item.text)
if item.info then
lw = lw + style.padding.x + style.font:get_width(item.info)
end
lh = style.font:get_height() + style.padding.y
end
return lw, lh
end
local function update_items_size(items, update_binding)
local width, height = 0, 0
for _, item in ipairs(items) do
if update_binding and item ~= DIVIDER then
item.info = keymap.get_binding(item.command)
end
local lw, lh = get_item_size(item)
width = math.max(width, lw)
height = height + lh
end
width = width + style.padding.x * 2
items.width, items.height = width, height
end
function ContextMenu:register(predicate, items)
predicate = command.generate_predicate(predicate)
update_items_size(items, true)
table.insert(self.itemset, { predicate = predicate, items = items })
end
function ContextMenu:show(x, y)
self.items = nil
local items_list = { width = 0, height = 0 }
for _, items in ipairs(self.itemset) do
if items.predicate(x, y) then
items_list.width = math.max(items_list.width, items.items.width)
items_list.height = items_list.height
for _, subitems in ipairs(items.items) do
if not subitems.command or command.is_valid(subitems.command) then
local lw, lh = get_item_size(subitems)
items_list.height = items_list.height + lh
table.insert(items_list, subitems)
end
end
end
end
if #items_list > 0 then
self.items = items_list
local w, h = self.items.width, self.items.height
-- by default the box is opened on the right and below
if x + w >= core.root_view.size.x then
x = x - w
end
if y + h >= core.root_view.size.y then
y = y - h
end
self.position.x, self.position.y = x, y
self.show_context_menu = true
core.request_cursor("arrow")
return true
end
return false
end
function ContextMenu:hide()
self.show_context_menu = false
self.items = nil
self.selected = -1
self.height = 0
core.request_cursor(core.active_view.cursor)
end
function ContextMenu:each_item()
local x, y, w = self.position.x, self.position.y, self.items.width
local oy = y
return coroutine.wrap(function()
for i, item in ipairs(self.items) do
local _, lh = get_item_size(item)
if y - oy > self.height then break end
coroutine.yield(i, item, x, y, w, lh)
y = y + lh
end
end)
end
function ContextMenu:on_mouse_moved(px, py)
if not self.show_context_menu then return end
self.selected = -1
for i, item, x, y, w, h in self:each_item() do
if px > x and px <= x + w and py > y and py <= y + h then
self.selected = i
break
end
end
return true
end
function ContextMenu:on_selected(item)
if type(item.command) == "string" then
command.perform(item.command)
else
item.command()
end
end
local function change_value(value, change)
return value + change
end
function ContextMenu:focus_previous()
self.selected = (self.selected == -1 or self.selected == 1) and #self.items or change_value(self.selected, -1)
if self:get_item_selected() == DIVIDER then
self.selected = change_value(self.selected, -1)
end
end
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
self:on_selected(selected)
end
end
function ContextMenu:on_mouse_pressed(button, px, py, clicks)
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
return caught
end
ContextMenu.move_towards = View.move_towards
function ContextMenu:update()
if self.show_context_menu then
self:move_towards("height", self.items.height, nil, "contextmenu")
end
end
function ContextMenu:draw()
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)
end
function ContextMenu:draw_context_menu()
if not self.items then return end
local bx, by, bw, bh = self.position.x, self.position.y, self.items.width, self.height
renderer.draw_rect(
bx - border_width,
by - border_width,
bw + (border_width * 2),
bh + (border_width * 2),
style.divider
)
renderer.draw_rect(bx, by, bw, bh, style.background3)
for i, item, x, y, w, h in self:each_item() do
if item == DIVIDER then
renderer.draw_rect(x, y, w, h, style.caret)
else
if i == self.selected then
renderer.draw_rect(x, y, w, h, style.selection)
end
common.draw_text(style.font, style.text, item.text, "left", x + style.padding.x, y, w, h)
if item.info then
common.draw_text(style.font, style.dim, item.info, "right", x, y, w - style.padding.x, h)
end
end
end
end
return ContextMenu

View File

@ -1,232 +0,0 @@
local common = require "core.common"
local config = require "core.config"
local dirwatch = {}
function dirwatch:__index(idx)
local value = rawget(self, idx)
if value ~= nil then return value end
return dirwatch[idx]
end
function dirwatch.new()
local t = {
scanned = {},
watched = {},
reverse_watched = {},
monitor = dirmonitor.new(),
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

38
data/core/doc/highlighter.lua Normal file → Executable file
View File

@ -1,5 +1,4 @@
local core = require "core"
local common = require "core.common"
local config = require "core.config"
local tokenizer = require "core.tokenizer"
local Object = require "core.object"
@ -22,21 +21,13 @@ function Highlighter:new(doc)
else
local max = math.min(self.first_invalid_line + 40, self.max_wanted_line)
local retokenized_from
for i = self.first_invalid_line, max do
local state = (i > 1) and self.lines[i - 1].state
local line = self.lines[i]
if not (line and line.init_state == state and line.text == self.doc.lines[i]) then
retokenized_from = retokenized_from or i
if not (line and line.init_state == state) then
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
if retokenized_from then
self:update_notify(retokenized_from, max - retokenized_from)
end
self.first_invalid_line = max + 1
core.redraw = true
@ -49,40 +40,16 @@ end
function Highlighter:reset()
self.lines = {}
self:soft_reset()
end
function Highlighter:soft_reset()
for i=1,#self.lines do
self.lines[i] = false
end
self.first_invalid_line = 1
self.max_wanted_line = 0
end
function Highlighter:invalidate(idx)
self.first_invalid_line = math.min(self.first_invalid_line, idx)
self.max_wanted_line = math.min(self.max_wanted_line, #self.doc.lines)
end
function Highlighter:insert_notify(line, n)
self:invalidate(line)
local blanks = { }
for i = 1, n do
blanks[i] = false
end
common.splice(self.lines, line, 0, blanks)
end
function Highlighter:remove_notify(line, n)
self:invalidate(line)
common.splice(self.lines, line, n)
end
function Highlighter:update_notify(line, n)
-- plugins can hook here to be notified that lines have been retokenized
end
function Highlighter:tokenize_line(idx, state)
local res = {}
@ -99,7 +66,6 @@ function Highlighter:get_line(idx)
local prev = self.lines[idx - 1]
line = self:tokenize_line(idx, prev and prev.state)
self.lines[idx] = line
self:update_notify(idx, 0)
end
self.max_wanted_line = math.max(self.max_wanted_line, idx)
return line

385
data/core/doc/init.lua Normal file → Executable file
View File

@ -1,11 +1,10 @@
local Object = require "core.object"
local Highlighter = require "core.doc.highlighter"
local core = require "core"
local syntax = require "core.syntax"
local config = require "core.config"
local common = require "core.common"
---@class core.doc : core.object
local Doc = Object:extend()
@ -18,21 +17,36 @@ local function split_lines(text)
end
function Doc:new(filename, abs_filename, new_file)
self.new_file = new_file
local function splice(t, at, remove, insert)
insert = insert or {}
local offset = #insert - remove
local old_len = #t
if offset < 0 then
for i = at - offset, old_len - offset do
t[i + offset] = t[i]
end
elseif offset > 0 then
for i = old_len, at, -1 do
t[i + offset] = t[i]
end
end
for i, item in ipairs(insert) do
t[at + i - 1] = item
end
end
function Doc:new(filename)
self:reset()
if filename then
self:set_filename(filename, abs_filename)
if not new_file then
self:load(filename)
end
self:load(filename)
end
end
function Doc:reset()
self.lines = { "\n" }
self.selections = { 1, 1, 1, 1 }
self.selection = { a = { line=1, col=1 }, b = { line=1, col=1 } }
self.undo_stack = { idx = 1 }
self.redo_stack = { idx = 1 }
self.clean_change_id = 1
@ -46,31 +60,28 @@ function Doc:reset_syntax()
local syn = syntax.get(self.filename or "", header)
if self.syntax ~= syn then
self.syntax = syn
self.highlighter:soft_reset()
self.highlighter:reset()
end
end
function Doc:set_filename(filename, abs_filename)
function Doc:set_filename(filename)
self.filename = filename
self.abs_filename = abs_filename
self:reset_syntax()
self.abs_filename = system.absolute_path(filename)
end
function Doc:load(filename)
local fp = assert( io.open(filename, "rb") )
self:reset()
self:set_filename(filename)
self.lines = {}
local i = 1
for line in fp:lines() do
if line:byte(-1) == 13 then
line = line:sub(1, -2)
self.crlf = true
end
table.insert(self.lines, line .. "\n")
self.highlighter.lines[i] = false
i = i + 1
end
if #self.lines == 0 then
table.insert(self.lines, "\n")
@ -80,32 +91,18 @@ function Doc:load(filename)
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)
if not filename then
assert(self.filename, "no filename set to default to")
filename = self.filename
abs_filename = self.abs_filename
else
assert(self.filename or abs_filename, "calling save on unnamed doc without absolute path")
end
function Doc:save(filename)
filename = filename or assert(self.filename, "no filename set to default to")
local fp = assert( io.open(filename, "wb") )
for _, line in ipairs(self.lines) do
if self.crlf then line = line:gsub("\n", "\r\n") end
fp:write(line)
end
fp:close()
self:set_filename(filename, abs_filename)
self.new_file = false
if filename then
self:set_filename(filename)
end
self:reset_syntax()
self:clean()
end
@ -116,11 +113,7 @@ end
function Doc:is_dirty()
if self.new_file then
return #self.lines > 1 or #self.lines[1] > 1
else
return self.clean_change_id ~= self:get_change_id()
end
return self.clean_change_id ~= self:get_change_id()
end
@ -129,125 +122,49 @@ function Doc:clean()
end
function Doc:get_indent_info()
if not self.indent_info then return config.tab_type, config.indent_size, false end
return self.indent_info.type or config.tab_type,
self.indent_info.size or config.indent_size,
self.indent_info.confirmed
end
function Doc:get_change_id()
return self.undo_stack.idx
end
-- Cursor section. Cursor indices are *only* valid during a get_selections() call.
-- Cursors will always be iterated in order from top to bottom. Through normal operation
-- curors can never swap positions; only merge or split, or change their position in cursor
-- order.
function Doc:get_selection(sort)
local idx, line1, col1, line2, col2, swap = self:get_selections(sort)({ self.selections, sort }, 0)
return line1, col1, line2, col2, swap
function Doc:set_selection(line1, col1, line2, col2, swap)
assert(not line2 == not col2, "expected 2 or 4 arguments")
if swap then line1, col1, line2, col2 = line2, col2, line1, col1 end
line1, col1 = self:sanitize_position(line1, col1)
line2, col2 = self:sanitize_position(line2 or line1, col2 or col1)
self.selection.a.line, self.selection.a.col = line1, col1
self.selection.b.line, self.selection.b.col = line2, col2
end
function Doc:get_selection_text(limit)
limit = limit or math.huge
local result = {}
for idx, line1, col1, line2, col2 in self:get_selections() do
if idx > limit then break end
if line1 ~= line2 or col1 ~= col2 then
local text = self:get_text(line1, col1, line2, col2)
if text ~= "" then result[#result + 1] = text end
end
end
return table.concat(result, "\n")
end
function Doc:has_selection()
local line1, col1, line2, col2 = self:get_selection(false)
return line1 ~= line2 or col1 ~= col2
end
function Doc:has_any_selection()
for idx, line1, col1, line2, col2 in self:get_selections() do
if line1 ~= line2 or col1 ~= col2 then return true end
end
return false
end
function Doc:sanitize_selection()
for idx, line1, col1, line2, col2 in self:get_selections() do
self:set_selections(idx, line1, col1, line2, col2)
end
end
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, true
end
return line1, col1, line2, col2, false
end
function Doc:set_selections(idx, line1, col1, line2, col2, swap, rm)
assert(not line2 == not col2, "expected 3 or 5 arguments")
if swap then line1, col1, line2, col2 = line2, col2, line1, col1 end
line1, col1 = self:sanitize_position(line1, col1)
line2, col2 = self:sanitize_position(line2 or line1, col2 or col1)
common.splice(self.selections, (idx - 1)*4 + 1, rm == nil and 4 or rm, { line1, col1, line2, col2 })
end
function Doc:add_selection(line1, col1, line2, col2, swap)
local l1, c1 = sort_positions(line1, col1, line2 or line1, col2 or col1)
local target = #self.selections / 4 + 1
for idx, tl1, tc1 in self:get_selections(true) do
if l1 < tl1 or l1 == tl1 and c1 < tc1 then
target = idx
break
end
function Doc:get_selection(sort)
local a, b = self.selection.a, self.selection.b
if sort then
return sort_positions(a.line, a.col, b.line, b.col)
end
self:set_selections(target, line1, col1, line2, col2, swap, 0)
return a.line, a.col, b.line, b.col
end
function Doc:remove_selection(idx)
common.splice(self.selections, (idx - 1) * 4 + 1, 4)
function Doc:has_selection()
local a, b = self.selection.a, self.selection.b
return not (a.line == b.line and a.col == b.col)
end
function Doc:set_selection(line1, col1, line2, col2, swap)
self.selections = {}
self:set_selections(1, line1, col1, line2, col2, swap)
function Doc:sanitize_selection()
self:set_selection(self:get_selection())
end
function Doc:merge_cursors(idx)
for i = (idx or (#self.selections - 3)), (idx or 5), -4 do
for j = 1, i - 4, 4 do
if self.selections[i] == self.selections[j] and
self.selections[i+1] == self.selections[j+1] then
common.splice(self.selections, i, 4)
break
end
end
end
end
local function selection_iterator(invariant, idx)
local target = invariant[3] and (idx*4 - 7) or (idx*4 + 1)
if target > #invariant[1] or target <= 0 or (type(invariant[3]) == "number" and invariant[3] ~= idx - 1) then return end
if invariant[2] then
return idx+(invariant[3] and -1 or 1), sort_positions(table.unpack(invariant[1], target, target+4))
else
return idx+(invariant[3] and -1 or 1), table.unpack(invariant[1], target, target+4)
end
end
-- If idx_reverse is true, it'll reverse iterate. If nil, or false, regular iterate.
-- If a number, runs for exactly that iteration.
function Doc:get_selections(sort_intra, idx_reverse)
return selection_iterator, { self.selections, sort_intra, idx_reverse },
idx_reverse == true and ((#self.selections / 4) + 1) or ((idx_reverse or -1)+1)
end
-- End of cursor seciton.
function Doc:sanitize_position(line, col)
line = common.clamp(line, 1, #self.lines)
@ -334,12 +251,14 @@ local function pop_undo(self, undo_stack, redo_stack, modified)
if cmd.type == "insert" then
local line, col, text = table.unpack(cmd)
self:raw_insert(line, col, text, redo_stack, cmd.time)
elseif cmd.type == "remove" then
local line1, col1, line2, col2 = table.unpack(cmd)
self:raw_remove(line1, col1, line2, col2, redo_stack, cmd.time)
elseif cmd.type == "selection" then
self.selections = { table.unpack(cmd) }
self:sanitize_selection()
self.selection.a.line, self.selection.a.col = cmd[1], cmd[2]
self.selection.b.line, self.selection.b.col = cmd[3], cmd[4]
end
modified = modified or (cmd.type ~= "selection")
@ -360,7 +279,6 @@ end
function Doc:raw_insert(line, col, text, undo_stack, time)
-- split text into lines and merge with line at insertion point
local lines = split_lines(text)
local len = #lines[#lines]
local before = self.lines[line]:sub(1, col - 1)
local after = self.lines[line]:sub(col)
for i = 1, #lines - 1 do
@ -370,23 +288,15 @@ function Doc:raw_insert(line, col, text, undo_stack, time)
lines[#lines] = lines[#lines] .. after
-- splice lines into line array
common.splice(self.lines, line, 1, lines)
-- keep cursors where they should be
for idx, cline1, ccol1, cline2, ccol2 in self:get_selections(true, true) do
if cline1 < line then break end
local line_addition = (line < cline1 or col < ccol1) and #lines - 1 or 0
local column_addition = line == cline1 and ccol1 > col and len or 0
self:set_selections(idx, cline1 + line_addition, ccol1 + column_addition, cline2 + line_addition, ccol2 + column_addition)
end
splice(self.lines, line, 1, lines)
-- push undo
local line2, col2 = self:position_offset(line, col, #text)
push_undo(undo_stack, time, "selection", table.unpack(self.selections))
push_undo(undo_stack, time, "selection", self:get_selection())
push_undo(undo_stack, time, "remove", line, col, line2, col2)
-- update highlighter and assure selection is in bounds
self.highlighter:insert_notify(line, #lines - 1)
self.highlighter:invalidate(line)
self:sanitize_selection()
end
@ -394,7 +304,7 @@ end
function Doc:raw_remove(line1, col1, line2, col2, undo_stack, time)
-- push undo
local text = self:get_text(line1, col1, line2, col2)
push_undo(undo_stack, time, "selection", table.unpack(self.selections))
push_undo(undo_stack, time, "selection", self:get_selection())
push_undo(undo_stack, time, "insert", line1, col1, text)
-- get line content before/after removed text
@ -402,18 +312,10 @@ function Doc:raw_remove(line1, col1, line2, col2, undo_stack, time)
local after = self.lines[line2]:sub(col2)
-- splice line into line array
common.splice(self.lines, line1, line2 - line1 + 1, { before .. after })
-- move all cursors back if they share a line with the removed text
for idx, cline1, ccol1, cline2, ccol2 in self:get_selections(true, true) do
if cline1 < line2 then break end
local line_removal = line2 - line1
local column_removal = line2 == cline2 and col2 < ccol1 and (line2 == line1 and col2 - col1 or col2) or 0
self:set_selections(idx, cline1 - line_removal, ccol1 - column_removal, cline2 - line_removal, ccol2 - column_removal)
end
splice(self.lines, line1, line2 - line1 + 1, { before .. after })
-- update highlighter and assure selection is in bounds
self.highlighter:remove_notify(line1, line2 - line1)
self.highlighter:invalidate(line1)
self:sanitize_selection()
end
@ -446,149 +348,66 @@ function Doc:redo()
end
function Doc:text_input(text, idx)
for sidx, line1, col1, line2, col2 in self:get_selections(true, idx or true) do
if line1 ~= line2 or col1 ~= col2 then
self:delete_to_cursor(sidx)
end
self:insert(line1, col1, text)
self:move_to_cursor(sidx, #text)
function Doc:text_input(text)
if self:has_selection() then
self:delete_to()
end
local line, col = self:get_selection()
self:insert(line, col, text)
self:move_to(#text)
end
function Doc:replace_cursor(idx, line1, col1, line2, col2, fn)
function Doc:replace(fn)
local line1, col1, line2, col2, swap
local had_selection = self:has_selection()
if had_selection then
line1, col1, line2, col2, swap = self:get_selection(true)
else
line1, col1, line2, col2 = 1, 1, #self.lines, #self.lines[#self.lines]
end
local old_text = self:get_text(line1, col1, line2, col2)
local new_text, res = fn(old_text)
local new_text, n = fn(old_text)
if old_text ~= new_text then
self:insert(line2, col2, new_text)
self:remove(line1, col1, line2, col2)
if line1 == line2 and col1 == col2 then
if had_selection then
line2, col2 = self:position_offset(line1, col1, #new_text)
self:set_selections(idx, line1, col1, line2, col2)
self:set_selection(line1, col1, line2, col2, swap)
end
end
return res
end
function Doc:replace(fn)
local has_selection, results = false, { }
for idx, line1, col1, line2, col2 in self:get_selections(true) do
if line1 ~= line2 or col1 ~= col2 then
results[idx] = self:replace_cursor(idx, line1, col1, line2, col2, fn)
has_selection = true
end
end
if not has_selection then
self:set_selection(table.unpack(self.selections))
results[1] = self:replace_cursor(1, 1, 1, #self.lines, #self.lines[#self.lines], fn)
end
return results
return n
end
function Doc:delete_to_cursor(idx, ...)
for sidx, line1, col1, line2, col2 in self:get_selections(true, idx) do
if line1 ~= line2 or col1 ~= col2 then
self:remove(line1, col1, line2, col2)
else
local l2, c2 = self:position_offset(line1, col1, ...)
self:remove(line1, col1, l2, c2)
line1, col1 = sort_positions(line1, col1, l2, c2)
end
self:set_selections(sidx, line1, col1)
end
self:merge_cursors(idx)
end
function Doc:delete_to(...) return self:delete_to_cursor(nil, ...) end
function Doc:move_to_cursor(idx, ...)
for sidx, line, col in self:get_selections(false, idx) do
self:set_selections(sidx, self:position_offset(line, col, ...))
end
self:merge_cursors(idx)
end
function Doc:move_to(...) return self:move_to_cursor(nil, ...) end
function Doc:select_to_cursor(idx, ...)
for sidx, line, col, line2, col2 in self:get_selections(false, idx) do
line, col = self:position_offset(line, col, ...)
self:set_selections(sidx, line, col, line2, col2)
end
self:merge_cursors(idx)
end
function Doc:select_to(...) return self:select_to_cursor(nil, ...) end
function Doc:get_indent_string()
local indent_type, indent_size = self:get_indent_info()
if indent_type == "hard" then
return "\t"
end
return string.rep(" ", indent_size)
end
-- returns the size of the original indent, and the indent
-- in your config format, rounded either up or down
function Doc:get_line_indent(line, rnd_up)
local _, e = line:find("^[ \t]+")
local indent_type, indent_size = self:get_indent_info()
local soft_tab = string.rep(" ", indent_size)
if indent_type == "hard" then
local indent = e and line:sub(1, e):gsub(soft_tab, "\t") or ""
return e, indent:gsub(" +", rnd_up and "\t" or "")
function Doc:delete_to(...)
local line, col = self:get_selection(true)
if self:has_selection() then
self:remove(self:get_selection())
else
local indent = e and line:sub(1, e):gsub("\t", soft_tab) or ""
local number = #indent / #soft_tab
return e, indent:sub(1,
(rnd_up and math.ceil(number) or math.floor(number))*#soft_tab)
local line2, col2 = self:position_offset(line, col, ...)
self:remove(line, col, line2, col2)
line, col = sort_positions(line, col, line2, col2)
end
self:set_selection(line, col)
end
-- un/indents text; behaviour varies based on selection and un/indent.
-- * if there's a selection, it will stay static around the
-- text for both indenting and unindenting.
-- * if you are in the beginning whitespace of a line, and are indenting, the
-- cursor will insert the exactly appropriate amount of spaces, and jump the
-- cursor to the beginning of first non whitespace characters
-- * if you are not in the beginning whitespace of a line, and you indent, it
-- inserts the appropriate whitespace, as if you typed them normally.
-- * if you are unindenting, the cursor will jump to the start of the line,
-- and remove the appropriate amount of spaces (or a tab).
function Doc:indent_text(unindent, line1, col1, line2, col2)
local text = self:get_indent_string()
local _, se = self.lines[line1]:find("^[ \t]+")
local in_beginning_whitespace = col1 == 1 or (se and col1 <= se + 1)
local has_selection = line1 ~= line2 or col1 ~= col2
if unindent or has_selection or in_beginning_whitespace then
local l1d, l2d = #self.lines[line1], #self.lines[line2]
for line = line1, line2 do
if not has_selection or #self.lines[line] > 1 then -- don't indent empty lines in a selection
local e, rnded = self:get_line_indent(self.lines[line], unindent)
self:remove(line, 1, line, (e or 0) + 1)
self:insert(line, 1,
unindent and rnded:sub(1, #rnded - #text) or rnded .. text)
end
end
l1d, l2d = #self.lines[line1] - l1d, #self.lines[line2] - l2d
if (unindent or in_beginning_whitespace) and not has_selection then
local start_cursor = (se and se + 1 or 1) + l1d or #(self.lines[line1])
return line1, start_cursor, line2, start_cursor
end
return line1, col1 + l1d, line2, col2 + l2d
end
self:insert(line1, col1, text)
return line1, col1 + #text, line1, col1 + #text
function Doc:move_to(...)
local line, col = self:get_selection()
self:set_selection(self:position_offset(line, col, ...))
end
function Doc:select_to(...)
local line, col, line2, col2 = self:get_selection()
line, col = self:position_offset(line, col, ...)
self:set_selection(line, col, line2, col2)
end
-- For plugins to add custom actions of document change
function Doc:on_text_change(type)
end
-- For plugins to get notified when a document is closed
function Doc:on_close()
core.log_quiet("Closed doc \"%s\"", self:get_name())
end
return Doc

59
data/core/doc/search.lua Normal file → Executable file
View File

@ -15,69 +15,36 @@ local function init_args(doc, line, col, text, opt)
opt = opt or default_opt
line, col = doc:sanitize_position(line, col)
if opt.no_case and not opt.regex then
text = text:lower()
if opt.no_case then
if opt.pattern then
text = text:gsub("%%?.", pattern_lower)
else
text = text:lower()
end
end
return doc, line, col, text, opt
end
-- This function is needed to uniform the behavior of
-- `regex:cmatch` and `string.find`.
local function regex_func(text, re, index, _)
local s, e = re:cmatch(text, index)
return s, e and e - 1
end
local function rfind(func, text, pattern, index, plain)
local s, e = func(text, pattern, 1, plain)
local last_s, last_e
if index < 0 then index = #text - index + 1 end
while e and e <= index do
last_s, last_e = s, e
s, e = func(text, pattern, s + 1, plain)
end
return last_s, last_e
end
function search.find(doc, line, col, text, opt)
doc, line, col, text, opt = init_args(doc, line, col, text, opt)
local plain = not opt.pattern
local pattern = text
local search_func = string.find
if opt.regex then
pattern = regex.compile(text, opt.no_case and "i" or "")
search_func = regex_func
end
local start, finish, step = line, #doc.lines, 1
if opt.reverse then
start, finish, step = line, 1, -1
end
for line = start, finish, step do
for line = line, #doc.lines do
local line_text = doc.lines[line]
if opt.no_case and not opt.regex then
if opt.no_case then
line_text = line_text:lower()
end
local s, e
if opt.reverse then
s, e = rfind(search_func, line_text, pattern, col - 1, plain)
else
s, e = search_func(line_text, pattern, col, plain)
end
local s, e = line_text:find(text, col, not opt.pattern)
if s then
return line, s, line, e + 1
end
col = opt.reverse and -1 or 1
col = 1
end
if opt.wrap then
opt = { no_case = opt.no_case, regex = opt.regex, reverse = opt.reverse }
if opt.reverse then
return search.find(doc, #doc.lines, #doc.lines[#doc.lines], text, opt)
else
return search.find(doc, 1, 1, text, opt)
end
opt = { no_case = opt.no_case, pattern = opt.pattern }
return search.find(doc, 1, 1, text, opt)
end
end

4
data/core/doc/translate.lua Normal file → Executable file
View File

@ -117,10 +117,6 @@ function translate.start_of_line(doc, line, col)
return line, 1
end
function translate.start_of_indentation(doc, line, col)
local s, e = doc.lines[line]:find("^%s*")
return line, col > e + 1 and e + 1 or 1
end
function translate.end_of_line(doc, line, col)
return line, math.huge

View File

@ -6,11 +6,9 @@ local keymap = require "core.keymap"
local translate = require "core.doc.translate"
local View = require "core.view"
---@class core.docview : core.view
---@field super core.view
local DocView = View:extend()
DocView.context = "session"
local function move_to_line_offset(dv, line, col, offset)
local xo = dv.last_x_offset
@ -30,9 +28,6 @@ DocView.translate = {
end,
["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()
return line + (max - min), 1
end,
@ -66,22 +61,19 @@ end
function DocView:try_close(do_close)
if self.doc:is_dirty()
and #core.get_views_referencing_doc(self.doc) == 1 then
core.command_view:enter("Unsaved Changes; Confirm Close", {
submit = function(_, item)
if item.text:match("^[cC]") then
do_close()
elseif item.text:match("^[sS]") then
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
core.command_view:enter("Unsaved Changes; Confirm Close", function(_, item)
if item.text:match("^[cC]") then
do_close()
elseif item.text:match("^[sS]") then
self.doc:save()
do_close()
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
do_close()
end
@ -104,7 +96,7 @@ function DocView:get_filename()
end
function DocView:get_scrollable_size()
function DocView:get_scrollable_size()
if not config.scroll_past_end then
return self:get_line_height() * (#self.doc.lines) + style.padding.y * 2
end
@ -123,23 +115,18 @@ end
function DocView:get_gutter_width()
local padding = style.padding.x * 2
return self:get_font():get_width(#self.doc.lines) + padding, padding
return self:get_font():get_width(#self.doc.lines) + style.padding.x * 2
end
function DocView:get_line_screen_position(line, col)
function DocView:get_line_screen_position(idx)
local x, y = self:get_content_offset()
local lh = self:get_line_height()
local gw = self:get_gutter_width()
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
return x + gw, y + (idx-1) * lh + style.padding.y
end
function DocView:get_line_text_y_offset()
local lh = self:get_line_height()
local th = self:get_font():get_height()
@ -164,14 +151,14 @@ function DocView:get_col_x_offset(line, col)
local font = style.syntax_fonts[type] or default_font
for char in common.utf8_chars(text) do
if column == col then
return xoffset
return xoffset / font:subpixel_scale()
end
xoffset = xoffset + font:get_width(char)
xoffset = xoffset + font:get_width_subpixel(char)
column = column + #char
end
end
return xoffset
return xoffset / default_font:subpixel_scale()
end
@ -180,12 +167,14 @@ function DocView:get_x_offset_col(line, x)
local xoffset, last_i, i = 0, 1, 1
local default_font = self:get_font()
local subpixel_scale = default_font:subpixel_scale()
local x_subpixel = subpixel_scale * x + subpixel_scale / 2
for _, type, text in self.doc.highlighter:each_token(line) do
local font = style.syntax_fonts[type] or default_font
for char in common.utf8_chars(text) do
local w = font:get_width(char)
if xoffset >= x then
return (xoffset - x > w / 2) and last_i or i
local w = font:get_width_subpixel(char)
if xoffset >= subpixel_scale * x then
return (xoffset - x_subpixel > w / 2) and last_i or i
end
xoffset = xoffset + w
last_i = i
@ -209,9 +198,8 @@ end
function DocView:scroll_to_line(line, ignore_if_visible, instant)
local min, max = self:get_visible_line_range()
if not (ignore_if_visible and line > min and line < max) then
local x, y = self:get_line_screen_position(line)
local ox, oy = self:get_content_offset()
self.scroll.to.y = math.max(0, y - oy - self.size.y / 2)
local lh = self:get_line_height()
self.scroll.to.y = math.max(0, lh * (line - 1) - self.size.y / 2)
if instant then
self.scroll.y = self.scroll.to.y
end
@ -220,10 +208,10 @@ end
function DocView:scroll_to_make_visible(line, col)
local ox, oy = self:get_content_offset()
local _, ly = self:get_line_screen_position(line, col)
local lh = self:get_line_height()
self.scroll.to.y = common.clamp(self.scroll.to.y, ly - oy - self.size.y + lh * 2, ly - oy - lh)
local min = self:get_line_height() * (line - 1)
local max = self:get_line_height() * (line + 2) - self.size.y
self.scroll.to.y = math.min(self.scroll.to.y, min)
self.scroll.to.y = math.max(self.scroll.to.y, max)
local gw = self:get_gutter_width()
local xoffset = self:get_col_x_offset(line, col)
local xmargin = 3 * self:get_font():get_width(' ')
@ -236,44 +224,20 @@ function DocView:scroll_to_make_visible(line, col)
end
end
function DocView:on_mouse_moved(x, y, ...)
DocView.super.on_mouse_moved(self, x, y, ...)
if self.hovered_scrollbar_track or self.dragging_scrollbar then
self.cursor = "arrow"
else
self.cursor = "ibeam"
end
if self.mouse_selecting then
local l1, c1 = self:resolve_screen_position(x, y)
local l2, c2, snap_type = table.unpack(self.mouse_selecting)
if keymap.modkeys["ctrl"] then
if l1 > l2 then l1, l2 = l2, l1 end
self.doc.selections = { }
for i = l1, l2 do
self.doc:set_selections(i - l1 + 1, i, math.min(c1, #self.doc.lines[i]), i, math.min(c2, #self.doc.lines[i]))
end
else
if snap_type then
l1, c1, l2, c2 = self:mouse_selection(self.doc, snap_type, l1, c1, l2, c2)
end
self.doc:set_selection(l1, c1, l2, c2)
end
end
end
function DocView:mouse_selection(doc, snap_type, line1, col1, line2, col2)
local function mouse_selection(doc, clicks, line1, col1, line2, col2)
local swap = line2 < line1 or line2 == line1 and col2 <= col1
if swap then
line1, col1, line2, col2 = line2, col2, line1, col1
end
if snap_type == "word" then
if clicks % 4 == 2 then
line1, col1 = translate.start_of_word(doc, line1, col1)
line2, col2 = translate.end_of_word(doc, line2, col2)
elseif snap_type == "lines" then
col1, col2 = 1, math.huge
elseif clicks % 4 == 3 then
if line2 == #doc.lines and doc.lines[#doc.lines] ~= "\n" then
doc:insert(math.huge, math.huge, "\n")
end
line1, col1, line2, col2 = line1, 1, line2 + 1, 1
end
if swap then
return line2, col2, line1, col1
@ -282,8 +246,46 @@ function DocView:mouse_selection(doc, snap_type, line1, col1, line2, col2)
end
function DocView:on_mouse_released(...)
DocView.super.on_mouse_released(self, ...)
function DocView:on_mouse_pressed(button, x, y, clicks)
local caught = DocView.super.on_mouse_pressed(self, button, x, y, clicks)
if caught then
return
end
if keymap.modkeys["shift"] then
if clicks % 2 == 1 then
local line1, col1 = select(3, self.doc:get_selection())
local line2, col2 = self:resolve_screen_position(x, y)
self.doc:set_selection(line2, col2, line1, col1)
end
else
local line, col = self:resolve_screen_position(x, y)
self.doc:set_selection(mouse_selection(self.doc, clicks, line, col, line, col))
self.mouse_selecting = { line, col, clicks = clicks }
end
core.blink_reset()
end
function DocView:on_mouse_moved(x, y, ...)
DocView.super.on_mouse_moved(self, x, y, ...)
if self:scrollbar_overlaps_point(x, y) or self.dragging_scrollbar then
self.cursor = "arrow"
else
self.cursor = "ibeam"
end
if self.mouse_selecting then
local l1, c1 = self:resolve_screen_position(x, y)
local l2, c2 = table.unpack(self.mouse_selecting)
local clicks = self.mouse_selecting.clicks
self.doc:set_selection(mouse_selection(self.doc, clicks, l1, c1, l2, c2))
end
end
function DocView:on_mouse_released(button)
DocView.super.on_mouse_released(self, button)
self.mouse_selecting = nil
end
@ -295,15 +297,13 @@ end
function DocView:update()
-- scroll to make caret visible and reset blink timer if it moved
local line1, col1, line2, col2 = self.doc:get_selection()
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
local line, col = self.doc:get_selection()
if (line ~= self.last_line or col ~= self.last_col) and self.size.x > 0 then
if core.active_view == self then
self:scroll_to_make_visible(line1, col1)
self:scroll_to_make_visible(line, col)
end
core.blink_reset()
self.last_line1, self.last_col1 = line1, col1
self.last_line2, self.last_col2 = line2, col2
self.last_line, self.last_col = line, col
end
-- update blink timer
@ -326,119 +326,94 @@ function DocView:draw_line_highlight(x, y)
end
function DocView:draw_line_text(line, x, y)
function DocView:draw_line_text(idx, x, y)
local default_font = self:get_font()
local tx, ty = x, y + self:get_line_text_y_offset()
for _, type, text in self.doc.highlighter:each_token(line) do
local subpixel_scale = default_font:subpixel_scale()
local tx, ty = subpixel_scale * x, y + self:get_line_text_y_offset()
for _, type, text in self.doc.highlighter:each_token(idx) do
local color = style.syntax[type]
local font = style.syntax_fonts[type] or default_font
tx = renderer.draw_text(font, text, tx, ty, color)
end
return self:get_line_height()
end
function DocView:draw_caret(x, y)
local lh = self:get_line_height()
renderer.draw_rect(x, y, style.caret_width, lh, style.caret)
end
function DocView:draw_line_body(line, x, y)
-- draw highlight if any selection ends on this line
local draw_highlight = false
local hcl = config.highlight_current_line
if hcl ~= false then
for lidx, line1, col1, line2, col2 in self.doc:get_selections(false) do
if line1 == line then
if hcl == "no_selection" then
if (line1 ~= line2) or (col1 ~= col2) then
draw_highlight = false
break
end
end
draw_highlight = true
break
end
if config.draw_whitespace then
tx = renderer.draw_text_subpixel(font, text, tx, ty, color, core.replacements, style.syntax.comment)
else
tx = renderer.draw_text_subpixel(font, text, tx, ty, color)
end
end
if draw_highlight and core.active_view == self then
end
function DocView:draw_line_body(idx, x, y)
local line, col = self.doc:get_selection()
-- draw selection if it overlaps this line
local line1, col1, line2, col2 = self.doc:get_selection(true)
if idx >= line1 and idx <= line2 then
local text = self.doc.lines[idx]
if line1 ~= idx then col1 = 1 end
if line2 ~= idx then col2 = #text + 1 end
local x1 = x + self:get_col_x_offset(idx, col1)
local x2 = x + self:get_col_x_offset(idx, col2)
local lh = self:get_line_height()
renderer.draw_rect(x1, y, x2 - x1, lh, style.selection)
end
-- draw line highlight if caret is on this line
if config.highlight_current_line and not self.doc:has_selection()
and line == idx and core.active_view == self then
self:draw_line_highlight(x + self.scroll.x, y)
end
-- 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
if line >= line1 and line <= line2 then
local text = self.doc.lines[line]
if line1 ~= line then col1 = 1 end
if line2 ~= line then col2 = #text + 1 end
local x1 = x + self:get_col_x_offset(line, col1)
local x2 = x + self:get_col_x_offset(line, col2)
if x1 ~= x2 then
renderer.draw_rect(x1, y, x2 - x1, lh, style.selection)
end
end
end
-- draw line's text
return self:draw_line_text(line, x, y)
self:draw_line_text(idx, x, y)
-- draw caret if it overlaps this line
local T = config.blink_period
if line == idx and core.active_view == self
and (core.blink_timer - core.blink_start) % T < T / 2
and system.window_has_focus() then
local lh = self:get_line_height()
local x1 = x + self:get_col_x_offset(line, col)
renderer.draw_rect(x1, y, style.caret_width, lh, style.caret)
end
end
function DocView:draw_line_gutter(line, x, y, width)
function DocView:draw_line_gutter(idx, x, y)
local color = style.line_number
for _, line1, _, line2 in self.doc:get_selections(true) do
if line >= line1 and line <= line2 then
color = style.line_number2
break
end
local line1, _, line2, _ = self.doc:get_selection(true)
if idx >= line1 and idx <= line2 then
color = style.line_number2
end
local yoffset = self:get_line_text_y_offset()
x = x + style.padding.x
local lh = self:get_line_height()
common.draw_text(self:get_font(), color, line, "right", x, y, width, lh)
return lh
renderer.draw_text(self:get_font(), idx, x, y + yoffset, color)
end
function DocView:draw_overlay()
if core.active_view == self then
local minline, maxline = self:get_visible_line_range()
-- draw caret if it overlaps this line
local T = config.blink_period
for _, line, col in self.doc:get_selections() do
if line >= minline and line <= maxline
and system.window_has_focus() then
if config.disable_blink
or (core.blink_timer - core.blink_start) % T < T / 2 then
self:draw_caret(self:get_line_screen_position(line, col))
end
end
end
end
end
function DocView:draw()
self:draw_background(style.background)
local _, indent_size = self.doc:get_indent_info()
self:get_font():set_tab_size(indent_size)
local font = self:get_font()
font:set_tab_size(config.indent_size)
local minline, maxline = self:get_visible_line_range()
local lh = self:get_line_height()
local x, y = self:get_line_screen_position(minline)
local gw, gpad = self:get_gutter_width()
local _, y = self:get_line_screen_position(minline)
local x = self.position.x
for i = minline, maxline do
y = y + (self:draw_line_gutter(i, self.position.x, y, gpad and gw - gpad or gw) or lh)
self:draw_line_gutter(i, x, y)
y = y + lh
end
local x, y = self:get_line_screen_position(minline)
local gw = self:get_gutter_width()
local pos = self.position
x, y = self:get_line_screen_position(minline)
-- the clip below ensure we don't write on the gutter region. On the
-- 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, self.size.y)
for i = minline, maxline do
y = y + (self:draw_line_body(i, x, y) or lh)
self:draw_line_body(i, x, y)
y = y + lh
end
self:draw_overlay()
core.pop_clip_rect()
self:draw_scrollbar()

View File

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

File diff suppressed because it is too large Load Diff

52
data/core/keymap-macos.lua Normal file → Executable file
View File

@ -6,7 +6,6 @@ local function keymap_macos(keymap)
["cmd+n"] = "core:new-doc",
["cmd+shift+c"] = "core:change-project-folder",
["cmd+shift+o"] = "core:open-project-folder",
["cmd+shift+r"] = "core:restart",
["cmd+ctrl+return"] = "core:toggle-fullscreen",
["cmd+ctrl+shift+j"] = "root:split-left",
@ -18,8 +17,7 @@ local function keymap_macos(keymap)
["cmd+ctrl+i"] = "root:switch-to-up",
["cmd+ctrl+k"] = "root:switch-to-down",
["cmd+w"] = "root:close-or-quit",
["ctrl+w"] = "root:close",
["ctrl+tab"] = "root:switch-to-next-tab",
["ctrl+shift+tab"] = "root:switch-to-previous-tab",
["cmd+pageup"] = "root:move-tab-left",
@ -33,8 +31,7 @@ local function keymap_macos(keymap)
["cmd+7"] = "root:switch-to-tab-7",
["cmd+8"] = "root:switch-to-tab-8",
["cmd+9"] = "root:switch-to-tab-9",
["wheel"] = "root:scroll",
["cmd+f"] = "find-replace:find",
["cmd+r"] = "find-replace:replace",
["f3"] = "find-replace:repeat-find",
@ -55,27 +52,23 @@ local function keymap_macos(keymap)
["shift+tab"] = "doc:unindent",
["backspace"] = "doc:backspace",
["shift+backspace"] = "doc:backspace",
["option+backspace"] = "doc:delete-to-previous-word-start",
["cmd+backspace"] = "doc:delete-to-previous-word-start",
["cmd+shift+backspace"] = "doc:delete-to-previous-word-start",
["cmd+backspace"] = "doc:delete-to-start-of-indentation",
["delete"] = "doc:delete",
["shift+delete"] = "doc:delete",
["option+delete"] = "doc:delete-to-next-word-end",
["cmd+delete"] = "doc:delete-to-next-word-end",
["cmd+shift+delete"] = "doc:delete-to-next-word-end",
["cmd+delete"] = "doc:delete-to-end-of-line",
["return"] = { "command:submit", "doc:newline", "dialog:select" },
["keypad enter"] = { "command:submit", "doc:newline", "dialog:select" },
["cmd+return"] = "doc:newline-below",
["cmd+shift+return"] = "doc:newline-above",
["cmd+j"] = "doc:join-lines",
["cmd+a"] = "doc:select-all",
["cmd+d"] = { "find-replace:select-add-next", "doc:select-word" },
["cmd+f3"] = "find-replace:select-next",
["cmd+d"] = { "find-replace:select-next", "doc:select-word" },
["cmd+l"] = "doc:select-lines",
["cmd+shift+l"] = { "find-replace:select-add-all", "doc:select-word" },
["cmd+/"] = "doc:toggle-line-comments",
["option+up"] = "doc:move-lines-up",
["option+down"] = "doc:move-lines-down",
["cmd+up"] = "doc:move-lines-up",
["cmd+down"] = "doc:move-lines-down",
["cmd+shift+d"] = "doc:duplicate-lines",
["cmd+shift+k"] = "doc:delete-lines",
@ -83,42 +76,31 @@ local function keymap_macos(keymap)
["right"] = { "doc:move-to-next-char", "dialog:next-entry"},
["up"] = { "command:select-previous", "doc:move-to-previous-line" },
["down"] = { "command:select-next", "doc:move-to-next-line" },
["option+left"] = "doc:move-to-previous-word-start",
["option+right"] = "doc:move-to-next-word-end",
["cmd+left"] = "doc:move-to-start-of-indentation",
["cmd+right"] = "doc:move-to-end-of-line",
["cmd+left"] = "doc:move-to-previous-word-start",
["cmd+right"] = "doc:move-to-next-word-end",
["cmd+["] = "doc:move-to-previous-block-start",
["cmd+]"] = "doc:move-to-next-block-end",
["home"] = "doc:move-to-start-of-indentation",
["home"] = "doc:move-to-start-of-line",
["end"] = "doc:move-to-end-of-line",
["cmd+up"] = "doc:move-to-start-of-doc",
["cmd+down"] = "doc:move-to-end-of-doc",
["cmd+home"] = "doc:move-to-start-of-doc",
["cmd+end"] = "doc:move-to-end-of-doc",
["pageup"] = "doc:move-to-previous-page",
["pagedown"] = "doc:move-to-next-page",
["shift+1lclick"] = "doc:select-to-cursor",
["ctrl+1lclick"] = "doc:split-cursor",
["1lclick"] = "doc:set-cursor",
["2lclick"] = "doc:set-cursor-word",
["3lclick"] = "doc:set-cursor-line",
["shift+left"] = "doc:select-to-previous-char",
["shift+right"] = "doc:select-to-next-char",
["shift+up"] = "doc:select-to-previous-line",
["shift+down"] = "doc:select-to-next-line",
["option+shift+left"] = "doc:select-to-previous-word-start",
["option+shift+right"] = "doc:select-to-next-word-end",
["cmd+shift+left"] = "doc:select-to-start-of-indentation",
["cmd+shift+right"] = "doc:select-to-end-of-line",
["cmd+shift+left"] = "doc:select-to-previous-word-start",
["cmd+shift+right"] = "doc:select-to-next-word-end",
["cmd+shift+["] = "doc:select-to-previous-block-start",
["cmd+shift+]"] = "doc:select-to-next-block-end",
["shift+home"] = "doc:select-to-start-of-indentation",
["shift+home"] = "doc:select-to-start-of-line",
["shift+end"] = "doc:select-to-end-of-line",
["cmd+shift+up"] = "doc:select-to-start-of-doc",
["cmd+shift+down"] = "doc:select-to-end-of-doc",
["cmd+shift+home"] = "doc:select-to-start-of-doc",
["cmd+shift+end"] = "doc:select-to-end-of-doc",
["shift+pageup"] = "doc:select-to-previous-page",
["shift+pagedown"] = "doc:select-to-next-page",
["cmd+option+up"] = "doc:create-cursor-previous-line",
["cmd+option+down"] = "doc:create-cursor-next-line"
}
end

View File

@ -1,140 +1,52 @@
local core = require "core"
local command = require "core.command"
local config = require "core.config"
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 = {}
---List of commands assigned to a shortcut been the key of the map the shortcut.
---@type 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 = {}
local macos = PLATFORM == "Mac OS X"
local os4 = PLATFORM == "AmigaOS 4"
local mos = PLATFORM == "MORPHOS"
local macos = rawget(_G, "MACOS_RESOURCES")
local os4 = false
if PLATFORM == "AmigaOS 4" then
os4 = true
end
-- 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"))
---@type table<keymap.modkey, keymap.modkey>
local modkeys_os = require("core.modkeys-" .. (macos and "macos" or os4 and "os4" or "generic"))
local modkey_map = modkeys_os.map
---@type keymap.modkey[]
local modkeys = modkeys_os.keys
---Generates a stroke sequence including currently pressed mod keys.
---@param key string
---@return string
local function key_to_stroke(key)
local function key_to_stroke(k)
local stroke = ""
for _, mk in ipairs(modkeys) do
if keymap.modkeys[mk] then
stroke = stroke .. mk .. "+"
end
end
return stroke .. key
return stroke .. k
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)
for stroke, commands in pairs(map) do
if type(commands) == "string" or type(commands) == "function" then
if type(commands) == "string" then
commands = { commands }
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
for _, cmd in ipairs(commands) do
keymap.reverse_map[cmd] = keymap.reverse_map[cmd] or {}
table.insert(keymap.reverse_map[cmd], stroke)
keymap.reverse_map[cmd] = stroke
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)
remove_duplicates(map)
for stroke, commands in pairs(map) do
if macos then
stroke = stroke:gsub("%f[%a]ctrl%f[%A]", "cmd")
end
if type(commands) == "string" then
commands = { commands }
end
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
else
keymap.map[stroke] = keymap.map[stroke] or {}
@ -143,42 +55,18 @@ function keymap.add(map, overwrite)
end
end
for _, cmd in ipairs(commands) do
keymap.reverse_map[cmd] = keymap.reverse_map[cmd] or {}
table.insert(keymap.reverse_map[cmd], stroke)
keymap.reverse_map[cmd] = stroke
end
end
end
---Unregisters the given shortcut and associated command.
---@param shortcut string
---@param cmd string
function keymap.unbind(shortcut, cmd)
remove_only(keymap.map, shortcut, cmd)
remove_only(keymap.reverse_map, cmd, shortcut)
end
---Returns all the shortcuts associated to a command unpacked for easy assignment.
---@param cmd string
---@return ...
function keymap.get_binding(cmd)
return table.unpack(keymap.reverse_map[cmd] or {})
end
---Returns all the shortcuts associated to a command packed in a table.
---@param cmd string
---@return table<integer, string> | nil shortcuts
function keymap.get_bindings(cmd)
return keymap.reverse_map[cmd]
end
--------------------------------------------------------------------------------
-- Events listening
--------------------------------------------------------------------------------
function keymap.on_key_pressed(k, ...)
function keymap.on_key_pressed(k)
local mk = modkey_map[k]
if mk then
keymap.modkeys[mk] = true
@ -188,39 +76,18 @@ function keymap.on_key_pressed(k, ...)
end
else
local stroke = key_to_stroke(k)
local commands, performed = keymap.map[stroke], false
local commands = keymap.map[stroke]
if commands then
for _, cmd in ipairs(commands) do
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
local performed = command.perform(cmd)
if performed then break end
end
return performed
return true
end
end
return false
end
function keymap.on_mouse_wheel(delta, ...)
return not (keymap.on_key_pressed("wheel" .. (delta > 0 and "up" or "down"), delta, ...)
or keymap.on_key_pressed("wheel", delta, ...))
end
function keymap.on_mouse_pressed(button, x, y, clicks)
local click_number = (((clicks - 1) % config.max_clicks) + 1)
return not (keymap.on_key_pressed(click_number .. button:sub(1,1) .. "click", x, y, clicks) or
keymap.on_key_pressed(button:sub(1,1) .. "click", x, y, clicks) or
keymap.on_key_pressed(click_number .. "click", x, y, clicks) or
keymap.on_key_pressed("click", x, y, clicks))
end
function keymap.on_key_released(k)
local mk = modkey_map[k]
@ -230,9 +97,6 @@ function keymap.on_key_released(k)
end
--------------------------------------------------------------------------------
-- Register default bindings
--------------------------------------------------------------------------------
if macos then
local keymap_macos = require("core.keymap-macos")
keymap_macos(keymap)
@ -246,9 +110,7 @@ keymap.add_direct {
["ctrl+n"] = "core:new-doc",
["ctrl+shift+c"] = "core:change-project-folder",
["ctrl+shift+o"] = "core:open-project-folder",
["ctrl+alt+r"] = "core:restart",
["alt+return"] = "core:toggle-fullscreen",
["f11"] = "core:toggle-fullscreen",
["alt+shift+j"] = "root:split-left",
["alt+shift+l"] = "root:split-right",
@ -273,14 +135,11 @@ keymap.add_direct {
["alt+7"] = "root:switch-to-tab-7",
["alt+8"] = "root:switch-to-tab-8",
["alt+9"] = "root:switch-to-tab-9",
["wheel"] = "root:scroll",
["ctrl+f"] = "find-replace:find",
["ctrl+r"] = "find-replace:replace",
["f3"] = "find-replace:repeat-find",
["shift+f3"] = "find-replace:previous-find",
["ctrl+i"] = "find-replace:toggle-sensitivity",
["ctrl+shift+i"] = "find-replace:toggle-regex",
["ctrl+g"] = "doc:go-to-line",
["ctrl+s"] = "doc:save",
["ctrl+shift+s"] = "doc:save-as",
@ -309,13 +168,9 @@ keymap.add_direct {
["ctrl+shift+return"] = "doc:newline-above",
["ctrl+j"] = "doc:join-lines",
["ctrl+a"] = "doc:select-all",
["ctrl+d"] = { "find-replace:select-add-next", "doc:select-word" },
["ctrl+f3"] = "find-replace:select-next",
["ctrl+shift+f3"] = "find-replace:select-previous",
["ctrl+d"] = { "find-replace:select-next", "doc:select-word" },
["ctrl+l"] = "doc:select-lines",
["ctrl+shift+l"] = { "find-replace:select-add-all", "doc:select-word" },
["ctrl+/"] = "doc:toggle-line-comments",
["ctrl+shift+/"] = "doc:toggle-block-comments",
["ctrl+up"] = "doc:move-lines-up",
["ctrl+down"] = "doc:move-lines-down",
["ctrl+shift+d"] = "doc:duplicate-lines",
@ -329,18 +184,13 @@ keymap.add_direct {
["ctrl+right"] = "doc:move-to-next-word-end",
["ctrl+["] = "doc:move-to-previous-block-start",
["ctrl+]"] = "doc:move-to-next-block-end",
["home"] = "doc:move-to-start-of-indentation",
["home"] = "doc:move-to-start-of-line",
["end"] = "doc:move-to-end-of-line",
["ctrl+home"] = "doc:move-to-start-of-doc",
["ctrl+end"] = "doc:move-to-end-of-doc",
["pageup"] = "doc:move-to-previous-page",
["pagedown"] = "doc:move-to-next-page",
["shift+1lclick"] = "doc:select-to-cursor",
["ctrl+1lclick"] = "doc:split-cursor",
["1lclick"] = "doc:set-cursor",
["2lclick"] = "doc:set-cursor-word",
["3lclick"] = "doc:set-cursor-line",
["shift+left"] = "doc:select-to-previous-char",
["shift+right"] = "doc:select-to-next-char",
["shift+up"] = "doc:select-to-previous-line",
@ -349,15 +199,12 @@ keymap.add_direct {
["ctrl+shift+right"] = "doc:select-to-next-word-end",
["ctrl+shift+["] = "doc:select-to-previous-block-start",
["ctrl+shift+]"] = "doc:select-to-next-block-end",
["shift+home"] = "doc:select-to-start-of-indentation",
["shift+home"] = "doc:select-to-start-of-line",
["shift+end"] = "doc:select-to-end-of-line",
["ctrl+shift+home"] = "doc:select-to-start-of-doc",
["ctrl+shift+end"] = "doc:select-to-end-of-doc",
["shift+pageup"] = "doc:select-to-previous-page",
["shift+pagedown"] = "doc:select-to-next-page",
["ctrl+shift+up"] = "doc:create-cursor-previous-line",
["ctrl+shift+down"] = "doc:create-cursor-next-line"
}
return keymap

184
data/core/logview.lua Normal file → Executable file
View File

@ -1,52 +1,16 @@
local core = require "core"
local common = require "core.common"
local config = require "core.config"
local keymap = require "core.keymap"
local style = require "core.style"
local View = require "core.view"
local function lines(text)
if text == "" then return 0 end
local l = 1
for _ in string.gmatch(text, "\n") do
l = l + 1
end
return l
end
local item_height_result = {}
local function get_item_height(item)
local h = item_height_result[item]
if not h then
h = {}
local l = 1 + lines(item.text) + lines(item.info or "")
h.normal = style.font:get_height() + style.padding.y
h.expanded = l * style.font:get_height() + style.padding.y
h.current = h.normal
h.target = h.current
item_height_result[item] = h
end
return h
end
local LogView = View:extend()
LogView.context = "session"
function LogView:new()
LogView.super.new(self)
self.last_item = core.log_items[#core.log_items]
self.expanding = {}
self.scrollable = true
self.yoffset = 0
core.status_view:show_message("i", style.text, "ctrl+click to copy entry")
end
@ -55,73 +19,6 @@ function LogView:get_name()
end
local function is_expanded(item)
local item_height = get_item_height(item)
return item_height.target == item_height.expanded
end
function LogView:expand_item(item)
item = get_item_height(item)
item.target = item.target == item.expanded and item.normal or item.expanded
table.insert(self.expanding, item)
end
function LogView:each_item()
local x, y = self:get_content_offset()
y = y + style.padding.y + self.yoffset
return coroutine.wrap(function()
for i = #core.log_items, 1, -1 do
local item = core.log_items[i]
local h = get_item_height(item).current
coroutine.yield(i, item, x, y, self.size.x, h)
y = y + h
end
end)
end
function LogView:get_scrollable_size()
local _, y_off = self:get_content_offset()
local last_y, last_h = 0, 0
for i, item, x, y, w, h in self:each_item() do
last_y, last_h = y, h
end
if not config.scroll_past_end then
return last_y + last_h - y_off + style.padding.y
end
return last_y + self.size.y - y_off
end
function LogView:on_mouse_pressed(button, px, py, clicks)
if LogView.super.on_mouse_pressed(self, button, px, py, clicks) then
return true
end
local index, selected
for i, item, x, y, w, h in self:each_item() do
if px >= x and py >= y and px < x + w and py < y + h then
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
function LogView:update()
local item = core.log_items[#core.log_items]
if self.last_item ~= item then
@ -130,15 +27,7 @@ function LogView:update()
self.yoffset = -(style.font:get_height() + style.padding.y)
end
local expanding = self.expanding[1]
if expanding then
self:move_towards(expanding, "current", expanding.target, nil, "logview")
if expanding.current == expanding.target then
table.remove(self.expanding, 1)
end
end
self:move_towards("yoffset", 0, nil, "logview")
self:move_towards("yoffset", 0)
LogView.super.update(self)
end
@ -146,70 +35,39 @@ end
local function draw_text_multiline(font, text, x, y, color)
local th = font:get_height()
local resx = x
local resx, resy = x, y
for line in text:gmatch("[^\n]+") do
resy = y
resx = renderer.draw_text(style.font, line, x, y, color)
y = y + th
end
return resx, y
return resx, resy
end
-- this is just to get a date string that's consistent
local datestr = os.date()
function LogView:draw()
self:draw_background(style.background)
local ox, oy = self:get_content_offset()
local th = style.font:get_height()
local lh = th + style.padding.y -- for one line
local iw = math.max(
style.icon_font:get_width(style.log.ERROR.icon),
style.icon_font:get_width(style.log.INFO.icon)
)
local y = oy + style.padding.y + self.yoffset
local tw = style.font:get_width(datestr)
for _, item, x, y, w, h in self:each_item() do
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.icon_font,
style.log[item.level].color,
style.log[item.level].icon,
"center",
x, y, iw, lh
)
x = x + style.padding.x
-- timestamps are always 15% of the width
local time = os.date(nil, item.time)
common.draw_text(style.font, style.dim, time, "left", x, y, tw, lh)
x = x + tw + style.padding.x
w = w - (x - self:get_content_offset())
if is_expanded(item) then
y = y + common.round(style.padding.y / 2)
_, y = draw_text_multiline(style.font, item.text, x, y, style.text)
local at = "at " .. common.home_encode(item.at)
_, y = common.draw_text(style.font, style.dim, at, "left", x, y, w, lh)
if item.info then
_, y = draw_text_multiline(style.font, item.info, x, y, style.dim)
end
else
local line, has_newline = string.match(item.text, "([^\n]+)(\n?)")
if has_newline ~= "" then
line = line .. " ..."
end
_, y = common.draw_text(style.font, style.text, line, "left", x, y, w, lh)
end
core.pop_clip_rect()
for i = #core.log_items, 1, -1 do
local x = ox + style.padding.x
local item = core.log_items[i]
local time = os.date(nil, item.time)
x = renderer.draw_text(style.font, time, x, y, style.dim)
x = x + style.padding.x
local subx = x
x, y = draw_text_multiline(style.font, item.text, x, y, style.text)
renderer.draw_text(style.font, " at " .. item.at, x, y, style.dim)
y = y + th
if item.info then
subx, y = draw_text_multiline(style.font, item.info, subx, y, style.dim)
y = y + th
end
y = y + style.padding.y
end
LogView.super.draw_scrollbar(self)
end

0
data/core/modkeys-generic.lua Normal file → Executable file
View File

0
data/core/modkeys-macos.lua Normal file → Executable file
View File

View File

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

172
data/core/nagview.lua Normal file → Executable file
View File

@ -11,19 +11,13 @@ local UNDERLINE_MARGIN = common.round(1 * SCALE)
local noop = function() end
---@class core.nagview : core.view
---@field super core.view
local NagView = View:extend()
function NagView:new()
NagView.super.new(self)
self.size.y = 0
self.show_height = 0
self.force_focus = false
self.queue = {}
self.scrollable = true
self.target_height = 0
self.on_mouse_pressed_root = nil
end
function NagView:get_title()
@ -52,20 +46,20 @@ function NagView:get_target_height()
return self.target_height + 2 * style.padding.y
end
function NagView:get_scrollable_size()
local w, h = system.get_window_size()
if self.visible and self:get_target_height() > h then
self.size.y = h
return self:get_target_height()
function NagView:update()
NagView.super.update(self)
if core.active_view == self and self.title then
self:move_towards(self.size, "y", self:get_target_height())
self:move_towards(self, "underline_progress", 1)
else
self.size.y = 0
self:move_towards(self.size, "y", 0)
end
return 0
end
function NagView:dim_window_content()
function NagView:draw_overlay()
local ox, oy = self:get_content_offset()
oy = oy + self.show_height
oy = oy + self.size.y
local w, h = core.root_view.size.x, core.root_view.size.y - oy
core.root_view:defer_draw(function()
renderer.draw_rect(ox, oy, w, h, style.nagbar_dim)
@ -87,7 +81,7 @@ function NagView:each_option()
bh = self:get_buttons_height()
ox,oy = self:get_content_offset()
ox = ox + self.size.x
oy = oy + self.show_height - bh - style.padding.y
oy = oy + self.size.y - bh - style.padding.y
for i = #self.options, 1, -1 do
opt = self.options[i]
@ -100,8 +94,6 @@ function NagView:each_option()
end
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, ...)
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
@ -111,55 +103,18 @@ function NagView:on_mouse_moved(mx, my, ...)
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)
if not self.visible then return false end
if NagView.super.on_mouse_pressed(self, button, mx, my, clicks) then return true end
if NagView.super.on_mouse_pressed(self, button, mx, my, clicks) then return end
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
self:change_hovered(i)
command.perform "dialog:select"
break
end
end
return true
end
function NagView:on_text_input(text)
if not self.visible then return end
if text:lower() == "y" then
command.perform "dialog:select-yes"
elseif text:lower() == "n" then
@ -167,39 +122,20 @@ function NagView:on_text_input(text)
end
end
function NagView:update()
if not self.visible and self.show_height <= 0 then return end
NagView.super.update(self)
if self.visible and core.active_view == self and self.title then
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
function NagView:draw()
if self.size.y <= 0 or not self.title then return end
local function draw_nagview_message(self)
self:dim_window_content()
self:draw_overlay()
self:draw_background(style.nagbar)
-- draw message's background
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
core.push_clip_rect(ox, oy, self.size.x, self.show_height)
-- if there are other items, show it
if #self.queue > 0 then
local str = string.format("[%d]", #self.queue)
ox = common.draw_text(style.font, style.nagbar_text, str, "left", ox, oy, self.size.x, self.show_height)
ox = common.draw_text(style.font, style.nagbar_text, str, "left", ox, oy, self.size.x, self.size.y)
ox = ox + style.padding.x
end
@ -232,17 +168,12 @@ local function draw_nagview_message(self)
common.draw_text(opt.font, style.nagbar_text, opt.text, "center", fx,fy,fw,fh)
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
local function findindex(tbl, prop)
for i, o in ipairs(tbl) do
if o[prop] then return i end
end
core.root_view:defer_draw(draw_nagview_message, self)
end
function NagView:get_message_height()
@ -253,31 +184,22 @@ function NagView:get_message_height()
return h
end
function NagView:next()
local opts = table.remove(self.queue, 1) or {}
if opts.title and opts.message and opts.options then
self.visible = true
self.title = opts.title
self.message = opts.message and opts.message .. "\n"
self.options = opts.options
self.on_selected = opts.on_selected
self.title = opts.title
self.message = opts.message and opts.message .. "\n"
self.options = opts.options
self.on_selected = opts.on_selected
if self.message and self.options then
local message_height = self:get_message_height()
-- self.target_height is the nagview height needed to display the message and
-- the buttons, excluding the top and bottom padding space.
self.target_height = math.max(message_height, self:get_buttons_height())
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)
self:change_hovered(findindex(self.options, "default_yes"))
end
self.force_focus = self.message ~= nil
core.set_active_view(self.message ~= nil and self or core.last_active_view)
end
function NagView:show(title, message, options, on_select)
@ -287,7 +209,39 @@ function NagView:show(title, message, options, on_select)
opts.options = assert(options, "No options")
opts.on_selected = on_select or noop
table.insert(self.queue, opts)
self:next()
if #self.queue > 0 and not self.title then self:next() end
end
return NagView
command.add(NagView, {
["dialog:previous-entry"] = function()
local v = core.active_view
local hover = v.hovered_item or 1
v:change_hovered(hover == 1 and #v.options or hover - 1)
end,
["dialog:next-entry"] = function()
local v = core.active_view
local hover = v.hovered_item or 1
v:change_hovered(hover == #v.options and 1 or hover + 1)
end,
["dialog:select-yes"] = function()
local v = core.active_view
if v ~= core.nag_view then return end
v:change_hovered(findindex(v.options, "default_yes"))
command.perform "dialog:select"
end,
["dialog:select-no"] = function()
local v = core.active_view
if v ~= core.nag_view then return end
v:change_hovered(findindex(v.options, "default_no"))
command.perform "dialog:select"
end,
["dialog:select"] = function()
local v = core.active_view
if v.hovered_item then
v.on_selected(v.options[v.hovered_item])
v:next()
end
end,
})
return NagView

View File

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

35
data/core/object.lua Normal file → Executable file
View File

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

View File

@ -1,71 +0,0 @@
-- So that in addition to regex.gsub(pattern, string), we can also do
-- pattern:gsub(string).
regex.__index = function(table, key) return regex[key]; end
regex.match = function(pattern_string, string, offset, options)
local pattern = type(pattern_string) == "table" and
pattern_string or regex.compile(pattern_string)
local res = { regex.cmatch(pattern, string, offset or 1, options or 0) }
res[2] = res[2] and res[2] - 1
return table.unpack(res)
end
-- Will iterate back through any UTF-8 bytes so that we don't replace bits
-- mid character.
local function previous_character(str, index)
local byte
repeat
index = index - 1
byte = string.byte(str, index)
until byte < 128 or byte >= 192
return index
end
-- Moves to the end of the identified character.
local function end_character(str, index)
local byte = string.byte(str, index + 1)
while byte and byte >= 128 and byte < 192 do
index = index + 1
byte = string.byte(str, index + 1)
end
return index
end
-- Build off matching. For now, only support basic replacements, but capture
-- groupings should be doable. We can even have custom group replacements and
-- transformations and stuff in lua. Currently, this takes group replacements
-- as \1 - \9.
-- Should work on UTF-8 text.
regex.gsub = function(pattern_string, str, replacement)
local pattern = type(pattern_string) == "table" and
pattern_string or regex.compile(pattern_string)
local result, indices = ""
local matches, replacements = {}, {}
repeat
indices = { regex.cmatch(pattern, str) }
if #indices > 0 then
table.insert(matches, indices)
local currentReplacement = replacement
if #indices > 2 then
for i = 1, (#indices/2 - 1) do
currentReplacement = string.gsub(
currentReplacement,
"\\" .. i,
str:sub(indices[i*2+1], end_character(str,indices[i*2+2]-1))
)
end
end
currentReplacement = string.gsub(currentReplacement, "\\%d", "")
table.insert(replacements, { indices[1], #currentReplacement+indices[1] })
if indices[1] > 1 then
result = result ..
str:sub(1, previous_character(str, indices[1])) .. currentReplacement
else
result = result .. currentReplacement
end
str = str:sub(indices[2])
end
until #indices == 0 or indices[1] == indices[2]
return result .. str, matches, replacements
end

999
data/core/rootview.lua Normal file → Executable file

File diff suppressed because it is too large Load Diff

46
data/core/start.lua Normal file → Executable file
View File

@ -1,8 +1,8 @@
-- this file is used by lite-xl to setup the Lua environment when starting
VERSION = "2.1.0r1"
MOD_VERSION = "3"
VERSION = "1.16.12"
MOD_VERSION = "1"
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 SCALE
PATHSEP = package.config:sub(1, 1)
EXEDIR = EXEFILE:match("^(.+)[/\\][^/\\]+$")
@ -12,46 +12,10 @@ else
local prefix = EXEDIR:match("^(.+)[/\\]bin$")
DATADIR = prefix and (prefix .. '/share/lite-xl') or (EXEDIR .. '/data')
end
USERDIR = (system.get_file_info(EXEDIR .. '/user') and (EXEDIR .. '/user'))
or ((os.getenv("XDG_CONFIG_HOME") and os.getenv("XDG_CONFIG_HOME") .. "/lite-xl"))
or (HOME and (HOME .. '/.config/lite-xl'))
USERDIR = os.getenv("XDG_CONFIG_HOME") or (HOME and (HOME .. '/.config/lite-xl') or (EXEDIR .. '/user'))
package.path = DATADIR .. '/?.lua;'
package.path = DATADIR .. '/?.lua;' .. package.path
package.path = DATADIR .. '/?/init.lua;' .. package.path
package.path = USERDIR .. '/?.lua;' .. package.path
package.path = USERDIR .. '/?/init.lua;' .. package.path
local suffix = PLATFORM == "Mac OS X" and 'lib' or (PLATFORM == "Windows" and 'dll' or 'so')
package.cpath =
USERDIR .. '/?.' .. ARCH .. "." .. suffix .. ";" ..
USERDIR .. '/?/init.' .. ARCH .. "." .. suffix .. ";" ..
USERDIR .. '/?.' .. suffix .. ";" ..
USERDIR .. '/?/init.' .. suffix .. ";" ..
DATADIR .. '/?.' .. ARCH .. "." .. suffix .. ";" ..
DATADIR .. '/?/init.' .. ARCH .. "." .. suffix .. ";" ..
DATADIR .. '/?.' .. suffix .. ";" ..
DATADIR .. '/?/init.' .. suffix .. ";"
package.native_plugins = {}
package.searchers = { package.searchers[1], package.searchers[2], function(modname)
local path = package.searchpath(modname, package.cpath)
if not path then return nil end
return system.load_native_plugin, path
end }
table.pack = table.pack or pack or function(...) return {...} end
table.unpack = table.unpack or unpack
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

1144
data/core/statusview.lua Normal file → Executable file

File diff suppressed because it is too large Load Diff

0
data/core/strict.lua Normal file → Executable file
View File

42
data/core/style.lua Normal file → Executable file
View File

@ -4,7 +4,6 @@ local style = {}
style.padding = { x = common.round(14 * SCALE), y = common.round(7 * SCALE) }
style.divider_size = common.round(1 * SCALE)
style.scrollbar_size = common.round(4 * SCALE)
style.expanded_scrollbar_size = common.round(12 * SCALE)
style.caret_width = common.round(2 * SCALE)
style.tab_width = common.round(170 * SCALE)
@ -22,13 +21,41 @@ style.tab_width = common.round(170 * SCALE)
--
-- On High DPI monitor or non RGB monitor you may consider using antialiasing grayscale instead.
-- The antialiasing grayscale with full hinting is interesting for crisp font rendering.
style.font = renderer.font.load(DATADIR .. "/fonts/FiraSans-Regular.ttf", 15 * SCALE)
style.big_font = style.font:copy(46 * SCALE)
style.icon_font = renderer.font.load(DATADIR .. "/fonts/icons.ttf", 16 * SCALE, {antialiasing="grayscale", hinting="full"})
style.icon_big_font = style.icon_font:copy(23 * SCALE)
style.code_font = renderer.font.load(DATADIR .. "/fonts/JetBrainsMono-Regular.ttf", 15 * SCALE)
style.font = renderer.font.load(DATADIR .. "/fonts/FiraSans-Regular.ttf", 13 * SCALE)
style.big_font = renderer.font.load(DATADIR .. "/fonts/FiraSans-Regular.ttf", 40 * SCALE)
style.icon_font = renderer.font.load(DATADIR .. "/fonts/icons.ttf", 14 * SCALE, {antialiasing="grayscale", hinting="full"})
style.icon_big_font = renderer.font.load(DATADIR .. "/fonts/icons.ttf", 20 * SCALE, {antialiasing="grayscale", hinting="full"})
style.code_font = renderer.font.load(DATADIR .. "/fonts/JetBrainsMono-Regular.ttf", 13 * SCALE)
style.background = { common.color "#2e2e32" }
style.background2 = { common.color "#252529" }
style.background3 = { common.color "#252529" }
style.text = { common.color "#97979c" }
style.caret = { common.color "#93DDFA" }
style.accent = { common.color "#e1e1e6" }
style.dim = { common.color "#525257" }
style.divider = { common.color "#202024" }
style.selection = { common.color "#48484f" }
style.line_number = { common.color "#525259" }
style.line_number2 = { common.color "#83838f" }
style.line_highlight = { common.color "#343438" }
style.scrollbar = { common.color "#414146" }
style.scrollbar2 = { common.color "#4b4b52" }
style.nagbar = { common.color "#FF0000" }
style.nagbar_text = { common.color "#FFFFFF" }
style.nagbar_dim = { common.color "rgba(0, 0, 0, 0.45)" }
style.syntax = {}
style.syntax["normal"] = { common.color "#e1e1e6" }
style.syntax["symbol"] = { common.color "#e1e1e6" }
style.syntax["comment"] = { common.color "#676b6f" }
style.syntax["keyword"] = { common.color "#E58AC9" }
style.syntax["keyword2"] = { common.color "#F77483" }
style.syntax["number"] = { common.color "#FFA94D" }
style.syntax["literal"] = { common.color "#FFA94D" }
style.syntax["string"] = { common.color "#f7c95c" }
style.syntax["operator"] = { common.color "#93DDFA" }
style.syntax["function"] = { common.color "#93DDFA" }
-- This can be used to override fonts per syntax group.
-- The syntax highlighter will take existing values from this table and
@ -37,7 +64,4 @@ style.syntax = {}
style.syntax_fonts = {}
-- style.syntax_fonts["comment"] = renderer.font.load(path_to_font, size_of_font, rendering_options)
style.log = {}
return style

22
data/core/syntax.lua Normal file → Executable file
View File

@ -3,28 +3,10 @@ local common = require "core.common"
local syntax = {}
syntax.items = {}
local plain_text_syntax = { name = "Plain Text", patterns = {}, symbols = {} }
local plain_text_syntax = { patterns = {}, symbols = {} }
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)
end
@ -40,7 +22,7 @@ end
function syntax.get(filename, header)
return find(filename, "files")
or (header and find(header, "headers"))
or find(header, "headers")
or plain_text_syntax
end

2
data/core/titleview.lua Normal file → Executable file
View File

@ -17,8 +17,6 @@ local title_commands = {
{symbol = "X", action = function() core.quit() end},
}
---@class core.titleview : core.view
---@field super core.view
local TitleView = View:extend()
local function title_view_height()

325
data/core/tokenizer.lua Normal file → Executable file
View File

@ -1,15 +1,11 @@
local core = require "core"
local syntax = require "core.syntax"
local common = require "core.common"
local tokenizer = {}
local bad_patterns = {}
local function push_token(t, type, text)
type = type or "normal"
local prev_type = t[#t-1]
local prev_text = t[#t]
if prev_type and (prev_type == type or prev_text:ufind("^%s*$")) then
if prev_type and (prev_type == type or prev_text:find("^%s*$")) then
t[#t-1] = type
t[#t] = prev_text .. text
else
@ -41,26 +37,49 @@ local function push_tokens(t, syn, pattern, full_text, find_results)
local fin = find_results[i + 1] - 1
local type = pattern.type[i - 2]
-- ↑ (i - 2) to convert from [3; n] to [1; n]
local text = full_text:usub(start, fin)
local text = full_text:sub(start, fin)
push_token(t, syn.symbols[text] or type, text)
end
else
local start, fin = find_results[1], find_results[2]
local text = full_text:usub(start, fin)
local text = full_text:sub(start, fin)
push_token(t, syn.symbols[text] or pattern.type, text)
end
end
local function is_escaped(text, idx, esc)
local byte = esc:byte()
local count = 0
for i = idx - 1, 1, -1 do
if text:byte(i) ~= byte then break end
count = count + 1
end
return count % 2 == 1
end
local function find_non_escaped(text, pattern, offset, esc)
while true do
local s, e = text:find(pattern, offset)
if not s then break end
if esc and is_escaped(text, s, esc) then
offset = e + 1
else
return s, e
end
end
end
-- 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.
-- 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
-- 1 integer in it. Calling `push_subsyntax` increases the stack depth. Calling
-- `pop_subsyntax` decreases it. The integers represent the index of a pattern
-- that we're following in the syntax. The top of the stack can be any valid
-- that we're following in the syntax. The top of the stack can be any valid
-- pattern index, any integer lower in the stack must represent a pattern that
-- specifies a subsyntax.
@ -95,19 +114,6 @@ local function retrieve_syntax_state(incoming_syntax, state)
return current_syntax, subsyntax_info, current_pattern_idx, current_level
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)
local res = {}
local i = 1
@ -118,22 +124,22 @@ function tokenizer.tokenize(incoming_syntax, text, state)
state = state or 0
-- 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.
-- subsyntax_info : info about the delimiters of this subsyntax.
-- current_pattern_idx: the index of the pattern we're on for this syntax.
-- current_level : how many subsyntaxes deep we are.
local current_syntax, subsyntax_info, current_pattern_idx, current_level =
retrieve_syntax_state(incoming_syntax, state)
-- Should be used to set the state variable. Don't modify it directly.
local function set_subsyntax_pattern_idx(pattern_idx)
current_pattern_idx = pattern_idx
state = bit32.replace(state, pattern_idx, current_level*8, 8)
end
local function push_subsyntax(entering_syntax, pattern_idx)
set_subsyntax_pattern_idx(pattern_idx)
current_level = current_level + 1
@ -142,182 +148,109 @@ function tokenizer.tokenize(incoming_syntax, text, state)
entering_syntax.syntax or syntax.get(entering_syntax.syntax)
current_pattern_idx = 0
end
local function pop_subsyntax()
set_subsyntax_pattern_idx(0)
current_level = current_level - 1
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)
end
local function find_text(text, p, offset, at_start, close)
local target, res = p.pattern or p.regex, { 1, offset - 1 }
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
p._regex = p._regex or regex.compile(p.regex)
code = p._regex
end
repeat
local next = res[2] + 1
-- If the pattern contained '^', allow matching only the whole line
if p.whole_line[p_idx] and next > 1 then
return
end
res = p.pattern and { text:ufind((at_start or p.whole_line[p_idx]) and "^" .. code or code, next) }
or { regex.match(code, text, text:ucharpos(next), (at_start or p.whole_line[p_idx]) and regex.ANCHORED or 0) }
if p.regex and #res > 0 then -- set correct utf8 len for regex result
local char_pos_1 = string.ulen(text:sub(1, res[1]))
local char_pos_2 = char_pos_1 + string.ulen(text:sub(res[1], res[2])) - 1
-- `regex.match` returns group results as a series of `begin, end`
-- we only want `begin`s
if #res >= 3 then
res[3] = char_pos_1 + string.ulen(text:sub(res[1], res[3])) - 1
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,
-- and if it is not itself escaped.
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
until not res[1] or not close or not target[3]
return table.unpack(res)
end
local text_len = text:ulen()
if text_len ~= nil then
while i <= text_len do
-- continue trying to match the end pattern of a pair if we have a state set
if current_pattern_idx > 0 then
local p = current_syntax.patterns[current_pattern_idx]
local s, e = find_text(text, p, i, false, true)
local cont = true
-- If we're in subsyntax mode, always check to see if we end our syntax
-- first, before the found delimeter, as ending the subsyntax takes
-- precedence over ending the delimiter in the subsyntax.
if subsyntax_info then
local ss, se = find_text(text, subsyntax_info, i, false, true)
-- If we find that we end the subsyntax before the
-- delimiter, push the token, and signal we shouldn't
-- treat the bit after as a token to be normally parsed
-- (as it's the syntax delimiter).
if ss and (s == nil or ss < s) then
push_token(res, p.type, text:usub(i, ss - 1))
i = ss
cont = false
end
end
-- If we don't have any concerns about syntax delimiters,
-- continue on as normal.
if cont then
if s then
push_token(res, p.type, text:usub(i, e))
set_subsyntax_pattern_idx(0)
i = e + 1
else
push_token(res, p.type, text:usub(i))
break
end
end
end
-- General end of syntax check. Applies in the case where
-- we're ending early in the middle of a delimiter, or
-- just normally, upon finding a token.
if subsyntax_info then
local s, e = find_text(text, subsyntax_info, i, true, true)
if s then
push_token(res, subsyntax_info.type, text:usub(i, e))
-- On finding unescaped delimiter, pop it.
pop_subsyntax()
i = e + 1
end
end
-- find matching pattern
local matched = false
for n, p in ipairs(current_syntax.patterns) do
local find_results = { find_text(text, p, i, true, false) }
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
push_tokens(res, current_syntax, p, text, find_results)
-- update state if this was a start|end pattern pair
if type(p.pattern or p.regex) == "table" then
-- If we have a subsyntax, push that onto the subsyntax stack.
if p.syntax then
push_subsyntax(p, n)
else
set_subsyntax_pattern_idx(n)
end
end
-- move cursor past this token
i = find_results[2] + 1
matched = true
break
end
end
-- consume character if we didn't match
if not matched then
push_token(res, "normal", text:usub(i, i))
i = i + 1
end
end
end
while i <= #text do
-- continue trying to match the end pattern of a pair if we have a state set
if current_pattern_idx > 0 then
local p = current_syntax.patterns[current_pattern_idx]
local s, e = find_non_escaped(text, p.pattern[2], i, p.pattern[3])
local cont = true
-- If we're in subsyntax mode, always check to see if we end our syntax
-- first, before the found delimeter, as ending the subsyntax takes
-- precedence over ending the delimiter in the subsyntax.
if subsyntax_info then
local ss, se = find_non_escaped(
text,
subsyntax_info.pattern[2],
i,
subsyntax_info.pattern[3]
)
-- If we find that we end the subsyntax before the
-- delimiter, push the token, and signal we shouldn't
-- treat the bit after as a token to be normally parsed
-- (as it's the syntax delimiter).
if ss and (s == nil or ss < s) then
push_token(res, p.type, text:sub(i, ss - 1))
i = ss
cont = false
end
end
-- If we don't have any concerns about syntax delimiters,
-- continue on as normal.
if cont then
if s then
push_token(res, p.type, text:sub(i, e))
set_subsyntax_pattern_idx(0)
i = e + 1
else
push_token(res, p.type, text:sub(i))
break
end
end
end
-- General end of syntax check. Applies in the case where
-- we're ending early in the middle of a delimiter, or
-- just normally, upon finding a token.
if subsyntax_info then
local s, e = find_non_escaped(
text,
"^" .. subsyntax_info.pattern[2],
i,
nil
)
if s then
push_token(res, subsyntax_info.type, text:sub(i, e))
-- On finding unescaped delimiter, pop it.
pop_subsyntax()
i = e + 1
end
end
-- find matching pattern
local matched = false
for n, p in ipairs(current_syntax.patterns) do
local pattern = (type(p.pattern) == "table") and p.pattern[1] or p.pattern
local find_results = { text:find("^" .. pattern, i) }
local start, fin = find_results[1], find_results[2]
if start then
-- matched pattern; make and add tokens
push_tokens(res, current_syntax, p, text, find_results)
-- update state if this was a start|end pattern pair
if type(p.pattern) == "table" then
-- If we have a subsyntax, push that onto the subsyntax stack.
if p.syntax then
push_subsyntax(p, n)
else
set_subsyntax_pattern_idx(n)
end
end
-- move cursor past this token
i = fin + 1
matched = true
break
end
end
-- consume character if we didn't match
if not matched then
push_token(res, "normal", text:sub(i, i))
i = i + 1
end
end
return res, state
end

View File

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

222
data/core/view.lua Normal file → Executable file
View File

@ -4,57 +4,9 @@ local style = require "core.style"
local common = require "core.common"
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()
-- context can be "application" or "session". The instance of objects
-- with context "session" will be closed when a project session is
-- terminated. The context "application" is for functional UI elements.
View.context = "application"
function View:new()
self.position = { x = 0, y = 0 }
@ -62,22 +14,14 @@ function View:new()
self.scroll = { x = 0, y = 0, to = { x = 0, y = 0 } }
self.cursor = "arrow"
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
function View:move_towards(t, k, dest, rate, name)
function View:move_towards(t, k, dest, rate)
if type(t) ~= "table" then
return self:move_towards(self, t, k, dest, rate, name)
return self:move_towards(self, t, k, dest, rate)
end
local val = t[k]
local diff = math.abs(val - dest)
if not config.transitions or diff < 0.5 or config.disabled_transitions[name] then
if not config.transitions or math.abs(val - dest) < 0.5 then
t[k] = dest
else
rate = rate or 0.5
@ -87,7 +31,7 @@ function View:move_towards(t, k, dest, rate, name)
end
t[k] = common.lerp(val, dest, rate)
end
if diff > 1e-8 then
if val ~= dest then
core.redraw = true
end
end
@ -98,150 +42,70 @@ function View:try_close(do_close)
end
---@return string
function View:get_name()
return "---"
end
---@return number
function View:get_scrollable_size()
return math.huge
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()
local sz = self:get_scrollable_size()
if sz <= self.size.y or sz == math.huge then
return 0, 0, 0, 0
end
local h = math.max(20, self.size.y * self.size.y / sz)
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.x + self.size.x - style.scrollbar_size,
self.position.y + self.scroll.y * (self.size.y - h) / (sz - self.size.y),
width,
style.scrollbar_size,
h
end
---@param x number
---@param y number
---@return boolean
function View:scrollbar_overlaps_point(x, y)
local sx, sy, sw, sh = self:get_scrollbar_rect()
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
return x >= sx - sw * 3 and x < sx + sw and y >= sy and y < sy + sh
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)
if self:scrollbar_track_overlaps_point(x, y) then
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
if self:scrollbar_overlaps_point(x, y) then
self.dragging_scrollbar = true
return true
end
end
---@param button core.view.mousebutton
---@param x number
---@param y number
function View:on_mouse_released(button, x, y)
self.dragging_scrollbar = false
end
---@param x number
---@param y number
---@param dx number
---@param dy number
function View:on_mouse_moved(x, y, dx, dy)
if self.dragging_scrollbar then
local delta = self:get_scrollable_size() / self.size.y * dy
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
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
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)
-- no-op
end
---@param y number
---@return boolean
function View:on_mouse_wheel(y)
function View:on_mouse_wheel(y)
if self.scrollable then
self.scroll.to.y = self.scroll.to.y + y * -config.mouse_wheel_scroll
end
end
function View:get_content_bounds()
local x = self.scroll.x
local y = self.scroll.y
@ -249,8 +113,6 @@ function View:get_content_bounds()
end
---@return number x
---@return number y
function View:get_content_offset()
local x = common.round(self.position.x - self.scroll.x)
local y = common.round(self.position.y - self.scroll.y)
@ -264,67 +126,25 @@ function View:clamp_scroll_position()
end
function View:update_scrollbar()
local x, y, w, h = self:get_scrollbar_rect()
self.scrollbar.w.to.thumb = w
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
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()
self:move_towards(self.scroll, "x", self.scroll.to.x, 0.3)
self:move_towards(self.scroll, "y", self.scroll.to.y, 0.3)
end
---@param color renderer.color
function View:draw_background(color)
local x, y = self.position.x, self.position.y
local w, h = self.size.x, self.size.y
renderer.draw_rect(x, y, w, h, color)
end
function View:draw_scrollbar_track()
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 color = highlight and style.scrollbar2 or style.scrollbar
renderer.draw_rect(self.scrollbar.x.thumb, self.scrollbar.y.thumb,
self.scrollbar.w.thumb, self.scrollbar.h.thumb, color)
renderer.draw_rect(x, y, w + x % 1, h + y % 1, color)
end
function View:draw_scrollbar()
self:draw_scrollbar_track()
self:draw_scrollbar_thumb()
local x, y, w, h = self:get_scrollbar_rect()
local highlight = self.hovered_scrollbar or self.dragging_scrollbar
local color = highlight and style.scrollbar2 or style.scrollbar
renderer.draw_rect(x, y, w, h, color)
end

0
data/fonts/FiraSans-Regular.ttf Normal file → Executable file
View File

0
data/fonts/JetBrainsMono-Regular.ttf Normal file → Executable file
View File

0
data/fonts/icons.ttf Normal file → Executable file
View File

470
data/plugins/autocomplete.lua Normal file → Executable file
View File

@ -1,4 +1,4 @@
-- mod-version:3
-- mod-version:1 -- lite-xl 1.16
local core = require "core"
local common = require "core.common"
local config = require "core.config"
@ -8,154 +8,44 @@ local keymap = require "core.keymap"
local translate = require "core.doc.translate"
local RootView = require "core.rootview"
local DocView = require "core.docview"
local Doc = require "core.doc"
config.plugins.autocomplete = common.merge({
-- Amount of characters that need to be written for autocomplete
min_len = 3,
-- The max amount of visible items
max_height = 6,
-- The max amount of scrollable items
max_suggestions = 100,
-- 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)
config.autocomplete_max_suggestions = 6
local autocomplete = {}
autocomplete.map = {}
autocomplete.map_manually = {}
autocomplete.on_close = nil
-- Flag that indicates if the autocomplete box was manually triggered
-- with the autocomplete.complete() function to prevent the suggestions
-- from getting cluttered with arbitrary document symbols by using the
-- autocomplete.map_manually table.
local triggered_manually = false
local mt = { __tostring = function(t) return t.text end }
function autocomplete.add(t, manually_triggered)
function autocomplete.add(t)
local items = {}
for text, info in pairs(t.items) do
if type(info) == "table" then
table.insert(
items,
setmetatable(
{
text = text,
info = info.info,
desc = info.desc, -- Description shown on item selected
onhover = info.onhover, -- A callback called once when item is hovered
onselect = info.onselect, -- A callback called when item is selected
data = info.data -- Optional data that can be used on cb
},
mt
)
)
else
info = (type(info) == "string") and info
table.insert(items, setmetatable({ text = text, info = info }, mt))
end
end
if not manually_triggered then
autocomplete.map[t.name] = { files = t.files or ".*", items = items }
else
autocomplete.map_manually[t.name] = { files = t.files or ".*", items = items }
info = (type(info) == "string") and info
table.insert(items, setmetatable({ text = text, info = info }, mt))
end
autocomplete.map[t.name] = { files = t.files or ".*", items = items }
end
--
-- Thread that scans open document symbols and cache them
--
local max_symbols = config.plugins.autocomplete.max_symbols
local max_symbols = config.max_symbols or 2000
core.add_thread(function()
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 s = {}
get_syntax_symbols(s, doc)
if doc.disable_symbols then return s end
if doc.disable_symbols then return {} end
local i = 1
local s = {}
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
if not s[sym] then
symbols_count = symbols_count + 1
if symbols_count > max_symbols then
s = nil
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,
"Too many symbols in "..filename_message..
": stopping auto-complete for this document according to "..
"config.plugins.autocomplete.max_symbols."
)
"Too many symbols in document "..doc.filename..
": stopping auto-complete for this document according to config.max_symbols.")
collectgarbage('collect')
return {}
end
@ -202,7 +92,6 @@ core.add_thread(function()
for _, doc in ipairs(core.docs) do
if not cache_is_valid(doc) then
valid = false
break
end
end
end
@ -220,29 +109,16 @@ local last_line, last_col
local function reset_suggestions()
suggestions_idx = 1
suggestions = {}
triggered_manually = false
local doc = core.active_view.doc
if autocomplete.on_close then
autocomplete.on_close(doc, suggestions[suggestions_idx])
autocomplete.on_close = nil
end
end
local function update_suggestions()
local doc = core.active_view.doc
local filename = doc and doc.filename or ""
local map = autocomplete.map
if triggered_manually then
map = autocomplete.map_manually
end
-- get all relevant suggestions for given filename
local items = {}
for _, v in pairs(map) do
for _, v in pairs(autocomplete.map) do
if common.match_pattern(filename, v.files) then
for _, item in pairs(v.items) do
table.insert(items, item)
@ -253,16 +129,16 @@ local function update_suggestions()
-- fuzzy match, remove duplicates and store
items = common.fuzzy_match(items, partial)
local j = 1
for i = 1, config.plugins.autocomplete.max_suggestions do
for i = 1, config.autocomplete_max_suggestions do
suggestions[i] = items[j]
while items[j] and items[i].text == items[j].text do
items[i].info = items[i].info or items[j].info
j = j + 1
end
end
suggestions_idx = 1
end
local function get_partial_symbol()
local doc = core.active_view.doc
local line2, col2 = doc:get_selection()
@ -270,19 +146,22 @@ local function get_partial_symbol()
return doc:get_text(line1, col1, line2, col2)
end
local function get_active_view()
if core.active_view:is(DocView) then
if getmetatable(core.active_view) == DocView then
return core.active_view
end
end
local function get_suggestions_rect(av)
if #suggestions == 0 then
return 0, 0, 0, 0
end
local line, col = av.doc:get_selection()
local x, y = av:get_line_screen_position(line, col - #partial)
local x, y = av:get_line_screen_position(line)
x = x + av:get_col_x_offset(line, col - #partial)
y = y + av:get_line_height() + style.padding.y
local font = av:get_font()
local th = font:get_height()
@ -296,148 +175,15 @@ local function get_suggestions_rect(av)
max_width = math.max(max_width, w)
end
local ah = config.plugins.autocomplete.max_height
local max_items = #suggestions
if max_items > ah then
max_items = ah
end
-- additional line to display total items
max_items = max_items + 1
if max_width < 150 then
max_width = 150
end
-- 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
x - style.padding.x,
y - style.padding.y,
max_width + style.padding.x * 2,
max_items * (th + style.padding.y) + style.padding.y
#suggestions * (th + style.padding.y) + style.padding.y
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)
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 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 = {}
for line in string.gmatch(text.."\n", "(.-)\n") do
local wrapper_lines = wrap_line(line, max_chars)
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
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
renderer.draw_rect(
x,
sy,
width + style.padding.x * 2,
height + style.padding.y * 2,
style.background3
)
-- draw text
for _, line in pairs(lines) do
common.draw_text(
font, style.text, line, "left",
x + style.padding.x, y, width, lh
)
y = y + lh
end
end
local function draw_suggestions_box(av)
if #suggestions <= 0 then
return
end
local ah = config.plugins.autocomplete.max_height
-- draw background rect
local rx, ry, rw, rh = get_suggestions_rect(av)
renderer.draw_rect(rx, ry, rw, rh, style.background3)
@ -446,14 +192,7 @@ local function draw_suggestions_box(av)
local font = av:get_font()
local lh = font:get_height() + style.padding.y
local y = ry + style.padding.y / 2
local show_count = #suggestions <= ah and #suggestions or ah
local start_index = suggestions_idx > ah and (suggestions_idx-(ah-1)) or 1
for i=start_index, start_index+show_count-1, 1 do
if not suggestions[i] then
break
end
local s = suggestions[i]
for i, s in ipairs(suggestions) do
local color = (i == suggestions_idx) and style.accent or style.text
common.draw_text(font, color, s.text, "left", rx + style.padding.x, y, rw, lh)
if s.info then
@ -461,54 +200,26 @@ local function draw_suggestions_box(av)
common.draw_text(style.font, color, s.info, "right", rx, y, rw - style.padding.x, lh)
end
y = y + lh
if suggestions_idx == i then
if s.onhover then
s.onhover(suggestions_idx, s)
s.onhover = nil
end
if s.desc and #s.desc > 0 then
draw_description_box(s.desc, av, rx, ry, rw, rh)
end
end
end
renderer.draw_rect(rx, y, rw, 2, style.caret)
renderer.draw_rect(rx, y+2, rw, lh, style.background)
common.draw_text(
style.font,
style.accent,
"Items",
"left",
rx + style.padding.x, y, rw, lh
)
common.draw_text(
style.font,
style.accent,
tostring(suggestions_idx) .. "/" .. tostring(#suggestions),
"right",
rx, y, rw - style.padding.x, lh
)
end
local function show_autocomplete()
-- patch event logic into RootView
local on_text_input = RootView.on_text_input
local update = RootView.update
local draw = RootView.draw
RootView.on_text_input = function(...)
on_text_input(...)
local av = get_active_view()
if av then
-- update partial symbol and suggestions
partial = get_partial_symbol()
if #partial >= config.plugins.autocomplete.min_len or triggered_manually then
if #partial >= 3 then
update_suggestions()
if not triggered_manually then
last_line, last_col = av.doc:get_selection()
else
local line, col = av.doc:get_selection()
local char = av.doc:get_char(line, col-1, line, col-1)
if char:match("%s") or (char:match("%p") and col ~= last_col) then
reset_suggestions()
end
end
last_line, last_col = av.doc:get_selection()
else
reset_suggestions()
end
@ -522,30 +233,6 @@ local function show_autocomplete()
end
end
--
-- Patch event logic into RootView and Doc
--
local on_text_input = RootView.on_text_input
local on_text_remove = Doc.remove
local update = RootView.update
local draw = RootView.draw
RootView.on_text_input = function(...)
on_text_input(...)
show_autocomplete()
end
Doc.remove = function(self, line1, col1, line2, col2)
on_text_remove(self, line1, col1, line2, col2)
if triggered_manually and line1 == line2 then
if last_col >= col1 then
reset_suggestions()
else
show_autocomplete()
end
end
end
RootView.update = function(...)
update(...)
@ -554,19 +241,13 @@ RootView.update = function(...)
if av then
-- reset suggestions if caret was moved
local line, col = av.doc:get_selection()
if not triggered_manually then
if line ~= last_line or col ~= last_col then
reset_suggestions()
end
else
if line ~= last_line or col < last_col then
reset_suggestions()
end
if line ~= last_line or col ~= last_col then
reset_suggestions()
end
end
end
RootView.draw = function(...)
draw(...)
@ -577,70 +258,20 @@ RootView.draw = function(...)
end
end
--
-- Public functions
--
function autocomplete.open(on_close)
triggered_manually = true
if on_close then
autocomplete.on_close = on_close
end
local av = get_active_view()
last_line, last_col = av.doc:get_selection()
update_suggestions()
end
function autocomplete.close()
reset_suggestions()
end
function autocomplete.is_open()
return #suggestions > 0
end
function autocomplete.complete(completions, on_close)
reset_suggestions()
autocomplete.map_manually = {}
autocomplete.add(completions, true)
autocomplete.open(on_close)
end
function autocomplete.can_complete()
if #partial >= config.plugins.autocomplete.min_len then
return true
end
return false
end
--
-- Commands
--
local function predicate()
local active_docview = get_active_view()
return active_docview and #suggestions > 0, active_docview
return get_active_view() and #suggestions > 0
end
command.add(predicate, {
["autocomplete:complete"] = function(dv)
local doc = dv.doc
["autocomplete:complete"] = function()
local doc = core.active_view.doc
local line, col = doc:get_selection()
local item = suggestions[suggestions_idx]
local text = item.text
local inserted = false
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
local text = suggestions[suggestions_idx].text
doc:insert(line, col, text)
doc:remove(line, col, line, col - #partial)
doc:set_selection(line, col + #text - #partial)
reset_suggestions()
end,
@ -652,19 +283,12 @@ command.add(predicate, {
suggestions_idx = math.min(suggestions_idx + 1, #suggestions)
end,
["autocomplete:cycle"] = function()
local newidx = suggestions_idx + 1
suggestions_idx = newidx > #suggestions and 1 or newidx
end,
["autocomplete:cancel"] = function()
reset_suggestions()
end,
})
--
-- Keymaps
--
keymap.add {
["tab"] = "autocomplete:complete",
["up"] = "autocomplete:previous",

114
data/plugins/autoreload.lua Normal file → Executable file
View File

@ -1,110 +1,50 @@
-- mod-version:3
-- mod-version:1 -- lite-xl 1.16
local core = require "core"
local config = require "core.config"
local style = require "core.style"
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 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)
times[doc] = system.get_file_info(doc.filename).modified
local info = system.get_file_info(doc.filename)
times[doc] = info.modified
end
local function reload_doc(doc)
doc:reload()
local fp = io.open(doc.filename, "r")
local text = fp:read("*a")
fp:close()
local sel = { doc:get_selection() }
doc:remove(1, 1, math.huge, math.huge)
doc:insert(1, 1, text:gsub("\r", ""):gsub("\n$", ""))
doc:set_selection(table.unpack(sel))
update_time(doc)
core.redraw = true
doc:clean()
core.log_quiet("Auto-reloaded doc \"%s\"", doc.filename)
end
local function check_prompt_reload(doc)
if doc and doc.deferred_reload then
core.nag_view:show("File Changed", doc.filename .. " has changed. Reload this file?", {
{ font = style.font, text = "Yes", default_yes = true },
{ font = style.font, text = "No" , default_no = true }
}, function(item)
if item.text == "Yes" then reload_doc(doc) end
doc.deferred_reload = false
end)
end
end
local function doc_changes_visiblity(doc, visibility)
if doc and visible[doc] ~= visibility and doc.abs_filename then
visible[doc] = visibility
if visibility then check_prompt_reload(doc) end
get_project_doc_watch(doc):watch(doc.abs_filename, visibility)
end
end
local on_check = dirwatch.check
function dirwatch:check(change_callback, ...)
on_check(self, function(dir)
for _, doc in ipairs(core.docs) do
if doc.abs_filename and (dir == common.dirname(doc.abs_filename) or dir == doc.abs_filename) then
local info = system.get_file_info(doc.filename or "")
if info and 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)
-- check all doc modified times
for _, doc in ipairs(core.docs) do
local info = system.get_file_info(doc.filename or "")
if info and times[doc] ~= info.modified then
reload_doc(doc)
end
coroutine.yield()
end
-- wait for next scan
coroutine.yield(config.project_scan_rate)
end
end)
-- patch `Doc.save|load` to store modified time
local load = Doc.load
local save = Doc.save
@ -117,8 +57,6 @@ end
Doc.save = function(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)
return res
end

View File

@ -1,78 +0,0 @@
-- mod-version:3
local core = require "core"
local command = require "core.command"
local keymap = require "core.keymap"
local ContextMenu = require "core.contextmenu"
local RootView = require "core.rootview"
local menu = ContextMenu()
local on_view_mouse_pressed = RootView.on_view_mouse_pressed
local on_mouse_moved = RootView.on_mouse_moved
local root_view_update = RootView.update
local root_view_draw = RootView.draw
function RootView:on_mouse_moved(...)
if menu:on_mouse_moved(...) then return end
on_mouse_moved(self, ...)
end
function RootView.on_view_mouse_pressed(button, x, y, clicks)
-- We give the priority to the menu to process mouse pressed events.
local handled = menu:on_mouse_pressed(button, x, y, clicks)
return handled or on_view_mouse_pressed(button, x, y, clicks)
end
function RootView:update(...)
root_view_update(self, ...)
menu:update()
end
function RootView:draw(...)
root_view_draw(self, ...)
menu:draw()
end
command.add("core.docview!", {
["context:show"] = function(dv)
menu:show(dv.position.x, dv.position.y)
end
})
keymap.add {
["menu"] = "context:show"
}
command.add(function() return menu.show_context_menu == true end, {
["context:focus-previous"] = function()
menu:focus_previous()
end,
["context:focus-next"] = function()
menu:focus_next()
end,
["context:hide"] = function()
menu:hide()
end,
["context:on-selected"] = function()
menu:call_selected_item()
end,
})
keymap.add { ["return"] = "context:on-selected" }
keymap.add { ["up"] = "context:focus-previous" }
keymap.add { ["down"] = "context:focus-next" }
keymap.add { ["escape"] = "context:hide" }
if require("plugins.scale") then
menu:register("core.docview", {
{ text = "Cut", command = "doc:cut" },
{ text = "Copy", command = "doc:copy" },
{ text = "Paste", command = "doc:paste" },
{ text = "Font +", command = "scale:increase" },
{ text = "Font -", command = "scale:decrease" },
{ text = "Font Reset", command = "scale:reset" },
ContextMenu.DIVIDER,
{ text = "Find", command = "find-replace:find" },
{ text = "Replace", command = "find-replace:replace" }
})
end
return menu

370
data/plugins/detectindent.lua Normal file → Executable file
View File

@ -1,256 +1,95 @@
-- mod-version:3
-- mod-version:1 -- lite-xl 1.16
local core = require "core"
local command = require "core.command"
local common = require "core.common"
local config = require "core.config"
local core_syntax = require "core.syntax"
local DocView = require "core.docview"
local Doc = require "core.doc"
local tokenizer = require "core.tokenizer"
local cache = setmetatable({}, { __mode = "k" })
local comments_cache = {}
local auto_detect_max_lines = 150
local function indent_occurrences_more_than_once(stat, idx)
if stat[idx-1] and stat[idx-1] == stat[idx] then
return true
elseif stat[idx+1] and stat[idx+1] == stat[idx] then
return true
local function add_to_stat(stat, val)
for i = 1, #stat do
if val == stat[i][1] then
stat[i][2] = stat[i][2] + 1
return
end
end
return false
stat[#stat + 1] = {val, 1}
end
local function optimal_indent_from_stat(stat)
if #stat == 0 then return nil, 0 end
table.sort(stat, function(a, b) return a > b end)
local best_indent = 0
local best_score = 0
local count = #stat
for x=1, count do
local indent = stat[x]
local bins = {}
for k = 1, #stat do
local indent = stat[k][1]
local score = 0
for y=1, count do
if y ~= x and stat[y] % indent == 0 then
score = score + 1
elseif
indent > stat[y]
and
indent_occurrences_more_than_once(stat, y)
then
score = 0
break
local mult_prev, lines_prev
for i = k, #stat do
if stat[i][1] % indent == 0 then
local mult = stat[i][1] / indent
if not mult_prev or (mult_prev + 1 == mult and lines_prev / stat[i][2] > 0.1) then
-- we add the number of lines to the score only if the previous
-- multiple of "indent" was populated with enough lines.
score = score + stat[i][2]
end
mult_prev, lines_prev = mult, stat[i][2]
end
end
if score > best_score then
best_indent = indent
best_score = score
end
if score > 0 then
break
end
bins[#bins + 1] = {indent, score}
end
return best_score > 0 and best_indent or nil, best_score
table.sort(bins, function(a, b) return a[2] > b[2] end)
return bins[1][1], bins[1][2]
end
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)
-- 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
i = i + 2
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)
return coroutine.wrap(function()
local comments = get_comment_patterns(syntax)
local tokens, state
local i = 0
local end_regex = nil
local end_pattern = nil
local inside_comment = false
for _, line in ipairs(lines) do
if line:gsub("^%s+", "") ~= "" then
local is_comment = false
if comments then
if not inside_comment then
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
tokens, state = tokenizer.tokenize(syntax, line, state)
local line_start = get_first_line_part(tokens)
if line_start then
i = i + 1
coroutine.yield(i, line_start)
end
end
end)
end
local auto_detect_max_lines = 100
local function detect_indent_stat(doc)
local stat = {}
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
local spaces = text:match("^ +")
if spaces then table.insert(stat, spaces:len()) end
local tabs = text:match("^\t+")
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
local str = text:match("^ %s+%S")
if str then add_to_stat(stat, #str - 1) end
local str = text:match("^\t+")
if str then tab_count = tab_count + 1 end
-- Stop parsing when files is very long. Not needed for euristic determination.
elseif i > max_lines then break end
if i > auto_detect_max_lines then break end
end
table.sort(stat, function(a, b) return a[1] < b[1] end)
local indent, score = optimal_indent_from_stat(stat)
if tab_count > score then
return "hard", config.indent_size, tab_count
@ -262,12 +101,7 @@ end
local function update_cache(doc)
local type, size, score = detect_indent_stat(doc)
local score_threshold = 2
if score < score_threshold then
-- use default values
type = config.tab_type
size = config.indent_size
end
local score_threshold = 4
cache[doc] = { type = type, size = size, confirmed = (score >= score_threshold) }
doc.indent_info = cache[doc]
end
@ -277,94 +111,44 @@ local new = Doc.new
function Doc:new(...)
new(self, ...)
update_cache(self)
if not cache[self].confirmed then
core.add_thread(function ()
while not cache[self].confirmed do
update_cache(self)
coroutine.yield(1)
end
end, self)
end
end
local clean = Doc.clean
function Doc:clean(...)
clean(self, ...)
local _, _, confirmed = self:get_indent_info()
if not confirmed then
update_cache(self)
update_cache(self)
end
local function with_indent_override(doc, fn, ...)
local c = cache[doc]
if not c then
return fn(...)
end
local type, size = config.tab_type, config.indent_size
config.tab_type, config.indent_size = c.type, c.size or config.indent_size
local r1, r2, r3 = fn(...)
config.tab_type, config.indent_size = type, size
return r1, r2, r3
end
local function set_indent_type(doc, type)
local _, indent_size = doc:get_indent_info()
cache[doc] = {
type = type,
size = indent_size,
confirmed = true
}
doc.indent_info = cache[doc]
end
local function set_indent_type_command(dv)
core.command_view:enter("Specify indent style for this file", {
submit = function(value)
local doc = dv.doc
value = value:lower()
set_indent_type(doc, value == "tabs" and "hard" or "soft")
end,
suggest = function(text)
return common.fuzzy_match({"tabs", "spaces"}, text)
end,
validate = function(text)
local t = text:lower()
return t == "tabs" or t == "spaces"
end
})
local perform = command.perform
function command.perform(...)
return with_indent_override(core.active_view.doc, perform, ...)
end
local function set_indent_size(doc, size)
local indent_type = doc:get_indent_info()
cache[doc] = {
type = indent_type,
size = size,
confirmed = true
}
doc.indent_info = cache[doc]
local draw = DocView.draw
function DocView:draw(...)
return with_indent_override(self.doc, draw, self, ...)
end
local function set_indent_size_command(dv)
core.command_view:enter("Specify indent size for current file", {
submit = function(value)
value = math.floor(tonumber(value))
local doc = dv.doc
set_indent_size(doc, value)
end,
validate = function(value)
value = tonumber(value)
return value ~= nil and value >= 1
end
})
end
command.add("core.docview", {
["indent:set-file-indent-type"] = set_indent_type_command,
["indent:set-file-indent-size"] = set_indent_size_command
})
command.add(
function()
return core.active_view:is(DocView)
and cache[core.active_view.doc]
and cache[core.active_view.doc].type == "soft"
end, {
["indent:switch-file-to-tabs-indentation"] = function()
set_indent_type(core.active_view.doc, "hard")
end
})
command.add(
function()
return core.active_view:is(DocView)
and cache[core.active_view.doc]
and cache[core.active_view.doc].type == "hard"
end, {
["indent:switch-file-to-spaces-indentation"] = function()
set_indent_type(core.active_view.doc, "soft")
end
})

View File

@ -1,304 +0,0 @@
-- mod-version:3
local style = require "core.style"
local DocView = require "core.docview"
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
function DocView:draw_line_text(idx, x, y)
if
not config.plugins.drawwhitespace.enabled
or
getmetatable(self) ~= DocView
then
return draw_line_text(self, idx, x, y)
end
local font = (self:get_font() or style.syntax_fonts["whitespace"] or style.syntax_fonts["comment"])
local font_size = font:get_size()
local _, indent_size = self.doc:get_indent_info()
reset_cache_if_needed()
if
not ws_cache[self.doc.highlighter]
or ws_cache[self.doc.highlighter].font ~= font
or ws_cache[self.doc.highlighter].font_size ~= font_size
or ws_cache[self.doc.highlighter].indent_size ~= indent_size
then
ws_cache[self.doc.highlighter] =
setmetatable(
{ font = font, font_size = font_size, indent_size = indent_size },
{ __mode = "k" }
)
end
if not ws_cache[self.doc.highlighter][idx] then -- need to cache line
local cache = {}
local tx
local text = self.doc.lines[idx]
for _, substitution in pairs(config.plugins.drawwhitespace.substitutions) do
local char = substitution.char
local sub = substitution.sub
local offset = 1
local show_leading = get_option(substitution, "show_leading")
local show_middle = get_option(substitution, "show_middle")
local show_trailing = get_option(substitution, "show_trailing")
local show_middle_min = get_option(substitution, "show_middle_min")
local base_color = get_option(substitution, "color")
local leading_color = get_option(substitution, "leading_color") or base_color
local middle_color = get_option(substitution, "middle_color") or base_color
local trailing_color = get_option(substitution, "trailing_color") or base_color
local pattern = char.."+"
while true do
local s, e = text:find(pattern, offset)
if not s then break end
tx = self:get_col_x_offset(idx, s)
local color = base_color
local draw = false
if e == #text - 1 then
draw = show_trailing
color = trailing_color
elseif s == 1 then
draw = show_leading
color = leading_color
else
draw = show_middle and (e - s + 1 >= show_middle_min)
color = middle_color
end
if draw then
local last_cache_idx = #cache
-- We need to draw tabs one at a time because they might have a
-- different size than the substituting character.
-- This also applies to any other char if we use non-monospace fonts
-- but we ignore this case for now.
if char == "\t" then
for i = s,e do
tx = self:get_col_x_offset(idx, i)
cache[last_cache_idx + 1] = sub
cache[last_cache_idx + 2] = tx
cache[last_cache_idx + 3] = font:get_width(sub)
cache[last_cache_idx + 4] = color
last_cache_idx = last_cache_idx + 4
end
else
cache[last_cache_idx + 1] = string.rep(sub, e - s + 1)
cache[last_cache_idx + 2] = tx
cache[last_cache_idx + 3] = font:get_width(cache[last_cache_idx + 1])
cache[last_cache_idx + 4] = color
end
end
offset = e + 1
end
end
ws_cache[self.doc.highlighter][idx] = cache
end
-- draw from cache
local x1, _, x2, _ = self:get_content_bounds()
x1 = x1 + x
x2 = x2 + x
local ty = y + self:get_line_text_y_offset()
local cache = ws_cache[self.doc.highlighter][idx]
for i=1,#cache,4 do
local sub = cache[i]
local tx = cache[i + 1] + x
local tw = cache[i + 2]
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
return draw_line_text(self, idx, x, y)
end

80
data/plugins/language_c.lua Normal file → Executable file
View File

@ -1,13 +1,11 @@
-- mod-version:3
-- mod-version:1 -- lite-xl 1.16
local syntax = require "core.syntax"
syntax.add {
name = "C",
files = { "%.c$" },
files = { "%.c$", "%.h$", "%.inl$" },
comment = "//",
block_comment = { "/*", "*/" },
patterns = {
{ pattern = "//.*", type = "comment" },
{ pattern = "//.-\n", type = "comment" },
{ pattern = { "/%*", "%*/" }, type = "comment" },
{ pattern = { '"', '"', '\\' }, type = "string" },
{ pattern = { "'", "'", '\\' }, type = "string" },
@ -15,64 +13,12 @@ syntax.add {
{ pattern = "%d+[%d%.eE]*f?", type = "number" },
{ pattern = "%.?%d+f?", type = "number" },
{ pattern = "[%+%-=/%*%^%%<>!~|&]", type = "operator" },
{ pattern = "##", type = "operator" },
{ pattern = "struct%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" },
-- 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 = "#include%s()<.->", type = {"keyword", "string"} },
{ pattern = "#[%a_][%w_]*", type = "keyword" },
},
symbols = {
["if"] = "keyword",
@ -97,9 +43,7 @@ syntax.add {
["case"] = "keyword",
["default"] = "keyword",
["auto"] = "keyword",
["struct"] = "keyword",
["union"] = "keyword",
["void"] = "keyword2",
["void"] = "keyword",
["int"] = "keyword2",
["short"] = "keyword2",
["long"] = "keyword2",
@ -111,18 +55,6 @@ syntax.add {
["true"] = "literal",
["false"] = "literal",
["NULL"] = "literal",
["#include"] = "keyword",
["#if"] = "keyword",
["#ifdef"] = "keyword",
["#ifndef"] = "keyword",
["#elif"] = "keyword",
["#else"] = "keyword",
["#elseif"] = "keyword",
["#endif"] = "keyword",
["#define"] = "keyword",
["#warning"] = "keyword",
["#error"] = "keyword",
["#pragma"] = "keyword",
},
}

131
data/plugins/language_cpp.lua Normal file → Executable file
View File

@ -1,108 +1,36 @@
-- mod-version:3
-- mod-version:1 -- lite-xl 1.16
pcall(require, "plugins.language_c")
local syntax = require "core.syntax"
syntax.add {
name = "C++",
files = {
"%.h$", "%.inl$", "%.cpp$", "%.cc$", "%.C$", "%.cxx$",
"%.c++$", "%.hh$", "%.H$", "%.hxx$", "%.hpp$", "%.h++$"
},
comment = "//",
block_comment = { "/*", "*/" },
patterns = {
{ pattern = "//.*", type = "comment" },
{ pattern = { "/%*", "%*/" }, type = "comment" },
{ pattern = { '"', '"', '\\' }, type = "string" },
{ pattern = { "'", "'", '\\' }, type = "string" },
{ pattern = "0x%x+", type = "number" },
{ pattern = "%d+[%d%.'eE]*f?", type = "number" },
{ pattern = "%.?%d+f?", type = "number" },
{ pattern = "[%+%-=/%*%^%%<>!~|:&]", type = "operator" },
{ pattern = "##", type = "operator" },
{ pattern = "//.-\n", type = "comment" },
{ pattern = { "/%*", "%*/" }, type = "comment" },
{ pattern = { '"', '"', '\\' }, type = "string" },
{ pattern = { "'", "'", '\\' }, type = "string" },
{ pattern = "0x%x+", type = "number" },
{ pattern = "%d+[%d%.eE]*f?", type = "number" },
{ pattern = "%.?%d+f?", type = "number" },
{ pattern = "[%+%-=/%*%^%%<>!~|&]", type = "operator" },
{ pattern = "struct%s()[%a_][%w_]*", type = {"keyword", "keyword2"} },
{ pattern = "class%s()[%a_][%w_]*", type = {"keyword", "keyword2"} },
{ pattern = "union%s()[%a_][%w_]*", type = {"keyword", "keyword2"} },
{ pattern = "namespace%s()[%a_][%w_]*", type = {"keyword", "keyword2"} },
-- 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 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" },
{ pattern = "[%a_][%w_]*::", type = "symbol" },
{ pattern = "::", type = "symbol" },
{ pattern = "[%a_][%w_]*", type = "symbol" },
{ pattern = "#include%s()<.->", type = {"keyword", "string"} },
{ pattern = "#[%a_][%w_]*", type = "keyword" },
},
symbols = {
["alignof"] = "keyword",
["alignas"] = "keyword",
["and"] = "keyword",
["and_eq"] = "keyword",
["not"] = "keyword",
["not_eq"] = "keyword",
["or"] = "keyword",
["or_eq"] = "keyword",
["xor"] = "keyword",
["xor_eq"] = "keyword",
["private"] = "keyword",
["protected"] = "keyword",
["public"] = "keyword",
@ -110,12 +38,9 @@ syntax.add {
["nullptr"] = "keyword",
["operator"] = "keyword",
["asm"] = "keyword",
["bitand"] = "keyword",
["bitor"] = "keyword",
["catch"] = "keyword",
["throw"] = "keyword",
["try"] = "keyword",
["class"] = "keyword",
["compl"] = "keyword",
["explicit"] = "keyword",
["export"] = "keyword",
@ -125,8 +50,8 @@ syntax.add {
["constinit"] = "keyword",
["const_cast"] = "keyword",
["dynamic_cast"] = "keyword",
["reinterpret_cast"] = "keyword",
["static_cast"] = "keyword",
["reinterpret_cast"] = "keyword",
["static_cast"] = "keyword",
["static_assert"] = "keyword",
["template"] = "keyword",
["this"] = "keyword",
@ -137,6 +62,7 @@ syntax.add {
["co_yield"] = "keyword",
["decltype"] = "keyword",
["delete"] = "keyword",
["export"] = "keyword",
["friend"] = "keyword",
["typeid"] = "keyword",
["typename"] = "keyword",
@ -144,7 +70,6 @@ syntax.add {
["override"] = "keyword",
["virtual"] = "keyword",
["using"] = "keyword",
["namespace"] = "keyword",
["new"] = "keyword",
["noexcept"] = "keyword",
["if"] = "keyword",
@ -158,8 +83,6 @@ syntax.add {
["continue"] = "keyword",
["return"] = "keyword",
["goto"] = "keyword",
["struct"] = "keyword",
["union"] = "keyword",
["typedef"] = "keyword",
["enum"] = "keyword",
["extern"] = "keyword",
@ -171,7 +94,8 @@ syntax.add {
["case"] = "keyword",
["default"] = "keyword",
["auto"] = "keyword",
["void"] = "keyword2",
["const"] = "keyword",
["void"] = "keyword",
["int"] = "keyword2",
["short"] = "keyword2",
["long"] = "keyword2",
@ -180,18 +104,12 @@ syntax.add {
["char"] = "keyword2",
["unsigned"] = "keyword2",
["bool"] = "keyword2",
["true"] = "literal",
["false"] = "literal",
["NULL"] = "literal",
["wchar_t"] = "keyword2",
["char8_t"] = "keyword2",
["char16_t"] = "keyword2",
["char32_t"] = "keyword2",
["true"] = "keyword2",
["false"] = "keyword2",
["#include"] = "keyword",
["#if"] = "keyword",
["#ifdef"] = "keyword",
["#ifndef"] = "keyword",
["#elif"] = "keyword",
["#else"] = "keyword",
["#elseif"] = "keyword",
["#endif"] = "keyword",
@ -199,5 +117,6 @@ syntax.add {
["#warning"] = "keyword",
["#error"] = "keyword",
["#pragma"] = "keyword",
},
},
}

6
data/plugins/language_css.lua Normal file → Executable file
View File

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

29
data/plugins/language_html.lua Normal file → Executable file
View File

@ -1,23 +1,30 @@
-- mod-version:3
-- mod-version:1 -- lite-xl 1.16
local syntax = require "core.syntax"
syntax.add {
name = "HTML",
files = { "%.html?$" },
block_comment = { "<!--", "-->" },
patterns = {
{
pattern = {
"<%s*[sS][cC][rR][iI][pP][tT]%f[%s>].->",
"<%s*/%s*[sS][cC][rR][iI][pP][tT]%s*>"
{
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]%f[%s>].->",
"<%s*/%s*[sS][tT][yY][lL][eE]%s*>"
{
pattern = {
"<%s*[sS][tT][yY][lL][eE][^>]*>",
"<%s*/%s*[sS][tT][yY][lL][eE]%s*>"
},
syntax = ".css",
type = "function"

14
data/plugins/language_js.lua Normal file → Executable file
View File

@ -1,20 +1,18 @@
-- mod-version:3
-- mod-version:1 -- lite-xl 1.16
local syntax = require "core.syntax"
syntax.add {
name = "JavaScript",
files = { "%.js$", "%.json$", "%.cson$", "%.mjs$", "%.cjs$" },
files = { "%.js$", "%.json$", "%.cson$" },
comment = "//",
block_comment = { "/*", "*/" },
patterns = {
{ pattern = "//.*", type = "comment" },
{ pattern = "//.-\n", type = "comment" },
{ pattern = { "/%*", "%*/" }, type = "comment" },
{ pattern = { '/[^= ]', '/', '\\' },type = "string" },
{ pattern = { '/%g', '/', '\\' }, type = "string" },
{ pattern = { '"', '"', '\\' }, type = "string" },
{ pattern = { "'", "'", '\\' }, type = "string" },
{ pattern = { "`", "`", '\\' }, type = "string" },
{ pattern = "0x[%da-fA-F_]+n?", type = "number" },
{ pattern = "-?%d+[%d%.eE_n]*", type = "number" },
{ pattern = "0x[%da-fA-F]+", type = "number" },
{ pattern = "-?%d+[%d%.eE]*", type = "number" },
{ pattern = "-?%.?%d+", type = "number" },
{ pattern = "[%+%-=/%*%^%%<>!~|&]", type = "operator" },
{ pattern = "[%a_][%w_]*%f[(]", type = "function" },

6
data/plugins/language_lua.lua Normal file → Executable file
View File

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

241
data/plugins/language_md.lua Normal file → Executable file
View File

@ -1,235 +1,22 @@
-- mod-version:3
-- mod-version:1 -- lite-xl 1.16
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 {
name = "Markdown",
files = { "%.md$", "%.markdown$" },
block_comment = { "<!--", "-->" },
space_handling = false, -- turn off this feature to handle it our selfs
patterns = {
---- Place patterns that require spaces at start to optimize matching speed
---- 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 = { "```cpp", "```" }, type = "string", syntax = ".cpp" },
{ pattern = { "```python", "```" }, type = "string", syntax = ".py" },
{ pattern = { "```ruby", "```" }, type = "string", syntax = ".rb" },
{ pattern = { "```perl", "```" }, type = "string", syntax = ".pl" },
{ pattern = { "```php", "```" }, type = "string", syntax = ".php" },
{ pattern = { "```javascript", "```" }, type = "string", syntax = ".js" },
{ pattern = { "```json", "```" }, type = "string", syntax = ".js" },
{ pattern = { "```html", "```" }, type = "string", syntax = ".html" },
{ pattern = { "```ini", "```" }, type = "string", syntax = ".ini" },
{ pattern = { "```xml", "```" }, type = "string", syntax = ".xml" },
{ pattern = { "```css", "```" }, type = "string", syntax = ".css" },
{ pattern = { "```lua", "```" }, type = "string", syntax = ".lua" },
{ pattern = { "```bash", "```" }, type = "string", syntax = ".sh" },
{ pattern = { "```sh", "```" }, type = "string", syntax = ".sh" },
{ pattern = { "```java", "```" }, type = "string", syntax = ".java" },
{ pattern = { "```c#", "```" }, type = "string", syntax = ".cs" },
{ pattern = { "```cmake", "```" }, type = "string", syntax = ".cmake" },
{ pattern = { "```d", "```" }, type = "string", syntax = ".d" },
{ pattern = { "```glsl", "```" }, type = "string", syntax = ".glsl" },
{ pattern = { "```c", "```" }, type = "string", syntax = ".c" },
{ pattern = { "```julia", "```" }, type = "string", syntax = ".jl" },
{ pattern = { "```rust", "```" }, type = "string", syntax = ".rs" },
{ pattern = { "```dart", "```" }, type = "string", syntax = ".dart" },
{ pattern = { "```v", "```" }, type = "string", syntax = ".v" },
{ pattern = { "```toml", "```" }, type = "string", syntax = ".toml" },
{ pattern = { "```yaml", "```" }, type = "string", syntax = ".yaml" },
{ pattern = { "```nim", "```" }, type = "string", syntax = ".nim" },
{ pattern = { "```typescript", "```" }, type = "string", syntax = ".ts" },
{ pattern = { "```rescript", "```" }, type = "string", syntax = ".res" },
{ pattern = { "```moon", "```" }, type = "string", syntax = ".moon" },
{ pattern = { "```go", "```" }, type = "string", syntax = ".go" },
{ pattern = { "```lobster", "```" }, type = "string", syntax = ".lobster" },
{ pattern = { "```liquid", "```" }, type = "string", syntax = ".liquid" },
{ pattern = { "```", "```" }, type = "string" },
{ pattern = { "``", "``" }, type = "string" },
{ pattern = { "%f[\\`]%`[%S]", "`" }, type = "string" },
-- strike
{ pattern = { "~~", "~~" }, type = "keyword2" },
-- highlight
{ pattern = { "==", "==" }, type = "literal" },
-- lines
{ pattern = "^%-%-%-+$" , type = "comment" },
{ pattern = "^%*%*%*+$", type = "comment" },
{ 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" },
-- optimize consecutive dashes used in tables
{ pattern = "%-+", type = "normal" },
{ pattern = "\\.", type = "normal" },
{ pattern = { "<!%-%-", "%-%->" }, type = "comment" },
{ pattern = { "```", "```" }, type = "string" },
{ pattern = { "``", "``", "\\" }, type = "string" },
{ pattern = { "`", "`", "\\" }, type = "string" },
{ pattern = { "~~", "~~", "\\" }, type = "keyword2" },
{ pattern = "%-%-%-+", type = "comment" },
{ pattern = "%*%s+", type = "operator" },
{ pattern = { "%*", "[%*\n]", "\\" }, type = "operator" },
{ pattern = { "%_", "[%_\n]", "\\" }, type = "keyword2" },
{ pattern = "#.-\n", type = "keyword" },
{ pattern = "!?%[.-%]%(.-%)", type = "function" },
{ pattern = "https?://%S+", type = "function" },
},
symbols = { },
}
-- 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)

34
data/plugins/language_python.lua Normal file → Executable file
View File

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

4
data/plugins/language_xml.lua Normal file → Executable file
View File

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

View File

@ -1,115 +0,0 @@
-- mod-version:3
local common = require "core.common"
local command = require "core.command"
local config = require "core.config"
local style = require "core.style"
local DocView = require "core.docview"
local CommandView = require "core.commandview"
config.plugins.lineguide = common.merge({
enabled = false,
width = 2,
rulers = {
-- 80,
-- 100,
-- 120,
config.line_limit
},
-- The config specification used by gui generators
config_spec = {
name = "Line Guide",
{
label = "Enabled",
description = "Disable or enable drawing of the line guide.",
path = "enabled",
type = "toggle",
default = true
},
{
label = "Width",
description = "Width in pixels of the line guide.",
path = "width",
type = "number",
default = 2,
min = 1
},
{
label = "Ruler Positions",
description = "The different column numbers for the line guides to draw.",
path = "rulers",
type = "list_strings",
default = { tostring(config.line_limit) or "80" },
get_value = function(rulers)
if type(rulers) == "table" then
local new_rulers = {}
for _, ruler in ipairs(rulers) do
table.insert(new_rulers, tostring(ruler))
end
return new_rulers
else
return { tostring(config.line_limit) }
end
end,
set_value = function(rulers)
local new_rulers = {}
for _, ruler in ipairs(rulers) do
local number = tonumber(ruler)
if number then
table.insert(new_rulers, number)
end
end
if #new_rulers == 0 then
table.insert(new_rulers, config.line_limit)
end
return new_rulers
end
}
}
}, config.plugins.lineguide)
local function get_ruler(v)
local result = nil
if type(v) == 'number' then
result = { columns = v }
elseif type(v) == 'table' then
result = v
end
return result
end
local draw_overlay = DocView.draw_overlay
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

@ -1,581 +0,0 @@
-- mod-version:3 --priority:10
local core = require "core"
local common = require "core.common"
local DocView = require "core.docview"
local Doc = require "core.doc"
local style = require "core.style"
local config = require "core.config"
local command = require "core.command"
local keymap = require "core.keymap"
local translate = require "core.doc.translate"
config.plugins.linewrapping = common.merge({
-- The type of wrapping to perform. Can be "letter" or "word".
mode = "letter",
-- If nil, uses the DocView's size, otherwise, uses this exact width. Can be a function.
width_override = nil,
-- Whether or not to draw a guide
guide = true,
-- Whether or not we should indent ourselves like the first line of a wrapped block.
indent = true,
-- Whether or not to enable wrapping by default when opening files.
enable_by_default = false,
-- Requires tokenization
require_tokenization = false,
-- The config specification used by gui generators
config_spec = {
name = "Line Wrapping",
{
label = "Mode",
description = "The type of wrapping to perform.",
path = "mode",
type = "selection",
default = "letter",
values = {
{"Letters", "letter"},
{"Words", "word"}
}
},
{
label = "Guide",
description = "Whether or not to draw a guide.",
path = "guide",
type = "toggle",
default = true
},
{
label = "Indent",
description = "Whether or not to follow the indentation of wrapped line.",
path = "indent",
type = "toggle",
default = true
},
{
label = "Enable by Default",
description = "Whether or not to enable wrapping by default when opening files.",
path = "enable_by_default",
type = "toggle",
default = false
},
{
label = "Require Tokenization",
description = "Use tokenization when applying wrapping.",
path = "require_tokenization",
type = "toggle",
default = false
}
}
}, config.plugins.linewrapping)
local LineWrapping = {}
-- Optimzation function. The tokenizer is relatively slow (at present), and
-- so if we don't need to run it, should be run sparingly.
local function spew_tokens(doc, line) if line < math.huge then return math.huge, "normal", doc.lines[line] end end
local function get_tokens(doc, line)
if config.plugins.linewrapping.require_tokenization then
return doc.highlighter:each_token(line)
end
return spew_tokens, doc, line
end
-- Computes the breaks for a given line, width and mode. Returns a list of columns
-- at which the line should be broken.
function LineWrapping.compute_line_breaks(doc, default_font, line, width, mode)
local xoffset, last_i, i, last_space, last_width, begin_width = 0, 1, 1, nil, 0, 0
local splits = { 1 }
for idx, type, text in get_tokens(doc, line) do
local font = style.syntax_fonts[type] or default_font
if idx == 1 or idx == math.huge and config.plugins.linewrapping.indent then
local _, indent_end = text:find("^%s+")
if indent_end then begin_width = font:get_width(text:sub(1, indent_end)) end
end
local w = font:get_width(text)
if xoffset + w > width then
for char in common.utf8_chars(text) do
w = font:get_width(char)
xoffset = xoffset + w
if xoffset > width then
if mode == "word" and last_space then
table.insert(splits, last_space + 1)
xoffset = w + begin_width + (xoffset - last_width)
else
table.insert(splits, i)
xoffset = w + begin_width
end
last_space = nil
elseif char == ' ' then
last_space = i
last_width = xoffset
end
i = i + #char
end
else
xoffset = xoffset + w
i = i + #text
end
end
return splits, begin_width
end
-- breaks are held in a single table that contains n*2 elements, where n is the amount of line breaks.
-- each element represents line and column of the break. line_offset will check from the specified line
-- if the first line has not changed breaks, it will stop there.
function LineWrapping.reconstruct_breaks(docview, default_font, width, line_offset)
if width ~= math.huge then
local doc = docview.doc
-- two elements per wrapped line; first maps to original line number, second to column number.
docview.wrapped_lines = { }
-- one element per actual line; maps to the first index of in wrapped_lines for this line
docview.wrapped_line_to_idx = { }
-- one element per actual line; gives the indent width for the acutal line
docview.wrapped_line_offsets = { }
docview.wrapped_settings = { ["width"] = width, ["font"] = default_font }
for i = line_offset or 1, #doc.lines do
local breaks, offset = LineWrapping.compute_line_breaks(doc, default_font, i, width, config.plugins.linewrapping.mode)
table.insert(docview.wrapped_line_offsets, offset)
for k, col in ipairs(breaks) do
table.insert(docview.wrapped_lines, i)
table.insert(docview.wrapped_lines, col)
end
end
-- list of indices for wrapped_lines, that are based on original line number
-- holds the index to the first in the wrapped_lines list.
local last_wrap = nil
for i = 1, #docview.wrapped_lines, 2 do
if not last_wrap or last_wrap ~= docview.wrapped_lines[i] then
table.insert(docview.wrapped_line_to_idx, (i + 1) / 2)
last_wrap = docview.wrapped_lines[i]
end
end
else
docview.wrapped_lines = nil
docview.wrapped_line_to_idx = nil
docview.wrapped_line_offsets = nil
docview.wrapped_settings = nil
end
end
-- When we have an insertion or deletion, we have four sections of text.
-- 1. The unaffected section, located prior to the cursor. This is completely ignored.
-- 2. The beginning of the affected line prior to the insertion or deletion. Begins on column 1 of the selection.
-- 3. The removed/pasted lines.
-- 4. Every line after the modification, begins one line after the selection in the initial document.
function LineWrapping.update_breaks(docview, old_line1, old_line2, net_lines)
-- Step 1: Determine the index for the line for #2.
local old_idx1 = docview.wrapped_line_to_idx[old_line1] or 1
-- Step 2: Determine the index of the line for #4.
local old_idx2 = (docview.wrapped_line_to_idx[old_line2 + 1] or ((#docview.wrapped_lines / 2) + 1)) - 1
-- Step 3: Remove all old breaks for the old lines from the table, and all old widths from wrapped_line_offsets.
local offset = (old_idx1 - 1) * 2 + 1
for i = old_idx1, old_idx2 do
table.remove(docview.wrapped_lines, offset)
table.remove(docview.wrapped_lines, offset)
end
for i = old_line1, old_line2 do
table.remove(docview.wrapped_line_offsets, old_line1)
end
-- Step 4: Shift the line number of wrapped_lines past #4 by the amount of inserted/deleted lines.
if net_lines ~= 0 then
for i = offset, #docview.wrapped_lines, 2 do
docview.wrapped_lines[i] = docview.wrapped_lines[i] + net_lines
end
end
-- Step 5: Compute the breaks and offsets for the lines for #2 and #3. Insert them into the table.
local new_line1 = old_line1
local new_line2 = old_line2 + net_lines
for line = new_line1, new_line2 do
local breaks, begin_width = LineWrapping.compute_line_breaks(docview.doc, docview.wrapped_settings.font, line, docview.wrapped_settings.width, config.plugins.linewrapping.mode)
table.insert(docview.wrapped_line_offsets, line, begin_width)
for i,b in ipairs(breaks) do
table.insert(docview.wrapped_lines, offset, b)
table.insert(docview.wrapped_lines, offset, line)
offset = offset + 2
end
end
-- Step 6: Recompute the wrapped_line_to_idx cache from #2.
local line = old_line1
offset = (old_idx1 - 1) * 2 + 1
while offset < #docview.wrapped_lines do
if docview.wrapped_lines[offset + 1] == 1 then
docview.wrapped_line_to_idx[line] = ((offset - 1) / 2) + 1
line = line + 1
end
offset = offset + 2
end
while line <= #docview.wrapped_line_to_idx do
table.remove(docview.wrapped_line_to_idx)
end
end
-- Draws a guide if applicable to show where wrapping is occurring.
function LineWrapping.draw_guide(docview)
if config.plugins.linewrapping.guide and docview.wrapped_settings.width ~= math.huge then
local x, y = docview:get_content_offset()
local gw = docview:get_gutter_width()
renderer.draw_rect(x + gw + docview.wrapped_settings.width, y, 1, core.root_view.size.y, style.selection)
end
end
function LineWrapping.update_docview_breaks(docview)
local 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

2
data/plugins/macro.lua Normal file → Executable file
View File

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

104
data/plugins/projectsearch.lua Normal file → Executable file
View File

@ -1,4 +1,4 @@
-- mod-version:3
-- mod-version:1 -- lite-xl 1.16
local core = require "core"
local common = require "core.common"
local keymap = require "core.keymap"
@ -9,13 +9,12 @@ local View = require "core.view"
local ResultsView = View:extend()
ResultsView.context = "session"
function ResultsView:new(path, text, fn)
function ResultsView:new(text, fn)
ResultsView.super.new(self)
self.scrollable = true
self.brightness = 0
self:begin_search(path, text, fn)
self:begin_search(text, fn)
end
@ -45,8 +44,8 @@ local function find_all_matches_in_file(t, filename, fn)
end
function ResultsView:begin_search(path, text, fn)
self.search_args = { path, text, fn }
function ResultsView:begin_search(text, fn)
self.search_args = { text, fn }
self.results = {}
self.last_file_idx = 1
self.query = text
@ -56,9 +55,9 @@ function ResultsView:begin_search(path, text, fn)
core.add_thread(function()
local i = 1
for dir_name, file in core.get_project_files() do
if file.type == "file" and (not path or (dir_name .. "/" .. file.filename):find(path, 1, true) == 1) then
local truncated_path = (dir_name == core.project_dir and "" or (dir_name .. PATHSEP))
find_all_matches_in_file(self.results, truncated_path .. file.filename, fn)
if file.type == "file" then
local path = (dir_name == core.project_dir and "" or (dir_name .. PATHSEP))
find_all_matches_in_file(self.results, path .. file.filename, fn)
end
self.last_file_idx = i
i = i + 1
@ -92,7 +91,7 @@ end
function ResultsView:on_mouse_pressed(...)
local caught = ResultsView.super.on_mouse_pressed(self, ...)
if not caught then
return self:open_selected_result()
self:open_selected_result()
end
end
@ -108,7 +107,6 @@ function ResultsView:open_selected_result()
dv.doc:set_selection(res.line, res.col)
dv:scroll_to_line(res.line, false, true)
end)
return true
end
@ -172,11 +170,11 @@ function ResultsView:draw()
local ox, oy = self:get_content_offset()
local x, y = ox + style.padding.x, oy + style.padding.y
local files_number = core.project_files_number()
local per = common.clamp(files_number and self.last_file_idx / files_number or 1, 0, 1)
local per = files_number and self.last_file_idx / files_number or 1
local text
if self.searching then
if files_number then
text = string.format("Searching %.f%% (%d of %d files, %d matches) for %q...",
text = string.format("Searching %d%% (%d of %d files, %d matches) for %q...",
per * 100, self.last_file_idx, files_number,
#self.results, self.query)
else
@ -219,72 +217,38 @@ function ResultsView:draw()
end
local function begin_search(path, text, fn)
local function begin_search(text, fn)
if text == "" then
core.error("Expected non-empty string")
return
end
local rv = ResultsView(path, text, fn)
local rv = ResultsView(text, fn)
core.root_view:get_active_node_default():add_view(rv)
end
local function get_selected_text()
local view = core.active_view
local doc = (view and view.doc) and view.doc or nil
if doc then
return doc:get_text(table.unpack({ doc:get_selection() }))
end
end
local function normalize_path(path)
if not path then return nil end
path = common.normalize_path(path)
for i, project_dir in ipairs(core.project_directories) do
if common.path_belongs_to(path, project_dir.name) then
return project_dir.item.filename .. PATHSEP .. common.relative_path(project_dir.name, path)
end
end
return path
end
command.add(nil, {
["project-search:find"] = function(path)
core.command_view:enter("Find Text In " .. (normalize_path(path) or "Project"), {
text = get_selected_text(),
select_text = true,
submit = function(text)
text = text:lower()
begin_search(path, text, function(line_text)
return line_text:lower():find(text, nil, true)
end)
end
})
["project-search:find"] = function()
core.command_view:enter("Find Text In Project", function(text)
text = text:lower()
begin_search(text, function(line_text)
return line_text:lower():find(text, nil, true)
end)
end)
end,
["project-search:find-regex"] = function(path)
core.command_view:enter("Find Regex In " .. (normalize_path(path) or "Project"), {
submit = function(text)
local re = regex.compile(text, "i")
begin_search(path, text, function(line_text)
return regex.cmatch(re, line_text)
end)
end
})
["project-search:find-pattern"] = function()
core.command_view:enter("Find Pattern In Project", function(text)
begin_search(text, function(line_text) return line_text:find(text) end)
end)
end,
["project-search:fuzzy-find"] = function(path)
core.command_view:enter("Fuzzy Find Text In " .. (normalize_path(path) or "Project"), {
text = get_selected_text(),
select_text = true,
submit = function(text)
begin_search(path, text, function(line_text)
return common.fuzzy_match(line_text, text) and 1
end)
end
})
["project-search:fuzzy-find"] = function()
core.command_view:enter("Fuzzy Find Text In Project", function(text)
begin_search(text, function(line_text)
return common.fuzzy_match(line_text, text) and 1
end)
end)
end,
})
@ -309,22 +273,22 @@ command.add(ResultsView, {
["project-search:refresh"] = function()
core.active_view:refresh()
end,
["project-search:move-to-previous-page"] = function()
local view = core.active_view
view.scroll.to.y = view.scroll.to.y - view.size.y
end,
["project-search:move-to-next-page"] = function()
local view = core.active_view
view.scroll.to.y = view.scroll.to.y + view.size.y
end,
["project-search:move-to-start-of-doc"] = function()
local view = core.active_view
view.scroll.to.y = 0
end,
["project-search:move-to-end-of-doc"] = function()
local view = core.active_view
view.scroll.to.y = view:get_scrollable_size()

6
data/plugins/quote.lua Normal file → Executable file
View File

@ -1,4 +1,4 @@
-- mod-version:3
-- mod-version:1 -- lite-xl 1.16
local core = require "core"
local command = require "core.command"
local keymap = require "core.keymap"
@ -19,8 +19,8 @@ end
command.add("core.docview", {
["quote:quote"] = function(dv)
dv.doc:replace(function(text)
["quote:quote"] = function()
core.active_view.doc:replace(function(text)
return '"' .. text:gsub("[\0-\31\\\"]", replace) .. '"'
end)
end,

6
data/plugins/reflow.lua Normal file → Executable file
View File

@ -1,4 +1,4 @@
-- mod-version:3
-- mod-version:1 -- lite-xl 1.16
local core = require "core"
local config = require "core.config"
local command = require "core.command"
@ -25,8 +25,8 @@ end
command.add("core.docview", {
["reflow:reflow"] = function(dv)
local doc = dv.doc
["reflow:reflow"] = function()
local doc = core.active_view.doc
doc:replace(function(text)
local prefix_set = "[^%w\n%[%](){}`'\"]*"

View File

@ -1,180 +0,0 @@
-- mod-version:3
local core = require "core"
local common = require "core.common"
local command = require "core.command"
local config = require "core.config"
local keymap = require "core.keymap"
local style = require "core.style"
local CommandView = require "core.commandview"
config.plugins.scale = common.merge({
-- The method used to apply the scaling: "code", "ui"
mode = "code",
-- Default scale applied at startup.
default_scale = "autodetect",
-- Allow using CTRL + MouseWheel for changing the scale.
use_mousewheel = true
}, config.plugins.scale)
local scale_steps = 0.05
local current_scale = SCALE
local default_scale = SCALE
local function set_scale(scale)
scale = common.clamp(scale, 0.2, 6)
-- save scroll positions
local scrolls = {}
for _, view in ipairs(core.root_view.root_node:get_children()) do
local n = view:get_scrollable_size()
if n ~= math.huge and not view:is(CommandView) and n > view.size.y then
scrolls[view] = view.scroll.y / (n - view.size.y)
end
end
local s = scale / current_scale
current_scale = scale
if config.plugins.scale.mode == "ui" then
SCALE = scale
style.padding.x = style.padding.x * s
style.padding.y = style.padding.y * s
style.divider_size = style.divider_size * s
style.scrollbar_size = style.scrollbar_size * s
style.caret_width = style.caret_width * s
style.tab_width = style.tab_width * s
for _, name in ipairs {"font", "big_font", "icon_font", "icon_big_font", "code_font"} do
style[name]:set_size(s * style[name]:get_size())
end
else
style.code_font:set_size(s * style.code_font:get_size())
end
for name, font in pairs(style.syntax_fonts) do
style.syntax_fonts[name]:set_size(s * font:get_size())
end
-- restore scroll positions
for view, n in pairs(scrolls) do
view.scroll.y = n * (view:get_scrollable_size() - view.size.y)
view.scroll.to.y = view.scroll.y
end
core.redraw = true
end
local function get_scale()
return current_scale
end
local function res_scale()
set_scale(default_scale)
end
local function inc_scale()
set_scale(current_scale + scale_steps)
end
local function dec_scale()
set_scale(current_scale - scale_steps)
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, {
["scale:reset" ] = function() res_scale() end,
["scale:decrease"] = function() dec_scale() end,
["scale:increase"] = function() inc_scale() end,
})
keymap.add {
["ctrl+0"] = "scale:reset",
["ctrl+-"] = "scale:decrease",
["ctrl+="] = "scale:increase"
}
if config.plugins.scale.use_mousewheel then
keymap.add {
["ctrl+wheelup"] = "scale:increase",
["ctrl+wheeldown"] = "scale:decrease"
}
end
return {
["set"] = set_scale,
["get"] = get_scale,
["increase"] = inc_scale,
["decrease"] = dec_scale,
["reset"] = res_scale
}

32
data/plugins/tabularize.lua Normal file → Executable file
View File

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

51
data/plugins/toolbarview.lua Normal file → Executable file
View File

@ -1,4 +1,4 @@
-- mod-version:3
-- mod-version:1 -- lite-xl 1.16
local core = require "core"
local common = require "core.common"
local command = require "core.command"
@ -7,26 +7,31 @@ local View = require "core.view"
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()
ToolbarView.super.new(self)
self.visible = true
self.init_size = true
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
function ToolbarView:update()
local dest_size = self.visible and (self.toolbar_font:get_height() + style.padding.y * 2) or 0
local dest_size = self.visible and toolbar_height() or 0
if self.init_size then
self.size.y = dest_size
self.init_size = nil
@ -41,24 +46,19 @@ function ToolbarView:toggle_visible()
self.visible = not self.visible
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()
local icon_h, icon_w = self.toolbar_font:get_height(), self:get_icon_width()
local icon_h, icon_w = style.icon_big_font:get_height(), style.icon_big_font:get_width("D")
local toolbar_spacing = icon_w / 2
local ox, oy = self:get_content_offset()
local index = 0
local iter = function()
index = index + 1
if index <= #self.toolbar_commands then
if index <= #toolbar_commands then
local dx = style.padding.x + (icon_w + toolbar_spacing) * (index - 1)
local dy = style.padding.y
if dx + icon_w > self.size.x then return end
return self.toolbar_commands[index], ox + dx, oy + dy, icon_w, icon_h
return toolbar_commands[index], ox + dx, oy + dy, icon_w, icon_h
end
end
return iter
@ -66,9 +66,9 @@ end
function ToolbarView:get_min_width()
local icon_w = self:get_icon_width()
local icon_w = style.icon_big_font:get_width("D")
local space = icon_w / 2
return 2 * style.padding.x + (icon_w + space) * #self.toolbar_commands - space
return 2 * style.padding.x + (icon_w + space) * #toolbar_commands - space
end
@ -76,20 +76,19 @@ function ToolbarView:draw()
self:draw_background(style.background2)
for item, x, y, w, h in self:each_item() do
local color = item == self.hovered_item and command.is_valid(item.command) and style.text or style.dim
common.draw_text(self.toolbar_font, color, item.symbol, nil, x, y, 0, h)
local color = item == self.hovered_item and style.text or style.dim
common.draw_text(style.icon_big_font, color, item.symbol, nil, x, y, 0, h)
end
end
function ToolbarView:on_mouse_pressed(button, x, y, clicks)
local caught = ToolbarView.super.on_mouse_pressed(self, button, x, y, clicks)
if caught then return caught end
if caught then return end
core.set_active_view(core.last_active_view)
if self.hovered_item and command.is_valid(self.hovered_item.command) then
if self.hovered_item then
command.perform(self.hovered_item.command)
end
return true
end

803
data/plugins/treeview.lua Normal file → Executable file
View File

@ -1,4 +1,4 @@
-- mod-version:3
-- mod-version:1 -- lite-xl 1.16
local core = require "core"
local common = require "core.common"
local command = require "core.command"
@ -6,16 +6,9 @@ local config = require "core.config"
local keymap = require "core.keymap"
local style = require "core.style"
local View = require "core.view"
local ContextMenu = require "core.contextmenu"
local RootView = require "core.rootview"
local CommandView = require "core.commandview"
config.plugins.treeview = common.merge({
-- Default treeview width
size = 200 * SCALE
}, config.plugins.treeview)
local tooltip_offset = style.font:get_height()
local default_treeview_size = 200 * SCALE
local tooltip_offset = style.font:get_height("A")
local tooltip_border = 1
local tooltip_delay = 0.5
local tooltip_alpha = 255
@ -43,27 +36,10 @@ function TreeView:new()
self.scrollable = true
self.visible = true
self.init_size = true
self.target_size = config.plugins.treeview.size
self.target_size = default_treeview_size
self.cache = {}
self.last = {}
self.tooltip = { x = 0, y = 0, begin = 0, alpha = 0 }
self.cursor_pos = { x = 0, y = 0 }
self.item_icon_width = 0
self.item_text_spacing = 0
self:add_core_hooks()
end
function TreeView:add_core_hooks()
-- 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
@ -75,7 +51,7 @@ function TreeView:set_target_size(axis, value)
end
function TreeView:get_cached(dir, item, dirname)
function TreeView:get_cached(item, dirname)
local dir_cache = self.cache[dirname]
if not dir_cache then
dir_cache = {}
@ -101,7 +77,6 @@ function TreeView:get_cached(dir, item, dirname)
end
t.name = basename
t.type = item.type
t.dir_name = dir.name -- points to top level "dir" item
dir_cache[cache_name] = t
end
return t
@ -109,7 +84,7 @@ end
function TreeView:get_name()
return nil
return "Project"
end
@ -126,13 +101,18 @@ end
function TreeView:check_cache()
-- invalidate cache's skip values if project_files has changed
for i = 1, #core.project_directories do
local dir = core.project_directories[i]
-- invalidate cache's skip values if directory is declared dirty
if dir.is_dirty and self.cache[dir.name] then
self:invalidate_cache(dir.name)
local last_files = self.last[dir.name]
if not last_files then
self.last[dir.name] = dir.files
else
if dir.files ~= last_files then
self:invalidate_cache(dir.name)
self.last[dir.name] = dir.files
end
end
dir.is_dirty = false
end
end
@ -148,56 +128,39 @@ function TreeView:each_item()
for k = 1, #core.project_directories do
local dir = core.project_directories[k]
local dir_cached = self:get_cached(dir, dir.item, dir.name)
local dir_cached = self:get_cached(dir.item, dir.name)
coroutine.yield(dir_cached, ox, y, w, h)
count_lines = count_lines + 1
y = y + h
local i = 1
if dir.files then -- if consumed max sys file descriptors this can be nil
while i <= #dir.files and dir_cached.expanded do
local item = dir.files[i]
local cached = self:get_cached(dir, item, dir.name)
while i <= #dir.files and dir_cached.expanded do
local item = dir.files[i]
local cached = self:get_cached(item, dir.name)
coroutine.yield(cached, ox, y, w, h)
count_lines = count_lines + 1
y = y + h
i = i + 1
coroutine.yield(cached, ox, y, w, h)
count_lines = count_lines + 1
y = y + h
i = i + 1
if not cached.expanded then
if cached.skip then
i = cached.skip
else
local depth = cached.depth
while i <= #dir.files do
if get_depth(dir.files[i].filename) <= depth then break end
i = i + 1
end
cached.skip = i
if not cached.expanded then
if cached.skip then
i = cached.skip
else
local depth = cached.depth
while i <= #dir.files do
if get_depth(dir.files[i].filename) <= depth then break end
i = i + 1
end
cached.skip = i
end
end -- while files
end
end
end -- while files
end -- for directories
self.count_lines = count_lines
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)
local icon_width = style.icon_font:get_width("D")
local xoffset = item.depth * style.padding.x + style.padding.x + icon_width
@ -208,21 +171,15 @@ end
function TreeView:on_mouse_moved(px, py, ...)
if not self.visible then return end
TreeView.super.on_mouse_moved(self, px, py, ...)
self.cursor_pos.x = px
self.cursor_pos.y = py
if self.dragging_scrollbar then
self.hovered_item = nil
return
end
if self.dragging_scrollbar then return end
local item_changed, tooltip_changed
for item, x,y,w,h in self:each_item() do
if px > x and py > y and px <= x + w and py <= y + h then
item_changed = true
self.hovered_item = item
x,y,w,h = self:get_text_bounding_box(item, x,y,w,h)
if px > x and py > y and px <= x + w and py <= y + h then
tooltip_changed = true
@ -237,6 +194,59 @@ function TreeView:on_mouse_moved(px, py, ...)
end
local function create_directory_in(item)
local path = item.abs_filename
core.command_view:enter("Create directory in " .. path, function(text)
local dirname = path .. PATHSEP .. text
local success, err = system.mkdir(dirname)
if not success then
core.error("cannot create directory %q: %s", dirname, err)
end
item.expanded = true
core.reschedule_project_scan()
end)
end
function TreeView:on_mouse_pressed(button, x, y, clicks)
local caught = TreeView.super.on_mouse_pressed(self, button, x, y, clicks)
if caught then
return
end
local hovered_item = self.hovered_item
if not hovered_item then
return
elseif hovered_item.type == "dir" then
if keymap.modkeys["ctrl"] and button == "left" then
create_directory_in(hovered_item)
else
if core.project_files_limit and not hovered_item.expanded then
local filename, abs_filename = hovered_item.filename, hovered_item.abs_filename
local index = 0
-- The loop below is used to find the first match starting from the end
-- in case there are multiple matches.
while index and index + #filename < #abs_filename do
index = string.find(abs_filename, filename, index + 1, true)
end
-- we assume here index is not nil because the abs_filename must contain the
-- relative filename
local dirname = string.sub(abs_filename, 1, index - 2)
if core.is_project_folder(dirname) then
core.scan_project_folder(dirname, filename)
self:invalidate_cache(dirname)
end
end
hovered_item.expanded = not hovered_item.expanded
end
else
core.try(function()
local doc_filename = common.relative_path(core.project_dir, hovered_item.abs_filename)
core.root_view:open_doc(core.open_doc(doc_filename))
end)
end
end
function TreeView:update()
-- update width
local dest = self.visible and self.target_size or 0
@ -244,28 +254,16 @@ function TreeView:update()
self.size.x = dest
self.init_size = false
else
self:move_towards(self.size, "x", dest, nil, "treeview")
self:move_towards(self.size, "x", dest)
end
if not self.visible then return end
local duration = system.get_time() - self.tooltip.begin
if self.hovered_item and self.tooltip.x and duration > tooltip_delay then
self:move_towards(self.tooltip, "alpha", tooltip_alpha, tooltip_alpha_rate, "treeview")
self:move_towards(self.tooltip, "alpha", tooltip_alpha, tooltip_alpha_rate)
else
self.tooltip.alpha = 0
end
self.item_icon_width = style.icon_font:get_width("D")
self.item_text_spacing = style.icon_font:get_width("f") / 2
-- this will make sure hovered_item is updated
-- we don't want events when the thing is scrolling fast
local dy = math.abs(self.scroll.to.y - self.scroll.y)
if self.scroll.to.y ~= 0 and dy < self:get_item_height() then
self:on_mouse_moved(self.cursor_pos.x, self.cursor_pos.y, 0, 0)
end
TreeView.super.update(self)
end
@ -294,171 +292,60 @@ function TreeView:draw_tooltip()
end
function TreeView:get_item_icon(item, active, hovered)
local character = "f"
if item.type == "dir" then
character = item.expanded and "D" or "d"
end
local font = style.icon_font
local color = style.text
if active or hovered then
color = style.accent
end
return character, font, color
end
function TreeView:get_item_text(item, active, hovered)
local text = item.name
local font = style.font
local color = style.text
if active or hovered then
color = style.accent
end
return text, font, color
end
function TreeView:draw_item_text(item, active, hovered, x, y, w, h)
local item_text, item_font, item_color = self:get_item_text(item, active, hovered)
common.draw_text(item_font, item_color, item_text, nil, x, y, 0, h)
end
function TreeView:draw_item_icon(item, active, hovered, x, y, w, h)
local icon_char, icon_font, icon_color = self:get_item_icon(item, active, hovered)
common.draw_text(icon_font, icon_color, icon_char, nil, x, y, 0, h)
return self.item_icon_width + self.item_text_spacing
end
function TreeView:draw_item_body(item, active, hovered, x, y, w, h)
x = x + self:draw_item_icon(item, active, hovered, x, y, w, h)
self:draw_item_text(item, active, hovered, x, y, w, h)
end
function TreeView:draw_item_chevron(item, active, hovered, x, y, w, h)
if item.type == "dir" then
local chevron_icon = item.expanded and "-" or "+"
local chevron_color = hovered and style.accent or style.text
common.draw_text(style.icon_font, chevron_color, chevron_icon, nil, x, y, 0, h)
end
return style.padding.x
end
function TreeView:draw_item_background(item, active, hovered, x, y, w, h)
if hovered then
local hover_color = { table.unpack(style.line_highlight) }
hover_color[4] = 160
renderer.draw_rect(x, y, w, h, hover_color)
elseif active then
renderer.draw_rect(x, y, w, h, style.line_highlight)
end
end
function TreeView:draw_item(item, active, hovered, x, y, w, h)
self:draw_item_background(item, active, hovered, x, y, w, h)
x = x + item.depth * style.padding.x + style.padding.x
x = x + self:draw_item_chevron(item, active, hovered, x, y, w, h)
self:draw_item_body(item, active, hovered, x, y, w, h)
end
function TreeView:draw()
if not self.visible then return end
self:draw_background(style.background2)
local _y, _h = self.position.y, self.size.y
local icon_width = style.icon_font:get_width("D")
local spacing = style.icon_font:get_width("f") / 2
local doc = core.active_view.doc
local active_filename = doc and system.absolute_path(doc.filename or "")
for item, x,y,w,h in self:each_item() do
if y + h >= _y and y < _y + _h then
self:draw_item(item,
item == self.selected_item,
item == self.hovered_item,
x, y, w, h)
local color = style.text
-- highlight active_view doc
if item.abs_filename == active_filename then
color = style.accent
end
-- hovered item background
if item == self.hovered_item then
renderer.draw_rect(x, y, w, h, style.line_highlight)
color = style.accent
end
-- icons
x = x + item.depth * style.padding.x + style.padding.x
if item.type == "dir" then
local icon1 = item.expanded and "-" or "+"
local icon2 = item.expanded and "D" or "d"
common.draw_text(style.icon_font, color, icon1, nil, x, y, 0, h)
x = x + style.padding.x
common.draw_text(style.icon_font, color, icon2, nil, x, y, 0, h)
x = x + icon_width
else
x = x + style.padding.x
common.draw_text(style.icon_font, color, "f", nil, x, y, 0, h)
x = x + icon_width
end
-- text
x = x + spacing
x = common.draw_text(style.font, color, item.name, nil, x, y, 0, h)
end
self:draw_scrollbar()
if self.hovered_item and self.tooltip.x and self.tooltip.alpha > 0 then
if self.hovered_item and self.tooltip.alpha > 0 then
core.root_view:defer_draw(self.draw_tooltip, self)
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
local view = TreeView()
local node = core.root_view:get_active_node()
view.node = node:split("left", view, {x = true}, true)
local treeview_node = node:split("left", view, {x = true}, true)
-- The toolbarview plugin is special because it is plugged inside
-- a treeview pane which is itelf provided in a plugin.
@ -466,13 +353,12 @@ view.node = node:split("left", view, {x = true}, true)
-- plugin to be independent of each other. In addition it is not the
-- plugin module that plug itself in the active node but it is plugged here
-- in the treeview node.
local toolbar_view = nil
local toolbar_plugin, ToolbarView = pcall(require, "plugins.toolbarview")
if config.plugins.toolbarview ~= false and toolbar_plugin then
toolbar_view = ToolbarView()
view.node:split("down", toolbar_view, {y = true})
local toolbar_plugin, ToolbarView = core.try(require, "plugins.toolbarview")
if config.toolbarview ~= false and toolbar_plugin then
local toolbar_view = ToolbarView()
treeview_node:split("down", toolbar_view, {y = true})
local min_toolbar_width = toolbar_view:get_min_width()
view:set_target_size("x", math.max(config.plugins.treeview.size, min_toolbar_width))
view:set_target_size("x", math.max(default_treeview_size, min_toolbar_width))
command.add(nil, {
["toolbar:toggle"] = function()
toolbar_view:toggle_visible()
@ -480,429 +366,12 @@ if config.plugins.toolbarview ~= false and toolbar_plugin then
})
end
-- Add a context menu to the treeview
local menu = ContextMenu()
local on_view_mouse_pressed = RootView.on_view_mouse_pressed
local on_mouse_moved = RootView.on_mouse_moved
local root_view_update = RootView.update
local root_view_draw = RootView.draw
function RootView:on_mouse_moved(...)
if menu:on_mouse_moved(...) then return end
on_mouse_moved(self, ...)
end
function RootView.on_view_mouse_pressed(button, x, y, clicks)
-- We give the priority to the menu to process mouse pressed events.
if button == "right" then
view.tooltip.alpha = 0
view.tooltip.x, view.tooltip.y = nil, nil
end
local handled = menu:on_mouse_pressed(button, x, y, clicks)
return handled or on_view_mouse_pressed(button, x, y, clicks)
end
function RootView:update(...)
root_view_update(self, ...)
menu:update()
end
function RootView:draw(...)
root_view_draw(self, ...)
menu:draw()
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)
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
end
menu:register(function() return view.hovered_item end, {
{ text = "Open in System", command = "treeview:open-in-system" },
ContextMenu.DIVIDER
})
menu:register(
function()
return view.hovered_item
and not is_project_folder(view.hovered_item.abs_filename)
end,
{
{ text = "Rename", command = "treeview:rename" },
{ text = "Delete", command = "treeview:delete" },
}
)
menu:register(
function()
return view.hovered_item and view.hovered_item.type == "dir"
end,
{
{ text = "New File", command = "treeview:new-file" },
{ text = "New Folder", command = "treeview:new-folder" },
}
)
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 commands and keymap
command.add(nil, {
["treeview:toggle"] = function()
view.visible = not view.visible
end,
["treeview:toggle-focus"] = function()
if not core.active_view:is(TreeView) then
if core.active_view:is(CommandView) then
previous_view = core.last_active_view
else
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
else
core.set_active_view(
previous_view or core.root_view:get_primary_node().active_view
)
end
end
})
command.add(TreeView, {
["treeview:next"] = function()
local item, _, item_y = view:get_next(view.selected_item)
view:set_selection(item, item_y)
end,
["treeview:previous"] = function()
local item, _, item_y = view:get_previous(view.selected_item)
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,
["treeview:deselect"] = function()
view.selected_item = nil
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_type = file_info.type == "dir" and "Directory" or "File"
-- Ask before deleting
local opt = {
{ font = style.font, text = "Yes", default_yes = true },
{ font = style.font, text = "No" , default_no = true }
}
core.nag_view:show(
string.format("Delete %s", file_type),
string.format(
"Are you sure you want to delete the %s?\n%s: %s",
file_type:lower(), file_type, relfilename
),
opt,
function(item)
if item.text == "Yes" then
if file_info.type == "dir" then
local deleted, error, path = common.rm(filename, true)
if not deleted then
core.error("Error: %s - \"%s\" ", error, path)
return
end
else
local removed, error = os.remove(filename)
if not removed then
core.error("Error: %s - \"%s\"", error, filename)
return
end
end
core.log("Deleted \"%s\"", filename)
end
end
)
end
})
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,
["treeview:new-file"] = function(item)
local text
if not is_project_folder(item.abs_filename) then
text = item.filename .. PATHSEP
end
core.command_view:enter("Filename", {
text = text,
submit = function(filename)
local doc_filename = 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") or PLATFORM == "MorphOS" 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))
elseif PLATFORM == "AmigaOS 4" then
system.exec(string.format("WBRUN %q SHOW=all VIEWBY=name", 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,
})
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
-- user or plugin modifications
view.toolbar = toolbar_view
view.contextmenu = menu
return view
keymap.add { ["ctrl+\\"] = "treeview:toggle" }

6
data/plugins/trimwhitespace.lua Normal file → Executable file
View File

@ -1,4 +1,4 @@
-- mod-version:3
-- mod-version:1 -- lite-xl 1.16
local core = require "core"
local command = require "core.command"
local Doc = require "core.doc"
@ -24,8 +24,8 @@ end
command.add("core.docview", {
["trim-whitespace:trim-trailing-whitespace"] = function(dv)
trim_trailing_whitespace(dv.doc)
["trim-whitespace:trim-trailing-whitespace"] = function()
trim_trailing_whitespace(core.active_view.doc)
end,
})

4
data/plugins/workspace.lua Normal file → Executable file
View File

@ -1,4 +1,4 @@
-- mod-version:3
-- mod-version:1 -- lite-xl 1.16
local core = require "core"
local common = require "core.common"
local DocView = require "core.docview"
@ -117,7 +117,7 @@ local function load_view(t)
-- cannot be read.
if dv and dv.doc then
dv.doc:set_selection(table.unpack(t.selection))
dv.last_line1, dv.last_col1, dv.last_line2, dv.last_col2 = dv.doc:get_selection()
dv.last_line, dv.last_col = dv.doc:get_selection()
dv.scroll.x, dv.scroll.to.x = t.scroll.x, t.scroll.x
dv.scroll.y, dv.scroll.to.y = t.scroll.y, t.scroll.y
end

View File

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

BIN
dev-utils/icon.icns Normal file

Binary file not shown.

View File

@ -2,9 +2,10 @@
Type=Application
Name=Lite XL
Comment=A lightweight text editor written in Lua
Exec=lite-xl %F
Exec=lite %F
Icon=lite-xl
Terminal=false
StartupWMClass=lite-xl
Categories=Development;IDE;
StartupNotify=false
Categories=Utility;TextEditor;Development;
MimeType=text/plain;

192
dev-utils/lite.svg Normal file
View File

@ -0,0 +1,192 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="640"
height="640.00006"
viewBox="0 0 169.33333 169.33334"
version="1.1"
id="svg8"
inkscape:version="0.92.3 (2405546, 2018-03-11)"
sodipodi:docname="lite.svg">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.83124992"
inkscape:cx="320"
inkscape:cy="320.00003"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
units="px"
inkscape:window-width="1299"
inkscape:window-height="713"
inkscape:window-x="67"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:lockguides="false" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-18.891505,-52.827384)">
<rect
style="opacity:1;fill:#302e31;fill-opacity:1;stroke:none;stroke-width:0.50622272;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:markers fill stroke"
id="rect905"
width="169.33333"
height="169.33333"
x="18.891504"
y="52.827408"
ry="9.2305775" />
<path
style="opacity:1;fill:#49464b;fill-opacity:1;stroke:none;stroke-width:0.1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:markers fill stroke"
d="m 40.05817,87.223213 h 13.229166 v 7.937499 H 40.05817 Z"
id="rect821" />
<path
style="opacity:1;fill:none;fill-opacity:1;stroke:none;stroke-width:0.1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:markers fill stroke"
d="m 61.224838,87.223213 h 18.520834 v 7.937499 H 61.224838 Z"
id="rect823" />
<path
style="opacity:1;fill:#d4d2d7;fill-opacity:1;stroke:none;stroke-width:0.1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:markers fill stroke"
d="M 79.745674,87.223213 H 122.079 v 7.937499 H 79.745674 Z"
id="rect825" />
<path
style="opacity:1;fill:#d4d2d7;fill-opacity:1;stroke:none;stroke-width:0.1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:markers fill stroke"
d="m 124.72483,87.223213 h 5.29167 v 7.937499 h -5.29167 z"
id="rect827" />
<path
style="opacity:1;fill:#f1a04a;fill-opacity:1;stroke:none;stroke-width:0.1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:markers fill stroke"
d="m 132.66232,87.223213 h 21.16667 v 7.937499 h -21.16667 z"
id="rect829" />
<path
style="opacity:1;fill:#49464b;fill-opacity:1;stroke:none;stroke-width:0.1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:markers fill stroke"
d="m 40.05817,100.45238 h 13.229166 v 7.93749 H 40.05817 Z"
id="rect831" />
<path
style="opacity:1;fill:#49464b;fill-opacity:1;stroke:none;stroke-width:0.1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:markers fill stroke"
d="m 40.05817,113.68155 h 13.229166 v 7.9375 H 40.05817 Z"
id="rect833" />
<path
style="opacity:1;fill:#646363;fill-opacity:1;stroke:none;stroke-width:0.1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:markers fill stroke"
d="m 58.579006,113.68155 h 13.229166 v 7.9375 H 58.579006 Z"
id="rect835" />
<path
style="opacity:1;fill:#646363;fill-opacity:1;stroke:none;stroke-width:0.1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:markers fill stroke"
d="m 74.454002,113.68155 h 15.875 v 7.9375 h -15.875 z"
id="rect837" />
<path
style="opacity:1;fill:#646363;fill-opacity:1;stroke:none;stroke-width:0.1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:markers fill stroke"
d="m 92.974831,113.68155 h 26.458339 v 7.9375 H 92.974831 Z"
id="rect839" />
<path
style="opacity:1;fill:#646363;fill-opacity:1;stroke:none;stroke-width:0.1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:markers fill stroke"
d="m 122.07899,113.68155 h 10.58333 v 7.9375 h -10.58333 z"
id="rect841" />
<path
style="opacity:1;fill:#646363;fill-opacity:1;stroke:none;stroke-width:0.1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:markers fill stroke"
d="m 137.95399,113.68155 h 15.875 v 7.9375 h -15.875 z"
id="rect843" />
<path
style="opacity:1;fill:#49464b;fill-opacity:1;stroke:none;stroke-width:0.1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:markers fill stroke"
d="m 40.05817,126.91071 h 13.229166 v 7.9375 H 40.05817 Z"
id="rect845" />
<path
style="opacity:1;fill:#e77280;fill-opacity:1;stroke:none;stroke-width:0.1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:markers fill stroke"
d="M 58.579006,126.91071 H 77.09984 v 7.9375 H 58.579006 Z"
id="rect847" />
<path
style="opacity:1;fill:#d4d2d7;fill-opacity:1;stroke:none;stroke-width:0.1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:markers fill stroke"
d="m 77.099838,126.91071 h 58.208332 v 7.93751 H 77.099838 Z"
id="rect849" />
<path
style="opacity:1;fill:#49464b;fill-opacity:1;stroke:none;stroke-width:0.1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:markers fill stroke"
d="m 40.05817,140.13988 h 13.229166 v 7.9375 H 40.05817 Z"
id="rect851" />
<path
style="opacity:1;fill:#d4d2d7;fill-opacity:1;stroke:none;stroke-width:0.1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:markers fill stroke"
d="m 58.579006,140.13988 h 66.145824 v 7.9375 H 58.579006 Z"
id="rect853"
inkscape:connector-curvature="0" />
<path
style="opacity:1;fill:#d682be;fill-opacity:1;stroke:none;stroke-width:0.1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:markers fill stroke"
d="m 124.72483,140.13988 h 37.04167 v 7.9375 h -37.04167 z"
id="rect855" />
<path
style="opacity:1;fill:#d4d2d7;fill-opacity:1;stroke:none;stroke-width:0.1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:markers fill stroke"
d="m 161.76651,140.13988 h 5.29165 v 7.9375 h -5.29165 z"
id="rect857" />
<path
style="opacity:1;fill:#49464b;fill-opacity:1;stroke:none;stroke-width:0.1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:markers fill stroke"
d="m 40.05817,153.36905 h 13.229166 v 7.9375 H 40.05817 Z"
id="rect859" />
<path
style="opacity:1;fill:#d682be;fill-opacity:1;stroke:none;stroke-width:0.1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:markers fill stroke"
d="m 69.162338,153.36905 h 23.812498 v 7.9375 H 69.162338 Z"
id="rect861" />
<path
style="opacity:1;fill:#f1a04a;fill-opacity:1;stroke:none;stroke-width:0.1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:markers fill stroke"
d="m 95.620674,153.36905 h 13.229156 v 7.9375 H 95.620674 Z"
id="rect863" />
<path
style="opacity:1;fill:#d682be;fill-opacity:1;stroke:none;stroke-width:0.1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:markers fill stroke"
d="M 111.49567,153.36905 H 122.079 v 7.9375 h -10.58333 z"
id="rect865" />
<path
style="opacity:1;fill:#49464b;fill-opacity:1;stroke:none;stroke-width:0.1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:markers fill stroke"
d="m 40.05817,166.59822 h 13.229166 v 7.9375 H 40.05817 Z"
id="rect867" />
<path
style="opacity:1;fill:#49464b;fill-opacity:1;stroke:none;stroke-width:0.1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:markers fill stroke"
d="m 40.05817,179.82738 h 13.229166 v 7.9375 H 40.05817 Z"
id="rect869" />
<path
style="opacity:1;fill:#d682be;fill-opacity:1;stroke:none;stroke-width:0.1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:markers fill stroke"
d="m 58.579006,179.82738 h 13.229166 v 7.9375 H 58.579006 Z"
id="rect871" />
<path
style="opacity:1;fill:#e77280;fill-opacity:1;stroke:none;stroke-width:0.1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:markers fill stroke"
d="m 74.454002,179.82738 h 15.875 v 7.9375 h -15.875 z"
id="rect873" />
<path
style="opacity:1;fill:#d4d2d7;fill-opacity:1;stroke:none;stroke-width:0.1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:markers fill stroke"
d="M 90.329002,179.82738 H 137.954 v 7.9375 H 90.329002 Z"
id="rect875"
inkscape:connector-curvature="0" />
<rect
style="opacity:1;fill:#e77280;fill-opacity:1;stroke:none;stroke-width:0.52916676;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:markers fill stroke"
id="rect14"
width="18.520836"
height="7.9375005"
x="61.224838"
y="87.223213" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 12 KiB

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