Compare commits
2 Commits
amiga2.1
...
context-me
Author | SHA1 | Date |
---|---|---|
takase1121 | e3c42d865e | |
takase1121 | 084012ed60 |
|
@ -6,6 +6,3 @@ indent_size = 2
|
|||
indent_style = space
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[meson.build]
|
||||
indent_size = 4
|
||||
|
|
|
@ -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/**/*
|
|
@ -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
|
|
@ -4,17 +4,14 @@ 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,204 +21,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@v3
|
||||
- name: Python Setup
|
||||
uses: actions/setup-python@v4
|
||||
- uses: actions/checkout@v2
|
||||
- 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@v3
|
||||
if: ${{ matrix.config.cc == 'gcc' }}
|
||||
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: Linux Artifacts
|
||||
path: ${{ env.INSTALL_NAME }}.tar.gz
|
||||
name: Ubuntu Package
|
||||
path: lite-xl-linux-*.tar.gz
|
||||
|
||||
build_macos:
|
||||
name: macOS
|
||||
runs-on: macos-11
|
||||
env:
|
||||
CC: clang
|
||||
CXX: clang++
|
||||
strategy:
|
||||
matrix:
|
||||
arch: ['x86_64', 'arm64']
|
||||
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-${{ matrix.arch }}" >> "$GITHUB_ENV"
|
||||
if [[ $(uname -m) != ${{ matrix.arch }} ]]; then echo "ARCH=--cross-arch ${{ matrix.arch }}" >> "$GITHUB_ENV"; fi
|
||||
- uses: actions/checkout@v3
|
||||
- name: Python Setup
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.9
|
||||
- name: Install Dependencies
|
||||
# --lhelper will eliminate a warning with arm64 and libusb
|
||||
run: bash scripts/install-dependencies.sh --debug --lhelper
|
||||
- name: Build
|
||||
run: |
|
||||
bash --version
|
||||
bash scripts/build.sh --bundle --debug --forcefallback $ARCH
|
||||
- name: Create DMG Image
|
||||
run: bash scripts/package.sh --version ${INSTALL_REF} $ARCH --debug --dmg
|
||||
- name: Upload DMG Image
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: macOS DMG Images
|
||||
path: ${{ env.INSTALL_NAME }}.dmg
|
||||
|
||||
build_macos_universal:
|
||||
name: macOS (Universal)
|
||||
runs-on: macos-11
|
||||
needs: build_macos
|
||||
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_NAME=lite-xl-${GITHUB_REF##*/}-macos-universal" >> "$GITHUB_ENV"
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.9'
|
||||
- name: Install dmgbuild
|
||||
run: pip install dmgbuild
|
||||
- uses: actions/checkout@v3
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v3
|
||||
id: download
|
||||
with:
|
||||
name: macOS DMG Images
|
||||
path: dmgs-original
|
||||
- name: Make universal bundles
|
||||
run: |
|
||||
bash --version
|
||||
bash scripts/make-universal-binaries.sh ${{ steps.download.outputs.download-path }} "${INSTALL_NAME}"
|
||||
- name: Upload DMG Image
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: macOS Universal DMG Images
|
||||
path: ${{ env.INSTALL_NAME }}.dmg
|
||||
|
||||
build_windows_msys2:
|
||||
name: Windows
|
||||
runs-on: windows-2019
|
||||
build-macox:
|
||||
name: Build Mac OS X
|
||||
runs-on: macos-10.15
|
||||
strategy:
|
||||
matrix:
|
||||
config:
|
||||
- {msystem: MINGW32, arch: i686}
|
||||
- {msystem: MINGW64, arch: x86_64}
|
||||
defaults:
|
||||
run:
|
||||
shell: msys2 {0}
|
||||
# - { 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@v3
|
||||
- uses: msys2/setup-msys2@v2
|
||||
with:
|
||||
msystem: ${{ matrix.config.msystem }}
|
||||
install: >-
|
||||
base-devel
|
||||
git
|
||||
zip
|
||||
mingw-w64-${{ matrix.config.arch }}-gcc
|
||||
mingw-w64-${{ matrix.config.arch }}-meson
|
||||
mingw-w64-${{ matrix.config.arch }}-ninja
|
||||
mingw-w64-${{ matrix.config.arch }}-ca-certificates
|
||||
mingw-w64-${{ matrix.config.arch }}-ntldd
|
||||
- name: Set Environment Variables
|
||||
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
|
||||
if: false
|
||||
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@v3
|
||||
with:
|
||||
name: Windows Artifacts
|
||||
path: ${{ env.INSTALL_NAME }}.zip
|
||||
|
||||
build_windows_msvc:
|
||||
name: Windows (MSVC)
|
||||
runs-on: windows-2019
|
||||
strategy:
|
||||
matrix:
|
||||
arch:
|
||||
- { target: x86, name: i686 }
|
||||
- { target: x64, name: x86_64 }
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ilammy/msvc-dev-cmd@v1
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
arch: ${{ matrix.arch.target }}
|
||||
- uses: actions/setup-python@v4
|
||||
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:
|
||||
python-version: '3.x'
|
||||
- name: Install meson and ninja
|
||||
run: pip install meson ninja
|
||||
- name: Set up environment variables
|
||||
run: |
|
||||
"INSTALL_NAME=lite-xl-$($env:GITHUB_REF -replace ".*/")-windows-msvc-${{ matrix.arch.name }}" >> $env:GITHUB_ENV
|
||||
"INSTALL_REF=$($env:GITHUB_REF -replace ".*/")" >> $env:GITHUB_ENV
|
||||
"LUA_SUBPROJECT_PATH=subprojects/$(awk -F ' *= *' '/directory/ { printf $2 }' subprojects/lua.wrap)" >> $env:GITHUB_ENV
|
||||
- name: Download and patch subprojects
|
||||
shell: bash
|
||||
run: |
|
||||
meson subprojects download
|
||||
cat resources/windows/001-lua-unicode.diff | patch -Np1 -d "$LUA_SUBPROJECT_PATH"
|
||||
- name: Configure
|
||||
run: |
|
||||
meson setup --wrap-mode=forcefallback build
|
||||
- name: Build
|
||||
run: |
|
||||
meson install -C build --destdir="../lite-xl"
|
||||
- name: Package
|
||||
run: |
|
||||
Remove-Item -Recurse -Force -Path "lite-xl/lib","lite-xl/include"
|
||||
Compress-Archive -Path lite-xl -DestinationPath "$env:INSTALL_NAME.zip"
|
||||
- name: Upload Artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: Windows Artifacts (MSVC)
|
||||
path: ${{ env.INSTALL_NAME }}.zip
|
||||
name: Mac OS X Package
|
||||
path: lite-xl-macosx-*.zip
|
||||
|
|
|
@ -1,267 +0,0 @@
|
|||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v[0-9]+.*
|
||||
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: Release Version
|
||||
default: v2.1.4
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Create Release
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
outputs:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
version: ${{ steps.tag.outputs.version }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
- name: Fetch Version
|
||||
id: tag
|
||||
run: |
|
||||
if [[ "${{ github.event.inputs.version }}" != "" ]]; then
|
||||
echo ::set-output name=version::${{ github.event.inputs.version }}
|
||||
else
|
||||
echo ::set-output name=version::${GITHUB_REF/refs\/tags\//}
|
||||
fi
|
||||
- name: Update Tag
|
||||
uses: richardsimko/update-tag@v1
|
||||
with:
|
||||
tag_name: ${{ steps.tag.outputs.version }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
tag_name: ${{ steps.tag.outputs.version }}
|
||||
name: Lite XL ${{ steps.tag.outputs.version }}
|
||||
draft: true
|
||||
body_path: changelog.md
|
||||
generate_release_notes: true
|
||||
|
||||
build_linux:
|
||||
name: Linux
|
||||
needs: release
|
||||
runs-on: ubuntu-latest
|
||||
container: ghcr.io/lite-xl/lite-xl-build-box:latest
|
||||
env:
|
||||
CC: gcc
|
||||
CXX: g++
|
||||
steps:
|
||||
- name: Set Environment Variables
|
||||
run: |
|
||||
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
||||
echo "INSTALL_REF=${{ needs.release.outputs.version }}" >> "$GITHUB_ENV"
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
# disabled because this will break our own Python install
|
||||
- name: Python Setup
|
||||
if: false
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.9
|
||||
|
||||
# disabled because the container has up-to-date packages
|
||||
- name: Update Packages
|
||||
if: false
|
||||
run: sudo apt-get update
|
||||
|
||||
# disabled as the dependencies are already installed
|
||||
- name: Install Dependencies
|
||||
if: false
|
||||
run: |
|
||||
bash scripts/install-dependencies.sh --debug
|
||||
sudo apt-get install -y ccache
|
||||
|
||||
- name: Build Portable
|
||||
run: |
|
||||
bash --version
|
||||
bash scripts/build.sh --debug --forcefallback --portable --release
|
||||
- name: Package Portables
|
||||
run: |
|
||||
bash scripts/package.sh --version ${INSTALL_REF} --debug --binary --release
|
||||
bash scripts/package.sh --version ${INSTALL_REF} --debug --addons --binary --release
|
||||
- name: Build AppImages
|
||||
run: |
|
||||
bash scripts/appimage.sh --debug --static --version ${INSTALL_REF} --release
|
||||
bash scripts/appimage.sh --debug --nobuild --addons --version ${INSTALL_REF}
|
||||
- name: Upload Files
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
tag_name: ${{ needs.release.outputs.version }}
|
||||
draft: true
|
||||
files: |
|
||||
lite-xl-${{ env.INSTALL_REF }}-linux-x86_64-portable.tar.gz
|
||||
lite-xl-${{ env.INSTALL_REF }}-addons-linux-x86_64-portable.tar.gz
|
||||
LiteXL-${{ env.INSTALL_REF }}-x86_64.AppImage
|
||||
LiteXL-${{ env.INSTALL_REF }}-addons-x86_64.AppImage
|
||||
|
||||
build_macos:
|
||||
name: macOS
|
||||
needs: release
|
||||
runs-on: macos-11
|
||||
strategy:
|
||||
matrix:
|
||||
arch: [x86_64, arm64]
|
||||
env:
|
||||
CC: clang
|
||||
CXX: clang++
|
||||
steps:
|
||||
- name: System Information
|
||||
run: |
|
||||
system_profiler SPSoftwareDataType
|
||||
bash --version
|
||||
gcc -v
|
||||
xcodebuild -version
|
||||
- name: Set Environment Variables
|
||||
run: |
|
||||
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
||||
echo "INSTALL_REF=${{ needs.release.outputs.version }}" >> "$GITHUB_ENV"
|
||||
echo "INSTALL_NAME=lite-xl-${{ needs.release.outputs.version }}-macos-${{ matrix.arch }}" >> "$GITHUB_ENV"
|
||||
echo "INSTALL_NAME_ADDONS=lite-xl-${{ needs.release.outputs.version }}-addons-macos-${{ matrix.arch }}" >> "$GITHUB_ENV"
|
||||
if [[ $(uname -m) != ${{ matrix.arch }} ]]; then echo "ARCH=--cross-arch ${{ matrix.arch }}" >> "$GITHUB_ENV"; fi
|
||||
- uses: actions/checkout@v3
|
||||
- name: Python Setup
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.9
|
||||
- name: Install Dependencies
|
||||
run: bash scripts/install-dependencies.sh --debug
|
||||
- name: Build
|
||||
run: |
|
||||
bash --version
|
||||
bash scripts/build.sh --bundle --debug --forcefallback --release $ARCH
|
||||
- name: Create DMG Image
|
||||
run: |
|
||||
bash scripts/package.sh --version ${INSTALL_REF} $ARCH --debug --dmg --release
|
||||
bash scripts/package.sh --version ${INSTALL_REF} $ARCH --debug --addons --dmg --release
|
||||
- name: Upload Artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: macOS DMG Images
|
||||
path: |
|
||||
${{ env.INSTALL_NAME }}.dmg
|
||||
${{ env.INSTALL_NAME_ADDONS }}.dmg
|
||||
- name: Upload Files
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
tag_name: ${{ needs.release.outputs.version }}
|
||||
draft: true
|
||||
files: |
|
||||
${{ env.INSTALL_NAME }}.dmg
|
||||
${{ env.INSTALL_NAME_ADDONS }}.dmg
|
||||
|
||||
build_macos_universal:
|
||||
name: macOS (Universal)
|
||||
needs: [release, build_macos]
|
||||
runs-on: macos-11
|
||||
steps:
|
||||
- name: System Information
|
||||
run: |
|
||||
system_profiler SPSoftwareDataType
|
||||
bash --version
|
||||
gcc -v
|
||||
xcodebuild -version
|
||||
- name: Set Environment Variables
|
||||
run: |
|
||||
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
||||
echo "INSTALL_BASE=lite-xl-${{ needs.release.outputs.version }}-macos" >> "$GITHUB_ENV"
|
||||
echo "INSTALL_BASE_ADDONS=lite-xl-${{ needs.release.outputs.version }}-addons-macos" >> "$GITHUB_ENV"
|
||||
- uses: actions/checkout@v2
|
||||
- name: Download Artifacts
|
||||
uses: actions/download-artifact@v3
|
||||
id: download
|
||||
with:
|
||||
name: macOS DMG Images
|
||||
path: dmgs-original
|
||||
- name: Python Setup
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.9
|
||||
- name: Install dmgbuild
|
||||
run: pip install dmgbuild
|
||||
- name: Prepare DMG Images
|
||||
run: |
|
||||
mkdir -p dmgs-addons dmgs-normal
|
||||
mv -v "${{ steps.download.outputs.download-path }}/$INSTALL_BASE-"{x86_64,arm64}.dmg dmgs-normal
|
||||
mv -v "${{ steps.download.outputs.download-path }}/$INSTALL_BASE_ADDONS-"{x86_64,arm64}.dmg dmgs-addons
|
||||
- name: Create Universal DMGs
|
||||
run: |
|
||||
bash --version
|
||||
bash scripts/make-universal-binaries.sh dmgs-normal "$INSTALL_BASE-universal"
|
||||
bash scripts/make-universal-binaries.sh dmgs-addons "$INSTALL_BASE_ADDONS-universal"
|
||||
- name: Upload Files
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
tag_name: ${{ needs.release.outputs.version }}
|
||||
draft: true
|
||||
files: |
|
||||
${{ env.INSTALL_BASE }}-universal.dmg
|
||||
${{ env.INSTALL_BASE_ADDONS }}-universal.dmg
|
||||
|
||||
build_windows_msys2:
|
||||
name: Windows
|
||||
needs: release
|
||||
runs-on: windows-2019
|
||||
strategy:
|
||||
matrix:
|
||||
msystem: [MINGW32, MINGW64]
|
||||
defaults:
|
||||
run:
|
||||
shell: msys2 {0}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: msys2/setup-msys2@v2
|
||||
with:
|
||||
msystem: ${{ matrix.msystem }}
|
||||
update: true
|
||||
install: >-
|
||||
base-devel
|
||||
git
|
||||
zip
|
||||
- name: Set Environment Variables
|
||||
run: |
|
||||
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
||||
echo "INSTALL_REF=${{ needs.release.outputs.version }}" >> "$GITHUB_ENV"
|
||||
if [[ "${MSYSTEM}" == "MINGW64" ]]; then
|
||||
echo "BUILD_ARCH=x86_64" >> "$GITHUB_ENV"
|
||||
echo "INSTALL_NAME=lite-xl-${{ needs.release.outputs.version }}-windows-x86_64" >> "$GITHUB_ENV"
|
||||
echo "INSTALL_NAME_ADDONS=lite-xl-${{ needs.release.outputs.version }}-addons-windows-x86_64" >> "$GITHUB_ENV"
|
||||
else
|
||||
echo "BUILD_ARCH=i686" >> "$GITHUB_ENV"
|
||||
echo "INSTALL_NAME=lite-xl-${{ needs.release.outputs.version }}-windows-i686" >> "$GITHUB_ENV"
|
||||
echo "INSTALL_NAME_ADDONS=lite-xl-${{ needs.release.outputs.version }}-addons-windows-i686" >> "$GITHUB_ENV"
|
||||
fi
|
||||
- name: Install Dependencies
|
||||
run: bash scripts/install-dependencies.sh --debug
|
||||
- name: Build
|
||||
run: |
|
||||
bash --version
|
||||
bash scripts/build.sh -U --debug --forcefallback --release
|
||||
- name: Package
|
||||
run: bash scripts/package.sh --version ${INSTALL_REF} --debug --binary --release
|
||||
- name: Build Installer
|
||||
run: bash scripts/innosetup/innosetup.sh --debug --version ${INSTALL_REF}
|
||||
- name: Package With Addons
|
||||
run: bash scripts/package.sh --version ${INSTALL_REF} --debug --addons --binary --release
|
||||
- name: Build Installer With Addons
|
||||
run: bash scripts/innosetup/innosetup.sh --debug --version ${INSTALL_REF} --addons
|
||||
- name: Upload Files
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
tag_name: ${{ needs.release.outputs.version }}
|
||||
draft: true
|
||||
files: |
|
||||
${{ env.INSTALL_NAME }}.zip
|
||||
${{ env.INSTALL_NAME_ADDONS }}.zip
|
||||
LiteXL-${{ env.INSTALL_REF }}-${{ env.BUILD_ARCH }}-setup.exe
|
||||
LiteXL-${{ env.INSTALL_REF }}-addons-${{ env.BUILD_ARCH }}-setup.exe
|
|
@ -1,31 +1,13 @@
|
|||
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
|
||||
*.tar.gz
|
||||
.lite-debug.log
|
||||
subprojects/lua
|
||||
subprojects/libagg
|
||||
subprojects/reproc
|
||||
lite-xl
|
||||
error.txt
|
||||
lite-xl*
|
||||
LiteXL*
|
||||
lite
|
||||
.config/
|
||||
*.lha
|
||||
*.o
|
||||
*.snalyzerinfo
|
||||
|
||||
|
||||
!resources/windows/*.diff
|
||||
!resources/windows/*.exe.manifest.in
|
||||
!resources/macos/*.py
|
||||
.ccls-cache
|
||||
compile_commands.json
|
||||
|
|
4
LICENSE
4
LICENSE
|
@ -1,6 +1,4 @@
|
|||
Copyright (c) 2020 rxi
|
||||
Copyright (c) 2020-2022 Francesco Abbate
|
||||
Copyright (c) 2022-present 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
|
||||
|
|
89
Makefile.mos
89
Makefile.mos
|
@ -1,89 +0,0 @@
|
|||
#
|
||||
# Project: Lite XL
|
||||
#
|
||||
# Created on: 26-12-2021
|
||||
#
|
||||
|
||||
LiteXL_OBJ := \
|
||||
src/main.o src/rencache.o src/renderer.o src/renwindow.o \
|
||||
src/api/api.o src/api/dirmonitor.o \
|
||||
src/api/regex.o src/api/renderer.o src/api/system.o \
|
||||
src/api/utf8.o src/platform/morphos.o \
|
||||
src/api/dirmonitor/mos.o src/platform/codesets.o
|
||||
|
||||
outfile := lite-xl
|
||||
compiler := ppc-morphos-gcc-11
|
||||
cxxcompiler := ppc-morphos-g++-11
|
||||
|
||||
INCPATH := -Isrc -Ilib/dmon -I/sdk/gg/usr/local/include/SDL2 \
|
||||
-I/sdk/gg/usr/include/freetype -I/sdk/gg/usr/include/lua5.4
|
||||
DFLAGS ?= -D__USE_INLINE__
|
||||
CFLAGS ?= -Wall -Wwrite-strings -O2 -noixemul -g -std=gnu11 -fno-strict-aliasing
|
||||
LFLAGS ?= -noixemul -lpcre2-8 -lSDL2 -llua54 -lagg -lfreetype -lm -lc -L/usr/local/lib
|
||||
|
||||
ifeq ($(DEBUG),1)
|
||||
CFLAGS += -g -gstabs
|
||||
LFLAGS += -gstabs
|
||||
endif
|
||||
|
||||
.PHONY: LiteXL clean release
|
||||
|
||||
default: LiteXL
|
||||
|
||||
clean:
|
||||
@echo "Cleaning compiler objects..."
|
||||
@rm -f $(LiteXL_OBJ)
|
||||
|
||||
LiteXL: $(LiteXL_OBJ)
|
||||
@echo "Linking LiteXL"
|
||||
$(compiler) -o $(outfile) $(LiteXL_OBJ) $(LFLAGS)
|
||||
|
||||
.c.o:
|
||||
@echo "Compiling $<"
|
||||
$(compiler) -c $< -o $*.o $(CFLAGS) $(INCPATH) $(DFLAGS)
|
||||
|
||||
src/main.o: src/main.c src/api/api.h src/rencache.h \
|
||||
src/renderer.h src/platform/morphos.h
|
||||
|
||||
src/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
|
||||
|
||||
src/platform/codesets.o: src/platform/codesets.c
|
||||
|
||||
release: clean LiteXL
|
||||
@echo "Creating release files..."
|
||||
@mkdir -p release/LiteXL2
|
||||
@cp -r resources/amiga/* release/LiteXL2/
|
||||
@mv release/LiteXL2/LiteXL2.info release/
|
||||
@rm release/LiteXL2/AutoInstall
|
||||
@cp -r data release/LiteXL2/
|
||||
@cp changelog.md release/LiteXL2/
|
||||
@cp $(outfile) release/LiteXL2/
|
||||
@strip release/LiteXL2/$(outfile)
|
||||
@cp README.md release/LiteXL2/
|
||||
@cp README_Amiga.md release/LiteXL2/
|
||||
@cp LICENSE release/LiteXL2/
|
||||
@cp -r licenses release/LiteXL2/
|
||||
@echo "Creating release archive..."
|
||||
@lha -aeqr3 a LiteXL2_MOS.lha release/
|
||||
@echo "Clean release files..."
|
||||
@delete release ALL QUIET FORCE
|
102
Makefile.os4
102
Makefile.os4
|
@ -1,102 +0,0 @@
|
|||
#
|
||||
# Project: Lite XL
|
||||
#
|
||||
# Created on: 26-12-2021
|
||||
#
|
||||
|
||||
LiteXL_OBJ := \
|
||||
src/main.o src/rencache.o src/renderer.o src/renwindow.o \
|
||||
src/api/api.o src/api/dirmonitor.o \
|
||||
src/api/regex.o src/api/renderer.o src/api/system.o \
|
||||
src/api/utf8.o src/platform/amigaos4.o \
|
||||
src/api/dirmonitor/os4.o src/platform/codesets.o
|
||||
|
||||
outfile := lite-xl
|
||||
compiler := gcc
|
||||
cxxcompiler := g++
|
||||
|
||||
INCPATH := -Isrc -I/sdk/local/newlib/include/SDL2 \
|
||||
-I/sdk/local/common/include/lua54 -I/sdk/local/common/include/freetype2
|
||||
|
||||
DFLAGS += -D__USE_INLINE__ -DLITE_XL_DATA_USE_EXEDIR
|
||||
CFLAGS ?= -Werror -Wwrite-strings -O3 -std=gnu11 -fno-strict-aliasing
|
||||
LFLAGS ?= -mcrt=newlib -lpcre2-8 -lSDL2 -llua54 -lfreetype -lpng -lz \
|
||||
-lpthread -athread=native
|
||||
|
||||
ifeq ($(DEBUG),1)
|
||||
CFLAGS += -g -gstabs
|
||||
LFLAGS += -gstabs
|
||||
|
||||
ifeq ($(PROFYLER),1)
|
||||
CFLAGS += -finstrument-functions -fno-inline -DPROFILING
|
||||
LFLAGS += -lprofyle
|
||||
endif
|
||||
|
||||
endif
|
||||
|
||||
|
||||
.PHONY: LiteXL clean release
|
||||
|
||||
default: LiteXL
|
||||
|
||||
clean:
|
||||
@echo "Cleaning compiler objects..."
|
||||
@rm -f $(LiteXL_OBJ)
|
||||
|
||||
LiteXL: $(LiteXL_OBJ)
|
||||
@echo "Linking LiteXL"
|
||||
@$(compiler) -o $(outfile) $(LiteXL_OBJ) $(LFLAGS)
|
||||
|
||||
|
||||
.c.o:
|
||||
@echo "Compiling $<"
|
||||
@$(compiler) -c $< -o $*.o $(CFLAGS) $(INCPATH) $(DFLAGS)
|
||||
|
||||
|
||||
src/main.o: src/main.c src/api/api.h src/rencache.h \
|
||||
src/renderer.h src/platform/amigaos4.h src/platform/codesets.h
|
||||
|
||||
src/rencache.o: src/rencache.c
|
||||
|
||||
src/renderer.o: src/renderer.c
|
||||
|
||||
src/renwindow.o: src/renwindow.c
|
||||
|
||||
src/api/api.o: src/api/api.c
|
||||
|
||||
src/api/regex.o: src/api/regex.c
|
||||
|
||||
src/api/renderer.o: src/api/renderer.c
|
||||
|
||||
src/api/system.o: src/api/system.c src/platform/amigaos4.h
|
||||
|
||||
src/platform/amigaos4.o: src/platform/amigaos4.c
|
||||
|
||||
src/platform/codesets.o: src/platform/codesets.c
|
||||
|
||||
src/api/dirmonitor.o: src/api/dirmonitor.c src/api/dirmonitor/os4.c
|
||||
|
||||
src/api/utf8.o: src/api/utf8.c src/platform/amigaos4.h
|
||||
|
||||
src/api/dirmonitor/os4.o: src/api/dirmonitor/os4.c
|
||||
|
||||
src/api/process.o: src/api/process.c
|
||||
|
||||
release: clean LiteXL
|
||||
@echo "Creating release files..."
|
||||
@mkdir -p release/LiteXL2
|
||||
@cp -r resources/amiga/* release/LiteXL2/
|
||||
@mv release/LiteXL2/LiteXL2.info release/
|
||||
@mv release/LiteXL2/AutoInstall release/
|
||||
@cp -r data release/LiteXL2/
|
||||
@cp changelog.md release/LiteXL2/
|
||||
@cp $(outfile) release/LiteXL2/
|
||||
@strip release/LiteXL2/$(outfile)
|
||||
@cp README.md release/LiteXL2/
|
||||
@cp README_Amiga.md release/LiteXL2/
|
||||
@cp LICENSE release/LiteXL2/
|
||||
@cp -r licenses release/LiteXL2/
|
||||
@echo "Creating release archive..."
|
||||
@lha -aeqr3 a LiteXL2_OS4.lha release/
|
||||
@echo "Clean release files..."
|
||||
@delete release ALL QUIET FORCE
|
151
README.md
151
README.md
|
@ -1,33 +1,32 @@
|
|||
# Lite XL
|
||||
|
||||
[![CI]](https://github.com/lite-xl/lite-xl/actions/workflows/build.yml)
|
||||
[![Discord Badge Image]](https://discord.gg/RWzqC3nx7K)
|
||||
|
||||
![screenshot-dark]
|
||||
|
||||
A lightweight text editor written in Lua, adapted from [lite].
|
||||
|
||||
* **[Get Lite XL]** — Download for Windows, Linux and Mac OS.
|
||||
* **[Get Lite XL]** — Download for Windows, Linux and Mac OS (notarized app).
|
||||
* **[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.
|
||||
including [build] instructions.
|
||||
|
||||
Lite XL has support for high DPI display on Windows and Linux and,
|
||||
since 1.16.7 release, it supports **retina displays** on macOS.
|
||||
|
||||
Please note that Lite XL is compatible with lite for most plugins and all color themes.
|
||||
We provide a separate lite-xl-plugins repository for Lite XL, because in some cases
|
||||
We provide a separate lite-plugins repository for Lite XL, because in some cases
|
||||
some adaptations may be needed to make them work better with Lite XL.
|
||||
The repository with modified plugins is https://github.com/lite-xl/lite-xl-plugins.
|
||||
The repository with modified plugins is https://github.com/franko/lite-plugins.
|
||||
|
||||
The changes and differences between Lite XL and rxi/lite are listed in the
|
||||
[changelog].
|
||||
|
||||
## Overview
|
||||
|
||||
Lite XL is derived from [lite].
|
||||
Lite XL is derived from lite.
|
||||
It is a lightweight text editor written mostly in Lua — it aims to provide
|
||||
something practical, pretty, *small* and fast easy to modify and extend,
|
||||
or to use without doing either.
|
||||
|
@ -43,128 +42,13 @@ the [plugins repository] or in the [Lite XL plugins repository].
|
|||
Additional color themes can be found in the [colors repository].
|
||||
These color themes are bundled with all releases of Lite XL by default.
|
||||
|
||||
## Quick Build Guide
|
||||
|
||||
If you compile Lite XL yourself, it is recommended to use the script
|
||||
`build-packages.sh`:
|
||||
|
||||
```sh
|
||||
bash build-packages.sh -h
|
||||
```
|
||||
|
||||
The script will run Meson and create a tar compressed archive with the application or,
|
||||
for Windows, a zip file. Lite XL can be easily installed
|
||||
by unpacking the archive in any directory of your choice.
|
||||
|
||||
Otherwise the following is an example of basic commands if you want to customize
|
||||
the build:
|
||||
|
||||
```sh
|
||||
meson setup --buildtype=release --prefix <prefix> build
|
||||
meson compile -C build
|
||||
DESTDIR="$(pwd)/lite-xl" meson install --skip-subprojects -C build
|
||||
```
|
||||
|
||||
where `<prefix>` might be one of `/`, `/usr` or `/opt`, the default is `/`.
|
||||
To build a bundle application on macOS:
|
||||
|
||||
```sh
|
||||
meson setup --buildtype=release --Dbundle=true --prefix / build
|
||||
meson compile -C build
|
||||
DESTDIR="$(pwd)/Lite XL.app" meson install --skip-subprojects -C build
|
||||
```
|
||||
|
||||
Please note that the package is relocatable to any prefix and the option prefix
|
||||
affects only the place where the application is actually installed.
|
||||
|
||||
## Installing Prebuilt
|
||||
|
||||
Head over to [releases](https://github.com/lite-xl/lite-xl/releases) and download the version for your operating system.
|
||||
|
||||
### Windows
|
||||
|
||||
Lite XL comes with installers on Windows for typical installations.
|
||||
Alternatively, we provide ZIP archives that you can download and extract anywhere and run directly.
|
||||
|
||||
To make Lite XL portable (e.g. running Lite XL from a thumb drive),
|
||||
simply create a `user` folder where `lite-xl.exe` is located.
|
||||
Lite XL will load and store all your configurations and plugins in the folder.
|
||||
|
||||
### macOS
|
||||
|
||||
We provide DMG files for macOS. Simply drag the program into your Applications folder.
|
||||
|
||||
> **Important**
|
||||
> Newer versions of Lite XL are signed with a self-signed certificate,
|
||||
> so you'll have to follow these steps when running Lite XL for the first time.
|
||||
>
|
||||
> 1. Find Lite XL in Finder (do not open it in Launchpad).
|
||||
> 2. Control-click Lite XL, then choose `Open` from the shortcut menu.
|
||||
> 3. Click `Open` in the popup menu.
|
||||
>
|
||||
> The correct steps may vary between macOS versions, so you should refer to
|
||||
> the [macOS User Guide](https://support.apple.com/en-my/guide/mac-help/mh40616/mac).
|
||||
>
|
||||
> On an older version of Lite XL, you will need to run these commands instead:
|
||||
>
|
||||
> ```sh
|
||||
> # clears attributes from the directory
|
||||
> xattr -cr /Applications/Lite\ XL.app
|
||||
> ```
|
||||
>
|
||||
> Otherwise, macOS will display a **very misleading error** saying that the application is damaged.
|
||||
|
||||
### Linux
|
||||
|
||||
Unzip the file and `cd` into the `lite-xl` directory:
|
||||
|
||||
```sh
|
||||
tar -xzf <file>
|
||||
cd lite-xl
|
||||
```
|
||||
|
||||
To run lite-xl without installing:
|
||||
```sh
|
||||
./lite-xl
|
||||
```
|
||||
|
||||
To install lite-xl copy files over into appropriate directories:
|
||||
|
||||
```sh
|
||||
rm -rf $HOME/.local/share/lite-xl $HOME/.local/bin/lite-xl
|
||||
mkdir -p $HOME/.local/bin && cp lite-xl $HOME/.local/bin/
|
||||
mkdir -p $HOME/.local/share/lite-xl && cp -r data/* $HOME/.local/share/lite-xl/
|
||||
```
|
||||
|
||||
If `$HOME/.local/bin` is not in PATH:
|
||||
|
||||
```sh
|
||||
echo -e 'export PATH=$PATH:$HOME/.local/bin' >> $HOME/.bashrc
|
||||
```
|
||||
|
||||
To get the icon to show up in app launcher:
|
||||
|
||||
```sh
|
||||
xdg-desktop-menu forceupdate
|
||||
```
|
||||
|
||||
You may need to logout and login again to see icon in app launcher.
|
||||
|
||||
To uninstall just run:
|
||||
|
||||
```sh
|
||||
rm -f $HOME/.local/bin/lite-xl
|
||||
rm -rf $HOME/.local/share/icons/hicolor/scalable/apps/lite-xl.svg \
|
||||
$HOME/.local/share/applications/org.lite_xl.lite_xl.desktop \
|
||||
$HOME/.local/share/metainfo/org.lite_xl.lite_xl.appdata.xml \
|
||||
$HOME/.local/share/lite-xl
|
||||
```
|
||||
|
||||
|
||||
## Contributing
|
||||
|
||||
Any additional functionality that can be added through a plugin should be done
|
||||
as a plugin, after which a pull request to the [Lite XL plugins repository] can be made.
|
||||
as a plugin, after which a pull request to the [plugins repository] can be made.
|
||||
|
||||
If the plugin uses any Lite XL-specific functionality,
|
||||
please open a pull request to the [Lite XL plugins repository].
|
||||
|
||||
Pull requests to improve or modify the editor itself are welcome.
|
||||
|
||||
|
@ -176,18 +60,17 @@ 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
|
||||
[website]: https://lite-xl.github.io
|
||||
[build]: https://lite-xl.github.io/en/build
|
||||
[Get Lite XL]: https://github.com/franko/lite-xl/releases/latest
|
||||
[Get plugins]: https://github.com/franko/lite-plugins
|
||||
[Get color themes]: https://github.com/rxi/lite-colors
|
||||
[changelog]: https://github.com/franko/lite-xl/blob/master/changelog.md
|
||||
[Lite XL plugins repository]: https://github.com/franko/lite-plugins
|
||||
[plugins repository]: https://github.com/rxi/lite-plugins
|
||||
[colors repository]: https://github.com/lite-xl/lite-xl-colors
|
||||
[colors repository]: https://github.com/rxi/lite-colors
|
||||
[LICENSE]: LICENSE
|
||||
[licenses]: licenses/licenses.md
|
||||
|
|
368
README_Amiga.md
368
README_Amiga.md
|
@ -1,368 +0,0 @@
|
|||
# Lite XL v2 for AmigaOS 4.1 FE & MorphOS 3
|
||||
|
||||
Lite XL is a lightweight text editor written in Lua and SDL2.
|
||||
|
||||
The port is not perfect and it might have issues here and there. It might
|
||||
crash from time to time, if there is a path problem, but overall it works
|
||||
pretty well. This is my daily editor for any kind of development.
|
||||
If it crashes on your system, try to delete to `.config` folder.
|
||||
|
||||
## Installation
|
||||
You can extract the Lite XL archive wherever you want and run the *lite*
|
||||
editor.
|
||||
|
||||
## Configuration folder
|
||||
This editor creates a `.config` folder where the configuration is saved, as
|
||||
well as plugins, themes etc.. By default this version uses the installation
|
||||
folder, but if you want to override it, you can create an ENV variable
|
||||
named `HOME` and set there your prefferable path.
|
||||
|
||||
You can check if there is one already set by executing the following command
|
||||
in a shell
|
||||
```
|
||||
GetEnv HOME
|
||||
```
|
||||
If there is one set, then you will see the path at the output.
|
||||
|
||||
Otherwise, you can set your home path be executing the following command.
|
||||
Change the path to the one of your preference.
|
||||
```
|
||||
SetEnv SAVE HOME "Sys:home/"
|
||||
```
|
||||
|
||||
## Addons
|
||||
### Colors
|
||||
Colors are lua files that set the color scheme of the editor. There are
|
||||
light and dark themes for you to choose.
|
||||
|
||||
To install and use them you have to copy the ones you would like from
|
||||
`addons/colors/light` or `addons/colors/dark` into the folder
|
||||
`.config/lite-xl/colors/`. Don't add light or dark folders. Just copy the
|
||||
.lua files in there.
|
||||
|
||||
Then you have to start Lite XL and open your configuration by clicking
|
||||
at the cog icon at the toolbar (bottom left sixth icon). Go at the line
|
||||
that looks like below
|
||||
```
|
||||
-- core.reload_module("colors.summer")
|
||||
```
|
||||
and change the `summer` with the name of your color theme. Also, remove
|
||||
the two dashes `--` at the start of the line and save the file. If you
|
||||
did everything right, the color schema should change instantly.
|
||||
|
||||
The themes can also be found at
|
||||
https://github.com/lite-xl/lite-xl-colors
|
||||
|
||||
### Plugins
|
||||
LiteXL is able to use plugins to extend its features. Those can be found
|
||||
at https://github.com/lite-xl/lite-xl-plugins and other websites. Not all
|
||||
of them will work fine on AmigaOS 4 or MorphOS, because of missing
|
||||
dependencies or filesystem issues.
|
||||
|
||||
To make it easier for you, I gathered some of the plugins that are working
|
||||
well, and I included them under `addons/plugins`. For you to install the
|
||||
ones you would like to use, you have to copy the `.lua` files into the
|
||||
folder `.config/lite-xl/plugins/` and restart the editor.
|
||||
|
||||
Please, choose wisely, because adding all the plugins might make the editor
|
||||
slower on your system. I would recommend you add only those that you really
|
||||
need.
|
||||
|
||||
The included plugins are the following:
|
||||
|
||||
**autoinsert**
|
||||
Automatically inserts closing brackets and quotes. Also allows selected
|
||||
text to be wrapped with brackets or quotes.
|
||||
|
||||
**autosaveonfocuslost**
|
||||
Automatically saves files that were changed when the main window loses
|
||||
focus by switching to another application
|
||||
|
||||
**autowrap**
|
||||
Automatically hardwraps lines when typing
|
||||
|
||||
**bigclock**
|
||||
Shows the current time and date in a view with large text
|
||||
|
||||
**bracketmatch**
|
||||
Underlines matching pair for bracket under the caret
|
||||
|
||||
**codesets**
|
||||
This plugin uses the codesets.library on AmigaOS 4 and the
|
||||
charsets.library on MorphOS to translate ISO encoded files to unicode
|
||||
and vice-versa. When this is enabled new menu items are added to
|
||||
load/save the code with a different encoding. Also there is a new
|
||||
section at the status bar that show the file encoding.
|
||||
This plugin is **EXPERIMENTAL** and heavily inspired from the encoding
|
||||
plugin at https://github.com/jgmdev/lite-xl-encoding
|
||||
|
||||
**colorpreview**
|
||||
Underlays color values (eg. `#ff00ff` or `rgb(255, 0, 255)`) with their
|
||||
resultant color.
|
||||
|
||||
**custom_caret**
|
||||
Customize the caret in the editor setting it to *underline*, *block* or
|
||||
*line* at the init.lua file in your config folder.
|
||||
For example add:
|
||||
`config.plugins.custom_caret.shape = "block"`
|
||||
|
||||
**EditorConfig**
|
||||
EditorConfig (https://editorconfig.org/) implementation for Lite XL
|
||||
|
||||
**ephemeral_tabs**
|
||||
Preview tabs. Opening a doc will replace the contents of the preview tab.
|
||||
Marks tabs as non-preview on any change or tab double clicking.
|
||||
|
||||
**ghmarkdown**
|
||||
Opens a preview of the current markdown file in a browser window.
|
||||
On AmigaOS 4 it uses *urlopen* and on MorphOS it uses *openurl* to load
|
||||
the generated html in the browser. It requires a GitHub application token
|
||||
because it uses its Rest API. Add it at the init.lua file in your config
|
||||
folder like below:
|
||||
`config.plugins.ghmarkdown.github_token = "<token here>"`
|
||||
|
||||
**indentguide**
|
||||
Adds indent guides
|
||||
|
||||
**language_guide**
|
||||
Syntax for the AmigaGuide scripting language
|
||||
|
||||
**language_hws**
|
||||
Syntax for the Hollywood language
|
||||
|
||||
**language_make**
|
||||
Syntax for the Make build system language
|
||||
|
||||
**language_sh**
|
||||
Syntax for shell scripting language
|
||||
|
||||
**lfautoinsert**
|
||||
Automatically inserts indentation and closing bracket/text after newline
|
||||
|
||||
**markers**
|
||||
Add markers to docs and jump between them quickly
|
||||
|
||||
**minimap**
|
||||
Shows a minimap on the right-hand side of the docview. Please note that
|
||||
this plugin will make the editor slower on file loading and scrolling.
|
||||
|
||||
**navigate**
|
||||
Allows moving back and forward between document positions, reducing the
|
||||
amount of scrolling
|
||||
|
||||
**nonicons**
|
||||
File icons set for TreeView. Download TTF font to your config/fonts
|
||||
folder from https://github.com/yamatsum/nonicons/tree/master/dist
|
||||
|
||||
**opacity**
|
||||
Change the opaqueness/transparency of lite-xl using LAmiga+mousewheel
|
||||
or a command.
|
||||
|
||||
**openfilelocation**
|
||||
Opens the parent directory of the current file in the file manager
|
||||
|
||||
**rainbowparen**
|
||||
Show nesting of parentheses with rainbow colours
|
||||
|
||||
**restoretabs**
|
||||
Keep a list of recently closed tabs, and restore the tab in order on
|
||||
cntrl+shift+t.
|
||||
|
||||
**select_colorscheme**
|
||||
Select a color theme, like VScode, Sublime Text.
|
||||
(plugin saves changes)
|
||||
|
||||
**selectionhighlight**
|
||||
Highlights regions of code that match the current selection
|
||||
|
||||
**smallclock**
|
||||
It adds a small clock at the bottom right corner.
|
||||
|
||||
**tetris**
|
||||
Play Tetris inside Lite XL.
|
||||
|
||||
## Tips and tricks
|
||||
### Transitions
|
||||
|
||||
If you want to disable the transitions and make the editor faster,
|
||||
open your configuration file by clicking at the cog icon at the toolbar
|
||||
(bottom left, 6th icon) and add the following line at the end of the file,
|
||||
and then save it. You might need to restart your editor.
|
||||
|
||||
```
|
||||
config.transitions = false
|
||||
```
|
||||
|
||||
### Hide files from the file list
|
||||
|
||||
If you would like to hide files or whole folder from the left side bar list,
|
||||
open your configuration by clicking at the cog icon at the toolbar
|
||||
(bottom left sixth icon) and add the followline at the end of the file and
|
||||
save it. This hides all the files that start with a dot, and all the `.info`
|
||||
files. You might need to restart your editor.
|
||||
|
||||
```
|
||||
config.ignore_files = {"^%.", "%.info$"}
|
||||
```
|
||||
|
||||
You can add as many rules as you want in there, to hide files or
|
||||
folders, as you like.
|
||||
|
||||
## I would like to thank
|
||||
|
||||
- IconDesigner for the proper glow icons that are included in the release
|
||||
- Capehill for his tireless work on SDL port for AmigaOS 4.1 FE
|
||||
- Michael Trebilcock for his port on liblua
|
||||
- Bruno "BeWorld" Peloille for his great work on porting SDL to MorphOS
|
||||
and for his valuable help
|
||||
- Lite XL original team for being helpful and providing info
|
||||
|
||||
Without all the above Lite XL would not be possible
|
||||
|
||||
## Support
|
||||
If you enjoy what I am doing and would like to keep me up during the night,
|
||||
please consider to buy me a coffee at:
|
||||
https://ko-fi.com/walkero
|
||||
|
||||
## Known issues
|
||||
You can find the known issues at
|
||||
https://git.walkero.gr/walkero/lite-xl/issues
|
||||
|
||||
# Changelog
|
||||
|
||||
## [2.1.4r1] - future
|
||||
### Added
|
||||
- Added the ability to open files and folders by drag 'n drop them on the
|
||||
LiteXL icon when this is on the AmiDock
|
||||
|
||||
### Updated
|
||||
- Updated the code to the upstream 2.1.4 release
|
||||
|
||||
### Fixed
|
||||
- Fix opening files from the root of a device
|
||||
|
||||
## [2.1.3r1] - 2024-03-09
|
||||
### Added
|
||||
- Added AmiUpdate support
|
||||
- Added the Tetris plugin
|
||||
|
||||
### Updated
|
||||
- Updated the code to the upstream 2.1.3 release
|
||||
- Compiled with SDL 2.30.0 that supports editing with ISO encodings, other
|
||||
than English. Now the editor should be able to support any language
|
||||
and in conjuction with the codesets plugin be able to make text
|
||||
encodings conversions
|
||||
- Now the codesets plugin supports MorphOS 3.18 and above
|
||||
|
||||
### Changed
|
||||
- Changed the way the "Open in system" option executes the WBRun command
|
||||
in AmigaOS 4, with a more secure way
|
||||
- Did a lot of code cleanup in sync with the upstream, and parts of code
|
||||
that were left over
|
||||
- Compiled with pcre2 10.42 (MorphOS version only)
|
||||
|
||||
### Fixed
|
||||
- I did a lot of changes on path manipulation and usage, fixing scanning
|
||||
the root of a partition or an assign path
|
||||
- Fixed an error with the codesets plugin, where an empty file could
|
||||
not be opened
|
||||
- Improved the folder suggestions when opening projects or changing paths.
|
||||
Now even the root folders of a partition are presented
|
||||
- Fixed ghmarkdown plugin, but now requires a GitHub token to be provided
|
||||
|
||||
## [2.1.2r1] - 2023-12-19
|
||||
### Added
|
||||
- Added the new experimental codesets plugin (AmigaOS4 version only).
|
||||
MorphOS version is in WIP
|
||||
|
||||
### Changed
|
||||
- Synced with the latest upstream v2.1.2 code
|
||||
- Compiled with gcc 11.3.0
|
||||
- Compiled with SDL 2.28.4
|
||||
- Compiled with libfreetype 2.13.x
|
||||
- Compiled with lua 5.4.6
|
||||
- Compiled with linpng 1.6.40 (AmigaOS4 version only)
|
||||
- Compiled with libz 1.2.13 (AmigaOS4 version only)
|
||||
|
||||
## [2.1.1r2] - 2022-05-14
|
||||
### Changed
|
||||
- Compiled with latest SDL v2.26.5-rc2
|
||||
|
||||
## [2.1.1r1] - 2022-01-29
|
||||
### Changed
|
||||
- Binary name changed to lite-xl
|
||||
- Updated the colour themes and the plugins that are included in the release
|
||||
- Compiled with latest SDL 2.26
|
||||
- Compiled with gcc 11
|
||||
- Synced the code with the upstream master branch at 8th January 2023
|
||||
|
||||
### Fixed
|
||||
- Set the default locale on AmigaOS 4, so as to fix some issues with decimal
|
||||
numbers
|
||||
|
||||
## [2.1.0r1] - 2022-10-10
|
||||
### Added
|
||||
- This version of LiteXL recognises changes that are done outside the editor
|
||||
in files and folders, and updates the items when it gets focus again.
|
||||
|
||||
### Changed
|
||||
- Synced the code with the latest upstream master branch, which means that
|
||||
this version is based on the latest available source
|
||||
- Now the plugins need to be version 3. The older versions will not work.
|
||||
All the included plugins are updated to the latest available version.
|
||||
- Compiled with SDL 2.24
|
||||
- Compiled with Lua 5.4
|
||||
|
||||
### Fixed
|
||||
- Fixed regex issues with some plugins
|
||||
- Fixed "Open in System" on AmigaOS 4 and MorphOS. When you right click
|
||||
at a file or a folder at the treeview at the left side, then depending
|
||||
the type of the item opens on Workbench. A folder opens in a list view
|
||||
and a file opens with its default tool
|
||||
- Fixed markdown preview on MorphOS. Now, it calls openurl with the html
|
||||
file (#20)
|
||||
- Fixed locale issues on MorphOS (again), since the previous fix didn't
|
||||
actually fixed the problem
|
||||
|
||||
## [2.0.3r3] - 2022-09-26
|
||||
### Added
|
||||
- Added plugin for AmigaGuide files
|
||||
- Added plugin for Hollywood files
|
||||
|
||||
### Fixed
|
||||
- Fixed non existing path crashes on OS4 and MorphOS
|
||||
- Fixed editor refresh whenever init.lua is changed, no matter the working
|
||||
folder
|
||||
- Fixed an issue when the user added a directory in the project that
|
||||
already existed
|
||||
- Fixed locale issue on start for MorphOS. Now it should start just fine
|
||||
no matter what locale the user has on his system.
|
||||
- Fixed "Find" on MorphOS that was not working (shortcut CTRL+F)
|
||||
- If the user selects to change the project folder and inserts Sys: or any
|
||||
partition name, the included folders will be listed as suggestions
|
||||
|
||||
### Changed
|
||||
- Removed linking with unix on OS4 build
|
||||
- Makefiles updated
|
||||
|
||||
## [2.0.3r2] - 2022-06-18
|
||||
### Added
|
||||
- First public MorphOS version released
|
||||
|
||||
### Changed
|
||||
- Merged source code for both AmigaOS 4 and MorphOS
|
||||
- Moved the declaration of the $VER and $STACK for the AmigaOS 4 version,
|
||||
so to happen only once (reported by capehill)
|
||||
|
||||
### Fixed
|
||||
- Fixed the usage of NumPad (reported by root)
|
||||
|
||||
## [2.0.3r1] - 2022-03-30
|
||||
### Changed
|
||||
- Applied all the necessary changes to make it run under AmigaOS 4.1 FE
|
||||
- Fixes and changes
|
||||
|
||||
# Disclaimer
|
||||
YOU MAY USE IT AT YOUR OWN RISK!
|
||||
I will not be held responsible for any data loss or problems you might get
|
||||
by using this software.
|
Binary file not shown.
|
@ -1,206 +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 " --cross-platform PLATFORM The platform to cross compile for."
|
||||
echo " --cross-arch ARCH The architecture to cross compile for."
|
||||
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 " --cross-file CROSS_FILE The cross file used for compiling."
|
||||
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 dmgbuild."
|
||||
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
|
||||
local cross_platform
|
||||
local cross_platform_option=()
|
||||
local cross_arch
|
||||
local cross_arch_option=()
|
||||
local cross_file
|
||||
local cross_file_option=()
|
||||
# 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
|
||||
;;
|
||||
--cross-platform)
|
||||
cross_platform="$2"
|
||||
shift
|
||||
shift
|
||||
;;
|
||||
--cross-arch)
|
||||
cross_arch="$2"
|
||||
shift
|
||||
shift
|
||||
;;
|
||||
--cross-file)
|
||||
cross_file="$2"
|
||||
shift
|
||||
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-xl"
|
||||
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-xl.exe" "$bindir"
|
||||
strip --strip-all "$bindir/lite-xl.exe"
|
||||
pushd ".package-build"
|
||||
local package_name="lite-xl-$os-$arch$portable.zip"
|
||||
zip "$package_name" -r "lite-xl"
|
||||
mv "$package_name" ..
|
||||
popd
|
||||
rm -fr ".package-build"
|
||||
echo "created package $package_name"
|
||||
}
|
||||
|
||||
if [[ -n $1 ]]; then
|
||||
show_help
|
||||
lite_build_package_macos () {
|
||||
local build="$1"
|
||||
local arch="$2"
|
||||
local os="macos"
|
||||
|
||||
local appdir=".package-build/lite-xl.app"
|
||||
local bindir="$appdir/Contents/MacOS"
|
||||
local datadir="$appdir/Contents/Resources"
|
||||
mkdir -p "$bindir" "$datadir"
|
||||
for module_name in core plugins colors fonts; do
|
||||
copy_directory_from_repo --strip-components=1 "data/$module_name" "$datadir"
|
||||
done
|
||||
for module_name in plugins colors; do
|
||||
cp -r "$build/third/data/$module_name" "$datadir"
|
||||
done
|
||||
cp resources/icons/icon.icns "$appdir/Contents/Resources/icon.icns"
|
||||
cp resources/macos/Info.plist "$appdir/Contents/Info.plist"
|
||||
cp "$build/src/lite-xl" "$bindir/lite-xl"
|
||||
strip "$bindir/lite-xl"
|
||||
pushd ".package-build"
|
||||
local package_name="lite-xl-$os-$arch.zip"
|
||||
zip "$package_name" -r "lite-xl.app"
|
||||
mv "$package_name" ..
|
||||
popd
|
||||
rm -fr ".package-build"
|
||||
echo "created package $package_name"
|
||||
}
|
||||
|
||||
lite_build_package_linux () {
|
||||
local portable=""
|
||||
if [ "$1" == "-portable" ]; then
|
||||
portable="-portable"
|
||||
shift
|
||||
fi
|
||||
local build="$1"
|
||||
local arch="$2"
|
||||
local os="linux"
|
||||
local pdir=".package-build/lite-xl"
|
||||
if [ "$portable" == "-portable" ]; then
|
||||
local bindir="$pdir"
|
||||
local datadir="$pdir/data"
|
||||
else
|
||||
local bindir="$pdir/bin"
|
||||
local datadir="$pdir/share/lite-xl"
|
||||
fi
|
||||
mkdir -p "$bindir"
|
||||
mkdir -p "$datadir"
|
||||
for module_name in core plugins colors fonts; do
|
||||
copy_directory_from_repo --strip-components=1 "data/$module_name" "$datadir"
|
||||
done
|
||||
for module_name in plugins colors; do
|
||||
cp -r "$build/third/data/$module_name" "$datadir"
|
||||
done
|
||||
cp "$build/src/lite-xl" "$bindir"
|
||||
strip "$bindir/lite-xl"
|
||||
if [ -z "$portable" ]; then
|
||||
mkdir -p "$pdir/share/applications" "$pdir/share/icons/hicolor/scalable/apps"
|
||||
cp "resources/linux/lite-xl.desktop" "$pdir/share/applications"
|
||||
cp "resources/icons/lite-xl.svg" "$pdir/share/icons/hicolor/scalable/apps/lite-xl.svg"
|
||||
fi
|
||||
pushd ".package-build"
|
||||
local package_name="lite-xl-$os-$arch$portable.tar.gz"
|
||||
tar czf "$package_name" "lite-xl"
|
||||
mv "$package_name" ..
|
||||
popd
|
||||
rm -fr ".package-build"
|
||||
echo "created package $package_name"
|
||||
}
|
||||
|
||||
lite_build_package () {
|
||||
if [[ "$OSTYPE" == msys || "$OSTYPE" == win32 ]]; then
|
||||
lite_build_package_windows "$@"
|
||||
elif [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
lite_build_package_macos "$@"
|
||||
elif [[ "$OSTYPE" == "linux"* || "$OSTYPE" == "freebsd"* ]]; then
|
||||
lite_build_package_linux "$@"
|
||||
else
|
||||
echo "Unknown OS type \"$OSTYPE\""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -n $build_dir ]]; then build_dir_option=("--builddir" "${build_dir}"); fi
|
||||
if [[ -n $dest_dir ]]; then dest_dir_option=("--destdir" "${dest_dir}"); fi
|
||||
if [[ -n $prefix ]]; then prefix_option=("--prefix" "${prefix}"); fi
|
||||
if [[ -n $version ]]; then version_option=("--version" "${version}"); fi
|
||||
if [[ -n $cross_platform ]]; then cross_platform_option=("--cross-platform" "${cross_platform}"); fi
|
||||
if [[ -n $cross_arch ]]; then cross_arch_option=("--cross-arch" "${cross_arch}"); fi
|
||||
if [[ -n $cross_file ]]; then cross_file_option=("--cross-file" "${cross_file}"); fi
|
||||
|
||||
|
||||
|
||||
source scripts/build.sh \
|
||||
${build_dir_option[@]} \
|
||||
${prefix_option[@]} \
|
||||
${cross_platform_option[@]} \
|
||||
${cross_arch_option[@]} \
|
||||
${cross_file_option[@]} \
|
||||
$debug \
|
||||
$force_fallback \
|
||||
$bundle \
|
||||
$portable \
|
||||
$release \
|
||||
$pgo
|
||||
|
||||
source scripts/package.sh \
|
||||
${build_dir_option[@]} \
|
||||
${dest_dir_option[@]} \
|
||||
${prefix_option[@]} \
|
||||
${version_option[@]} \
|
||||
${cross_platform_option[@]} \
|
||||
${cross_arch_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
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
CC="${CC:-gcc}"
|
||||
CXX="${CXX:-g++}"
|
||||
CFLAGS=
|
||||
CXXFLAGS=
|
||||
LDFLAGS=
|
||||
BUILD_TYPE=Release
|
||||
|
||||
packages=(pcre2 freetype2 sdl2 lua)
|
1379
changelog.md
1379
changelog.md
File diff suppressed because it is too large
Load Diff
|
@ -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
|
|
@ -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) }
|
|
@ -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
|
|
@ -1,93 +1,22 @@
|
|||
local core = require "core"
|
||||
local command = {}
|
||||
|
||||
---A predicate function accepts arguments from `command.perform()` and evaluates to a boolean. </br>
|
||||
---If the function returns true, then the function associated with the command is executed.
|
||||
---
|
||||
---The predicate function can also return other values after the boolean, which will
|
||||
---be passed into the function associated with the command.
|
||||
---@alias core.command.predicate_function fun(...: any): boolean, ...
|
||||
|
||||
---A predicate is a string, an Object or a function, that is used to determine
|
||||
---whether a command should be executed.
|
||||
---
|
||||
---If the predicate is a string, it is resolved into an `Object` via `require()`
|
||||
---and checked against the active view with `Object:extends()`. </br>
|
||||
---For example, `"core.docview"` will match any view that inherits from `DocView`. </br>
|
||||
---A `!` can be appended to the predicate to strictly match the current view via `Object:is()`,
|
||||
---instead of matching any view that inherits the predicate.
|
||||
---
|
||||
---If the predicate is a table, it is checked against the active view with `Object:extends()`.
|
||||
---Strict matching via `Object:is()` is not available.
|
||||
---
|
||||
---If the predicate is a function, it must behave like a predicate function.
|
||||
---@see core.command.predicate_function
|
||||
---@alias core.command.predicate string|core.object|core.command.predicate_function
|
||||
|
||||
---A command is identified by a command name.
|
||||
---The command name contains a category and the name itself, separated by a colon (':').
|
||||
---
|
||||
---All commands should be in lowercase and should not contain whitespaces; instead
|
||||
---they should be replaced by a dash ('-').
|
||||
---@alias core.command.command_name string
|
||||
|
||||
---The predicate and its associated function.
|
||||
---@class core.command.command
|
||||
---@field predicate core.command.predicate_function
|
||||
---@field perform fun(...: any)
|
||||
|
||||
---@type { [string]: core.command.command }
|
||||
command.map = {}
|
||||
|
||||
---@type core.command.predicate_function
|
||||
local always_true = function() return true end
|
||||
|
||||
|
||||
---This function takes in a predicate and produces a predicate function
|
||||
---that is internally used to dispatch and execute commands.
|
||||
---
|
||||
---This function should not be called manually.
|
||||
---@see core.command.predicate
|
||||
---@param predicate core.command.predicate|nil If nil, the predicate always evaluates to true.
|
||||
---@return core.command.predicate_function
|
||||
function command.generate_predicate(predicate)
|
||||
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
|
||||
---@cast predicate core.command.predicate_function
|
||||
return predicate
|
||||
end
|
||||
|
||||
|
||||
---Adds commands to the map.
|
||||
---
|
||||
---The function accepts a table containing a list of commands
|
||||
---and their functions. </br>
|
||||
---If a command already exists, it will be replaced.
|
||||
---@see core.command.predicate
|
||||
---@see core.command.command_name
|
||||
---@param predicate core.command.predicate
|
||||
---@param map { [core.command.command_name]: fun(...: any) }
|
||||
function command.add(predicate, map)
|
||||
predicate = command.generate_predicate(predicate)
|
||||
for name, fn in pairs(map) do
|
||||
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
|
||||
|
@ -97,85 +26,42 @@ local function capitalize_first(str)
|
|||
return str:sub(1, 1):upper() .. str:sub(2)
|
||||
end
|
||||
|
||||
---Prettifies the command name.
|
||||
---
|
||||
---This function adds a space between the colon and the command name,
|
||||
---replaces dashes with spaces and capitalizes the command appropriately.
|
||||
---@see core.command.command_name
|
||||
---@param name core.command.command_name
|
||||
---@return string
|
||||
function command.prettify_name(name)
|
||||
---@diagnostic disable-next-line: redundant-return-value
|
||||
return name:gsub(":", ": "):gsub("-", " "):gsub("%S+", capitalize_first)
|
||||
end
|
||||
|
||||
|
||||
---Returns all the commands that can be executed (their predicates evaluate to true).
|
||||
---@return core.command.command_name[]
|
||||
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
|
||||
|
||||
---Checks whether a command can be executed (its predicate evaluates to true).
|
||||
---@param name core.command.command_name
|
||||
---@param ... any
|
||||
---@return boolean
|
||||
function command.is_valid(name, ...)
|
||||
return command.map[name] and command.map[name].predicate(...)
|
||||
end
|
||||
|
||||
local function perform(name, ...)
|
||||
local function perform(name)
|
||||
local cmd = command.map[name]
|
||||
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
|
||||
end
|
||||
|
||||
|
||||
---Performs a command.
|
||||
---
|
||||
---The arguments passed into this function are forwarded to the predicate function. </br>
|
||||
---If the predicate function returns more than 1 value, the other values are passed
|
||||
---to the command.
|
||||
---
|
||||
---Otherwise, the arguments passed into this function are passed directly
|
||||
---to the command.
|
||||
---@see core.command.predicate
|
||||
---@see core.command.predicate_function
|
||||
---@param name core.command.command_name
|
||||
---@param ... any
|
||||
---@return boolean # true if the command is performed successfully.
|
||||
function command.perform(name, ...)
|
||||
local ok, res = core.try(perform, name, ...)
|
||||
function command.perform(...)
|
||||
local ok, res = core.try(perform, ...)
|
||||
return not ok or res
|
||||
end
|
||||
|
||||
|
||||
---Inserts the default commands for Lite XL into the map.
|
||||
function command.add_defaults()
|
||||
local reg = {
|
||||
"core", "root", "command", "doc", "findreplace",
|
||||
"files", "dialog", "log", "statusbar"
|
||||
"files", "drawwhitespace", "dialog"
|
||||
}
|
||||
for _, name in ipairs(reg) do
|
||||
require("core.commands." .. name)
|
||||
|
|
|
@ -2,23 +2,23 @@ local core = require "core"
|
|||
local command = require "core.command"
|
||||
|
||||
command.add("core.commandview", {
|
||||
["command:submit"] = function(active_view)
|
||||
active_view:submit()
|
||||
["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,
|
||||
})
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
local core = require "core"
|
||||
local command = require "core.command"
|
||||
|
||||
|
||||
command.add(nil, {
|
||||
["context-menu:show"] = function()
|
||||
core.context_menu:show(core.active_view.position.x, core.active_view.position.y)
|
||||
end
|
||||
})
|
|
@ -6,22 +6,10 @@ local LogView = require "core.logview"
|
|||
|
||||
|
||||
local fullscreen = false
|
||||
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, {
|
||||
|
@ -39,56 +27,47 @@ command.add(nil, {
|
|||
|
||||
["core:toggle-fullscreen"] = function()
|
||||
fullscreen = not fullscreen
|
||||
if fullscreen then
|
||||
restore_title_view = core.title_view.visible
|
||||
end
|
||||
system.set_window_mode(fullscreen and "fullscreen" or "normal")
|
||||
core.show_title_bar(not fullscreen and restore_title_view)
|
||||
core.title_view:configure_hit_test(not fullscreen and restore_title_view)
|
||||
core.show_title_bar(not fullscreen)
|
||||
core.title_view:configure_hit_test(not fullscreen)
|
||||
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 +76,56 @@ 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
|
||||
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 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,
|
||||
})
|
||||
end
|
||||
core.error("Cannot open file %s: %s", text, err)
|
||||
elseif path_stat.type == 'dir' then
|
||||
core.error("Cannot open %s, is a folder", text)
|
||||
else
|
||||
return true
|
||||
end
|
||||
end)
|
||||
end,
|
||||
|
||||
["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 +136,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.basepath(common.home_encode(dirname))
|
||||
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 +194,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,
|
||||
})
|
||||
|
|
|
@ -3,25 +3,30 @@ local command = require "core.command"
|
|||
local common = require "core.common"
|
||||
|
||||
command.add("core.nagview", {
|
||||
["dialog:previous-entry"] = function(v)
|
||||
["dialog:previous-entry"] = function()
|
||||
local v = core.active_view
|
||||
local hover = v.hovered_item or 1
|
||||
v:change_hovered(hover == 1 and #v.options or hover - 1)
|
||||
end,
|
||||
["dialog:next-entry"] = function(v)
|
||||
["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(v)
|
||||
["dialog:select-yes"] = function()
|
||||
local v = core.active_view
|
||||
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)
|
||||
["dialog:select-no"] = function()
|
||||
local v = core.active_view
|
||||
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)
|
||||
["dialog:select"] = function()
|
||||
local v = core.active_view
|
||||
if v.hovered_item then
|
||||
v.on_selected(v.options[v.hovered_item])
|
||||
v:next()
|
||||
|
|
|
@ -3,9 +3,12 @@ local command = require "core.command"
|
|||
local common = require "core.common"
|
||||
local config = require "core.config"
|
||||
local translate = require "core.doc.translate"
|
||||
local style = require "core.style"
|
||||
local DocView = require "core.docview"
|
||||
local tokenizer = require "core.tokenizer"
|
||||
|
||||
|
||||
local function dv()
|
||||
return core.active_view
|
||||
end
|
||||
|
||||
|
||||
local function doc()
|
||||
|
@ -13,6 +16,14 @@ local function doc()
|
|||
end
|
||||
|
||||
|
||||
local function get_indent_string()
|
||||
if config.tab_type == "hard" then
|
||||
return "\t"
|
||||
end
|
||||
return string.rep(" ", config.indent_size)
|
||||
end
|
||||
|
||||
|
||||
local function doc_multiline_selections(sort)
|
||||
local iter, state, idx, line1, col1, line2, col2 = doc():get_selections(sort)
|
||||
return function()
|
||||
|
@ -37,196 +48,46 @@ local function save(filename)
|
|||
filename = core.normalize_to_project_dir(filename)
|
||||
abs_filename = core.project_absolute_path(filename)
|
||||
end
|
||||
local ok, err = pcall(doc().save, doc(), filename, abs_filename)
|
||||
if ok then
|
||||
local saved_filename = doc().filename
|
||||
core.log("Saved \"%s\"", saved_filename)
|
||||
else
|
||||
core.error(err)
|
||||
core.nag_view:show("Saving failed", string.format("Couldn't save file \"%s\". Do you want to save to another location?", doc().filename), {
|
||||
{ text = "Yes", default_yes = true },
|
||||
{ text = "No", default_no = true }
|
||||
}, function(item)
|
||||
if item.text == "Yes" then
|
||||
core.add_thread(function()
|
||||
-- we need to run this in a thread because of the odd way the nagview is.
|
||||
command.perform("doc:save-as")
|
||||
end)
|
||||
end
|
||||
end)
|
||||
end
|
||||
doc():save(filename, abs_filename)
|
||||
local saved_filename = doc().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(true, true) do
|
||||
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 (text .. " " .. full_text)
|
||||
core.cursor_clipboard_whole_line[idx] = false
|
||||
local text = doc():get_text(line1, col1, line2, col2)
|
||||
if delete then
|
||||
doc():delete_to_cursor(idx, 0)
|
||||
end
|
||||
else -- Cut/copy whole line
|
||||
-- Remove newline from the text. It will be added as needed on paste.
|
||||
text = string.sub(doc().lines[line1], 1, -2)
|
||||
full_text = full_text == "" and text .. "\n" or (text .. "\n" .. full_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
|
||||
doc():set_selections(idx, line1, col1, line2, col2)
|
||||
end
|
||||
full_text = full_text == "" and text or (full_text .. "\n" .. text)
|
||||
doc().cursor_clipboard[idx] = text
|
||||
else
|
||||
doc().cursor_clipboard[idx] = ""
|
||||
end
|
||||
core.cursor_clipboard[idx] = text
|
||||
end
|
||||
if delete then doc():merge_cursors() end
|
||||
core.cursor_clipboard["full"] = full_text
|
||||
system.set_clipboard(full_text)
|
||||
end
|
||||
|
||||
local function split_cursor(dv, direction)
|
||||
local function split_cursor(direction)
|
||||
local new_cursors = {}
|
||||
local dv_translate = direction < 0
|
||||
and DocView.translate.previous_line
|
||||
or DocView.translate.next_line
|
||||
for _, line1, col1 in dv.doc:get_selections() do
|
||||
if line1 + direction >= 1 and line1 + direction <= #dv.doc.lines then
|
||||
table.insert(new_cursors, { dv_translate(dv.doc, line1, col1, dv) })
|
||||
for _, line1, col1 in doc():get_selections() do
|
||||
if line1 > 1 and line1 < #doc().lines then
|
||||
table.insert(new_cursors, { line1 + direction, col1 })
|
||||
end
|
||||
end
|
||||
-- add selections in the order that will leave the "last" added one as doc.last_selection
|
||||
local start, stop = 1, #new_cursors
|
||||
if direction < 0 then
|
||||
start, stop = #new_cursors, 1
|
||||
end
|
||||
for i = start, stop, direction do
|
||||
local v = new_cursors[i]
|
||||
dv.doc:add_selection(v[1], v[2])
|
||||
end
|
||||
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
|
||||
else
|
||||
doc():insert(line1, col1, comment[1] .. " ")
|
||||
col2 = col2 + (line1 == line2 and (#comment[1] + 1) or 0)
|
||||
doc():insert(line2, col2, " " .. comment[2])
|
||||
|
||||
return line1, col1, line2, col2 + #comment[2] + 1
|
||||
end
|
||||
end
|
||||
|
||||
local function insert_paste(doc, value, whole_line, idx)
|
||||
if whole_line then
|
||||
local line1, col1 = doc:get_selection_idx(idx)
|
||||
doc:insert(line1, 1, value:gsub("\r", "").."\n")
|
||||
-- Because we're inserting at the start of the line,
|
||||
-- if the cursor is in the middle of the line
|
||||
-- it gets carried to the next line along with the old text.
|
||||
-- If it's at the start of the line it doesn't get carried,
|
||||
-- so we move it of as many characters as we're adding.
|
||||
if col1 == 1 then
|
||||
doc:move_to_cursor(idx, #value+1)
|
||||
end
|
||||
else
|
||||
doc:text_input(value:gsub("\r", ""), idx)
|
||||
end
|
||||
end
|
||||
|
||||
local commands = {
|
||||
["doc:select-none"] = function(dv)
|
||||
local l1, c1 = dv.doc:get_selection_idx(dv.doc.last_selection)
|
||||
if not l1 then
|
||||
l1, c1 = dv.doc:get_selection_idx(1)
|
||||
end
|
||||
dv.doc:set_selection(l1, c1)
|
||||
["doc:undo"] = function()
|
||||
doc():undo()
|
||||
end,
|
||||
|
||||
["doc:redo"] = function()
|
||||
doc():redo()
|
||||
end,
|
||||
|
||||
["doc:cut"] = function()
|
||||
|
@ -237,283 +98,213 @@ local commands = {
|
|||
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
|
||||
if core.cursor_clipboard["full"] ~= clipboard then
|
||||
core.cursor_clipboard = {}
|
||||
core.cursor_clipboard_whole_line = {}
|
||||
for idx in dv.doc:get_selections() do
|
||||
insert_paste(dv.doc, clipboard, false, idx)
|
||||
end
|
||||
return
|
||||
end
|
||||
-- Use internal clipboard(s)
|
||||
-- If there are mixed whole lines and normal lines, consider them all as normal
|
||||
local only_whole_lines = true
|
||||
for _,whole_line in pairs(core.cursor_clipboard_whole_line) do
|
||||
if not whole_line then
|
||||
only_whole_lines = false
|
||||
break
|
||||
end
|
||||
end
|
||||
if #core.cursor_clipboard_whole_line == (#dv.doc.selections/4) then
|
||||
-- If we have the same number of clipboards and selections,
|
||||
-- paste each clipboard into its corresponding selection
|
||||
for idx in dv.doc:get_selections() do
|
||||
insert_paste(dv.doc, core.cursor_clipboard[idx], only_whole_lines, idx)
|
||||
end
|
||||
else
|
||||
-- Paste every clipboard and add a selection at the end of each one
|
||||
local new_selections = {}
|
||||
for idx in dv.doc:get_selections() do
|
||||
for cb_idx in ipairs(core.cursor_clipboard_whole_line) do
|
||||
insert_paste(dv.doc, core.cursor_clipboard[cb_idx], only_whole_lines, idx)
|
||||
if not only_whole_lines then
|
||||
table.insert(new_selections, {dv.doc:get_selection_idx(idx)})
|
||||
end
|
||||
end
|
||||
if only_whole_lines then
|
||||
table.insert(new_selections, {dv.doc:get_selection_idx(idx)})
|
||||
end
|
||||
end
|
||||
local first = true
|
||||
for _,selection in pairs(new_selections) do
|
||||
if first then
|
||||
dv.doc:set_selection(table.unpack(selection))
|
||||
first = false
|
||||
else
|
||||
dv.doc:add_selection(table.unpack(selection))
|
||||
end
|
||||
end
|
||||
["doc:paste"] = function()
|
||||
for idx, line1, col1, line2, col2 in doc():get_selections() do
|
||||
local value = doc().cursor_clipboard[idx] or system.get_clipboard()
|
||||
doc():text_input(value:gsub("\r", ""), idx)
|
||||
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 ]*")
|
||||
["doc:newline"] = function()
|
||||
for idx, line, col in doc():get_selections(false, true) do
|
||||
local indent = 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 not config.keep_newline_whitespace and dv.doc.lines[line]:match("^%s+$") then
|
||||
dv.doc:remove(line, 1, line, math.huge)
|
||||
doc():text_input("\n" .. indent, idx)
|
||||
end
|
||||
end,
|
||||
|
||||
["doc:newline-below"] = function()
|
||||
for idx, line in doc():get_selections(false, true) do
|
||||
local indent = doc().lines[line]:match("^[\t ]*")
|
||||
doc():insert(line, math.huge, "\n" .. indent)
|
||||
doc():set_selections(idx, line + 1, math.huge)
|
||||
end
|
||||
end,
|
||||
|
||||
["doc:newline-above"] = function()
|
||||
for idx, line in doc():get_selections(false, true) do
|
||||
local indent = doc().lines[line]:match("^[\t ]*")
|
||||
doc():insert(line, 1, indent .. "\n")
|
||||
doc():set_selections(idx, line, math.huge)
|
||||
end
|
||||
end,
|
||||
|
||||
["doc:delete"] = function()
|
||||
for idx, line1, col1, line2, col2 in doc():get_selections() do
|
||||
if line1 == line2 and col1 == col2 and doc().lines[line1]:find("^%s*$", col1) then
|
||||
doc():remove(line1, col1, line1, math.huge)
|
||||
end
|
||||
dv.doc:text_input("\n" .. indent, idx)
|
||||
doc():delete_to_cursor(idx, translate.next_char)
|
||||
end
|
||||
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
|
||||
end,
|
||||
|
||||
["doc:newline-above"] = function(dv)
|
||||
for idx, line in dv.doc:get_selections(false, true) do
|
||||
local indent = dv.doc.lines[line]:match("^[\t ]*")
|
||||
dv.doc:insert(line, 1, indent .. "\n")
|
||||
dv.doc:set_selections(idx, line, math.huge)
|
||||
end
|
||||
end,
|
||||
|
||||
["doc:delete"] = function(dv)
|
||||
for idx, line1, col1, line2, col2 in dv.doc:get_selections(true, true) do
|
||||
if line1 == line2 and col1 == col2 and dv.doc.lines[line1]:find("^%s*$", col1) then
|
||||
dv.doc:remove(line1, col1, line1, math.huge)
|
||||
end
|
||||
dv.doc:delete_to_cursor(idx, translate.next_char)
|
||||
end
|
||||
end,
|
||||
|
||||
["doc:backspace"] = function(dv)
|
||||
local _, indent_size = dv.doc:get_indent_info()
|
||||
for idx, line1, col1, line2, col2 in dv.doc:get_selections(true, true) do
|
||||
["doc:backspace"] = function()
|
||||
for idx, line1, col1, line2, col2 in 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)
|
||||
goto continue
|
||||
local text = doc():get_text(line1, 1, line1, col1)
|
||||
if #text >= config.indent_size and text:find("^ *$") then
|
||||
doc():delete_to_cursor(idx, 0, -config.indent_size)
|
||||
return
|
||||
end
|
||||
end
|
||||
dv.doc:delete_to_cursor(idx, translate.previous_char)
|
||||
::continue::
|
||||
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]
|
||||
["doc:select-all"] = function()
|
||||
doc():set_selection(1, 1, math.huge, math.huge)
|
||||
end,
|
||||
|
||||
["doc:select-lines"] = function(dv)
|
||||
for idx, line1, _, line2 in dv.doc:get_selections(true) do
|
||||
["doc:select-none"] = function()
|
||||
local line, col = doc():get_selection()
|
||||
doc():set_selection(line, col)
|
||||
end,
|
||||
|
||||
|
||||
["doc:indent"] = function()
|
||||
for idx, line1, col1, line2, col2 in doc_multiline_selections(true) do
|
||||
local l1, c1, l2, c2 = doc():indent_text(false, line1, col1, line2, col2)
|
||||
if l1 then
|
||||
doc():set_selections(idx, l1, c1, l2, c2)
|
||||
end
|
||||
end
|
||||
end,
|
||||
|
||||
["doc:select-lines"] = function()
|
||||
for idx, line1, _, line2 in doc():get_selections(true) do
|
||||
append_line_if_last_line(line2)
|
||||
dv.doc:set_selections(idx, line1, 1, line2 + 1, 1)
|
||||
doc():set_selections(idx, line1, 1, line2 + 1, 1)
|
||||
end
|
||||
end,
|
||||
|
||||
["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)
|
||||
["doc:select-word"] = function()
|
||||
for idx, line1, col1 in doc():get_selections(true) do
|
||||
local line1, col1 = translate.start_of_word(doc(), line1, col1)
|
||||
local line2, col2 = translate.end_of_word(doc(), line1, col1)
|
||||
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
|
||||
["doc:join-lines"] = function()
|
||||
for idx, line1, col1, line2, col2 in doc():get_selections(true) do
|
||||
if line1 == line2 then line2 = line2 + 1 end
|
||||
local text = dv.doc:get_text(line1, 1, line2, math.huge)
|
||||
local text = doc():get_text(line1, 1, line2, math.huge)
|
||||
text = text:gsub("(.-)\n[\t ]*", function(x)
|
||||
return x:find("^%s*$") and x or x .. " "
|
||||
end)
|
||||
dv.doc:insert(line1, 1, text)
|
||||
dv.doc:remove(line1, #text + 1, line2, math.huge)
|
||||
doc():insert(line1, 1, text)
|
||||
doc():remove(line1, #text + 1, line2, math.huge)
|
||||
if line1 ~= line2 or col1 ~= col2 then
|
||||
dv.doc:set_selections(idx, line1, math.huge)
|
||||
doc():set_selections(idx, line1, math.huge)
|
||||
end
|
||||
end
|
||||
end,
|
||||
|
||||
["doc:indent"] = function(dv)
|
||||
["doc:indent"] = function()
|
||||
for idx, line1, col1, line2, col2 in doc_multiline_selections(true) do
|
||||
local l1, c1, l2, c2 = dv.doc:indent_text(false, line1, col1, line2, col2)
|
||||
local l1, c1, l2, c2 = doc():indent_text(false, line1, col1, line2, col2)
|
||||
if l1 then
|
||||
dv.doc:set_selections(idx, l1, c1, l2, c2)
|
||||
doc():set_selections(idx, l1, c1, l2, c2)
|
||||
end
|
||||
end
|
||||
end,
|
||||
|
||||
["doc:unindent"] = function(dv)
|
||||
["doc:unindent"] = function()
|
||||
for idx, line1, col1, line2, col2 in doc_multiline_selections(true) do
|
||||
local l1, c1, l2, c2 = dv.doc:indent_text(true, line1, col1, line2, col2)
|
||||
local l1, c1, l2, c2 = doc():indent_text(true, line1, col1, line2, col2)
|
||||
if l1 then
|
||||
dv.doc:set_selections(idx, l1, c1, l2, c2)
|
||||
doc():set_selections(idx, l1, c1, l2, c2)
|
||||
end
|
||||
end
|
||||
end,
|
||||
|
||||
["doc:duplicate-lines"] = function(dv)
|
||||
["doc:duplicate-lines"] = function()
|
||||
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)
|
||||
doc():insert(line2 + 1, 1, text)
|
||||
local n = line2 - line1 + 1
|
||||
dv.doc:set_selections(idx, line1 + n, col1, line2 + n, col2)
|
||||
doc():set_selections(idx, line1 + n, col1, line2 + n, col2)
|
||||
end
|
||||
end,
|
||||
|
||||
["doc:delete-lines"] = function(dv)
|
||||
["doc:delete-lines"] = function()
|
||||
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)
|
||||
doc():remove(line1, 1, line2 + 1, 1)
|
||||
doc():set_selections(idx, line1, col1)
|
||||
end
|
||||
end,
|
||||
|
||||
["doc:move-lines-up"] = function(dv)
|
||||
["doc:move-lines-up"] = function()
|
||||
for idx, line1, col1, line2, col2 in doc_multiline_selections(true) do
|
||||
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)
|
||||
doc():insert(line2 + 1, 1, text)
|
||||
doc():remove(line1 - 1, 1, line1, 1)
|
||||
doc():set_selections(idx, line1 - 1, col1, line2 - 1, col2)
|
||||
end
|
||||
end
|
||||
end,
|
||||
|
||||
["doc:move-lines-down"] = function(dv)
|
||||
["doc:move-lines-down"] = function()
|
||||
for idx, line1, col1, line2, col2 in doc_multiline_selections(true) do
|
||||
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)
|
||||
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_selections(idx, line1 + 1, col1, line2 + 1, col2)
|
||||
end
|
||||
end
|
||||
end,
|
||||
|
||||
["doc:toggle-block-comments"] = function(dv)
|
||||
for idx, line1, col1, line2, col2 in doc_multiline_selections(true) do
|
||||
local current_syntax = dv.doc.syntax
|
||||
if line1 > 1 then
|
||||
-- Use the previous line state, as it will be the state
|
||||
-- of the beginning of the current line
|
||||
local state = dv.doc.highlighter:get_line(line1 - 1).state
|
||||
local syntaxes = tokenizer.extract_subsyntaxes(dv.doc.syntax, state)
|
||||
-- Go through all the syntaxes until the first with `block_comment` defined
|
||||
for _, s in pairs(syntaxes) do
|
||||
if s.block_comment then
|
||||
current_syntax = s
|
||||
break
|
||||
["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 .. " "
|
||||
for idx, line1, _, line2 in doc_multiline_selections(true) do
|
||||
local uncomment = true
|
||||
local start_offset = math.huge
|
||||
for line = line1, line2 do
|
||||
local text = doc().lines[line]
|
||||
local s = text:find("%S")
|
||||
local cs, ce = text:find(comment_text, s, true)
|
||||
if s and cs ~= s then
|
||||
uncomment = false
|
||||
start_offset = math.min(start_offset, s)
|
||||
end
|
||||
end
|
||||
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
|
||||
elseif s then
|
||||
doc():insert(line, start_offset, comment_text)
|
||||
end
|
||||
end
|
||||
local comment = current_syntax.block_comment
|
||||
if not comment then
|
||||
if dv.doc.syntax.comment then
|
||||
command.perform "doc:toggle-line-comments"
|
||||
end
|
||||
return
|
||||
end
|
||||
-- if nothing is selected, toggle the whole line
|
||||
if line1 == line2 and col1 == col2 then
|
||||
col1 = 1
|
||||
col2 = #dv.doc.lines[line2]
|
||||
end
|
||||
dv.doc:set_selections(idx, block_comment(comment, line1, col1, line2, col2))
|
||||
end
|
||||
end,
|
||||
|
||||
["doc:toggle-line-comments"] = function(dv)
|
||||
for idx, line1, col1, line2, col2 in doc_multiline_selections(true) do
|
||||
local current_syntax = dv.doc.syntax
|
||||
if line1 > 1 then
|
||||
-- Use the previous line state, as it will be the state
|
||||
-- of the beginning of the current line
|
||||
local state = dv.doc.highlighter:get_line(line1 - 1).state
|
||||
local syntaxes = tokenizer.extract_subsyntaxes(dv.doc.syntax, state)
|
||||
-- Go through all the syntaxes until the first with comments defined
|
||||
for _, s in pairs(syntaxes) do
|
||||
if s.comment or s.block_comment then
|
||||
current_syntax = s
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
local comment = current_syntax.comment or current_syntax.block_comment
|
||||
if comment then
|
||||
dv.doc:set_selections(idx, line_comment(comment, line1, col1, line2, col2))
|
||||
end
|
||||
end
|
||||
end,
|
||||
|
||||
["doc:upper-case"] = function(dv)
|
||||
dv.doc:replace(string.uupper)
|
||||
["doc:upper-case"] = function()
|
||||
doc():replace(string.upper)
|
||||
end,
|
||||
|
||||
["doc:lower-case"] = function(dv)
|
||||
dv.doc:replace(string.ulower)
|
||||
["doc:lower-case"] = function()
|
||||
doc():replace(string.lower)
|
||||
end,
|
||||
|
||||
["doc:go-to-line"] = function(dv)
|
||||
["doc:go-to-line"] = function()
|
||||
local dv = dv()
|
||||
|
||||
local items
|
||||
local function init_items()
|
||||
if items then return end
|
||||
|
@ -525,197 +316,137 @@ 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
|
||||
["file: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)
|
||||
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, function (text)
|
||||
return common.home_encode_list(common.path_suggest(common.home_expand(text)))
|
||||
end)
|
||||
end,
|
||||
|
||||
|
||||
["file:delete"] = function(dv)
|
||||
local filename = dv.doc.abs_filename
|
||||
["file:delete"] = function()
|
||||
local filename = 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
|
||||
for i,docview in ipairs(core.get_views_referencing_doc(doc())) do
|
||||
local node = core.root_view.root_node:get_node_for_view(docview)
|
||||
node:close_view(core.root_view.root_node, docview)
|
||||
node:close_view(core.root_view, docview)
|
||||
end
|
||||
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)
|
||||
["doc:create-cursor-previous-line"] = function()
|
||||
split_cursor(-1)
|
||||
doc():merge_cursors()
|
||||
end,
|
||||
|
||||
["doc:create-cursor-previous-line"] = function(dv)
|
||||
split_cursor(dv, -1)
|
||||
dv.doc:merge_cursors()
|
||||
end,
|
||||
|
||||
["doc:create-cursor-next-line"] = function(dv)
|
||||
split_cursor(dv, 1)
|
||||
dv.doc:merge_cursors()
|
||||
["doc:create-cursor-next-line"] = function()
|
||||
split_cursor(1)
|
||||
doc():merge_cursors()
|
||||
end
|
||||
|
||||
}
|
||||
|
||||
command.add(function(x, y)
|
||||
if x == nil or y == nil or not core.active_view:extends(DocView) then return false end
|
||||
local dv = core.active_view
|
||||
local x1,y1,x2,y2 = dv.position.x, dv.position.y, dv.position.x + dv.size.x, dv.position.y + dv.size.y
|
||||
return x >= x1 + dv:get_gutter_width() and x < x2 and y >= y1 and y < y2, dv, x, y
|
||||
end, {
|
||||
["doc:set-cursor"] = function(dv, x, y)
|
||||
set_cursor(dv, x, y, "set")
|
||||
end,
|
||||
|
||||
["doc:set-cursor-word"] = function(dv, x, y)
|
||||
set_cursor(dv, x, y, "word")
|
||||
end,
|
||||
|
||||
["doc:set-cursor-line"] = function(dv, x, y, clicks)
|
||||
set_cursor(dv, x, y, "lines")
|
||||
end,
|
||||
|
||||
["doc:split-cursor"] = function(dv, x, y, clicks)
|
||||
local line, col = dv:resolve_screen_position(x, y)
|
||||
local removal_target = nil
|
||||
for idx, line1, col1 in dv.doc:get_selections(true) do
|
||||
if line1 == line and col1 == col and #doc().selections > 4 then
|
||||
removal_target = idx
|
||||
end
|
||||
end
|
||||
if removal_target then
|
||||
dv.doc:remove_selection(removal_target)
|
||||
else
|
||||
dv.doc:add_selection(line, col, line, col)
|
||||
end
|
||||
dv.mouse_selecting = { line, col, "set" }
|
||||
end
|
||||
})
|
||||
|
||||
local translations = {
|
||||
["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
|
||||
commands["doc:move-to-previous-char"] = function()
|
||||
for idx, line1, col1, line2, col2 in 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)
|
||||
doc():set_selections(idx, line1, col1)
|
||||
end
|
||||
end
|
||||
dv.doc:merge_cursors()
|
||||
doc():move_to(translate.previous_char)
|
||||
end
|
||||
|
||||
commands["doc:move-to-next-char"] = function(dv)
|
||||
for idx, line1, col1, line2, col2 in dv.doc:get_selections(true) do
|
||||
commands["doc:move-to-next-char"] = function()
|
||||
for idx, line1, col1, line2, col2 in 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)
|
||||
doc():set_selections(idx, line2, col2)
|
||||
end
|
||||
end
|
||||
dv.doc:merge_cursors()
|
||||
doc():move_to(translate.next_char)
|
||||
end
|
||||
|
||||
command.add("core.docview", commands)
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
local command = require "core.command"
|
||||
local config = require "core.config"
|
||||
|
||||
command.add(nil, {
|
||||
["draw-whitespace:toggle"] = function()
|
||||
config.draw_whitespace = not config.draw_whitespace
|
||||
end,
|
||||
|
||||
["draw-whitespace:disable"] = function()
|
||||
config.draw_whitespace = false
|
||||
end,
|
||||
|
||||
["draw-whitespace:enable"] = function()
|
||||
config.draw_whitespace = true
|
||||
end,
|
||||
})
|
|
@ -4,13 +4,11 @@ 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, path = common.mkdirp(text)
|
||||
if not success then
|
||||
core.error("cannot create directory %q: %s", path, err)
|
||||
end
|
||||
})
|
||||
end)
|
||||
end,
|
||||
})
|
||||
|
|
|
@ -7,15 +7,14 @@ 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_last_finds = 50
|
||||
local last_finds, last_view, last_fn, last_text, last_sel
|
||||
|
||||
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:is(DocView) and core.active_view.doc or last_view.doc
|
||||
end
|
||||
|
||||
local function get_find_tooltip()
|
||||
|
@ -30,196 +29,95 @@ local function get_find_tooltip()
|
|||
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)
|
||||
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
|
||||
return true
|
||||
else
|
||||
last_view.doc:set_selection(table.unpack(sel))
|
||||
found_expression = false
|
||||
last_view.doc:set_selection(unpack(sel))
|
||||
return false
|
||||
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
|
||||
end
|
||||
table.insert(t, 1, v)
|
||||
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
|
||||
last_view, last_sel, last_finds = core.active_view,
|
||||
{ core.active_view.doc:get_selection() }, {}
|
||||
local text, found = last_view.doc:get_text(unpack(last_sel)), false
|
||||
|
||||
core.command_view:set_text(text, true)
|
||||
core.status_view:show_tooltip(get_find_tooltip())
|
||||
|
||||
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)
|
||||
core.status_view:remove_tooltip()
|
||||
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
|
||||
else
|
||||
core.error("Couldn't find %q", text)
|
||||
last_view.doc:set_selection(unpack(last_sel))
|
||||
last_view:scroll_to_make_visible(unpack(last_sel))
|
||||
end
|
||||
})
|
||||
end, function(text)
|
||||
found = update_preview(last_sel, search_fn, text)
|
||||
last_fn, last_text = search_fn, text
|
||||
end, function(explicit)
|
||||
core.status_view:remove_tooltip()
|
||||
if explicit then
|
||||
last_view.doc:set_selection(unpack(last_sel))
|
||||
last_view:scroll_to_make_visible(unpack(last_sel))
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
|
||||
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:show_tooltip(get_find_tooltip())
|
||||
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, function() end, function()
|
||||
core.status_view:remove_tooltip()
|
||||
end
|
||||
})
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
local function 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
|
||||
for _, l1, c1, l2, c2 in doc():get_selections(true, true) do
|
||||
if not il1 then
|
||||
il1, ic1 = l1, c1
|
||||
end
|
||||
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 not is_in_any_selection(l2, c2) then
|
||||
doc():add_selection(l2, c2, l1, c1)
|
||||
if not all then
|
||||
core.active_view:scroll_to_make_visible(l2, c2)
|
||||
return
|
||||
end
|
||||
end
|
||||
until not all or not l2
|
||||
if all then break end
|
||||
end
|
||||
end
|
||||
|
||||
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 })
|
||||
if l2 then doc():set_selection(l2, c2, l1, c1) end
|
||||
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, case_sensitive, find_regex)
|
||||
local opt = { wrap = true, no_case = not case_sensitive, regex = find_regex }
|
||||
return search.find(doc, line, col, text, opt)
|
||||
end)
|
||||
end,
|
||||
|
||||
["find-replace:replace"] = function()
|
||||
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)
|
||||
replace("Text", doc():get_text(doc():get_selection(true)), function(text, old, new)
|
||||
if not find_regex then
|
||||
return text:gsub(old:gsub("%W", "%%%1"), new:gsub("%%", "%%%%"), nil)
|
||||
end
|
||||
local result, matches = regex.gsub(regex.compile(old, "m"), text, new)
|
||||
return result, matches
|
||||
local result, matches = regex.gsub(regex.compile(old), text, new)
|
||||
return result, #matches
|
||||
end)
|
||||
end,
|
||||
|
||||
|
@ -243,42 +141,38 @@ command.add("core.docview!", {
|
|||
})
|
||||
|
||||
local function valid_for_finding()
|
||||
-- Allow using this while in the CommandView
|
||||
if core.active_view:is(CommandView) and last_view then
|
||||
return true, last_view
|
||||
end
|
||||
return core.active_view:is(DocView), core.active_view
|
||||
return core.active_view:is(DocView) or core.active_view:is(CommandView)
|
||||
end
|
||||
|
||||
command.add(valid_for_finding, {
|
||||
["find-replace:repeat-find"] = function(dv)
|
||||
["find-replace:repeat-find"] = function()
|
||||
if not last_fn then
|
||||
core.error("No find to continue from")
|
||||
else
|
||||
local sl1, sc1, sl2, sc2 = dv.doc:get_selection(true)
|
||||
local line1, col1, line2, col2 = last_fn(dv.doc, sl2, sc2, last_text, case_sensitive, find_regex, false)
|
||||
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)
|
||||
if line1 then
|
||||
dv.doc:set_selection(line2, col2, line1, col1)
|
||||
dv:scroll_to_line(line2, true)
|
||||
else
|
||||
core.error("Couldn't find %q", last_text)
|
||||
if last_view.doc ~= doc() then
|
||||
last_finds = {}
|
||||
end
|
||||
if #last_finds >= max_last_finds then
|
||||
table.remove(last_finds, 1)
|
||||
end
|
||||
table.insert(last_finds, { sl1, sc1, sl2, sc2 })
|
||||
doc():set_selection(line2, col2, line1, col1)
|
||||
last_view:scroll_to_line(line2, true)
|
||||
end
|
||||
end
|
||||
end,
|
||||
|
||||
["find-replace:previous-find"] = function(dv)
|
||||
if not last_fn then
|
||||
core.error("No find to continue from")
|
||||
else
|
||||
local sl1, sc1, sl2, sc2 = dv.doc:get_selection(true)
|
||||
local line1, col1, line2, col2 = last_fn(dv.doc, sl1, sc1, last_text, case_sensitive, find_regex, true)
|
||||
if line1 then
|
||||
dv.doc:set_selection(line2, col2, line1, col1)
|
||||
dv:scroll_to_line(line2, true)
|
||||
else
|
||||
core.error("Couldn't find %q", last_text)
|
||||
end
|
||||
["find-replace:previous-find"] = function()
|
||||
local sel = table.remove(last_finds)
|
||||
if not sel or doc() ~= last_view.doc then
|
||||
core.error("No previous finds")
|
||||
return
|
||||
end
|
||||
doc():set_selection(table.unpack(sel))
|
||||
last_view:scroll_to_line(sel[3], true)
|
||||
end,
|
||||
})
|
||||
|
||||
|
@ -286,12 +180,12 @@ 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
|
||||
update_preview(last_sel, last_fn, last_text)
|
||||
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
|
||||
update_preview(last_sel, last_fn, last_text)
|
||||
end
|
||||
})
|
||||
|
|
|
@ -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
|
||||
})
|
|
@ -3,15 +3,16 @@ 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)
|
||||
["root:close-or-quit"] = function()
|
||||
local node = core.root_view:get_active_node()
|
||||
if node and (not node:is_empty() or not node.is_primary_node) then
|
||||
node:close_active_view(core.root_view.root_node)
|
||||
else
|
||||
|
@ -20,30 +21,27 @@ local t = {
|
|||
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 +49,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 +58,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 +86,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 +95,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 +106,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,25 +114,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,
|
||||
["root:horizontal-scroll"] = function(delta)
|
||||
local view = (core.root_view.overlapping_node and core.root_view.overlapping_node.active_view) or core.active_view
|
||||
if view and view.scrollable then
|
||||
view.scroll.to.x = view.scroll.to.x + delta * -config.mouse_wheel_scroll
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
})
|
||||
|
|
|
@ -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,
|
||||
})
|
|
@ -6,55 +6,33 @@ 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
|
||||
}
|
||||
|
||||
|
||||
function CommandView:new()
|
||||
CommandView.super.new(self, SingleLineDoc())
|
||||
self.suggestion_idx = 1
|
||||
self.suggestions_offset = 1
|
||||
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
|
||||
|
@ -65,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
|
||||
|
@ -89,10 +60,6 @@ function CommandView:get_scrollable_size()
|
|||
return 0
|
||||
end
|
||||
|
||||
function CommandView:get_h_scrollable_size()
|
||||
return 0
|
||||
end
|
||||
|
||||
|
||||
function CommandView:scroll_to_make_visible()
|
||||
-- no-op function to disable this functionality
|
||||
|
@ -105,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
|
||||
|
@ -115,58 +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
|
||||
|
||||
local function get_suggestions_offset()
|
||||
local max_visible = math.min(max_suggestions, #self.suggestions)
|
||||
if dir > 0 then
|
||||
if self.suggestions_offset + max_visible < self.suggestion_idx + 1 then
|
||||
return self.suggestion_idx - max_visible + 1
|
||||
elseif self.suggestions_offset > self.suggestion_idx then
|
||||
return self.suggestion_idx
|
||||
end
|
||||
else
|
||||
if self.suggestions_offset > self.suggestion_idx then
|
||||
return self.suggestion_idx
|
||||
elseif self.suggestions_offset + max_visible < self.suggestion_idx + 1 then
|
||||
return self.suggestion_idx - max_visible + 1
|
||||
end
|
||||
end
|
||||
return self.suggestions_offset
|
||||
end
|
||||
|
||||
if self.state.show_suggestions then
|
||||
local n = self.suggestion_idx + dir
|
||||
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
|
||||
|
||||
self.suggestions_offset = get_suggestions_offset()
|
||||
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
|
||||
|
||||
|
||||
|
@ -180,60 +98,28 @@ end
|
|||
function CommandView:submit()
|
||||
local suggestion = self.suggestions[self.suggestion_idx]
|
||||
local text = self:get_text()
|
||||
if self.state.validate(text, suggestion) then
|
||||
if self.state.validate(text) then
|
||||
local submit = self.state.submit
|
||||
self:exit(true)
|
||||
submit(text, suggestion)
|
||||
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
|
||||
|
||||
|
||||
|
@ -246,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
|
||||
|
||||
|
||||
|
@ -277,7 +156,6 @@ function CommandView:update_suggestions()
|
|||
end
|
||||
self.suggestions = res
|
||||
self.suggestion_idx = 1
|
||||
self.suggestions_offset = 1
|
||||
end
|
||||
|
||||
|
||||
|
@ -291,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 = (self.suggestion_idx - self.suggestions_offset + 1) * self:get_suggestion_line_height()
|
||||
self:move_towards("selection_offset", dest, nil, "commandview")
|
||||
local dest = math.min(self.suggestion_idx, max_suggestions) * self:get_suggestion_line_height()
|
||||
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
|
||||
|
||||
|
||||
|
@ -346,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
|
||||
|
||||
|
||||
|
@ -357,7 +224,6 @@ local function draw_suggestions_box(self)
|
|||
local h = math.ceil(self.suggestions_height)
|
||||
local rx, ry, rw, rh = self.position.x, self.position.y - h - dh, self.size.x, h
|
||||
|
||||
core.push_clip_rect(rx, ry, rw, rh)
|
||||
-- draw suggestions background
|
||||
if #self.suggestions > 0 then
|
||||
renderer.draw_rect(rx, ry, rw, rh, style.background3)
|
||||
|
@ -367,18 +233,20 @@ local function draw_suggestions_box(self)
|
|||
end
|
||||
|
||||
-- draw suggestion text
|
||||
local first = math.max(self.suggestions_offset, 1)
|
||||
local last = math.min(self.suggestions_offset + max_suggestions, #self.suggestions)
|
||||
for i=first, last do
|
||||
local suggestion_offset = math.max(self.suggestion_idx - max_suggestions, 0)
|
||||
core.push_clip_rect(rx, ry, rw, rh)
|
||||
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 - first + 1) * lh - dh
|
||||
local y = self.position.y - (i - suggestion_offset) * lh - dh
|
||||
common.draw_text(self:get_font(), color, item.text, nil, x, y, 0, lh)
|
||||
|
||||
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
|
||||
|
@ -386,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
|
||||
|
||||
|
||||
|
|
|
@ -1,69 +1,27 @@
|
|||
local common = {}
|
||||
|
||||
|
||||
---Checks if the byte at offset is a UTF-8 continuation byte.
|
||||
---
|
||||
---UTF-8 encodes code points in 1 to 4 bytes.
|
||||
---For a multi-byte sequence, each byte following the start byte is a continuation byte.
|
||||
---@param s string
|
||||
---@param offset? integer The offset of the string to start searching. Defaults to 1.
|
||||
---@return boolean
|
||||
function common.is_utf8_cont(s, offset)
|
||||
local byte = s:byte(offset or 1)
|
||||
function common.is_utf8_cont(char)
|
||||
local byte = char:byte()
|
||||
return byte >= 0x80 and byte < 0xc0
|
||||
end
|
||||
|
||||
|
||||
---Returns an iterator that yields a UTF-8 character on each iteration.
|
||||
---@param text string
|
||||
---@return fun(): string
|
||||
function common.utf8_chars(text)
|
||||
return text:gmatch("[\0-\x7f\xc2-\xf4][\x80-\xbf]*")
|
||||
end
|
||||
|
||||
|
||||
---Clamps the number n between lo and hi.
|
||||
---@param n number
|
||||
---@param lo number
|
||||
---@param hi number
|
||||
---@return number
|
||||
function common.clamp(n, lo, hi)
|
||||
return math.max(math.min(n, hi), lo)
|
||||
end
|
||||
|
||||
|
||||
---Returns a new table containing the contents of b merged into a.
|
||||
---@param a table|nil
|
||||
---@param b table?
|
||||
---@return table
|
||||
function common.merge(a, b)
|
||||
a = type(a) == "table" and a or {}
|
||||
local t = {}
|
||||
for k, v in pairs(a) do
|
||||
t[k] = v
|
||||
end
|
||||
if b and type(b) == "table" then
|
||||
for k, v in pairs(b) do
|
||||
t[k] = v
|
||||
end
|
||||
end
|
||||
return t
|
||||
end
|
||||
|
||||
|
||||
---Returns the value of a number rounded to the nearest integer.
|
||||
---@param n number
|
||||
---@return number
|
||||
function common.round(n)
|
||||
return n >= 0 and math.floor(n + 0.5) or math.ceil(n - 0.5)
|
||||
end
|
||||
|
||||
|
||||
---Returns the first index where a subtable in tbl has prop set.
|
||||
---If none is found, nil is returned.
|
||||
---@param tbl table
|
||||
---@param prop any
|
||||
---@return number|nil
|
||||
function common.find_index(tbl, prop)
|
||||
for i, o in ipairs(tbl) do
|
||||
if o[prop] then return i end
|
||||
|
@ -71,16 +29,6 @@ function common.find_index(tbl, prop)
|
|||
end
|
||||
|
||||
|
||||
---Returns a value between a and b on a linear scale, based on the
|
||||
---interpolation point t.
|
||||
---
|
||||
---If a and b are tables, a table containing the result for all the
|
||||
---elements in a and b is returned.
|
||||
---@param a number
|
||||
---@param b number
|
||||
---@param t number
|
||||
---@return number
|
||||
---@overload fun(a: table, b: table, t: number): table
|
||||
function common.lerp(a, b, t)
|
||||
if type(a) ~= "table" then
|
||||
return a + (b - a) * t
|
||||
|
@ -93,64 +41,46 @@ function common.lerp(a, b, t)
|
|||
end
|
||||
|
||||
|
||||
---Returns the euclidean distance between two points.
|
||||
---@param x1 number
|
||||
---@param y1 number
|
||||
---@param x2 number
|
||||
---@param y2 number
|
||||
---@return number
|
||||
function common.distance(x1, y1, x2, y2)
|
||||
return math.sqrt(((x2-x1) ^ 2)+((y2-y1) ^ 2))
|
||||
end
|
||||
|
||||
|
||||
---Parses a CSS color string.
|
||||
---
|
||||
---Only these formats are supported:
|
||||
---* `rgb(r, g, b)`
|
||||
---* `rgba(r, g, b, a)`
|
||||
---* `#rrggbbaa`
|
||||
---* `#rrggbb`
|
||||
---@param str string
|
||||
---@return number r
|
||||
---@return number g
|
||||
---@return number b
|
||||
---@return number a
|
||||
function common.color(str)
|
||||
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
|
||||
|
||||
|
||||
---Splices a numerically indexed table.
|
||||
---This function mutates the original table.
|
||||
---@param t any[]
|
||||
---@param at number Index at which to start splicing.
|
||||
---@param remove number Number of elements to remove.
|
||||
---@param insert? any[] A table containing elements to insert after splicing.
|
||||
function common.splice(t, at, remove, insert)
|
||||
assert(remove >= 0, "bad argument #3 to 'splice' (non-negative value expected)")
|
||||
insert = insert or {}
|
||||
local len = #insert
|
||||
if remove ~= len then table.move(t, at + remove, #t + remove, at + len) end
|
||||
table.move(insert, 1, len, at, t)
|
||||
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
|
||||
|
@ -171,16 +101,6 @@ local function fuzzy_match_items(items, needle, files)
|
|||
end
|
||||
|
||||
|
||||
---Performs fuzzy matching.
|
||||
---
|
||||
---If the haystack is a string, a score ranging from 0 to 1 is returned. </br>
|
||||
---If the haystack is a table, a table containing the haystack sorted in ascending
|
||||
---order of similarity is returned.
|
||||
---@param haystack string
|
||||
---@param needle string
|
||||
---@param files boolean If true, the matching process will be performed in reverse to better match paths.
|
||||
---@return number
|
||||
---@overload fun(haystack: string[], needle: string, files: boolean): string[]
|
||||
function common.fuzzy_match(haystack, needle, files)
|
||||
if type(haystack) == "table" then
|
||||
return fuzzy_match_items(haystack, needle, files)
|
||||
|
@ -189,14 +109,6 @@ function common.fuzzy_match(haystack, needle, files)
|
|||
end
|
||||
|
||||
|
||||
---Performs fuzzy matching and returns recently used strings if needed.
|
||||
---
|
||||
---If the needle is empty, then a list of recently used strings
|
||||
---are added to the result, followed by strings from the haystack.
|
||||
---@param haystack string[]
|
||||
---@param recents string[]
|
||||
---@param needle string
|
||||
---@return string[]
|
||||
function common.fuzzy_match_with_recents(haystack, recents, needle)
|
||||
if needle == "" then
|
||||
local recents_ext = {}
|
||||
|
@ -215,42 +127,9 @@ function common.fuzzy_match_with_recents(haystack, recents, needle)
|
|||
end
|
||||
|
||||
|
||||
---Returns a list of paths that are relative to the input path.
|
||||
---
|
||||
---If a root directory is specified, the function returns paths
|
||||
---that are relative to the root directory.
|
||||
---@param text string The input path.
|
||||
---@param root? string The root directory.
|
||||
---@return string[]
|
||||
function common.path_suggest(text, root)
|
||||
if root and root:sub(-1) ~= PATHSEP then
|
||||
root = root .. PATHSEP
|
||||
end
|
||||
local path, name
|
||||
if (PLATFORM == "AmigaOS 4" or PLATFORM == "MorphOS") then
|
||||
path, name = text:match("^(.-)([^:"..PATHSEP.."]*)$")
|
||||
else
|
||||
path, name = text:match("^(.-)([^"..PATHSEP.."]*)$")
|
||||
end
|
||||
|
||||
local clean_dotslash = false
|
||||
-- ignore root if path is absolute
|
||||
local is_absolute = common.is_absolute_path(text)
|
||||
if not is_absolute then
|
||||
if path == "" then
|
||||
path = root or "."
|
||||
clean_dotslash = not root
|
||||
else
|
||||
path = (root or "") .. path
|
||||
end
|
||||
end
|
||||
|
||||
-- Only in Windows allow using both styles of PATHSEP
|
||||
if (PATHSEP == "\\" and not string.match(path:sub(-1), "[\\/]")) or
|
||||
(PATHSEP ~= "\\" and path:sub(-1) ~= PATHSEP) then
|
||||
path = path .. PATHSEP
|
||||
end
|
||||
local files = system.list_dir(path) or {}
|
||||
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
|
||||
|
@ -259,19 +138,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
|
||||
|
@ -281,16 +147,8 @@ function common.path_suggest(text, root)
|
|||
end
|
||||
|
||||
|
||||
---Returns a list of directories that are related to a path.
|
||||
---@param text string The input path.
|
||||
---@return string[]
|
||||
function common.dir_path_suggest(text)
|
||||
local path, name
|
||||
if (PLATFORM == "AmigaOS 4" or PLATFORM == "MorphOS") then
|
||||
path, name = text:match("^(.-)([^:"..PATHSEP.."]*)$")
|
||||
else
|
||||
path, name = text:match("^(.-)([^"..PATHSEP.."]*)$")
|
||||
end
|
||||
local path, name = text:match("^(.-)([^/\\]*)$")
|
||||
local files = system.list_dir(path == "" and "." or path) or {}
|
||||
local res = {}
|
||||
for _, file in ipairs(files) do
|
||||
|
@ -304,18 +162,8 @@ function common.dir_path_suggest(text)
|
|||
end
|
||||
|
||||
|
||||
---Filters a list of paths to find those that are related to the input path.
|
||||
---@param text string The input path.
|
||||
---@param dir_list string[] A list of paths to filter.
|
||||
---@return string[]
|
||||
function common.dir_list_suggest(text, dir_list)
|
||||
local path, name
|
||||
if (PLATFORM == "AmigaOS 4" or PLATFORM == "MorphOS") then
|
||||
path, name = text:match("^(.-)([^:"..PATHSEP.."]*)$")
|
||||
else
|
||||
path, name = text:match("^(.-)([^"..PATHSEP.."]*)$")
|
||||
end
|
||||
|
||||
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
|
||||
|
@ -326,15 +174,6 @@ function common.dir_list_suggest(text, dir_list)
|
|||
end
|
||||
|
||||
|
||||
---Matches a string against a list of patterns.
|
||||
---
|
||||
---If a match was found, its start and end index is returned.
|
||||
---Otherwise, false is returned.
|
||||
---@param text string
|
||||
---@param pattern string|string[]
|
||||
---@param ... any Other options for string.find().
|
||||
---@return number|boolean start_index
|
||||
---@return number|nil end_index
|
||||
function common.match_pattern(text, pattern, ...)
|
||||
if type(pattern) == "string" then
|
||||
return text:find(pattern, ...)
|
||||
|
@ -347,24 +186,8 @@ function common.match_pattern(text, pattern, ...)
|
|||
end
|
||||
|
||||
|
||||
---Draws text onto the window.
|
||||
---The function returns the X and Y coordinates of the bottom-right
|
||||
---corner of the text.
|
||||
---@param font renderer.font
|
||||
---@param color renderer.color
|
||||
---@param text string
|
||||
---@param align string
|
||||
---| '"left"' # Align text to the left of the bounding box
|
||||
---| '"right"' # Align text to the right of the bounding box
|
||||
---| '"center"' # Center text in the bounding box
|
||||
---@param x number
|
||||
---@param y number
|
||||
---@param w number
|
||||
---@param h number
|
||||
---@return number x_advance
|
||||
---@return number y_advance
|
||||
function common.draw_text(font, color, text, align, x,y,w,h)
|
||||
local tw, th = font:get_width(text), font:get_height()
|
||||
local tw, th = font:get_width(text), font:get_height(text)
|
||||
if align == "center" then
|
||||
x = x + (w - tw) / 2
|
||||
elseif align == "right" then
|
||||
|
@ -375,16 +198,6 @@ function common.draw_text(font, color, text, align, x,y,w,h)
|
|||
end
|
||||
|
||||
|
||||
---Prints the execution time of a function.
|
||||
---
|
||||
---The execution time and percentage of frame time
|
||||
---for the function is printed to standard output. </br>
|
||||
---The frame rate is always assumed to be 60 FPS, thus
|
||||
---a value of 100% would mean that the benchmark took
|
||||
---1/60 of a second to execute.
|
||||
---@param name string
|
||||
---@param fn fun(...: any): any
|
||||
---@return any # The result returned by the function
|
||||
function common.bench(name, fn, ...)
|
||||
local start = system.get_time()
|
||||
local res = fn(...)
|
||||
|
@ -395,129 +208,34 @@ function common.bench(name, fn, ...)
|
|||
return res
|
||||
end
|
||||
|
||||
-- From gvx/Ser
|
||||
local oddvals = {[tostring(1/0)] = "1/0", [tostring(-1/0)] = "-1/0", [tostring(-(0/0))] = "-(0/0)", [tostring(0/0)] = "0/0"}
|
||||
|
||||
local function serialize(val, pretty, indent_str, escape, sort, limit, level)
|
||||
local space = pretty and " " or ""
|
||||
local indent = pretty and string.rep(indent_str, level) or ""
|
||||
local newline = pretty and "\n" or ""
|
||||
local ty = type(val)
|
||||
if ty == "string" then
|
||||
local out = string.format("%q", val)
|
||||
if escape then
|
||||
out = string.gsub(out, "\\\n", "\\n")
|
||||
out = string.gsub(out, "\\7", "\\a")
|
||||
out = string.gsub(out, "\\8", "\\b")
|
||||
out = string.gsub(out, "\\9", "\\t")
|
||||
out = string.gsub(out, "\\11", "\\v")
|
||||
out = string.gsub(out, "\\12", "\\f")
|
||||
out = string.gsub(out, "\\13", "\\r")
|
||||
end
|
||||
return out
|
||||
elseif ty == "table" then
|
||||
-- early exit
|
||||
if level >= limit then return tostring(val) end
|
||||
local next_indent = pretty and (indent .. indent_str) or ""
|
||||
function common.serialize(val)
|
||||
if type(val) == "string" then
|
||||
return string.format("%q", val)
|
||||
elseif type(val) == "table" then
|
||||
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 .. "}"
|
||||
end
|
||||
if ty == "number" then
|
||||
-- tostring is locale-dependent, so we need to replace an eventual `,` with `.`
|
||||
local res, _ = tostring(val):gsub(",", ".")
|
||||
-- handle inf/nan
|
||||
return oddvals[res] or res
|
||||
return "{" .. table.concat(t, ",") .. "}"
|
||||
end
|
||||
return tostring(val)
|
||||
end
|
||||
|
||||
|
||||
---@class common.serializeoptions
|
||||
---@field pretty boolean Enables pretty printing.
|
||||
---@field indent_str string The indentation character to use. Defaults to `" "`.
|
||||
---@field escape boolean Uses normal escape characters ("\n") instead of decimal escape sequences ("\10").
|
||||
---@field limit number Limits the depth when serializing nested tables. Defaults to `math.huge`.
|
||||
---@field sort boolean Sorts the output if it is a sortable table.
|
||||
---@field initial_indent number The initial indentation level. Defaults to 0.
|
||||
|
||||
---Serializes a value into a Lua string that is loadable with load().
|
||||
---
|
||||
---Only these basic types are supported:
|
||||
---* nil
|
||||
---* boolean
|
||||
---* number (except very large numbers and special constants, e.g. `math.huge`, `inf` and `nan`)
|
||||
---* integer
|
||||
---* string
|
||||
---* table
|
||||
---
|
||||
---@param val any
|
||||
---@param opts? common.serializeoptions
|
||||
---@return string
|
||||
function common.serialize(val, opts)
|
||||
opts = opts or {}
|
||||
local indent_str = opts.indent_str or " "
|
||||
local initial_indent = opts.initial_indent or 0
|
||||
local indent = opts.pretty and string.rep(indent_str, initial_indent) or ""
|
||||
local limit = (opts.limit or math.huge) + initial_indent
|
||||
return indent .. serialize(val, opts.pretty, indent_str,
|
||||
opts.escape, opts.sort, limit, initial_indent)
|
||||
end
|
||||
|
||||
|
||||
---Returns the last portion of a path.
|
||||
---@param path string
|
||||
---@return string
|
||||
function common.basename(path)
|
||||
-- a path should never end by / or \ except if it is '/' (unix root) or
|
||||
-- 'X:\' (windows drive)
|
||||
return path:match("[^"..PATHSEP.."]+$") or path
|
||||
return path:match("[^\\/]+$") or path
|
||||
end
|
||||
|
||||
|
||||
---Returns the base path with the pathsep, if needed.
|
||||
---@param path string
|
||||
---@return string
|
||||
function common.basepath(path)
|
||||
-- Check for AmigaOS 4 and MorphOS if the last character is semicolon
|
||||
-- In these systems the volume name doesn't have a / or \ after the name
|
||||
-- but it is like VOLUME:
|
||||
if (PLATFORM == "AmigaOS 4" or PLATFORM == "MorphOS") and (string.sub(path, -1) == ":") then
|
||||
return path
|
||||
end
|
||||
return path .. PATHSEP
|
||||
end
|
||||
|
||||
|
||||
---Returns the directory name of a path.
|
||||
---If the path doesn't have a directory, this function may return nil.
|
||||
---@param path string
|
||||
---@return string|nil
|
||||
-- can return nil if there is no directory part in the path
|
||||
function common.dirname(path)
|
||||
if (PLATFORM == "AmigaOS 4" or PLATFORM == "MorphOS") then
|
||||
local drive, relpath = path:match('^([%w%s]*:)(.+)')
|
||||
if drive and relpath then
|
||||
local dir = relpath:match("(.+)["..PATHSEP.."][^"..PATHSEP.."]+$")
|
||||
if dir then
|
||||
return drive .. dir
|
||||
end
|
||||
end
|
||||
return path
|
||||
end
|
||||
return path:match("(.+)["..PATHSEP.."][^"..PATHSEP.."]+$")
|
||||
return path:match("(.+)[\\/][^\\/]+$")
|
||||
end
|
||||
|
||||
|
||||
---Returns a path where the user's home directory is replaced by `"~"`.
|
||||
---@param text string
|
||||
---@return string
|
||||
function common.home_encode(text)
|
||||
if HOME and string.find(text, HOME, 1, true) == 1 then
|
||||
local dir_pos = #HOME + 1
|
||||
|
@ -531,9 +249,6 @@ function common.home_encode(text)
|
|||
end
|
||||
|
||||
|
||||
---Returns a list of paths where the user's home directory is replaced by `"~"`.
|
||||
---@param paths string[] A list of paths to encode
|
||||
---@return string[]
|
||||
function common.home_encode_list(paths)
|
||||
local t = {}
|
||||
for i = 1, #paths do
|
||||
|
@ -543,145 +258,54 @@ function common.home_encode_list(paths)
|
|||
end
|
||||
|
||||
|
||||
---Expands the `"~"` prefix in a path into the user's home directory.
|
||||
---This function is not guaranteed to return an absolute path.
|
||||
---@param text string
|
||||
---@return string
|
||||
function common.home_expand(text)
|
||||
if text == nil then
|
||||
return HOME
|
||||
end
|
||||
return HOME and text:gsub("^~", HOME) or text
|
||||
end
|
||||
|
||||
|
||||
local function split_on_slash(s, sep_pattern)
|
||||
local t = {}
|
||||
if s:match("^["..PATHSEP.."]") then
|
||||
if s:match("^[/\\]") then
|
||||
t[#t + 1] = ""
|
||||
end
|
||||
if (PLATFORM == "AmigaOS 4" or PLATFORM == "MorphOS") then
|
||||
local drive = s:match("^([%w%s]*:)")
|
||||
if drive then
|
||||
t[#t + 1] = ""
|
||||
s = s:gsub("^" .. drive, "")
|
||||
end
|
||||
end
|
||||
for fragment in string.gmatch(s, "([^"..PATHSEP.."]+)") do
|
||||
for fragment in string.gmatch(s, "([^/\\]+)") do
|
||||
t[#t + 1] = fragment
|
||||
end
|
||||
return t
|
||||
end
|
||||
|
||||
|
||||
---Normalizes the drive letter in a Windows path to uppercase.
|
||||
---This function expects an absolute path, e.g. a path from `system.absolute_path`.
|
||||
---
|
||||
---This function is needed because the path returned by `system.absolute_path`
|
||||
---may contain drive letters in upper or lowercase.
|
||||
---@param filename string|nil The input path.
|
||||
---@return string|nil
|
||||
function common.normalize_volume(filename)
|
||||
if not filename then return end
|
||||
if PATHSEP == '\\' then
|
||||
local drive, rem = filename:match('^([a-zA-Z]:\\)(.-)'..PATHSEP..'?$')
|
||||
if drive then
|
||||
return drive:upper() .. rem
|
||||
end
|
||||
end
|
||||
if (PLATFORM == "AmigaOS 4" or PLATFORM == "MorphOS") then
|
||||
local drive, rem = filename:match('^([%w%s]*:)(.-)' .. PATHSEP .. '?$')
|
||||
if drive then
|
||||
return drive .. rem
|
||||
end
|
||||
end
|
||||
return filename
|
||||
end
|
||||
|
||||
|
||||
---Normalizes a path into the same format across platforms.
|
||||
---
|
||||
---On Windows, all drive letters are converted to uppercase.
|
||||
---UNC paths with drive letters are converted back to ordinary Windows paths.
|
||||
---All path separators (`"/"`, `"\\"`) are converted to platform-specific ones.
|
||||
---@param filename string|nil
|
||||
---@return string|nil
|
||||
function common.normalize_path(filename)
|
||||
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
|
||||
elseif (PLATFORM == "AmigaOS 4" or PLATFORM == "MorphOS") then
|
||||
local drive, relpath = filename:match('^([%w%s]*:)(.+)')
|
||||
if relpath then
|
||||
volume, filename = drive, relpath
|
||||
end
|
||||
else
|
||||
local relpath = filename:match('^/(.+)')
|
||||
if relpath then
|
||||
volume, filename = "/", relpath
|
||||
end
|
||||
local drive, rem = filename:match('^([a-zA-Z])(:.*)')
|
||||
filename = drive and drive:upper() .. rem or filename
|
||||
end
|
||||
local parts = split_on_slash(filename, PATHSEP)
|
||||
local accu = {}
|
||||
for _, part in ipairs(parts) do
|
||||
if part == '..' 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
|
||||
table.remove(accu)
|
||||
elseif part ~= '.' then
|
||||
table.insert(accu, part)
|
||||
end
|
||||
end
|
||||
local npath = table.concat(accu, PATHSEP)
|
||||
return (volume or "") .. (npath == "" and PATHSEP or npath)
|
||||
return table.concat(accu, PATHSEP)
|
||||
end
|
||||
|
||||
|
||||
---Checks whether a path is absolute or relative.
|
||||
---@param path string
|
||||
---@return boolean
|
||||
function common.is_absolute_path(path)
|
||||
return path:sub(1, 1) == PATHSEP or path:match("^(%a):\\") or path:match('^([%w%s]*):')
|
||||
end
|
||||
|
||||
|
||||
---Checks whether a path belongs to a parent directory.
|
||||
---@param filename string The path to check.
|
||||
---@param path string The parent path.
|
||||
---@return boolean
|
||||
function common.path_belongs_to(filename, path)
|
||||
return string.find(filename, common.basepath(path), 1, true) == 1
|
||||
return string.find(filename, path .. PATHSEP, 1, true) == 1
|
||||
end
|
||||
|
||||
|
||||
---Checks whether a path is relative to another path.
|
||||
---@param ref_dir string The path to check against.
|
||||
---@param dir string The input path.
|
||||
---@return boolean
|
||||
function common.relative_path(ref_dir, dir)
|
||||
local drive_pattern = "^(%a):\\"
|
||||
if (PLATFORM == "AmigaOS 4" or PLATFORM == "MorphOS") then
|
||||
drive_pattern = "^([%w%s]*:)"
|
||||
end
|
||||
local drive, ref_drive = dir:match(drive_pattern), ref_dir:match(drive_pattern)
|
||||
if drive and ref_drive and drive ~= ref_drive then
|
||||
-- Windows, different drives, system.absolute_path fails for C:\..\D:\
|
||||
return dir
|
||||
end
|
||||
end
|
||||
local ref_ls = split_on_slash(ref_dir)
|
||||
local dir_ls = split_on_slash(dir)
|
||||
local i = 1
|
||||
|
@ -700,11 +324,6 @@ function common.relative_path(ref_dir, dir)
|
|||
end
|
||||
|
||||
|
||||
---Creates a directory recursively if necessary.
|
||||
---@param path string
|
||||
---@return boolean success
|
||||
---@return string|nil error
|
||||
---@return string|nil path The path where an error occured.
|
||||
function common.mkdirp(path)
|
||||
local stat = system.get_file_info(path)
|
||||
if stat and stat.type then
|
||||
|
@ -714,12 +333,12 @@ function common.mkdirp(path)
|
|||
while path and path ~= "" do
|
||||
local success_mkdir = system.mkdir(path)
|
||||
if success_mkdir then break end
|
||||
local updir, basedir = path:match("(.*)["..PATHSEP.."](.+)$")
|
||||
local updir, basedir = path:match("(.*)[/\\](.+)$")
|
||||
table.insert(subdirs, 1, basedir or path)
|
||||
path = updir
|
||||
end
|
||||
for _, dirname in ipairs(subdirs) do
|
||||
path = path and common.basepath(path) .. dirname or dirname
|
||||
path = path and path .. PATHSEP .. dirname or dirname
|
||||
if not system.mkdir(path) then
|
||||
return false, "cannot create directory", path
|
||||
end
|
||||
|
@ -727,13 +346,6 @@ function common.mkdirp(path)
|
|||
return true
|
||||
end
|
||||
|
||||
|
||||
---Removes a path.
|
||||
---@param path string
|
||||
---@param recursively boolean If true, the function will attempt to remove everything in the specified path.
|
||||
---@return boolean success
|
||||
---@return string|nil error
|
||||
---@return string|nil path The path where the error occured.
|
||||
function common.rm(path, recursively)
|
||||
local stat = system.get_file_info(path)
|
||||
if not stat or (stat.type ~= "file" and stat.type ~= "dir") then
|
||||
|
@ -781,5 +393,4 @@ function common.rm(path, recursively)
|
|||
return true
|
||||
end
|
||||
|
||||
|
||||
return common
|
||||
|
|
|
@ -1,105 +1,36 @@
|
|||
local common = require "core.common"
|
||||
|
||||
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
|
||||
---@type "expanded" | "contracted" | false @Force the scrollbar status of the DocView
|
||||
config.force_scrollbar_status = false
|
||||
config.file_size_limit = 10
|
||||
config.ignore_files = {
|
||||
-- folders
|
||||
"^%.svn/", "^%.git/", "^%.hg/", "^CVS/", "^%.Trash/", "^%.Trash%-.*/",
|
||||
"^node_modules/", "^%.cache/", "^__pycache__/",
|
||||
-- files
|
||||
"%.pyc$", "%.pyo$", "%.exe$", "%.dll$", "%.obj$", "%.o$",
|
||||
"%.a$", "%.lib$", "%.so$", "%.dylib$", "%.ncb$", "%.sdf$",
|
||||
"%.suo$", "%.pdb$", "%.idb$", "%.class$", "%.psd$", "%.db$",
|
||||
"^desktop%.ini$", "^%.DS_Store$", "^%.directory$",
|
||||
}
|
||||
config.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.keep_newline_whitespace = false
|
||||
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
|
||||
|
||||
-- holds the plugins real config table
|
||||
local plugins_config = {}
|
||||
|
||||
-- virtual representation of plugins config table
|
||||
-- Disable plugin loading setting to false the config entry
|
||||
-- of the same name.
|
||||
config.plugins = {}
|
||||
|
||||
-- allows virtual access to the plugins config table
|
||||
setmetatable(config.plugins, {
|
||||
__index = function(_, k)
|
||||
if not plugins_config[k] then
|
||||
plugins_config[k] = { enabled = true, config = {} }
|
||||
end
|
||||
if plugins_config[k].enabled ~= false then
|
||||
return plugins_config[k].config
|
||||
end
|
||||
return false
|
||||
end,
|
||||
__newindex = function(_, k, v)
|
||||
if not plugins_config[k] then
|
||||
plugins_config[k] = { enabled = nil, config = {} }
|
||||
end
|
||||
if v == false and package.loaded["plugins."..k] then
|
||||
local core = require "core"
|
||||
core.warn("[%s] is already enabled, restart the editor for the change to take effect", k)
|
||||
return
|
||||
elseif plugins_config[k].enabled == false and v ~= false then
|
||||
plugins_config[k].enabled = true
|
||||
end
|
||||
if v == false then
|
||||
plugins_config[k].enabled = false
|
||||
elseif type(v) == "table" then
|
||||
plugins_config[k].enabled = true
|
||||
plugins_config[k].config = common.merge(plugins_config[k].config, v)
|
||||
end
|
||||
end,
|
||||
__pairs = function()
|
||||
return coroutine.wrap(function()
|
||||
for name, status in pairs(plugins_config) do
|
||||
coroutine.yield(name, status.config)
|
||||
end
|
||||
end)
|
||||
end
|
||||
})
|
||||
|
||||
config.plugins.trimwhitespace = false
|
||||
config.plugins.lineguide = false
|
||||
|
||||
return config
|
||||
|
|
|
@ -5,52 +5,28 @@ 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_padding = 5
|
||||
local DIVIDER = {}
|
||||
|
||||
---An item in the context menu.
|
||||
---@class core.contextmenu.item
|
||||
---@field text string
|
||||
---@field info string|nil If provided, this text is displayed on the right side of the menu.
|
||||
---@field command string|fun()
|
||||
|
||||
---A list of items with the same predicate.
|
||||
---@see core.command.predicate
|
||||
---@class core.contextmenu.itemset
|
||||
---@field predicate core.command.predicate
|
||||
---@field items core.contextmenu.item[]
|
||||
|
||||
---A context menu.
|
||||
---@class core.contextmenu : core.object
|
||||
---@field itemset core.contextmenu.itemset[]
|
||||
---@field show_context_menu boolean
|
||||
---@field selected number
|
||||
---@field position core.view.position
|
||||
---@field current_scale number
|
||||
local ContextMenu = Object:extend()
|
||||
|
||||
---A unique value representing the divider in a context menu.
|
||||
ContextMenu.DIVIDER = DIVIDER
|
||||
|
||||
---Creates a new context menu.
|
||||
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 + divider_padding * SCALE * 2
|
||||
lh = divider_width
|
||||
else
|
||||
lw = style.font:get_width(item.text)
|
||||
if item.info then
|
||||
|
@ -61,11 +37,19 @@ local function get_item_size(item)
|
|||
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)
|
||||
function ContextMenu:register(predicate, items)
|
||||
if type(predicate) == "string" then
|
||||
predicate = require(predicate)
|
||||
end
|
||||
if type(predicate) == "table" then
|
||||
local class = predicate
|
||||
predicate = function() return core.active_view:is(class) end
|
||||
end
|
||||
|
||||
local width, height = 0, 0 --precalculate the size of context menu
|
||||
for i, item in ipairs(items) do
|
||||
if item ~= DIVIDER then
|
||||
item.info = item.info or keymap.reverse_map[item.command]
|
||||
end
|
||||
local lw, lh = get_item_size(item)
|
||||
width = math.max(width, lw)
|
||||
|
@ -73,34 +57,18 @@ local function update_items_size(items, update_binding)
|
|||
end
|
||||
width = width + style.padding.x * 2
|
||||
items.width, items.height = width, height
|
||||
end
|
||||
|
||||
---Registers a list of items into the context menu with a predicate.
|
||||
---@param predicate core.command.predicate
|
||||
---@param items core.contextmenu.item[]
|
||||
function ContextMenu:register(predicate, items)
|
||||
predicate = command.generate_predicate(predicate)
|
||||
update_items_size(items, true)
|
||||
table.insert(self.itemset, { predicate = predicate, items = items })
|
||||
end
|
||||
|
||||
---Shows the context menu.
|
||||
---@param x number
|
||||
---@param y number
|
||||
---@return boolean # If true, the context menu is shown.
|
||||
function ContextMenu:show(x, y)
|
||||
self.items = nil
|
||||
local items_list = { width = 0, height = 0 }
|
||||
for _, items in ipairs(self.itemset) do
|
||||
if items.predicate(x, y) then
|
||||
items_list.width = math.max(items_list.width, items.items.width)
|
||||
items_list.height = items_list.height
|
||||
items_list.height = items_list.height + items.items.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
|
||||
table.insert(items_list, subitems)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -110,28 +78,27 @@ function ContextMenu:show(x, y)
|
|||
local w, h = self.items.width, self.items.height
|
||||
|
||||
-- by default the box is opened on the right and below
|
||||
x = common.clamp(x, 0, core.root_view.size.x - w - style.padding.x)
|
||||
y = common.clamp(y, 0, core.root_view.size.y - h)
|
||||
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
|
||||
|
||||
---Hides the context menu.
|
||||
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
|
||||
|
||||
---Returns an iterator that iterates over each context menu item and their dimensions.
|
||||
---@return fun(): number, core.contextmenu.item, number, number, number, number
|
||||
function ContextMenu:each_item()
|
||||
local x, y, w = self.position.x, self.position.y, self.items.width
|
||||
local oy = y
|
||||
|
@ -145,12 +112,8 @@ function ContextMenu:each_item()
|
|||
end)
|
||||
end
|
||||
|
||||
---Event handler for mouse movements.
|
||||
---@param px any
|
||||
---@param py any
|
||||
---@return boolean # true if the event is caught.
|
||||
function ContextMenu:on_mouse_moved(px, py)
|
||||
if not self.show_context_menu then return false end
|
||||
if not self.show_context_menu then return end
|
||||
|
||||
self.selected = -1
|
||||
for i, item, x, y, w, h in self:each_item() do
|
||||
|
@ -159,12 +122,14 @@ function ContextMenu:on_mouse_moved(px, py)
|
|||
break
|
||||
end
|
||||
end
|
||||
if self.selected >= 0 then
|
||||
core.request_cursor("arrow")
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
---Event handler for when the selection is confirmed.
|
||||
---@param item core.contextmenu.item
|
||||
function ContextMenu:on_selected(item)
|
||||
if item.disabled then return end
|
||||
if type(item.command) == "string" then
|
||||
command.perform(item.command)
|
||||
else
|
||||
|
@ -172,94 +137,56 @@ function ContextMenu:on_selected(item)
|
|||
end
|
||||
end
|
||||
|
||||
local function change_value(value, change)
|
||||
return value + change
|
||||
end
|
||||
|
||||
---Selects the the previous item.
|
||||
function ContextMenu:focus_previous()
|
||||
self.selected = (self.selected == -1 or self.selected == 1) and #self.items or change_value(self.selected, -1)
|
||||
if self:get_item_selected() == DIVIDER then
|
||||
self.selected = change_value(self.selected, -1)
|
||||
end
|
||||
end
|
||||
|
||||
---Selects the next item.
|
||||
function ContextMenu:focus_next()
|
||||
self.selected = (self.selected == -1 or self.selected == #self.items) and 1 or change_value(self.selected, 1)
|
||||
if self:get_item_selected() == DIVIDER then
|
||||
self.selected = change_value(self.selected, 1)
|
||||
end
|
||||
end
|
||||
|
||||
---Gets the currently selected item.
|
||||
---@return core.contextmenu.item|nil
|
||||
function ContextMenu:get_item_selected()
|
||||
return (self.items or {})[self.selected]
|
||||
end
|
||||
|
||||
---Hides the context menu and performs the command if an item is selected.
|
||||
function ContextMenu:call_selected_item()
|
||||
local selected = self:get_item_selected()
|
||||
self:hide()
|
||||
if selected then
|
||||
self:on_selected(selected)
|
||||
end
|
||||
end
|
||||
|
||||
---Event handler for mouse press.
|
||||
---@param button core.view.mousebutton
|
||||
---@param px number
|
||||
---@param py number
|
||||
---@param clicks number
|
||||
---@return boolean # true if the event is caught.
|
||||
function ContextMenu:on_mouse_pressed(button, px, py, clicks)
|
||||
function ContextMenu:on_mouse_pressed(button, x, y, clicks)
|
||||
local selected = (self.items or {})[self.selected]
|
||||
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)
|
||||
self:hide()
|
||||
if button == "left" then
|
||||
if selected then
|
||||
self:on_selected(selected)
|
||||
caught = true
|
||||
end
|
||||
end
|
||||
|
||||
if button == "right" then
|
||||
caught = self:show(x, y)
|
||||
end
|
||||
return caught
|
||||
end
|
||||
|
||||
---@type fun(self: table, k: string, dest: number, rate?: number, name?: string)
|
||||
ContextMenu.move_towards = View.move_towards
|
||||
|
||||
---Event handler for content update.
|
||||
function ContextMenu:update()
|
||||
if self.show_context_menu then
|
||||
self:move_towards("height", self.items.height, nil, "contextmenu")
|
||||
-- copied from core.docview
|
||||
function ContextMenu:move_towards(t, k, dest, rate)
|
||||
if type(t) ~= "table" then
|
||||
return self:move_towards(self, t, k, dest, rate)
|
||||
end
|
||||
local val = t[k]
|
||||
if not config.transitions or math.abs(val - dest) < 0.5 then
|
||||
t[k] = dest
|
||||
else
|
||||
rate = rate or 0.5
|
||||
if config.fps ~= 60 or config.animation_rate ~= 1 then
|
||||
local dt = 60 / config.fps
|
||||
rate = 1 - common.clamp(1 - rate, 1e-8, 1 - 1e-8)^(config.animation_rate * dt)
|
||||
end
|
||||
t[k] = common.lerp(val, dest, rate)
|
||||
end
|
||||
if val ~= dest then
|
||||
core.redraw = true
|
||||
end
|
||||
end
|
||||
|
||||
function ContextMenu:update()
|
||||
if self.show_context_menu then
|
||||
self:move_towards("height", self.items.height)
|
||||
end
|
||||
end
|
||||
|
||||
---Draws the context menu.
|
||||
---
|
||||
---This wraps `ContextMenu:draw_context_menu()`.
|
||||
---@see core.contextmenu.draw_context_menu
|
||||
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
|
||||
|
||||
---Draws the context menu.
|
||||
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
|
||||
|
@ -275,13 +202,14 @@ function ContextMenu:draw_context_menu()
|
|||
|
||||
for i, item, x, y, w, h in self:each_item() do
|
||||
if item == DIVIDER then
|
||||
renderer.draw_rect(x, y + divider_padding * SCALE, w, divider_width, style.divider)
|
||||
renderer.draw_rect(x, y, w, h, style.caret)
|
||||
else
|
||||
if i == self.selected then
|
||||
if i == self.selected and not self.disabled 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)
|
||||
local text_color = item.disabled and style.dim or style.text
|
||||
common.draw_text(style.font, text_color, 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
|
||||
|
|
|
@ -1,240 +0,0 @@
|
|||
local common = require "core.common"
|
||||
local config = require "core.config"
|
||||
local dirwatch = {}
|
||||
|
||||
function dirwatch:__index(idx)
|
||||
local value = rawget(self, idx)
|
||||
if value ~= nil then return value end
|
||||
return dirwatch[idx]
|
||||
end
|
||||
|
||||
function dirwatch.new()
|
||||
local t = {
|
||||
scanned = {},
|
||||
watched = {},
|
||||
reverse_watched = {},
|
||||
monitor = dirmonitor.new(),
|
||||
single_watch_top = nil,
|
||||
single_watch_count = 0
|
||||
}
|
||||
setmetatable(t, dirwatch)
|
||||
return t
|
||||
end
|
||||
|
||||
|
||||
function dirwatch:scan(directory, bool)
|
||||
if bool == false then return self:unwatch(directory) end
|
||||
self.scanned[directory] = system.get_file_info(directory).modified
|
||||
end
|
||||
|
||||
-- Should be called on every directory in a subdirectory.
|
||||
-- In windows, this is a no-op for anything underneath a top-level directory,
|
||||
-- but code should be called anyway, so we can ensure that we have a proper
|
||||
-- experience across all platforms. Should be an absolute path.
|
||||
-- Can also be called on individual files, though this should be used sparingly,
|
||||
-- so as not to run into system limits (like in the autoreload plugin).
|
||||
function dirwatch:watch(directory, bool)
|
||||
if bool == false then return self:unwatch(directory) end
|
||||
local info = system.get_file_info(directory)
|
||||
if not info then return end
|
||||
if not self.watched[directory] and not self.scanned[directory] then
|
||||
if self.monitor:mode() == "single" then
|
||||
if info.type ~= "dir" then return self:scan(directory) end
|
||||
if not self.single_watch_top or directory:find(self.single_watch_top, 1, true) ~= 1 then
|
||||
-- Get the highest level of directory that is common to this directory, and the original.
|
||||
local target = directory
|
||||
while self.single_watch_top and self.single_watch_top:find(target, 1, true) ~= 1 do
|
||||
target = common.dirname(target)
|
||||
end
|
||||
if target ~= self.single_watch_top then
|
||||
local value = self.monitor:watch(target)
|
||||
if value and value < 0 then
|
||||
return self:scan(directory)
|
||||
end
|
||||
self.single_watch_top = target
|
||||
end
|
||||
end
|
||||
self.single_watch_count = self.single_watch_count + 1
|
||||
self.watched[directory] = true
|
||||
else
|
||||
local value = self.monitor:watch(directory)
|
||||
-- If for whatever reason, we can't watch this directory, revert back to scanning.
|
||||
-- Don't bother trying to find out why, for now.
|
||||
if value and value < 0 then
|
||||
return self:scan(directory)
|
||||
end
|
||||
self.watched[directory] = value
|
||||
self.reverse_watched[value] = directory
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- this should be an absolute path
|
||||
function dirwatch:unwatch(directory)
|
||||
if self.watched[directory] then
|
||||
if self.monitor:mode() == "multiple" then
|
||||
self.monitor:unwatch(self.watched[directory])
|
||||
self.reverse_watched[directory] = nil
|
||||
else
|
||||
self.single_watch_count = self.single_watch_count - 1
|
||||
if self.single_watch_count == 0 then
|
||||
self.single_watch_top = nil
|
||||
self.monitor:unwatch(directory)
|
||||
end
|
||||
end
|
||||
self.watched[directory] = nil
|
||||
elseif self.scanned[directory] then
|
||||
self.scanned[directory] = nil
|
||||
end
|
||||
end
|
||||
|
||||
-- designed to be run inside a coroutine.
|
||||
function dirwatch:check(change_callback, scan_time, wait_time)
|
||||
local had_change = false
|
||||
local last_error
|
||||
self.monitor:check(function(id)
|
||||
had_change = true
|
||||
if self.monitor:mode() == "single" then
|
||||
local path = common.dirname(id)
|
||||
if not string.match(id, "^/") and not string.match(id, "^%a:[/\\]") then
|
||||
path = common.dirname(self.single_watch_top .. PATHSEP .. id)
|
||||
end
|
||||
change_callback(path)
|
||||
elseif self.reverse_watched[id] then
|
||||
change_callback(self.reverse_watched[id])
|
||||
end
|
||||
end, function(err)
|
||||
last_error = err
|
||||
end)
|
||||
if last_error ~= nil then error(last_error) end
|
||||
local start_time = system.get_time()
|
||||
for directory, old_modified in pairs(self.scanned) do
|
||||
if old_modified then
|
||||
local info = system.get_file_info(directory)
|
||||
local new_modified = info and info.modified
|
||||
if old_modified ~= new_modified then
|
||||
change_callback(directory)
|
||||
had_change = true
|
||||
self.scanned[directory] = new_modified
|
||||
end
|
||||
end
|
||||
if system.get_time() - start_time > (scan_time or 0.01) then
|
||||
coroutine.yield(wait_time or 0.01)
|
||||
start_time = system.get_time()
|
||||
end
|
||||
end
|
||||
return had_change
|
||||
end
|
||||
|
||||
|
||||
-- inspect config.ignore_files patterns and prepare ready to use entries.
|
||||
local function compile_ignore_files()
|
||||
local ipatterns = config.ignore_files
|
||||
local compiled = {}
|
||||
-- config.ignore_files could be a simple string...
|
||||
if type(ipatterns) ~= "table" then ipatterns = {ipatterns} end
|
||||
for i, pattern in ipairs(ipatterns) do
|
||||
-- we ignore malformed pattern that raise an error
|
||||
if pcall(string.match, "a", pattern) then
|
||||
table.insert(compiled, {
|
||||
use_path = pattern:match("/[^/$]"), -- contains a slash but not at the end
|
||||
-- An '/' or '/$' at the end means we want to match a directory.
|
||||
match_dir = pattern:match(".+/%$?$"), -- to be used as a boolen value
|
||||
pattern = pattern -- get the actual pattern
|
||||
})
|
||||
end
|
||||
end
|
||||
return compiled
|
||||
end
|
||||
|
||||
|
||||
local function fileinfo_pass_filter(info, ignore_compiled)
|
||||
if info.size >= config.file_size_limit * 1e6 then return false end
|
||||
local basename = common.basename(info.filename)
|
||||
-- replace '\' with '/' for Windows where PATHSEP = '\'
|
||||
local fullname = "/" .. info.filename:gsub("\\", "/")
|
||||
for _, compiled in ipairs(ignore_compiled) do
|
||||
local test = compiled.use_path and fullname or basename
|
||||
if compiled.match_dir then
|
||||
if info.type == "dir" and string.match(test .. "/", compiled.pattern) then
|
||||
return false
|
||||
end
|
||||
else
|
||||
if string.match(test, compiled.pattern) then
|
||||
return false
|
||||
end
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
|
||||
local function compare_file(a, b)
|
||||
return system.path_compare(a.filename, a.type, b.filename, b.type)
|
||||
end
|
||||
|
||||
|
||||
-- compute a file's info entry completed with "filename" to be used
|
||||
-- in project scan or falsy if it shouldn't appear in the list.
|
||||
local function get_project_file_info(root, file, ignore_compiled)
|
||||
local info = system.get_file_info(common.basepath(root) .. file)
|
||||
|
||||
-- info can be not nil but info.type may be nil if is neither a file neither
|
||||
-- a directory, for example for /dev/* entries on linux.
|
||||
if info and info.type then
|
||||
info.filename = file
|
||||
return fileinfo_pass_filter(info, ignore_compiled) and info
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- "root" will by an absolute path without trailing '/'
|
||||
-- "path" will be a path starting without '/' and without trailing '/'
|
||||
-- or the empty string.
|
||||
-- It identifies a sub-path within "root".
|
||||
-- The current path location will therefore always be: root .. path.
|
||||
-- When recursing, "root" will always be the same, only "path" will change.
|
||||
-- Returns a list of file "items". In each item the "filename" will be the
|
||||
-- complete file path relative to "root" *without* the trailing '/', and without the starting '/'.
|
||||
function dirwatch.get_directory_files(dir, root, path, entries_count, recurse_pred)
|
||||
local t = {}
|
||||
local t0 = system.get_time()
|
||||
local ignore_compiled = compile_ignore_files()
|
||||
|
||||
local all = system.list_dir(common.basepath(root) .. path)
|
||||
|
||||
if not all then return nil end
|
||||
local entries = { }
|
||||
for _, file in ipairs(all) do
|
||||
local info = get_project_file_info(root, (path ~= "" and (path .. PATHSEP) or "") .. file, ignore_compiled)
|
||||
if info then
|
||||
table.insert(entries, info)
|
||||
end
|
||||
end
|
||||
table.sort(entries, compare_file)
|
||||
|
||||
local recurse_complete = true
|
||||
for _, info in ipairs(entries) do
|
||||
table.insert(t, info)
|
||||
entries_count = entries_count + 1
|
||||
if info.type == "dir" then
|
||||
if recurse_pred(dir, info.filename, entries_count, system.get_time() - t0) then
|
||||
local t_rec, complete, n = dirwatch.get_directory_files(dir, root, info.filename, entries_count, recurse_pred)
|
||||
recurse_complete = recurse_complete and complete
|
||||
if n ~= nil then
|
||||
entries_count = n
|
||||
for _, info_rec in ipairs(t_rec) do
|
||||
table.insert(t, info_rec)
|
||||
end
|
||||
end
|
||||
else
|
||||
recurse_complete = false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return t, recurse_complete, entries_count
|
||||
end
|
||||
|
||||
|
||||
return dirwatch
|
|
@ -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"
|
||||
|
@ -10,101 +9,53 @@ local Highlighter = Object:extend()
|
|||
|
||||
function Highlighter:new(doc)
|
||||
self.doc = doc
|
||||
self.running = false
|
||||
self:reset()
|
||||
end
|
||||
|
||||
-- init incremental syntax highlighting
|
||||
function Highlighter:start()
|
||||
if self.running then return end
|
||||
self.running = true
|
||||
-- init incremental syntax highlighting
|
||||
core.add_thread(function()
|
||||
while self.first_invalid_line <= self.max_wanted_line do
|
||||
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 line and line.resume and (line.init_state ~= state or line.text ~= self.doc.lines[i]) then
|
||||
-- Reset the progress if no longer valid
|
||||
line.resume = nil
|
||||
end
|
||||
if not (line and line.init_state == state and line.text == self.doc.lines[i] and not line.resume) then
|
||||
retokenized_from = retokenized_from or i
|
||||
self.lines[i] = self:tokenize_line(i, state, line and line.resume)
|
||||
if self.lines[i].resume then
|
||||
self.first_invalid_line = i
|
||||
goto yield
|
||||
while true do
|
||||
if self.first_invalid_line > self.max_wanted_line then
|
||||
self.max_wanted_line = 0
|
||||
coroutine.yield(1 / config.fps)
|
||||
|
||||
else
|
||||
local max = math.min(self.first_invalid_line + 40, self.max_wanted_line)
|
||||
|
||||
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) then
|
||||
self.lines[i] = self:tokenize_line(i, state)
|
||||
end
|
||||
elseif retokenized_from then
|
||||
self:update_notify(retokenized_from, i - retokenized_from - 1)
|
||||
retokenized_from = nil
|
||||
end
|
||||
end
|
||||
|
||||
self.first_invalid_line = max + 1
|
||||
::yield::
|
||||
if retokenized_from then
|
||||
self:update_notify(retokenized_from, max - retokenized_from)
|
||||
self.first_invalid_line = max + 1
|
||||
core.redraw = true
|
||||
coroutine.yield()
|
||||
end
|
||||
core.redraw = true
|
||||
coroutine.yield()
|
||||
end
|
||||
self.max_wanted_line = 0
|
||||
self.running = false
|
||||
end, self)
|
||||
end
|
||||
|
||||
local function set_max_wanted_lines(self, amount)
|
||||
self.max_wanted_line = amount
|
||||
if self.first_invalid_line <= self.max_wanted_line then
|
||||
self:start()
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function Highlighter:reset()
|
||||
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)
|
||||
set_max_wanted_lines(self, 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
|
||||
self.max_wanted_line = math.min(self.max_wanted_line, #self.doc.lines)
|
||||
end
|
||||
|
||||
|
||||
function Highlighter:tokenize_line(idx, state, resume)
|
||||
function Highlighter:tokenize_line(idx, state)
|
||||
local res = {}
|
||||
res.init_state = state
|
||||
res.text = self.doc.lines[idx]
|
||||
res.tokens, res.state, res.resume = tokenizer.tokenize(self.doc.syntax, res.text, state, resume)
|
||||
res.tokens, res.state = tokenizer.tokenize(self.doc.syntax, res.text, state)
|
||||
return res
|
||||
end
|
||||
|
||||
|
@ -115,9 +66,8 @@ 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
|
||||
set_max_wanted_lines(self, math.max(self.max_wanted_line, idx))
|
||||
self.max_wanted_line = math.max(self.max_wanted_line, idx)
|
||||
return line
|
||||
end
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ local syntax = require "core.syntax"
|
|||
local config = require "core.config"
|
||||
local common = require "core.common"
|
||||
|
||||
---@class core.doc : core.object
|
||||
|
||||
local Doc = Object:extend()
|
||||
|
||||
|
||||
|
@ -33,7 +33,7 @@ end
|
|||
function Doc:reset()
|
||||
self.lines = { "\n" }
|
||||
self.selections = { 1, 1, 1, 1 }
|
||||
self.last_selection = 1
|
||||
self.cursor_clipboard = {}
|
||||
self.undo_stack = { idx = 1 }
|
||||
self.redo_stack = { idx = 1 }
|
||||
self.clean_change_id = 1
|
||||
|
@ -44,15 +44,10 @@ end
|
|||
|
||||
function Doc:reset_syntax()
|
||||
local header = self:get_text(1, 1, self:position_offset(1, 1, 128))
|
||||
local path = self.abs_filename
|
||||
if not path and self.filename then
|
||||
path = common.basepath(core.project_dir) .. self.filename
|
||||
end
|
||||
if path then path = common.normalize_path(path) end
|
||||
local syn = syntax.get(path, header)
|
||||
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
|
||||
|
||||
|
@ -60,7 +55,6 @@ end
|
|||
function Doc:set_filename(filename, abs_filename)
|
||||
self.filename = filename
|
||||
self.abs_filename = abs_filename
|
||||
self:reset_syntax()
|
||||
end
|
||||
|
||||
|
||||
|
@ -68,15 +62,12 @@ function Doc:load(filename)
|
|||
local fp = assert( io.open(filename, "rb") )
|
||||
self:reset()
|
||||
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")
|
||||
|
@ -86,23 +77,11 @@ 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
|
||||
local fp = assert( io.open(filename, "wb") )
|
||||
for _, line in ipairs(self.lines) do
|
||||
|
@ -112,6 +91,7 @@ function Doc:save(filename, abs_filename)
|
|||
fp:close()
|
||||
self:set_filename(filename, abs_filename)
|
||||
self.new_file = false
|
||||
self:reset_syntax()
|
||||
self:clean()
|
||||
end
|
||||
|
||||
|
@ -122,12 +102,7 @@ end
|
|||
|
||||
|
||||
function Doc:is_dirty()
|
||||
if self.new_file then
|
||||
if self.filename then return true end
|
||||
return #self.lines > 1 or #self.lines[1] > 1
|
||||
else
|
||||
return self.clean_change_id ~= self:get_change_id()
|
||||
end
|
||||
return self.clean_change_id ~= self:get_change_id() or self.new_file
|
||||
end
|
||||
|
||||
|
||||
|
@ -136,62 +111,17 @@ 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
|
||||
|
||||
local function sort_positions(line1, col1, line2, col2)
|
||||
if line1 > line2 or line1 == line2 and col1 > col2 then
|
||||
return line2, col2, line1, col1, true
|
||||
end
|
||||
return line1, col1, line2, col2, false
|
||||
end
|
||||
|
||||
-- Cursor section. Cursor indices are *only* valid during a get_selections() call.
|
||||
-- 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 line1, col1, line2, col2, swap = self:get_selection_idx(self.last_selection, sort)
|
||||
if not line1 then
|
||||
line1, col1, line2, col2, swap = self:get_selection_idx(1, sort)
|
||||
end
|
||||
return line1, col1, line2, col2, swap
|
||||
end
|
||||
|
||||
|
||||
---Get the selection specified by `idx`
|
||||
---@param idx integer @the index of the selection to retrieve
|
||||
---@param sort? boolean @whether to sort the selection returned
|
||||
---@return integer,integer,integer,integer,boolean? @line1, col1, line2, col2, was the selection sorted
|
||||
function Doc:get_selection_idx(idx, sort)
|
||||
local line1, col1, line2, col2 = self.selections[idx*4-3], self.selections[idx*4-2], self.selections[idx*4-1], self.selections[idx*4]
|
||||
if line1 and sort then
|
||||
return sort_positions(line1, col1, line2, col2)
|
||||
else
|
||||
return line1, col1, line2, col2
|
||||
end
|
||||
end
|
||||
|
||||
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")
|
||||
local idx, line1, col1, line2, col2 = self:get_selections(sort)({ self.selections, sort }, 0)
|
||||
return line1, col1, line2, col2, sort
|
||||
end
|
||||
|
||||
function Doc:has_selection()
|
||||
|
@ -199,19 +129,19 @@ function Doc:has_selection()
|
|||
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
|
||||
return line2, col2, line1, col1
|
||||
end
|
||||
return line1, col1, line2, col2
|
||||
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
|
||||
|
@ -230,22 +160,11 @@ function Doc:add_selection(line1, col1, line2, col2, swap)
|
|||
end
|
||||
end
|
||||
self:set_selections(target, line1, col1, line2, col2, swap, 0)
|
||||
self.last_selection = target
|
||||
end
|
||||
|
||||
|
||||
function Doc:remove_selection(idx)
|
||||
if self.last_selection >= idx then
|
||||
self.last_selection = self.last_selection - 1
|
||||
end
|
||||
common.splice(self.selections, (idx - 1) * 4 + 1, 4)
|
||||
end
|
||||
|
||||
|
||||
function Doc:set_selection(line1, col1, line2, col2, swap)
|
||||
self.selections = {}
|
||||
self.selections, self.cursor_clipboard = {}, {}
|
||||
self:set_selections(1, line1, col1, line2, col2, swap)
|
||||
self.last_selection = 1
|
||||
end
|
||||
|
||||
function Doc:merge_cursors(idx)
|
||||
|
@ -254,9 +173,6 @@ function Doc:merge_cursors(idx)
|
|||
if self.selections[i] == self.selections[j] and
|
||||
self.selections[i+1] == self.selections[j+1] then
|
||||
common.splice(self.selections, i, 4)
|
||||
if self.last_selection >= (i+3)/4 then
|
||||
self.last_selection = self.last_selection - 1
|
||||
end
|
||||
break
|
||||
end
|
||||
end
|
||||
|
@ -267,9 +183,9 @@ 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))
|
||||
return idx+(invariant[3] and -1 or 1), sort_positions(unpack(invariant[1], target, target+4))
|
||||
else
|
||||
return idx+(invariant[3] and -1 or 1), table.unpack(invariant[1], target, target+4)
|
||||
return idx+(invariant[3] and -1 or 1), unpack(invariant[1], target, target+4)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -282,13 +198,9 @@ end
|
|||
-- End of cursor seciton.
|
||||
|
||||
function Doc:sanitize_position(line, col)
|
||||
local nlines = #self.lines
|
||||
if line > nlines then
|
||||
return nlines, #self.lines[nlines]
|
||||
elseif line < 1 then
|
||||
return 1, 1
|
||||
end
|
||||
return line, common.clamp(col, 1, #self.lines[line])
|
||||
line = common.clamp(line, 1, #self.lines)
|
||||
col = common.clamp(col, 1, #self.lines[line])
|
||||
return line, col
|
||||
end
|
||||
|
||||
|
||||
|
@ -374,8 +286,7 @@ local function pop_undo(self, undo_stack, redo_stack, modified)
|
|||
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.selections = { unpack(cmd) }
|
||||
end
|
||||
|
||||
modified = modified or (cmd.type ~= "selection")
|
||||
|
@ -396,7 +307,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
|
||||
|
@ -408,21 +318,13 @@ function Doc:raw_insert(line, col, text, undo_stack, time)
|
|||
-- splice lines into line array
|
||||
common.splice(self.lines, line, 1, lines)
|
||||
|
||||
-- keep cursors where they should be
|
||||
for idx, cline1, ccol1, cline2, ccol2 in self:get_selections(true, true) do
|
||||
if cline1 < line then break end
|
||||
local line_addition = (line < cline1 or col < ccol1) and #lines - 1 or 0
|
||||
local column_addition = line == cline1 and ccol1 > col and len or 0
|
||||
self:set_selections(idx, cline1 + line_addition, ccol1 + column_addition, cline2 + line_addition, ccol2 + column_addition)
|
||||
end
|
||||
|
||||
-- push undo
|
||||
local line2, col2 = self:position_offset(line, col, #text)
|
||||
push_undo(undo_stack, time, "selection", table.unpack(self.selections))
|
||||
push_undo(undo_stack, time, "selection", unpack(self.selections))
|
||||
push_undo(undo_stack, time, "remove", line, col, line2, col2)
|
||||
|
||||
-- update highlighter and assure selection is in bounds
|
||||
self.highlighter:insert_notify(line, #lines - 1)
|
||||
self.highlighter:invalidate(line)
|
||||
self:sanitize_selection()
|
||||
end
|
||||
|
||||
|
@ -430,68 +332,24 @@ 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", unpack(self.selections))
|
||||
push_undo(undo_stack, time, "insert", line1, col1, text)
|
||||
|
||||
-- get line content before/after removed text
|
||||
local before = self.lines[line1]:sub(1, col1 - 1)
|
||||
local after = self.lines[line2]:sub(col2)
|
||||
|
||||
local line_removal = line2 - line1
|
||||
local col_removal = col2 - col1
|
||||
|
||||
-- splice line into line array
|
||||
common.splice(self.lines, line1, line_removal + 1, { before .. after })
|
||||
|
||||
local merge = false
|
||||
|
||||
-- keep selections in correct positions: each pair (line, col)
|
||||
-- * remains unchanged if before the deleted text
|
||||
-- * is set to (line1, col1) if in the deleted text
|
||||
-- * is set to (line1, col - col_removal) if on line2 but out of the deleted text
|
||||
-- * is set to (line - line_removal, col) if after line2
|
||||
for idx, cline1, ccol1, cline2, ccol2 in self:get_selections(true, true) do
|
||||
if cline2 < line1 then break end
|
||||
local l1, c1, l2, c2 = cline1, ccol1, cline2, ccol2
|
||||
|
||||
if cline1 > line1 or (cline1 == line1 and ccol1 > col1) then
|
||||
if cline1 > line2 then
|
||||
l1 = l1 - line_removal
|
||||
else
|
||||
l1 = line1
|
||||
c1 = (cline1 == line2 and ccol1 > col2) and c1 - col_removal or col1
|
||||
end
|
||||
end
|
||||
|
||||
if cline2 > line1 or (cline2 == line1 and ccol2 > col1) then
|
||||
if cline2 > line2 then
|
||||
l2 = l2 - line_removal
|
||||
else
|
||||
l2 = line1
|
||||
c2 = (cline2 == line2 and ccol2 > col2) and c2 - col_removal or col1
|
||||
end
|
||||
end
|
||||
|
||||
if l1 == line1 and c1 == col1 then merge = true end
|
||||
self:set_selections(idx, l1, c1, l2, c2)
|
||||
end
|
||||
|
||||
if merge then
|
||||
self:merge_cursors()
|
||||
end
|
||||
common.splice(self.lines, line1, line2 - line1 + 1, { before .. after })
|
||||
|
||||
-- update highlighter and assure selection is in bounds
|
||||
self.highlighter:remove_notify(line1, line_removal)
|
||||
self.highlighter:invalidate(line1)
|
||||
self:sanitize_selection()
|
||||
end
|
||||
|
||||
|
||||
function Doc:insert(line, col, text)
|
||||
self.redo_stack = { idx = 1 }
|
||||
-- Reset the clean id when we're pushing something new before it
|
||||
if self:get_change_id() < self.clean_change_id then
|
||||
self.clean_change_id = -1
|
||||
end
|
||||
line, col = self:sanitize_position(line, col)
|
||||
self:raw_insert(line, col, text, self.undo_stack, system.get_time())
|
||||
self:on_text_change("insert")
|
||||
|
@ -519,7 +377,7 @@ end
|
|||
|
||||
|
||||
function Doc:text_input(text, idx)
|
||||
for sidx, line1, col1, line2, col2 in self:get_selections(true, idx or true) do
|
||||
for sidx, line1, col1, line2, col2 in self:get_selections(true, idx) do
|
||||
if line1 ~= line2 or col1 ~= col2 then
|
||||
self:delete_to_cursor(sidx)
|
||||
end
|
||||
|
@ -529,44 +387,22 @@ function Doc:text_input(text, idx)
|
|||
end
|
||||
|
||||
|
||||
function Doc:ime_text_editing(text, start, length, idx)
|
||||
for sidx, line1, col1, line2, col2 in self:get_selections(true, idx or true) do
|
||||
if line1 ~= line2 or col1 ~= col2 then
|
||||
self:delete_to_cursor(sidx)
|
||||
end
|
||||
self:insert(line1, col1, text)
|
||||
self:set_selections(sidx, line1, col1 + #text, line1, col1)
|
||||
function Doc:replace(fn)
|
||||
local line1, col1, line2, col2 = self:get_selection(true)
|
||||
if line1 == line2 and col1 == col2 then
|
||||
line1, col1, line2, col2 = 1, 1, #self.lines, #self.lines[#self.lines]
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function Doc:replace_cursor(idx, line1, col1, line2, col2, fn)
|
||||
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
|
||||
line2, col2 = self:position_offset(line1, col1, #new_text)
|
||||
self:set_selections(idx, line1, col1, line2, col2)
|
||||
self:set_selection(line1, col1, line2, col2)
|
||||
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
|
||||
|
||||
|
||||
|
@ -604,21 +440,19 @@ 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
|
||||
local function get_indent_string()
|
||||
if config.tab_type == "hard" then
|
||||
return "\t"
|
||||
end
|
||||
return string.rep(" ", indent_size)
|
||||
return string.rep(" ", config.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 function 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 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
|
||||
|
@ -640,19 +474,17 @@ end
|
|||
-- * 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 text = 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
|
||||
local e, rnded = 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
|
||||
l1d, l2d = #self.lines[line1] - l1d, #self.lines[line2] - l2d
|
||||
if (unindent or in_beginning_whitespace) and not has_selection then
|
||||
|
|
|
@ -22,73 +22,37 @@ local function init_args(doc, line, col, text, opt)
|
|||
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
|
||||
|
||||
local re
|
||||
if opt.regex then
|
||||
pattern = regex.compile(text, opt.no_case and "i" or "")
|
||||
search_func = regex_func
|
||||
re = regex.compile(text, opt.no_case and "i" or "")
|
||||
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
|
||||
line_text = line_text:lower()
|
||||
end
|
||||
local s, e
|
||||
if opt.reverse then
|
||||
s, e = rfind(search_func, line_text, pattern, col - 1, plain)
|
||||
if opt.regex then
|
||||
local s, e = re:cmatch(line_text, col)
|
||||
if s then
|
||||
return line, s, line, e
|
||||
end
|
||||
col = 1
|
||||
else
|
||||
s, e = search_func(line_text, pattern, col, plain)
|
||||
end
|
||||
if s then
|
||||
local line2 = line
|
||||
-- If we've matched the newline too,
|
||||
-- return until the initial character of the next line.
|
||||
if e >= #doc.lines[line] then
|
||||
line2 = line + 1
|
||||
e = 0
|
||||
if opt.no_case then
|
||||
line_text = line_text:lower()
|
||||
end
|
||||
-- Avoid returning matches that go beyond the last line.
|
||||
-- This is needed to avoid selecting the "last" newline.
|
||||
if line2 <= #doc.lines then
|
||||
return line, s, line2, e + 1
|
||||
local s, e = line_text:find(text, col, true)
|
||||
if s then
|
||||
return line, s, line, e + 1
|
||||
end
|
||||
col = 1
|
||||
end
|
||||
col = opt.reverse and -1 or 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, regex = opt.regex }
|
||||
return search.find(doc, 1, 1, text, opt)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -4,14 +4,11 @@ local config = require "core.config"
|
|||
local style = require "core.style"
|
||||
local keymap = require "core.keymap"
|
||||
local translate = require "core.doc.translate"
|
||||
local ime = require "core.ime"
|
||||
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
|
||||
|
@ -31,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,
|
||||
|
@ -61,33 +55,25 @@ function DocView:new(doc)
|
|||
self.doc = assert(doc)
|
||||
self.font = "code_font"
|
||||
self.last_x_offset = {}
|
||||
self.ime_selection = { from = 0, size = 0 }
|
||||
self.ime_status = false
|
||||
self.hovering_gutter = false
|
||||
self.v_scrollbar:set_forced_status(config.force_scrollbar_status)
|
||||
self.h_scrollbar:set_forced_status(config.force_scrollbar_status)
|
||||
end
|
||||
|
||||
|
||||
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
|
||||
|
@ -111,17 +97,9 @@ end
|
|||
|
||||
|
||||
function DocView:get_scrollable_size()
|
||||
if not config.scroll_past_end then
|
||||
local _, _, _, h_scroll = self.h_scrollbar:get_track_rect()
|
||||
return self:get_line_height() * (#self.doc.lines) + style.padding.y * 2 + h_scroll
|
||||
end
|
||||
return self:get_line_height() * (#self.doc.lines - 1) + self.size.y
|
||||
end
|
||||
|
||||
function DocView:get_h_scrollable_size()
|
||||
return math.huge
|
||||
end
|
||||
|
||||
|
||||
function DocView:get_font()
|
||||
return style[self.font]
|
||||
|
@ -139,18 +117,14 @@ function DocView:get_gutter_width()
|
|||
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()
|
||||
|
@ -161,40 +135,28 @@ end
|
|||
function DocView:get_visible_line_range()
|
||||
local x, y, x2, y2 = self:get_content_bounds()
|
||||
local lh = self:get_line_height()
|
||||
local minline = math.max(1, math.floor((y - style.padding.y) / lh) + 1)
|
||||
local maxline = math.min(#self.doc.lines, math.floor((y2 - style.padding.y) / lh) + 1)
|
||||
local minline = math.max(1, math.floor(y / lh))
|
||||
local maxline = math.min(#self.doc.lines, math.floor(y2 / lh) + 1)
|
||||
return minline, maxline
|
||||
end
|
||||
|
||||
|
||||
function DocView:get_col_x_offset(line, col)
|
||||
local default_font = self:get_font()
|
||||
local _, indent_size = self.doc:get_indent_info()
|
||||
default_font:set_tab_size(indent_size)
|
||||
local column = 1
|
||||
local xoffset = 0
|
||||
for _, type, text in self.doc.highlighter:each_token(line) do
|
||||
local font = style.syntax_fonts[type] or default_font
|
||||
if font ~= default_font then font:set_tab_size(indent_size) end
|
||||
local length = #text
|
||||
if column + length <= col then
|
||||
xoffset = xoffset + font:get_width(text)
|
||||
column = column + length
|
||||
if column >= col then
|
||||
return xoffset
|
||||
end
|
||||
else
|
||||
for char in common.utf8_chars(text) do
|
||||
if column >= col then
|
||||
return xoffset
|
||||
end
|
||||
xoffset = xoffset + font:get_width(char)
|
||||
column = column + #char
|
||||
for char in common.utf8_chars(text) do
|
||||
if column == col then
|
||||
return xoffset / font:subpixel_scale()
|
||||
end
|
||||
xoffset = xoffset + font:get_width_subpixel(char)
|
||||
column = column + #char
|
||||
end
|
||||
end
|
||||
|
||||
return xoffset
|
||||
return xoffset / default_font:subpixel_scale()
|
||||
end
|
||||
|
||||
|
||||
|
@ -203,27 +165,18 @@ function DocView:get_x_offset_col(line, x)
|
|||
|
||||
local xoffset, last_i, i = 0, 1, 1
|
||||
local default_font = self:get_font()
|
||||
local _, indent_size = self.doc:get_indent_info()
|
||||
default_font:set_tab_size(indent_size)
|
||||
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
|
||||
if font ~= default_font then font:set_tab_size(indent_size) end
|
||||
local width = font:get_width(text)
|
||||
-- Don't take the shortcut if the width matches x,
|
||||
-- because we need last_i which should be calculated using utf-8.
|
||||
if xoffset + width < x then
|
||||
xoffset = xoffset + width
|
||||
i = i + #text
|
||||
else
|
||||
for char in common.utf8_chars(text) do
|
||||
local w = font:get_width(char)
|
||||
if xoffset >= x then
|
||||
return (xoffset - x > w / 2) and last_i or i
|
||||
end
|
||||
xoffset = xoffset + w
|
||||
last_i = i
|
||||
i = i + #char
|
||||
for char in common.utf8_chars(text) do
|
||||
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
|
||||
i = i + #char
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -243,10 +196,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()
|
||||
local _, _, _, scroll_h = self.h_scrollbar:get_track_rect()
|
||||
self.scroll.to.y = math.max(0, y - oy - (self.size.y - scroll_h) / 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
|
||||
|
@ -255,69 +206,36 @@ end
|
|||
|
||||
|
||||
function DocView:scroll_to_make_visible(line, col)
|
||||
local _, oy = self:get_content_offset()
|
||||
local _, ly = self:get_line_screen_position(line, col)
|
||||
local lh = self:get_line_height()
|
||||
local _, _, _, scroll_h = self.h_scrollbar:get_track_rect()
|
||||
self.scroll.to.y = common.clamp(self.scroll.to.y, ly - oy - self.size.y + scroll_h + 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(' ')
|
||||
local xsup = xoffset + gw + xmargin
|
||||
local xinf = xoffset - xmargin
|
||||
local _, _, scroll_w = self.v_scrollbar:get_track_rect()
|
||||
local size_x = math.max(0, self.size.x - scroll_w)
|
||||
if xsup > self.scroll.x + size_x then
|
||||
self.scroll.to.x = xsup - size_x
|
||||
if xsup > self.scroll.x + self.size.x then
|
||||
self.scroll.to.x = xsup - self.size.x
|
||||
elseif xinf < self.scroll.x then
|
||||
self.scroll.to.x = math.max(0, xinf)
|
||||
end
|
||||
end
|
||||
|
||||
function DocView:on_mouse_moved(x, y, ...)
|
||||
DocView.super.on_mouse_moved(self, x, y, ...)
|
||||
|
||||
self.hovering_gutter = false
|
||||
local gw = self:get_gutter_width()
|
||||
|
||||
if self:scrollbar_hovering() or self:scrollbar_dragging() then
|
||||
self.cursor = "arrow"
|
||||
elseif gw > 0 and x >= self.position.x and x <= (self.position.x + gw) then
|
||||
self.cursor = "arrow"
|
||||
self.hovering_gutter = true
|
||||
else
|
||||
self.cursor = "ibeam"
|
||||
end
|
||||
|
||||
if self.mouse_selecting then
|
||||
local l1, c1 = self:resolve_screen_position(x, y)
|
||||
local l2, c2, snap_type = table.unpack(self.mouse_selecting)
|
||||
if keymap.modkeys["ctrl"] then
|
||||
if l1 > l2 then l1, l2 = l2, l1 end
|
||||
self.doc.selections = { }
|
||||
for i = l1, l2 do
|
||||
self.doc:set_selections(i - l1 + 1, i, math.min(c1, #self.doc.lines[i]), i, math.min(c2, #self.doc.lines[i]))
|
||||
end
|
||||
else
|
||||
if snap_type then
|
||||
l1, c1, l2, c2 = self:mouse_selection(self.doc, snap_type, l1, c1, l2, c2)
|
||||
end
|
||||
self.doc:set_selection(l1, c1, l2, c2)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function DocView:mouse_selection(doc, snap_type, line1, col1, line2, col2)
|
||||
local 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
|
||||
|
@ -327,30 +245,57 @@ end
|
|||
|
||||
|
||||
function DocView:on_mouse_pressed(button, x, y, clicks)
|
||||
if button ~= "left" or not self.hovering_gutter then
|
||||
return DocView.super.on_mouse_pressed(self, button, x, y, clicks)
|
||||
local caught = DocView.super.on_mouse_pressed(self, button, x, y, clicks)
|
||||
if caught then
|
||||
return
|
||||
end
|
||||
local line = self:resolve_screen_position(x, y)
|
||||
if keymap.modkeys["shift"] then
|
||||
local sline, scol, sline2, scol2 = self.doc:get_selection(true)
|
||||
if line > sline then
|
||||
self.doc:set_selection(sline, 1, line, #self.doc.lines[line])
|
||||
else
|
||||
self.doc:set_selection(line, 1, sline2, #self.doc.lines[sline2])
|
||||
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
|
||||
if clicks == 1 then
|
||||
self.doc:set_selection(line, 1, line, 1)
|
||||
elseif clicks == 2 then
|
||||
self.doc:set_selection(line, 1, line, #self.doc.lines[line])
|
||||
local line, col = self:resolve_screen_position(x, y)
|
||||
if keymap.modkeys["ctrl"] then
|
||||
self.doc:add_selection(mouse_selection(self.doc, clicks, line, col, line, col))
|
||||
else
|
||||
self.doc:set_selection(mouse_selection(self.doc, clicks, line, col, line, col))
|
||||
end
|
||||
self.mouse_selecting = { line, col, clicks = clicks }
|
||||
end
|
||||
return true
|
||||
core.blink_reset()
|
||||
end
|
||||
|
||||
|
||||
function DocView:on_mouse_released(...)
|
||||
DocView.super.on_mouse_released(self, ...)
|
||||
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
|
||||
if keymap.modkeys["ctrl"] then
|
||||
if l1 > l2 then l1, l2 = l2, l1 end
|
||||
self.doc.selections = { }
|
||||
for i = l1, l2 do
|
||||
self.doc:set_selections(i - l1 + 1, i, math.min(c1, #self.doc.lines[i]), i, math.min(c2, #self.doc.lines[i]))
|
||||
end
|
||||
else
|
||||
self.doc:set_selection(mouse_selection(self.doc, clicks, l1, c1, l2, c2))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function DocView:on_mouse_released(button)
|
||||
DocView.super.on_mouse_released(self, button)
|
||||
self.mouse_selecting = nil
|
||||
end
|
||||
|
||||
|
@ -359,58 +304,16 @@ function DocView:on_text_input(text)
|
|||
self.doc:text_input(text)
|
||||
end
|
||||
|
||||
function DocView:on_ime_text_editing(text, start, length)
|
||||
self.doc:ime_text_editing(text, start, length)
|
||||
self.ime_status = #text > 0
|
||||
self.ime_selection.from = start
|
||||
self.ime_selection.size = length
|
||||
|
||||
-- Set the composition bounding box that the system IME
|
||||
-- will consider when drawing its interface
|
||||
local line1, col1, line2, col2 = self.doc:get_selection(true)
|
||||
local col = math.min(col1, col2)
|
||||
self:update_ime_location()
|
||||
self:scroll_to_make_visible(line1, col + start)
|
||||
end
|
||||
|
||||
---Update the composition bounding box that the system IME
|
||||
---will consider when drawing its interface
|
||||
function DocView:update_ime_location()
|
||||
if not self.ime_status then return end
|
||||
|
||||
local line1, col1, line2, col2 = self.doc:get_selection(true)
|
||||
local x, y = self:get_line_screen_position(line1)
|
||||
local h = self:get_line_height()
|
||||
local col = math.min(col1, col2)
|
||||
|
||||
local x1, x2 = 0, 0
|
||||
|
||||
if self.ime_selection.size > 0 then
|
||||
-- focus on a part of the text
|
||||
local from = col + self.ime_selection.from
|
||||
local to = from + self.ime_selection.size
|
||||
x1 = self:get_col_x_offset(line1, from)
|
||||
x2 = self:get_col_x_offset(line1, to)
|
||||
else
|
||||
-- focus the whole text
|
||||
x1 = self:get_col_x_offset(line1, col1)
|
||||
x2 = self:get_col_x_offset(line2, col2)
|
||||
end
|
||||
|
||||
ime.set_location(x + x1, y, x2 - x1, h)
|
||||
end
|
||||
|
||||
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
|
||||
if core.active_view == self and not ime.editing then
|
||||
self:scroll_to_make_visible(line1, col1)
|
||||
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(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
|
||||
|
@ -423,8 +326,6 @@ function DocView:update()
|
|||
core.blink_timer = tb
|
||||
end
|
||||
|
||||
self:update_ime_location()
|
||||
|
||||
DocView.super.update(self)
|
||||
end
|
||||
|
||||
|
@ -435,24 +336,19 @@ 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()
|
||||
local last_token = nil
|
||||
local tokens = self.doc.highlighter:get_line(line).tokens
|
||||
local tokens_count = #tokens
|
||||
if tokens[tokens_count] ~= nil and string.sub(tokens[tokens_count], -1) == "\n" then
|
||||
last_token = tokens_count - 1
|
||||
end
|
||||
for tidx, type, text in self.doc.highlighter:each_token(line) do
|
||||
local 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
|
||||
-- do not render newline, fixes issue #1164
|
||||
if tidx == last_token then text = text:sub(1, -2) end
|
||||
tx = renderer.draw_text(font, text, tx, ty, color)
|
||||
if tx > self.position.x + self.size.x then 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
|
||||
return self:get_line_height()
|
||||
end
|
||||
|
||||
function DocView:draw_caret(x, y)
|
||||
|
@ -460,84 +356,43 @@ function DocView:draw_caret(x, y)
|
|||
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
|
||||
function DocView:draw_line_body(idx, x, y)
|
||||
-- draw selection if it overlaps this line
|
||||
for lidx, line1, col1, line2, col2 in self.doc:get_selections(true) do
|
||||
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
|
||||
end
|
||||
if draw_highlight and core.active_view == self then
|
||||
self:draw_line_highlight(x + self.scroll.x, y)
|
||||
end
|
||||
|
||||
-- draw selection if it overlaps this line
|
||||
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
|
||||
-- draw line highlight if caret is on this line
|
||||
if config.highlight_current_line and (line1 == line2 and col1 == col2)
|
||||
and line1 == idx and core.active_view == self then
|
||||
self:draw_line_highlight(x + self.scroll.x, y)
|
||||
end
|
||||
end
|
||||
|
||||
-- draw line's text
|
||||
return self:draw_line_text(line, x, y)
|
||||
self:draw_line_text(idx, x, y)
|
||||
end
|
||||
|
||||
|
||||
function DocView:draw_line_gutter(line, x, y, width)
|
||||
function DocView:draw_line_gutter(idx, x, y, width)
|
||||
local color = style.line_number
|
||||
for _, line1, _, line2 in self.doc:get_selections(true) do
|
||||
if line >= line1 and line <= line2 then
|
||||
if idx >= line1 and idx <= line2 then
|
||||
color = style.line_number2
|
||||
break
|
||||
end
|
||||
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
|
||||
end
|
||||
|
||||
|
||||
function DocView:draw_ime_decoration(line1, col1, line2, col2)
|
||||
local x, y = self:get_line_screen_position(line1)
|
||||
local line_size = math.max(1, SCALE)
|
||||
local lh = self:get_line_height()
|
||||
|
||||
-- Draw IME underline
|
||||
local x1 = self:get_col_x_offset(line1, col1)
|
||||
local x2 = self:get_col_x_offset(line2, col2)
|
||||
renderer.draw_rect(x + math.min(x1, x2), y + lh - line_size, math.abs(x1 - x2), line_size, style.text)
|
||||
|
||||
-- Draw IME selection
|
||||
local col = math.min(col1, col2)
|
||||
local from = col + self.ime_selection.from
|
||||
local to = from + self.ime_selection.size
|
||||
x1 = self:get_col_x_offset(line1, from)
|
||||
if from ~= to then
|
||||
x2 = self:get_col_x_offset(line1, to)
|
||||
line_size = style.caret_width
|
||||
renderer.draw_rect(x + math.min(x1, x2), y + lh - line_size, math.abs(x1 - x2), line_size, style.caret)
|
||||
end
|
||||
self:draw_caret(x + x1, y)
|
||||
common.draw_text(self:get_font(), color, idx, "right", x, y + yoffset, width, self:get_line_height())
|
||||
end
|
||||
|
||||
|
||||
|
@ -546,17 +401,12 @@ function DocView:draw_overlay()
|
|||
local minline, maxline = self:get_visible_line_range()
|
||||
-- draw caret if it overlaps this line
|
||||
local T = config.blink_period
|
||||
for _, line1, col1, line2, col2 in self.doc:get_selections() do
|
||||
if line1 >= minline and line1 <= maxline
|
||||
for _, line, col in self.doc:get_selections() do
|
||||
if line >= minline and line <= maxline
|
||||
and (core.blink_timer - core.blink_start) % T < T / 2
|
||||
and system.window_has_focus() then
|
||||
if ime.editing then
|
||||
self:draw_ime_decoration(line1, col1, line2, col2)
|
||||
else
|
||||
if config.disable_blink
|
||||
or (core.blink_timer - core.blink_start) % T < T / 2 then
|
||||
self:draw_caret(self:get_line_screen_position(line1, col1))
|
||||
end
|
||||
end
|
||||
local x, y = self:get_line_screen_position(line)
|
||||
self:draw_caret(x + self:get_col_x_offset(line, col), y)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -564,8 +414,8 @@ 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)
|
||||
|
||||
self:get_font():set_tab_size(config.indent_size)
|
||||
|
||||
local minline, maxline = self:get_visible_line_range()
|
||||
local lh = self:get_line_height()
|
||||
|
@ -573,16 +423,16 @@ function DocView:draw()
|
|||
local x, y = self:get_line_screen_position(minline)
|
||||
local gw, gpad = self:get_gutter_width()
|
||||
for i = minline, maxline do
|
||||
y = y + (self:draw_line_gutter(i, self.position.x, y, gpad and gw - gpad or gw) or lh)
|
||||
self:draw_line_gutter(i, self.position.x, y, gpad and gw - gpad or gw)
|
||||
y = y + lh
|
||||
end
|
||||
|
||||
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()
|
||||
|
|
|
@ -1,52 +0,0 @@
|
|||
local style = require "core.style"
|
||||
local keymap = require "core.keymap"
|
||||
local View = require "core.view"
|
||||
|
||||
---@class core.emptyview : core.view
|
||||
---@field super core.view
|
||||
local EmptyView = View:extend()
|
||||
|
||||
local function draw_text(x, y, color)
|
||||
local lines = {
|
||||
{ fmt = "%s to run a command", cmd = "core:find-command" },
|
||||
{ fmt = "%s to open a file from the project", cmd = "core:find-file" },
|
||||
{ fmt = "%s to change project folder", cmd = "core:change-project-folder" },
|
||||
{ fmt = "%s to open a project folder", cmd = "core:open-project-folder" },
|
||||
}
|
||||
local th = style.big_font:get_height()
|
||||
local dh = 2 * th + style.padding.y * 2
|
||||
local x1, y1 = x, y + ((dh - th) / #lines)
|
||||
local xv = x1
|
||||
local title = "Lite XL"
|
||||
local version = "version " .. VERSION
|
||||
local title_width = style.big_font:get_width(title)
|
||||
local version_width = style.font:get_width(version)
|
||||
if version_width > title_width then
|
||||
version = VERSION
|
||||
version_width = style.font:get_width(version)
|
||||
xv = x1 - (version_width - title_width)
|
||||
end
|
||||
x = renderer.draw_text(style.big_font, title, x1, y1, color)
|
||||
renderer.draw_text(style.font, version, xv, y1 + th, color)
|
||||
x = x + style.padding.x
|
||||
renderer.draw_rect(x, y, math.ceil(1 * SCALE), dh, color)
|
||||
th = style.font:get_height()
|
||||
y = y + (dh - (th + style.padding.y) * #lines) / 2
|
||||
local w = 0
|
||||
for _, line in ipairs(lines) do
|
||||
local text = string.format(line.fmt, keymap.get_binding(line.cmd))
|
||||
w = math.max(w, renderer.draw_text(style.font, text, x + style.padding.x, y, color))
|
||||
y = y + th + style.padding.y
|
||||
end
|
||||
return w, dh
|
||||
end
|
||||
|
||||
function EmptyView:draw()
|
||||
self:draw_background(style.background)
|
||||
local w, h = draw_text(0, 0, { 0, 0, 0, 0 })
|
||||
local x = self.position.x + math.max(style.padding.x, (self.size.x - w) / 2)
|
||||
local y = self.position.y + (self.size.y - h) / 2
|
||||
draw_text(x, y, style.dim)
|
||||
end
|
||||
|
||||
return EmptyView
|
|
@ -1,92 +0,0 @@
|
|||
local core = require "core"
|
||||
|
||||
local ime = { }
|
||||
|
||||
function ime.reset()
|
||||
ime.editing = false
|
||||
ime.last_location = { x = 0, y = 0, w = 0, h = 0 }
|
||||
end
|
||||
|
||||
---Convert from utf-8 offset and length (from SDL) to byte offsets
|
||||
---@param text string @Textediting string
|
||||
---@param start integer @0-based utf-8 offset of the starting position of the selection
|
||||
---@param length integer @Size of the utf-8 length of the selection
|
||||
function ime.ingest(text, start, length)
|
||||
if #text == 0 then
|
||||
-- finished textediting
|
||||
ime.reset()
|
||||
return "", 0, 0
|
||||
end
|
||||
|
||||
ime.editing = true
|
||||
|
||||
if start < 0 then
|
||||
-- we assume no selection and caret at the end
|
||||
return text, #text, 0
|
||||
end
|
||||
|
||||
-- start is 0-based, so we use start + 1
|
||||
local start_byte = utf8.offset(text, start + 1)
|
||||
if not start_byte then
|
||||
-- bad start offset
|
||||
-- we assume it meant the last byte of the text
|
||||
start_byte = #text
|
||||
else
|
||||
start_byte = math.min(start_byte - 1, #text)
|
||||
end
|
||||
|
||||
if length < 0 then
|
||||
-- caret only
|
||||
return text, start_byte, 0
|
||||
end
|
||||
|
||||
local end_byte = utf8.offset(text, start + length + 1)
|
||||
if not end_byte or end_byte - 1 < start_byte then
|
||||
-- bad length, assume caret only
|
||||
return text, start_byte, 0
|
||||
end
|
||||
|
||||
end_byte = math.min(end_byte - 1, #text)
|
||||
return text, start_byte, end_byte - start_byte
|
||||
end
|
||||
|
||||
---Forward the given textediting SDL event data to Views.
|
||||
---@param text string @Textediting string
|
||||
---@param start integer @0-based utf-8 offset of the starting position of the selection
|
||||
---@param length integer @Size of the utf-8 length of the selection
|
||||
function ime.on_text_editing(text, start, length, ...)
|
||||
if ime.editing or #text > 0 then
|
||||
core.root_view:on_ime_text_editing(ime.ingest(text, start, length, ...))
|
||||
end
|
||||
end
|
||||
|
||||
---Stop IME composition.
|
||||
---Might not completely work on every platform.
|
||||
function ime.stop()
|
||||
if ime.editing then
|
||||
-- SDL_ClearComposition for now doesn't work everywhere
|
||||
system.clear_ime()
|
||||
ime.on_text_editing("", 0, 0)
|
||||
end
|
||||
end
|
||||
|
||||
---Set the bounding box of the text pertaining the IME.
|
||||
---The IME will draw its interface based on this info.
|
||||
---@param x number
|
||||
---@param y number
|
||||
---@param w number
|
||||
---@param h number
|
||||
function ime.set_location(x, y, w, h)
|
||||
if not ime.last_location or
|
||||
ime.last_location.x ~= x or
|
||||
ime.last_location.y ~= y or
|
||||
ime.last_location.w ~= w or
|
||||
ime.last_location.h ~= h
|
||||
then
|
||||
ime.last_location.x, ime.last_location.y, ime.last_location.w, ime.last_location.h = x, y, w, h
|
||||
system.set_text_input_rect(x, y, w, h)
|
||||
end
|
||||
end
|
||||
|
||||
ime.reset()
|
||||
return ime
|
1128
data/core/init.lua
1128
data/core/init.lua
File diff suppressed because it is too large
Load Diff
|
@ -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+option+r"] = "core:restart",
|
||||
["cmd+ctrl+return"] = "core:toggle-fullscreen",
|
||||
|
||||
["cmd+ctrl+shift+j"] = "root:split-left",
|
||||
|
@ -33,10 +32,6 @@ 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",
|
||||
["hwheel"] = "root:horizontal-scroll",
|
||||
["shift+hwheel"] = "root:horizontal-scroll",
|
||||
|
||||
["cmd+f"] = "find-replace:find",
|
||||
["cmd+r"] = "find-replace:replace",
|
||||
["f3"] = "find-replace:repeat-find",
|
||||
|
@ -57,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",
|
||||
|
||||
|
@ -85,42 +76,33 @@ 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"
|
||||
["cmd+shift+up"] = "doc:create-cursor-previous-line",
|
||||
["cmd+shift+down"] = "doc:create-cursor-next-line"
|
||||
}
|
||||
end
|
||||
|
||||
|
|
|
@ -1,165 +1,49 @@
|
|||
local core = require "core"
|
||||
local command = require "core.command"
|
||||
local config = require "core.config"
|
||||
local ime = require "core.ime"
|
||||
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")
|
||||
|
||||
-- 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 "generic"))
|
||||
local modkey_map = modkeys_os.map
|
||||
|
||||
---@type keymap.modkey[]
|
||||
local modkeys = modkeys_os.keys
|
||||
|
||||
|
||||
---Normalizes a stroke sequence to follow the modkeys table
|
||||
---@param stroke string
|
||||
---@return string
|
||||
local function normalize_stroke(stroke)
|
||||
local stroke_table = {}
|
||||
for key in stroke:gmatch("[^+]+") do
|
||||
table.insert(stroke_table, key)
|
||||
end
|
||||
table.sort(stroke_table, function(a, b)
|
||||
if a == b then return false end
|
||||
for _, mod in ipairs(modkeys) do
|
||||
if a == mod or b == mod then
|
||||
return a == mod
|
||||
end
|
||||
end
|
||||
return a < b
|
||||
end)
|
||||
return table.concat(stroke_table, "+")
|
||||
end
|
||||
|
||||
|
||||
---Generates a stroke sequence including currently pressed mod keys.
|
||||
---@param key string
|
||||
---@return string
|
||||
local function key_to_stroke(key)
|
||||
local keys = { key }
|
||||
local function key_to_stroke(k)
|
||||
local stroke = ""
|
||||
for _, mk in ipairs(modkeys) do
|
||||
if keymap.modkeys[mk] then
|
||||
table.insert(keys, mk)
|
||||
stroke = stroke .. mk .. "+"
|
||||
end
|
||||
end
|
||||
return normalize_stroke(table.concat(keys, "+"))
|
||||
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
|
||||
local normalized_stroke = normalize_stroke(stroke)
|
||||
if type(commands) == "string" or type(commands) == "function" then
|
||||
commands = { commands }
|
||||
end
|
||||
if keymap.map[normalized_stroke] then
|
||||
for _, registered_cmd in ipairs(keymap.map[normalized_stroke]) do
|
||||
local j = 0
|
||||
for i=1, #commands do
|
||||
while commands[i + j] == registered_cmd do
|
||||
j = j + 1
|
||||
end
|
||||
commands[i] = commands[i + j]
|
||||
end
|
||||
end
|
||||
end
|
||||
if #commands < 1 then
|
||||
map[stroke] = nil
|
||||
else
|
||||
map[stroke] = commands
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---Add bindings by replacing commands that were previously assigned to a shortcut.
|
||||
---@param map keymap.map
|
||||
function keymap.add_direct(map)
|
||||
for stroke, commands in pairs(map) do
|
||||
stroke = normalize_stroke(stroke)
|
||||
|
||||
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
|
||||
stroke = normalize_stroke(stroke)
|
||||
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 {}
|
||||
|
@ -168,43 +52,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)
|
||||
shortcut = normalize_stroke(shortcut)
|
||||
remove_only(keymap.map, shortcut, cmd)
|
||||
remove_only(keymap.reverse_map, cmd, shortcut)
|
||||
end
|
||||
|
||||
|
||||
---Returns all the shortcuts associated to a command unpacked for easy assignment.
|
||||
---@param cmd string
|
||||
---@return ...
|
||||
function keymap.get_binding(cmd)
|
||||
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
|
||||
|
@ -214,62 +73,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_y, delta_x, ...)
|
||||
local y_direction = delta_y > 0 and "up" or "down"
|
||||
local x_direction = delta_x > 0 and "left" or "right"
|
||||
-- Try sending a "cumulative" event for both scroll directions
|
||||
if delta_y ~= 0 and delta_x ~= 0 then
|
||||
local result = keymap.on_key_pressed("wheel" .. y_direction .. x_direction, delta_y, delta_x, ...)
|
||||
if not result then
|
||||
result = keymap.on_key_pressed("wheelyx", delta_y, delta_x, ...)
|
||||
end
|
||||
if result then return true end
|
||||
end
|
||||
-- Otherwise send each direction as its own separate event
|
||||
local y_result, x_result
|
||||
if delta_y ~= 0 then
|
||||
y_result = keymap.on_key_pressed("wheel" .. y_direction, delta_y, ...)
|
||||
if not y_result then
|
||||
y_result = keymap.on_key_pressed("wheel", delta_y, ...)
|
||||
end
|
||||
end
|
||||
if delta_x ~= 0 then
|
||||
x_result = keymap.on_key_pressed("wheel" .. x_direction, delta_x, ...)
|
||||
if not x_result then
|
||||
x_result = keymap.on_key_pressed("hwheel", delta_x, ...)
|
||||
end
|
||||
end
|
||||
return y_result or x_result
|
||||
end
|
||||
|
||||
function keymap.on_mouse_pressed(button, x, y, clicks)
|
||||
local click_number = (((clicks - 1) % config.max_clicks) + 1)
|
||||
return not (keymap.on_key_pressed(click_number .. button:sub(1,1) .. "click", x, y, clicks) or
|
||||
keymap.on_key_pressed(button:sub(1,1) .. "click", x, y, clicks) or
|
||||
keymap.on_key_pressed(click_number .. "click", x, y, clicks) or
|
||||
keymap.on_key_pressed("click", x, y, clicks))
|
||||
end
|
||||
|
||||
function keymap.on_key_released(k)
|
||||
local mk = modkey_map[k]
|
||||
|
@ -279,9 +94,6 @@ function keymap.on_key_released(k)
|
|||
end
|
||||
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Register default bindings
|
||||
--------------------------------------------------------------------------------
|
||||
if macos then
|
||||
local keymap_macos = require("core.keymap-macos")
|
||||
keymap_macos(keymap)
|
||||
|
@ -295,7 +107,6 @@ 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",
|
||||
|
||||
|
@ -322,9 +133,6 @@ 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",
|
||||
["hwheel"] = "root:horizontal-scroll",
|
||||
["shift+wheel"] = "root:horizontal-scroll",
|
||||
|
||||
["ctrl+f"] = "find-replace:find",
|
||||
["ctrl+r"] = "find-replace:replace",
|
||||
|
@ -360,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",
|
||||
|
@ -380,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",
|
||||
|
@ -400,14 +199,16 @@ 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"
|
||||
["ctrl+shift+down"] = "doc:create-cursor-next-line",
|
||||
|
||||
["menu"] = "context-menu:show"
|
||||
}
|
||||
|
||||
return keymap
|
||||
|
|
|
@ -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,100 +19,15 @@ 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
|
||||
local lh = style.font:get_height() + style.padding.y
|
||||
if 0 < self.scroll.to.y then
|
||||
local index = #core.log_items
|
||||
while index > 1 and self.last_item ~= core.log_items[index] do
|
||||
index = index - 1
|
||||
end
|
||||
local diff_index = #core.log_items - index
|
||||
self.scroll.to.y = self.scroll.to.y + diff_index * lh
|
||||
self.scroll.y = self.scroll.to.y
|
||||
else
|
||||
self.yoffset = -lh
|
||||
end
|
||||
self.last_item = item
|
||||
self.scroll.to.y = 0
|
||||
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
|
||||
|
@ -156,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
|
||||
|
||||
|
||||
|
|
|
@ -9,6 +9,6 @@ modkeys.map = {
|
|||
["right alt"] = "altgr",
|
||||
}
|
||||
|
||||
modkeys.keys = { "ctrl", "shift", "alt", "altgr" }
|
||||
modkeys.keys = { "ctrl", "alt", "altgr", "shift" }
|
||||
|
||||
return modkeys
|
||||
|
|
|
@ -13,6 +13,6 @@ modkeys.map = {
|
|||
["right alt"] = "altgr",
|
||||
}
|
||||
|
||||
modkeys.keys = { "ctrl", "alt", "option", "altgr", "shift", "cmd" }
|
||||
modkeys.keys = { "cmd", "ctrl", "alt", "option", "altgr", "shift" }
|
||||
|
||||
return modkeys
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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,11 +81,11 @@ 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]
|
||||
bw = style.font:get_width(opt.text) + 2 * BORDER_WIDTH + style.padding.x
|
||||
bw = opt.font:get_width(opt.text) + 2 * BORDER_WIDTH + style.padding.x
|
||||
|
||||
ox = ox - bw - style.padding.x
|
||||
coroutine.yield(i, opt, ox,oy,bw,bh)
|
||||
|
@ -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
|
||||
|
||||
|
@ -230,29 +166,8 @@ local function draw_nagview_message(self)
|
|||
renderer.draw_rect(lx,ly,uw,UNDERLINE_WIDTH, style.nagbar_text)
|
||||
end
|
||||
|
||||
common.draw_text(style.font, style.nagbar_text, opt.text, "center", fx,fy,fw,fh)
|
||||
common.draw_text(opt.font, style.nagbar_text, opt.text, "center", fx,fy,fw,fh)
|
||||
end
|
||||
|
||||
self:draw_scrollbar()
|
||||
|
||||
core.pop_clip_rect()
|
||||
end
|
||||
|
||||
function NagView:draw()
|
||||
if (not self.visible and self.show_height <= 0) or not self.title then
|
||||
return
|
||||
end
|
||||
core.root_view:defer_draw(draw_nagview_message, self)
|
||||
end
|
||||
|
||||
function NagView:on_scale_change(new_scale, old_scale)
|
||||
BORDER_WIDTH = common.round(1 * new_scale)
|
||||
UNDERLINE_WIDTH = common.round(2 * new_scale)
|
||||
UNDERLINE_MARGIN = common.round(1 * new_scale)
|
||||
self.target_height = math.max(
|
||||
self:get_message_height(),
|
||||
self:get_buttons_height()
|
||||
)
|
||||
end
|
||||
|
||||
function NagView:get_message_height()
|
||||
|
@ -263,31 +178,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)
|
||||
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)
|
||||
|
@ -297,7 +203,7 @@ 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
|
||||
|
|
|
@ -1,763 +0,0 @@
|
|||
local core = require "core"
|
||||
local common = require "core.common"
|
||||
local config = require "core.config"
|
||||
local style = require "core.style"
|
||||
local Object = require "core.object"
|
||||
local EmptyView = require "core.emptyview"
|
||||
local View = require "core.view"
|
||||
|
||||
---@class core.node : core.object
|
||||
local Node = Object:extend()
|
||||
|
||||
function Node:new(type)
|
||||
self.type = type or "leaf"
|
||||
self.position = { x = 0, y = 0 }
|
||||
self.size = { x = 0, y = 0 }
|
||||
self.views = {}
|
||||
self.divider = 0.5
|
||||
if self.type == "leaf" then
|
||||
self:add_view(EmptyView())
|
||||
end
|
||||
self.hovered = {x = -1, y = -1 }
|
||||
self.hovered_close = 0
|
||||
self.tab_shift = 0
|
||||
self.tab_offset = 1
|
||||
self.tab_width = style.tab_width
|
||||
self.move_towards = View.move_towards
|
||||
end
|
||||
|
||||
|
||||
function Node:propagate(fn, ...)
|
||||
self.a[fn](self.a, ...)
|
||||
self.b[fn](self.b, ...)
|
||||
end
|
||||
|
||||
|
||||
function Node:on_mouse_moved(x, y, ...)
|
||||
if self.type == "leaf" then
|
||||
self.hovered.x, self.hovered.y = x, y
|
||||
self.active_view:on_mouse_moved(x, y, ...)
|
||||
else
|
||||
self:propagate("on_mouse_moved", x, y, ...)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function Node:on_mouse_released(...)
|
||||
if self.type == "leaf" then
|
||||
self.active_view:on_mouse_released(...)
|
||||
else
|
||||
self:propagate("on_mouse_released", ...)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function Node:on_mouse_left()
|
||||
if self.type == "leaf" then
|
||||
self.active_view:on_mouse_left()
|
||||
else
|
||||
self:propagate("on_mouse_left")
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function Node:consume(node)
|
||||
for k, _ in pairs(self) do self[k] = nil end
|
||||
for k, v in pairs(node) do self[k] = v end
|
||||
end
|
||||
|
||||
|
||||
local type_map = { up="vsplit", down="vsplit", left="hsplit", right="hsplit" }
|
||||
|
||||
-- The "locked" argument below should be in the form {x = <boolean>, y = <boolean>}
|
||||
-- and it indicates if the node want to have a fixed size along the axis where the
|
||||
-- boolean is true. If not it will be expanded to take all the available space.
|
||||
-- The "resizable" flag indicates if, along the "locked" axis the node can be resized
|
||||
-- by the user. If the node is marked as resizable their view should provide a
|
||||
-- set_target_size method.
|
||||
function Node:split(dir, view, locked, resizable)
|
||||
assert(self.type == "leaf", "Tried to split non-leaf node")
|
||||
local node_type = assert(type_map[dir], "Invalid direction")
|
||||
local last_active = core.active_view
|
||||
local child = Node()
|
||||
child:consume(self)
|
||||
self:consume(Node(node_type))
|
||||
self.a = child
|
||||
self.b = Node()
|
||||
if view then self.b:add_view(view) end
|
||||
if locked then
|
||||
assert(type(locked) == 'table')
|
||||
self.b.locked = locked
|
||||
self.b.resizable = resizable or false
|
||||
core.set_active_view(last_active)
|
||||
end
|
||||
if dir == "up" or dir == "left" then
|
||||
self.a, self.b = self.b, self.a
|
||||
return self.a
|
||||
end
|
||||
return self.b
|
||||
end
|
||||
|
||||
function Node:remove_view(root, view)
|
||||
if #self.views > 1 then
|
||||
local idx = self:get_view_idx(view)
|
||||
if idx < self.tab_offset then
|
||||
self.tab_offset = self.tab_offset - 1
|
||||
end
|
||||
table.remove(self.views, idx)
|
||||
if self.active_view == view then
|
||||
self:set_active_view(self.views[idx] or self.views[#self.views])
|
||||
end
|
||||
else
|
||||
local parent = self:get_parent_node(root)
|
||||
local is_a = (parent.a == self)
|
||||
local other = parent[is_a and "b" or "a"]
|
||||
local locked_size_x, locked_size_y = other:get_locked_size()
|
||||
local locked_size
|
||||
if parent.type == "hsplit" then
|
||||
locked_size = locked_size_x
|
||||
else
|
||||
locked_size = locked_size_y
|
||||
end
|
||||
local next_primary
|
||||
if self.is_primary_node then
|
||||
next_primary = core.root_view:select_next_primary_node()
|
||||
end
|
||||
if locked_size or (self.is_primary_node and not next_primary) then
|
||||
self.views = {}
|
||||
self:add_view(EmptyView())
|
||||
else
|
||||
if other == next_primary then
|
||||
next_primary = parent
|
||||
end
|
||||
parent:consume(other)
|
||||
local p = parent
|
||||
while p.type ~= "leaf" do
|
||||
p = p[is_a and "a" or "b"]
|
||||
end
|
||||
p:set_active_view(p.active_view)
|
||||
if self.is_primary_node then
|
||||
next_primary.is_primary_node = true
|
||||
end
|
||||
end
|
||||
end
|
||||
core.last_active_view = nil
|
||||
end
|
||||
|
||||
function Node:close_view(root, view)
|
||||
local do_close = function()
|
||||
self:remove_view(root, view)
|
||||
end
|
||||
view:try_close(do_close)
|
||||
end
|
||||
|
||||
|
||||
function Node:close_active_view(root)
|
||||
self:close_view(root, self.active_view)
|
||||
end
|
||||
|
||||
|
||||
function Node:add_view(view, idx)
|
||||
assert(self.type == "leaf", "Tried to add view to non-leaf node")
|
||||
assert(not self.locked, "Tried to add view to locked node")
|
||||
if self.views[1] and self.views[1]:is(EmptyView) then
|
||||
table.remove(self.views)
|
||||
end
|
||||
table.insert(self.views, idx or (#self.views + 1), view)
|
||||
self:set_active_view(view)
|
||||
end
|
||||
|
||||
|
||||
function Node:set_active_view(view)
|
||||
assert(self.type == "leaf", "Tried to set active view on non-leaf node")
|
||||
local last_active_view = self.active_view
|
||||
self.active_view = view
|
||||
core.set_active_view(view)
|
||||
if last_active_view and last_active_view ~= view then
|
||||
last_active_view:on_mouse_left()
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function Node:get_view_idx(view)
|
||||
for i, v in ipairs(self.views) do
|
||||
if v == view then return i end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function Node:get_node_for_view(view)
|
||||
for _, v in ipairs(self.views) do
|
||||
if v == view then return self end
|
||||
end
|
||||
if self.type ~= "leaf" then
|
||||
return self.a:get_node_for_view(view) or self.b:get_node_for_view(view)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function Node:get_parent_node(root)
|
||||
if root.a == self or root.b == self then
|
||||
return root
|
||||
elseif root.type ~= "leaf" then
|
||||
return self:get_parent_node(root.a) or self:get_parent_node(root.b)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function Node:get_children(t)
|
||||
t = t or {}
|
||||
for _, view in ipairs(self.views) do
|
||||
table.insert(t, view)
|
||||
end
|
||||
if self.a then self.a:get_children(t) end
|
||||
if self.b then self.b:get_children(t) end
|
||||
return t
|
||||
end
|
||||
|
||||
|
||||
-- return the width including the padding space and separately
|
||||
-- the padding space itself
|
||||
local function get_scroll_button_width()
|
||||
local w = style.icon_font:get_width(">")
|
||||
local pad = w
|
||||
return w + 2 * pad, pad
|
||||
end
|
||||
|
||||
|
||||
function Node:get_divider_overlapping_point(px, py)
|
||||
if self.type ~= "leaf" then
|
||||
local axis = self.type == "hsplit" and "x" or "y"
|
||||
if self.a:is_resizable(axis) and self.b:is_resizable(axis) then
|
||||
local p = 6
|
||||
local x, y, w, h = self:get_divider_rect()
|
||||
x, y = x - p, y - p
|
||||
w, h = w + p * 2, h + p * 2
|
||||
if px > x and py > y and px < x + w and py < y + h then
|
||||
return self
|
||||
end
|
||||
end
|
||||
return self.a:get_divider_overlapping_point(px, py)
|
||||
or self.b:get_divider_overlapping_point(px, py)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function Node:get_visible_tabs_number()
|
||||
return math.min(#self.views - self.tab_offset + 1, config.max_tabs)
|
||||
end
|
||||
|
||||
|
||||
function Node:get_tab_overlapping_point(px, py)
|
||||
if not self:should_show_tabs() then return nil end
|
||||
local tabs_number = self:get_visible_tabs_number()
|
||||
local x1, y1, w, h = self:get_tab_rect(self.tab_offset)
|
||||
local x2, y2 = self:get_tab_rect(self.tab_offset + tabs_number)
|
||||
if px >= x1 and py >= y1 and px < x2 and py < y1 + h then
|
||||
return math.floor((px - x1) / w) + self.tab_offset
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function Node:should_show_tabs()
|
||||
if self.locked then return false end
|
||||
local dn = core.root_view.dragged_node
|
||||
if #self.views > 1
|
||||
or (dn and dn.dragging) then -- show tabs while dragging
|
||||
return true
|
||||
elseif config.always_show_tabs then
|
||||
return not self.views[1]:is(EmptyView)
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
|
||||
local function close_button_location(x, w)
|
||||
local cw = style.icon_font:get_width("C")
|
||||
local pad = style.padding.x / 2
|
||||
return x + w - cw - pad, cw, pad
|
||||
end
|
||||
|
||||
|
||||
function Node:get_scroll_button_index(px, py)
|
||||
if #self.views == 1 then return end
|
||||
for i = 1, 2 do
|
||||
local x, y, w, h = self:get_scroll_button_rect(i)
|
||||
if px >= x and px < x + w and py >= y and py < y + h then
|
||||
return i
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function Node:tab_hovered_update(px, py)
|
||||
local tab_index = self:get_tab_overlapping_point(px, py)
|
||||
self.hovered_tab = tab_index
|
||||
self.hovered_close = 0
|
||||
self.hovered_scroll_button = 0
|
||||
if tab_index then
|
||||
local x, y, w, h = self:get_tab_rect(tab_index)
|
||||
local cx, cw = close_button_location(x, w)
|
||||
if px >= cx and px < cx + cw and py >= y and py < y + h and config.tab_close_button then
|
||||
self.hovered_close = tab_index
|
||||
end
|
||||
else
|
||||
self.hovered_scroll_button = self:get_scroll_button_index(px, py) or 0
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function Node:get_child_overlapping_point(x, y)
|
||||
local child
|
||||
if self.type == "leaf" then
|
||||
return self
|
||||
elseif self.type == "hsplit" then
|
||||
child = (x < self.b.position.x) and self.a or self.b
|
||||
elseif self.type == "vsplit" then
|
||||
child = (y < self.b.position.y) and self.a or self.b
|
||||
end
|
||||
return child:get_child_overlapping_point(x, y)
|
||||
end
|
||||
|
||||
|
||||
function Node:get_scroll_button_rect(index)
|
||||
local w, pad = get_scroll_button_width()
|
||||
local h = style.font:get_height() + style.padding.y * 2
|
||||
local x = self.position.x + (index == 1 and self.size.x - w * 2 or self.size.x - w)
|
||||
return x, self.position.y, w, h, pad
|
||||
end
|
||||
|
||||
|
||||
function Node:get_tab_rect(idx)
|
||||
local maxw = self.size.x
|
||||
local x0 = self.position.x
|
||||
local x1 = x0 + common.clamp(self.tab_width * (idx - 1) - self.tab_shift, 0, maxw)
|
||||
local x2 = x0 + common.clamp(self.tab_width * idx - self.tab_shift, 0, maxw)
|
||||
local h = style.font:get_height() + style.padding.y * 2
|
||||
return x1, self.position.y, x2 - x1, h
|
||||
end
|
||||
|
||||
|
||||
function Node:get_divider_rect()
|
||||
local x, y = self.position.x, self.position.y
|
||||
if self.type == "hsplit" then
|
||||
return x + self.a.size.x, y, style.divider_size, self.size.y
|
||||
elseif self.type == "vsplit" then
|
||||
return x, y + self.a.size.y, self.size.x, style.divider_size
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- Return two values for x and y axis and each of them is either falsy or a number.
|
||||
-- A falsy value indicate no fixed size along the corresponding direction.
|
||||
function Node:get_locked_size()
|
||||
if self.type == "leaf" then
|
||||
if self.locked then
|
||||
local size = self.active_view.size
|
||||
-- The values below should be either a falsy value or a number
|
||||
local sx = (self.locked and self.locked.x) and size.x
|
||||
local sy = (self.locked and self.locked.y) and size.y
|
||||
return sx, sy
|
||||
end
|
||||
else
|
||||
local x1, y1 = self.a:get_locked_size()
|
||||
local x2, y2 = self.b:get_locked_size()
|
||||
-- The values below should be either a falsy value or a number
|
||||
local sx, sy
|
||||
if self.type == 'hsplit' then
|
||||
if x1 and x2 then
|
||||
local dsx = (x1 < 1 or x2 < 1) and 0 or style.divider_size
|
||||
sx = x1 + x2 + dsx
|
||||
end
|
||||
sy = y1 or y2
|
||||
else
|
||||
if y1 and y2 then
|
||||
local dsy = (y1 < 1 or y2 < 1) and 0 or style.divider_size
|
||||
sy = y1 + y2 + dsy
|
||||
end
|
||||
sx = x1 or x2
|
||||
end
|
||||
return sx, sy
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function Node.copy_position_and_size(dst, src)
|
||||
dst.position.x, dst.position.y = src.position.x, src.position.y
|
||||
dst.size.x, dst.size.y = src.size.x, src.size.y
|
||||
end
|
||||
|
||||
|
||||
-- calculating the sizes is the same for hsplits and vsplits, except the x/y
|
||||
-- axis are swapped; this function lets us use the same code for both
|
||||
local function calc_split_sizes(self, x, y, x1, x2, y1, y2)
|
||||
local ds = ((x1 and x1 < 1) or (x2 and x2 < 1)) and 0 or style.divider_size
|
||||
local n = x1 and x1 + ds or (x2 and self.size[x] - x2 or math.floor(self.size[x] * self.divider))
|
||||
self.a.position[x] = self.position[x]
|
||||
self.a.position[y] = self.position[y]
|
||||
self.a.size[x] = n - ds
|
||||
self.a.size[y] = self.size[y]
|
||||
self.b.position[x] = self.position[x] + n
|
||||
self.b.position[y] = self.position[y]
|
||||
self.b.size[x] = self.size[x] - n
|
||||
self.b.size[y] = self.size[y]
|
||||
end
|
||||
|
||||
|
||||
function Node:update_layout()
|
||||
if self.type == "leaf" then
|
||||
local av = self.active_view
|
||||
if self:should_show_tabs() then
|
||||
local _, _, _, th = self:get_tab_rect(1)
|
||||
av.position.x, av.position.y = self.position.x, self.position.y + th
|
||||
av.size.x, av.size.y = self.size.x, self.size.y - th
|
||||
else
|
||||
Node.copy_position_and_size(av, self)
|
||||
end
|
||||
else
|
||||
local x1, y1 = self.a:get_locked_size()
|
||||
local x2, y2 = self.b:get_locked_size()
|
||||
if self.type == "hsplit" then
|
||||
calc_split_sizes(self, "x", "y", x1, x2)
|
||||
elseif self.type == "vsplit" then
|
||||
calc_split_sizes(self, "y", "x", y1, y2)
|
||||
end
|
||||
self.a:update_layout()
|
||||
self.b:update_layout()
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function Node:scroll_tabs_to_visible()
|
||||
local index = self:get_view_idx(self.active_view)
|
||||
if index then
|
||||
local tabs_number = self:get_visible_tabs_number()
|
||||
if self.tab_offset > index then
|
||||
self.tab_offset = index
|
||||
elseif self.tab_offset + tabs_number - 1 < index then
|
||||
self.tab_offset = index - tabs_number + 1
|
||||
elseif tabs_number < config.max_tabs and self.tab_offset > 1 then
|
||||
self.tab_offset = #self.views - config.max_tabs + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function Node:scroll_tabs(dir)
|
||||
local view_index = self:get_view_idx(self.active_view)
|
||||
if dir == 1 then
|
||||
if self.tab_offset > 1 then
|
||||
self.tab_offset = self.tab_offset - 1
|
||||
local last_index = self.tab_offset + self:get_visible_tabs_number() - 1
|
||||
if view_index > last_index then
|
||||
self:set_active_view(self.views[last_index])
|
||||
end
|
||||
end
|
||||
elseif dir == 2 then
|
||||
local tabs_number = self:get_visible_tabs_number()
|
||||
if self.tab_offset + tabs_number - 1 < #self.views then
|
||||
self.tab_offset = self.tab_offset + 1
|
||||
local view_index = self:get_view_idx(self.active_view)
|
||||
if view_index < self.tab_offset then
|
||||
self:set_active_view(self.views[self.tab_offset])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function Node:target_tab_width()
|
||||
local n = self:get_visible_tabs_number()
|
||||
local w = self.size.x
|
||||
if #self.views > n then
|
||||
w = self.size.x - get_scroll_button_width() * 2
|
||||
end
|
||||
return common.clamp(style.tab_width, w / config.max_tabs, w / n)
|
||||
end
|
||||
|
||||
|
||||
function Node:update()
|
||||
if self.type == "leaf" then
|
||||
self:scroll_tabs_to_visible()
|
||||
for _, view in ipairs(self.views) do
|
||||
view:update()
|
||||
end
|
||||
self:tab_hovered_update(self.hovered.x, self.hovered.y)
|
||||
local tab_width = self:target_tab_width()
|
||||
self:move_towards("tab_shift", tab_width * (self.tab_offset - 1), nil, "tabs")
|
||||
self:move_towards("tab_width", tab_width, nil, "tabs")
|
||||
else
|
||||
self.a:update()
|
||||
self.b:update()
|
||||
end
|
||||
end
|
||||
|
||||
function Node:draw_tab_title(view, font, is_active, is_hovered, x, y, w, h)
|
||||
local text = view and view:get_name() or ""
|
||||
local dots_width = font:get_width("…")
|
||||
local align = "center"
|
||||
if font:get_width(text) > w then
|
||||
align = "left"
|
||||
for i = 1, #text do
|
||||
local reduced_text = text:sub(1, #text - i)
|
||||
if font:get_width(reduced_text) + dots_width <= w then
|
||||
text = reduced_text .. "…"
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
local color = style.dim
|
||||
if is_active then color = style.text end
|
||||
if is_hovered then color = style.text end
|
||||
common.draw_text(font, color, text, align, x, y, w, h)
|
||||
end
|
||||
|
||||
function Node:draw_tab_borders(view, is_active, is_hovered, x, y, w, h, standalone)
|
||||
-- Tabs deviders
|
||||
local ds = style.divider_size
|
||||
local color = style.dim
|
||||
local padding_y = style.padding.y
|
||||
renderer.draw_rect(x + w, y + padding_y, ds, h - padding_y*2, style.dim)
|
||||
if standalone then
|
||||
renderer.draw_rect(x-1, y-1, w+2, h+2, style.background2)
|
||||
end
|
||||
-- Full border
|
||||
if is_active then
|
||||
color = style.text
|
||||
renderer.draw_rect(x, y, w, h, style.background)
|
||||
renderer.draw_rect(x + w, y, ds, h, style.divider)
|
||||
renderer.draw_rect(x - ds, y, ds, h, style.divider)
|
||||
end
|
||||
return x + ds, y, w - ds*2, h
|
||||
end
|
||||
|
||||
function Node:draw_tab(view, is_active, is_hovered, is_close_hovered, x, y, w, h, standalone)
|
||||
x, y, w, h = self:draw_tab_borders(view, is_active, is_hovered, x, y, w, h, standalone)
|
||||
-- Close button
|
||||
local cx, cw, cpad = close_button_location(x, w)
|
||||
local show_close_button = ((is_active or is_hovered) and not standalone and config.tab_close_button)
|
||||
if show_close_button then
|
||||
local close_style = is_close_hovered and style.text or style.dim
|
||||
common.draw_text(style.icon_font, close_style, "C", nil, cx, y, cw, h)
|
||||
end
|
||||
-- Title
|
||||
x = x + cpad
|
||||
w = cx - x
|
||||
core.push_clip_rect(x, y, w, h)
|
||||
self:draw_tab_title(view, style.font, is_active, is_hovered, x, y, w, h)
|
||||
core.pop_clip_rect()
|
||||
end
|
||||
|
||||
function Node:draw_tabs()
|
||||
local _, y, w, h, scroll_padding = self:get_scroll_button_rect(1)
|
||||
local x = self.position.x
|
||||
local ds = style.divider_size
|
||||
local dots_width = style.font:get_width("…")
|
||||
core.push_clip_rect(x, y, self.size.x, h)
|
||||
renderer.draw_rect(x, y, self.size.x, h, style.background2)
|
||||
renderer.draw_rect(x, y + h - ds, self.size.x, ds, style.divider)
|
||||
local tabs_number = self:get_visible_tabs_number()
|
||||
|
||||
for i = self.tab_offset, self.tab_offset + tabs_number - 1 do
|
||||
local view = self.views[i]
|
||||
local x, y, w, h = self:get_tab_rect(i)
|
||||
self:draw_tab(view, view == self.active_view,
|
||||
i == self.hovered_tab, i == self.hovered_close,
|
||||
x, y, w, h)
|
||||
end
|
||||
|
||||
if #self.views > tabs_number then
|
||||
local _, pad = get_scroll_button_width()
|
||||
local xrb, yrb, wrb, hrb = self:get_scroll_button_rect(1)
|
||||
renderer.draw_rect(xrb + pad, yrb, wrb * 2, hrb, style.background2)
|
||||
local left_button_style = (self.hovered_scroll_button == 1 and self.tab_offset > 1) and style.text or style.dim
|
||||
common.draw_text(style.icon_font, left_button_style, "<", nil, xrb + scroll_padding, yrb, 0, h)
|
||||
|
||||
xrb, yrb, wrb = self:get_scroll_button_rect(2)
|
||||
local right_button_style = (self.hovered_scroll_button == 2 and #self.views > self.tab_offset + tabs_number - 1) and style.text or style.dim
|
||||
common.draw_text(style.icon_font, right_button_style, ">", nil, xrb + scroll_padding, yrb, 0, h)
|
||||
end
|
||||
|
||||
core.pop_clip_rect()
|
||||
end
|
||||
|
||||
|
||||
function Node:draw()
|
||||
if self.type == "leaf" then
|
||||
if self:should_show_tabs() then
|
||||
self:draw_tabs()
|
||||
end
|
||||
local pos, size = self.active_view.position, self.active_view.size
|
||||
core.push_clip_rect(pos.x, pos.y, size.x, size.y)
|
||||
self.active_view:draw()
|
||||
core.pop_clip_rect()
|
||||
else
|
||||
local x, y, w, h = self:get_divider_rect()
|
||||
renderer.draw_rect(x, y, w, h, style.divider)
|
||||
self:propagate("draw")
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function Node:is_empty()
|
||||
if self.type == "leaf" then
|
||||
return #self.views == 0 or (#self.views == 1 and self.views[1]:is(EmptyView))
|
||||
else
|
||||
return self.a:is_empty() and self.b:is_empty()
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function Node:close_all_docviews(keep_active)
|
||||
local node_active_view = self.active_view
|
||||
local lost_active_view = false
|
||||
if self.type == "leaf" then
|
||||
local i = 1
|
||||
while i <= #self.views do
|
||||
local view = self.views[i]
|
||||
if view.context == "session" and (not keep_active or view ~= self.active_view) then
|
||||
table.remove(self.views, i)
|
||||
if view == node_active_view then
|
||||
lost_active_view = true
|
||||
end
|
||||
else
|
||||
i = i + 1
|
||||
end
|
||||
end
|
||||
self.tab_offset = 1
|
||||
if #self.views == 0 and self.is_primary_node then
|
||||
-- if we are not the primary view and we had the active view it doesn't
|
||||
-- matter to reattribute the active view because, within the close_all_docviews
|
||||
-- top call, the primary node will take the active view anyway.
|
||||
-- Set the empty view and takes the active view.
|
||||
self:add_view(EmptyView())
|
||||
elseif #self.views > 0 and lost_active_view then
|
||||
-- In practice we never get there but if a view remain we need
|
||||
-- to reset the Node's active view.
|
||||
self:set_active_view(self.views[1])
|
||||
end
|
||||
else
|
||||
self.a:close_all_docviews(keep_active)
|
||||
self.b:close_all_docviews(keep_active)
|
||||
if self.a:is_empty() and not self.a.is_primary_node then
|
||||
self:consume(self.b)
|
||||
elseif self.b:is_empty() and not self.b.is_primary_node then
|
||||
self:consume(self.a)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Returns true for nodes that accept either "proportional" resizes (based on the
|
||||
-- node.divider) or "locked" resizable nodes (along the resize axis).
|
||||
function Node:is_resizable(axis)
|
||||
if self.type == 'leaf' then
|
||||
return not self.locked or not self.locked[axis] or self.resizable
|
||||
else
|
||||
local a_resizable = self.a:is_resizable(axis)
|
||||
local b_resizable = self.b:is_resizable(axis)
|
||||
return a_resizable and b_resizable
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- Return true iff it is a locked pane along the rezise axis and is
|
||||
-- declared "resizable".
|
||||
function Node:is_locked_resizable(axis)
|
||||
return self.locked and self.locked[axis] and self.resizable
|
||||
end
|
||||
|
||||
|
||||
function Node:resize(axis, value)
|
||||
-- the application works fine with non-integer values but to have pixel-perfect
|
||||
-- placements of view elements, like the scrollbar, we round the value to be
|
||||
-- an integer.
|
||||
value = math.floor(value)
|
||||
if self.type == 'leaf' then
|
||||
-- If it is not locked we don't accept the
|
||||
-- resize operation here because for proportional panes the resize is
|
||||
-- done using the "divider" value of the parent node.
|
||||
if self:is_locked_resizable(axis) then
|
||||
return self.active_view:set_target_size(axis, value)
|
||||
end
|
||||
else
|
||||
if self.type == (axis == "x" and "hsplit" or "vsplit") then
|
||||
-- we are resizing a node that is splitted along the resize axis
|
||||
if self.a:is_locked_resizable(axis) and self.b:is_locked_resizable(axis) then
|
||||
local rem_value = value - self.a.size[axis]
|
||||
if rem_value >= 0 then
|
||||
return self.b.active_view:set_target_size(axis, rem_value)
|
||||
else
|
||||
self.b.active_view:set_target_size(axis, 0)
|
||||
return self.a.active_view:set_target_size(axis, value)
|
||||
end
|
||||
end
|
||||
else
|
||||
-- we are resizing a node that is splitted along the axis perpendicular
|
||||
-- to the resize axis
|
||||
local a_resizable = self.a:is_resizable(axis)
|
||||
local b_resizable = self.b:is_resizable(axis)
|
||||
if a_resizable and b_resizable then
|
||||
self.a:resize(axis, value)
|
||||
self.b:resize(axis, value)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function Node:get_split_type(mouse_x, mouse_y)
|
||||
local x, y = self.position.x, self.position.y
|
||||
local w, h = self.size.x, self.size.y
|
||||
local _, _, _, tab_h = self:get_scroll_button_rect(1)
|
||||
y = y + tab_h
|
||||
h = h - tab_h
|
||||
|
||||
local local_mouse_x = mouse_x - x
|
||||
local local_mouse_y = mouse_y - y
|
||||
|
||||
if local_mouse_y < 0 then
|
||||
return "tab"
|
||||
else
|
||||
local left_pct = local_mouse_x * 100 / w
|
||||
local top_pct = local_mouse_y * 100 / h
|
||||
if left_pct <= 30 then
|
||||
return "left"
|
||||
elseif left_pct >= 70 then
|
||||
return "right"
|
||||
elseif top_pct <= 30 then
|
||||
return "up"
|
||||
elseif top_pct >= 70 then
|
||||
return "down"
|
||||
end
|
||||
return "middle"
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function Node:get_drag_overlay_tab_position(x, y, dragged_node, dragged_index)
|
||||
local tab_index = self:get_tab_overlapping_point(x, y)
|
||||
if not tab_index then
|
||||
local first_tab_x = self:get_tab_rect(1)
|
||||
if x < first_tab_x then
|
||||
-- mouse before first visible tab
|
||||
tab_index = self.tab_offset or 1
|
||||
else
|
||||
-- mouse after last visible tab
|
||||
tab_index = self:get_visible_tabs_number() + (self.tab_offset - 1 or 0)
|
||||
end
|
||||
end
|
||||
local tab_x, tab_y, tab_w, tab_h = self:get_tab_rect(tab_index)
|
||||
if x > tab_x + tab_w / 2 and tab_index <= #self.views then
|
||||
-- use next tab
|
||||
tab_x = tab_x + tab_w
|
||||
tab_index = tab_index + 1
|
||||
end
|
||||
if self == dragged_node and dragged_index and tab_index > dragged_index then
|
||||
-- the tab we are moving is counted in tab_index
|
||||
tab_index = tab_index - 1
|
||||
tab_x = tab_x - tab_w
|
||||
end
|
||||
return tab_index, tab_x, tab_y, tab_w, tab_h
|
||||
end
|
||||
|
||||
return Node
|
|
@ -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
|
||||
|
||||
---Metamethod to allow using the object call as a constructor.
|
||||
---@return core.object
|
||||
|
||||
function Object:__call(...)
|
||||
local obj = setmetatable({}, self)
|
||||
obj:new(...)
|
||||
|
|
|
@ -1,82 +1,70 @@
|
|||
|
||||
-- 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
|
||||
|
||||
---Looks for the first match of `pattern` in the string `str`.
|
||||
---If it finds a match, it returns the indices of `str` where this occurrence
|
||||
---starts and ends; otherwise, it returns `nil`.
|
||||
---If the pattern has captures, the captured start and end indexes are returned,
|
||||
---after the two initial ones.
|
||||
---
|
||||
---@param pattern string|table The regex pattern to use, either as a simple string or precompiled.
|
||||
---@param str string The string to search for valid matches.
|
||||
---@param offset? integer The position on the subject to start searching.
|
||||
---@param options? integer A bit field of matching options, eg: regex.NOTBOL | regex.NOTEMPTY
|
||||
---
|
||||
---@return integer? start Offset where the first match was found; `nil` if no match.
|
||||
---@return integer? end Offset where the first match ends; `nil` if no match.
|
||||
---@return integer? ... #Captured matches offsets.
|
||||
regex.find_offsets = function(pattern, str, offset, options)
|
||||
if type(pattern) ~= "table" then
|
||||
pattern = regex.compile(pattern)
|
||||
end
|
||||
local res = { regex.cmatch(pattern, str, offset or 1, options or 0) }
|
||||
-- Reduce every end delimiter by 1
|
||||
for i = 2,#res,2 do
|
||||
res[i] = res[i] - 1
|
||||
end
|
||||
return table.unpack(res)
|
||||
regex.match = function(pattern_string, string, offset, options)
|
||||
local pattern = type(pattern_string) == "table" and
|
||||
pattern_string or regex.compile(pattern_string)
|
||||
return regex.cmatch(pattern, string, offset or 1, options or 0)
|
||||
end
|
||||
|
||||
---Behaves like `string.match`.
|
||||
---Looks for the first match of `pattern` in the string `str`.
|
||||
---If it finds a match, it returns the matched string; otherwise, it returns `nil`.
|
||||
---If the pattern has captures, only the captured strings are returned.
|
||||
---If a capture is empty, its offset is returned instead.
|
||||
---
|
||||
---@param pattern string|table The regex pattern to use, either as a simple string or precompiled.
|
||||
---@param str string The string to search for valid matches.
|
||||
---@param offset? integer The position on the subject to start searching.
|
||||
---@param options? integer A bit field of matching options, eg: regex.NOTBOL | regex.NOTEMPTY
|
||||
---
|
||||
---@return (string|integer)? ... #List of captured matches; the entire match if no matches were specified; if the match is empty, its offset is returned instead.
|
||||
regex.match = function(pattern, str, offset, options)
|
||||
local res = { regex.find(pattern, str, offset, options) }
|
||||
if #res == 0 then return end
|
||||
-- If available, only return captures
|
||||
if #res > 2 then return table.unpack(res, 3) end
|
||||
return string.sub(str, res[1], res[2])
|
||||
-- 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
|
||||
|
||||
---Behaves like `string.find`.
|
||||
---Looks for the first match of `pattern` in the string `str`.
|
||||
---If it finds a match, it returns the indices of `str` where this occurrence
|
||||
---starts and ends; otherwise, it returns `nil`.
|
||||
---If the pattern has captures, the captured strings are returned,
|
||||
---after the two indexes ones.
|
||||
---If a capture is empty, its offset is returned instead.
|
||||
---
|
||||
---@param pattern string|table The regex pattern to use, either as a simple string or precompiled.
|
||||
---@param str string The string to search for valid matches.
|
||||
---@param offset? integer The position on the subject to start searching.
|
||||
---@param options? integer A bit field of matching options, eg: regex.NOTBOL | regex.NOTEMPTY
|
||||
---
|
||||
---@return integer? start Offset where the first match was found; `nil` if no match.
|
||||
---@return integer? end Offset where the first match ends; `nil` if no match.
|
||||
---@return (string|integer)? ... #List of captured matches; if the match is empty, its offset is returned instead.
|
||||
regex.find = function(pattern, str, offset, options)
|
||||
local res = { regex.find_offsets(pattern, str, offset, options) }
|
||||
local out = { }
|
||||
if #res == 0 then return end
|
||||
out[1] = res[1]
|
||||
out[2] = res[2]
|
||||
for i = 3,#res,2 do
|
||||
if res[i] > res[i+1] then
|
||||
-- Like in string.find, if the group has size 0, return the index
|
||||
table.insert(out, res[i])
|
||||
else
|
||||
table.insert(out, string.sub(str, res[i], res[i+1]))
|
||||
-- Moves to the end of the identified character.
|
||||
local function end_character(str, index)
|
||||
local byte = string.byte(str, index + 1)
|
||||
while byte >= 128 and byte < 192 do
|
||||
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
|
||||
end
|
||||
return table.unpack(out)
|
||||
until #indices == 0 or indices[1] == indices[2]
|
||||
return result .. str, matches, replacements
|
||||
end
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,343 +0,0 @@
|
|||
local core = require "core"
|
||||
local common = require "core.common"
|
||||
local config = require "core.config"
|
||||
local style = require "core.style"
|
||||
local Object = require "core.object"
|
||||
|
||||
---Scrollbar
|
||||
---Use Scrollbar:set_size to set the bounding box of the view the scrollbar belongs to.
|
||||
---Use Scrollbar:update to update the scrollbar animations.
|
||||
---Use Scrollbar:draw to draw the scrollbar.
|
||||
---Use Scrollbar:on_mouse_pressed, Scrollbar:on_mouse_released,
|
||||
---Scrollbar:on_mouse_moved and Scrollbar:on_mouse_left to react to mouse movements;
|
||||
---the scrollbar won't update automatically.
|
||||
---Use Scrollbar:set_percent to set the scrollbar location externally.
|
||||
---
|
||||
---To manage all the orientations, the scrollbar changes the coordinates system
|
||||
---accordingly. The "normal" coordinate system adapts the scrollbar coordinates
|
||||
---as if it's always a vertical scrollbar, positioned at the end of the bounding box.
|
||||
---@class core.scrollbar : core.object
|
||||
local Scrollbar = Object:extend()
|
||||
|
||||
---@class ScrollbarOptions
|
||||
---@field direction "v" | "h" @Vertical or Horizontal
|
||||
---@field alignment "s" | "e" @Start or End (left to right, top to bottom)
|
||||
---@field force_status "expanded" | "contracted" | false @Force the scrollbar status
|
||||
---@field expanded_size number? @Override the default value specified by `style.expanded_scrollbar_size`
|
||||
---@field contracted_size number? @Override the default value specified by `style.scrollbar_size`
|
||||
|
||||
---@param options ScrollbarOptions
|
||||
function Scrollbar:new(options)
|
||||
---Position information of the owner
|
||||
self.rect = {
|
||||
x = 0, y = 0, w = 0, h = 0,
|
||||
---Scrollable size
|
||||
scrollable = 0
|
||||
}
|
||||
self.normal_rect = {
|
||||
across = 0,
|
||||
along = 0,
|
||||
across_size = 0,
|
||||
along_size = 0,
|
||||
scrollable = 0
|
||||
}
|
||||
---@type integer @Position in percent [0-1]
|
||||
self.percent = 0
|
||||
---@type boolean @Scrollbar dragging status
|
||||
self.dragging = false
|
||||
---@type integer @Private. Used to offset the start of the drag from the top of the thumb
|
||||
self.drag_start_offset = 0
|
||||
---What is currently being hovered. `thumb` implies` track`
|
||||
self.hovering = { track = false, thumb = false }
|
||||
---@type "v" | "h"@Vertical or Horizontal
|
||||
self.direction = options.direction or "v"
|
||||
---@type "s" | "e" @Start or End (left to right, top to bottom)
|
||||
self.alignment = options.alignment or "e"
|
||||
---@type number @Private. Used to keep track of animations
|
||||
self.expand_percent = 0
|
||||
---@type "expanded" | "contracted" | false @Force the scrollbar status
|
||||
self.force_status = options.force_status
|
||||
self:set_forced_status(options.force_status)
|
||||
---@type number? @Override the default value specified by `style.expanded_scrollbar_size`
|
||||
self.contracted_size = options.contracted_size
|
||||
---@type number? @Override the default value specified by `style.scrollbar_size`
|
||||
self.expanded_size = options.expanded_size
|
||||
end
|
||||
|
||||
|
||||
---Set the status the scrollbar is forced to keep
|
||||
---@param status "expanded" | "contracted" | false @The status to force
|
||||
function Scrollbar:set_forced_status(status)
|
||||
self.force_status = status
|
||||
if self.force_status == "expanded" then
|
||||
self.expand_percent = 1
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function Scrollbar:real_to_normal(x, y, w, h)
|
||||
x, y, w, h = x or 0, y or 0, w or 0, h or 0
|
||||
if self.direction == "v" then
|
||||
if self.alignment == "s" then
|
||||
x = (self.rect.x + self.rect.w) - x - w
|
||||
end
|
||||
return x, y, w, h
|
||||
else
|
||||
if self.alignment == "s" then
|
||||
y = (self.rect.y + self.rect.h) - y - h
|
||||
end
|
||||
return y, x, h, w
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function Scrollbar:normal_to_real(x, y, w, h)
|
||||
x, y, w, h = x or 0, y or 0, w or 0, h or 0
|
||||
if self.direction == "v" then
|
||||
if self.alignment == "s" then
|
||||
x = (self.rect.x + self.rect.w) - x - w
|
||||
end
|
||||
return x, y, w, h
|
||||
else
|
||||
if self.alignment == "s" then
|
||||
x = (self.rect.y + self.rect.h) - x - w
|
||||
end
|
||||
return y, x, h, w
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function Scrollbar:_get_thumb_rect_normal()
|
||||
local nr = self.normal_rect
|
||||
local sz = nr.scrollable
|
||||
if sz == math.huge or sz <= nr.along_size
|
||||
then
|
||||
return 0, 0, 0, 0
|
||||
end
|
||||
local scrollbar_size = self.contracted_size or style.scrollbar_size
|
||||
local expanded_scrollbar_size = self.expanded_size or style.expanded_scrollbar_size
|
||||
local along_size = math.max(20, nr.along_size * nr.along_size / sz)
|
||||
local across_size = scrollbar_size
|
||||
across_size = across_size + (expanded_scrollbar_size - scrollbar_size) * self.expand_percent
|
||||
return
|
||||
nr.across + nr.across_size - across_size,
|
||||
nr.along + self.percent * nr.scrollable * (nr.along_size - along_size) / (sz - nr.along_size),
|
||||
across_size,
|
||||
along_size
|
||||
end
|
||||
|
||||
---Get the thumb rect (the part of the scrollbar that can be dragged)
|
||||
---@return integer,integer,integer,integer @x, y, w, h
|
||||
function Scrollbar:get_thumb_rect()
|
||||
return self:normal_to_real(self:_get_thumb_rect_normal())
|
||||
end
|
||||
|
||||
|
||||
function Scrollbar:_get_track_rect_normal()
|
||||
local nr = self.normal_rect
|
||||
local sz = nr.scrollable
|
||||
if sz <= nr.along_size or sz == math.huge then
|
||||
return 0, 0, 0, 0
|
||||
end
|
||||
local scrollbar_size = self.contracted_size or style.scrollbar_size
|
||||
local expanded_scrollbar_size = self.expanded_size or style.expanded_scrollbar_size
|
||||
local across_size = scrollbar_size
|
||||
across_size = across_size + (expanded_scrollbar_size - scrollbar_size) * self.expand_percent
|
||||
return
|
||||
nr.across + nr.across_size - across_size,
|
||||
nr.along,
|
||||
across_size,
|
||||
nr.along_size
|
||||
end
|
||||
|
||||
---Get the track rect (the "background" of the scrollbar)
|
||||
---@return number,number,number,number @x, y, w, h
|
||||
function Scrollbar:get_track_rect()
|
||||
return self:normal_to_real(self:_get_track_rect_normal())
|
||||
end
|
||||
|
||||
|
||||
function Scrollbar:_overlaps_normal(x, y)
|
||||
local sx, sy, sw, sh = self:_get_thumb_rect_normal()
|
||||
local scrollbar_size = self.contracted_size or style.scrollbar_size
|
||||
local result
|
||||
if x >= sx - scrollbar_size * 3 and x <= sx + sw and y >= sy and y <= sy + sh then
|
||||
result = "thumb"
|
||||
else
|
||||
sx, sy, sw, sh = self:_get_track_rect_normal()
|
||||
if x >= sx - scrollbar_size * 3 and x <= sx + sw and y >= sy and y <= sy + sh then
|
||||
result = "track"
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
---Get what part of the scrollbar the coordinates overlap
|
||||
---@return "thumb"|"track"|nil
|
||||
function Scrollbar:overlaps(x, y)
|
||||
x, y = self:real_to_normal(x, y)
|
||||
return self:_overlaps_normal(x, y)
|
||||
end
|
||||
|
||||
|
||||
function Scrollbar:_on_mouse_pressed_normal(button, x, y, clicks)
|
||||
local overlaps = self:_overlaps_normal(x, y)
|
||||
if overlaps then
|
||||
local _, along, _, along_size = self:_get_thumb_rect_normal()
|
||||
self.dragging = true
|
||||
if overlaps == "thumb" then
|
||||
self.drag_start_offset = along - y
|
||||
return true
|
||||
elseif overlaps == "track" then
|
||||
local nr = self.normal_rect
|
||||
self.drag_start_offset = - along_size / 2
|
||||
return common.clamp((y - nr.along - along_size / 2) / (nr.along_size - along_size), 0, 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---Updates the scrollbar with mouse pressed info.
|
||||
---Won't update the scrollbar position automatically.
|
||||
---Use Scrollbar:set_percent to update it.
|
||||
---
|
||||
---This sets the dragging status if needed.
|
||||
---
|
||||
---Returns a falsy value if the event happened outside the scrollbar.
|
||||
---Returns `true` if the thumb was pressed.
|
||||
---If the track was pressed this returns a value between 0 and 1
|
||||
---representing the percent of the position.
|
||||
---@return boolean|number
|
||||
function Scrollbar:on_mouse_pressed(button, x, y, clicks)
|
||||
if button ~= "left" then return end
|
||||
x, y = self:real_to_normal(x, y)
|
||||
return self:_on_mouse_pressed_normal(button, x, y, clicks)
|
||||
end
|
||||
|
||||
---Updates the scrollbar hover status.
|
||||
---This gets called by other functions and shouldn't be called manually
|
||||
function Scrollbar:_update_hover_status_normal(x, y)
|
||||
local overlaps = self:_overlaps_normal(x, y)
|
||||
self.hovering.thumb = overlaps == "thumb"
|
||||
self.hovering.track = self.hovering.thumb or overlaps == "track"
|
||||
return self.hovering.track or self.hovering.thumb
|
||||
end
|
||||
|
||||
function Scrollbar:_on_mouse_released_normal(button, x, y)
|
||||
self.dragging = false
|
||||
return self:_update_hover_status_normal(x, y)
|
||||
end
|
||||
|
||||
---Updates the scrollbar dragging status
|
||||
function Scrollbar:on_mouse_released(button, x, y)
|
||||
if button ~= "left" then return end
|
||||
x, y = self:real_to_normal(x, y)
|
||||
return self:_on_mouse_released_normal(button, x, y)
|
||||
end
|
||||
|
||||
|
||||
function Scrollbar:_on_mouse_moved_normal(x, y, dx, dy)
|
||||
if self.dragging then
|
||||
local nr = self.normal_rect
|
||||
return common.clamp((y - nr.along + self.drag_start_offset) / nr.along_size, 0, 1)
|
||||
end
|
||||
return self:_update_hover_status_normal(x, y)
|
||||
end
|
||||
|
||||
---Updates the scrollbar with mouse moved info.
|
||||
---Won't update the scrollbar position automatically.
|
||||
---Use Scrollbar:set_percent to update it.
|
||||
---
|
||||
---This updates the hovering status.
|
||||
---
|
||||
---Returns a falsy value if the event happened outside the scrollbar.
|
||||
---Returns `true` if the scrollbar is hovered.
|
||||
---If the scrollbar was being dragged, this returns a value between 0 and 1
|
||||
---representing the percent of the position.
|
||||
---@return boolean|number
|
||||
function Scrollbar:on_mouse_moved(x, y, dx, dy)
|
||||
x, y = self:real_to_normal(x, y)
|
||||
dx, dy = self:real_to_normal(dx, dy) -- TODO: do we need this? (is this even correct?)
|
||||
return self:_on_mouse_moved_normal(x, y, dx, dy)
|
||||
end
|
||||
|
||||
---Updates the scrollbar hovering status
|
||||
function Scrollbar:on_mouse_left()
|
||||
self.hovering.track, self.hovering.thumb = false, false
|
||||
end
|
||||
|
||||
---Updates the bounding box of the view the scrollbar belongs to.
|
||||
---@param x number
|
||||
---@param y number
|
||||
---@param w number
|
||||
---@param h number
|
||||
---@param scrollable number @size of the scrollable area
|
||||
function Scrollbar:set_size(x, y, w, h, scrollable)
|
||||
self.rect.x, self.rect.y, self.rect.w, self.rect.h = x, y, w, h
|
||||
self.rect.scrollable = scrollable
|
||||
|
||||
local nr = self.normal_rect
|
||||
nr.across, nr.along, nr.across_size, nr.along_size = self:real_to_normal(x, y, w, h)
|
||||
nr.scrollable = scrollable
|
||||
end
|
||||
|
||||
---Updates the scrollbar location
|
||||
---@param percent number @number between 0 and 1 representing the position of the middle part of the thumb
|
||||
function Scrollbar:set_percent(percent)
|
||||
self.percent = percent
|
||||
end
|
||||
|
||||
---Updates the scrollbar animations
|
||||
function Scrollbar:update()
|
||||
-- TODO: move the animation code to its own class
|
||||
if not self.force_status then
|
||||
local dest = (self.hovering.track or self.dragging) and 1 or 0
|
||||
local diff = math.abs(self.expand_percent - dest)
|
||||
if not config.transitions or diff < 0.05 or config.disabled_transitions["scroll"] then
|
||||
self.expand_percent = dest
|
||||
else
|
||||
local rate = 0.3
|
||||
if config.fps ~= 60 or config.animation_rate ~= 1 then
|
||||
local dt = 60 / config.fps
|
||||
rate = 1 - common.clamp(1 - rate, 1e-8, 1 - 1e-8)^(config.animation_rate * dt)
|
||||
end
|
||||
self.expand_percent = common.lerp(self.expand_percent, dest, rate)
|
||||
end
|
||||
if diff > 1e-8 then
|
||||
core.redraw = true
|
||||
end
|
||||
elseif self.force_status == "expanded" then
|
||||
self.expand_percent = 1
|
||||
elseif self.force_status == "contracted" then
|
||||
self.expand_percent = 0
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
---Draw the scrollbar track
|
||||
function Scrollbar:draw_track()
|
||||
if not (self.hovering.track or self.dragging)
|
||||
and self.expand_percent == 0 then
|
||||
return
|
||||
end
|
||||
local color = { table.unpack(style.scrollbar_track) }
|
||||
color[4] = color[4] * self.expand_percent
|
||||
local x, y, w, h = self:get_track_rect()
|
||||
renderer.draw_rect(x, y, w, h, color)
|
||||
end
|
||||
|
||||
---Draw the scrollbar thumb
|
||||
function Scrollbar:draw_thumb()
|
||||
local highlight = self.hovering.thumb or self.dragging
|
||||
local color = highlight and style.scrollbar2 or style.scrollbar
|
||||
local x, y, w, h = self:get_thumb_rect()
|
||||
renderer.draw_rect(x, y, w, h, color)
|
||||
end
|
||||
|
||||
---Draw both the scrollbar track and thumb
|
||||
function Scrollbar:draw()
|
||||
self:draw_track()
|
||||
self:draw_thumb()
|
||||
end
|
||||
|
||||
|
||||
return Scrollbar
|
|
@ -1,57 +1,22 @@
|
|||
-- this file is used by lite-xl to setup the Lua environment when starting
|
||||
VERSION = "2.1.4r1"
|
||||
MOD_VERSION = "3"
|
||||
VERSION = "2.0-beta1"
|
||||
MOD_VERSION = "1"
|
||||
|
||||
SCALE = tonumber(os.getenv("LITE_SCALE") or os.getenv("GDK_SCALE") or os.getenv("QT_SCALE_FACTOR")) or 1
|
||||
SCALE = tonumber(os.getenv("LITE_SCALE")) or SCALE
|
||||
PATHSEP = package.config:sub(1, 1)
|
||||
|
||||
EXEDIR = EXEFILE:match("^(.+)[/\\][^/\\]+$")
|
||||
if MACOS_RESOURCES then
|
||||
DATADIR = MACOS_RESOURCES
|
||||
else
|
||||
local prefix = os.getenv('LITE_PREFIX') or EXEDIR:match("^(.+)[/\\]bin$")
|
||||
DATADIR = prefix and (prefix .. PATHSEP .. 'share' .. PATHSEP .. 'lite-xl') or (EXEDIR .. PATHSEP .. 'data')
|
||||
local prefix = EXEDIR:match("^(.+)[/\\]bin$")
|
||||
DATADIR = prefix and (prefix .. '/share/lite-xl') or (EXEDIR .. '/data')
|
||||
end
|
||||
USERDIR = (system.get_file_info(EXEDIR .. PATHSEP .. 'user') and (EXEDIR .. PATHSEP .. 'user'))
|
||||
or os.getenv("LITE_USERDIR")
|
||||
or ((os.getenv("XDG_CONFIG_HOME") and os.getenv("XDG_CONFIG_HOME") .. PATHSEP .. "lite-xl"))
|
||||
or (HOME and (HOME .. PATHSEP .. '.config' .. PATHSEP .. 'lite-xl'))
|
||||
USERDIR = (os.getenv("XDG_CONFIG_HOME") and os.getenv("XDG_CONFIG_HOME") .. "/lite-xl")
|
||||
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, err = package.searchpath(modname, package.cpath)
|
||||
if not path then return err end
|
||||
return system.load_native_plugin, path
|
||||
end }
|
||||
|
||||
table.pack = table.pack or pack or function(...) return {...} end
|
||||
table.unpack = table.unpack or unpack
|
||||
|
||||
bit32 = bit32 or require "core.bit"
|
||||
|
||||
require "core.utf8string"
|
||||
|
||||
-- Because AppImages change the working directory before running the executable,
|
||||
-- we need to change it back to the original one.
|
||||
-- https://github.com/AppImage/AppImageKit/issues/172
|
||||
-- https://github.com/AppImage/AppImageKit/pull/191
|
||||
local appimage_owd = os.getenv("OWD")
|
||||
if os.getenv("APPIMAGE") and appimage_owd then
|
||||
system.chdir(appimage_owd)
|
||||
end
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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 = style.font:copy(40 * SCALE)
|
||||
style.icon_font = renderer.font.load(DATADIR .. "/fonts/icons.ttf", 14 * SCALE, {antialiasing="grayscale", hinting="full"})
|
||||
style.icon_big_font = style.icon_font:copy(20 * SCALE)
|
||||
style.code_font = renderer.font.load(DATADIR .. "/fonts/JetBrainsMono-Regular.ttf", 13 * SCALE)
|
||||
|
||||
style.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,6 +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
|
||||
|
|
|
@ -3,49 +3,26 @@ 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
|
||||
|
||||
|
||||
local function find(string, field)
|
||||
local best_match = 0
|
||||
local best_syntax
|
||||
for i = #syntax.items, 1, -1 do
|
||||
local t = syntax.items[i]
|
||||
local s, e = common.match_pattern(string, t[field] or {})
|
||||
if s and e - s > best_match then
|
||||
best_match = e - s
|
||||
best_syntax = t
|
||||
if common.match_pattern(string, t[field] or {}) then
|
||||
return t
|
||||
end
|
||||
end
|
||||
return best_syntax
|
||||
end
|
||||
|
||||
function syntax.get(filename, header)
|
||||
return (filename and find(filename, "files"))
|
||||
or (header and find(header, "headers"))
|
||||
return find(filename, "files")
|
||||
or find(header, "headers")
|
||||
or plain_text_syntax
|
||||
end
|
||||
|
||||
|
|
|
@ -3,14 +3,6 @@ local common = require "core.common"
|
|||
local style = require "core.style"
|
||||
local View = require "core.view"
|
||||
|
||||
local icon_colors = {
|
||||
bg = { common.color "#2e2e32ff" },
|
||||
color6 = { common.color "#e1e1e6ff" },
|
||||
color7 = { common.color "#ffa94dff" },
|
||||
color8 = { common.color "#93ddfaff" },
|
||||
color9 = { common.color "#f7c95cff" }
|
||||
};
|
||||
|
||||
local restore_command = {
|
||||
symbol = "w", action = function() system.set_window_mode("normal") end
|
||||
}
|
||||
|
@ -25,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()
|
||||
|
@ -51,10 +41,6 @@ function TitleView:configure_hit_test(borderless)
|
|||
end
|
||||
end
|
||||
|
||||
function TitleView:on_scale_change()
|
||||
self:configure_hit_test(self.visible)
|
||||
end
|
||||
|
||||
function TitleView:update()
|
||||
self.size.y = self.visible and title_view_height() or 0
|
||||
title_commands[2] = core.window_mode == "maximized" and restore_command or maximize_command
|
||||
|
@ -67,11 +53,7 @@ function TitleView:draw_window_title()
|
|||
local ox, oy = self:get_content_offset()
|
||||
local color = style.text
|
||||
local x, y = ox + style.padding.x, oy + style.padding.y
|
||||
common.draw_text(style.icon_font, icon_colors.bg, "5", nil, x, y, 0, h)
|
||||
common.draw_text(style.icon_font, icon_colors.color6, "6", nil, x, y, 0, h)
|
||||
common.draw_text(style.icon_font, icon_colors.color7, "7", nil, x, y, 0, h)
|
||||
common.draw_text(style.icon_font, icon_colors.color8, "8", nil, x, y, 0, h)
|
||||
x = common.draw_text(style.icon_font, icon_colors.color9, "9 ", nil, x, y, 0, h)
|
||||
x = common.draw_text(style.icon_font, color, "M ", nil, x, y, 0, h)
|
||||
local title = core.compose_window_title(core.window_title)
|
||||
common.draw_text(style.font, color, title, nil, x, y, 0, h)
|
||||
end
|
||||
|
|
|
@ -1,16 +1,11 @@
|
|||
local core = require "core"
|
||||
local syntax = require "core.syntax"
|
||||
local config = require "core.config"
|
||||
|
||||
local tokenizer = {}
|
||||
local bad_patterns = {}
|
||||
|
||||
local function push_token(t, type, text)
|
||||
if not text or #text == 0 then return end
|
||||
type = type or "normal"
|
||||
local prev_type = t[#t-1]
|
||||
local prev_text = t[#t]
|
||||
if prev_type and (prev_type == type or (prev_text:ufind("^%s*$") and type ~= "incomplete")) then
|
||||
if prev_type and (prev_type == type or prev_text:find("^%s*$")) then
|
||||
t[#t-1] = type
|
||||
t[#t] = prev_text .. text
|
||||
else
|
||||
|
@ -42,47 +37,41 @@ 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
|
||||
|
||||
-- State is a string of bytes, where the count of bytes represents the depth
|
||||
-- of the subsyntax we are currently in. Each individual byte represents the
|
||||
-- index of the pattern for the current subsyntax in relation to its parent
|
||||
-- syntax. Using a string of bytes allows us to have as many subsyntaxes as
|
||||
-- bytes can be stored on a string while keeping some level of performance in
|
||||
-- comparison to a Lua table. The only limitation is that a syntax would not
|
||||
-- be able to contain more than 255 patterns.
|
||||
--
|
||||
-- Lets say a state contains 2 bytes byte #1 with value `3` and byte #2 with
|
||||
-- a value of `5`. This would mean that on the parent syntax at index `3` a
|
||||
-- pattern subsyntax that matched current text was found, then inside that
|
||||
-- subsyntax another subsyntax pattern at index `5` that matched current text
|
||||
-- was also found.
|
||||
|
||||
-- Calling `push_subsyntax` appends the current subsyntax pattern index to the
|
||||
-- state and increases the stack depth. Calling `pop_subsyntax` clears the
|
||||
-- last appended subsyntax and decreases the stack.
|
||||
-- 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.
|
||||
|
||||
-- You can think of it as a maximum 4 integer (0-255) stack. It always has
|
||||
-- 1 integer in it. Calling `push_subsyntax` increases the stack depth. Calling
|
||||
-- `pop_subsyntax` decreases it. The integers represent the index of a pattern
|
||||
-- that we're following in the syntax. The top of the stack can be any valid
|
||||
-- pattern index, any integer lower in the stack must represent a pattern that
|
||||
-- specifies a subsyntax.
|
||||
|
||||
-- If you do not have subsyntaxes in your syntax, the three most
|
||||
-- singificant numbers will always be 0, the stack will only ever be length 1
|
||||
-- and the state variable will only ever range from 0-255.
|
||||
local function retrieve_syntax_state(incoming_syntax, state)
|
||||
local current_syntax, subsyntax_info, current_pattern_idx, current_level =
|
||||
incoming_syntax, nil, state:byte(1) or 0, 1
|
||||
if
|
||||
current_pattern_idx > 0
|
||||
and
|
||||
current_syntax.patterns[current_pattern_idx]
|
||||
then
|
||||
-- If the state is not empty we iterate over each byte, and find which
|
||||
incoming_syntax, nil, state, 0
|
||||
if state > 0 and (state > 255 or current_syntax.patterns[state].syntax) then
|
||||
-- If we have higher bits, then decode them one at a time, and find which
|
||||
-- syntax we're using. Rather than walking the bytes, and calling into
|
||||
-- `syntax` each time, we could probably cache this in a single table.
|
||||
for i = 1, #state do
|
||||
local target = state:byte(i)
|
||||
for i = 0, 2 do
|
||||
local target = bit32.extract(state, i*8, 8)
|
||||
if target ~= 0 then
|
||||
if current_syntax.patterns[target].syntax then
|
||||
subsyntax_info = current_syntax.patterns[target]
|
||||
|
@ -102,87 +91,32 @@ local function retrieve_syntax_state(incoming_syntax, state)
|
|||
return current_syntax, subsyntax_info, current_pattern_idx, current_level
|
||||
end
|
||||
|
||||
---Return the list of syntaxes used in the specified state.
|
||||
---@param base_syntax table @The initial base syntax (the syntax of the file)
|
||||
---@param state string @The state of the tokenizer to extract from
|
||||
---@return table @Array of syntaxes starting from the innermost one
|
||||
function tokenizer.extract_subsyntaxes(base_syntax, state)
|
||||
local current_syntax
|
||||
local t = {}
|
||||
repeat
|
||||
current_syntax = retrieve_syntax_state(base_syntax, state)
|
||||
table.insert(t, current_syntax)
|
||||
state = string.sub(state, 2)
|
||||
until #state == 0
|
||||
return t
|
||||
end
|
||||
|
||||
local function report_bad_pattern(log_fn, syntax, pattern_idx, msg, ...)
|
||||
if not bad_patterns[syntax] then
|
||||
bad_patterns[syntax] = { }
|
||||
end
|
||||
if bad_patterns[syntax][pattern_idx] then return end
|
||||
bad_patterns[syntax][pattern_idx] = true
|
||||
log_fn("Malformed pattern #%d <%s> in %s language plugin.\n" .. msg,
|
||||
pattern_idx,
|
||||
syntax.patterns[pattern_idx].pattern or syntax.patterns[pattern_idx].regex,
|
||||
syntax.name or "unnamed", ...)
|
||||
end
|
||||
|
||||
---@param incoming_syntax table
|
||||
---@param text string
|
||||
---@param state string
|
||||
function tokenizer.tokenize(incoming_syntax, text, state, resume)
|
||||
local res
|
||||
function tokenizer.tokenize(incoming_syntax, text, state)
|
||||
local res = {}
|
||||
local i = 1
|
||||
|
||||
if #incoming_syntax.patterns == 0 then
|
||||
return { "normal", text }
|
||||
end
|
||||
|
||||
state = state or string.char(0)
|
||||
|
||||
if resume then
|
||||
res = resume.res
|
||||
-- Remove "incomplete" tokens
|
||||
while res[#res-1] == "incomplete" do
|
||||
table.remove(res, #res)
|
||||
table.remove(res, #res)
|
||||
end
|
||||
i = resume.i
|
||||
state = resume.state
|
||||
end
|
||||
|
||||
res = res or {}
|
||||
|
||||
state = state or 0
|
||||
-- incoming_syntax : the parent syntax of the file.
|
||||
-- state : a string of bytes representing syntax state (see above)
|
||||
|
||||
-- state : a 32-bit number representing syntax state (see above)
|
||||
|
||||
-- current_syntax : the syntax we're currently in.
|
||||
-- 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
|
||||
local state_len = #state
|
||||
if current_level > state_len then
|
||||
state = state .. string.char(pattern_idx)
|
||||
elseif state_len == 1 then
|
||||
state = string.char(pattern_idx)
|
||||
else
|
||||
state = ("%s%s%s"):format(
|
||||
state:sub(1,current_level-1),
|
||||
string.char(pattern_idx),
|
||||
state:sub(current_level+1)
|
||||
)
|
||||
end
|
||||
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
|
||||
|
@ -191,96 +125,40 @@ function tokenizer.tokenize(incoming_syntax, text, state, resume)
|
|||
entering_syntax.syntax or syntax.get(entering_syntax.syntax)
|
||||
current_pattern_idx = 0
|
||||
end
|
||||
|
||||
|
||||
local function pop_subsyntax()
|
||||
current_level = current_level - 1
|
||||
state = string.sub(state, 1, current_level)
|
||||
set_subsyntax_pattern_idx(0)
|
||||
current_syntax, subsyntax_info, current_pattern_idx, current_level =
|
||||
current_level = current_level - 1
|
||||
set_subsyntax_pattern_idx(0)
|
||||
current_syntax, subsyntax_info, current_pattern_idx, current_level =
|
||||
retrieve_syntax_state(incoming_syntax, state)
|
||||
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)
|
||||
code = target[p_idx]
|
||||
else
|
||||
p.pattern = p.pattern and code:usub(2)
|
||||
p.regex = p.regex and code:usub(2)
|
||||
code = p.pattern or p.regex
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local target, res = p.pattern or p.regex, { 1, offset - 1 }, p.regex
|
||||
local code = type(target) == "table" and target[close and 2 or 1] or target
|
||||
if p.regex and type(p.regex) ~= "table" then
|
||||
p._regex = p._regex or regex.compile(p.regex)
|
||||
code = p._regex
|
||||
end
|
||||
|
||||
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.find(code, text, text:ucharpos(next), (at_start or p.whole_line[p_idx]) and regex.ANCHORED or 0) }
|
||||
if p.regex and #res > 0 then -- set correct utf8 len for regex result
|
||||
local char_pos_1 = res[1] > next and string.ulen(text:sub(1, res[1])) or next
|
||||
local char_pos_2 = string.ulen(text:sub(1, res[2]))
|
||||
for i=3,#res do
|
||||
res[i] = string.ulen(text:sub(1, res[i] - 1)) + 1
|
||||
end
|
||||
res[1] = char_pos_1
|
||||
res[2] = char_pos_2
|
||||
end
|
||||
if res[1] and target[3] then
|
||||
-- Check to see if the escaped character is there,
|
||||
-- and if it is not itself escaped.
|
||||
res = p.pattern and { text:find(at_start and "^" .. code or code, res[2]+1) }
|
||||
or { regex.match(code, text, res[2]+1, at_start and regex.ANCHORED or 0) }
|
||||
if res[1] and close and target[3] then
|
||||
local count = 0
|
||||
for i = res[1] - 1, 1, -1 do
|
||||
if text:ubyte(i) ~= target[3]:ubyte() then break end
|
||||
if text:byte(i) ~= target[3]:byte() then break end
|
||||
count = count + 1
|
||||
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
|
||||
-- Check to see if the escaped character is there,
|
||||
-- and if it is not itself escaped.
|
||||
if count % 2 == 0 then break end
|
||||
end
|
||||
until not res[1] or not close or not target[3]
|
||||
return table.unpack(res)
|
||||
return unpack(res)
|
||||
end
|
||||
|
||||
local text_len = text:ulen()
|
||||
local start_time = system.get_time()
|
||||
local starting_i = i
|
||||
while text_len ~= nil and i <= text_len do
|
||||
-- Every 200 chars, check if we're out of time
|
||||
if i - starting_i > 200 then
|
||||
starting_i = i
|
||||
if system.get_time() - start_time > 0.5 / config.fps then
|
||||
-- We're out of time
|
||||
push_token(res, "incomplete", string.usub(text, i))
|
||||
return res, string.char(0), {
|
||||
res = res,
|
||||
i = i,
|
||||
state = state
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
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]
|
||||
|
@ -292,12 +170,12 @@ function tokenizer.tokenize(incoming_syntax, text, state, resume)
|
|||
-- 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
|
||||
-- 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))
|
||||
push_token(res, p.type, text:sub(i, ss - 1))
|
||||
i = ss
|
||||
cont = false
|
||||
end
|
||||
|
@ -306,11 +184,11 @@ function tokenizer.tokenize(incoming_syntax, text, state, resume)
|
|||
-- continue on as normal.
|
||||
if cont then
|
||||
if s then
|
||||
push_token(res, p.type, text:usub(i, e))
|
||||
push_token(res, p.type, text:sub(i, e))
|
||||
set_subsyntax_pattern_idx(0)
|
||||
i = e + 1
|
||||
else
|
||||
push_token(res, p.type, text:usub(i))
|
||||
push_token(res, p.type, text:sub(i))
|
||||
break
|
||||
end
|
||||
end
|
||||
|
@ -318,15 +196,13 @@ function tokenizer.tokenize(incoming_syntax, text, state, resume)
|
|||
-- 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.
|
||||
while subsyntax_info do
|
||||
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))
|
||||
push_token(res, subsyntax_info.type, text:sub(i, e))
|
||||
-- On finding unescaped delimiter, pop it.
|
||||
pop_subsyntax()
|
||||
i = e + 1
|
||||
else
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -335,28 +211,6 @@ function tokenizer.tokenize(incoming_syntax, text, state, resume)
|
|||
for n, p in ipairs(current_syntax.patterns) do
|
||||
local find_results = { find_text(text, p, i, true, false) }
|
||||
if find_results[1] then
|
||||
-- Check for patterns successfully matching nothing
|
||||
if find_results[1] > find_results[2] then
|
||||
report_bad_pattern(core.warn, current_syntax, n,
|
||||
"Pattern successfully matched, but nothing was captured.")
|
||||
goto continue
|
||||
end
|
||||
|
||||
-- Check for patterns with mismatching number of `types`
|
||||
local type_is_table = type(p.type) == "table"
|
||||
local n_types = type_is_table and #p.type or 1
|
||||
if #find_results == 2 and type_is_table then
|
||||
report_bad_pattern(core.warn, current_syntax, n,
|
||||
"Token type is a table, but a string was expected.")
|
||||
p.type = p.type[1]
|
||||
elseif #find_results - 1 > n_types then
|
||||
report_bad_pattern(core.error, current_syntax, n,
|
||||
"Not enough token types: got %d needed %d.", n_types, #find_results - 1)
|
||||
elseif #find_results - 1 < n_types then
|
||||
report_bad_pattern(core.warn, current_syntax, n,
|
||||
"Too many token types: got %d needed %d.", n_types, #find_results - 1)
|
||||
end
|
||||
|
||||
-- matched pattern; make and add tokens
|
||||
push_tokens(res, current_syntax, p, text, find_results)
|
||||
-- update state if this was a start|end pattern pair
|
||||
|
@ -364,7 +218,7 @@ function tokenizer.tokenize(incoming_syntax, text, state, resume)
|
|||
-- If we have a subsyntax, push that onto the subsyntax stack.
|
||||
if p.syntax then
|
||||
push_subsyntax(p, n)
|
||||
else
|
||||
else
|
||||
set_subsyntax_pattern_idx(n)
|
||||
end
|
||||
end
|
||||
|
@ -372,13 +226,12 @@ function tokenizer.tokenize(incoming_syntax, text, state, resume)
|
|||
i = find_results[2] + 1
|
||||
matched = true
|
||||
break
|
||||
::continue::
|
||||
end
|
||||
end
|
||||
|
||||
-- consume character if we didn't match
|
||||
if not matched then
|
||||
push_token(res, "normal", text:usub(i, i))
|
||||
push_token(res, "normal", text:sub(i, i))
|
||||
i = i + 1
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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
|
|
@ -1,57 +1,12 @@
|
|||
local core = require "core"
|
||||
local config = require "core.config"
|
||||
local style = require "core.style"
|
||||
local common = require "core.common"
|
||||
local Object = require "core.object"
|
||||
local Scrollbar = require "core.scrollbar"
|
||||
|
||||
---@class core.view.position
|
||||
---@field x number
|
||||
---@field y number
|
||||
|
||||
---@class core.view.scroll
|
||||
---@field x number
|
||||
---@field y number
|
||||
---@field to core.view.position
|
||||
|
||||
---@class core.view.thumbtrack
|
||||
---@field thumb number
|
||||
---@field track number
|
||||
|
||||
---@class core.view.thumbtrackwidth
|
||||
---@field thumb number
|
||||
---@field track number
|
||||
---@field to core.view.thumbtrack
|
||||
|
||||
---@class core.view.scrollbar
|
||||
---@field x core.view.thumbtrack
|
||||
---@field y core.view.thumbtrack
|
||||
---@field w core.view.thumbtrackwidth
|
||||
---@field h core.view.thumbtrack
|
||||
|
||||
---@alias core.view.cursor "'arrow'" | "'ibeam'" | "'sizeh'" | "'sizev'" | "'hand'"
|
||||
|
||||
---@alias core.view.mousebutton "'left'" | "'right'"
|
||||
|
||||
---@alias core.view.context "'application'" | "'session'"
|
||||
|
||||
---Base view.
|
||||
---@class core.view : core.object
|
||||
---@field context core.view.context
|
||||
---@field super core.object
|
||||
---@field position core.view.position
|
||||
---@field size core.view.position
|
||||
---@field scroll core.view.scroll
|
||||
---@field cursor core.view.cursor
|
||||
---@field scrollable boolean
|
||||
---@field v_scrollbar core.scrollbar
|
||||
---@field h_scrollbar core.scrollbar
|
||||
---@field current_scale number
|
||||
local View = Object:extend()
|
||||
|
||||
-- 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 }
|
||||
|
@ -59,18 +14,14 @@ function View:new()
|
|||
self.scroll = { x = 0, y = 0, to = { x = 0, y = 0 } }
|
||||
self.cursor = "arrow"
|
||||
self.scrollable = false
|
||||
self.v_scrollbar = Scrollbar({direction = "v", alignment = "e"})
|
||||
self.h_scrollbar = Scrollbar({direction = "h", alignment = "e"})
|
||||
self.current_scale = SCALE
|
||||
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
|
||||
|
@ -80,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
|
||||
|
@ -91,153 +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
|
||||
function View:get_h_scrollable_size()
|
||||
return 0
|
||||
|
||||
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)
|
||||
return
|
||||
self.position.x + self.size.x - style.scrollbar_size,
|
||||
self.position.y + self.scroll.y * (self.size.y - h) / (sz - self.size.y),
|
||||
style.scrollbar_size,
|
||||
h
|
||||
end
|
||||
|
||||
|
||||
---@param x number
|
||||
---@param y number
|
||||
---@return boolean
|
||||
function View:scrollbar_overlaps_point(x, y)
|
||||
return not (not (self.v_scrollbar:overlaps(x, y) or self.h_scrollbar:overlaps(x, y)))
|
||||
local sx, sy, sw, sh = self:get_scrollbar_rect()
|
||||
return x >= sx - sw * 3 and x < sx + sw and y >= sy and y < sy + sh
|
||||
end
|
||||
|
||||
|
||||
---@return boolean
|
||||
function View:scrollbar_dragging()
|
||||
return self.v_scrollbar.dragging or self.h_scrollbar.dragging
|
||||
end
|
||||
|
||||
|
||||
---@return boolean
|
||||
function View:scrollbar_hovering()
|
||||
return self.v_scrollbar.hovering.track or self.h_scrollbar.hovering.track
|
||||
end
|
||||
|
||||
|
||||
---@param button core.view.mousebutton
|
||||
---@param x number
|
||||
---@param y number
|
||||
---@param clicks integer
|
||||
---return boolean
|
||||
function View:on_mouse_pressed(button, x, y, clicks)
|
||||
if not self.scrollable then return end
|
||||
local result = self.v_scrollbar:on_mouse_pressed(button, x, y, clicks)
|
||||
if result then
|
||||
if result ~= true then
|
||||
self.scroll.to.y = result * self:get_scrollable_size()
|
||||
end
|
||||
return true
|
||||
end
|
||||
result = self.h_scrollbar:on_mouse_pressed(button, x, y, clicks)
|
||||
if result then
|
||||
if result ~= true then
|
||||
self.scroll.to.x = result * self:get_h_scrollable_size()
|
||||
end
|
||||
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)
|
||||
if not self.scrollable then return end
|
||||
self.v_scrollbar:on_mouse_released(button, x, y)
|
||||
self.h_scrollbar: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 not self.scrollable then return end
|
||||
local result
|
||||
if self.h_scrollbar.dragging then goto skip_v_scrollbar end
|
||||
result = self.v_scrollbar:on_mouse_moved(x, y, dx, dy)
|
||||
if result then
|
||||
if result ~= true then
|
||||
self.scroll.to.y = result * self:get_scrollable_size()
|
||||
if not config.animate_drag_scroll then
|
||||
self:clamp_scroll_position()
|
||||
self.scroll.y = self.scroll.to.y
|
||||
end
|
||||
end
|
||||
-- hide horizontal scrollbar
|
||||
self.h_scrollbar:on_mouse_left()
|
||||
return true
|
||||
end
|
||||
::skip_v_scrollbar::
|
||||
result = self.h_scrollbar:on_mouse_moved(x, y, dx, dy)
|
||||
if result then
|
||||
if result ~= true then
|
||||
self.scroll.to.x = result * self:get_h_scrollable_size()
|
||||
if not config.animate_drag_scroll then
|
||||
self:clamp_scroll_position()
|
||||
self.scroll.x = self.scroll.to.x
|
||||
end
|
||||
end
|
||||
return true
|
||||
if self.dragging_scrollbar then
|
||||
local delta = self:get_scrollable_size() / self.size.y * dy
|
||||
self.scroll.to.y = self.scroll.to.y + delta
|
||||
end
|
||||
self.hovered_scrollbar = self:scrollbar_overlaps_point(x, y)
|
||||
end
|
||||
|
||||
|
||||
function View:on_mouse_left()
|
||||
if not self.scrollable then return end
|
||||
self.v_scrollbar:on_mouse_left()
|
||||
self.h_scrollbar:on_mouse_left()
|
||||
end
|
||||
|
||||
|
||||
---@param filename string
|
||||
---@param x number
|
||||
---@param y number
|
||||
---@return boolean
|
||||
function View:on_file_dropped(filename, x, y)
|
||||
return false
|
||||
end
|
||||
|
||||
|
||||
---@param text string
|
||||
function View:on_text_input(text)
|
||||
-- no-op
|
||||
end
|
||||
|
||||
|
||||
function View:on_ime_text_editing(text, start, length)
|
||||
-- no-op
|
||||
function View:on_mouse_wheel(y)
|
||||
if self.scrollable then
|
||||
self.scroll.to.y = self.scroll.to.y + y * -config.mouse_wheel_scroll
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
---@param y number @Vertical scroll delta; positive is "up"
|
||||
---@param x number @Horizontal scroll delta; positive is "left"
|
||||
---@return boolean @Capture event
|
||||
function View:on_mouse_wheel(y, x)
|
||||
-- no-op
|
||||
end
|
||||
|
||||
---Can be overriden to listen for scale change events to apply
|
||||
---any neccesary changes in sizes, padding, etc...
|
||||
---@param new_scale number
|
||||
---@param prev_scale number
|
||||
function View:on_scale_change(new_scale, prev_scale) end
|
||||
|
||||
function View:get_content_bounds()
|
||||
local x = self.scroll.x
|
||||
local y = self.scroll.y
|
||||
|
@ -245,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)
|
||||
|
@ -257,50 +123,28 @@ end
|
|||
function View:clamp_scroll_position()
|
||||
local max = self:get_scrollable_size() - self.size.y
|
||||
self.scroll.to.y = common.clamp(self.scroll.to.y, 0, max)
|
||||
|
||||
max = self:get_h_scrollable_size() - self.size.x
|
||||
self.scroll.to.x = common.clamp(self.scroll.to.x, 0, max)
|
||||
end
|
||||
|
||||
|
||||
function View:update_scrollbar()
|
||||
local v_scrollable = self:get_scrollable_size()
|
||||
self.v_scrollbar:set_size(self.position.x, self.position.y, self.size.x, self.size.y, v_scrollable)
|
||||
self.v_scrollbar:set_percent(self.scroll.y/v_scrollable)
|
||||
self.v_scrollbar:update()
|
||||
|
||||
local h_scrollable = self:get_h_scrollable_size()
|
||||
self.h_scrollbar:set_size(self.position.x, self.position.y, self.size.x, self.size.y, h_scrollable)
|
||||
self.h_scrollbar:set_percent(self.scroll.x/h_scrollable)
|
||||
self.h_scrollbar:update()
|
||||
end
|
||||
|
||||
|
||||
function View:update()
|
||||
if self.current_scale ~= SCALE then
|
||||
self:on_scale_change(SCALE, self.current_scale)
|
||||
self.current_scale = SCALE
|
||||
end
|
||||
|
||||
self:clamp_scroll_position()
|
||||
self: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")
|
||||
if not self.scrollable then return end
|
||||
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)
|
||||
renderer.draw_rect(x, y, w + x % 1, h + y % 1, color)
|
||||
end
|
||||
|
||||
|
||||
function View:draw_scrollbar()
|
||||
self.v_scrollbar:draw()
|
||||
self.h_scrollbar:draw()
|
||||
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
|
||||
|
||||
|
||||
|
|
Binary file not shown.
|
@ -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"
|
||||
|
@ -10,66 +10,14 @@ 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.plugins.autocomplete = {
|
||||
-- Amount of characters that need to be written for autocomplete
|
||||
min_len = 1
|
||||
-- The max amount of visible items
|
||||
max_height = 6
|
||||
-- The max amount of scrollable items
|
||||
max_suggestions = 100
|
||||
}
|
||||
|
||||
local autocomplete = {}
|
||||
|
||||
|
@ -85,7 +33,7 @@ local triggered_manually = false
|
|||
|
||||
local mt = { __tostring = function(t) return t.text end }
|
||||
|
||||
function autocomplete.add(t, manually_triggered)
|
||||
function autocomplete.add(t, triggered_manually)
|
||||
local items = {}
|
||||
for text, info in pairs(t.items) do
|
||||
if type(info) == "table" then
|
||||
|
@ -95,10 +43,9 @@ function autocomplete.add(t, manually_triggered)
|
|||
{
|
||||
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
|
||||
desc = info.desc, -- Description shown on item selected
|
||||
cb = info.cb, -- A callback called once when item is selected
|
||||
data = info.data -- Optional data that can be used on cb
|
||||
},
|
||||
mt
|
||||
)
|
||||
|
@ -109,7 +56,7 @@ function autocomplete.add(t, manually_triggered)
|
|||
end
|
||||
end
|
||||
|
||||
if not manually_triggered then
|
||||
if not triggered_manually then
|
||||
autocomplete.map[t.name] = { files = t.files or ".*", items = items }
|
||||
else
|
||||
autocomplete.map_manually[t.name] = { files = t.files or ".*", items = items }
|
||||
|
@ -119,43 +66,26 @@ end
|
|||
--
|
||||
-- Thread that scans open document symbols and cache them
|
||||
--
|
||||
local max_symbols = config.plugins.autocomplete.max_symbols
|
||||
local max_symbols = config.max_symbols
|
||||
|
||||
core.add_thread(function()
|
||||
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 +132,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
|
||||
|
@ -212,14 +141,12 @@ end)
|
|||
|
||||
|
||||
local partial = ""
|
||||
local suggestions_offset = 1
|
||||
local suggestions_idx = 1
|
||||
local suggestions = {}
|
||||
local last_line, last_col
|
||||
|
||||
|
||||
local function reset_suggestions()
|
||||
suggestions_offset = 1
|
||||
suggestions_idx = 1
|
||||
suggestions = {}
|
||||
|
||||
|
@ -232,6 +159,16 @@ local function reset_suggestions()
|
|||
end
|
||||
end
|
||||
|
||||
local function in_table(value, table_array)
|
||||
for i, element in pairs(table_array) do
|
||||
if element == value then
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
local function update_suggestions()
|
||||
local doc = core.active_view.doc
|
||||
local filename = doc and doc.filename or ""
|
||||
|
@ -262,8 +199,6 @@ local function update_suggestions()
|
|||
j = j + 1
|
||||
end
|
||||
end
|
||||
suggestions_idx = 1
|
||||
suggestions_offset = 1
|
||||
end
|
||||
|
||||
local function get_partial_symbol()
|
||||
|
@ -274,161 +209,67 @@ local function get_partial_symbol()
|
|||
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 last_max_width = 0
|
||||
local function get_suggestions_rect(av)
|
||||
if #suggestions == 0 then
|
||||
last_max_width = 0
|
||||
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()
|
||||
|
||||
local ah = config.plugins.autocomplete.max_height
|
||||
|
||||
local max_items = math.min(ah, #suggestions)
|
||||
|
||||
local show_count = math.min(#suggestions, ah)
|
||||
local start_index = math.max(suggestions_idx-(ah-1), 1)
|
||||
|
||||
local max_width = 0
|
||||
for i = start_index, start_index + show_count - 1 do
|
||||
local s = suggestions[i]
|
||||
for _, s in ipairs(suggestions) do
|
||||
local w = font:get_width(s.text)
|
||||
if s.info then
|
||||
w = w + style.font:get_width(s.info) + style.padding.x
|
||||
end
|
||||
max_width = math.max(max_width, w)
|
||||
end
|
||||
max_width = math.max(last_max_width, max_width)
|
||||
last_max_width = max_width
|
||||
|
||||
max_width = max_width + style.padding.x * 2
|
||||
x = x - style.padding.x
|
||||
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 > core.root_view.size.x then
|
||||
max_width = core.root_view.size.x
|
||||
end
|
||||
if max_width < 150 * SCALE then
|
||||
max_width = 150 * SCALE
|
||||
end
|
||||
|
||||
-- if portion not visiable to right, reposition to DocView right margin
|
||||
if x + max_width > core.root_view.size.x then
|
||||
x = (av.size.x + av.position.x) - max_width
|
||||
if max_width < 150 then
|
||||
max_width = 150
|
||||
end
|
||||
|
||||
return
|
||||
x,
|
||||
x - style.padding.x,
|
||||
y - style.padding.y,
|
||||
max_width,
|
||||
max_width + style.padding.x * 2,
|
||||
max_items * (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
|
||||
width = math.max(width, style.font:get_width(line))
|
||||
table.insert(lines, line)
|
||||
end
|
||||
|
||||
if draw_left then
|
||||
x = sx - (style.padding.x / 4) - width - (style.padding.x * 2)
|
||||
end
|
||||
|
||||
local height = #lines * font:get_height()
|
||||
local height = #lines * style.font:get_height()
|
||||
|
||||
-- draw background rect
|
||||
renderer.draw_rect(
|
||||
x,
|
||||
sx + sw + style.padding.x / 4,
|
||||
sy,
|
||||
width + style.padding.x * 2,
|
||||
height + style.padding.y * 2,
|
||||
|
@ -436,10 +277,13 @@ local function draw_description_box(text, av, sx, sy, sw, sh)
|
|||
)
|
||||
|
||||
-- draw text
|
||||
local lh = style.font:get_height()
|
||||
local y = sy + style.padding.y
|
||||
local x = sx + sw + style.padding.x / 4
|
||||
|
||||
for _, line in pairs(lines) do
|
||||
common.draw_text(
|
||||
font, style.text, line, "left",
|
||||
x + style.padding.x, y, width, lh
|
||||
style.font, style.text, line, "left", x + style.padding.x, y, width, lh
|
||||
)
|
||||
y = y + lh
|
||||
end
|
||||
|
@ -460,38 +304,26 @@ 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 = math.min(#suggestions, ah)
|
||||
local start_index = suggestions_offset
|
||||
local show_count = #suggestions <= ah and #suggestions or ah
|
||||
local start_index = suggestions_idx > ah and (suggestions_idx-(ah-1)) or 1
|
||||
|
||||
for i=start_index, start_index+show_count-1, 1 do
|
||||
if not suggestions[i] then
|
||||
break
|
||||
end
|
||||
local s = suggestions[i]
|
||||
|
||||
local info_size = s.info and (style.font:get_width(s.info) + style.padding.x) or style.padding.x
|
||||
|
||||
local color = (i == suggestions_idx) and style.accent or style.text
|
||||
-- Push clip to avoid that the suggestion text gets drawn over suggestion type/icon
|
||||
core.push_clip_rect(rx + style.padding.x, y, rw - info_size - style.padding.x, lh)
|
||||
local x_adv = common.draw_text(font, color, s.text, "left", rx + style.padding.x, y, rw, lh)
|
||||
core.pop_clip_rect()
|
||||
-- If the text wasn't fully visible, draw an ellipsis
|
||||
if x_adv > rx + rw - info_size then
|
||||
local ellipsis_size = font:get_width("…")
|
||||
local ell_x = rx + rw - info_size - ellipsis_size
|
||||
renderer.draw_rect(ell_x, y, ellipsis_size, lh, style.background3)
|
||||
common.draw_text(font, color, "…", "left", ell_x, y, ellipsis_size, lh)
|
||||
end
|
||||
common.draw_text(font, color, s.text, "left", rx + style.padding.x, y, rw, lh)
|
||||
if s.info then
|
||||
color = (i == suggestions_idx) and style.text or style.dim
|
||||
common.draw_text(style.font, color, s.info, "right", rx, y, rw - style.padding.x, lh)
|
||||
end
|
||||
y = y + lh
|
||||
if suggestions_idx == i then
|
||||
if s.onhover then
|
||||
s.onhover(suggestions_idx, s)
|
||||
s.onhover = nil
|
||||
if s.cb then
|
||||
s.cb(suggestions_idx, s)
|
||||
s.cb = nil
|
||||
s.data = nil
|
||||
end
|
||||
if s.desc and #s.desc > 0 then
|
||||
draw_description_box(s.desc, av, rx, ry, rw, rh)
|
||||
|
@ -615,11 +447,8 @@ function autocomplete.open(on_close)
|
|||
end
|
||||
|
||||
local av = get_active_view()
|
||||
if av then
|
||||
partial = get_partial_symbol()
|
||||
last_line, last_col = av.doc:get_selection()
|
||||
update_suggestions()
|
||||
end
|
||||
last_line, last_col = av.doc:get_selection()
|
||||
update_suggestions()
|
||||
end
|
||||
|
||||
function autocomplete.close()
|
||||
|
@ -651,67 +480,26 @@ 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
|
||||
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()
|
||||
local sz = #current_partial
|
||||
|
||||
for _, line1, col1, line2, _ in doc:get_selections(true) do
|
||||
local n = col1 - 1
|
||||
local line = doc.lines[line1]
|
||||
for i = 1, sz + 1 do
|
||||
local j = sz - i
|
||||
local subline = line:sub(n - j, n)
|
||||
local subpartial = current_partial:sub(i, -1)
|
||||
if subpartial == subline then
|
||||
doc:remove(line1, col1, line2, n - j)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
doc:text_input(item.text)
|
||||
end
|
||||
["autocomplete:complete"] = function()
|
||||
local doc = core.active_view.doc
|
||||
local line, col = doc:get_selection()
|
||||
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,
|
||||
|
||||
["autocomplete:previous"] = function()
|
||||
suggestions_idx = (suggestions_idx - 2) % #suggestions + 1
|
||||
|
||||
local ah = math.min(config.plugins.autocomplete.max_height, #suggestions)
|
||||
if suggestions_offset > suggestions_idx then
|
||||
suggestions_offset = suggestions_idx
|
||||
elseif suggestions_offset + ah < suggestions_idx + 1 then
|
||||
suggestions_offset = suggestions_idx - ah + 1
|
||||
end
|
||||
suggestions_idx = math.max(suggestions_idx - 1, 1)
|
||||
end,
|
||||
|
||||
["autocomplete:next"] = function()
|
||||
suggestions_idx = (suggestions_idx % #suggestions) + 1
|
||||
|
||||
local ah = math.min(config.plugins.autocomplete.max_height, #suggestions)
|
||||
if suggestions_offset + ah < suggestions_idx + 1 then
|
||||
suggestions_offset = suggestions_idx - ah + 1
|
||||
elseif suggestions_offset > suggestions_idx then
|
||||
suggestions_offset = suggestions_idx
|
||||
end
|
||||
end,
|
||||
|
||||
["autocomplete:cycle"] = function()
|
||||
local newidx = suggestions_idx + 1
|
||||
suggestions_idx = newidx > #suggestions and 1 or newidx
|
||||
suggestions_idx = math.min(suggestions_idx + 1, #suggestions)
|
||||
end,
|
||||
|
||||
["autocomplete:cancel"] = function()
|
||||
|
|
|
@ -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?", {
|
||||
{ text = "Yes", default_yes = true },
|
||||
{ text = "No", default_no = true }
|
||||
}, function(item)
|
||||
if item.text == "Yes" then reload_doc(doc) end
|
||||
doc.deferred_reload = false
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
local function doc_changes_visiblity(doc, visibility)
|
||||
if doc and visible[doc] ~= visibility and doc.abs_filename then
|
||||
visible[doc] = visibility
|
||||
if visibility then check_prompt_reload(doc) end
|
||||
get_project_doc_watch(doc):watch(doc.abs_filename, visibility)
|
||||
end
|
||||
end
|
||||
|
||||
local on_check = dirwatch.check
|
||||
function dirwatch:check(change_callback, ...)
|
||||
on_check(self, function(dir)
|
||||
for _, doc in ipairs(core.docs) do
|
||||
if doc.abs_filename and (dir == common.dirname(doc.abs_filename) or dir == doc.abs_filename) then
|
||||
local info = system.get_file_info(doc.filename or "")
|
||||
if info and info.type == "file" and times[doc] ~= info.modified then
|
||||
if not doc:is_dirty() and not config.plugins.autoreload.always_show_nagview then
|
||||
reload_doc(doc)
|
||||
else
|
||||
doc.deferred_reload = true
|
||||
if doc == core.active_view.doc then check_prompt_reload(doc) end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
change_callback(dir)
|
||||
end, ...)
|
||||
end
|
||||
|
||||
local core_set_active_view = core.set_active_view
|
||||
function core.set_active_view(view)
|
||||
core_set_active_view(view)
|
||||
doc_changes_visiblity(view.doc, true)
|
||||
end
|
||||
|
||||
local node_set_active_view = Node.set_active_view
|
||||
function Node:set_active_view(view)
|
||||
if self.active_view then doc_changes_visiblity(self.active_view.doc, false) end
|
||||
node_set_active_view(self, view)
|
||||
doc_changes_visiblity(self.active_view.doc, true)
|
||||
end
|
||||
|
||||
core.add_thread(function()
|
||||
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
|
||||
|
|
|
@ -1,85 +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 config = require "core.config"
|
||||
|
||||
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" }
|
||||
|
||||
|
||||
local cmds = {
|
||||
{ text = "Cut", command = "doc:cut" },
|
||||
{ text = "Copy", command = "doc:copy" },
|
||||
{ text = "Paste", command = "doc:paste" },
|
||||
ContextMenu.DIVIDER,
|
||||
{ text = "Find", command = "find-replace:find" },
|
||||
{ text = "Replace", command = "find-replace:replace" }
|
||||
}
|
||||
|
||||
if config.plugins.scale ~= false and require("plugins.scale") then
|
||||
table.move(cmds, 4, 6, 7)
|
||||
cmds[4] = { text = "Font +", command = "scale:increase" }
|
||||
cmds[5] = { text = "Font -", command = "scale:decrease" }
|
||||
cmds[6] = { text = "Font Reset", command = "scale:reset" }
|
||||
end
|
||||
|
||||
menu:register("core.docview", cmds)
|
||||
|
||||
|
||||
return menu
|
|
@ -1,258 +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, _loop)
|
||||
_loop = _loop or 1
|
||||
if _loop > 5 then return end
|
||||
if comments_cache[syntax] then
|
||||
if #comments_cache[syntax] > 0 then
|
||||
return comments_cache[syntax]
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
local comments = {}
|
||||
for idx=1, #syntax.patterns do
|
||||
local pattern = syntax.patterns[idx]
|
||||
local startp = ""
|
||||
if
|
||||
type(pattern.type) == "string"
|
||||
and
|
||||
(pattern.type == "comment" or pattern.type == "string")
|
||||
then
|
||||
local not_is_string = pattern.type ~= "string"
|
||||
if pattern.pattern then
|
||||
startp = type(pattern.pattern) == "table"
|
||||
and pattern.pattern[1] or pattern.pattern
|
||||
if not_is_string and startp:sub(1, 1) ~= "^" then
|
||||
startp = "^%s*" .. startp
|
||||
elseif not_is_string then
|
||||
startp = "^%s*" .. startp:sub(2, startp:len())
|
||||
end
|
||||
if type(pattern.pattern) == "table" then
|
||||
table.insert(comments, {"p", startp, pattern.pattern[2]})
|
||||
elseif not_is_string then
|
||||
table.insert(comments, {"p", startp})
|
||||
end
|
||||
elseif pattern.regex then
|
||||
startp = type(pattern.regex) == "table"
|
||||
and pattern.regex[1] or pattern.regex
|
||||
if not_is_string and startp:sub(1, 1) ~= "^" then
|
||||
startp = "^\\s*" .. startp
|
||||
elseif not_is_string then
|
||||
startp = "^\\s*" .. startp:sub(2, startp:len())
|
||||
end
|
||||
if type(pattern.regex) == "table" then
|
||||
table.insert(comments, {
|
||||
"r", regex.compile(startp), regex.compile(pattern.regex[2])
|
||||
})
|
||||
elseif not_is_string then
|
||||
table.insert(comments, {"r", regex.compile(startp)})
|
||||
end
|
||||
end
|
||||
elseif pattern.syntax then
|
||||
local subsyntax = type(pattern.syntax) == 'table' and pattern.syntax
|
||||
or core_syntax.get("file"..pattern.syntax, "")
|
||||
local sub_comments = get_comment_patterns(subsyntax, _loop + 1)
|
||||
if sub_comments then
|
||||
for s=1, #sub_comments do
|
||||
table.insert(comments, sub_comments[s])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if #comments == 0 then
|
||||
local single_line_comment = syntax.comment
|
||||
and escape_comment_tokens(syntax.comment) or nil
|
||||
local block_comment = nil
|
||||
if syntax.block_comment then
|
||||
block_comment = {
|
||||
escape_comment_tokens(syntax.block_comment[1]),
|
||||
escape_comment_tokens(syntax.block_comment[2])
|
||||
}
|
||||
end
|
||||
if single_line_comment then
|
||||
table.insert(comments, {"p", "^%s*" .. single_line_comment})
|
||||
end
|
||||
if block_comment then
|
||||
table.insert(comments, {"p", "^%s*" .. block_comment[1], block_comment[2]})
|
||||
end
|
||||
end
|
||||
comments_cache[syntax] = comments
|
||||
if #comments > 0 then
|
||||
return comments
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
|
||||
local function get_non_empty_lines(syntax, lines)
|
||||
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.find_offsets(
|
||||
comment[2], line, 1, regex.ANCHORED
|
||||
)
|
||||
if start then
|
||||
if not regex.find_offsets(
|
||||
comment[3], line, ending+1, regex.ANCHORED
|
||||
)
|
||||
then
|
||||
is_comment = true
|
||||
inside_comment = true
|
||||
end_regex = comment[3]
|
||||
end
|
||||
break
|
||||
end
|
||||
elseif regex.find_offsets(comment[2], line, 1, regex.ANCHORED) then
|
||||
is_comment = true
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif end_pattern and line:find(end_pattern) then
|
||||
is_comment = true
|
||||
inside_comment = false
|
||||
end_pattern = nil
|
||||
elseif end_regex and regex.find_offsets(end_regex, line) then
|
||||
is_comment = true
|
||||
inside_comment = false
|
||||
end_regex = nil
|
||||
end
|
||||
end
|
||||
if
|
||||
not is_comment
|
||||
and
|
||||
not inside_comment
|
||||
then
|
||||
i = i + 1
|
||||
coroutine.yield(i, line)
|
||||
end
|
||||
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 and #spaces > 1 then table.insert(stat, #spaces) 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
|
||||
|
@ -264,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
|
||||
|
@ -279,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
|
||||
})
|
||||
|
|
|
@ -1,321 +0,0 @@
|
|||
-- mod-version:3
|
||||
|
||||
local style = require "core.style"
|
||||
local DocView = require "core.docview"
|
||||
local common = require "core.common"
|
||||
local command = require "core.command"
|
||||
local config = require "core.config"
|
||||
local Highlighter = require "core.doc.highlighter"
|
||||
|
||||
config.plugins.drawwhitespace = common.merge({
|
||||
enabled = false,
|
||||
show_leading = true,
|
||||
show_trailing = true,
|
||||
show_middle = true,
|
||||
|
||||
show_middle_min = 1,
|
||||
|
||||
color = style.syntax.whitespace or style.syntax.comment,
|
||||
leading_color = nil,
|
||||
middle_color = nil,
|
||||
trailing_color = nil,
|
||||
|
||||
substitutions = {
|
||||
{
|
||||
char = " ",
|
||||
sub = "·",
|
||||
-- You can put any of the previous options here too.
|
||||
-- For example:
|
||||
-- show_middle_min = 2,
|
||||
-- show_leading = false,
|
||||
},
|
||||
{
|
||||
char = "\t",
|
||||
sub = "»",
|
||||
},
|
||||
},
|
||||
|
||||
config_spec = {
|
||||
name = "Draw Whitespace",
|
||||
{
|
||||
label = "Enabled",
|
||||
description = "Disable or enable the drawing of white spaces.",
|
||||
path = "enabled",
|
||||
type = "toggle",
|
||||
default = false
|
||||
},
|
||||
{
|
||||
label = "Show Leading",
|
||||
description = "Draw whitespaces starting at the beginning of a line.",
|
||||
path = "show_leading",
|
||||
type = "toggle",
|
||||
default = true,
|
||||
},
|
||||
{
|
||||
label = "Show Middle",
|
||||
description = "Draw whitespaces on the middle of a line.",
|
||||
path = "show_middle",
|
||||
type = "toggle",
|
||||
default = true,
|
||||
},
|
||||
{
|
||||
label = "Show Trailing",
|
||||
description = "Draw whitespaces on the end of a line.",
|
||||
path = "show_trailing",
|
||||
type = "toggle",
|
||||
default = true,
|
||||
},
|
||||
{
|
||||
label = "Show Trailing as Error",
|
||||
description = "Uses an error square to spot them easily, requires 'Show Trailing' enabled.",
|
||||
path = "show_trailing_error",
|
||||
type = "toggle",
|
||||
default = false,
|
||||
on_apply = function(enabled)
|
||||
local found = nil
|
||||
local substitutions = config.plugins.drawwhitespace.substitutions
|
||||
for i, sub in ipairs(substitutions) do
|
||||
if sub.trailing_error then
|
||||
found = i
|
||||
end
|
||||
end
|
||||
if found == nil and enabled then
|
||||
table.insert(substitutions, {
|
||||
char = " ",
|
||||
sub = "█",
|
||||
show_leading = false,
|
||||
show_middle = false,
|
||||
show_trailing = true,
|
||||
trailing_color = style.error,
|
||||
trailing_error = true
|
||||
})
|
||||
elseif found ~= nil and not enabled then
|
||||
table.remove(substitutions, found)
|
||||
end
|
||||
end
|
||||
}
|
||||
}
|
||||
}, config.plugins.drawwhitespace)
|
||||
|
||||
|
||||
local ws_cache
|
||||
local cached_settings
|
||||
local function reset_cache()
|
||||
ws_cache = setmetatable({}, { __mode = "k" })
|
||||
local settings = config.plugins.drawwhitespace
|
||||
cached_settings = {
|
||||
show_leading = settings.show_leading,
|
||||
show_trailing = settings.show_trailing,
|
||||
show_middle = settings.show_middle,
|
||||
show_middle_min = settings.show_middle_min,
|
||||
color = settings.color,
|
||||
leading_color = settings.leading_color,
|
||||
middle_color = settings.middle_color,
|
||||
trailing_color = settings.trailing_color,
|
||||
substitutions = settings.substitutions,
|
||||
}
|
||||
end
|
||||
reset_cache()
|
||||
|
||||
local function reset_cache_if_needed()
|
||||
local settings = config.plugins.drawwhitespace
|
||||
if
|
||||
not ws_cache or
|
||||
cached_settings.show_leading ~= settings.show_leading
|
||||
or cached_settings.show_trailing ~= settings.show_trailing
|
||||
or cached_settings.show_middle ~= settings.show_middle
|
||||
or cached_settings.show_middle_min ~= settings.show_middle_min
|
||||
or cached_settings.color ~= settings.color
|
||||
or cached_settings.leading_color ~= settings.leading_color
|
||||
or cached_settings.middle_color ~= settings.middle_color
|
||||
or cached_settings.trailing_color ~= settings.trailing_color
|
||||
-- we assume that the entire table changes
|
||||
or cached_settings.substitutions ~= settings.substitutions
|
||||
then
|
||||
reset_cache()
|
||||
end
|
||||
end
|
||||
|
||||
-- Move cache to make space for new lines
|
||||
local prev_insert_notify = Highlighter.insert_notify
|
||||
function Highlighter:insert_notify(line, n, ...)
|
||||
prev_insert_notify(self, line, n, ...)
|
||||
if not ws_cache[self] then
|
||||
ws_cache[self] = {}
|
||||
end
|
||||
local to = math.min(line + n, #self.doc.lines)
|
||||
for i=#self.doc.lines+n,to,-1 do
|
||||
ws_cache[self][i] = ws_cache[self][i - n]
|
||||
end
|
||||
for i=line,to do
|
||||
ws_cache[self][i] = nil
|
||||
end
|
||||
end
|
||||
|
||||
-- Close the cache gap created by removed lines
|
||||
local prev_remove_notify = Highlighter.remove_notify
|
||||
function Highlighter:remove_notify(line, n, ...)
|
||||
prev_remove_notify(self, line, n, ...)
|
||||
if not ws_cache[self] then
|
||||
ws_cache[self] = {}
|
||||
end
|
||||
local to = math.max(line + n, #self.doc.lines)
|
||||
for i=line,to do
|
||||
ws_cache[self][i] = ws_cache[self][i + n]
|
||||
end
|
||||
end
|
||||
|
||||
-- Remove changed lines from the cache
|
||||
local prev_update_notify = Highlighter.update_notify
|
||||
function Highlighter:update_notify(line, n, ...)
|
||||
prev_update_notify(self, line, n, ...)
|
||||
if not ws_cache[self] then
|
||||
ws_cache[self] = {}
|
||||
end
|
||||
for i=line,line+n do
|
||||
ws_cache[self][i] = nil
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function get_option(substitution, option)
|
||||
if substitution[option] == nil then
|
||||
return config.plugins.drawwhitespace[option]
|
||||
end
|
||||
return substitution[option]
|
||||
end
|
||||
|
||||
local draw_line_text = DocView.draw_line_text
|
||||
function DocView:draw_line_text(idx, x, y)
|
||||
if
|
||||
not config.plugins.drawwhitespace.enabled
|
||||
or
|
||||
getmetatable(self) ~= DocView
|
||||
then
|
||||
return draw_line_text(self, idx, x, y)
|
||||
end
|
||||
|
||||
local font = (self:get_font() or style.syntax_fonts["whitespace"] or style.syntax_fonts["comment"])
|
||||
local font_size = font:get_size()
|
||||
local _, indent_size = self.doc:get_indent_info()
|
||||
|
||||
reset_cache_if_needed()
|
||||
if
|
||||
not ws_cache[self.doc.highlighter]
|
||||
or ws_cache[self.doc.highlighter].font ~= font
|
||||
or ws_cache[self.doc.highlighter].font_size ~= font_size
|
||||
or ws_cache[self.doc.highlighter].indent_size ~= indent_size
|
||||
then
|
||||
ws_cache[self.doc.highlighter] =
|
||||
setmetatable(
|
||||
{ font = font, font_size = font_size, indent_size = indent_size },
|
||||
{ __mode = "k" }
|
||||
)
|
||||
end
|
||||
|
||||
if not ws_cache[self.doc.highlighter][idx] then -- need to cache line
|
||||
local cache = {}
|
||||
|
||||
local tx
|
||||
local text = self.doc.lines[idx]
|
||||
|
||||
for _, substitution in pairs(config.plugins.drawwhitespace.substitutions) do
|
||||
local char = substitution.char
|
||||
local sub = substitution.sub
|
||||
local offset = 1
|
||||
|
||||
local show_leading = get_option(substitution, "show_leading")
|
||||
local show_middle = get_option(substitution, "show_middle")
|
||||
local show_trailing = get_option(substitution, "show_trailing")
|
||||
|
||||
local show_middle_min = get_option(substitution, "show_middle_min")
|
||||
|
||||
local base_color = get_option(substitution, "color")
|
||||
local leading_color = get_option(substitution, "leading_color") or base_color
|
||||
local middle_color = get_option(substitution, "middle_color") or base_color
|
||||
local trailing_color = get_option(substitution, "trailing_color") or base_color
|
||||
|
||||
local pattern = char.."+"
|
||||
while true do
|
||||
local s, e = text:find(pattern, offset)
|
||||
if not s then break end
|
||||
|
||||
tx = self:get_col_x_offset(idx, s)
|
||||
|
||||
local color = base_color
|
||||
local draw = false
|
||||
|
||||
if e >= #text - 1 then
|
||||
draw = show_trailing
|
||||
color = trailing_color
|
||||
elseif s == 1 then
|
||||
draw = show_leading
|
||||
color = leading_color
|
||||
else
|
||||
draw = show_middle and (e - s + 1 >= show_middle_min)
|
||||
color = middle_color
|
||||
end
|
||||
|
||||
if draw then
|
||||
local last_cache_idx = #cache
|
||||
-- We need to draw tabs one at a time because they might have a
|
||||
-- different size than the substituting character.
|
||||
-- This also applies to any other char if we use non-monospace fonts
|
||||
-- but we ignore this case for now.
|
||||
if char == "\t" then
|
||||
for i = s,e do
|
||||
tx = self:get_col_x_offset(idx, i)
|
||||
cache[last_cache_idx + 1] = sub
|
||||
cache[last_cache_idx + 2] = tx
|
||||
cache[last_cache_idx + 3] = font:get_width(sub)
|
||||
cache[last_cache_idx + 4] = color
|
||||
last_cache_idx = last_cache_idx + 4
|
||||
end
|
||||
else
|
||||
cache[last_cache_idx + 1] = string.rep(sub, e - s + 1)
|
||||
cache[last_cache_idx + 2] = tx
|
||||
cache[last_cache_idx + 3] = font:get_width(cache[last_cache_idx + 1])
|
||||
cache[last_cache_idx + 4] = color
|
||||
end
|
||||
end
|
||||
offset = e + 1
|
||||
end
|
||||
end
|
||||
ws_cache[self.doc.highlighter][idx] = cache
|
||||
end
|
||||
|
||||
-- draw from cache
|
||||
local x1, _, x2, _ = self:get_content_bounds()
|
||||
x1 = x1 + x
|
||||
x2 = x2 + x
|
||||
local ty = y + self:get_line_text_y_offset()
|
||||
local cache = ws_cache[self.doc.highlighter][idx]
|
||||
for i=1,#cache,4 do
|
||||
local tx = cache[i + 1] + x
|
||||
local tw = cache[i + 2]
|
||||
if tx <= x2 then
|
||||
local sub = cache[i]
|
||||
local color = cache[i + 3]
|
||||
if tx + tw >= x1 then
|
||||
tx = renderer.draw_text(font, sub, tx, ty, color)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return draw_line_text(self, idx, x, y)
|
||||
end
|
||||
|
||||
|
||||
command.add(nil, {
|
||||
["draw-whitespace:toggle"] = function()
|
||||
config.plugins.drawwhitespace.enabled = not config.plugins.drawwhitespace.enabled
|
||||
end,
|
||||
|
||||
["draw-whitespace:disable"] = function()
|
||||
config.plugins.drawwhitespace.enabled = false
|
||||
end,
|
||||
|
||||
["draw-whitespace:enable"] = function()
|
||||
config.plugins.drawwhitespace.enabled = true
|
||||
end,
|
||||
})
|
|
@ -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",
|
||||
|
@ -115,7 +59,6 @@ syntax.add {
|
|||
["#if"] = "keyword",
|
||||
["#ifdef"] = "keyword",
|
||||
["#ifndef"] = "keyword",
|
||||
["#elif"] = "keyword",
|
||||
["#else"] = "keyword",
|
||||
["#elseif"] = "keyword",
|
||||
["#endif"] = "keyword",
|
||||
|
|
|
@ -1,109 +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++$",
|
||||
"%.ino$"
|
||||
"%.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+[%x']*", type = "number" },
|
||||
{ pattern = "%d+[%d%.'eE]*f?", type = "number" },
|
||||
{ pattern = "%.?%d+[%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",
|
||||
|
@ -111,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",
|
||||
|
@ -126,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",
|
||||
|
@ -138,6 +62,7 @@ syntax.add {
|
|||
["co_yield"] = "keyword",
|
||||
["decltype"] = "keyword",
|
||||
["delete"] = "keyword",
|
||||
["export"] = "keyword",
|
||||
["friend"] = "keyword",
|
||||
["typeid"] = "keyword",
|
||||
["typename"] = "keyword",
|
||||
|
@ -145,7 +70,6 @@ syntax.add {
|
|||
["override"] = "keyword",
|
||||
["virtual"] = "keyword",
|
||||
["using"] = "keyword",
|
||||
["namespace"] = "keyword",
|
||||
["new"] = "keyword",
|
||||
["noexcept"] = "keyword",
|
||||
["if"] = "keyword",
|
||||
|
@ -159,8 +83,6 @@ syntax.add {
|
|||
["continue"] = "keyword",
|
||||
["return"] = "keyword",
|
||||
["goto"] = "keyword",
|
||||
["struct"] = "keyword",
|
||||
["union"] = "keyword",
|
||||
["typedef"] = "keyword",
|
||||
["enum"] = "keyword",
|
||||
["extern"] = "keyword",
|
||||
|
@ -172,7 +94,8 @@ syntax.add {
|
|||
["case"] = "keyword",
|
||||
["default"] = "keyword",
|
||||
["auto"] = "keyword",
|
||||
["void"] = "keyword2",
|
||||
["const"] = "keyword",
|
||||
["void"] = "keyword",
|
||||
["int"] = "keyword2",
|
||||
["short"] = "keyword2",
|
||||
["long"] = "keyword2",
|
||||
|
@ -181,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",
|
||||
|
@ -200,5 +117,6 @@ syntax.add {
|
|||
["#warning"] = "keyword",
|
||||
["#error"] = "keyword",
|
||||
["#pragma"] = "keyword",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -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" },
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -1,75 +1,22 @@
|
|||
-- mod-version:3
|
||||
-- mod-version:1 -- lite-xl 1.16
|
||||
local syntax = require "core.syntax"
|
||||
|
||||
-- Regex pattern explanation:
|
||||
-- This will match / and will look ahead for something that looks like a regex.
|
||||
--
|
||||
-- (?!/) Don't match empty regexes.
|
||||
--
|
||||
-- (?>...) this is using an atomic group to minimize backtracking, as that'd
|
||||
-- cause "Catastrophic Backtracking" in some cases.
|
||||
--
|
||||
-- [^\\[\/]++ will match anything that's isn't an escape, a start of character
|
||||
-- class or an end of pattern, without backtracking (the second +).
|
||||
--
|
||||
-- \\. will match anything that's escaped.
|
||||
--
|
||||
-- \[(?:[^\\\]++]|\\.)*+\] will match character classes.
|
||||
--
|
||||
-- /[gmiyuvsd]*\s*[\n,;\)\]\}\.]) will match the end of pattern delimiter, optionally
|
||||
-- followed by pattern options, and anything that can
|
||||
-- be after a pattern.
|
||||
--
|
||||
-- Demo with some unit tests (click on the Unit Tests entry): https://regex101.com/r/Vx5L5V/1
|
||||
-- Note that it has a couple of changes to make it work on that platform.
|
||||
local regex_pattern = {
|
||||
[=[\/(?=(?!\/)(?:(?>[^\\[\/]++|\\.|\[(?:[^\\\]]++|\\.)*+\])*+)++\/[gmiyuvsd]*\s*(?:[\n,;\)\]\}\.]|\/[\/*]))()]=],
|
||||
"/()[gmiyuvsd]*", "\\"
|
||||
}
|
||||
|
||||
-- For the moment let's not actually differentiate the insides of the regex,
|
||||
-- as this will need new token types...
|
||||
local inner_regex_syntax = {
|
||||
patterns = {
|
||||
{ pattern = "%(()%?[:!=><]", type = { "string", "string" } },
|
||||
{ pattern = "[.?+*%(%)|]", type = "string" },
|
||||
{ pattern = "{%d*,?%d*}", type = "string" },
|
||||
{ regex = { [=[\[()\^?]=], [=[(?:\]|(?=\n))()]=], "\\" },
|
||||
type = { "string", "string" },
|
||||
syntax = { -- Inside character class
|
||||
patterns = {
|
||||
{ pattern = "\\\\", type = "string" },
|
||||
{ pattern = "\\%]", type = "string" },
|
||||
{ pattern = "[^%]\n]", type = "string" }
|
||||
},
|
||||
symbols = {}
|
||||
}
|
||||
},
|
||||
{ regex = "\\/", type = "string" },
|
||||
{ regex = "[^/\n]", type = "string" },
|
||||
},
|
||||
symbols = {}
|
||||
}
|
||||
|
||||
syntax.add {
|
||||
name = "JavaScript",
|
||||
files = { "%.js$", "%.json$", "%.cson$", "%.mjs$", "%.cjs$" },
|
||||
files = { "%.js$", "%.json$", "%.cson$" },
|
||||
comment = "//",
|
||||
block_comment = { "/*", "*/" },
|
||||
patterns = {
|
||||
{ pattern = "//.*", type = "comment" },
|
||||
{ pattern = { "/%*", "%*/" }, type = "comment" },
|
||||
{ regex = regex_pattern, syntax = inner_regex_syntax, type = {"string", "string"} },
|
||||
{ pattern = { '"', '"', '\\' }, type = "string" },
|
||||
{ pattern = { "'", "'", '\\' }, type = "string" },
|
||||
{ pattern = { "`", "`", '\\' }, type = "string" },
|
||||
-- Use (?:\/(?!\/|\*))? to avoid that a regex can start after a number, while also allowing // and /* comments
|
||||
{ regex = [[-?0[xXbBoO][\da-fA-F_]+n?()\s*()(?:\/(?!\/|\*))?]], type = {"number", "normal", "operator"} },
|
||||
{ regex = [[-?\d+[0-9.eE_n]*()\s*()(?:\/(?!\/|\*))?]], type = {"number", "normal", "operator"} },
|
||||
{ regex = [[-?\.?\d+()\s*()(?:\/(?!\/|\*))?]], type = {"number", "normal", "operator"} },
|
||||
{ pattern = "[%+%-=/%*%^%%<>!~|&]", type = "operator" },
|
||||
{ pattern = "[%a_][%w_]*%f[(]", type = "function" },
|
||||
{ pattern = "[%a_][%w_]*", type = "symbol" },
|
||||
{ pattern = "//.-\n", type = "comment" },
|
||||
{ pattern = { "/%*", "%*/" }, type = "comment" },
|
||||
{ pattern = { '/%g', '/', '\\' }, type = "string" },
|
||||
{ pattern = { '"', '"', '\\' }, type = "string" },
|
||||
{ pattern = { "'", "'", '\\' }, 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 = {
|
||||
["async"] = "keyword",
|
||||
|
@ -93,7 +40,6 @@ syntax.add {
|
|||
["get"] = "keyword",
|
||||
["if"] = "keyword",
|
||||
["import"] = "keyword",
|
||||
["from"] = "keyword",
|
||||
["in"] = "keyword",
|
||||
["of"] = "keyword",
|
||||
["instanceof"] = "keyword",
|
||||
|
|
|
@ -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" },
|
||||
|
|
|
@ -1,268 +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 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___", "___" }, type = "markdown_bold_italic" },
|
||||
{ pattern = { "%s__", "__" }, type = "markdown_bold" },
|
||||
{ pattern = { "%s_[%S]", "_" }, type = "markdown_italic" },
|
||||
-- reference links
|
||||
{
|
||||
pattern = "^%s*%[%^()["..in_squares_match.."]+()%]: ",
|
||||
type = { "function", "number", "function" }
|
||||
},
|
||||
{
|
||||
pattern = "^%s*%[%^?()["..in_squares_match.."]+()%]:%s+.*",
|
||||
type = { "function", "number", "function" }
|
||||
},
|
||||
-- optimization
|
||||
{ pattern = "%s+", type = "normal" },
|
||||
|
||||
---- HTML rules imported and adapted from language_html
|
||||
---- to not conflict with markdown rules
|
||||
-- Inline JS and CSS
|
||||
{
|
||||
pattern = {
|
||||
"<%s*[sS][cC][rR][iI][pP][tT]%s+[tT][yY][pP][eE]%s*=%s*" ..
|
||||
"['\"]%a+/[jJ][aA][vV][aA][sS][cC][rR][iI][pP][tT]['\"]%s*>",
|
||||
"<%s*/[sS][cC][rR][iI][pP][tT]>"
|
||||
},
|
||||
syntax = ".js",
|
||||
type = "function"
|
||||
},
|
||||
{
|
||||
pattern = {
|
||||
"<%s*[sS][cC][rR][iI][pP][tT]%s*>",
|
||||
"<%s*/%s*[sS][cC][rR][iI][pP][tT]>"
|
||||
},
|
||||
syntax = ".js",
|
||||
type = "function"
|
||||
},
|
||||
{
|
||||
pattern = {
|
||||
"<%s*[sS][tT][yY][lL][eE][^>]*>",
|
||||
"<%s*/%s*[sS][tT][yY][lL][eE]%s*>"
|
||||
},
|
||||
syntax = ".css",
|
||||
type = "function"
|
||||
},
|
||||
-- Comments
|
||||
{ pattern = { "<!%-%-", "%-%->" }, type = "comment" },
|
||||
-- Tags
|
||||
{ pattern = "%f[^<]![%a_][%w_]*", type = "keyword2" },
|
||||
{ pattern = "%f[^<][%a_][%w_]*", type = "function" },
|
||||
{ pattern = "%f[^<]/[%a_][%w_]*", type = "function" },
|
||||
-- Attributes
|
||||
{
|
||||
pattern = "[a-z%-]+%s*()=%s*()\".-\"",
|
||||
type = { "keyword", "operator", "string" }
|
||||
},
|
||||
{
|
||||
pattern = "[a-z%-]+%s*()=%s*()'.-'",
|
||||
type = { "keyword", "operator", "string" }
|
||||
},
|
||||
{
|
||||
pattern = "[a-z%-]+%s*()=%s*()%-?%d[%d%.]*",
|
||||
type = { "keyword", "operator", "number" }
|
||||
},
|
||||
-- Entities
|
||||
{ pattern = "&#?[a-zA-Z0-9]+;", type = "keyword2" },
|
||||
|
||||
---- Markdown rules
|
||||
-- math
|
||||
{ pattern = { "%$%$", "%$%$", "\\" }, type = "string", syntax = ".tex"},
|
||||
{ regex = { "\\$", [[\$|(?=\\*\n)]], "\\" }, type = "string", syntax = ".tex"},
|
||||
-- code blocks
|
||||
{ pattern = { "```caddyfile", "```" }, type = "string", syntax = "Caddyfile" },
|
||||
{ pattern = { "```c++", "```" }, type = "string", syntax = ".cpp" },
|
||||
{ pattern = { "```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" },
|
||||
-- lines
|
||||
{ pattern = "^%-%-%-+\n" , type = "comment" },
|
||||
{ pattern = "^%*%*%*+\n", type = "comment" },
|
||||
{ pattern = "^___+\n", type = "comment" },
|
||||
{ pattern = "^===+\n", type = "comment" },
|
||||
-- strike
|
||||
{ pattern = { "~~", "~~" }, type = "keyword2" },
|
||||
-- highlight
|
||||
{ pattern = { "==", "==" }, type = "literal" },
|
||||
-- bold and italic
|
||||
{ pattern = { "%*%*%*%S", "%*%*%*" }, type = "markdown_bold_italic" },
|
||||
{ pattern = { "%*%*%S", "%*%*" }, type = "markdown_bold" },
|
||||
-- handle edge case where asterisk can be at end of line and not close
|
||||
{
|
||||
pattern = { "%f[\\%*]%*[%S]", "%*%f[^%*]" },
|
||||
type = "markdown_italic"
|
||||
},
|
||||
-- alternative bold italic formats
|
||||
{ pattern = "^___[%s%p%w]+___" , type = "markdown_bold_italic" },
|
||||
{ pattern = "^__[%s%p%w]+__" , type = "markdown_bold" },
|
||||
{ pattern = "^_[%s%p%w]+_" , type = "markdown_italic" },
|
||||
-- heading with custom id
|
||||
{
|
||||
pattern = "^#+%s[%w%s%p]+(){()#[%w%-]+()}",
|
||||
type = { "keyword", "function", "string", "function" }
|
||||
},
|
||||
-- headings
|
||||
{ pattern = "^#+%s.+\n", type = "keyword" },
|
||||
-- superscript and subscript
|
||||
{
|
||||
pattern = "%^()%d+()%^",
|
||||
type = { "function", "number", "function" }
|
||||
},
|
||||
{
|
||||
pattern = "%~()%d+()%~",
|
||||
type = { "function", "number", "function" }
|
||||
},
|
||||
-- definitions
|
||||
{ pattern = "^:%s.+", type = "function" },
|
||||
-- emoji
|
||||
{ pattern = ":[a-zA-Z0-9_%-]+:", type = "literal" },
|
||||
-- images and link
|
||||
{
|
||||
pattern = "!?%[!?%[()["..in_squares_match.."]+()%]%(()["..in_parenthesis_match.."]+()%)%]%(()["..in_parenthesis_match.."]+()%)",
|
||||
type = { "function", "string", "function", "number", "function", "number", "function" }
|
||||
},
|
||||
{
|
||||
pattern = "!?%[!?%[?()["..in_squares_match.."]+()%]?%]%(()["..in_parenthesis_match.."]+()%)",
|
||||
type = { "function", "string", "function", "number", "function" }
|
||||
},
|
||||
-- reference links
|
||||
{
|
||||
pattern = "%[()["..in_squares_match.."]+()%] *()%[()["..in_squares_match.."]+()%]",
|
||||
type = { "function", "string", "function", "function", "number", "function" }
|
||||
},
|
||||
{
|
||||
pattern = "!?%[%^?()["..in_squares_match.."]+()%]",
|
||||
type = { "function", "number", "function" }
|
||||
},
|
||||
-- url's and email
|
||||
{
|
||||
pattern = "<[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+%.[a-zA-Z0-9-.]+>",
|
||||
type = "function"
|
||||
},
|
||||
{ pattern = "<https?://%S+>", type = "function" },
|
||||
{ pattern = "https?://%S+", type = "function" },
|
||||
-- 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()
|
||||
local custom_fonts = { bold = {font = nil, color = nil}, italic = {}, bold_italic = {} }
|
||||
local initial_color
|
||||
local last_code_font
|
||||
|
||||
local function set_font(attr)
|
||||
local attributes = {}
|
||||
if attr ~= "bold_italic" then
|
||||
attributes[attr] = true
|
||||
else
|
||||
attributes["bold"] = true
|
||||
attributes["italic"] = true
|
||||
end
|
||||
local font = style.code_font:copy(
|
||||
style.code_font:get_size(),
|
||||
attributes
|
||||
)
|
||||
custom_fonts[attr].font = font
|
||||
style.syntax_fonts["markdown_"..attr] = font
|
||||
end
|
||||
|
||||
local function set_color(attr)
|
||||
custom_fonts[attr].color = style.syntax["keyword2"]
|
||||
style.syntax["markdown_"..attr] = style.syntax["keyword2"]
|
||||
end
|
||||
|
||||
-- Add 3 type of font styles for use on markdown files
|
||||
for attr, _ in pairs(custom_fonts) do
|
||||
-- Only set it if the font wasn't manually customized
|
||||
if not style.syntax_fonts["markdown_"..attr] then
|
||||
set_font(attr)
|
||||
end
|
||||
|
||||
-- Only set it if the color wasn't manually customized
|
||||
if not style.syntax["markdown_"..attr] then
|
||||
set_color(attr)
|
||||
end
|
||||
end
|
||||
|
||||
while true do
|
||||
if last_code_font ~= style.code_font then
|
||||
last_code_font = style.code_font
|
||||
for attr, _ in pairs(custom_fonts) do
|
||||
-- Only set it if the font wasn't manually customized
|
||||
if style.syntax_fonts["markdown_"..attr] == custom_fonts[attr].font then
|
||||
set_font(attr)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if initial_color ~= style.syntax["keyword2"] then
|
||||
initial_color = style.syntax["keyword2"]
|
||||
for attr, _ in pairs(custom_fonts) do
|
||||
-- Only set it if the color wasn't manually customized
|
||||
if style.syntax["markdown_"..attr] == custom_fonts[attr].color then
|
||||
set_color(attr)
|
||||
end
|
||||
end
|
||||
end
|
||||
coroutine.yield(1)
|
||||
end
|
||||
end)
|
||||
|
|
|
@ -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$", "%.pyi$" },
|
||||
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 = "-?0[xboXBO][%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",
|
||||
|
|
|
@ -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" },
|
||||
|
|
|
@ -1,115 +1,21 @@
|
|||
-- mod-version:3
|
||||
local common = require "core.common"
|
||||
local command = require "core.command"
|
||||
-- mod-version:1 -- lite-xl 1.16
|
||||
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(...)
|
||||
if
|
||||
type(config.plugins.lineguide) == "table"
|
||||
and
|
||||
config.plugins.lineguide.enabled
|
||||
and
|
||||
self:is(DocView)
|
||||
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
|
||||
local ns = ("n"):rep(config.line_limit)
|
||||
local ss = self:get_font():subpixel_scale()
|
||||
local offset = self:get_font():get_width_subpixel(ns) / ss
|
||||
local x = self:get_line_screen_position(1) + offset
|
||||
local y = self.position.y
|
||||
local w = math.ceil(SCALE * 1)
|
||||
local h = self.size.y
|
||||
|
||||
for k,v in ipairs(config.plugins.lineguide.rulers) do
|
||||
local ruler = get_ruler(v)
|
||||
local color = style.guide or style.selection
|
||||
renderer.draw_rect(x, y, w, h, color)
|
||||
|
||||
if ruler then
|
||||
local x = line_x + (character_width * ruler.columns)
|
||||
local y = self.position.y
|
||||
local w = ruler_width
|
||||
local h = self.size.y
|
||||
|
||||
renderer.draw_rect(x, y, w, h, ruler.color or ruler_color)
|
||||
end
|
||||
end
|
||||
end
|
||||
-- everything else like the cursor above the line guides
|
||||
draw_overlay(self, ...)
|
||||
end
|
||||
|
||||
command.add(nil, {
|
||||
["lineguide:toggle"] = function()
|
||||
config.plugins.lineguide.enabled = not config.plugins.lineguide.enabled
|
||||
end
|
||||
})
|
||||
|
|
|
@ -1,600 +0,0 @@
|
|||
-- mod-version:3 --priority:10
|
||||
local core = require "core"
|
||||
local common = require "core.common"
|
||||
local DocView = require "core.docview"
|
||||
local Doc = require "core.doc"
|
||||
local style = require "core.style"
|
||||
local config = require "core.config"
|
||||
local command = require "core.command"
|
||||
local keymap = require "core.keymap"
|
||||
local translate = require "core.doc.translate"
|
||||
|
||||
|
||||
config.plugins.linewrapping = common.merge({
|
||||
-- The type of wrapping to perform. Can be "letter" or "word".
|
||||
mode = "letter",
|
||||
-- If nil, uses the DocView's size, otherwise, uses this exact width. Can be a function.
|
||||
width_override = nil,
|
||||
-- Whether or not to draw a guide
|
||||
guide = true,
|
||||
-- Whether or not we should indent ourselves like the first line of a wrapped block.
|
||||
indent = true,
|
||||
-- Whether or not to enable wrapping by default when opening files.
|
||||
enable_by_default = false,
|
||||
-- Requires tokenization
|
||||
require_tokenization = false,
|
||||
-- The config specification used by gui generators
|
||||
config_spec = {
|
||||
name = "Line Wrapping",
|
||||
{
|
||||
label = "Mode",
|
||||
description = "The type of wrapping to perform.",
|
||||
path = "mode",
|
||||
type = "selection",
|
||||
default = "letter",
|
||||
values = {
|
||||
{"Letters", "letter"},
|
||||
{"Words", "word"}
|
||||
}
|
||||
},
|
||||
{
|
||||
label = "Guide",
|
||||
description = "Whether or not to draw a guide.",
|
||||
path = "guide",
|
||||
type = "toggle",
|
||||
default = true
|
||||
},
|
||||
{
|
||||
label = "Indent",
|
||||
description = "Whether or not to follow the indentation of wrapped line.",
|
||||
path = "indent",
|
||||
type = "toggle",
|
||||
default = true
|
||||
},
|
||||
{
|
||||
label = "Enable by Default",
|
||||
description = "Whether or not to enable wrapping by default when opening files.",
|
||||
path = "enable_by_default",
|
||||
type = "toggle",
|
||||
default = false
|
||||
},
|
||||
{
|
||||
label = "Require Tokenization",
|
||||
description = "Use tokenization when applying wrapping.",
|
||||
path = "require_tokenization",
|
||||
type = "toggle",
|
||||
default = false
|
||||
}
|
||||
}
|
||||
}, config.plugins.linewrapping)
|
||||
|
||||
local LineWrapping = {}
|
||||
|
||||
-- Optimzation function. The tokenizer is relatively slow (at present), and
|
||||
-- so if we don't need to run it, should be run sparingly.
|
||||
local function spew_tokens(doc, line) if line < math.huge then return math.huge, "normal", doc.lines[line] end end
|
||||
local function get_tokens(doc, line)
|
||||
if config.plugins.linewrapping.require_tokenization then
|
||||
return doc.highlighter:each_token(line)
|
||||
end
|
||||
return spew_tokens, doc, line
|
||||
end
|
||||
|
||||
-- Computes the breaks for a given line, width and mode. Returns a list of columns
|
||||
-- at which the line should be broken.
|
||||
function LineWrapping.compute_line_breaks(doc, default_font, line, width, mode)
|
||||
local xoffset, last_i, i, last_space, last_width, begin_width = 0, 1, 1, nil, 0, 0
|
||||
local splits = { 1 }
|
||||
for idx, type, text in get_tokens(doc, line) do
|
||||
local font = style.syntax_fonts[type] or default_font
|
||||
if idx == 1 or idx == math.huge and config.plugins.linewrapping.indent then
|
||||
local _, indent_end = text:find("^%s+")
|
||||
if indent_end then begin_width = font:get_width(text:sub(1, indent_end)) end
|
||||
end
|
||||
local w = font:get_width(text)
|
||||
if xoffset + w > width then
|
||||
for char in common.utf8_chars(text) do
|
||||
w = font:get_width(char)
|
||||
xoffset = xoffset + w
|
||||
if xoffset > width then
|
||||
if mode == "word" and last_space then
|
||||
table.insert(splits, last_space + 1)
|
||||
xoffset = w + begin_width + (xoffset - last_width)
|
||||
else
|
||||
table.insert(splits, i)
|
||||
xoffset = w + begin_width
|
||||
end
|
||||
last_space = nil
|
||||
elseif char == ' ' then
|
||||
last_space = i
|
||||
last_width = xoffset
|
||||
end
|
||||
i = i + #char
|
||||
end
|
||||
else
|
||||
xoffset = xoffset + w
|
||||
i = i + #text
|
||||
end
|
||||
end
|
||||
return splits, begin_width
|
||||
end
|
||||
|
||||
-- breaks are held in a single table that contains n*2 elements, where n is the amount of line breaks.
|
||||
-- each element represents line and column of the break. line_offset will check from the specified line
|
||||
-- if the first line has not changed breaks, it will stop there.
|
||||
function LineWrapping.reconstruct_breaks(docview, default_font, width, line_offset)
|
||||
if width ~= math.huge then
|
||||
local doc = docview.doc
|
||||
-- two elements per wrapped line; first maps to original line number, second to column number.
|
||||
docview.wrapped_lines = { }
|
||||
-- one element per actual line; maps to the first index of in wrapped_lines for this line
|
||||
docview.wrapped_line_to_idx = { }
|
||||
-- one element per actual line; gives the indent width for the acutal line
|
||||
docview.wrapped_line_offsets = { }
|
||||
docview.wrapped_settings = { ["width"] = width, ["font"] = default_font }
|
||||
for i = line_offset or 1, #doc.lines do
|
||||
local breaks, offset = LineWrapping.compute_line_breaks(doc, default_font, i, width, config.plugins.linewrapping.mode)
|
||||
table.insert(docview.wrapped_line_offsets, offset)
|
||||
for k, col in ipairs(breaks) do
|
||||
table.insert(docview.wrapped_lines, i)
|
||||
table.insert(docview.wrapped_lines, col)
|
||||
end
|
||||
end
|
||||
-- list of indices for wrapped_lines, that are based on original line number
|
||||
-- holds the index to the first in the wrapped_lines list.
|
||||
local last_wrap = nil
|
||||
for i = 1, #docview.wrapped_lines, 2 do
|
||||
if not last_wrap or last_wrap ~= docview.wrapped_lines[i] then
|
||||
table.insert(docview.wrapped_line_to_idx, (i + 1) / 2)
|
||||
last_wrap = docview.wrapped_lines[i]
|
||||
end
|
||||
end
|
||||
else
|
||||
docview.wrapped_lines = nil
|
||||
docview.wrapped_line_to_idx = nil
|
||||
docview.wrapped_line_offsets = nil
|
||||
docview.wrapped_settings = nil
|
||||
end
|
||||
end
|
||||
|
||||
-- When we have an insertion or deletion, we have four sections of text.
|
||||
-- 1. The unaffected section, located prior to the cursor. This is completely ignored.
|
||||
-- 2. The beginning of the affected line prior to the insertion or deletion. Begins on column 1 of the selection.
|
||||
-- 3. The removed/pasted lines.
|
||||
-- 4. Every line after the modification, begins one line after the selection in the initial document.
|
||||
function LineWrapping.update_breaks(docview, old_line1, old_line2, net_lines)
|
||||
-- Step 1: Determine the index for the line for #2.
|
||||
local old_idx1 = docview.wrapped_line_to_idx[old_line1] or 1
|
||||
-- Step 2: Determine the index of the line for #4.
|
||||
local old_idx2 = (docview.wrapped_line_to_idx[old_line2 + 1] or ((#docview.wrapped_lines / 2) + 1)) - 1
|
||||
-- Step 3: Remove all old breaks for the old lines from the table, and all old widths from wrapped_line_offsets.
|
||||
local offset = (old_idx1 - 1) * 2 + 1
|
||||
for i = old_idx1, old_idx2 do
|
||||
table.remove(docview.wrapped_lines, offset)
|
||||
table.remove(docview.wrapped_lines, offset)
|
||||
end
|
||||
for i = old_line1, old_line2 do
|
||||
table.remove(docview.wrapped_line_offsets, old_line1)
|
||||
end
|
||||
-- Step 4: Shift the line number of wrapped_lines past #4 by the amount of inserted/deleted lines.
|
||||
if net_lines ~= 0 then
|
||||
for i = offset, #docview.wrapped_lines, 2 do
|
||||
docview.wrapped_lines[i] = docview.wrapped_lines[i] + net_lines
|
||||
end
|
||||
end
|
||||
-- Step 5: Compute the breaks and offsets for the lines for #2 and #3. Insert them into the table.
|
||||
local new_line1 = old_line1
|
||||
local new_line2 = old_line2 + net_lines
|
||||
for line = new_line1, new_line2 do
|
||||
local breaks, begin_width = LineWrapping.compute_line_breaks(docview.doc, docview.wrapped_settings.font, line, docview.wrapped_settings.width, config.plugins.linewrapping.mode)
|
||||
table.insert(docview.wrapped_line_offsets, line, begin_width)
|
||||
for i,b in ipairs(breaks) do
|
||||
table.insert(docview.wrapped_lines, offset, b)
|
||||
table.insert(docview.wrapped_lines, offset, line)
|
||||
offset = offset + 2
|
||||
end
|
||||
end
|
||||
-- Step 6: Recompute the wrapped_line_to_idx cache from #2.
|
||||
local line = old_line1
|
||||
offset = (old_idx1 - 1) * 2 + 1
|
||||
while offset < #docview.wrapped_lines do
|
||||
if docview.wrapped_lines[offset + 1] == 1 then
|
||||
docview.wrapped_line_to_idx[line] = ((offset - 1) / 2) + 1
|
||||
line = line + 1
|
||||
end
|
||||
offset = offset + 2
|
||||
end
|
||||
while line <= #docview.wrapped_line_to_idx do
|
||||
table.remove(docview.wrapped_line_to_idx)
|
||||
end
|
||||
end
|
||||
|
||||
-- Draws a guide if applicable to show where wrapping is occurring.
|
||||
function LineWrapping.draw_guide(docview)
|
||||
if config.plugins.linewrapping.guide and docview.wrapped_settings.width ~= math.huge then
|
||||
local x, y = docview:get_content_offset()
|
||||
local gw = docview:get_gutter_width()
|
||||
renderer.draw_rect(x + gw + docview.wrapped_settings.width, y, 1, core.root_view.size.y, style.selection)
|
||||
end
|
||||
end
|
||||
|
||||
function LineWrapping.update_docview_breaks(docview)
|
||||
local w = docview.v_scrollbar.expanded_size or style.expanded_scrollbar_size
|
||||
local width = (type(config.plugins.linewrapping.width_override) == "function" and config.plugins.linewrapping.width_override(docview))
|
||||
or config.plugins.linewrapping.width_override or (docview.size.x - docview:get_gutter_width() - w)
|
||||
if (not docview.wrapped_settings or docview.wrapped_settings.width == nil or width ~= docview.wrapped_settings.width) then
|
||||
docview.scroll.to.x = 0
|
||||
LineWrapping.reconstruct_breaks(docview, docview:get_font(), width)
|
||||
end
|
||||
end
|
||||
|
||||
local function get_idx_line_col(docview, idx)
|
||||
local doc = docview.doc
|
||||
if not docview.wrapped_settings then
|
||||
if idx > #doc.lines then return #doc.lines, #doc.lines[#doc.lines] + 1 end
|
||||
return idx, 1
|
||||
end
|
||||
if idx < 1 then return 1, 1 end
|
||||
local offset = (idx - 1) * 2 + 1
|
||||
if offset > #docview.wrapped_lines then return #doc.lines, #doc.lines[#doc.lines] + 1 end
|
||||
return docview.wrapped_lines[offset], docview.wrapped_lines[offset + 1]
|
||||
end
|
||||
|
||||
local function get_idx_line_length(docview, idx)
|
||||
local doc = docview.doc
|
||||
if not docview.wrapped_settings then
|
||||
if idx > #doc.lines then return #doc.lines[#doc.lines] + 1 end
|
||||
return #doc.lines[idx]
|
||||
end
|
||||
local offset = (idx - 1) * 2 + 1
|
||||
local start = docview.wrapped_lines[offset + 1]
|
||||
if docview.wrapped_lines[offset + 2] and docview.wrapped_lines[offset + 2] == docview.wrapped_lines[offset] then
|
||||
return docview.wrapped_lines[offset + 3] - docview.wrapped_lines[offset + 1]
|
||||
else
|
||||
return #doc.lines[docview.wrapped_lines[offset]] - docview.wrapped_lines[offset + 1] + 1
|
||||
end
|
||||
end
|
||||
|
||||
local function get_total_wrapped_lines(docview)
|
||||
if not docview.wrapped_settings then return docview.doc and #docview.doc.lines end
|
||||
return #docview.wrapped_lines / 2
|
||||
end
|
||||
|
||||
-- If line end, gives the end of an index line, rather than the first character of the next line.
|
||||
local function get_line_idx_col_count(docview, line, col, line_end, ndoc)
|
||||
local doc = docview.doc
|
||||
if not docview.wrapped_settings then return common.clamp(line, 1, #doc.lines), col, 1, 1 end
|
||||
if line > #doc.lines then return get_line_idx_col_count(docview, #doc.lines, #doc.lines[#doc.lines] + 1) end
|
||||
line = math.max(line, 1)
|
||||
local idx = docview.wrapped_line_to_idx[line] or 1
|
||||
local ncol, scol = 1, 1
|
||||
if col then
|
||||
local i = idx + 1
|
||||
while line == docview.wrapped_lines[(i - 1) * 2 + 1] and col >= docview.wrapped_lines[(i - 1) * 2 + 2] do
|
||||
local nscol = docview.wrapped_lines[(i - 1) * 2 + 2]
|
||||
if line_end and col == nscol then
|
||||
break
|
||||
end
|
||||
scol = nscol
|
||||
i = i + 1
|
||||
idx = idx + 1
|
||||
end
|
||||
ncol = (col - scol) + 1
|
||||
end
|
||||
local count = (docview.wrapped_line_to_idx[line + 1] or (get_total_wrapped_lines(docview) + 1)) - (docview.wrapped_line_to_idx[line] or get_total_wrapped_lines(docview))
|
||||
return idx, ncol, count, scol
|
||||
end
|
||||
|
||||
local function get_line_col_from_index_and_x(docview, idx, x)
|
||||
local doc = docview.doc
|
||||
local line, col = get_idx_line_col(docview, idx)
|
||||
if idx < 1 then return 1, 1 end
|
||||
local xoffset, last_i, i = (col ~= 1 and docview.wrapped_line_offsets[line] or 0), col, 1
|
||||
if x < xoffset then return line, col end
|
||||
local default_font = docview:get_font()
|
||||
for _, type, text in doc.highlighter:each_token(line) do
|
||||
local font, w = style.syntax_fonts[type] or default_font, 0
|
||||
for char in common.utf8_chars(text) do
|
||||
if i >= col then
|
||||
if xoffset >= x then
|
||||
return line, (xoffset - x > (w / 2) and last_i or i)
|
||||
end
|
||||
w = font:get_width(char)
|
||||
xoffset = xoffset + w
|
||||
end
|
||||
last_i = i
|
||||
i = i + #char
|
||||
end
|
||||
end
|
||||
return line, #doc.lines[line]
|
||||
end
|
||||
|
||||
|
||||
local open_files = setmetatable({ }, { __mode = "k" })
|
||||
|
||||
local old_doc_insert = Doc.raw_insert
|
||||
function Doc:raw_insert(line, col, text, undo_stack, time)
|
||||
local old_lines = #self.lines
|
||||
old_doc_insert(self, line, col, text, undo_stack, time)
|
||||
if open_files[self] then
|
||||
for i,docview in ipairs(open_files[self]) do
|
||||
if docview.wrapped_settings then
|
||||
local lines = #self.lines - old_lines
|
||||
LineWrapping.update_breaks(docview, line, line, lines)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local old_doc_remove = Doc.raw_remove
|
||||
function Doc:raw_remove(line1, col1, line2, col2, undo_stack, time)
|
||||
local old_lines = #self.lines
|
||||
old_doc_remove(self, line1, col1, line2, col2, undo_stack, time)
|
||||
if open_files[self] then
|
||||
for i,docview in ipairs(open_files[self]) do
|
||||
if docview.wrapped_settings then
|
||||
local lines = #self.lines - old_lines
|
||||
LineWrapping.update_breaks(docview, line1, line2, lines)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local old_doc_update = DocView.update
|
||||
function DocView:update()
|
||||
old_doc_update(self)
|
||||
if self.wrapped_settings and self.size.x > 0 then
|
||||
LineWrapping.update_docview_breaks(self)
|
||||
end
|
||||
end
|
||||
|
||||
function DocView:get_scrollable_size()
|
||||
if not config.scroll_past_end then
|
||||
return self:get_line_height() * get_total_wrapped_lines(self) + style.padding.y * 2
|
||||
end
|
||||
return self:get_line_height() * (get_total_wrapped_lines(self) - 1) + self.size.y
|
||||
end
|
||||
|
||||
local old_get_h_scrollable_size = DocView.get_h_scrollable_size
|
||||
function DocView:get_h_scrollable_size(...)
|
||||
if self.wrapping_enabled then return 0 end
|
||||
return old_get_h_scrollable_size(self, ...)
|
||||
end
|
||||
|
||||
local old_new = DocView.new
|
||||
function DocView:new(doc)
|
||||
old_new(self, doc)
|
||||
if not open_files[doc] then open_files[doc] = {} end
|
||||
table.insert(open_files[doc], self)
|
||||
if config.plugins.linewrapping.enable_by_default then
|
||||
self.wrapping_enabled = true
|
||||
LineWrapping.update_docview_breaks(self)
|
||||
else
|
||||
self.wrapping_enabled = false
|
||||
end
|
||||
end
|
||||
|
||||
local old_scroll_to_line = DocView.scroll_to_line
|
||||
function DocView:scroll_to_line(...)
|
||||
if self.wrapping_enabled then LineWrapping.update_docview_breaks(self) end
|
||||
old_scroll_to_line(self, ...)
|
||||
end
|
||||
|
||||
local old_scroll_to_make_visible = DocView.scroll_to_make_visible
|
||||
function DocView:scroll_to_make_visible(line, col)
|
||||
if self.wrapping_enabled then LineWrapping.update_docview_breaks(self) end
|
||||
old_scroll_to_make_visible(self, line, col)
|
||||
if self.wrapped_settings then self.scroll.to.x = 0 end
|
||||
end
|
||||
|
||||
local old_get_visible_line_range = DocView.get_visible_line_range
|
||||
function DocView:get_visible_line_range()
|
||||
if not self.wrapped_settings then return old_get_visible_line_range(self) end
|
||||
local x, y, x2, y2 = self:get_content_bounds()
|
||||
local lh = self:get_line_height()
|
||||
local minline = get_idx_line_col(self, math.max(1, math.floor(y / lh)))
|
||||
local maxline = get_idx_line_col(self, math.min(get_total_wrapped_lines(self), math.floor(y2 / lh) + 1))
|
||||
return minline, maxline
|
||||
end
|
||||
|
||||
local old_get_x_offset_col = DocView.get_x_offset_col
|
||||
function DocView:get_x_offset_col(line, x)
|
||||
if not self.wrapped_settings then return old_get_x_offset_col(self, line, x) end
|
||||
local idx = get_line_idx_col_count(self, line)
|
||||
return get_line_col_from_index_and_x(self, idx, x)
|
||||
end
|
||||
|
||||
-- If line end is true, returns the end of the previous line, in a multi-line break.
|
||||
local old_get_col_x_offset = DocView.get_col_x_offset
|
||||
function DocView:get_col_x_offset(line, col, line_end)
|
||||
if not self.wrapped_settings then return old_get_col_x_offset(self, line, col) end
|
||||
local idx, ncol, count, scol = get_line_idx_col_count(self, line, col, line_end)
|
||||
local xoffset, i = (scol ~= 1 and self.wrapped_line_offsets[line] or 0), 1
|
||||
local default_font = self:get_font()
|
||||
for _, type, text in self.doc.highlighter:each_token(line) do
|
||||
if i + #text >= scol then
|
||||
if i < scol then
|
||||
text = text:sub(scol - i + 1)
|
||||
i = scol
|
||||
end
|
||||
local font = style.syntax_fonts[type] or default_font
|
||||
for char in common.utf8_chars(text) do
|
||||
if i >= col then
|
||||
return xoffset
|
||||
end
|
||||
xoffset = xoffset + font:get_width(char)
|
||||
i = i + #char
|
||||
end
|
||||
else
|
||||
i = i + #text
|
||||
end
|
||||
end
|
||||
return xoffset
|
||||
end
|
||||
|
||||
local old_get_line_screen_position = DocView.get_line_screen_position
|
||||
function DocView:get_line_screen_position(line, col)
|
||||
if not self.wrapped_settings then return old_get_line_screen_position(self, line, col) end
|
||||
local idx, ncol, count = get_line_idx_col_count(self, line, col)
|
||||
local x, y = self:get_content_offset()
|
||||
local lh = self:get_line_height()
|
||||
local gw = self:get_gutter_width()
|
||||
return x + gw + (col and self:get_col_x_offset(line, col) or 0), y + (idx-1) * lh + style.padding.y
|
||||
end
|
||||
|
||||
local old_resolve_screen_position = DocView.resolve_screen_position
|
||||
function DocView:resolve_screen_position(x, y)
|
||||
if not self.wrapped_settings then return old_resolve_screen_position(self, x, y) end
|
||||
local ox, oy = self:get_line_screen_position(1)
|
||||
local idx = common.clamp(math.floor((y - oy) / self:get_line_height()) + 1, 1, get_total_wrapped_lines(self))
|
||||
return get_line_col_from_index_and_x(self, idx, x - ox)
|
||||
end
|
||||
|
||||
local old_draw_line_text = DocView.draw_line_text
|
||||
function DocView:draw_line_text(line, x, y)
|
||||
if not self.wrapped_settings then return old_draw_line_text(self, line, x, y) end
|
||||
local default_font = self:get_font()
|
||||
local tx, ty, begin_width = x, y + self:get_line_text_y_offset(), self.wrapped_line_offsets[line]
|
||||
local lh = self:get_line_height()
|
||||
local idx, _, count = get_line_idx_col_count(self, line)
|
||||
local total_offset = 1
|
||||
for _, type, text in self.doc.highlighter:each_token(line) do
|
||||
local color = style.syntax[type]
|
||||
local font = style.syntax_fonts[type] or default_font
|
||||
local token_offset = 1
|
||||
-- Split tokens if we're at the end of the document.
|
||||
while text ~= nil and token_offset <= #text do
|
||||
local next_line, next_line_start_col = get_idx_line_col(self, idx + 1)
|
||||
if next_line ~= line then
|
||||
next_line_start_col = #self.doc.lines[line]
|
||||
end
|
||||
local max_length = next_line_start_col - total_offset
|
||||
local rendered_text = text:sub(token_offset, token_offset + max_length - 1)
|
||||
tx = renderer.draw_text(font, rendered_text, tx, ty, color)
|
||||
total_offset = total_offset + #rendered_text
|
||||
if total_offset ~= next_line_start_col or max_length == 0 then break end
|
||||
token_offset = token_offset + #rendered_text
|
||||
idx = idx + 1
|
||||
tx, ty = x + begin_width, ty + lh
|
||||
end
|
||||
end
|
||||
return lh * count
|
||||
end
|
||||
|
||||
local old_draw_line_body = DocView.draw_line_body
|
||||
function DocView:draw_line_body(line, x, y)
|
||||
if not self.wrapped_settings then return old_draw_line_body(self, line, x, y) end
|
||||
local lh = self:get_line_height()
|
||||
local idx0, _, count = get_line_idx_col_count(self, line)
|
||||
for lidx, line1, col1, line2, col2 in self.doc:get_selections(true) do
|
||||
if line >= line1 and line <= line2 then
|
||||
if line1 ~= line then col1 = 1 end
|
||||
if line2 ~= line then col2 = #self.doc.lines[line] + 1 end
|
||||
if col1 ~= col2 then
|
||||
local idx1, ncol1 = get_line_idx_col_count(self, line, col1)
|
||||
local idx2, ncol2 = get_line_idx_col_count(self, line, col2)
|
||||
local start = 0
|
||||
for i = idx1, idx2 do
|
||||
local x1, x2 = x + (idx1 == i and self:get_col_x_offset(line1, col1) or 0)
|
||||
if idx2 == i then
|
||||
x2 = x + self:get_col_x_offset(line, col2)
|
||||
else
|
||||
start = start + get_idx_line_length(self, i, line)
|
||||
x2 = x + self:get_col_x_offset(line, start + 1, true)
|
||||
end
|
||||
renderer.draw_rect(x1, y + (i - idx0) * lh, x2 - x1, lh, style.selection)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
local draw_highlight = nil
|
||||
for lidx, line1, col1, line2, col2 in self.doc:get_selections(true) do
|
||||
-- draw line highlight if caret is on this line
|
||||
if draw_highlight ~= false and config.highlight_current_line
|
||||
and line1 == line and core.active_view == self then
|
||||
draw_highlight = (line1 == line2 and col1 == col2)
|
||||
end
|
||||
end
|
||||
if draw_highlight then
|
||||
for i=1,count do
|
||||
self:draw_line_highlight(x + self.scroll.x, y + lh * (i - 1))
|
||||
end
|
||||
end
|
||||
-- draw line's text
|
||||
return self:draw_line_text(line, x, y)
|
||||
end
|
||||
|
||||
local old_draw = DocView.draw
|
||||
function DocView:draw()
|
||||
old_draw(self)
|
||||
if self.wrapped_settings then
|
||||
LineWrapping.draw_guide(self)
|
||||
end
|
||||
end
|
||||
|
||||
local old_draw_line_gutter = DocView.draw_line_gutter
|
||||
function DocView:draw_line_gutter(line, x, y, width)
|
||||
local lh = self:get_line_height()
|
||||
local _, _, count = get_line_idx_col_count(self, line)
|
||||
return (old_draw_line_gutter(self, line, x, y, width) or lh) * count
|
||||
end
|
||||
|
||||
local old_translate_end_of_line = translate.end_of_line
|
||||
function translate.end_of_line(doc, line, col)
|
||||
if not core.active_view or core.active_view.doc ~= doc or not core.active_view.wrapped_settings then old_translate_end_of_line(doc, line, col) end
|
||||
local idx, ncol = get_line_idx_col_count(core.active_view, line, col)
|
||||
local nline, ncol2 = get_idx_line_col(core.active_view, idx + 1)
|
||||
if nline ~= line then return line, math.huge end
|
||||
return line, ncol2 - 1
|
||||
end
|
||||
|
||||
local old_translate_start_of_line = translate.start_of_line
|
||||
function translate.start_of_line(doc, line, col)
|
||||
if not core.active_view or core.active_view.doc ~= doc or not core.active_view.wrapped_settings then old_translate_start_of_line(doc, line, col) end
|
||||
local idx, ncol = get_line_idx_col_count(core.active_view, line, col)
|
||||
local nline, ncol2 = get_idx_line_col(core.active_view, idx - 1)
|
||||
if nline ~= line then return line, 1 end
|
||||
return line, ncol2 + 1
|
||||
end
|
||||
|
||||
local old_previous_line = DocView.translate.previous_line
|
||||
function DocView.translate.previous_line(doc, line, col, dv)
|
||||
if not dv.wrapped_settings then return old_previous_line(doc, line, col, dv) end
|
||||
local idx, ncol = get_line_idx_col_count(dv, line, col)
|
||||
return get_line_col_from_index_and_x(dv, idx - 1, dv:get_col_x_offset(line, col))
|
||||
end
|
||||
|
||||
local old_next_line = DocView.translate.next_line
|
||||
function DocView.translate.next_line(doc, line, col, dv)
|
||||
if not dv.wrapped_settings then return old_next_line(doc, line, col, dv) end
|
||||
local idx, ncol = get_line_idx_col_count(dv, line, col)
|
||||
return get_line_col_from_index_and_x(dv, idx + 1, dv:get_col_x_offset(line, col))
|
||||
end
|
||||
|
||||
command.add(nil, {
|
||||
["line-wrapping:enable"] = function()
|
||||
if core.active_view and core.active_view.doc then
|
||||
core.active_view.wrapping_enabled = true
|
||||
LineWrapping.update_docview_breaks(core.active_view)
|
||||
end
|
||||
end,
|
||||
["line-wrapping:disable"] = function()
|
||||
if core.active_view and core.active_view.doc then
|
||||
core.active_view.wrapping_enabled = false
|
||||
LineWrapping.reconstruct_breaks(core.active_view, core.active_view:get_font(), math.huge)
|
||||
end
|
||||
end,
|
||||
["line-wrapping:toggle"] = function()
|
||||
if core.active_view and core.active_view.doc and core.active_view.wrapped_settings then
|
||||
command.perform("line-wrapping:disable")
|
||||
else
|
||||
command.perform("line-wrapping:enable")
|
||||
end
|
||||
end
|
||||
})
|
||||
|
||||
keymap.add {
|
||||
["f10"] = "line-wrapping:toggle",
|
||||
}
|
||||
|
||||
return LineWrapping
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
@ -6,16 +6,15 @@ local command = require "core.command"
|
|||
local style = require "core.style"
|
||||
local View = require "core.view"
|
||||
|
||||
---@class plugins.projectsearch.resultsview : 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,122 +217,41 @@ function ResultsView:draw()
|
|||
end
|
||||
|
||||
|
||||
---@param path string
|
||||
---@param text string
|
||||
---@param fn fun(line_text:string):...
|
||||
---@return plugins.projectsearch.resultsview?
|
||||
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)
|
||||
return rv
|
||||
end
|
||||
|
||||
|
||||
local function get_selected_text()
|
||||
local view = core.active_view
|
||||
local doc = (view and view.doc) and view.doc or nil
|
||||
if doc then
|
||||
return doc:get_text(table.unpack({ doc:get_selection() }))
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function normalize_path(path)
|
||||
if not path then return nil end
|
||||
path = common.normalize_path(path)
|
||||
for i, project_dir in ipairs(core.project_directories) do
|
||||
if common.path_belongs_to(path, project_dir.name) then
|
||||
return project_dir.item.filename .. PATHSEP .. common.relative_path(project_dir.name, path)
|
||||
end
|
||||
end
|
||||
return path
|
||||
end
|
||||
|
||||
---@class plugins.projectsearch
|
||||
local projectsearch = {}
|
||||
|
||||
---@type plugins.projectsearch.resultsview
|
||||
projectsearch.ResultsView = ResultsView
|
||||
|
||||
---@param text string
|
||||
---@param path string
|
||||
---@param insensitive? boolean
|
||||
---@return plugins.projectsearch.resultsview?
|
||||
function projectsearch.search_plain(text, path, insensitive)
|
||||
if insensitive then text = text:lower() end
|
||||
return begin_search(path, text, function(line_text)
|
||||
if insensitive then
|
||||
return line_text:lower():find(text, nil, true)
|
||||
else
|
||||
return line_text:find(text, nil, true)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
---@param text string
|
||||
---@param path string
|
||||
---@param insensitive? boolean
|
||||
---@return plugins.projectsearch.resultsview?
|
||||
function projectsearch.search_regex(text, path, insensitive)
|
||||
local re, errmsg
|
||||
if insensitive then
|
||||
re, errmsg = regex.compile(text, "i")
|
||||
else
|
||||
re, errmsg = regex.compile(text)
|
||||
end
|
||||
if not re then core.log("%s", errmsg) return end
|
||||
return begin_search(path, text, function(line_text)
|
||||
return regex.cmatch(re, line_text)
|
||||
end)
|
||||
end
|
||||
|
||||
---@param text string
|
||||
---@param path string
|
||||
---@param insensitive? boolean
|
||||
---@return plugins.projectsearch.resultsview?
|
||||
function projectsearch.search_fuzzy(text, path, insensitive)
|
||||
if insensitive then text = text:lower() end
|
||||
return begin_search(path, text, function(line_text)
|
||||
if insensitive then
|
||||
return common.fuzzy_match(line_text:lower(), text) and 1
|
||||
else
|
||||
return common.fuzzy_match(line_text, text) and 1
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
|
||||
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)
|
||||
projectsearch.search_plain(text, path, true)
|
||||
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)
|
||||
projectsearch.search_regex(text, path, true)
|
||||
end
|
||||
})
|
||||
["project-search:find-regex"] = function()
|
||||
core.command_view:enter("Find Regex In Project", function(text)
|
||||
local re = regex.compile(text, "i")
|
||||
begin_search(text, function(line_text)
|
||||
return regex.cmatch(re, line_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)
|
||||
projectsearch.search_fuzzy(text, path, true)
|
||||
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,
|
||||
})
|
||||
|
||||
|
@ -359,22 +276,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()
|
||||
|
@ -394,6 +311,3 @@ keymap.add {
|
|||
["home"] = "project-search:move-to-start-of-doc",
|
||||
["end"] = "project-search:move-to-end-of-doc"
|
||||
}
|
||||
|
||||
|
||||
return projectsearch
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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%[%](){}`'\"]*"
|
||||
|
||||
|
|
|
@ -1,21 +1,19 @@
|
|||
-- mod-version:3
|
||||
-- mod-version:1 -- lite-xl 1.16
|
||||
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 RootView = require "core.rootview"
|
||||
local CommandView = require "core.commandview"
|
||||
|
||||
config.plugins.scale = common.merge({
|
||||
-- The method used to apply the scaling: "code", "ui"
|
||||
config.plugins.scale = {
|
||||
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_level = 0
|
||||
local scale_steps = 0.05
|
||||
|
||||
local current_scale = SCALE
|
||||
|
@ -25,53 +23,42 @@ local function set_scale(scale)
|
|||
scale = common.clamp(scale, 0.2, 6)
|
||||
|
||||
-- save scroll positions
|
||||
local v_scrolls = {}
|
||||
local h_scrolls = {}
|
||||
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 n > view.size.y then
|
||||
v_scrolls[view] = view.scroll.y / (n - view.size.y)
|
||||
end
|
||||
local hn = view:get_h_scrollable_size()
|
||||
if hn ~= math.huge and hn > view.size.x then
|
||||
h_scrolls[view] = view.scroll.x / (hn - view.size.x)
|
||||
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
|
||||
|
||||
-- we set scale_level in case this was called by user
|
||||
scale_level = (scale - default_scale) / scale_steps
|
||||
|
||||
if config.plugins.scale.mode == "ui" then
|
||||
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.expanded_scrollbar_size = style.expanded_scrollbar_size * s
|
||||
style.caret_width = style.caret_width * s
|
||||
style.tab_width = style.tab_width * s
|
||||
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())
|
||||
renderer.font.set_size(style[name], 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())
|
||||
renderer.font.set_size(style.code_font, s * style.code_font:get_size())
|
||||
end
|
||||
|
||||
-- restore scroll positions
|
||||
for view, n in pairs(v_scrolls) do
|
||||
for view, n in pairs(scrolls) do
|
||||
view.scroll.y = n * (view:get_scrollable_size() - view.size.y)
|
||||
view.scroll.to.y = view.scroll.y
|
||||
end
|
||||
for view, hn in pairs(h_scrolls) do
|
||||
view.scroll.x = hn * (view:get_h_scrollable_size() - view.size.x)
|
||||
view.scroll.to.x = view.scroll.x
|
||||
end
|
||||
|
||||
core.redraw = true
|
||||
end
|
||||
|
@ -80,86 +67,31 @@ local function get_scale()
|
|||
return current_scale
|
||||
end
|
||||
|
||||
local function res_scale()
|
||||
set_scale(default_scale)
|
||||
end
|
||||
local on_mouse_wheel = RootView.on_mouse_wheel
|
||||
|
||||
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)
|
||||
function RootView:on_mouse_wheel(d, ...)
|
||||
if keymap.modkeys["ctrl"] and config.plugins.scale.use_mousewheel then
|
||||
if d < 0 then command.perform "scale:decrease" end
|
||||
if d > 0 then command.perform "scale:increase" end
|
||||
else
|
||||
return on_mouse_wheel(self, d, ...)
|
||||
end
|
||||
end
|
||||
|
||||
-- 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
|
||||
}
|
||||
}
|
||||
local function res_scale()
|
||||
scale_level = 0
|
||||
set_scale(default_scale)
|
||||
end
|
||||
|
||||
local function inc_scale()
|
||||
scale_level = scale_level + 1
|
||||
set_scale(default_scale + scale_level * scale_steps)
|
||||
end
|
||||
|
||||
local function dec_scale()
|
||||
scale_level = scale_level - 1
|
||||
set_scale(default_scale + scale_level * scale_steps)
|
||||
end
|
||||
|
||||
|
||||
command.add(nil, {
|
||||
|
@ -171,16 +103,9 @@ command.add(nil, {
|
|||
keymap.add {
|
||||
["ctrl+0"] = "scale:reset",
|
||||
["ctrl+-"] = "scale:decrease",
|
||||
["ctrl+="] = "scale:increase"
|
||||
["ctrl+="] = "scale:increase",
|
||||
}
|
||||
|
||||
if config.plugins.scale.use_mousewheel then
|
||||
keymap.add {
|
||||
["ctrl+wheelup"] = "scale:increase",
|
||||
["ctrl+wheeldown"] = "scale:decrease"
|
||||
}
|
||||
end
|
||||
|
||||
return {
|
||||
["set"] = set_scale,
|
||||
["get"] = get_scale,
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
@ -39,31 +44,21 @@ end
|
|||
|
||||
function ToolbarView:toggle_visible()
|
||||
self.visible = not self.visible
|
||||
if self.tooltip then
|
||||
core.status_view:remove_tooltip()
|
||||
self.tooltip = false
|
||||
end
|
||||
self.hovered_item = nil
|
||||
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
|
||||
|
@ -71,37 +66,33 @@ 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
|
||||
|
||||
|
||||
function ToolbarView:draw()
|
||||
if not self.visible then return end
|
||||
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)
|
||||
if not self.visible then return end
|
||||
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
|
||||
|
||||
|
||||
function ToolbarView:on_mouse_moved(px, py, ...)
|
||||
if not self.visible then return end
|
||||
ToolbarView.super.on_mouse_moved(self, px, py, ...)
|
||||
self.hovered_item = nil
|
||||
local x_min, x_max, y_min, y_max = self.size.x, 0, self.size.y, 0
|
||||
|
|
|
@ -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"
|
||||
|
@ -8,13 +8,9 @@ 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 default_treeview_size = 200 * SCALE
|
||||
local tooltip_offset = style.font:get_height()
|
||||
local tooltip_border = 1
|
||||
local tooltip_delay = 0.5
|
||||
|
@ -24,7 +20,7 @@ local tooltip_alpha_rate = 1
|
|||
|
||||
local function get_depth(filename)
|
||||
local n = 1
|
||||
for _ in filename:gmatch(PATHSEP) do
|
||||
for sep in filename:gmatch("[\\/]") do
|
||||
n = n + 1
|
||||
end
|
||||
return n
|
||||
|
@ -43,13 +39,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
|
||||
end
|
||||
|
||||
|
||||
|
@ -61,7 +54,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 = {}
|
||||
|
@ -72,7 +65,7 @@ function TreeView:get_cached(dir, item, dirname)
|
|||
-- used only to identify the entry into the cache.
|
||||
local cache_name = item.filename .. (item.topdir and ":" or "")
|
||||
local t = dir_cache[cache_name]
|
||||
if not t or t.type ~= item.type then
|
||||
if not t then
|
||||
t = {}
|
||||
local basename = common.basename(item.filename)
|
||||
if item.topdir then
|
||||
|
@ -83,11 +76,10 @@ function TreeView:get_cached(dir, item, dirname)
|
|||
else
|
||||
t.filename = item.filename
|
||||
t.depth = get_depth(item.filename)
|
||||
t.abs_filename = common.basepath(dirname) .. item.filename
|
||||
t.abs_filename = dirname .. PATHSEP .. item.filename
|
||||
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
|
||||
|
@ -95,7 +87,7 @@ end
|
|||
|
||||
|
||||
function TreeView:get_name()
|
||||
return nil
|
||||
return "Project"
|
||||
end
|
||||
|
||||
|
||||
|
@ -112,13 +104,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
|
||||
|
||||
|
@ -134,56 +131,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
|
||||
|
@ -194,14 +174,8 @@ end
|
|||
|
||||
|
||||
function TreeView:on_mouse_moved(px, py, ...)
|
||||
if not self.visible then return end
|
||||
self.cursor_pos.x = px
|
||||
self.cursor_pos.y = py
|
||||
if TreeView.super.on_mouse_moved(self, px, py, ...) then
|
||||
-- mouse movement handled by the View (scrollbar)
|
||||
self.hovered_item = nil
|
||||
return
|
||||
end
|
||||
TreeView.super.on_mouse_moved(self, px, py, ...)
|
||||
if self.dragging_scrollbar then return end
|
||||
|
||||
local item_changed, tooltip_changed
|
||||
for item, x,y,w,h in self:each_item() do
|
||||
|
@ -223,6 +197,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 or button ~= "left" then
|
||||
return
|
||||
end
|
||||
local hovered_item = self.hovered_item
|
||||
if not hovered_item then
|
||||
return
|
||||
elseif hovered_item.type == "dir" then
|
||||
if keymap.modkeys["ctrl"] and button == "left" then
|
||||
create_directory_in(hovered_item)
|
||||
else
|
||||
if core.project_files_limit and not hovered_item.expanded then
|
||||
local filename, abs_filename = hovered_item.filename, hovered_item.abs_filename
|
||||
local index = 0
|
||||
-- The loop below is used to find the first match starting from the end
|
||||
-- in case there are multiple matches.
|
||||
while index and index + #filename < #abs_filename do
|
||||
index = string.find(abs_filename, filename, index + 1, true)
|
||||
end
|
||||
-- we assume here index is not nil because the abs_filename must contain the
|
||||
-- relative filename
|
||||
local dirname = string.sub(abs_filename, 1, index - 2)
|
||||
if core.is_project_folder(dirname) then
|
||||
core.scan_project_folder(dirname, filename)
|
||||
self:invalidate_cache(dirname)
|
||||
end
|
||||
end
|
||||
hovered_item.expanded = not hovered_item.expanded
|
||||
end
|
||||
else
|
||||
core.try(function()
|
||||
local doc_filename = 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
|
||||
|
@ -230,28 +257,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
|
||||
|
||||
|
@ -280,171 +295,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.
|
||||
|
@ -453,12 +357,12 @@ view.node = node:split("left", view, {x = true}, true)
|
|||
-- plugin module that plug itself in the active node but it is plugged here
|
||||
-- in the treeview node.
|
||||
local toolbar_view = nil
|
||||
local toolbar_plugin, ToolbarView = pcall(require, "plugins.toolbarview")
|
||||
local toolbar_plugin, ToolbarView = core.try(require, "plugins.toolbarview")
|
||||
if config.plugins.toolbarview ~= false and toolbar_plugin then
|
||||
toolbar_view = ToolbarView()
|
||||
view.node:split("down", toolbar_view, {y = true})
|
||||
treeview_node:split("down", toolbar_view, {y = true})
|
||||
local min_toolbar_width = toolbar_view:get_min_width()
|
||||
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()
|
||||
|
@ -499,38 +403,19 @@ function RootView:draw(...)
|
|||
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
|
||||
return common.basename(core.project_dir) == path
|
||||
end
|
||||
|
||||
local function is_primary_project_folder(path)
|
||||
return core.project_dir == path
|
||||
end
|
||||
|
||||
|
||||
local function treeitem() return view.hovered_item or view.selected_item end
|
||||
|
||||
|
||||
menu:register(function() return core.active_view:is(TreeView) and treeitem() end, {
|
||||
menu:register(function() return view.hovered_item end, {
|
||||
{ text = "Open in System", command = "treeview:open-in-system" },
|
||||
ContextMenu.DIVIDER
|
||||
})
|
||||
|
||||
menu:register(
|
||||
function()
|
||||
local item = treeitem()
|
||||
return core.active_view:is(TreeView) and item and not is_project_folder(item.abs_filename)
|
||||
return view.hovered_item
|
||||
and not is_project_folder(view.hovered_item.filename)
|
||||
end,
|
||||
{
|
||||
{ text = "Rename", command = "treeview:rename" },
|
||||
|
@ -540,8 +425,7 @@ menu:register(
|
|||
|
||||
menu:register(
|
||||
function()
|
||||
local item = treeitem()
|
||||
return core.active_view:is(TreeView) and item and item.type == "dir"
|
||||
return view.hovered_item and view.hovered_item.type == "dir"
|
||||
end,
|
||||
{
|
||||
{ text = "New File", command = "treeview:new-file" },
|
||||
|
@ -549,144 +433,60 @@ menu:register(
|
|||
}
|
||||
)
|
||||
|
||||
menu:register(
|
||||
function()
|
||||
local item = treeitem()
|
||||
return core.active_view:is(TreeView) and item
|
||||
and not is_primary_project_folder(item.abs_filename)
|
||||
and is_project_folder(item.abs_filename)
|
||||
end,
|
||||
{
|
||||
{ text = "Remove directory", command = "treeview:remove-project-directory" },
|
||||
}
|
||||
)
|
||||
|
||||
local previous_view = nil
|
||||
|
||||
-- Register the TreeView commands and keymap
|
||||
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
|
||||
["treeview:rename"] = function()
|
||||
local old_filename = view.hovered_item.filename
|
||||
core.command_view:set_text(old_filename)
|
||||
core.command_view:enter("Rename", function(filename)
|
||||
os.rename(old_filename, filename)
|
||||
core.reschedule_project_scan()
|
||||
core.log("Renamed \"%s\" to \"%s\"", old_filename, filename)
|
||||
end, common.path_suggest)
|
||||
end,
|
||||
|
||||
else
|
||||
core.set_active_view(
|
||||
previous_view or core.root_view:get_primary_node().active_view
|
||||
)
|
||||
["treeview:new-file"] = function()
|
||||
local dir_name = view.hovered_item.filename
|
||||
if not is_project_folder(dir_name) then
|
||||
core.command_view:set_text(dir_name .. "/")
|
||||
end
|
||||
end
|
||||
})
|
||||
|
||||
command.add(
|
||||
function()
|
||||
return not menu.show_context_menu and core.active_view:extends(TreeView), TreeView
|
||||
end, {
|
||||
["treeview:next"] = function()
|
||||
local item, _, item_y = view:get_next(view.selected_item)
|
||||
view:set_selection(item, item_y)
|
||||
core.command_view:enter("Filename", function(filename)
|
||||
local doc_filename = core.project_dir .. PATHSEP .. filename
|
||||
local file = io.open(doc_filename, "a+")
|
||||
file:write("")
|
||||
file:close()
|
||||
core.root_view:open_doc(core.open_doc(doc_filename))
|
||||
core.reschedule_project_scan()
|
||||
core.log("Created %s", doc_filename)
|
||||
end, common.path_suggest)
|
||||
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)
|
||||
["treeview:new-folder"] = function()
|
||||
local dir_name = view.hovered_item.filename
|
||||
if not is_project_folder(dir_name) then
|
||||
core.command_view:set_text(dir_name .. "/")
|
||||
end
|
||||
core.command_view:enter("Folder Name", function(filename)
|
||||
local dir_path = core.project_dir .. PATHSEP .. filename
|
||||
common.mkdirp(dir_path)
|
||||
core.reschedule_project_scan()
|
||||
core.log("Created %s", dir_path)
|
||||
end, common.path_suggest)
|
||||
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,
|
||||
})
|
||||
|
||||
|
||||
command.add(
|
||||
function()
|
||||
local item = treeitem()
|
||||
return item ~= nil and (core.active_view == view or menu.show_context_menu), item
|
||||
end, {
|
||||
["treeview:delete"] = function(item)
|
||||
local filename = item.abs_filename
|
||||
local relfilename = item.filename
|
||||
if item.dir_name ~= core.project_dir then
|
||||
-- add secondary project dirs names to the file path to show
|
||||
relfilename = common.basepath(common.basename(item.dir_name)) .. PATHSEP .. relfilename
|
||||
end
|
||||
["treeview:delete"] = function()
|
||||
local filename = view.hovered_item.abs_filename
|
||||
local relfilename = view.hovered_item.filename
|
||||
local file_info = system.get_file_info(filename)
|
||||
local file_type = file_info.type == "dir" and "Directory" or "File"
|
||||
-- Ask before deleting
|
||||
local opt = {
|
||||
{ text = "Yes", default_yes = true },
|
||||
{ text = "No", default_no = true }
|
||||
{ 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),
|
||||
|
@ -710,174 +510,27 @@ command.add(
|
|||
return
|
||||
end
|
||||
end
|
||||
core.reschedule_project_scan()
|
||||
core.log("Deleted \"%s\"", filename)
|
||||
end
|
||||
end
|
||||
)
|
||||
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 = common.basepath(item.dir_name) .. filename
|
||||
end
|
||||
local res, err = os.rename(old_abs_filename, abs_filename)
|
||||
if res then -- successfully renamed
|
||||
for _, doc in ipairs(core.docs) do
|
||||
if doc.abs_filename and old_abs_filename == doc.abs_filename then
|
||||
doc:set_filename(filename, abs_filename) -- make doc point to the new filename
|
||||
doc:reset_syntax()
|
||||
break -- only first needed
|
||||
end
|
||||
end
|
||||
core.log("Renamed \"%s\" to \"%s\"", old_filename, filename)
|
||||
else
|
||||
core.error("Error while renaming \"%s\" to \"%s\": %s", old_abs_filename, abs_filename, err)
|
||||
end
|
||||
end,
|
||||
suggest = function(text)
|
||||
return common.path_suggest(text, item.dir_name)
|
||||
end
|
||||
})
|
||||
end,
|
||||
["treeview:open-in-system"] = function()
|
||||
local hovered_item = view.hovered_item
|
||||
|
||||
["treeview:new-file"] = function(item)
|
||||
local text
|
||||
if not is_project_folder(item.abs_filename) then
|
||||
text = item.filename .. PATHSEP
|
||||
end
|
||||
core.command_view:enter("Filename", {
|
||||
text = text,
|
||||
submit = function(filename)
|
||||
local doc_filename = common.basepath(item.dir_name) .. filename
|
||||
core.log(doc_filename)
|
||||
local file = io.open(doc_filename, "a+")
|
||||
file:write("")
|
||||
file:close()
|
||||
core.root_view:open_doc(core.open_doc(doc_filename))
|
||||
core.log("Created %s", doc_filename)
|
||||
end,
|
||||
suggest = function(text)
|
||||
return common.path_suggest(text, item.dir_name)
|
||||
end
|
||||
})
|
||||
end,
|
||||
|
||||
["treeview:new-folder"] = function(item)
|
||||
local text
|
||||
if not is_project_folder(item.abs_filename) then
|
||||
text = item.filename .. PATHSEP
|
||||
end
|
||||
core.command_view:enter("Folder Name", {
|
||||
text = text,
|
||||
submit = function(filename)
|
||||
local dir_path = common.basepath(item.dir_name) .. filename
|
||||
common.mkdirp(dir_path)
|
||||
core.log("Created %s", dir_path)
|
||||
end,
|
||||
suggest = function(text)
|
||||
return common.path_suggest(text, item.dir_name)
|
||||
end
|
||||
})
|
||||
end,
|
||||
|
||||
["treeview:open-in-system"] = function(item)
|
||||
if PLATFORM == "Windows" then
|
||||
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))
|
||||
system.exec("start " .. hovered_item.abs_filename)
|
||||
elseif string.find(PLATFORM, "Mac") then
|
||||
system.exec(string.format("open %q", hovered_item.abs_filename))
|
||||
elseif PLATFORM == "Linux" then
|
||||
system.exec(string.format("xdg-open %q", hovered_item.abs_filename))
|
||||
end
|
||||
end
|
||||
})
|
||||
|
||||
local projectsearch = pcall(require, "plugins.projectsearch")
|
||||
if projectsearch then
|
||||
menu:register(function()
|
||||
local item = treeitem()
|
||||
return item and item.type == "dir"
|
||||
end, {
|
||||
{ text = "Find in directory", command = "treeview:search-in-directory" }
|
||||
})
|
||||
command.add(function()
|
||||
return view.hovered_item and view.hovered_item.type == "dir"
|
||||
end, {
|
||||
["treeview:search-in-directory"] = function(item)
|
||||
command.perform("project-search:find", view.hovered_item.abs_filename)
|
||||
end
|
||||
})
|
||||
end
|
||||
|
||||
command.add(function()
|
||||
local item = treeitem()
|
||||
return item
|
||||
and not is_primary_project_folder(item.abs_filename)
|
||||
and is_project_folder(item.abs_filename), item
|
||||
end, {
|
||||
["treeview:remove-project-directory"] = function(item)
|
||||
core.remove_project_directory(item.dir_name)
|
||||
end,
|
||||
})
|
||||
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
keymap.add { ["ctrl+\\"] = "treeview:toggle" }
|
||||
|
||||
-- Return the treeview with toolbar and contextmenu to allow
|
||||
-- user or plugin modifications
|
||||
|
|
|
@ -1,53 +1,10 @@
|
|||
-- mod-version:3
|
||||
local common = require "core.common"
|
||||
local config = require "core.config"
|
||||
-- mod-version:1 -- lite-xl 1.16
|
||||
local core = require "core"
|
||||
local command = require "core.command"
|
||||
local Doc = require "core.doc"
|
||||
|
||||
---@class config.plugins.trimwhitespace
|
||||
---@field enabled boolean
|
||||
---@field trim_empty_end_lines boolean
|
||||
config.plugins.trimwhitespace = common.merge({
|
||||
enabled = false,
|
||||
trim_empty_end_lines = false,
|
||||
config_spec = {
|
||||
name = "Trim Whitespace",
|
||||
{
|
||||
label = "Enabled",
|
||||
description = "Disable or enable the trimming of white spaces by default.",
|
||||
path = "enabled",
|
||||
type = "toggle",
|
||||
default = false
|
||||
},
|
||||
{
|
||||
label = "Trim Empty End Lines",
|
||||
description = "Remove any empty new lines at the end of documents.",
|
||||
path = "trim_empty_end_lines",
|
||||
type = "toggle",
|
||||
default = false
|
||||
}
|
||||
}
|
||||
}, config.plugins.trimwhitespace)
|
||||
|
||||
---@class plugins.trimwhitespace
|
||||
local trimwhitespace = {}
|
||||
|
||||
---Disable whitespace trimming for a specific document.
|
||||
---@param doc core.doc
|
||||
function trimwhitespace.disable(doc)
|
||||
doc.disable_trim_whitespace = true
|
||||
end
|
||||
|
||||
---Re-enable whitespace trimming if previously disabled.
|
||||
---@param doc core.doc
|
||||
function trimwhitespace.enable(doc)
|
||||
doc.disable_trim_whitespace = nil
|
||||
end
|
||||
|
||||
---Perform whitespace trimming in all lines of a document except the
|
||||
---line where the caret is currently positioned.
|
||||
---@param doc core.doc
|
||||
function trimwhitespace.trim(doc)
|
||||
local function trim_trailing_whitespace(doc)
|
||||
local cline, ccol = doc:get_selection()
|
||||
for i = 1, #doc.lines do
|
||||
local old_text = doc:get_text(i, 1, i, math.huge)
|
||||
|
@ -65,54 +22,16 @@ function trimwhitespace.trim(doc)
|
|||
end
|
||||
end
|
||||
|
||||
---Removes all empty new lines at the end of the document.
|
||||
---@param doc core.doc
|
||||
---@param raw_remove? boolean Perform the removal not registering to undo stack
|
||||
function trimwhitespace.trim_empty_end_lines(doc, raw_remove)
|
||||
for _=#doc.lines, 1, -1 do
|
||||
local l = #doc.lines
|
||||
if l > 1 and doc.lines[l] == "\n" then
|
||||
local current_line = doc:get_selection()
|
||||
if current_line == l then
|
||||
doc:set_selection(l-1, math.huge, l-1, math.huge)
|
||||
end
|
||||
if not raw_remove then
|
||||
doc:remove(l-1, math.huge, l, math.huge)
|
||||
else
|
||||
table.remove(doc.lines, l)
|
||||
end
|
||||
else
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
command.add("core.docview", {
|
||||
["trim-whitespace:trim-trailing-whitespace"] = function(dv)
|
||||
trimwhitespace.trim(dv.doc)
|
||||
end,
|
||||
|
||||
["trim-whitespace:trim-empty-end-lines"] = function(dv)
|
||||
trimwhitespace.trim_empty_end_lines(dv.doc)
|
||||
["trim-whitespace:trim-trailing-whitespace"] = function()
|
||||
trim_trailing_whitespace(core.active_view.doc)
|
||||
end,
|
||||
})
|
||||
|
||||
|
||||
local doc_save = Doc.save
|
||||
local save = Doc.save
|
||||
Doc.save = function(self, ...)
|
||||
if
|
||||
config.plugins.trimwhitespace.enabled
|
||||
and
|
||||
not self.disable_trim_whitespace
|
||||
then
|
||||
trimwhitespace.trim(self)
|
||||
if config.plugins.trimwhitespace.trim_empty_end_lines then
|
||||
trimwhitespace.trim_empty_end_lines(self)
|
||||
end
|
||||
end
|
||||
doc_save(self, ...)
|
||||
trim_trailing_whitespace(self)
|
||||
save(self, ...)
|
||||
end
|
||||
|
||||
|
||||
return trimwhitespace
|
||||
|
|
|
@ -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"
|
||||
|
@ -7,7 +7,7 @@ local LogView = require "core.logview"
|
|||
|
||||
local function workspace_files_for(project_dir)
|
||||
local basename = common.basename(project_dir)
|
||||
local workspace_dir = common.basepath(USERDIR) .. "ws"
|
||||
local workspace_dir = USERDIR .. PATHSEP .. "ws"
|
||||
local info_wsdir = system.get_file_info(workspace_dir)
|
||||
if not info_wsdir then
|
||||
local ok, err = system.mkdir(workspace_dir)
|
||||
|
@ -22,7 +22,7 @@ local function workspace_files_for(project_dir)
|
|||
if file:sub(1, n) == basename then
|
||||
local id = tonumber(file:sub(n + 1):match("^-(%d+)$"))
|
||||
if id then
|
||||
coroutine.yield(common.basepath(workspace_dir) .. file, id)
|
||||
coroutine.yield(workspace_dir .. PATHSEP .. file, id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -52,7 +52,7 @@ local function get_workspace_filename(project_dir)
|
|||
id = id + 1
|
||||
end
|
||||
local basename = common.basename(project_dir)
|
||||
return common.basepath(USERDIR) .. "ws" .. PATHSEP .. basename .. "-" .. tostring(id)
|
||||
return USERDIR .. PATHSEP .. "ws" .. PATHSEP .. basename .. "-" .. tostring(id)
|
||||
end
|
||||
|
||||
|
||||
|
@ -83,8 +83,7 @@ local function save_view(view)
|
|||
filename = view.doc.filename,
|
||||
selection = { view.doc:get_selection() },
|
||||
scroll = { x = view.scroll.to.x, y = view.scroll.to.y },
|
||||
crlf = view.doc.crlf,
|
||||
text = view.doc.new_file and view.doc:get_text(1, 1, math.huge, math.huge)
|
||||
text = not view.doc.filename and view.doc:get_text(1, 1, math.huge, math.huge)
|
||||
}
|
||||
end
|
||||
if mt == LogView then return end
|
||||
|
@ -93,8 +92,7 @@ local function save_view(view)
|
|||
return {
|
||||
type = "view",
|
||||
active = (core.active_view == view),
|
||||
module = name,
|
||||
scroll = { x = view.scroll.to.x, y = view.scroll.to.y, to = { x = view.scroll.to.x, y = view.scroll.to.y } },
|
||||
module = name
|
||||
}
|
||||
end
|
||||
end
|
||||
|
@ -107,6 +105,7 @@ local function load_view(t)
|
|||
if not t.filename then
|
||||
-- document not associated to a file
|
||||
dv = DocView(core.open_doc())
|
||||
if t.text then dv.doc:insert(1, 1, t.text) end
|
||||
else
|
||||
-- we have a filename, try to read the file
|
||||
local ok, doc = pcall(core.open_doc, t.filename)
|
||||
|
@ -114,13 +113,11 @@ local function load_view(t)
|
|||
dv = DocView(doc)
|
||||
end
|
||||
end
|
||||
-- doc view "dv" can be nil here if the filename associated to the document
|
||||
-- cannot be read.
|
||||
if dv and dv.doc then
|
||||
if dv.doc.new_file and t.text then
|
||||
dv.doc:insert(1, 1, t.text)
|
||||
dv.doc.crlf = t.crlf
|
||||
end
|
||||
dv.doc:set_selection(table.unpack(t.selection))
|
||||
dv.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
|
||||
|
@ -165,9 +162,6 @@ local function load_node(node, t)
|
|||
if t.active_view == i then
|
||||
active_view = view
|
||||
end
|
||||
if not view:is(DocView) then
|
||||
view.scroll = v.scroll
|
||||
end
|
||||
end
|
||||
end
|
||||
if active_view then
|
||||
|
|
|
@ -1,70 +0,0 @@
|
|||
---@meta
|
||||
|
||||
---
|
||||
---Functionality that allows to monitor a directory or file for changes
|
||||
---using the native facilities provided by the current operating system
|
||||
---for better efficiency and performance.
|
||||
---@class dirmonitor
|
||||
dirmonitor = {}
|
||||
|
||||
---@alias dirmonitor.callback fun(fd_or_path:integer|string)
|
||||
|
||||
---
|
||||
---Creates a new dirmonitor object.
|
||||
---
|
||||
---@return dirmonitor
|
||||
function dirmonitor.new() end
|
||||
|
||||
---
|
||||
---Monitors a directory or file for changes.
|
||||
---
|
||||
---In "multiple" mode you will need to call this method more than once to
|
||||
---recursively monitor directories and files.
|
||||
---
|
||||
---In "single" mode you will only need to call this method for the parent
|
||||
---directory and every sub directory and files will get automatically monitored.
|
||||
---
|
||||
---@param path string
|
||||
---
|
||||
---@return integer fd The file descriptor id assigned to the monitored path when
|
||||
---the mode is "multiple", in "single" mode: 1 for success or -1 on failure.
|
||||
function dirmonitor:watch(path) end
|
||||
|
||||
---
|
||||
---Stops monitoring a file descriptor in "multiple" mode
|
||||
---or in "single" mode a directory path.
|
||||
---
|
||||
---@param fd_or_path integer | string A file descriptor or path.
|
||||
function dirmonitor:unwatch(fd_or_path) end
|
||||
|
||||
---
|
||||
---Verify if the resources registered for monitoring have changed, should
|
||||
---be called periodically to check for changes.
|
||||
---
|
||||
---The callback will be called for each file or directory that was:
|
||||
---edited, removed or added. A file descriptor will be passed to the
|
||||
---callback in "multiple" mode or a path in "single" mode.
|
||||
---
|
||||
---If an error occurred during the callback execution, the error callback will be called with the error object.
|
||||
---This callback should not manipulate coroutines to avoid deadlocks.
|
||||
---
|
||||
---@param callback dirmonitor.callback
|
||||
---@param error_callback fun(error: any): nil
|
||||
---
|
||||
---@return boolean? changes True when changes were detected.
|
||||
function dirmonitor:check(callback, error_callback) end
|
||||
|
||||
---
|
||||
---Get the working mode for the current file system monitoring backend.
|
||||
---
|
||||
---"multiple": various file descriptors are needed to recursively monitor a
|
||||
---directory contents, backends: inotify and kqueue.
|
||||
---
|
||||
---"single": a single process takes care of monitoring a path recursively
|
||||
---so no individual file descriptors are used, backends: win32 and fsevents.
|
||||
---
|
||||
---@return "single" | "multiple"
|
||||
function dirmonitor:mode() end
|
||||
|
||||
|
||||
return dirmonitor
|
|
@ -4,13 +4,8 @@
|
|||
---@type table<integer, string>
|
||||
ARGS = {}
|
||||
|
||||
---The current platform tuple used for native modules loading,
|
||||
---for example: "x86_64-linux", "x86_64-darwin", "x86_64-windows", etc...
|
||||
---@type string
|
||||
ARCH = "Architecture-OperatingSystem"
|
||||
|
||||
---The current operating system.
|
||||
---@type string | "Windows" | "Mac OS X" | "Linux" | "iOS" | "Android"
|
||||
---@type string | "'Windows'" | "'Mac OS X'" | "'Linux'" | "'iOS'" | "'Android'"
|
||||
PLATFORM = "Operating System"
|
||||
|
||||
---The current text or ui scale.
|
||||
|
|
|
@ -57,51 +57,48 @@ process.WAIT_INFINITE = -1
|
|||
---@type integer
|
||||
process.WAIT_DEADLINE = -2
|
||||
|
||||
---Default behavior for redirecting streams.
|
||||
---This flag is deprecated and for backwards compatibility with reproc only.
|
||||
---The behavior of this flag may change in future versions of Lite XL.
|
||||
---Used for the process.options stdin, stdout and stderr fields.
|
||||
---@type integer
|
||||
process.REDIRECT_DEFAULT = 0
|
||||
|
||||
---Allow Process API to read this stream via process:read functions.
|
||||
---Used for the process.options stdin, stdout and stderr fields.
|
||||
---@type integer
|
||||
process.REDIRECT_PIPE = 1
|
||||
|
||||
---Redirect this stream to the parent.
|
||||
---Used for the process.options stdin, stdout and stderr fields.
|
||||
---@type integer
|
||||
process.REDIRECT_PARENT = 2
|
||||
|
||||
---Discard this stream (piping it to /dev/null)
|
||||
---Used for the process.options stdin, stdout and stderr fields.
|
||||
---@type integer
|
||||
process.REDIRECT_DISCARD = 3
|
||||
|
||||
---Redirect this stream to stdout.
|
||||
---This flag can only be used on process.options.stderr.
|
||||
---Used for the process.options stdin, stdout and stderr fields.
|
||||
---@type integer
|
||||
process.REDIRECT_STDOUT = 4
|
||||
|
||||
---@alias process.errortype
|
||||
---| `process.ERROR_PIPE`
|
||||
---| `process.ERROR_WOULDBLOCK`
|
||||
---| `process.ERROR_TIMEDOUT`
|
||||
---| `process.ERROR_INVAL`
|
||||
---| `process.ERROR_NOMEM`
|
||||
---|>'process.ERROR_PIPE'
|
||||
---| 'process.ERROR_WOULDBLOCK'
|
||||
---| 'process.ERROR_TIMEDOUT'
|
||||
---| 'process.ERROR_INVAL'
|
||||
---| 'process.ERROR_NOMEM'
|
||||
|
||||
---@alias process.streamtype
|
||||
---| `process.STREAM_STDIN`
|
||||
---| `process.STREAM_STDOUT`
|
||||
---| `process.STREAM_STDERR`
|
||||
---|>'process.STREAM_STDIN'
|
||||
---| 'process.STREAM_STDOUT'
|
||||
---| 'process.STREAM_STDERR'
|
||||
|
||||
---@alias process.waittype
|
||||
---| `process.WAIT_INFINITE`
|
||||
---| `process.WAIT_DEADLINE`
|
||||
---|>'process.WAIT_INFINITE'
|
||||
---| 'process.WAIT_DEADLINE'
|
||||
|
||||
---@alias process.redirecttype
|
||||
---| `process.REDIRECT_DEFAULT`
|
||||
---| `process.REDIRECT_PIPE`
|
||||
---| `process.REDIRECT_PARENT`
|
||||
---| `process.REDIRECT_DISCARD`
|
||||
---| `process.REDIRECT_STDOUT`
|
||||
---|>'process.REDIRECT_DEFAULT'
|
||||
---| 'process.REDIRECT_PIPE'
|
||||
---| 'process.REDIRECT_PARENT'
|
||||
---| 'process.REDIRECT_DISCARD'
|
||||
---| 'process.REDIRECT_STDOUT'
|
||||
|
||||
---
|
||||
--- Options that can be passed to process.start()
|
||||
|
@ -112,6 +109,7 @@ process.REDIRECT_STDOUT = 4
|
|||
---@field public stdout process.redirecttype
|
||||
---@field public stderr process.redirecttype
|
||||
---@field public env table<string, string>
|
||||
process.options = {}
|
||||
|
||||
---
|
||||
---Create and start a new process
|
||||
|
@ -123,7 +121,7 @@ process.REDIRECT_STDOUT = 4
|
|||
---@return process | nil
|
||||
---@return string errmsg
|
||||
---@return process.errortype | integer errcode
|
||||
function process.start(command_and_params, options) end
|
||||
function process:start(command_and_params, options) end
|
||||
|
||||
---
|
||||
---Translates an error code into a useful text message
|
||||
|
@ -232,6 +230,3 @@ function process:returncode() end
|
|||
---
|
||||
---@return boolean
|
||||
function process:running() end
|
||||
|
||||
|
||||
return process
|
||||
|
|
|
@ -31,9 +31,9 @@ regex.NOTEMPTY = 0x00000004
|
|||
regex.NOTEMPTY_ATSTART = 0x00000008
|
||||
|
||||
---@alias regex.modifiers
|
||||
---| "i" # Case insesitive matching
|
||||
---| "m" # Multiline matching
|
||||
---| "s" # Match all characters with dot (.) metacharacter even new lines
|
||||
---|>'"i"' # Case insesitive matching
|
||||
---| '"m"' # Multiline matching
|
||||
---| '"s"' # Match all characters with dot (.) metacharacter even new lines
|
||||
|
||||
---
|
||||
---Compiles a regular expression pattern that can be used to search in strings.
|
||||
|
@ -41,8 +41,8 @@ regex.NOTEMPTY_ATSTART = 0x00000008
|
|||
---@param pattern string
|
||||
---@param options? regex.modifiers A string of one or more pattern modifiers.
|
||||
---
|
||||
---@return regex? regex Ready to use regular expression object or nil on error.
|
||||
---@return string? error The error message if compiling the pattern failed.
|
||||
---@return regex|string regex Ready to use regular expression object or error
|
||||
---message if compiling the pattern failed.
|
||||
function regex.compile(pattern, options) end
|
||||
|
||||
---
|
||||
|
@ -53,42 +53,5 @@ function regex.compile(pattern, options) end
|
|||
---@param options? integer A bit field of matching options, eg:
|
||||
---regex.NOTBOL | regex.NOTEMPTY
|
||||
---
|
||||
---@return integer? ... List of offsets where a match was found.
|
||||
---@return table<integer, integer> list List of offsets where a match was found.
|
||||
function regex:cmatch(subject, offset, options) end
|
||||
|
||||
---
|
||||
---Returns an iterator function that, each time it is called, returns the
|
||||
---next captures from `pattern` over the string subject.
|
||||
---
|
||||
---Example:
|
||||
---```lua
|
||||
--- s = "hello world hello world"
|
||||
--- for hello, world in regex.gmatch("(hello)\\s+(world)", s) do
|
||||
--- print(hello .. " " .. world)
|
||||
--- end
|
||||
---```
|
||||
---
|
||||
---@param pattern string
|
||||
---@param subject string
|
||||
---@param offset? integer
|
||||
---
|
||||
---@return fun():string, ...
|
||||
function regex.gmatch(pattern, subject, offset) end
|
||||
|
||||
---
|
||||
---Replaces the matched pattern globally on the subject with the given
|
||||
---replacement, supports named captures ((?'name'<pattern>), ${name}) and
|
||||
---$[1-9][0-9]* substitutions. Raises an error when failing to compile the
|
||||
---pattern or by a substitution mistake.
|
||||
---
|
||||
---@param pattern regex|string
|
||||
---@param subject string
|
||||
---@param replacement string
|
||||
---@param limit? integer Limits the number of substitutions that will be done.
|
||||
---
|
||||
---@return string? replaced_subject
|
||||
---@return integer? total_replacements
|
||||
function regex.gsub(pattern, subject, replacement, limit) end
|
||||
|
||||
|
||||
return regex
|
||||
|
|
|
@ -6,25 +6,20 @@
|
|||
renderer = {}
|
||||
|
||||
---
|
||||
---Array of bytes that represents a color used by the rendering functions.
|
||||
---Note: indexes for rgba are numerical 1 = r, 2 = g, 3 = b, 4 = a but for
|
||||
---documentation purposes the letters r, g, b, a were used.
|
||||
---Represents a color used by the rendering functions.
|
||||
---@class renderer.color
|
||||
---@field public r number Red
|
||||
---@field public g number Green
|
||||
---@field public b number Blue
|
||||
---@field public a number Alpha
|
||||
renderer.color = {}
|
||||
|
||||
---
|
||||
---Represent options that affect a font's rendering.
|
||||
---@class renderer.fontoptions
|
||||
---@field public antialiasing "none" | "grayscale" | "subpixel"
|
||||
---@field public hinting "slight" | "none" | "full"
|
||||
---@field public bold boolean
|
||||
---@field public italic boolean
|
||||
---@field public underline boolean
|
||||
---@field public smoothing boolean
|
||||
---@field public strikethrough boolean
|
||||
---@field public antialiasing "'grayscale'" | "'subpixel'"
|
||||
---@field public hinting "'slight'" | "'none'" | '"full"'
|
||||
renderer.fontoptions = {}
|
||||
|
||||
---
|
||||
---@class renderer.font
|
||||
|
@ -35,29 +30,18 @@ renderer.font = {}
|
|||
---
|
||||
---@param path string
|
||||
---@param size number
|
||||
---@param options? renderer.fontoptions
|
||||
---@param options renderer.fontoptions
|
||||
---
|
||||
---@return renderer.font
|
||||
function renderer.font.load(path, size, options) end
|
||||
|
||||
---
|
||||
---Combines an array of fonts into a single one for broader charset support,
|
||||
---the order of the list determines the fonts precedence when retrieving
|
||||
---a symbol from it.
|
||||
---
|
||||
---@param fonts renderer.font[]
|
||||
---
|
||||
---@return renderer.font
|
||||
function renderer.font.group(fonts) end
|
||||
|
||||
---
|
||||
---Clones a font object into a new one.
|
||||
---
|
||||
---@param size? number Optional new size for cloned font.
|
||||
---@param options? renderer.fontoptions
|
||||
---
|
||||
---@return renderer.font
|
||||
function renderer.font:copy(size, options) end
|
||||
function renderer.font:copy(size) end
|
||||
|
||||
---
|
||||
---Set the amount of characters that represent a tab.
|
||||
|
@ -74,6 +58,15 @@ function renderer.font:set_tab_size(chars) end
|
|||
---@return number
|
||||
function renderer.font:get_width(text) end
|
||||
|
||||
---
|
||||
---Get the width in subpixels of the given text when
|
||||
---rendered with this font.
|
||||
---
|
||||
---@param text string
|
||||
---
|
||||
---@return number
|
||||
function renderer.font:get_width_subpixel(text) end
|
||||
|
||||
---
|
||||
---Get the height in pixels that occupies a single character
|
||||
---when rendered with this font.
|
||||
|
@ -81,6 +74,12 @@ function renderer.font:get_width(text) end
|
|||
---@return number
|
||||
function renderer.font:get_height() end
|
||||
|
||||
---
|
||||
---Gets the font subpixel scale.
|
||||
---
|
||||
---@return number
|
||||
function renderer.font:subpixel_scale() end
|
||||
|
||||
---
|
||||
---Get the current size of the font.
|
||||
---
|
||||
|
@ -94,11 +93,23 @@ function renderer.font:get_size() end
|
|||
function renderer.font:set_size(size) end
|
||||
|
||||
---
|
||||
---Get the current path of the font as a string if a single font or as an
|
||||
---array of strings if a group font.
|
||||
---Assistive functionality to replace characters in a
|
||||
---rendered text with other characters.
|
||||
---@class renderer.replacements
|
||||
renderer.replacements = {}
|
||||
|
||||
---
|
||||
---@return string | table<integer, string>
|
||||
function renderer.font:get_path() end
|
||||
---Create a new character replacements object.
|
||||
---
|
||||
---@return renderer.replacements
|
||||
function renderer.replacements.new() end
|
||||
|
||||
---
|
||||
---Add to internal map a character to character replacement.
|
||||
---
|
||||
---@param original_char string Should be a single character like '\t'
|
||||
---@param replacement_char string Should be a single character like '»'
|
||||
function renderer.replacements:add(original_char, replacement_char) end
|
||||
|
||||
---
|
||||
---Toggles drawing debugging rectangles on the currently rendered sections
|
||||
|
@ -142,16 +153,29 @@ function renderer.set_clip_rect(x, y, width, height) end
|
|||
function renderer.draw_rect(x, y, width, height, color) end
|
||||
|
||||
---
|
||||
---Draw text and return the x coordinate where the text finished drawing.
|
||||
---Draw text.
|
||||
---
|
||||
---@param font renderer.font
|
||||
---@param text string
|
||||
---@param x number
|
||||
---@param y number
|
||||
---@param color renderer.color
|
||||
---@param replace renderer.replacements
|
||||
---@param color_replace renderer.color
|
||||
---
|
||||
---@return number x
|
||||
function renderer.draw_text(font, text, x, y, color) end
|
||||
---@return number x_subpixel
|
||||
function renderer.draw_text(font, text, x, y, color, replace, color_replace) end
|
||||
|
||||
|
||||
return renderer
|
||||
---
|
||||
---Draw text at subpixel level.
|
||||
---
|
||||
---@param font renderer.font
|
||||
---@param text string
|
||||
---@param x number
|
||||
---@param y number
|
||||
---@param color renderer.color
|
||||
---@param replace renderer.replacements
|
||||
---@param color_replace renderer.color
|
||||
---
|
||||
---@return number x_subpixel
|
||||
function renderer.draw_text_subpixel(font, text, x, y, color, replace, color_replace) end
|
||||
|
|
|
@ -1,165 +0,0 @@
|
|||
---@meta
|
||||
|
||||
---UTF-8 equivalent of string.byte
|
||||
---@param s string
|
||||
---@param i? integer
|
||||
---@param j? integer
|
||||
---@return integer
|
||||
---@return ...
|
||||
function string.ubyte(s, i, j) end
|
||||
|
||||
---UTF-8 equivalent of string.char
|
||||
---@param byte integer
|
||||
---@param ... integer
|
||||
---@return string
|
||||
---@return ...
|
||||
function string.uchar(byte, ...) end
|
||||
|
||||
---UTF-8 equivalent of string.find
|
||||
---@param s string
|
||||
---@param pattern string
|
||||
---@param init? integer
|
||||
---@param plain? boolean
|
||||
---@return integer start
|
||||
---@return integer end
|
||||
---@return ... captured
|
||||
function string.ufind(s, pattern, init, plain) end
|
||||
|
||||
---UTF-8 equivalent of string.gmatch
|
||||
---@param s string
|
||||
---@param pattern string
|
||||
---@param init? integer
|
||||
---@return fun():string, ...
|
||||
function string.ugmatch(s, pattern, init) end
|
||||
|
||||
---UTF-8 equivalent of string.gsub
|
||||
---@param s string
|
||||
---@param pattern string
|
||||
---@param repl string|table|function
|
||||
---@param n integer
|
||||
---@return string
|
||||
---@return integer count
|
||||
function string.ugsub(s, pattern, repl, n) end
|
||||
|
||||
---UTF-8 equivalent of string.len
|
||||
---@param s string
|
||||
---@return integer
|
||||
function string.ulen(s) end
|
||||
|
||||
---UTF-8 equivalent of string.lower
|
||||
---@param s string
|
||||
---@return string
|
||||
function string.ulower(s) end
|
||||
|
||||
---UTF-8 equivalent of string.match
|
||||
---@param s string
|
||||
---@param pattern string
|
||||
---@param init? integer
|
||||
---@return string | number captured
|
||||
function string.umatch(s, pattern, init) end
|
||||
|
||||
---UTF-8 equivalent of string.reverse
|
||||
---@param s string
|
||||
---@return string
|
||||
function string.ureverse(s) end
|
||||
|
||||
---UTF-8 equivalent of string.sub
|
||||
---@param s string
|
||||
---@param i integer
|
||||
---@param j? integer
|
||||
---@return string
|
||||
function string.usub(s, i, j) end
|
||||
|
||||
---UTF-8 equivalent of string.upper
|
||||
---@param s string
|
||||
---@return string
|
||||
function string.uupper(s) end
|
||||
|
||||
---Equivalent to utf8.escape()
|
||||
---@param s string
|
||||
---@return string utf8_string
|
||||
function string.uescape(s) end
|
||||
|
||||
|
||||
---Equivalent to utf8.charpos()
|
||||
---@param s string
|
||||
---@param charpos? integer
|
||||
---@param index? integer
|
||||
---@return integer charpos
|
||||
---@return integer codepoint
|
||||
function string.ucharpos(s, charpos, index) end
|
||||
|
||||
---Equivalent to utf8.next()
|
||||
---@param s string
|
||||
---@param charpos? integer
|
||||
---@param index? integer
|
||||
---@return integer charpos
|
||||
---@return integer codepoint
|
||||
function string.unext(s, charpos, index) end
|
||||
|
||||
---Equivalent to utf8.insert()
|
||||
---@param s string
|
||||
---@param idx? integer
|
||||
---@param substring string
|
||||
---@return string new_string
|
||||
function string.uinsert(s, idx, substring) end
|
||||
|
||||
---Equivalent to utf8.remove()
|
||||
---@param s string
|
||||
---@param start? integer
|
||||
---@param stop? integer
|
||||
---@return string new_string
|
||||
function string.uremove(s, start, stop) end
|
||||
|
||||
---Equivalent to utf8.width()
|
||||
---@param s string
|
||||
---@param ambi_is_double? boolean
|
||||
---@param default_width? integer
|
||||
---@return integer width
|
||||
function string.uwidth(s, ambi_is_double, default_width) end
|
||||
|
||||
---Equivalent to utf8.widthindex()
|
||||
---@param s string
|
||||
---@param location integer
|
||||
---@param ambi_is_double? boolean
|
||||
---@param default_width? integer
|
||||
---@return integer idx
|
||||
---@return integer offset
|
||||
---@return integer width
|
||||
function string.uwidthindex(s, location, ambi_is_double, default_width) end
|
||||
|
||||
---Equivalent to utf8.title()
|
||||
---@param s string
|
||||
---@return string new_string
|
||||
function string.utitle(s) end
|
||||
|
||||
---Equivalent to utf8.fold()
|
||||
---@param s string
|
||||
---@return string new_string
|
||||
function string.ufold(s) end
|
||||
|
||||
---Equivalent to utf8.ncasecmp()
|
||||
---@param a string
|
||||
---@param b string
|
||||
---@return integer result
|
||||
function string.uncasecmp(a, b) end
|
||||
|
||||
---Equivalent to utf8.offset()
|
||||
---@param s string
|
||||
---@param n integer
|
||||
---@param i? integer
|
||||
---@return integer position_in_bytes
|
||||
function string.uoffset(s, n, i) end
|
||||
|
||||
---Equivalent to utf8.codepoint()
|
||||
---@param s string
|
||||
---@param i? integer
|
||||
---@param j? integer
|
||||
---@return integer code
|
||||
---@return ...
|
||||
function string.ucodepoint(s, i, j) end
|
||||
|
||||
---Equivalent to utf8.codes()
|
||||
---@param s string
|
||||
---@return fun():integer, integer
|
||||
function string.ucodes(s) end
|
|
@ -6,15 +6,15 @@
|
|||
system = {}
|
||||
|
||||
---@alias system.fileinfotype
|
||||
---| "file" # It is a file.
|
||||
---| "dir" # It is a directory.
|
||||
---|>'"file"' # It is a file.
|
||||
---| '"dir"' # It is a directory.
|
||||
|
||||
---
|
||||
---@class system.fileinfo
|
||||
---@field public modified number A timestamp in seconds.
|
||||
---@field public size number Size in bytes.
|
||||
---@field public type system.fileinfotype Type of file
|
||||
---@field public symlink boolean The directory is a symlink. This field is only set on Linux and on directories.
|
||||
system.fileinfo = {}
|
||||
|
||||
---
|
||||
---Core function used to retrieve the current event been triggered by SDL.
|
||||
|
@ -24,7 +24,7 @@ system = {}
|
|||
---
|
||||
---Window events:
|
||||
--- * "quit"
|
||||
--- * "resized" -> width, height (in points)
|
||||
--- * "resized" -> width, height
|
||||
--- * "exposed"
|
||||
--- * "minimized"
|
||||
--- * "maximized"
|
||||
|
@ -38,18 +38,12 @@ system = {}
|
|||
--- * "keypressed" -> key_name
|
||||
--- * "keyreleased" -> key_name
|
||||
--- * "textinput" -> text
|
||||
--- * "textediting" -> text, start, length
|
||||
---
|
||||
---Mouse events:
|
||||
--- * "mousepressed" -> button_name, x, y, amount_of_clicks
|
||||
--- * "mousereleased" -> button_name, x, y
|
||||
--- * "mousemoved" -> x, y, relative_x, relative_y
|
||||
--- * "mousewheel" -> y, x
|
||||
---
|
||||
---Touch events:
|
||||
--- * "touchpressed" -> x, y, finger_id
|
||||
--- * "touchreleased" -> x, y, finger_id
|
||||
--- * "touchmoved" -> x, y, distance_x, distance_y, finger_id
|
||||
--- * "mousewheel" -> y
|
||||
---
|
||||
---@return string type
|
||||
---@return any? arg1
|
||||
|
@ -61,16 +55,16 @@ function system.poll_event() end
|
|||
---
|
||||
---Wait until an event is triggered.
|
||||
---
|
||||
---@param timeout? number Amount of seconds, also supports fractions
|
||||
---of a second, eg: 0.01. If not provided, waits forever.
|
||||
---@param timeout number Amount of seconds, also supports fractions
|
||||
---of a second, eg: 0.01
|
||||
---
|
||||
---@return boolean status True on success or false if there was an error or if no event was received.
|
||||
---@return boolean status True on success or false if there was an error.
|
||||
function system.wait_event(timeout) end
|
||||
|
||||
---
|
||||
---Change the cursor type displayed on screen.
|
||||
---
|
||||
---@param type string | "arrow" | "ibeam" | "sizeh" | "sizev" | "hand"
|
||||
---@param type string | "'arrow'" | "'ibeam'" | "'sizeh'" | "'sizev'" | "'hand'"
|
||||
function system.set_cursor(type) end
|
||||
|
||||
---
|
||||
|
@ -80,10 +74,10 @@ function system.set_cursor(type) end
|
|||
function system.set_window_title(title) end
|
||||
|
||||
---@alias system.windowmode
|
||||
---| "normal"
|
||||
---| "minimized"
|
||||
---| "maximized"
|
||||
---| "fullscreen"
|
||||
---|>'"normal"'
|
||||
---| '"minimized"'
|
||||
---| '"maximized"'
|
||||
---| '"fullscreen"'
|
||||
|
||||
---
|
||||
---Change the window mode.
|
||||
|
@ -107,12 +101,10 @@ function system.set_window_bordered(bordered) end
|
|||
---When then window is run borderless (without system decorations), this
|
||||
---function allows to set the size of the different regions that allow
|
||||
---for custom window management.
|
||||
---To disable custom window management, call this function without any
|
||||
---arguments
|
||||
---
|
||||
---@param title_height? number Height of the window decoration
|
||||
---@param controls_width? number Width of window controls (maximize,minimize and close buttons, etc).
|
||||
---@param resize_border? number The amount of pixels reserved for resizing
|
||||
---@param title_height number
|
||||
---@param controls_width number This is for minimize, maximize, close, etc...
|
||||
---@param resize_border number The amount of pixels reserved for resizing
|
||||
function system.set_window_hit_test(title_height, controls_width, resize_border) end
|
||||
|
||||
---
|
||||
|
@ -139,30 +131,6 @@ function system.set_window_size(width, height, x, y) end
|
|||
---@return boolean
|
||||
function system.window_has_focus() end
|
||||
|
||||
---
|
||||
---Gets the mode of the window.
|
||||
---
|
||||
---@return system.windowmode
|
||||
function system.get_window_mode() end
|
||||
|
||||
---
|
||||
---Sets the position of the IME composition window.
|
||||
---
|
||||
---@param x number
|
||||
---@param y number
|
||||
---@param width number
|
||||
---@param height number
|
||||
function system.set_text_input_rect(x, y, width, height) end
|
||||
|
||||
---
|
||||
---Clears any ongoing composition on the IME
|
||||
function system.clear_ime() end
|
||||
|
||||
---
|
||||
---Raise the main window and give it input focus.
|
||||
---Note: may not always be obeyed by the users window manager.
|
||||
function system.raise_window() end
|
||||
|
||||
---
|
||||
---Opens a message box to display an error message.
|
||||
---
|
||||
|
@ -170,14 +138,6 @@ function system.raise_window() end
|
|||
---@param message string
|
||||
function system.show_fatal_error(title, message) end
|
||||
|
||||
---
|
||||
---Deletes an empty directory.
|
||||
---
|
||||
---@param path string
|
||||
---@return boolean success True if the operation suceeded, false otherwise
|
||||
---@return string? message An error message if the operation failed
|
||||
function system.rmdir(path) end
|
||||
|
||||
---
|
||||
---Change the current directory path which affects relative file operations.
|
||||
---This function raises an error if the path doesn't exists.
|
||||
|
@ -192,7 +152,6 @@ function system.chdir(path) end
|
|||
---@param directory_path string
|
||||
---
|
||||
---@return boolean created True on success or false on failure.
|
||||
---@return string? message The error message if the operation failed.
|
||||
function system.mkdir(directory_path) end
|
||||
|
||||
---
|
||||
|
@ -209,7 +168,7 @@ function system.list_dir(path) end
|
|||
---
|
||||
---@param path string
|
||||
---
|
||||
---@return string? abspath
|
||||
---@return string
|
||||
function system.absolute_path(path) end
|
||||
|
||||
---
|
||||
|
@ -221,28 +180,6 @@ function system.absolute_path(path) end
|
|||
---@return string? message Error message in case of error.
|
||||
function system.get_file_info(path) end
|
||||
|
||||
|
||||
---@alias system.fstype
|
||||
---| "ext2/ext3"
|
||||
---| "nfs"
|
||||
---| "fuse"
|
||||
---| "smb"
|
||||
---| "smb2"
|
||||
---| "reiserfs"
|
||||
---| "tmpfs"
|
||||
---| "ramfs"
|
||||
---| "ntfs"
|
||||
|
||||
---
|
||||
---Gets the filesystem type of a path.
|
||||
---Note: This only works on Linux.
|
||||
---
|
||||
---@param path string Can be path to a directory or a file
|
||||
---
|
||||
---@return system.fstype
|
||||
function system.get_fs_type(path) end
|
||||
|
||||
|
||||
---
|
||||
---Retrieve the text currently stored on the clipboard.
|
||||
---
|
||||
|
@ -255,12 +192,6 @@ function system.get_clipboard() end
|
|||
---@param text string
|
||||
function system.set_clipboard(text) end
|
||||
|
||||
---
|
||||
---Get the process id of lite-xl itself.
|
||||
---
|
||||
---@return integer
|
||||
function system.get_process_id() end
|
||||
|
||||
---
|
||||
---Get amount of iterations since the application was launched
|
||||
---also known as SDL_GetPerformanceCounter() / SDL_GetPerformanceFrequency()
|
||||
|
@ -278,9 +209,7 @@ function system.sleep(seconds) end
|
|||
---Similar to os.execute() but does not return the exit status of the
|
||||
---executed command and executes the process in a non blocking way by
|
||||
---forking it to the background.
|
||||
---Note: Do not use this function, use the Process API instead.
|
||||
---
|
||||
---@deprecated
|
||||
---@param command string The command to execute.
|
||||
function system.exec(command) end
|
||||
|
||||
|
@ -302,27 +231,4 @@ function system.fuzzy_match(haystack, needle, file) end
|
|||
---
|
||||
---@param opacity number A value from 0.0 to 1.0, the lower the value
|
||||
---the less visible the window will be.
|
||||
---@return boolean success True if the operation suceeded.
|
||||
function system.set_window_opacity(opacity) end
|
||||
|
||||
---
|
||||
---Loads a lua native module using the default Lua API or lite-xl native plugin API.
|
||||
---Note: Never use this function directly.
|
||||
---
|
||||
---@param name string the name of the module
|
||||
---@param path string the path to the shared library file
|
||||
---@return number nargs the return value of the entrypoint
|
||||
function system.load_native_plugin(name, path) end
|
||||
|
||||
---
|
||||
---Compares two paths in the order used by TreeView.
|
||||
---
|
||||
---@param path1 string
|
||||
---@param type1 system.fileinfotype
|
||||
---@param path2 string
|
||||
---@param type2 system.fileinfotype
|
||||
---@return boolean compare_result True if path1 < path2
|
||||
function system.path_compare(path1, type1, path2, type2) end
|
||||
|
||||
|
||||
return system
|
||||
|
|
|
@ -1,194 +0,0 @@
|
|||
---@meta
|
||||
|
||||
---Additional utf8 support not provided by lua.
|
||||
---@class utf8extra
|
||||
utf8extra = {}
|
||||
|
||||
---UTF-8 equivalent of string.byte
|
||||
---@param s string
|
||||
---@param i? integer
|
||||
---@param j? integer
|
||||
---@return integer
|
||||
---@return ...
|
||||
function utf8extra.byte(s, i, j) end
|
||||
|
||||
---UTF-8 equivalent of string.char
|
||||
---@param byte integer
|
||||
---@param ... integer
|
||||
---@return string
|
||||
---@return ...
|
||||
function utf8extra.char(byte, ...) end
|
||||
|
||||
---UTF-8 equivalent of string.find
|
||||
---@param s string
|
||||
---@param pattern string
|
||||
---@param init? integer
|
||||
---@param plain? boolean
|
||||
---@return integer start
|
||||
---@return integer end
|
||||
---@return ... captured
|
||||
function utf8extra.find(s, pattern, init, plain) end
|
||||
|
||||
---UTF-8 equivalent of string.gmatch
|
||||
---@param s string
|
||||
---@param pattern string
|
||||
---@param init? integer
|
||||
---@return fun():string, ...
|
||||
function utf8extra.gmatch(s, pattern, init) end
|
||||
|
||||
---UTF-8 equivalent of string.gsub
|
||||
---@param s string
|
||||
---@param pattern string
|
||||
---@param repl string|table|function
|
||||
---@param n integer
|
||||
---@return string
|
||||
---@return integer count
|
||||
function utf8extra.gsub(s, pattern, repl, n) end
|
||||
|
||||
---UTF-8 equivalent of string.len
|
||||
---@param s string
|
||||
---@return integer
|
||||
function utf8extra.len(s) end
|
||||
|
||||
---UTF-8 equivalent of string.lower
|
||||
---@param s string
|
||||
---@return string
|
||||
function utf8extra.lower(s) end
|
||||
|
||||
---UTF-8 equivalent of string.match
|
||||
---@param s string
|
||||
---@param pattern string
|
||||
---@param init? integer
|
||||
---@return string | number captured
|
||||
function utf8extra.match(s, pattern, init) end
|
||||
|
||||
---UTF-8 equivalent of string.reverse
|
||||
---@param s string
|
||||
---@return string
|
||||
function utf8extra.reverse(s) end
|
||||
|
||||
---UTF-8 equivalent of string.sub
|
||||
---@param s string
|
||||
---@param i integer
|
||||
---@param j? integer
|
||||
---@return string
|
||||
function utf8extra.sub(s, i, j) end
|
||||
|
||||
---UTF-8 equivalent of string.upper
|
||||
---@param s string
|
||||
---@return string
|
||||
function utf8extra.upper(s) end
|
||||
|
||||
---Escape a str to UTF-8 format string. It support several escape format:
|
||||
---* %ddd - which ddd is a decimal number at any length: change Unicode code point to UTF-8 format.
|
||||
---* %{ddd} - same as %nnn but has bracket around.
|
||||
---* %uddd - same as %ddd, u stands Unicode
|
||||
---* %u{ddd} - same as %{ddd}
|
||||
---* %xhhh - hexadigit version of %ddd
|
||||
---* %x{hhh} same as %xhhh.
|
||||
---* %? - '?' stands for any other character: escape this character.
|
||||
---Example:
|
||||
---```lua
|
||||
---local u = utf8.escape
|
||||
---print(u"%123%u123%{123}%u{123}%xABC%x{ABC}")
|
||||
---print(u"%%123%?%d%%u")
|
||||
---```
|
||||
---@param s string
|
||||
---@return string utf8_string
|
||||
function utf8extra.escape(s) end
|
||||
|
||||
---Convert UTF-8 position to byte offset. if only index is given, return byte
|
||||
---offset of this UTF-8 char index. if both charpos and index is given, a new
|
||||
---charpos will be calculated, by add/subtract UTF-8 char index to current
|
||||
---charpos. in all cases, it returns a new char position, and code point
|
||||
---(a number) at this position.
|
||||
---@param s string
|
||||
---@param charpos? integer
|
||||
---@param index? integer
|
||||
---@return integer charpos
|
||||
---@return integer codepoint
|
||||
function utf8extra.charpos(s, charpos, index) end
|
||||
|
||||
---Iterate though the UTF-8 string s. If only s is given, it can used as a iterator:
|
||||
---```lua
|
||||
--- for pos, code in utf8.next, "utf8-string" do
|
||||
--- -- ...
|
||||
--- end
|
||||
---````
|
||||
---If only charpos is given, return the next byte offset of in string. if
|
||||
---charpos and index is given, a new charpos will be calculated, by add/subtract
|
||||
---UTF-8 char offset to current charpos. in all case, it return a new char
|
||||
---position (in bytes), and code point (a number) at this position.
|
||||
---@param s string
|
||||
---@param charpos? integer
|
||||
---@param index? integer
|
||||
---@return integer charpos
|
||||
---@return integer codepoint
|
||||
function utf8extra.next(s, charpos, index) end
|
||||
|
||||
---Insert a substring to s. If idx is given, insert substring before char at
|
||||
---this index, otherwise substring will concat to s. idx can be negative.
|
||||
---@param s string
|
||||
---@param idx? integer
|
||||
---@param substring string
|
||||
---@return string new_string
|
||||
function utf8extra.insert(s, idx, substring) end
|
||||
|
||||
---Delete a substring in s. If neither start nor stop is given, delete the last
|
||||
---UTF-8 char in s, otherwise delete char from start to end of s. if stop is
|
||||
---given, delete char from start to stop (include start and stop). start and
|
||||
---stop can be negative.
|
||||
---@param s string
|
||||
---@param start? integer
|
||||
---@param stop? integer
|
||||
---@return string new_string
|
||||
function utf8extra.remove(s, start, stop) end
|
||||
|
||||
---Calculate the width of UTF-8 string s. if ambi_is_double is given, the
|
||||
---ambiguous width character's width is 2, otherwise it's 1. fullwidth/doublewidth
|
||||
---character's width is 2, and other character's width is 1. if default_width is
|
||||
---given, it will be the width of unprintable character, used display a
|
||||
---non-character mark for these characters. if s is a code point, return the
|
||||
---width of this code point.
|
||||
---@param s string
|
||||
---@param ambi_is_double? boolean
|
||||
---@param default_width? integer
|
||||
---@return integer width
|
||||
function utf8extra.width(s, ambi_is_double, default_width) end
|
||||
|
||||
---Return the character index at given location in string s. this is a reverse
|
||||
---operation of utf8.width(). this function returns a index of location, and a
|
||||
---offset in UTF-8 encoding. e.g. if cursor is at the second column (middle)
|
||||
---of the wide char, offset will be 2. the width of character at idx is
|
||||
---returned, also.
|
||||
---@param s string
|
||||
---@param location integer
|
||||
---@param ambi_is_double? boolean
|
||||
---@param default_width? integer
|
||||
---@return integer idx
|
||||
---@return integer offset
|
||||
---@return integer width
|
||||
function utf8extra.widthindex(s, location, ambi_is_double, default_width) end
|
||||
|
||||
---Convert UTF-8 string s to title-case, used to compare by ignore case. if s
|
||||
---is a number, it's treat as a code point and return a convert code point
|
||||
---(number). utf8.lower/utf8.pper has the same extension.
|
||||
---@param s string
|
||||
---@return string new_string
|
||||
function utf8extra.title(s) end
|
||||
|
||||
---Convert UTF-8 string s to folded case, used to compare by ignore case. if s
|
||||
---is a number, it's treat as a code point and return a convert code point
|
||||
---(number). utf8.lower/utf8.pper has the same extension.
|
||||
---@param s string
|
||||
---@return string new_string
|
||||
function utf8extra.fold(s) end
|
||||
|
||||
---Compare a and b without case, -1 means a < b, 0 means a == b and 1 means a > b.
|
||||
---@param a string
|
||||
---@param b string
|
||||
---@return integer result
|
||||
function utf8extra.ncasecmp(a, b) end
|
||||
|
||||
|
||||
return utf8extra
|
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue